From 4a6034e1272d860333bafa8f6bdf21e0aed9c4ba Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Tue, 17 Dec 2024 08:23:01 +0000 Subject: [PATCH] Added tools for building source generators based on Corvus.JsonSchema (#513) * Added tools library for people building source generators based on Corvus.JsonSchema * Added self-contained version of CJEx. * Removed package dependency on Community.HighPerformance. Added README Added Corvus.Json.JsonSchema libraries as direct inclusions. * Fixed nuget packaging. --- .../TypeDeclarationExtensions.cs | 4 + .../BinaryJsonNumber.NetStandard.cs | 1 - .../Corvus.Json/Internal/JsonReaderHelper.cs | 1 - .../Corvus.Json/Internal/LowAllocJsonUtils.cs | 40 +- .../Corvus.Json/JsonElementExtensions.cs | 2 +- .../Corvus.Json/ValidationContext.cs | 1 - .../Buffers/ArrayPoolBufferWriter.cs | 383 ++++++ .../Buffers/Enums/AllocationMode.cs | 21 + .../Buffers/Interfaces/IBuffer{T}.cs | 49 + .../Buffers/MemoryBufferWriter{T}.cs | 189 +++ .../Buffers/MemoryDebugView{T}.cs | 57 + .../Buffers/MemoryOwner{T}.cs | 338 ++++++ .../Buffers/SpanOwner{T}.cs | 201 ++++ .../Enumerables/SpanEnumerable{T}.cs | 180 +++ .../Enumerables/SpanTokenizer{T}.cs | 105 ++ .../Extensions/ArrayExtensions.1D.cs | 178 +++ .../Extensions/ArrayPoolExtensions.cs | 97 ++ .../Extensions/BoolExtensions.cs | 75 ++ .../Helpers/Internals/BitOperations.cs | 40 + .../Helpers/Internals/RuntimeHelpers.cs | 280 +++++ .../Helpers/Internals/SpanHelper.Count.cs | 387 ++++++ .../Helpers/ObjectMarshal.cs | 141 +++ ...alueStringBuilder.AppendSpanFormattable.cs | 31 + .../ValueStringBuilder.Replace.cs | 165 +++ .../ValueStringBuilder.cs | 413 +++++++ .../Corvus.Json.SourceGeneratorTools.csproj | 151 +++ .../Corvus.UriTemplates/IUriTemplateParser.cs | 192 +++ .../Internal/StringBuilderPool.cs | 19 + .../Corvus.UriTemplates/MatchWithVerb.cs | 26 + .../MatchWithVerb{TMatch}.cs | 102 ++ .../ParameterByNameAndRangeCache.cs | 237 ++++ .../Corvus.UriTemplates/ParameterCache.cs | 206 ++++ .../Corvus.UriTemplates/ParameterValue.cs | 34 + .../ReadOnlySpanCallback.cs | 16 + .../DictionaryTemplateParameterProvider.cs | 292 +++++ .../TavisApi/DictionaryUriTemplateResolver.cs | 87 ++ .../TavisApi/QueryStringParameterOrder.cs | 24 + .../TavisApi/TemplateMatch.cs | 26 + .../TavisApi/UriExtensions.cs | 190 +++ .../TavisApi/UriTemplate.cs | 305 +++++ .../TavisApi/UriTemplateExtensions.cs | 89 ++ .../TavisApi/UriTemplateTable.cs | 66 + .../TemplateMatchResult.cs | 43 + .../ITemplateParameterProvider.cs | 30 + .../OperatorInfo.cs | 60 + .../TemplateParameterProvider.cs | 118 ++ .../VariableProcessingState.cs | 27 + .../VariableSpecification.cs | 149 +++ .../UriTemplateAndVerbTable.cs | 22 + .../UriTemplateAndVerbTable{TMatch}.cs | 164 +++ .../UriTemplateParameters.cs | 86 ++ .../UriTemplateParserExtensions.cs | 135 +++ .../UriTemplateParserFactory.cs | 1072 +++++++++++++++++ .../UriTemplateRegexBuilder.cs | 182 +++ ...r{TParameterProvider,TParameterPayload}.cs | 353 ++++++ .../Corvus.UriTemplates/UriTemplateTable.cs | 32 + .../UriTemplateTable{TMatch}.cs | 164 +++ .../IndexRange/Index.cs | 157 +++ .../IndexRange/Range.cs | 100 ++ .../Metaschema.cs | 126 ++ .../README.md | 75 ++ .../SourceGeneratorHelpers.cs | 358 ++++++ .../corvus/meta/corvus-extensions.json | 11 + .../metaschema/corvus/schema.json | 60 + .../draft2019-09/meta/applicator.json | 53 + .../metaschema/draft2019-09/meta/content.json | 14 + .../metaschema/draft2019-09/meta/core.json | 54 + .../metaschema/draft2019-09/meta/format.json | 11 + .../draft2019-09/meta/hyper-schema.json | 26 + .../draft2019-09/meta/meta-data.json | 34 + .../draft2019-09/meta/validation.json | 95 ++ .../metaschema/draft2019-09/schema.json | 42 + .../draft2020-12/meta/applicator.json | 45 + .../metaschema/draft2020-12/meta/content.json | 14 + .../metaschema/draft2020-12/meta/core.json | 48 + .../draft2020-12/meta/format-annotation.json | 11 + .../draft2020-12/meta/format-assertion.json | 11 + .../draft2020-12/meta/hyper-schema.json | 26 + .../draft2020-12/meta/meta-data.json | 34 + .../draft2020-12/meta/unevaluated.json | 12 + .../draft2020-12/meta/validation.json | 95 ++ .../metaschema/draft2020-12/schema.json | 58 + .../metaschema/draft4/schema.json | 149 +++ .../metaschema/draft6/schema.json | 155 +++ .../metaschema/draft7/schema.json | 172 +++ Solutions/Corvus.JsonSchema.sln | 534 +++----- 86 files changed, 10286 insertions(+), 372 deletions(-) create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/ArrayPoolBufferWriter.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/Enums/AllocationMode.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/Interfaces/IBuffer{T}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryBufferWriter{T}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryDebugView{T}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryOwner{T}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/SpanOwner{T}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Enumerables/SpanEnumerable{T}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Enumerables/SpanTokenizer{T}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/ArrayExtensions.1D.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/ArrayPoolExtensions.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/BoolExtensions.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/BitOperations.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/RuntimeHelpers.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/SpanHelper.Count.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/ObjectMarshal.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.AppendSpanFormattable.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.Replace.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.Json.SourceGeneratorTools.csproj create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/IUriTemplateParser.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/Internal/StringBuilderPool.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/MatchWithVerb.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/MatchWithVerb{TMatch}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterByNameAndRangeCache.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterCache.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterValue.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ReadOnlySpanCallback.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/DictionaryTemplateParameterProvider.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/DictionaryUriTemplateResolver.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/QueryStringParameterOrder.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/TemplateMatch.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriExtensions.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplate.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplateExtensions.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplateTable.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateMatchResult.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/ITemplateParameterProvider.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/OperatorInfo.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/TemplateParameterProvider.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/VariableProcessingState.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/VariableSpecification.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateAndVerbTable.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateAndVerbTable{TMatch}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParameters.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParserExtensions.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParserFactory.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateRegexBuilder.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateResolver{TParameterProvider,TParameterPayload}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateTable.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateTable{TMatch}.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/IndexRange/Index.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/IndexRange/Range.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/Metaschema.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/README.md create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/SourceGeneratorHelpers.cs create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/corvus/meta/corvus-extensions.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/corvus/schema.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/applicator.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/content.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/core.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/format.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/hyper-schema.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/meta-data.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/validation.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/schema.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/applicator.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/content.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/core.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/format-annotation.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/format-assertion.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/hyper-schema.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/meta-data.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/unevaluated.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/validation.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/schema.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft4/schema.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft6/schema.json create mode 100644 Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft7/schema.json diff --git a/Solutions/Corvus.Json.CodeGeneration/Corvus.Json.CodeGeneration/TypeDeclarations/TypeDeclarationExtensions.cs b/Solutions/Corvus.Json.CodeGeneration/Corvus.Json.CodeGeneration/TypeDeclarations/TypeDeclarationExtensions.cs index eb2220408..df20ca93c 100644 --- a/Solutions/Corvus.Json.CodeGeneration/Corvus.Json.CodeGeneration/TypeDeclarations/TypeDeclarationExtensions.cs +++ b/Solutions/Corvus.Json.CodeGeneration/Corvus.Json.CodeGeneration/TypeDeclarations/TypeDeclarationExtensions.cs @@ -2072,6 +2072,7 @@ private static void SetSingleConstantValue(TypeDeclaration typeDeclaration) // We don't have an existing constant value, foundElement = value; } +#if !BUILDING_SOURCE_GENERATOR else if (JsonElement.DeepEquals(foundElement, value)) { // The new constant is the same as the old value @@ -2079,6 +2080,7 @@ private static void SetSingleConstantValue(TypeDeclaration typeDeclaration) // constant from one of the composed types) continue; } +#endif else { // We have more than keyword that explicitly provides a single constant value @@ -2105,6 +2107,7 @@ private static void SetSingleConstantValue(TypeDeclaration typeDeclaration) // an explicit declaration foundElement = constantValue; } +#if !BUILDING_SOURCE_GENERATOR else if (JsonElement.DeepEquals(foundElement, constantValue)) { // The new type is the same as the old type @@ -2112,6 +2115,7 @@ private static void SetSingleConstantValue(TypeDeclaration typeDeclaration) // type from one of the composed types) continue; } +#endif else { // We have more than keyword that explicitly provides a format diff --git a/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/BinaryJsonNumber.NetStandard.cs b/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/BinaryJsonNumber.NetStandard.cs index 14b9eceb6..ca300028e 100644 --- a/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/BinaryJsonNumber.NetStandard.cs +++ b/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/BinaryJsonNumber.NetStandard.cs @@ -11,7 +11,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json; -using CommunityToolkit.HighPerformance; namespace Corvus.Json; diff --git a/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/Internal/JsonReaderHelper.cs b/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/Internal/JsonReaderHelper.cs index 58a5955fd..375614de0 100644 --- a/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/Internal/JsonReaderHelper.cs +++ b/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/Internal/JsonReaderHelper.cs @@ -15,7 +15,6 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Text; -using CommunityToolkit.HighPerformance.Buffers; namespace Corvus.Json { diff --git a/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/Internal/LowAllocJsonUtils.cs b/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/Internal/LowAllocJsonUtils.cs index 086da46c2..7780fa155 100644 --- a/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/Internal/LowAllocJsonUtils.cs +++ b/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/Internal/LowAllocJsonUtils.cs @@ -38,8 +38,27 @@ public static bool ProcessRawText( in Utf8Parser callback, [NotNullWhen(true)] out TResult? result) { +#if BUILDING_SOURCE_GENERATOR + PooledWriter? writerPair = null; + try + { + writerPair = WriterPool.Get(); + (Utf8JsonWriter w, ArrayPoolBufferWriter writer) = writerPair.Get(); + element.WriteTo(w); + w.Flush(); + return callback(writer.WrittenSpan[1..^1], state, out result); + } + finally + { + if (writerPair is not null) + { + WriterPool.Return(writerPair); + } + } +#else ReadOnlySpan rawValue = JsonMarshal.GetRawUtf8Value(element); return callback(rawValue[1..^1], state, out result); +#endif } /// @@ -58,8 +77,27 @@ public static bool ProcessRawValue( in Utf8Parser callback, [NotNullWhen(true)] out TResult? result) { +#if BUILDING_SOURCE_GENERATOR + PooledWriter? writerPair = null; + try + { + writerPair = WriterPool.Get(); + (Utf8JsonWriter w, ArrayPoolBufferWriter writer) = writerPair.Get(); + element.WriteTo(w); + w.Flush(); + return callback(writer.WrittenSpan, state, out result); + } + finally + { + if (writerPair is not null) + { + WriterPool.Return(writerPair); + } + } +#else ReadOnlySpan rawValue = JsonMarshal.GetRawUtf8Value(element); - return callback(rawValue[1..^1], state, out result); + return callback(rawValue, state, out result); +#endif } /// diff --git a/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/JsonElementExtensions.cs b/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/JsonElementExtensions.cs index 5386fb007..f69c4f73a 100644 --- a/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/JsonElementExtensions.cs +++ b/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/JsonElementExtensions.cs @@ -59,7 +59,7 @@ public static bool TryGetRawText(this JsonElement element, in U /// public static bool TryGetRawText(this JsonElement element, in Utf8Parser parser, in TState state, bool decode, [NotNullWhen(true)] out TResult? value) { - return element.ProcessRawValue(new Utf8ParserStateWrapper(parser, state, decode), ProcessRawText, out value); + return element.ProcessRawText(new Utf8ParserStateWrapper(parser, state, decode), ProcessRawText, out value); } /// diff --git a/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/ValidationContext.cs b/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/ValidationContext.cs index 55001df28..333f8a815 100644 --- a/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/ValidationContext.cs +++ b/Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/ValidationContext.cs @@ -5,7 +5,6 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Runtime.InteropServices; -using CommunityToolkit.HighPerformance; namespace Corvus.Json; diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/ArrayPoolBufferWriter.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/ArrayPoolBufferWriter.cs new file mode 100644 index 000000000..d08b6e689 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/ArrayPoolBufferWriter.cs @@ -0,0 +1,383 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using CommunityToolkit.HighPerformance.Buffers.Views; +#if NET6_0_OR_GREATER +using BitOperations = System.Numerics.BitOperations; +#else +using BitOperations = CommunityToolkit.HighPerformance.Helpers.Internals.BitOperations; +#endif + +namespace CommunityToolkit.HighPerformance.Buffers; + +/// +/// Represents a heap-based, array-backed output sink into which data can be written. +/// +/// The type of items to write to the current instance. +/// +/// This is a custom implementation that replicates the +/// functionality and API surface of the array-based buffer writer available in +/// .NET Standard 2.1, with the main difference being the fact that in this case +/// the arrays in use are rented from the shared instance, +/// and that is also available on .NET Standard 2.0. +/// +[DebuggerTypeProxy(typeof(MemoryDebugView<>))] +[DebuggerDisplay("{ToString(),raw}")] +public sealed class ArrayPoolBufferWriter : IBuffer, IMemoryOwner +{ + /// + /// The default buffer size to use to expand empty arrays. + /// + private const int DefaultInitialBufferSize = 256; + + /// + /// The instance used to rent . + /// + private readonly ArrayPool pool; + + /// + /// The underlying array. + /// + private T[]? array; + +#pragma warning disable IDE0032 // Use field over auto-property (clearer and faster) + /// + /// The starting offset within . + /// + private int index; +#pragma warning restore IDE0032 + + /// + /// Initializes a new instance of the class. + /// + public ArrayPoolBufferWriter() + : this(ArrayPool.Shared, DefaultInitialBufferSize) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The instance to use. + public ArrayPoolBufferWriter(ArrayPool pool) + : this(pool, DefaultInitialBufferSize) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The minimum capacity with which to initialize the underlying buffer. + /// Thrown when is not valid. + public ArrayPoolBufferWriter(int initialCapacity) + : this(ArrayPool.Shared, initialCapacity) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The instance to use. + /// The minimum capacity with which to initialize the underlying buffer. + /// Thrown when is not valid. + public ArrayPoolBufferWriter(ArrayPool pool, int initialCapacity) + { + // Since we're using pooled arrays, we can rent the buffer with the + // default size immediately, we don't need to use lazy initialization + // to save unnecessary memory allocations in this case. + // Additionally, we don't need to manually throw the exception if + // the requested size is not valid, as that'll be thrown automatically + // by the array pool in use when we try to rent an array with that size. + this.pool = pool; + this.array = pool.Rent(initialCapacity); + this.index = 0; + } + + /// + Memory IMemoryOwner.Memory + { + // This property is explicitly implemented so that it's hidden + // under normal usage, as the name could be confusing when + // displayed besides WrittenMemory and GetMemory(). + // The IMemoryOwner interface is implemented primarily + // so that the AsStream() extension can be used on this type, + // allowing users to first create a ArrayPoolBufferWriter + // instance to write data to, then get a stream through the + // extension and let it take care of returning the underlying + // buffer to the shared pool when it's no longer necessary. + // Inlining is not needed here since this will always be a callvirt. + get => MemoryMarshal.AsMemory(WrittenMemory); + } + + /// + public ReadOnlyMemory WrittenMemory + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.AsMemory(0, this.index); + } + } + + /// + public ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.AsSpan(0, this.index); + } + } + + /// + public int WrittenCount + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.index; + } + + /// + public int Capacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.Length; + } + } + + /// + public int FreeCapacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.Length - this.index; + } + } + + /// + public void Clear() + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + array.AsSpan(0, this.index).Clear(); + + this.index = 0; + } + + /// + public void Advance(int count) + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + if (count < 0) + { + ThrowArgumentOutOfRangeExceptionForNegativeCount(); + } + + if (this.index > array!.Length - count) + { + ThrowArgumentExceptionForAdvancedTooFar(); + } + + this.index += count; + } + + /// + public Memory GetMemory(int sizeHint = 0) + { + CheckBufferAndEnsureCapacity(sizeHint); + + return this.array.AsMemory(this.index); + } + + /// + public Span GetSpan(int sizeHint = 0) + { + CheckBufferAndEnsureCapacity(sizeHint); + + return this.array.AsSpan(this.index); + } + + /// + /// Gets an instance wrapping the underlying array in use. + /// + /// An instance wrapping the underlying array in use. + /// Thrown when the buffer in use has already been disposed. + /// + /// This method is meant to be used when working with APIs that only accept an array as input, and should be used with caution. + /// In particular, the returned array is rented from an array pool, and it is responsibility of the caller to ensure that it's + /// not used after the current instance is disposed. Doing so is considered undefined + /// behavior, as the same array might be in use within another instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArraySegment DangerousGetArray() + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return new(array!, 0, this.index); + } + + /// + public void Dispose() + { + T[]? array = this.array; + + if (array is null) + { + return; + } + + this.array = null; + + this.pool.Return(array); + } + + /// + public override string ToString() + { + // See comments in MemoryOwner about this + if (typeof(T) == typeof(char) && + this.array is char[] chars) + { + return new(chars, 0, this.index); + } + + // Same representation used in Span + return $"CommunityToolkit.HighPerformance.Buffers.ArrayPoolBufferWriter<{typeof(T)}>[{this.index}]"; + } + + /// + /// Ensures that has enough free space to contain a given number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckBufferAndEnsureCapacity(int sizeHint) + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + if (sizeHint < 0) + { + ThrowArgumentOutOfRangeExceptionForNegativeSizeHint(); + } + + if (sizeHint == 0) + { + sizeHint = 1; + } + + if (sizeHint > array!.Length - this.index) + { + ResizeBuffer(sizeHint); + } + } + + /// + /// Resizes to ensure it can fit the specified number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.NoInlining)] + private void ResizeBuffer(int sizeHint) + { + uint minimumSize = (uint)this.index + (uint)sizeHint; + + // The ArrayPool class has a maximum threshold of 1024 * 1024 for the maximum length of + // pooled arrays, and once this is exceeded it will just allocate a new array every time + // of exactly the requested size. In that case, we manually round up the requested size to + // the nearest power of two, to ensure that repeated consecutive writes when the array in + // use is bigger than that threshold don't end up causing a resize every single time. + if (minimumSize > 1024 * 1024) + { + minimumSize = BitOperations.RoundUpToPowerOf2(minimumSize); + } + + this.pool.Resize(ref this.array, (int)minimumSize); + } + + /// + /// Throws an when the requested count is negative. + /// + private static void ThrowArgumentOutOfRangeExceptionForNegativeCount() + { + throw new ArgumentOutOfRangeException("count", "The count can't be a negative value."); + } + + /// + /// Throws an when the size hint is negative. + /// + private static void ThrowArgumentOutOfRangeExceptionForNegativeSizeHint() + { + throw new ArgumentOutOfRangeException("sizeHint", "The size hint can't be a negative value."); + } + + /// + /// Throws an when the requested count is negative. + /// + private static void ThrowArgumentExceptionForAdvancedTooFar() + { + throw new ArgumentException("The buffer writer has advanced too far."); + } + + /// + /// Throws an when is . + /// + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException("The current buffer has already been disposed."); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/Enums/AllocationMode.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/Enums/AllocationMode.cs new file mode 100644 index 000000000..7e29a9f79 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/Enums/AllocationMode.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.HighPerformance.Buffers; + +/// +/// An that indicates a mode to use when allocating buffers. +/// +public enum AllocationMode +{ + /// + /// The default allocation mode for pooled memory (rented buffers are not cleared). + /// + Default, + + /// + /// Clear pooled buffers when renting them. + /// + Clear +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/Interfaces/IBuffer{T}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/Interfaces/IBuffer{T}.cs new file mode 100644 index 000000000..843ed6b56 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/Interfaces/IBuffer{T}.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; + +namespace CommunityToolkit.HighPerformance.Buffers; + +/// +/// An interface that expands with the ability to also inspect +/// the written data, and to reset the underlying buffer to write again from the start. +/// +/// The type of items in the current buffer. +public interface IBuffer : IBufferWriter +{ + /// + /// Gets the data written to the underlying buffer so far, as a . + /// + ReadOnlyMemory WrittenMemory { get; } + + /// + /// Gets the data written to the underlying buffer so far, as a . + /// + ReadOnlySpan WrittenSpan { get; } + + /// + /// Gets the amount of data written to the underlying buffer so far. + /// + int WrittenCount { get; } + + /// + /// Gets the total amount of space within the underlying buffer. + /// + int Capacity { get; } + + /// + /// Gets the amount of space available that can still be written into without forcing the underlying buffer to grow. + /// + int FreeCapacity { get; } + + /// + /// Clears the data written to the underlying buffer. + /// + /// + /// You must clear the instance before trying to re-use it. + /// + void Clear(); +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryBufferWriter{T}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryBufferWriter{T}.cs new file mode 100644 index 000000000..a306ac8ee --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryBufferWriter{T}.cs @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using CommunityToolkit.HighPerformance.Buffers.Views; + +namespace CommunityToolkit.HighPerformance.Buffers; + +/// +/// Represents an output sink into which data can be written, backed by a instance. +/// +/// The type of items to write to the current instance. +/// +/// This is a custom implementation that wraps a instance. +/// It can be used to bridge APIs consuming an with existing +/// instances (or objects that can be converted to a ), to ensure the data is written directly +/// to the intended buffer, with no possibility of doing additional allocations or expanding the available capacity. +/// +[DebuggerTypeProxy(typeof(MemoryDebugView<>))] +[DebuggerDisplay("{ToString(),raw}")] +public sealed class MemoryBufferWriter : IBuffer +{ + /// + /// The underlying instance. + /// + private readonly Memory memory; + +#pragma warning disable IDE0032 // Use field over auto-property (like in ArrayPoolBufferWriter) + /// + /// The starting offset within . + /// + private int index; +#pragma warning restore IDE0032 + + /// + /// Initializes a new instance of the class. + /// + /// The target instance to write to. + public MemoryBufferWriter(Memory memory) + { + this.memory = memory; + } + + /// + public ReadOnlyMemory WrittenMemory + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.memory.Slice(0, this.index); + } + + /// + public ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.memory.Slice(0, this.index).Span; + } + + /// + public int WrittenCount + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.index; + } + + /// + public int Capacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.memory.Length; + } + + /// + public int FreeCapacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.memory.Length - this.index; + } + + /// + public void Clear() + { + this.memory.Slice(0, this.index).Span.Clear(); + this.index = 0; + } + + /// + public void Advance(int count) + { + if (count < 0) + { + ThrowArgumentOutOfRangeExceptionForNegativeCount(); + } + + if (this.index > this.memory.Length - count) + { + ThrowArgumentExceptionForAdvancedTooFar(); + } + + this.index += count; + } + + /// + public Memory GetMemory(int sizeHint = 0) + { + ValidateSizeHint(sizeHint); + + return this.memory.Slice(this.index); + } + + /// + public Span GetSpan(int sizeHint = 0) + { + ValidateSizeHint(sizeHint); + + return this.memory.Slice(this.index).Span; + } + + /// + /// Validates the requested size for either or . + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidateSizeHint(int sizeHint) + { + if (sizeHint < 0) + { + ThrowArgumentOutOfRangeExceptionForNegativeSizeHint(); + } + + if (sizeHint == 0) + { + sizeHint = 1; + } + + if (sizeHint > FreeCapacity) + { + ThrowArgumentExceptionForCapacityExceeded(); + } + } + + /// + public override string ToString() + { + // See comments in MemoryOwner about this + if (typeof(T) == typeof(char)) + { + return this.memory.Slice(0, this.index).ToString(); + } + + // Same representation used in Span + return $"CommunityToolkit.HighPerformance.Buffers.MemoryBufferWriter<{typeof(T)}>[{this.index}]"; + } + + /// + /// Throws an when the requested count is negative. + /// + private static void ThrowArgumentOutOfRangeExceptionForNegativeCount() + { + throw new ArgumentOutOfRangeException("count", "The count can't be a negative value."); + } + + /// + /// Throws an when the size hint is negative. + /// + private static void ThrowArgumentOutOfRangeExceptionForNegativeSizeHint() + { + throw new ArgumentOutOfRangeException("sizeHint", "The size hint can't be a negative value."); + } + + /// + /// Throws an when the requested count is negative. + /// + private static void ThrowArgumentExceptionForAdvancedTooFar() + { + throw new ArgumentException("The buffer writer has advanced too far."); + } + + /// + /// Throws an when the requested size exceeds the capacity. + /// + private static void ThrowArgumentExceptionForCapacityExceeded() + { + throw new ArgumentException("The buffer writer doesn't have enough capacity left."); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryDebugView{T}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryDebugView{T}.cs new file mode 100644 index 000000000..eb7d24fdf --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryDebugView{T}.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Diagnostics; + +namespace CommunityToolkit.HighPerformance.Buffers.Views; + +/// +/// A debug proxy used to display items in a 1D layout. +/// +/// The type of items to display. +internal sealed class MemoryDebugView +{ + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView(ArrayPoolBufferWriter? arrayPoolBufferWriter) + { + this.Items = arrayPoolBufferWriter?.WrittenSpan.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView(MemoryBufferWriter? memoryBufferWriter) + { + this.Items = memoryBufferWriter?.WrittenSpan.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView(MemoryOwner? memoryOwner) + { + this.Items = memoryOwner?.Span.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView(SpanOwner spanOwner) + { + this.Items = spanOwner.Span.ToArray(); + } + + /// + /// Gets the items to display for the current instance + /// + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + public T[]? Items { get; } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryOwner{T}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryOwner{T}.cs new file mode 100644 index 000000000..5f4c6d81d --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/MemoryOwner{T}.cs @@ -0,0 +1,338 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +#if NET6_0_OR_GREATER +using System.Runtime.InteropServices; +#endif +using CommunityToolkit.HighPerformance.Buffers.Views; + +namespace CommunityToolkit.HighPerformance.Buffers; + +/// +/// An implementation with an embedded length and a fast accessor. +/// +/// The type of items to store in the current instance. +[DebuggerTypeProxy(typeof(MemoryDebugView<>))] +[DebuggerDisplay("{ToString(),raw}")] +public sealed class MemoryOwner : IMemoryOwner +{ + /// + /// The starting offset within . + /// + private readonly int start; + +#pragma warning disable IDE0032 + /// + /// The usable length within (starting from ). + /// + private readonly int length; +#pragma warning restore IDE0032 + + /// + /// The instance used to rent . + /// + private readonly ArrayPool pool; + + /// + /// The underlying array. + /// + private T[]? array; + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// The instance to use. + /// Indicates the allocation mode to use for the new buffer to rent. + private MemoryOwner(int length, ArrayPool pool, AllocationMode mode) + { + this.start = 0; + this.length = length; + this.pool = pool; + this.array = pool.Rent(length); + + if (mode == AllocationMode.Clear) + { + this.array.AsSpan(0, length).Clear(); + } + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The starting offset within . + /// The length of the array to use. + /// The instance currently in use. + /// The input array to use. + private MemoryOwner(int start, int length, ArrayPool pool, T[] array) + { + this.start = start; + this.length = length; + this.pool = pool; + this.array = array; + } + + /// + /// Gets an empty instance. + /// + public static MemoryOwner Empty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(0, ArrayPool.Shared, AllocationMode.Default); + } + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryOwner Allocate(int size) => new(size, ArrayPool.Shared, AllocationMode.Default); + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// The instance currently in use. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryOwner Allocate(int size, ArrayPool pool) => new(size, pool, AllocationMode.Default); + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// Indicates the allocation mode to use for the new buffer to rent. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryOwner Allocate(int size, AllocationMode mode) => new(size, ArrayPool.Shared, mode); + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// The instance currently in use. + /// Indicates the allocation mode to use for the new buffer to rent. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryOwner Allocate(int size, ArrayPool pool, AllocationMode mode) => new(size, pool, mode); + + /// + /// Gets the number of items in the current instance + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.length; + } + + /// + public Memory Memory + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return new(array!, this.start, this.length); + } + } + + /// + /// Gets a wrapping the memory belonging to the current instance. + /// + public Span Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + +#if NET6_0_OR_GREATER + ref T r0 = ref array!.DangerousGetReferenceAt(this.start); + + // On .NET 6+ runtimes, we can manually create a span from the starting reference to + // skip the argument validations, which include an explicit null check, covariance check + // for the array and the actual validation for the starting offset and target length. We + // only do this on .NET 6+ as we can leverage the runtime-specific array layout to get + // a fast access to the initial element, which makes this trick worth it. Otherwise, on + // runtimes where we would need to at least access a static field to retrieve the base + // byte offset within an SZ array object, we can get better performance by just using the + // default Span constructor and paying the cost of the extra conditional branches, + // especially if T is a value type, in which case the covariance check is JIT removed. + return MemoryMarshal.CreateSpan(ref r0, this.length); +#else + return new(array!, this.start, this.length); +#endif + } + } + + /// + /// Returns a reference to the first element within the current instance, with no bounds check. + /// + /// A reference to the first element within the current instance. + /// Thrown when the buffer in use has already been disposed. + /// + /// This method does not perform bounds checks on the underlying buffer, but does check whether + /// the buffer itself has been disposed or not. This check should not be removed, and it's also + /// the reason why the method to get a reference at a specified offset is not present. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReference() + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return ref array!.DangerousGetReferenceAt(this.start); + } + + /// + /// Gets an instance wrapping the underlying array in use. + /// + /// An instance wrapping the underlying array in use. + /// Thrown when the buffer in use has already been disposed. + /// + /// This method is meant to be used when working with APIs that only accept an array as input, and should be used with caution. + /// In particular, the returned array is rented from an array pool, and it is responsibility of the caller to ensure that it's + /// not used after the current instance is disposed. Doing so is considered undefined behavior, + /// as the same array might be in use within another instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArraySegment DangerousGetArray() + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return new(array!, this.start, this.length); + } + + /// + /// Slices the buffer currently in use and returns a new instance. + /// + /// The starting offset within the current buffer. + /// The length of the buffer to use. + /// A new instance using the target range of items. + /// Thrown when the buffer in use has already been disposed. + /// Thrown when or are not valid. + /// + /// Using this method will dispose the current instance, and should only be used when an oversized + /// buffer is rented and then adjusted in size, to avoid having to rent a new buffer of the new + /// size and copy the previous items into the new one, or needing an additional variable/field + /// to manually handle to track the used range within a given instance. + /// + public MemoryOwner Slice(int start, int length) + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + this.array = null; + + if ((uint)start > this.length) + { + ThrowInvalidOffsetException(); + } + + if ((uint)length > (this.length - start)) + { + ThrowInvalidLengthException(); + } + + // We're transferring the ownership of the underlying array, so the current + // instance no longer needs to be disposed. Because of this, we can manually + // suppress the finalizer to reduce the overhead on the garbage collector. + GC.SuppressFinalize(this); + + return new(start, length, this.pool, array!); + } + + /// + public void Dispose() + { + T[]? array = this.array; + + if (array is null) + { + return; + } + + this.array = null; + + this.pool.Return(array); + } + + /// + public override string ToString() + { + // Normally we would throw if the array has been disposed, + // but in this case we'll just return the non formatted + // representation as a fallback, since the ToString method + // is generally expected not to throw exceptions. + if (typeof(T) == typeof(char) && + this.array is char[] chars) + { + return new(chars, this.start, this.length); + } + + // Same representation used in Span + return $"CommunityToolkit.HighPerformance.Buffers.MemoryOwner<{typeof(T)}>[{this.length}]"; + } + + /// + /// Throws an when is . + /// + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(nameof(MemoryOwner), "The current buffer has already been disposed"); + } + + /// + /// Throws an when the is invalid. + /// + private static void ThrowInvalidOffsetException() + { + throw new ArgumentOutOfRangeException(nameof(start), "The input start parameter was not valid"); + } + + /// + /// Throws an when the is invalid. + /// + private static void ThrowInvalidLengthException() + { + throw new ArgumentOutOfRangeException(nameof(length), "The input length parameter was not valid"); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/SpanOwner{T}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/SpanOwner{T}.cs new file mode 100644 index 000000000..d3129e448 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Buffers/SpanOwner{T}.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +#if NET6_0_OR_GREATER +using System.Runtime.InteropServices; +#endif +using CommunityToolkit.HighPerformance.Buffers.Views; + +namespace CommunityToolkit.HighPerformance.Buffers; + +/// +/// A stack-only type with the ability to rent a buffer of a specified length and getting a from it. +/// This type mirrors but without allocations and with further optimizations. +/// As this is a stack-only type, it relies on the duck-typed pattern introduced with C# 8. +/// It should be used like so: +/// +/// using (SpanOwner<byte> buffer = SpanOwner<byte>.Allocate(1024)) +/// { +/// // Use the buffer here... +/// } +/// +/// As soon as the code leaves the scope of that block, the underlying buffer will automatically +/// be disposed. The APIs in rely on this pattern for extra performance, eg. they don't perform +/// the additional checks that are done in to ensure that the buffer hasn't been disposed +/// before returning a or instance from it. +/// As such, this type should always be used with a block or expression. +/// Not doing so will cause the underlying buffer not to be returned to the shared pool. +/// +/// The type of items to store in the current instance. +[DebuggerTypeProxy(typeof(MemoryDebugView<>))] +[DebuggerDisplay("{ToString(),raw}")] +public readonly ref struct SpanOwner +{ +#pragma warning disable IDE0032 + /// + /// The usable length within . + /// + private readonly int length; +#pragma warning restore IDE0032 + + /// + /// The instance used to rent . + /// + private readonly ArrayPool pool; + + /// + /// The underlying array. + /// + private readonly T[] array; + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// The instance to use. + /// Indicates the allocation mode to use for the new buffer to rent. + private SpanOwner(int length, ArrayPool pool, AllocationMode mode) + { + this.length = length; + this.pool = pool; + this.array = pool.Rent(length); + + if (mode == AllocationMode.Clear) + { + this.array.AsSpan(0, length).Clear(); + } + } + + /// + /// Gets an empty instance. + /// + public static SpanOwner Empty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(0, ArrayPool.Shared, AllocationMode.Default); + } + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanOwner Allocate(int size) => new(size, ArrayPool.Shared, AllocationMode.Default); + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// The instance to use. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanOwner Allocate(int size, ArrayPool pool) => new(size, pool, AllocationMode.Default); + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// Indicates the allocation mode to use for the new buffer to rent. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanOwner Allocate(int size, AllocationMode mode) => new(size, ArrayPool.Shared, mode); + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// The instance to use. + /// Indicates the allocation mode to use for the new buffer to rent. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanOwner Allocate(int size, ArrayPool pool, AllocationMode mode) => new(size, pool, mode); + + /// + /// Gets the number of items in the current instance + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.length; + } + + /// + /// Gets a wrapping the memory belonging to the current instance. + /// + public Span Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NET6_0_OR_GREATER + ref T r0 = ref this.array!.DangerousGetReference(); + + return MemoryMarshal.CreateSpan(ref r0, this.length); +#else + return new(this.array, 0, this.length); +#endif + } + } + + /// + /// Returns a reference to the first element within the current instance, with no bounds check. + /// + /// A reference to the first element within the current instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReference() + { + return ref this.array.DangerousGetReference(); + } + + /// + /// Gets an instance wrapping the underlying array in use. + /// + /// An instance wrapping the underlying array in use. + /// + /// This method is meant to be used when working with APIs that only accept an array as input, and should be used with caution. + /// In particular, the returned array is rented from an array pool, and it is responsibility of the caller to ensure that it's + /// not used after the current instance is disposed. Doing so is considered undefined behavior, + /// as the same array might be in use within another instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArraySegment DangerousGetArray() + { + return new(this.array!, 0, this.length); + } + + /// + /// Implements the duck-typed method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + this.pool.Return(this.array); + } + + /// + public override string ToString() + { + if (typeof(T) == typeof(char) && + this.array is char[] chars) + { + return new(chars, 0, this.length); + } + + // Same representation used in Span + return $"CommunityToolkit.HighPerformance.Buffers.SpanOwner<{typeof(T)}>[{this.length}]"; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Enumerables/SpanEnumerable{T}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Enumerables/SpanEnumerable{T}.cs new file mode 100644 index 000000000..739b0d38d --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Enumerables/SpanEnumerable{T}.cs @@ -0,0 +1,180 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.HighPerformance.Enumerables; + +/// +/// A that enumerates the items in a given instance. +/// +/// The type of items to enumerate. +[EditorBrowsable(EditorBrowsableState.Never)] +public ref struct SpanEnumerable +{ + /// + /// The source instance. + /// + private readonly Span span; + + /// + /// The current index within . + /// + private int index; + + /// + /// Initializes a new instance of the struct. + /// + /// The source instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SpanEnumerable(Span span) + { + this.span = span; + this.index = -1; + } + + /// + /// Implements the duck-typed method. + /// + /// An instance targeting the current value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly SpanEnumerable GetEnumerator() => this; + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + return ++this.index < this.span.Length; + } + + /// + /// Gets the duck-typed property. + /// + public readonly Item Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NETSTANDARD2_1_OR_GREATER + ref T r0 = ref MemoryMarshal.GetReference(this.span); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)this.index); + + // On .NET Standard 2.1 and .NET Core (or on any target that offers runtime + // support for the Span types), we can save 4 bytes by piggybacking the + // current index in the length of the wrapped span. We're going to use the + // first item as the target reference, and the length as a host for the + // current original offset. This is not possible on eg. .NET Standard 2.0, + // as we lack the API to create Span-s from arbitrary references. + return new(ref ri, this.index); +#else + return new(this.span, this.index); +#endif + } + } + + /// + /// An item from a source instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly ref struct Item + { +#if NET7_0_OR_GREATER + /// + /// The reference for the instance. + /// + private readonly ref T reference; + + /// + /// The index of the current instance. + /// + private readonly int index; +#else + /// + /// The source instance. + /// + private readonly Span span; +#endif + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Initializes a new instance of the struct. + /// + /// A reference to the target value. + /// The index of the target value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Item(ref T value, int index) + { +#if NET7_0_OR_GREATER + this.reference = ref value; + this.index = index; +#else + this.span = MemoryMarshal.CreateSpan(ref value, index); +#endif + } +#else + /// + /// The current index within . + /// + private readonly int index; + + /// + /// Initializes a new instance of the struct. + /// + /// The source instance. + /// The current index within . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Item(Span span, int index) + { + this.span = span; + this.index = index; + } +#endif + + /// + /// Gets the reference to the current value. + /// + public ref T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NET7_0_OR_GREATER + return ref this.reference; +#elif NETSTANDARD2_1_OR_GREATER + return ref MemoryMarshal.GetReference(this.span); +#else + ref T r0 = ref MemoryMarshal.GetReference(this.span); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)this.index); + + return ref ri; +#endif + } + } + + /// + /// Gets the current index. + /// + public int Index + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NET7_0_OR_GREATER + return this.index; +#elif NETSTANDARD2_1_OR_GREATER + return this.span.Length; +#else + return this.index; +#endif + } + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Enumerables/SpanTokenizer{T}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Enumerables/SpanTokenizer{T}.cs new file mode 100644 index 000000000..edb91318a --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Enumerables/SpanTokenizer{T}.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.HighPerformance.Enumerables; + +/// +/// A that tokenizes a given instance. +/// +/// The type of items to enumerate. +[EditorBrowsable(EditorBrowsableState.Never)] +public ref struct SpanTokenizer + where T : IEquatable +{ + /// + /// The source instance. + /// + private readonly Span span; + + /// + /// The separator item to use. + /// + private readonly T separator; + + /// + /// The current initial offset. + /// + private int start; + + /// + /// The current final offset. + /// + private int end; + + /// + /// Initializes a new instance of the struct. + /// + /// The source instance. + /// The separator item to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SpanTokenizer(Span span, T separator) + { + this.span = span; + this.separator = separator; + this.start = 0; + this.end = -1; + } + + /// + /// Implements the duck-typed method. + /// + /// An instance targeting the current value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly SpanTokenizer GetEnumerator() => this; + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + int newEnd = this.end + 1; + int length = this.span.Length; + + // Additional check if the separator is not the last character + if (newEnd <= length) + { + this.start = newEnd; + + // Here we're inside the 'CommunityToolkit.HighPerformance.Enumerables' namespace, so the + // 'MemoryExtensions' type from the .NET Community Toolkit would be bound instead. Because + // want the one from the BCL (to search by value), we can use its fully qualified name. + int index = System.MemoryExtensions.IndexOf(this.span.Slice(newEnd), this.separator); + + // Extract the current subsequence + if (index >= 0) + { + this.end = newEnd + index; + + return true; + } + + this.end = length; + + return true; + } + + return false; + } + + /// + /// Gets the duck-typed property. + /// + public readonly Span Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.span.Slice(this.start, this.end - this.start); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/ArrayExtensions.1D.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/ArrayExtensions.1D.cs new file mode 100644 index 000000000..3e45c0e45 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/ArrayExtensions.1D.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; +#if NET6_0_OR_GREATER +using System.Runtime.InteropServices; +#endif +using CommunityToolkit.HighPerformance.Enumerables; +#if NETSTANDARD +using CommunityToolkit.HighPerformance.Helpers; +#endif +using CommunityToolkit.HighPerformance.Helpers.Internals; +using RuntimeHelpers = CommunityToolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; + +namespace CommunityToolkit.HighPerformance; + +/// +/// Helpers for working with the type. +/// +public static partial class ArrayExtensions +{ + /// + /// Returns a reference to the first element within a given array, with no bounds checks. + /// + /// The type of elements in the input array instance. + /// The input array instance. + /// A reference to the first element within , or the location it would have used, if is empty. + /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to perform checks in case the returned value is dereferenced. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T DangerousGetReference(this T[] array) + { +#if NET6_0_OR_GREATER + return ref MemoryMarshal.GetArrayDataReference(array); +#else + IntPtr offset = RuntimeHelpers.GetArrayDataByteOffset(); + + return ref ObjectMarshal.DangerousGetObjectDataReferenceAt(array, offset); +#endif + } + + /// + /// Returns a reference to an element at a specified index within a given array, with no bounds checks. + /// + /// The type of elements in the input array instance. + /// The input array instance. + /// The index of the element to retrieve within . + /// A reference to the element within at the index specified by . + /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the parameter is valid. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T DangerousGetReferenceAt(this T[] array, int i) + { +#if NET6_0_OR_GREATER + ref T r0 = ref MemoryMarshal.GetArrayDataReference(array); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)i); + + return ref ri; +#else + IntPtr offset = RuntimeHelpers.GetArrayDataByteOffset(); + ref T r0 = ref ObjectMarshal.DangerousGetObjectDataReferenceAt(array, offset); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)i); + + return ref ri; +#endif + } + + /// + /// Counts the number of occurrences of a given value into a target array instance. + /// + /// The type of items in the input array instance. + /// The input array instance. + /// The value to look for. + /// The number of occurrences of in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Count(this T[] array, T value) + where T : IEquatable + { + ref T r0 = ref array.DangerousGetReference(); + nint length = RuntimeHelpers.GetArrayNativeLength(array); + nint count = SpanHelper.Count(ref r0, length, value); + + if ((nuint)count > int.MaxValue) + { + ThrowOverflowException(); + } + + return (int)count; + } + + /// + /// Enumerates the items in the input array instance, as pairs of reference/index values. + /// This extension should be used directly within a loop: + /// + /// int[] numbers = new[] { 1, 2, 3, 4, 5, 6, 7 }; + /// + /// foreach (var item in numbers.Enumerate()) + /// { + /// // Access the index and value of each item here... + /// int index = item.Index; + /// ref int value = ref item.Value; + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of items to enumerate. + /// The source array to enumerate. + /// A wrapper type that will handle the reference/index enumeration for . + /// The returned value shouldn't be used directly: use this extension in a loop. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanEnumerable Enumerate(this T[] array) + { + return new(array); + } + + /// + /// Tokenizes the values in the input array instance using a specified separator. + /// This extension should be used directly within a loop: + /// + /// char[] text = "Hello, world!".ToCharArray(); + /// + /// foreach (var token in text.Tokenize(',')) + /// { + /// // Access the tokens here... + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of items in the array to tokenize. + /// The source array to tokenize. + /// The separator item to use. + /// A wrapper type that will handle the tokenization for . + /// The returned value shouldn't be used directly: use this extension in a loop. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanTokenizer Tokenize(this T[] array, T separator) + where T : IEquatable + { + return new(array, separator); + } + + /////// + /////// Gets a content hash from the input array instance using the Djb2 algorithm. + /////// For more info, see the documentation for . + /////// + /////// The type of items in the input array instance. + /////// The input array instance. + /////// The Djb2 value for the input array instance. + /////// The Djb2 hash is fully deterministic and with no random components. + ////[MethodImpl(MethodImplOptions.AggressiveInlining)] + ////public static int GetDjb2HashCode(this T[] array) + //// where T : notnull + ////{ + //// ref T r0 = ref array.DangerousGetReference(); + //// nint length = RuntimeHelpers.GetArrayNativeLength(array); + + //// return SpanHelper.GetDjb2HashCode(ref r0, length); + ////} + + /// + /// Checks whether or not a given array is covariant. + /// + /// The type of items in the input array instance. + /// The input array instance. + /// Whether or not is covariant. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsCovariant(this T[] array) + { + return default(T) is null && array.GetType() != typeof(T[]); + } + + /// + /// Throws an when the "column" parameter is invalid. + /// + private static void ThrowOverflowException() + { + throw new OverflowException(); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/ArrayPoolExtensions.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/ArrayPoolExtensions.cs new file mode 100644 index 000000000..844407692 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/ArrayPoolExtensions.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; + +namespace CommunityToolkit.HighPerformance; + +/// +/// Helpers for working with the type. +/// +public static class ArrayPoolExtensions +{ + /// + /// Changes the number of elements of a rented one-dimensional array to the specified new size. + /// + /// The type of items into the target array to resize. + /// The target instance to use to resize the array. + /// The rented array to resize, or to create a new array. + /// The size of the new array. + /// Indicates whether the contents of the array should be cleared before reuse. + /// Thrown when is less than 0. + /// When this method returns, the caller must not use any references to the old array anymore. + public static void Resize(this ArrayPool pool, [NotNull] ref T[]? array, int newSize, bool clearArray = false) + { + // If the old array is null, just create a new one with the requested size + if (array is null) + { + array = pool.Rent(newSize); + + return; + } + + // If the new size is the same as the current size, do nothing + if (array.Length == newSize) + { + return; + } + + // Rent a new array with the specified size, and copy as many items from the current array + // as possible to the new array. This mirrors the behavior of the Array.Resize API from + // the BCL: if the new size is greater than the length of the current array, copy all the + // items from the original array into the new one. Otherwise, copy as many items as possible, + // until the new array is completely filled, and ignore the remaining items in the first array. + T[] newArray = pool.Rent(newSize); + int itemsToCopy = Math.Min(array.Length, newSize); + + Array.Copy(array, 0, newArray, 0, itemsToCopy); + + pool.Return(array, clearArray); + + array = newArray; + } + + /// + /// Ensures that when the method returns is not null and is at least in length. + /// Contents of are not copied if a new array is rented. + /// + /// The type of items into the target array given as input. + /// The target instance used to rent and/or return the array. + /// The rented array to ensure capacity for, or to rent a new array. + /// The minimum length of when the method returns. + /// Indicates whether the contents of the array should be cleared if returned to the pool. + /// Thrown when is less than 0. + /// When this method returns, the caller must not use any references to the old array anymore. + public static void EnsureCapacity(this ArrayPool pool, [NotNull] ref T[]? array, int capacity, bool clearArray = false) + { + if (capacity < 0) + { + ThrowArgumentOutOfRangeExceptionForNegativeArrayCapacity(); + } + + if (array is null) + { + array = pool.Rent(capacity); + } + else if (array.Length < capacity) + { + // Ensure rent succeeds before returning the original array to the pool + T[] newArray = pool.Rent(capacity); + + pool.Return(array, clearArray); + + array = newArray; + } + } + + /// + /// Throws an when the "capacity" parameter is negative. + /// + private static void ThrowArgumentOutOfRangeExceptionForNegativeArrayCapacity() + { + throw new ArgumentOutOfRangeException("capacity", "The array capacity must be a positive number."); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/BoolExtensions.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/BoolExtensions.cs new file mode 100644 index 000000000..92ee5894e --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Extensions/BoolExtensions.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.HighPerformance; + +/// +/// Helpers for working with the type. +/// +public static class BoolExtensions +{ + /// + /// Converts the given value into a . + /// + /// The input value to convert. + /// 1 if is , 0 otherwise. + /// This method does not contain branching instructions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe byte ToByte(this bool flag) + { + // Whenever we need to take the address of an argument, we make a local copy first. + // This will be removed by the JIT anyway, but it can help produce better codegen and + // remove unwanted stack spills if the caller is using constant arguments. This is + // because taking the address of an argument can interfere with some of the flow + // analysis executed by the JIT, which can in some cases block constant propagation. + bool copy = flag; + + return *(byte*)© + } + + /// + /// Converts the given value to an mask with + /// all bits representing the value of the input flag (either 0xFFFFFFFF or 0x00000000). + /// + /// The input value to convert. + /// 0xFFFFFFFF if is , 0x00000000 otherwise. + /// + /// This method does not contain branching instructions, and it is only guaranteed to work with + /// values being either 0 or 1. Operations producing a result, + /// such as numerical comparisons, always result in a valid value. If the value is + /// produced by fields with a custom , + /// or by using or other unsafe APIs to directly manipulate the underlying + /// data though, it is responsibility of the caller to ensure the validity of the provided value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int ToBitwiseMask32(this bool flag) + { + bool copy = flag; + byte rangeFlag = *(byte*)© + int negativeFlag = rangeFlag - 1; + int mask = ~negativeFlag; + + return mask; + } + + /// + /// Converts the given value to a mask with + /// all bits representing the value of the input flag (either all 1s or 0s). + /// + /// The input value to convert. + /// All 1s if is , all 0s otherwise. + /// This method does not contain branching instructions. See additional note in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe long ToBitwiseMask64(this bool flag) + { + bool copy = flag; + byte rangeFlag = *(byte*)© + long negativeFlag = (long)rangeFlag - 1; + long mask = ~negativeFlag; + + return mask; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/BitOperations.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/BitOperations.cs new file mode 100644 index 000000000..19e1b6506 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/BitOperations.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !NET6_0_OR_GREATER + +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.HighPerformance.Helpers.Internals; + +/// +/// Utility methods for intrinsic bit-twiddling operations. The methods use hardware intrinsics +/// when available on the underlying platform, otherwise they use optimized software fallbacks. +/// +internal static class BitOperations +{ + /// + /// Round the given integral value up to a power of 2. + /// + /// The value. + /// + /// The smallest power of 2 which is greater than or equal to . + /// If is 0 or the result overflows, returns 0. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe uint RoundUpToPowerOf2(uint value) + { + // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + + return value + 1; + } +} + +#endif \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/RuntimeHelpers.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/RuntimeHelpers.cs new file mode 100644 index 000000000..ad205b3ab --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/RuntimeHelpers.cs @@ -0,0 +1,280 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// The portable implementation in this type is originally from CoreFX. +// See https://github.com/dotnet/corefx/blob/release/2.1/src/System.Memory/src/System/SpanHelpers.cs. + +using System; +#if !NETSTANDARD2_1_OR_GREATER +using System.Reflection; +#endif +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.HighPerformance.Helpers.Internals; + +/// +/// A helper class that with utility methods for dealing with references, and other low-level details. +/// It also contains some APIs that act as polyfills for .NET Standard 2.0 and below. +/// +internal static class RuntimeHelpers +{ + /// + /// Converts a length of items from one size to another (rounding towards zero). + /// + /// The source type of items. + /// The target type of items. + /// The input length to convert. + /// The converted length for the specified argument and types. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int ConvertLength(int length) + where TFrom : unmanaged + where TTo : unmanaged + { + if (sizeof(TFrom) == sizeof(TTo)) + { + return length; + } + else if (sizeof(TFrom) == 1) + { + return (int)((uint)length / (uint)sizeof(TTo)); + } + else + { + ulong targetLength = (ulong)(uint)length * (uint)sizeof(TFrom) / (uint)sizeof(TTo); + + return checked((int)targetLength); + } + } + + /// + /// Gets the length of a given array as a native integer. + /// + /// The type of values in the array. + /// The input instance. + /// The total length of as a native integer. + /// + /// This method is needed because this expression is not inlined correctly if the target array + /// is only visible as a non-generic instance, because the C# compiler will + /// not be able to emit the opcode instead of calling the right method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint GetArrayNativeLength(T[] array) + { + return (nint)array.LongLength; + } + + /// + /// Gets the length of a given array as a native integer. + /// + /// The input instance. + /// The total length of as a native integer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint GetArrayNativeLength(Array array) + { + return (nint)array.LongLength; + } + +#if !NET6_0_OR_GREATER + /// + /// Gets the byte offset to the first element in a SZ array. + /// + /// The type of values in the array. + /// The byte offset to the first element in a SZ array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IntPtr GetArrayDataByteOffset() + { + return TypeInfo.ArrayDataByteOffset; + } +#endif + + /// + /// Gets the byte offset to the first element in a 2D array. + /// + /// The type of values in the array. + /// The byte offset to the first element in a 2D array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IntPtr GetArray2DDataByteOffset() + { + return TypeInfo.Array2DDataByteOffset; + } + + /// + /// Gets the byte offset to the first element in a 3D array. + /// + /// The type of values in the array. + /// The byte offset to the first element in a 3D array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IntPtr GetArray3DDataByteOffset() + { + return TypeInfo.Array3DDataByteOffset; + } + +#if !NETSTANDARD2_1_OR_GREATER + /// + /// Gets a byte offset describing a portable pinnable reference. This can either be an + /// interior pointer into some object data (described with a valid reference + /// and a reference to some of its data), or a raw pointer (described with a + /// reference to an , and a reference that is assumed to refer to pinned data). + /// + /// The type of field being referenced. + /// The input hosting the target field. + /// A reference to a target field of type within . + /// + /// The value representing the offset to the target field from the start of the object data + /// for the parameter , or the value of the raw pointer passed as a tracked reference. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe IntPtr GetObjectDataOrReferenceByteOffset(object? obj, ref T data) + { + if (obj is null) + { + return (IntPtr)Unsafe.AsPointer(ref data); + } + + return ObjectMarshal.DangerousGetObjectDataByteOffset(obj, ref data); + } + + /// + /// Gets a reference from data describing a portable pinnable reference. This can either be an + /// interior pointer into some object data (described with a valid reference + /// and a byte offset into its data), or a raw pointer (described with a + /// reference to an , and a byte offset representing the value of the raw pointer). + /// + /// The type of reference to retrieve. + /// The input hosting the target field. + /// The input byte offset for the reference to retrieve. + /// A reference matching the given parameters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe ref T GetObjectDataAtOffsetOrPointerReference(object? obj, IntPtr offset) + { + if (obj is null) + { + return ref Unsafe.AsRef((void*)offset); + } + + return ref ObjectMarshal.DangerousGetObjectDataReferenceAt(obj, offset); + } + + /// + /// Checks whether or not a given type is a reference type or contains references. + /// + /// The type to check. + /// Whether or not respects the constraint. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsReferenceOrContainsReferences() + { + return TypeInfo.IsReferenceOrContainsReferences; + } + + /// + /// Implements the logic for . + /// + /// The current type to check. + /// Whether or not is a reference type or contains references. + private static bool IsReferenceOrContainsReferences(Type type) + { + // Common case, for primitive types + if (type.IsPrimitive) + { + return false; + } + + // Explicitly check for pointer types first + if (type.IsPointer) + { + return false; + } + + // Check for value types (this has to be after checking for pointers) + if (!type.IsValueType) + { + return true; + } + + // Check if the type is Nullable + if (Nullable.GetUnderlyingType(type) is Type nullableType) + { + type = nullableType; + } + + if (type.IsEnum) + { + return false; + } + + // Complex struct, recursively inspect all fields + foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + if (IsReferenceOrContainsReferences(field.FieldType)) + { + return true; + } + } + + return false; + } +#endif + + /// + /// A private generic class to preload type info for arbitrary runtime types. + /// + /// The type to load info for. + private static class TypeInfo + { + /// + /// The byte offset to the first element in a SZ array. + /// + public static readonly IntPtr ArrayDataByteOffset = MeasureArrayDataByteOffset(); + + /// + /// The byte offset to the first element in a 2D array. + /// + public static readonly IntPtr Array2DDataByteOffset = MeasureArray2DDataByteOffset(); + + /// + /// The byte offset to the first element in a 3D array. + /// + public static readonly IntPtr Array3DDataByteOffset = MeasureArray3DDataByteOffset(); + +#if !NETSTANDARD2_1_OR_GREATER + /// + /// Indicates whether does not respect the constraint. + /// + public static readonly bool IsReferenceOrContainsReferences = IsReferenceOrContainsReferences(typeof(T)); +#endif + + /// + /// Computes the value for . + /// + /// The value of for the current runtime. + private static IntPtr MeasureArrayDataByteOffset() + { + T[]? array = new T[1]; + + return ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array[0]); + } + + /// + /// Computes the value for . + /// + /// The value of for the current runtime. + private static IntPtr MeasureArray2DDataByteOffset() + { + T[,]? array = new T[1, 1]; + + return ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array[0, 0]); + } + + /// + /// Computes the value for . + /// + /// The value of for the current runtime. + private static IntPtr MeasureArray3DDataByteOffset() + { + T[,,]? array = new T[1, 1, 1]; + + return ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array[0, 0, 0]); + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/SpanHelper.Count.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/SpanHelper.Count.cs new file mode 100644 index 000000000..77d08ca78 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/Internals/SpanHelper.Count.cs @@ -0,0 +1,387 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.HighPerformance.Helpers.Internals; + +/// +/// Helpers to process sequences of values by reference. +/// +internal static partial class SpanHelper +{ + /// + /// Counts the number of occurrences of a given value into a target search space. + /// + /// A reference to the start of the search space. + /// The number of items in the search space. + /// The value to look for. + /// The type of value to look for. + /// The number of occurrences of in the search space + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint Count(ref T r0, nint length, T value) + where T : IEquatable + { + if (!Vector.IsHardwareAccelerated) + { + return CountSequential(ref r0, length, value); + } + + // Special vectorized version when using a supported type + if (typeof(T) == typeof(byte) || + typeof(T) == typeof(sbyte) || + typeof(T) == typeof(bool)) + { + ref sbyte r1 = ref Unsafe.As(ref r0); + sbyte target = Unsafe.As(ref value); + + return CountSimd(ref r1, length, target); + } + + if (typeof(T) == typeof(char) || + typeof(T) == typeof(ushort) || + typeof(T) == typeof(short)) + { + ref short r1 = ref Unsafe.As(ref r0); + short target = Unsafe.As(ref value); + + return CountSimd(ref r1, length, target); + } + + if (typeof(T) == typeof(int) || + typeof(T) == typeof(uint)) + { + ref int r1 = ref Unsafe.As(ref r0); + int target = Unsafe.As(ref value); + + return CountSimd(ref r1, length, target); + } + + if (typeof(T) == typeof(long) || + typeof(T) == typeof(ulong)) + { + ref long r1 = ref Unsafe.As(ref r0); + long target = Unsafe.As(ref value); + + return CountSimd(ref r1, length, target); + } + +#if NET6_0_OR_GREATER + if (typeof(T) == typeof(nint) || + typeof(T) == typeof(nuint)) + { + ref nint r1 = ref Unsafe.As(ref r0); + nint target = Unsafe.As(ref value); + + return CountSimd(ref r1, length, target); + } +#endif + + return CountSequential(ref r0, length, value); + } + + /// + /// Implements with a sequential search. + /// + private static nint CountSequential(ref T r0, nint length, T value) + where T : IEquatable + { + nint result = 0; + nint offset = 0; + + // Main loop with 8 unrolled iterations + while (length >= 8) + { + result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 4).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 5).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 6).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 7).Equals(value).ToByte(); + + length -= 8; + offset += 8; + } + + if (length >= 4) + { + result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); + + length -= 4; + offset += 4; + } + + // Iterate over the remaining values and count those that match + while (length > 0) + { + result += Unsafe.Add(ref r0, offset).Equals(value).ToByte(); + + length -= 1; + offset += 1; + } + + return result; + } + + /// + /// Implements with a vectorized search. + /// + private static nint CountSimd(ref T r0, nint length, T value) + where T : unmanaged, IEquatable + { + nint result = 0; + nint offset = 0; + + // Skip the initialization overhead if there are not enough items + if (length >= Vector.Count) + { + Vector vc = new(value); + + do + { + // Calculate the maximum sequential area that can be processed in + // one pass without the risk of numeric overflow in the dot product + // to sum the partial results. We also backup the current offset to + // be able to track how many items have been processed, which lets + // us avoid updating a third counter (length) in the loop body. + nint max = GetUpperBound(); + nint chunkLength = length <= max ? length : max; + nint initialOffset = offset; + + Vector partials = Vector.Zero; + + // Unrolled vectorized loop, with 8 unrolled iterations. We only run this when the + // current type T is at least 2 bytes in size, otherwise the average chunk length + // would always be too small to be able to trigger the unrolled loop, and the overall + // performance would just be slightly worse due to the additional conditional branches. + if (typeof(T) != typeof(sbyte)) + { + while (chunkLength >= Vector.Count * 8) + { + ref T ri0 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 0)); + Vector vi0 = Unsafe.As>(ref ri0); + Vector ve0 = Vector.Equals(vi0, vc); + + partials -= ve0; + + ref T ri1 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 1)); + Vector vi1 = Unsafe.As>(ref ri1); + Vector ve1 = Vector.Equals(vi1, vc); + + partials -= ve1; + + ref T ri2 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 2)); + Vector vi2 = Unsafe.As>(ref ri2); + Vector ve2 = Vector.Equals(vi2, vc); + + partials -= ve2; + + ref T ri3 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 3)); + Vector vi3 = Unsafe.As>(ref ri3); + Vector ve3 = Vector.Equals(vi3, vc); + + partials -= ve3; + + ref T ri4 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 4)); + Vector vi4 = Unsafe.As>(ref ri4); + Vector ve4 = Vector.Equals(vi4, vc); + + partials -= ve4; + + ref T ri5 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 5)); + Vector vi5 = Unsafe.As>(ref ri5); + Vector ve5 = Vector.Equals(vi5, vc); + + partials -= ve5; + + ref T ri6 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 6)); + Vector vi6 = Unsafe.As>(ref ri6); + Vector ve6 = Vector.Equals(vi6, vc); + + partials -= ve6; + + ref T ri7 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 7)); + Vector vi7 = Unsafe.As>(ref ri7); + Vector ve7 = Vector.Equals(vi7, vc); + + partials -= ve7; + + chunkLength -= Vector.Count * 8; + offset += Vector.Count * 8; + } + } + + while (chunkLength >= Vector.Count) + { + ref T ri = ref Unsafe.Add(ref r0, offset); + + // Load the current Vector register, and then use + // Vector.Equals to check for matches. This API sets the + // values corresponding to matching pairs to all 1s. + // Since the input type is guaranteed to always be signed, + // this means that a value with all 1s represents -1, as + // signed numbers are represented in two's complement. + // So we can just subtract this intermediate value to the + // partial results, which effectively sums 1 for each match. + Vector vi = Unsafe.As>(ref ri); + Vector ve = Vector.Equals(vi, vc); + + partials -= ve; + + chunkLength -= Vector.Count; + offset += Vector.Count; + } + +#if NET6_0_OR_GREATER + result += CastToNativeInt(Vector.Sum(partials)); +#else + result += CastToNativeInt(Vector.Dot(partials, Vector.One)); +#endif + length -= offset - initialOffset; + } + while (length >= Vector.Count); + } + + // Optional 8 unrolled iterations. This is only done when a single SIMD + // register can contain over 8 values of the current type, as otherwise + // there could never be enough items left after the vectorized path + if (Vector.Count > 8 && + length >= 8) + { + result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 4).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 5).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 6).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 7).Equals(value).ToByte(); + + length -= 8; + offset += 8; + } + + // Optional 4 unrolled iterations + if (Vector.Count > 4 && + length >= 4) + { + result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); + + length -= 4; + offset += 4; + } + + // Iterate over the remaining values and count those that match + while (length > 0) + { + result += Unsafe.Add(ref r0, offset).Equals(value).ToByte(); + + length -= 1; + offset += 1; + } + + return result; + } + + /// + /// Gets the upper bound for partial sums with a given parameter. + /// + /// The type argument currently in use. + /// The native value representing the upper bound. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe nint GetUpperBound() + where T : unmanaged + { + if (typeof(T) == typeof(sbyte)) + { + return sbyte.MaxValue; + } + + if (typeof(T) == typeof(short)) + { + return short.MaxValue; + } + + if (typeof(T) == typeof(int)) + { + return int.MaxValue; + } + + if (typeof(T) == typeof(long)) + { + if (sizeof(nint) == sizeof(int)) + { + return int.MaxValue; + } + + // If we are on a 64 bit architecture and we are counting with a SIMD vector of 64 + // bit values, we can use long.MaxValue as the upper bound, as a native integer will + // be able to contain such a value with no overflows. This will allow the count tight + // loop to process all the items in the target area in a single pass (except the mod). + // The (void*) cast is necessary to ensure the right constant is produced on runtimes + // before .NET 5 that don't natively support C# 9. For instance, removing that (void*) + // cast results in the value 0xFFFFFFFFFFFFFFFF (-1) instead of 0x7FFFFFFFFFFFFFFFF. + return (nint)(void*)long.MaxValue; + } + +#if NET6_0_OR_GREATER + if (typeof(T) == typeof(nint)) + { + return nint.MaxValue; + } +#endif + + throw null!; + } + + /// + /// Casts a value of a given type to a native . + /// + /// The input type to cast. + /// The input value to cast to native . + /// The native cast of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static nint CastToNativeInt(T value) + where T : unmanaged + { + if (typeof(T) == typeof(sbyte)) + { + return (byte)(sbyte)(object)value; + } + + if (typeof(T) == typeof(short)) + { + return (ushort)(short)(object)value; + } + + if (typeof(T) == typeof(int)) + { + return (nint)(uint)(int)(object)value; + } + + if (typeof(T) == typeof(long)) + { + return (nint)(ulong)(long)(object)value; + } + +#if NET6_0_OR_GREATER + if (typeof(T) == typeof(nint)) + { + return (nint)(object)value; + } +#endif + + throw null!; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/ObjectMarshal.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/ObjectMarshal.cs new file mode 100644 index 000000000..866c7dbce --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Community.HighPerformance/Helpers/ObjectMarshal.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.HighPerformance.Helpers; + +/// +/// Helpers for working with instances. +/// +public static class ObjectMarshal +{ + /// + /// Calculates the byte offset to a specific field within a given . + /// + /// The type of field being referenced. + /// The input hosting the target field. + /// A reference to a target field of type within . + /// + /// The value representing the offset to the target field from the start of the object data + /// for the parameter . The offset is in relation to the first usable byte after the method table. + /// + /// The input parameters are not validated, and it's responsibility of the caller to ensure that + /// the reference is actually pointing to a memory location within . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IntPtr DangerousGetObjectDataByteOffset(object obj, ref T data) + { + RawObjectData? rawObj = Unsafe.As(obj)!; + ref byte r0 = ref rawObj.Data; + ref byte r1 = ref Unsafe.As(ref data); + + return Unsafe.ByteOffset(ref r0, ref r1); + } + + /// + /// Gets a reference to data within a given at a specified offset. + /// + /// The type of reference to retrieve. + /// The input hosting the target field. + /// The input byte offset for the reference to retrieve. + /// A reference at a specified offset within . + /// + /// None of the input arguments is validated, and it is responsibility of the caller to ensure they are valid. + /// In particular, using an invalid offset might cause the retrieved reference to be misaligned with the + /// desired data, which would break the type system. Or, if the offset causes the retrieved reference to point + /// to a memory location outside of the input instance, that might lead to runtime crashes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T DangerousGetObjectDataReferenceAt(object obj, IntPtr offset) + { + RawObjectData? rawObj = Unsafe.As(obj)!; + ref byte r0 = ref rawObj.Data; + ref byte r1 = ref Unsafe.AddByteOffset(ref r0, offset); + ref T r2 = ref Unsafe.As(ref r1); + + return ref r2; + } + + // Description adapted from CoreCLR, see: + // https://source.dot.net/#System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs,301. + // CLR objects are laid out in memory as follows: + // [ sync block || pMethodTable || raw data .. ] + // ^ ^ + // | \-- ref Unsafe.As(owner).Data + // \-- object + // The reference to RawObjectData.Data points to the first data byte in the + // target object, skipping over the sync block, method table and string length. + // Even though the description above links to the CoreCLR source, this approach + // can actually work on any .NET runtime, as it doesn't rely on a specific memory + // layout. Even if some 3rd party .NET runtime had some additional fields in the + // object header, before the field being referenced, the returned offset would still + // be valid when used on instances of that particular type, as it's only being + // used as a relative offset from the location pointed by the object reference. + [StructLayout(LayoutKind.Explicit)] + private sealed class RawObjectData + { + [FieldOffset(0)] + public byte Data; + } + + /// + /// Tries to get a boxed value from an input instance. + /// + /// The type of value to try to unbox. + /// The input instance to check. + /// The resulting value, if was in fact a boxed value. + /// if a value was retrieved correctly, otherwise. + /// + /// This extension behaves just like the following method: + /// + /// public static bool TryUnbox<T>(object obj, out T value) + /// { + /// if (obj is T) + /// { + /// value = (T)obj; + /// + /// return true; + /// } + /// + /// value = default; + /// + /// return false; + /// } + /// + /// But in a more efficient way, and with the ability to also assign the unboxed value + /// directly on an existing T variable, which is not possible with the code above. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryUnbox(this object obj, out T value) + where T : struct + { + if (obj.GetType() == typeof(T)) + { + value = Unsafe.Unbox(obj); + + return true; + } + + value = default; + + return false; + } + + /// + /// Unboxes a value from an input instance. + /// + /// The type of value to unbox. + /// The input instance, representing a boxed value. + /// The value boxed in . + /// Thrown when is not of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T DangerousUnbox(object obj) + where T : struct + { + return ref Unsafe.Unbox(obj); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.AppendSpanFormattable.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.AppendSpanFormattable.cs new file mode 100644 index 000000000..14446f634 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.AppendSpanFormattable.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// +// +// Derived from code Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See https://github.com/dotnet/runtime/blob/c1049390d5b33483203f058b0e1457d2a1f62bf4/src/libraries/Common/src/System/Text/ValueStringBuilder.cs +// + +#pragma warning disable // Currently this is a straight copy of the original code, so we disable warnings to avoid diagnostic problems. + +#if NET8_0_OR_GREATER + +namespace Corvus.HighPerformance; + +public ref partial struct ValueStringBuilder +{ + public void AppendSpanFormattable(T value, string? format = null, IFormatProvider? provider = null) where T : ISpanFormattable + { + if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider)) + { + _pos += charsWritten; + } + else + { + Append(value.ToString(format, provider)); + } + } +} + +#endif \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.Replace.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.Replace.cs new file mode 100644 index 000000000..414513b4c --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.Replace.cs @@ -0,0 +1,165 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// +// +// Derived from code Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See https://github.com/dotnet/runtime/blob/c1049390d5b33483203f058b0e1457d2a1f62bf4/src/libraries/Common/src/System/Text/ValueStringBuilder.cs +// + +#pragma warning disable // Currently this is a straight copy of the original code, so we disable warnings to avoid diagnostic problems. + +#if !NET8_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif + +namespace Corvus.HighPerformance; + +public ref partial struct ValueStringBuilder +{ +#if !NET8_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Replace( + string oldValue, + string newValue, + int startIndex, + int count) + => Replace(oldValue.AsSpan(), newValue.AsSpan(), startIndex, count); +#endif + + public void Replace( + ReadOnlySpan oldValue, + ReadOnlySpan newValue, + int startIndex, + int count) + { + if (startIndex < 0 || (startIndex + count) > _pos) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + + if (count == 0) + { + return; + } + + Span rangeBuffer = _chars.Slice(startIndex, count); + + int diff = newValue.Length - oldValue.Length; + if (diff == 0) + { + int matchIndex = rangeBuffer.IndexOf(oldValue); + if (matchIndex == -1) + { + return; + } + + Span remainingBuffer = rangeBuffer; + do + { + remainingBuffer = remainingBuffer[matchIndex..]; + newValue.CopyTo(remainingBuffer); + remainingBuffer = remainingBuffer[oldValue.Length..]; + + matchIndex = remainingBuffer.IndexOf(oldValue); + } while (matchIndex != -1); + + return; + } + + if (diff < 0) + { + int matchIndex = rangeBuffer.IndexOf(oldValue); + if (matchIndex == -1) + { + return; + } + + // We will never need to grow the buffer, but we might need to shift characters + // down. + Span remainingTargetBuffer = _chars[(startIndex + matchIndex)..this._pos]; + Span remainingSourceBuffer = remainingTargetBuffer; + int endOfSearchRangeRelativeToRemainingSourceBuffer = count - matchIndex; + do + { + this._pos += diff; + + newValue.CopyTo(remainingTargetBuffer); + + remainingSourceBuffer = remainingSourceBuffer[oldValue.Length..]; + endOfSearchRangeRelativeToRemainingSourceBuffer -= oldValue.Length; + remainingTargetBuffer = remainingTargetBuffer[newValue.Length..]; + + matchIndex = remainingSourceBuffer[..endOfSearchRangeRelativeToRemainingSourceBuffer] + .IndexOf(oldValue); + + int lengthOfChunkToRelocate = matchIndex == -1 + ? remainingSourceBuffer.Length + : matchIndex; + remainingSourceBuffer[..lengthOfChunkToRelocate].CopyTo(remainingTargetBuffer); + + remainingSourceBuffer = remainingSourceBuffer[lengthOfChunkToRelocate..]; + endOfSearchRangeRelativeToRemainingSourceBuffer -= lengthOfChunkToRelocate; + remainingTargetBuffer = remainingTargetBuffer[lengthOfChunkToRelocate..]; + } while (matchIndex != -1); + + return; + } + else + { + int matchIndex = rangeBuffer.IndexOf(oldValue); + if (matchIndex == -1) + { + return; + } + + Span matchIndexes = stackalloc int[(rangeBuffer.Length + oldValue.Length - 1) / oldValue.Length]; + + int matchCount = 0; + int currentRelocationDistance = 0; + while (matchIndex != -1) + { + matchIndexes[matchCount++] = matchIndex; + currentRelocationDistance += diff; + + int nextIndex = rangeBuffer[(matchIndex + oldValue.Length)..].IndexOf(oldValue); + matchIndex = nextIndex == -1 ? -1 : matchIndex + nextIndex + oldValue.Length; + } + + int relocationRangeEndIndex = this._pos; + + int growBy = (this._pos + currentRelocationDistance) - _chars.Length; + if (growBy > 0) + { + Grow(growBy); + } + this._pos += currentRelocationDistance; + + + // We work from the back of the string when growing to avoid having to + // shift anything more than once. + do + { + matchIndex = matchIndexes[matchCount - 1]; + + int relocationTargetStart = startIndex + matchIndex + oldValue.Length + currentRelocationDistance; + int relocationSourceStart = startIndex + matchIndex + oldValue.Length; + int endOfSearchRangeRelativeToRemainingSourceBuffer = count - matchIndex; + + Span relocationTargetBuffer = _chars[relocationTargetStart..]; + Span sourceBuffer = _chars[relocationSourceStart..relocationRangeEndIndex]; + + sourceBuffer.CopyTo(relocationTargetBuffer); + + currentRelocationDistance -= diff; + Span replaceTargetBuffer = this._chars.Slice(startIndex + matchIndex + currentRelocationDistance); + newValue.CopyTo(replaceTargetBuffer); + + relocationRangeEndIndex = matchIndex + startIndex; + matchIndex = rangeBuffer[..matchIndex].LastIndexOf(oldValue); + + matchCount -= 1; + } while (matchCount > 0); + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.cs new file mode 100644 index 000000000..2472185a0 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.HighPerformance/ValueStringBuilder.cs @@ -0,0 +1,413 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// +// +// Derived from code Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See https://github.com/dotnet/runtime/blob/c1049390d5b33483203f058b0e1457d2a1f62bf4/src/libraries/Common/src/System/Text/ValueStringBuilder.cs +// + +#pragma warning disable // Currently this is a straight copy of the original code, so we disable warnings to avoid diagnostic problems. + +using System.Buffers; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Corvus.HighPerformance; + +public ref partial struct ValueStringBuilder +{ + private char[]? _arrayToReturnToPool; + private Span _chars; + private int _pos; + + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } + + public ValueStringBuilder(int initialCapacity) + { + _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _chars = _arrayToReturnToPool; + _pos = 0; + } + + public int Length + { + get + { + if (_pos < 0) { throw new ObjectDisposedException("Either Dispose or GetRentedBuffer or GetRentedBufferAndLength has already been called"); } + return _pos; + } + + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= _chars.Length); + _pos = value; + } + } + + public int Capacity => _chars.Length; + + /// + /// Return the underlying rented buffer, if any, and the length of the string. This also + /// disposes the instance. + /// + /// + /// + /// Once you have retrieved this, you must not make any further use of + /// . You should call + /// once you no longer require the buffer. + /// + /// + public (char[]? Buffer, int Length) GetRentedBufferAndLengthAndDispose() + { + char[]? result = _arrayToReturnToPool; + int length = Length; + SetToDisposed(); + return (result, length); + } + + /// + /// Returns the buffer retrieved from . + /// + /// The buffer to return. + public static void ReturnRentedBuffer(char[]? buffer) + { + if (buffer is char[] b) + { + ArrayPool.Shared.Return(b); + } + } + + public void EnsureCapacity(int capacity) + { + // This is not expected to be called this with negative capacity + Debug.Assert(capacity >= 0); + + // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception. + if ((uint)capacity > (uint)_chars.Length) + Grow(capacity - _pos); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)" + /// + public ref char GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_chars); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null char after + public ref char GetPinnableReference(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return ref MemoryMarshal.GetReference(_chars); + } + + public ref char this[int index] + { + get + { + Debug.Assert(index < _pos); + return ref _chars[index]; + } + } + + public string CreateStringAndDispose() + { + string s = _chars.Slice(0, _pos).ToString(); + Dispose(); + return s; + } + + /// Returns the underlying storage of the builder. + public Span RawChars => _chars; + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null char after + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return _chars.Slice(0, _pos); + } + + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); + + public bool TryCopyTo(Span destination, out int charsWritten) + { + if (_chars.Slice(0, _pos).TryCopyTo(destination)) + { + charsWritten = _pos; + Dispose(); + return true; + } + else + { + charsWritten = 0; + Dispose(); + return false; + } + } + + public void Insert(int index, char value, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + _chars.Slice(index, count).Fill(value); + _pos += count; + } + + public void Insert(int index, string? s) + { + if (s == null) + { + return; + } + + int count = s.Length; + + if (_pos > _chars.Length - count) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + s +#if !NETCOREAPP + .AsSpan() +#endif + .CopyTo(_chars.Slice(index)); + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + int pos = _pos; + Span chars = _chars; + if ((uint)pos < (uint)chars.Length) + { + chars[pos] = c; + _pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string? s) + { + if (s == null) + { + return; + } + + int pos = _pos; + if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. + { + _chars[pos] = s[0]; + _pos = pos + 1; + } + else + { + AppendSlow( + s +#if !NETCOREAPP + .AsSpan() +#endif + ); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(int value, string? format = null, IFormatProvider? provider = null) + { +#if NET8_0_OR_GREATER + this.AppendSpanFormattable(value, format, provider); +#else + if (format is not null || provider is not null) + { + Append(value.ToString(format, provider)); + return; + } + + bool isNegative = value < 0; + int length = isNegative ? 1 : 0; + value = Math.Abs(value); + + // Calculate the length of the integer + length += value == 0 ? 1 : (int)Math.Floor(Math.Log10(value) + 1); + if (_pos > _chars.Length - length) + { + Grow(length); + } + + if (isNegative) + { + _chars[_pos] = '-'; + } + + int index = _pos + length - 1; + do + { + _chars[index--] = (char)('0' + (value % 10)); + value /= 10; + } + while (value > 0); + + _pos += length; +#endif + } + + private void AppendSlow(ReadOnlySpan s) + { + int pos = _pos; + if (pos > _chars.Length - s.Length) + { + Grow(s.Length); + } + + s.CopyTo(_chars.Slice(pos)); + _pos += s.Length; + } + + public void Append(char c, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + Span dst = _chars.Slice(_pos, count); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + _pos += count; + } + + public void Append(scoped ReadOnlySpan value) + { + int pos = _pos; + if (pos > _chars.Length - value.Length) + { + Grow(value.Length); + } + + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + int origPos = _pos; + if (origPos > _chars.Length - length) + { + Grow(length); + } + + _pos = origPos + length; + return _chars.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) + { + Grow(1); + Append(c); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int additionalCapacityBeyondPos) + { + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + + const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength + + // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try + // to double the size if possible, bounding the doubling to not go beyond the max array length. + int newCapacity = (int)Math.Max( + (uint)(_pos + additionalCapacityBeyondPos), + Math.Min((uint)_chars.Length * 2, ArrayMaxLength)); + + // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. + // This could also go negative if the actual required length wraps around. + char[] poolArray = ArrayPool.Shared.Rent(newCapacity); + + _chars.Slice(0, _pos).CopyTo(poolArray); + + char[]? toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + char[]? toReturn = _arrayToReturnToPool; + SetToDisposed(); + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + /// + /// Puts the object into a state preventing accidental continued use an a pool already returned + /// to an array, or attempting to retrieve information from the object after it has either + /// been disposed, or had its buffer returned to the pool as a result of calling + /// . + /// + private void SetToDisposed() + { + this = default(ValueStringBuilder) with { _pos = -1 }; + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.Json.SourceGeneratorTools.csproj b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.Json.SourceGeneratorTools.csproj new file mode 100644 index 000000000..1106b924e --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.Json.SourceGeneratorTools.csproj @@ -0,0 +1,151 @@ + + + + + netstandard2.0 + 12.0 + enable + $(DefineConstants);BUILDING_SOURCE_GENERATOR + True + $(NoWarn);nullable;IDE0007;NU5128;RS2008;SA0001;SA1633;SA1101;SA1201;SA1202;SA1204;SA1401;SA1413;SA1512;SA1629 + + + + Apache-2.0 + Tools to assist people building .NET Source Generators with Corvus.JsonSchema. + + + + + + + + + + + + + + + Corvus.Json.ExtendedTypes\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.JsonReference\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.CSharp\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.201909\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.202012\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.4\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.6\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.7\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.CorvusVocabulary\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.OpenApi30\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.Draft201909\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.Draft202012\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.Draft4\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.Draft6\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.Draft7\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.OpenApi30\%(RecursiveDir)\%(Filename)%(Extension) + + + Corvus.Json.CodeGeneration.OpenApi31\%(RecursiveDir)\%(Filename)%(Extension) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/IUriTemplateParser.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/IUriTemplateParser.cs new file mode 100644 index 000000000..af140c122 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/IUriTemplateParser.cs @@ -0,0 +1,192 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Diagnostics.CodeAnalysis; + +namespace Corvus.UriTemplates; + +/// +/// A callback when a parameter is found, in which the match is identified with a . +/// +/// The type of the state to pass. +/// Whether to reset the parameters that we have seen so far. +/// The name of the parameter. +/// The string representation of the parameter. +/// The state to pass. +public delegate void ParameterCallback(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref TState state); + +/// +/// A callback when a parameter is found, in which the match is identified with a . +/// +/// The type of the state to pass. +/// Whether to reset the parameters that we have seen so far. +/// Identifies the parameter name. +/// The range in the input URI string at which the parameter was found. +/// The state to pass. +public delegate void ParameterCallbackWithRange(bool reset, ParameterName nameHandle, Range valueRange, ref TState state); + +/// +/// The interface implemented by an URI parser. +/// +public interface IUriTemplateParser +{ + /// + /// Determines if the UriTemplate matches the given URI. + /// + /// The URI to match. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// if the template is a match for the URI. + bool IsMatch(in ReadOnlySpan uri, bool requiresRootedMatch = false); + + /// + /// Parses the given URI, calling your parameter callback for each named parameter discovered. + /// + /// The type of the state to pass. + /// The URI to parse. + /// Called by the parser for each parameter that is discovered. + /// The state to pass to the callback. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// if the uri was successfully parsed, otherwise false. + /// + /// + /// This is a low-allocation operation, but you should take care with your implementation of your + /// if you wish to minimize allocation in your call tree. + /// + /// + /// The parameter callbacks occur as the parameters are matched. If the parse operation ultimately fails, + /// those parameters are invalid, and should be disregarded. + /// + /// + bool ParseUri(in ReadOnlySpan uri, ParameterCallback parameterCallback, ref TState state, bool requiresRootedMatch = false); + + /// + /// Parses the given URI, calling your parameter callback for each named parameter discovered. + /// + /// The type of the state to pass. + /// The URI to parse. + /// Called by the parser for each parameter that is discovered. + /// The state to pass to the callback. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// if the uri was successfully parsed, otherwise false. + /// + /// + /// This is a low-allocation operation, but you should take care with your implementation of your + /// if you wish to minimize allocation in your call tree. + /// + /// + /// The parameter callbacks occur as the parameters are matched. If the parse operation ultimately fails, + /// those parameters are invalid, and should be disregarded. + /// + /// + /// + /// This method was added in 1.3, so libraries that depend on an older version, and which implement this + /// interface will not have this method available. In most cases, the implementation of this interface + /// will be supplied by this library, and so all methods will be available, but it is virtual to support + /// the rare case where someone has implemented their own version against an older version of the library. + /// This exception will be thrown if that is the case. + /// +#if NET8_0_OR_GREATER + virtual bool ParseUri(in ReadOnlySpan uri, ParameterCallbackWithRange parameterCallback, ref TState state, bool requiresRootedMatch = false) => + throw new NotImplementedException(); +#else + bool ParseUri(in ReadOnlySpan uri, ParameterCallbackWithRange parameterCallback, ref TState state, bool requiresRootedMatch = false); +#endif + +#if !NET8_0_OR_GREATER + /// + /// Determines if the UriTemplate matches the given URI. + /// + /// The URI to match. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// if the template is a match for the URI. + public bool IsMatch(string uri, bool requiresRootedMatch = false); + + /// + /// Parses the given URI, calling your parameter callback for each named parameter discovered. + /// + /// The type of the state to pass. + /// The URI to parse. + /// Called by the parser for each parameter that is discovered. + /// The state to pass to the callback. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// if the uri was successfully parsed, otherwise false. + /// + /// + /// This is a low-allocation operation, but you should take care with your implementation of your + /// if you wish to minimize allocation in your call tree. + /// + /// + /// The parameter callbacks occur as the parameters are matched. If the parse operation ultimately fails, + /// those parameters are invalid, and should be disregarded. + /// + /// + bool ParseUri(string uri, ParameterCallback parameterCallback, ref TState state, bool requiresRootedMatch = false); + + /// + /// Parses the given URI, calling your parameter callback for each named parameter discovered. + /// + /// The type of the state to pass. + /// The URI to parse. + /// Called by the parser for each parameter that is discovered. + /// The state to pass to the callback. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// if the uri was successfully parsed, otherwise false. + /// + /// + /// This is a low-allocation operation, but you should take care with your implementation of your + /// if you wish to minimize allocation in your call tree. + /// + /// + /// The parameter callbacks occur as the parameters are matched. If the parse operation ultimately fails, + /// those parameters are invalid, and should be disregarded. + /// + /// + bool ParseUri(string uri, ParameterCallbackWithRange parameterCallback, ref TState state, bool requiresRootedMatch = false); +#endif +} + +/// +/// Provides access to a parameter name. +/// +public readonly struct ParameterName : IEquatable +{ + private readonly string escapedUriTemplate; + private readonly Range range; + + /// + /// Creates a . + /// + /// The escaped URI template containing the parameter name. + /// Range in the escaped URI template containing the parameter name. + internal ParameterName(string escapedUriTemplate, Range range) + { + this.escapedUriTemplate = escapedUriTemplate; + this.range = range; + } + + /// + /// Gets the parameter name. + /// + public ReadOnlySpan Span => this.escapedUriTemplate.AsSpan()[this.range]; + +#pragma warning disable IDE0024, CS1591 // Overzealous documentation warnings. + public static bool operator ==(ParameterName left, ParameterName right) => left.Equals(right); + + public static bool operator !=(ParameterName left, ParameterName right) => !left.Equals(right); +#pragma warning restore IDE0024, CS1591 + + /// + public bool Equals(ParameterName other) => this.escapedUriTemplate == other.escapedUriTemplate && this.range.Equals(other.range); + + /// + public override bool Equals([NotNullWhen(true)] object? obj) => obj is ParameterName other && this.Equals(other); + + /// + public override int GetHashCode() => +#if NET8_0_OR_GREATER + string.GetHashCode(this.Span); +#else + this.Span.ToString().GetHashCode(); +#endif +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/Internal/StringBuilderPool.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/Internal/StringBuilderPool.cs new file mode 100644 index 000000000..545318c8a --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/Internal/StringBuilderPool.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Text; +using Microsoft.Extensions.ObjectPool; + +namespace Corvus.UriTemplates.Internal; + +/// +/// A provider. +/// +internal static class StringBuilderPool +{ + /// + /// Gets a shared pool. + /// + public static readonly DefaultObjectPool Shared = new(new StringBuilderPooledObjectPolicy()); +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/MatchWithVerb.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/MatchWithVerb.cs new file mode 100644 index 000000000..324ee191c --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/MatchWithVerb.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates; + +/// +/// Factory for . +/// +public static class MatchWithVerb +{ + /// + /// Gets a new builder. + /// + /// The type of the match. + /// An instance of a builder for a verb matcher. + public static MatchWithVerb.Builder CreateBuilder() => new(); + + /// + /// Gets a new builder. + /// + /// The type of the match. + /// The initial capacity of the dicionary. + /// An instance of a builder for a verb matcher. + public static MatchWithVerb.Builder CreateBuilder(int initialCapacity) => new(initialCapacity); +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/MatchWithVerb{TMatch}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/MatchWithVerb{TMatch}.cs new file mode 100644 index 000000000..51e20427e --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/MatchWithVerb{TMatch}.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Collections.Frozen; +using System.Diagnostics.CodeAnalysis; + +namespace Corvus.UriTemplates; + +/// +/// A match result for with an additional verb. +/// +/// The type of the match result. +public sealed class MatchWithVerb +{ + private readonly FrozenDictionary verbsToMatches; + + private MatchWithVerb(FrozenDictionary verbsToMatches) + { + this.verbsToMatches = verbsToMatches; + } + + /// + /// Try to match the given verb. + /// + /// The verb to match. + /// The match for the verb. + /// if the verb was a match, otherwise false. + public bool TryMatch(string verb, [MaybeNullWhen(false)] out TMatch match) + { + return this.verbsToMatches.TryGetValue(verb, out match); + } + + /// + /// Try to match the given verb. + /// + /// The verb to match. + /// The match for the verb. + /// if the verb was a match, otherwise false. + public bool TryMatch(ReadOnlySpan verb, [MaybeNullWhen(false)] out TMatch match) + { + foreach (KeyValuePair kvp in this.verbsToMatches) + { +#if NET8_0_OR_GREATER + if (verb.Equals(kvp.Key, StringComparison.Ordinal)) +#else + if (verb.Equals(kvp.Key.AsSpan(), StringComparison.Ordinal)) +#endif + { + match = kvp.Value; + return true; + } + } + + match = default; + return false; + } + + /// + /// A builder for a verb matcher. + /// + public sealed class Builder + { + private readonly Dictionary verbsToMatches; + + /// + /// Initializes a new instance of the class. + /// + internal Builder() + { + this.verbsToMatches = new Dictionary(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial capacity for the matcher. + internal Builder(int initialCapacity) + { + this.verbsToMatches = new Dictionary(initialCapacity); + } + + /// + /// Add a verb to the matcher. + /// + /// The verb to add. + /// The match. + public void Add(string verb, TMatch match) + { + this.verbsToMatches.Add(verb, match); + } + + /// + /// Convert the builder to an instance of a matcher. + /// + /// The instance of the matcher created from the builder. + public MatchWithVerb ToMatchWithVerb() + { + return new MatchWithVerb(this.verbsToMatches.ToFrozenDictionary()); + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterByNameAndRangeCache.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterByNameAndRangeCache.cs new file mode 100644 index 000000000..6252b6ec6 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterByNameAndRangeCache.cs @@ -0,0 +1,237 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Buffers; +using System.Diagnostics.CodeAnalysis; + +using CommunityToolkit.HighPerformance; + +namespace Corvus.UriTemplates; + +/// +/// A cache for parameters extracted from a URI template. +/// +internal struct ParameterByNameAndRangeCache +{ + private readonly int bufferIncrement; + private ParameterValue[] items; + private int written; + private bool rented = false; + + private ParameterByNameAndRangeCache(int initialCapacity) + { + this.bufferIncrement = initialCapacity; + this.items = ArrayPool.Shared.Rent(initialCapacity); + this.written = 0; + this.rented = true; + } + + /// + /// Enumerate the parameters in the parser. + /// + /// The type of the state for the callback. + /// The parser to use. + /// The uri to parse. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// The callback to receive the enumerated parameters. + /// The state for the callback. + /// if the parser was successful, otherwise . + internal static bool EnumerateParameters(IUriTemplateParser parser, ReadOnlySpan uri, int initialCapacity, EnumerateParametersCallbackWithRange callback, ref TState state) + { + ParameterByNameAndRangeCache cache = Rent(initialCapacity); + try + { + if (parser.ParseUri(uri, HandleParameters, ref cache)) + { + cache.EnumerateParameters(callback, ref state); + return true; + } + } + finally + { + cache.Return(); + } + + return false; + } + + /// + /// Gets the parameters from the URI template. + /// + /// The parser to use. + /// The uri to parse. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// A . + /// if parsing succeeded. + internal static bool TryGetUriTemplateParameters( + IUriTemplateParser parser, + ReadOnlySpan uri, + int initialCapacity, + bool requiresRootedMatch, + [NotNullWhen(true)] out UriTemplateParameters? templateParameters) + { + ParameterByNameAndRangeCache cache = Rent(initialCapacity); + + try + { + if (parser.ParseUri(uri, HandleParameters, ref cache, requiresRootedMatch)) + { + templateParameters = cache.Detach(); + return true; + } + + templateParameters = null; + return false; + } + finally + { + cache.Return(); + } + } + + /// + /// Rent an instance of a parameter cache. + /// + /// The initial capacity of the cache. + /// An instance of a parameter cache. + /// When you have finished with the cache, call to relinquish any internal resources. + internal static ParameterByNameAndRangeCache Rent(int initialCapacity) + { + return new(initialCapacity); + } + + /// + /// Frees a array either as part of normal usage, or + /// when it was returned by and the new owner is done with it. + /// + /// The rented array to return. + internal static void Return(ParameterValue[] items) + { + ArrayPool.Shared.Return(items); + } + + /// + /// Tries to match the URI using the parser. + /// + /// The uri template parser with which to match. + /// The URI to match. + /// if the uri matches the template. + /// + /// The parameter cache will contain the matched parameters if the parser matched successfully. + /// + internal bool TryMatch(IUriTemplateParser parser, ReadOnlySpan uri) + { + return parser.ParseUri(uri, HandleParameters, ref this); + } + + /// + /// Try to get a parameter from the cache. + /// + /// The URI template from which this cache was built. + /// The name of the parameter. + /// The range of the parameter. + /// if the parameter exists, otherwise false. + internal readonly bool TryGetParameter(ReadOnlySpan uriTemplate, ReadOnlySpan name, out Range range) + { + for (int i = 0; i < this.written; ++i) + { + ref ParameterValue item = ref this.items[i]; + if (item.Name.Span.Equals(name, StringComparison.Ordinal)) + { + range = item.ValueRange; + return true; + } + } + + range = default; + return false; + } + + /// + /// Reset the items written. + /// + internal void Reset() + { + this.written = 0; + } + + /// + /// Enumerate the parameters in the cache. + /// + /// The type of the state for enumeration. + /// The callback that will be passed the parameters to enumerate. + /// The initial state. + internal readonly void EnumerateParameters(EnumerateParametersCallbackWithRange callback, ref TState state) + { + for (int i = 0; i < this.written; ++i) + { + ref ParameterValue item = ref this.items[i]; + callback(item.Name, item.ValueRange, ref state); + } + } + + /// + /// Return the resources used by the cache. + /// + internal void Return() + { + if (this.rented) + { + Return(this.items); + this.rented = false; + } + } + + /// + /// A parameter handler for . + /// + /// Indicates whether to reset the parameter cache, ignoring any parameters that have been seen. + /// The name of the parameter. + /// The range of the parameter value in the URI being matches to the URI template. + /// The parameter cache. + /// Pass this to , as the callback. + private static void HandleParameters(bool reset, ParameterName name, Range range, ref ParameterByNameAndRangeCache state) + { + if (!reset) + { + state.Add(name, range); + } + else + { + state.Reset(); + } + } + + /// + /// Hands the ownership of the array over to a newly-created + /// . + /// + /// + /// The that now owns the cached data. + /// + private UriTemplateParameters Detach() + { + this.rented = false; + return new UriTemplateParameters(this.items); + } + + /// + /// Add a parameter to the cache. + /// + /// The name of the parameter to add. + /// The range of the parameter value in the URI being matches to the URI template. + private void Add(ParameterName name, Range range) + { + if (this.written == this.items.Length) + { + ArrayPool.Shared.Resize(ref this.items, this.items.Length + this.bufferIncrement); + } + + this.items[this.written] = new(name, range); + this.written++; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterCache.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterCache.cs new file mode 100644 index 000000000..c0f390ced --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterCache.cs @@ -0,0 +1,206 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Buffers; +using CommunityToolkit.HighPerformance; + +namespace Corvus.UriTemplates; + +/// +/// A cache for parameters extracted from a URI template. +/// +internal struct ParameterCache +{ + private readonly int bufferIncrement; + private CacheEntry[] items; + private int written; + private bool rented = false; + + private ParameterCache(int initialCapacity) + { + this.bufferIncrement = initialCapacity; + this.items = ArrayPool.Shared.Rent(initialCapacity); + this.written = 0; + this.rented = true; + } + + /// + /// Enumerate the parameters in the parser. + /// + /// The type of the state for the callback. + /// The parser to use. + /// The uri to parse. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// The callback to receive the enumerated parameters. + /// The state for the callback. + /// if the parser was successful, otherwise . + public static bool EnumerateParameters(IUriTemplateParser parser, ReadOnlySpan uri, int initialCapacity, EnumerateParametersCallback callback, ref TState state) + { + ParameterCache cache = Rent(initialCapacity); + try + { + if (parser.ParseUri(uri, HandleParameters, ref cache)) + { + cache.EnumerateParameters(callback, ref state); + return true; + } + } + finally + { + cache.Return(); + } + + return false; + } + + /// + /// Rent an instance of a parameter cache. + /// + /// The initial capacity of the cache. + /// An instance of a parameter cache. + /// When you have finished with the cache, call to relinquish any internal resources. + internal static ParameterCache Rent(int initialCapacity) + { + return new(initialCapacity); + } + + /// + /// Tries to match the URI using the parser. + /// + /// The uri template parser with which to match. + /// The URI to match. + /// if the uri matches the template. + /// + /// The parameter cache will contain the matched parameters if the parser matched successfully. + /// + internal bool TryMatch(IUriTemplateParser parser, ReadOnlySpan uri) + { + return parser.ParseUri(uri, HandleParameters, ref this); + } + + /// + /// Try to get a parameter from the cache. + /// + /// The name of the parameter. + /// The value of the parameter. + /// if the parameter exists, otherwise false. + internal readonly bool TryGetParameter(ReadOnlySpan name, out ReadOnlySpan value) + { + for (int i = 0; i < this.written; ++i) + { + ref CacheEntry item = ref this.items[i]; + if (item.Name.Equals(name, StringComparison.Ordinal)) + { + value = item.Value; + return true; + } + } + + value = default; + return false; + } + + /// + /// Reset the items written. + /// + internal void Reset() + { + this.ResetItems(); + this.written = 0; + } + + /// + /// Enumerate the parameters in the cache. + /// + /// The type of the state for enumeration. + /// The callback that will be passed the parameters to enumerate. + /// The initial state. + internal readonly void EnumerateParameters(EnumerateParametersCallback callback, ref TState state) + { + for (int i = 0; i < this.written; ++i) + { + ref CacheEntry item = ref this.items[i]; + callback(item.Name, item.Value, ref state); + } + } + + /// + /// Return the resources used by the cache. + /// + internal void Return() + { + if (this.rented) + { + this.ResetItems(); + ArrayPool.Shared.Return(this.items); + this.rented = false; + } + } + + /// + /// A parameter handler for . + /// + /// Indicates whether to reset the parameter cache, ignoring any parameters that have been seen. + /// The name of the parameter. + /// The value of the parameter. + /// The parameter cache. + /// Pass this to , as the callback. + private static void HandleParameters(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref ParameterCache state) + { + if (!reset) + { + state.Add(name, value); + } + else + { + state.Reset(); + } + } + + /// + /// Add a parameter to the cache. + /// + /// The name of the parameter to add. + /// The value of the parameter to add. + private void Add(ReadOnlySpan name, ReadOnlySpan value) + { +#pragma warning disable SA1010 // Opening square brackets should be spaced correctly - analyzers not yet up to date + char[] entryArray = name.Length + value.Length > 0 ? ArrayPool.Shared.Rent(name.Length + value.Length) : []; +#pragma warning restore SA1010 // Opening square brackets should be spaced correctly + name.CopyTo(entryArray); + value.CopyTo(entryArray.AsSpan(name.Length)); + + if (this.written == this.items.Length) + { + ArrayPool.Shared.Resize(ref this.items, this.items.Length + this.bufferIncrement); + } + + this.items[this.written] = new(entryArray, name.Length, value.Length); + this.written++; + } + + private readonly void ResetItems() + { + for (int i = 0; i < this.written; ++i) + { + this.items[i].Return(); + } + } + + private readonly struct CacheEntry(char[] entry, int nameLength, int valueLength) + { + public ReadOnlySpan Name => nameLength > 0 ? entry.AsSpan(0, nameLength) : default; + + public ReadOnlySpan Value => valueLength > 0 ? entry.AsSpan(nameLength, valueLength) : default; + + public void Return() + { + if (entry.Length > 0) + { + ArrayPool.Shared.Return(entry); + } + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterValue.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterValue.cs new file mode 100644 index 000000000..5e7b78516 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ParameterValue.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates; + +/// +/// A parameter value. +/// +public readonly struct ParameterValue(ParameterName name, Range valueRange) +{ + /// + /// Gets the parameter name. + /// + public ParameterName Name => name; + + /// + /// Gets the range in the input URI string at which the parameter was found. + /// + internal Range ValueRange => valueRange; + + /// + /// Gets the value of the parameter. + /// + /// + /// The URI containing this parameter value. This must be the same URI that was used + /// to parse the parameter. + /// + /// The parameter value. + public ReadOnlySpan GetValue(ReadOnlySpan uri) + { + return uri[this.ValueRange]; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ReadOnlySpanCallback.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ReadOnlySpanCallback.cs new file mode 100644 index 000000000..81a97464e --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/ReadOnlySpanCallback.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates; + +/// +/// A callback for receiving data from a . +/// +/// The span item type. +/// The type of the state to pass. +/// The return type. +/// An argument providing whatever state the callback requires. +/// The span. +/// Whatever information the callback wishes to return. +public delegate TResult ReadOnlySpanCallback(TState state, ReadOnlySpan span); \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/DictionaryTemplateParameterProvider.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/DictionaryTemplateParameterProvider.cs new file mode 100644 index 000000000..bf1000cfd --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/DictionaryTemplateParameterProvider.cs @@ -0,0 +1,292 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +using Corvus.HighPerformance; +using Corvus.UriTemplates.TemplateParameterProviders; + +namespace Corvus.UriTemplates.TavisApi; + +/// +/// Implements a parameter provider over a JsonAny. +/// +internal class DictionaryTemplateParameterProvider : ITemplateParameterProvider> +{ + /// + /// Process the given variable. + /// + /// The specification for the variable. + /// The parameters. + /// The output to which to format the parameter. + /// + /// if the variable was successfully processed, + /// if the parameter was not present, or + /// if the parmeter could not be processed because it was incompatible with the variable specification in the template. + public VariableProcessingState ProcessVariable(ref VariableSpecification variableSpecification, in IDictionary parameters, ref ValueStringBuilder output) + { + string varName = variableSpecification.VarName.ToString(); + if (!parameters.ContainsKey(varName) + || parameters[varName] == null + || (TryGetList(parameters[varName], out IList? l) && l.Count == 0) + || (TryGetDictionary(parameters[varName], out IDictionary? d) && d.Count == 0)) + { + return VariableProcessingState.NotProcessed; + } + + if (variableSpecification.First) + { + if (variableSpecification.OperatorInfo.First != '\0') + { + output.Append(variableSpecification.OperatorInfo.First); + } + } + else + { + output.Append(variableSpecification.OperatorInfo.Separator); + } + + object? value = parameters[varName]; + + if (TryGetList(value, out IList? list)) + { + if (variableSpecification.OperatorInfo.Named && !variableSpecification.Explode) //// exploding will prefix with list name + { + AppendName(ref output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, list.Count == 0); + } + + AppendArray(ref output, variableSpecification.OperatorInfo, variableSpecification.Explode, variableSpecification.VarName, list); + } + else if (TryGetDictionary(value, out IDictionary? dictionary)) + { + if (variableSpecification.PrefixLength != 0) + { + return VariableProcessingState.Failure; + } + + if (variableSpecification.OperatorInfo.Named && !variableSpecification.Explode) //// exploding will prefix with list name + { + AppendName(ref output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, dictionary.Count == 0); + } + + AppendObject(ref output, variableSpecification.OperatorInfo, variableSpecification.Explode, dictionary); + } + else if (value is string stringValue) + { + if (variableSpecification.OperatorInfo.Named) + { + AppendNameAndStringValue(ref output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, stringValue, variableSpecification.PrefixLength, variableSpecification.OperatorInfo.AllowReserved); + } + else + { + AppendValue(ref output, stringValue, variableSpecification.PrefixLength, variableSpecification.OperatorInfo.AllowReserved); + } + } + else + { + // Fallback to string + string? fallbackStringValue = value?.ToString(); + if (variableSpecification.OperatorInfo.Named) + { + AppendName(ref output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, string.IsNullOrEmpty(fallbackStringValue)); + } + + AppendValue(ref output, fallbackStringValue, variableSpecification.PrefixLength, variableSpecification.OperatorInfo.AllowReserved); + } + + return VariableProcessingState.Success; + } + + private static bool TryGetDictionary(object? value, [NotNullWhen(true)] out IDictionary? dictionary) + { + if (value is IDictionary result) + { + dictionary = result; + return true; + } + + dictionary = null; + return false; + } + + private static bool TryGetList(object? value, [NotNullWhen(true)] out IList? list) + { + var result = value as IList; + if (result == null && value is IEnumerable enumerable) + { + result = enumerable.ToList(); + } + + list = result; + return result != null; + } + + /// + /// Append an array to the result. + /// + /// The output buffer. + /// The operator info. + /// Whether to explode the array. + /// The variable name. + /// The array to add. + private static void AppendArray(ref ValueStringBuilder output, in OperatorInfo op, bool explode, ReadOnlySpan variable, in IList array) + { + bool isFirst = true; + foreach (object item in array) + { + if (!isFirst) + { + output.Append(explode ? op.Separator : ','); + } + else + { + isFirst = false; + } + + if (op.Named && explode) + { + output.Append(variable); + output.Append('='); + } + + AppendValue(ref output, item.ToString(), 0, op.AllowReserved); + } + } + + /// + /// Append an object to the output. + /// + /// The output buffer. + /// The operator info. + /// Whether to explode the object. + /// The object instance to append. + private static void AppendObject(ref ValueStringBuilder output, in OperatorInfo op, bool explode, in IDictionary instance) + { + bool isFirst = true; + foreach (KeyValuePair value in instance) + { + if (!isFirst) + { + if (explode) + { + output.Append(op.Separator); + } + else + { + output.Append(','); + } + } + else + { + isFirst = false; + } + + WriteEncodedPropertyName(value.Key.AsSpan(), ref output, op.AllowReserved, out bool decoded); + + if (explode) + { + output.Append('='); + } + else + { + output.Append(','); + } + + AppendValue(ref output, value.Value, 0, op.AllowReserved); + } + } + + /// + /// Encoded and write the property name to the output. + /// + /// The name to write. + /// The output buffer. + /// A value indicating whether to allow reserved characters. + /// Whether the value was written successfully. + /// if the value was written successfully. + private static bool WriteEncodedPropertyName(ReadOnlySpan name, ref ValueStringBuilder output, bool allowReserved, out bool result) + { + TemplateParameterProvider.Encode(ref output, name, allowReserved); + result = true; + return true; + } + + /// + /// Append a variable to the result. + /// + /// The output buffer to which the URI template is written. + /// The variable name. + /// The string to apply if the value is empty. + /// True if the value is empty. + private static void AppendName(ref ValueStringBuilder output, ReadOnlySpan variable, string ifEmpty, bool valueIsEmpty) + { + output.Append(variable); + + if (valueIsEmpty) + { + output.Append(ifEmpty); + } + else + { + output.Append('='); + } + } + + /// + /// Appends a value to the result. + /// + /// The output buffer to which to write the value. + /// The variable name. + /// The string to add if the value is empty. + /// The value to append. + /// The prefix length. + /// Whether to allow reserved characters. + private static void AppendNameAndStringValue(ref ValueStringBuilder output, ReadOnlySpan variable, string ifEmpty, string? value, int prefixLength, bool allowReserved) + { + output.Append(variable); + + ReadOnlySpan span = value.AsSpan(); + + // Write the name separator + if (span.Length == 0) + { + output.Append(ifEmpty); + } + else + { + output.Append('='); + } + + WriteStringValue(ref output, span, prefixLength, allowReserved); + } + + /// + /// Appends a value to the result. + /// + /// The output buffer to which to write the value. + /// The value to append. + /// The prefix length. + /// Whether to allow reserved characters. + private static void AppendValue(ref ValueStringBuilder output, string? value, int prefixLength, bool allowReserved) + { + WriteStringValue(ref output, value.AsSpan(), prefixLength, allowReserved); + } + + private static void WriteStringValue(ref ValueStringBuilder output, ReadOnlySpan span, int prefixLength, bool allowReserved) + { + // Write the value + ReadOnlySpan valueString = span; + + if (prefixLength != 0) + { + if (prefixLength < valueString.Length) + { + valueString = valueString[..prefixLength]; + } + } + + TemplateParameterProvider.Encode(ref output, valueString, allowReserved); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/DictionaryUriTemplateResolver.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/DictionaryUriTemplateResolver.cs new file mode 100644 index 000000000..03ac9feb5 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/DictionaryUriTemplateResolver.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Runtime.CompilerServices; + +using Corvus.HighPerformance; + +namespace Corvus.UriTemplates.TavisApi; + +/// +/// A wrapper around +/// for a . +/// +internal static class DictionaryUriTemplateResolver +{ + private static readonly Dictionary EmptyDictionary = new(); + private static readonly DictionaryTemplateParameterProvider ParameterProvider = new(); + + /// + /// Resolve the template into an output result. + /// + /// The type of the state passed to the callback. + /// The template to resolve. + /// If then partially resolve the result. + /// The parameters to apply to the template. + /// An optional callback which is provided each parameter name as they are discovered. + /// The callback which is provided with the resolved template. + /// The state passed to the callback(s). + /// if the URI matched the template, and the parameters were resolved successfully. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryResolveResult(ReadOnlySpan template, bool resolvePartially, in IDictionary parameters, ParameterNameCallback parameterNameCallback, ResolvedUriTemplateCallback callback, ref TState state) + { + return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, resolvePartially, parameters, callback, parameterNameCallback, ref state); + } + + /// + /// Resolve the template into an output result. + /// + /// The type of the state passed to the callback. + /// The template to resolve. + /// If then partially resolve the result. + /// The parameters to apply to the template. + /// The callback which is provided with the resolved template. + /// The state passed to the callback(s). + /// if the URI matched the template, and the parameters were resolved successfully. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryResolveResult(ReadOnlySpan template, bool resolvePartially, in IDictionary parameters, ResolvedUriTemplateCallback callback, ref TState state) + { + return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, resolvePartially, parameters, callback, null, ref state); + } + + /// + /// Resolve the template into an output result. + /// + /// The template to resolve. + /// The output buffer into which to resolve the template. + /// If then partially resolve the result. + /// The parameters to apply to the template. + /// if the URI matched the template, and the parameters were resolved successfully. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryResolveResult(ReadOnlySpan template, ref ValueStringBuilder output, bool resolvePartially, in IDictionary parameters) + { + object? nullState = default; + return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, ref output, resolvePartially, parameters, null, ref nullState); + } + + /// + /// Get the parameter names from the template. + /// + /// The type of the state for the callback. + /// The template for the callback. + /// The callback provided with the parameter names. + /// The state for the callback. + /// if the URI matched the template, and the parameters were resolved successfully. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetParameterNames(ReadOnlySpan template, ParameterNameCallback callback, ref TState state) + { + return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, true, EmptyDictionary, Nop, callback, ref state); + +#pragma warning disable RCS1163 // Unused parameter. + static void Nop(ReadOnlySpan value, ref TState state) + { +#pragma warning restore RCS1163 // Unused parameter. + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/QueryStringParameterOrder.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/QueryStringParameterOrder.cs new file mode 100644 index 000000000..82b559660 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/QueryStringParameterOrder.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// +// +// Derived from Tavis.UriTemplate https://github.com/tavis-software/Tavis.UriTemplates/blob/master/License.txt +// + +namespace Corvus.UriTemplates.TavisApi; + +/// +/// Determines the parameter ordering in query string parameters. +/// +public enum QueryStringParameterOrder +{ + /// + /// Strict ordering as per the template. + /// + Strict, + + /// + /// Arbitrary ordering. + /// + Any, +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/TemplateMatch.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/TemplateMatch.cs new file mode 100644 index 000000000..b638574e4 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/TemplateMatch.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates.TavisApi; + +/// +/// A template match. +/// +public class TemplateMatch +{ + /// + /// Gets or sets the match key. + /// + public string? Key { get; set; } + + /// + /// Gets or sets the matched template. + /// + public UriTemplate? Template { get; set; } + + /// + /// Gets or sets the matched parameters. + /// + public IDictionary? Parameters { get; set; } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriExtensions.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriExtensions.cs new file mode 100644 index 000000000..94e021147 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriExtensions.cs @@ -0,0 +1,190 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// +// +// Derived from Tavis.UriTemplate https://github.com/tavis-software/Tavis.UriTemplates/blob/master/License.txt +// + +using System.Buffers; + +namespace Corvus.UriTemplates.TavisApi; + +/// +/// A delegate for recieving query string parameters. +/// +/// The type of the state for the callback. +/// The name of the parameter. +/// The value of the parameter. +/// The state for the callback. +public delegate void QueryStringParameterCallback(ReadOnlySpan name, ReadOnlySpan value, ref TState state); + +/// +/// Extension methods for converting a URI into a URI template. +/// +public static partial class UriExtensions +{ + private enum State + { + LookingForName, + LookingForValue, + } + + /// + /// Make a template from a URI, by templatizing the existing query string parameters. + /// + /// The URI for which to make a template. + /// The URI template, with templatized query string, and parameters populated from the values in the query string. + public static UriTemplate MakeTemplate(this Uri uri) + { + Dictionary parameters = uri.GetQueryStringParameters(); + return uri.MakeTemplate((IDictionary)parameters); + } + + /// + /// Make a template from a URI and an ordered set of parameters, removing the query string and fragment, + /// and replacing the query with the parameter names from the dictionary. + /// + /// The base URI. + /// The parameters to apply. + /// The URI template, with templatized query string, and parameters populated from the values provided. + public static UriTemplate MakeTemplate(this Uri uri, IDictionary parameters) + { + string target = uri.GetComponents( + UriComponents.AbsoluteUri + & ~UriComponents.Query + & ~UriComponents.Fragment, + UriFormat.Unescaped); + var template = new UriTemplate(target + "{?" + string.Join(",", parameters.Keys.ToArray()) + "}"); + template.AddParameters(parameters); + + return template; + } + + /// + /// Get the query string parameters from the given URI. + /// + /// The target URI for which to recover the query string parameters. + /// A map of the query string parameters. + public static Dictionary GetQueryStringParameters(this Uri target) + { + Dictionary parameters = new(); + + GetQueryStringParameters(target, AccumulateResults, ref parameters); + + return parameters; + + static void AccumulateResults(ReadOnlySpan name, ReadOnlySpan value, ref Dictionary state) + { + state.Add(name.ToString(), value.ToString()); + } + } + + /// + /// Get the query string parameters from the given URI. + /// + /// The type of the state for the callback. + /// The target URI for which to recover the query string parameters. + /// The callback to receieve the query parameters. + /// The state for the callback. + public static void GetQueryStringParameters(this Uri target, QueryStringParameterCallback callback, ref TState state) + { +#if NET8_0_OR_GREATER + MatchQueryParameters(target.Query, callback, ref state); +#else + MatchQueryParameters(target.Query.AsSpan(), callback, ref state); +#endif + } + + private static bool MatchQueryParameters(ReadOnlySpan query, QueryStringParameterCallback callback, ref TState state) + { + // Skip the initial '?' + int currentIndex = 1; + State currentState = State.LookingForName; + int nameSegmentStart = 1; + int nameSegmentEnd = 1; + int valueSegmentStart = -1; + + while (currentIndex < query.Length) + { + switch (currentState) + { + case State.LookingForName: + if (query[currentIndex] == '=') + { + nameSegmentEnd = currentIndex; + valueSegmentStart = currentIndex + 1; + currentState = State.LookingForValue; + + // That's an empty name + if (nameSegmentStart >= nameSegmentEnd) + { + return false; + } + } + else if (!IsPermittedValueCharacter(query[currentIndex])) + { + return false; + } + + break; + + case State.LookingForValue: + if (query[currentIndex] == '&') + { + ReadOnlySpan name = query[nameSegmentStart..nameSegmentEnd]; + ReadOnlySpan value = valueSegmentStart == currentIndex ? ReadOnlySpan.Empty : query[valueSegmentStart..currentIndex]; + + ExecuteCallback(callback, name, value, ref state); + + currentState = State.LookingForName; + nameSegmentStart = currentIndex + 1; + } + + break; + } + + ++currentIndex; + } + + if (currentState == State.LookingForValue) + { + ExecuteCallback(callback, query[nameSegmentStart..nameSegmentEnd], query[valueSegmentStart..currentIndex], ref state); + } + + return true; + + static void ExecuteCallback(QueryStringParameterCallback callback, ReadOnlySpan name, ReadOnlySpan value, ref TState state) + { + char[]? pooledArray = null; + + Span lowerName = name.Length <= 256 ? + stackalloc char[256] : + (pooledArray = ArrayPool.Shared.Rent(name.Length)); + + try + { + name.ToLowerInvariant(lowerName); + callback(lowerName[..name.Length], value, ref state); + } + finally + { + if (pooledArray is not null) + { + ArrayPool.Shared.Return(pooledArray, true); + } + } + } + } + + private static bool IsPermittedValueCharacter(char v) + { + return + v == '-' || + (v >= 'A' && v <= 'Z') || + (v >= 'a' && v <= 'z') || + (v >= '0' && v <= '9') || + v == '.' || + v == '_' || + v == '~'; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplate.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplate.cs new file mode 100644 index 000000000..6ff8e7863 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplate.cs @@ -0,0 +1,305 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// +// +// Derived from Tavis.UriTemplate https://github.com/tavis-software/Tavis.UriTemplates/blob/master/License.txt +// + +namespace Corvus.UriTemplates.TavisApi; + +/// +/// A UriTemplate conforming (broadly!) to the Tavis API. +/// +public class UriTemplate +{ + private static readonly Uri ComponentBaseUri = new("https://localhost.com", UriKind.Absolute); + private readonly object lockObject = new(); + private readonly string template; + private readonly Dictionary parameters; + private readonly bool resolvePartially; + private IUriTemplateParser? parser; + + /// + /// Initializes a new instance of the class. + /// + /// The URI template. + /// Indicates whether to allow partial resolution. + /// Indicates whether to use case insensitive parameter names. + public UriTemplate(string template, bool resolvePartially = false, bool caseInsensitiveParameterNames = false) + { + this.resolvePartially = resolvePartially; + this.template = template; + this.parameters = caseInsensitiveParameterNames + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(); + } + + /// + /// Create a matching regular expression for the uri template. + /// + /// The uri template for which to get the regular expression. + /// The matching regular expression. + public static string CreateMatchingRegex(string uriTemplate) + { + return UriTemplateRegexBuilder.CreateMatchingRegex(uriTemplate); + } + + /// + /// Create a matching regular expression for the uri template. + /// + /// The uri template for which to get the regular expression. + /// The matching regular expression. + public static string CreateMatchingRegex2(string uriTemplate) + { + return UriTemplateRegexBuilder.CreateMatchingRegex(uriTemplate); + } + + /// + public override string ToString() + { + return this.template; + } + + /// + /// Set a parameter. + /// + /// The name of the parameter to set. + /// The value of the parameter. + public void SetParameter(string name, object? value) + { + this.parameters[name] = value; + } + + /// + /// Clears the parameter with the given name. + /// + /// The name of the parameter to clear. + public void ClearParameter(string name) + { + this.parameters.Remove(name); + } + + /// + /// Set a parameter. + /// + /// The name of the parameter to set. + /// The value of the parameter. + public void SetParameter(string name, string? value) + { + this.parameters[name] = value; + } + + /// + /// Set a parameter. + /// + /// The name of the parameter to set. + /// The value of the parameter. + public void SetParameter(string name, IEnumerable? value) + { + this.parameters[name] = value; + } + + /// + /// Set a parameter. + /// + /// The name of the parameter to set. + /// The value of the parameter. + public void SetParameter(string name, IDictionary? value) + { + this.parameters[name] = value; + } + + /// + /// Get the names of the parameters in the template. + /// + /// The parameters in the template. + public IEnumerable GetParameterNames() + { + List builder = new(); + + DictionaryUriTemplateResolver.TryGetParameterNames(this.template.AsSpan(), AccumulateParameterNames, ref builder); + return builder; + + static void AccumulateParameterNames(ReadOnlySpan name, ref List state) + { + state.Add(name.ToString()); + } + } + + /// + /// Applies the parameters to the template and returns the result. + /// + /// The resulting URI or partially-resolved template. + public string Resolve() + { + string result = string.Empty; + if (!DictionaryUriTemplateResolver.TryResolveResult(this.template.AsSpan(), this.resolvePartially, this.parameters, ResolveToString, ref result)) + { + throw new ArgumentException("Malformed template."); + } + + return result; + + static void ResolveToString(ReadOnlySpan name, ref string state) + { + state = name.ToString(); + } + } + + /// + /// Gets the parameters from the given URI. + /// + /// The URI from which to get the parameters. + /// Whether to apply strict or relaxed query parameter ordering. + /// The parameters decomposed from the Uri. + public IDictionary? GetParameters(Uri uri, QueryStringParameterOrder order = QueryStringParameterOrder.Strict) + { + switch (order) + { + case QueryStringParameterOrder.Strict: + { + IUriTemplateParser? parser = this.parser; + + if (parser == null) + { + parser = UriTemplateParserFactory.CreateParser(this.template); + lock (this.lockObject) + { + this.parser = parser; + } + } + + var parameters = new Dictionary(); + + if (parser.ParseUri(uri.OriginalString.AsSpan(), AddResults, ref parameters)) + { + return parameters; + } + else + { + return null; + } + + static void AddResults(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref Dictionary results) + { + if (reset) + { + results.Clear(); + } + else + { + // Note we are making no attempt to make this low-allocation + results.Add(name.ToString(), Uri.UnescapeDataString(value.ToString())); + } + } + } + + case QueryStringParameterOrder.Any: + { + if (!uri.IsAbsoluteUri) + { + uri = new Uri(ComponentBaseUri, uri); + } + + string uriString = uri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path | UriComponents.Fragment, UriFormat.UriEscaped); + var uriWithoutQuery = new Uri(uriString, UriKind.Absolute); + + IDictionary pathParameters = this.GetParameters(uriWithoutQuery) ?? new Dictionary(this.parameters.Comparer); + + HashSet parameterNames = this.GetParameterNamesHashSet(); + + (HashSet ParameterNames, IDictionary PathParameters) parameterState = (parameterNames, pathParameters); + + uri.GetQueryStringParameters(MatchParameterNames, ref parameterState); + + return pathParameters.Count == 0 ? null : pathParameters; + } + + default: + throw new ArgumentOutOfRangeException(nameof(order), order, null); + } + } + + /// + /// Matches the given URI against the template. + /// + /// The URI to match. + /// Whether query string ordering is strict. + /// if the uri matches the template. + /// The parameter order was not understood. + internal bool IsMatch(Uri uri, QueryStringParameterOrder order = QueryStringParameterOrder.Strict) + { + switch (order) + { + case QueryStringParameterOrder.Strict: + { + IUriTemplateParser? parser = this.parser; + + if (parser == null) + { + parser = UriTemplateParserFactory.CreateParser(this.template); + lock (this.lockObject) + { + this.parser = parser; + } + } + + if (parser.IsMatch(uri.OriginalString.AsSpan())) + { + return true; + } + else + { + return false; + } + } + + case QueryStringParameterOrder.Any: + { + if (!uri.IsAbsoluteUri) + { + uri = new Uri(ComponentBaseUri, uri); + } + + IDictionary pathParameters = new Dictionary(this.parameters.Comparer); + + HashSet parameterNames = this.GetParameterNamesHashSet(); + + (HashSet ParameterNames, IDictionary PathParameters) parameterState = (parameterNames, pathParameters); + + uri.GetQueryStringParameters(MatchParameterNames, ref parameterState); + + return pathParameters.Count != 0; + } + + default: + throw new ArgumentOutOfRangeException(nameof(order), order, null); + } + } + + /// + /// Get the names of the parameters in the template. + /// + /// The parameters in the template. + internal HashSet GetParameterNamesHashSet() + { + HashSet builder = new(); + + DictionaryUriTemplateResolver.TryGetParameterNames(this.template.AsSpan(), AccumulateParameterNames, ref builder); + return builder; + + static void AccumulateParameterNames(ReadOnlySpan name, ref HashSet state) + { + state.Add(name.ToString()); + } + } + + private static void MatchParameterNames(ReadOnlySpan name, ReadOnlySpan value, ref (HashSet ParameterNames, IDictionary PathParameters) state) + { + string name1 = name.ToString(); + if (state.ParameterNames.Contains(name1)) + { + state.PathParameters.Add(name1, value.ToString()); + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplateExtensions.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplateExtensions.cs new file mode 100644 index 000000000..9b6d32630 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplateExtensions.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// +// +// Derived from Tavis.UriTemplate https://github.com/tavis-software/Tavis.UriTemplates/blob/master/License.txt +// + +using System.Reflection; + +namespace Corvus.UriTemplates.TavisApi; + +/// +/// Extension methods for URI templates. +/// +public static class UriTemplateExtensions +{ + /// + /// Create a matching regular expression for the uri template. + /// + /// The uri template for which to get the regular expression. + /// The matching regular expression. + public static string CreateMatchingRegex(string uriTemplate) + { + return UriTemplateRegexBuilder.CreateMatchingRegex(uriTemplate); + } + + /// + /// Create a matching regular expression for the uri template. + /// + /// The uri template for which to get the regular expression. + /// The matching regular expression. + public static string CreateMatchingRegex2(string uriTemplate) + { + return UriTemplateRegexBuilder.CreateMatchingRegex(uriTemplate); + } + + /// + /// Add a parameter to a UriTemplate. + /// + /// The template to which to add the parameter. + /// The name of the parameter. + /// The value to add. + /// The updated UriTemplate. + public static UriTemplate AddParameter(this UriTemplate template, string name, object value) + { + template.SetParameter(name, value); + + return template; + } + + /// + /// Adds parameters to a UriTemplate. + /// + /// The template to which to add the parameter. + /// The object whose public instance properties represent the parameters to add. + /// The updated UriTemplate. + public static UriTemplate AddParameters(this UriTemplate template, object parametersObject) + { + if (parametersObject != null) + { + IEnumerable properties = parametersObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + foreach (PropertyInfo propinfo in properties) + { + template.SetParameter(propinfo.Name, propinfo.GetValue(parametersObject, null)); + } + } + + return template; + } + + /// + /// Adds parameters to a UriTemplate. + /// + /// The template to which to add the parameter. + /// The dictionary whose key value pairs represent the parameters to add. + /// The updated UriTemplate. + public static UriTemplate AddParameters(this UriTemplate uriTemplate, IDictionary linkParameters) + { + if (linkParameters != null) + { + foreach (KeyValuePair parameter in linkParameters) + { + uriTemplate.SetParameter(parameter.Key, parameter.Value); + } + } + + return uriTemplate; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplateTable.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplateTable.cs new file mode 100644 index 000000000..bbb8a46f8 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TavisApi/UriTemplateTable.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates.TavisApi; + +/// +/// A Uri Template table. +/// +public class UriTemplateTable +{ + private readonly Dictionary templates = new(); + + /// + /// Get the URI template with the specified key. + /// + /// The key for which to retrieve the URI template. + /// The URI template with the given key, or null if no template is found. + public UriTemplate? this[string key] + { + get + { + if (this.templates.TryGetValue(key, out UriTemplate? value)) + { + return value; + } + else + { + return null; + } + } + } + + /// + /// Adds a URI template to the table. + /// + /// The key in the table. + /// The template to add. + public void Add(string key, UriTemplate template) + { + this.templates.Add(key, template); + } + + /// + /// Match a URL to the table. + /// + /// The URL to match. + /// The (option) query string parameter ordering constraint. + /// The matched template, or null if no template was matched. + public TemplateMatch? Match(Uri url, QueryStringParameterOrder order = QueryStringParameterOrder.Strict) + { + foreach (KeyValuePair template in this.templates) + { + if (template.Value.IsMatch(url, order)) + { + IDictionary? parameters = template.Value.GetParameters(url, order); + if (parameters != null) + { + return new TemplateMatch() { Key = template.Key, Parameters = parameters, Template = template.Value }; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateMatchResult.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateMatchResult.cs new file mode 100644 index 000000000..2e2d68421 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateMatchResult.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates; + +#if NET8_0_OR_GREATER +/// +/// A result from matching a template in a template table. +/// +/// The type of the result. +/// The user-specified result for the match. +/// The URI template parser that matched. +public readonly record struct TemplateMatchResult(T Result, IUriTemplateParser Parser); +#else +/// +/// A result from matching a template in a template table. +/// +/// The type of the result. +public readonly struct TemplateMatchResult +{ + /// + /// Initializes a new instance of the struct. + /// + /// The user-specified result for the match. + /// The URI template parser that matched. + public TemplateMatchResult(T result, IUriTemplateParser parser) + { + this.Result = result; + this.Parser = parser; + } + + /// + /// Gets the match result. + /// + public T Result { get; } + + /// + /// Gets the . + /// + public IUriTemplateParser Parser { get; } +} +#endif \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/ITemplateParameterProvider.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/ITemplateParameterProvider.cs new file mode 100644 index 000000000..8c8eb55b3 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/ITemplateParameterProvider.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using Corvus.HighPerformance; + +namespace Corvus.UriTemplates.TemplateParameterProviders; + +/// +/// Supplies parameters to the . +/// +/// The type of the parameter. +/// +/// This allows us to abstract our parameter provision mechanism to +/// efficiently format parameters into our output. +/// +public interface ITemplateParameterProvider +{ + /// + /// Process the given variable. + /// + /// The specification for the variable. + /// The parameters. + /// The output to which to format the parameter. + /// + /// if the variable was successfully processed, + /// if the parameter was not present, or + /// if the parameter could not be processed because it was incompatible with the variable specification in the template. + VariableProcessingState ProcessVariable(ref VariableSpecification variableSpecification, in TParameterPayload parameters, ref ValueStringBuilder output); +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/OperatorInfo.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/OperatorInfo.cs new file mode 100644 index 000000000..93b834cdc --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/OperatorInfo.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates.TemplateParameterProviders; + +/// +/// Gets the operator info for a variable specification. +/// +public readonly struct OperatorInfo +{ + /// + /// Initializes a new instance of the struct. + /// + /// The defualt value. + /// If this is the first parameter. + /// The separator. + /// If this is a named parameter. + /// The value to use if empty. + /// Whether to allow reserved characters. + public OperatorInfo(bool @default, char first, char separator, bool named, string ifEmpty, bool allowReserved) + { + this.Default = @default; + this.First = first; + this.Separator = separator; + this.Named = named; + this.IfEmpty = ifEmpty; + this.AllowReserved = allowReserved; + } + + /// + /// Gets a value indicating whether this is default. + /// + public bool Default { get; } + + /// + /// Gets the first element. + /// + public char First { get; } + + /// + /// Gets the separator. + /// + public char Separator { get; } + + /// + /// Gets a value indicating whether this is named. + /// + public bool Named { get; } + + /// + /// Gets the string to use if empty. + /// + public string IfEmpty { get; } + + /// + /// Gets a value indicating whether this allows reserved symbols. + /// + public bool AllowReserved { get; } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/TemplateParameterProvider.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/TemplateParameterProvider.cs new file mode 100644 index 000000000..ac776165d --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/TemplateParameterProvider.cs @@ -0,0 +1,118 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Text; + +using Corvus.HighPerformance; + +namespace Corvus.UriTemplates.TemplateParameterProviders; + +/// +/// Provides helpers for implementers of a . +/// +public static class TemplateParameterProvider +{ + private const string UriReservedSymbols = ":/?#[]@!$&'()*+,;="; + private const string UriUnreservedSymbols = "-._~"; +#if NET8_0_OR_GREATER + private static readonly SearchValues PossibleHexChars = SearchValues.Create("0123456789AaBbCcDdEeF"); +#else + private const string PossibleHexChars = "0123456789AaBbCcDdEeF"; +#endif +#pragma warning disable SA1010 // Opening square brackets should be spaced correctly - Analysers need to catch up with the new syntax. + private static readonly char[] HexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; +#pragma warning restore SA1010 // Opening square brackets should be spaced correctly + + /// + /// Encode a value to the output. + /// + /// The output buffer. + /// The value to encode. + /// A value indicating whether to allow reserved symbols. + public static void Encode(ref ValueStringBuilder output, ReadOnlySpan value, bool allowReserved) + { + int length = value.Length; + for (int i = 0; i < length; ++i) + { + char c = value[i]; + if ((c >= 'A' && c <= 'z') //// Alpha + || (c >= '0' && c <= '9') //// Digit + || UriUnreservedSymbols.IndexOf(c) != -1 //// Unreserved symbols - These should never be percent encoded + || (allowReserved && UriReservedSymbols.IndexOf(c) != -1)) //// Reserved symbols - should be included if requested (+) + { + output.Append(c); + } + else if (allowReserved && c == '%' && IsEscapeSequence(value, i)) + { + output.Append(value.Slice(i, 3)); + + // Skip the next two characters + i += 2; + } + else + { + WriteHexDigits(ref output, c); + } + } + + static void WriteHexDigits(ref ValueStringBuilder output, char c) + { +#if NET8_0_OR_GREATER + Span source = stackalloc char[1]; + source[0] = c; + Span bytes = stackalloc byte[Encoding.UTF8.GetMaxByteCount(1)]; + int encoded = Encoding.UTF8.GetBytes(source, bytes); + foreach (byte aByte in bytes[..encoded]) + { + output.Append('%'); + output.Append(HexDigits[(aByte & 240) >> 4]); + output.Append(HexDigits[aByte & 15]); + } +#else + byte[] bytes = ArrayPool.Shared.Rent(4); + char[] source = ArrayPool.Shared.Rent(1); + + try + { + source[0] = c; + int encoded = Encoding.UTF8.GetBytes(source, 0, 1, bytes, 0); + foreach (byte aByte in bytes.AsSpan()[..encoded]) + { + output.Append('%'); + output.Append(HexDigits[(aByte & 240) >> 4]); + output.Append(HexDigits[aByte & 15]); + } + } + finally + { + ArrayPool.Shared.Return(bytes); + ArrayPool.Shared.Return(source); + } +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsEscapeSequence(ReadOnlySpan value, int i) + { + if (value.Length <= i + 2) + { + return false; + } + + return IsHex(value[i + 1]) && IsHex(value[i + 2]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHex(char v) + { +#if NET8_0_OR_GREATER + return PossibleHexChars.Contains(v); +#else + return PossibleHexChars.IndexOf(v) >= 0; +#endif + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/VariableProcessingState.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/VariableProcessingState.cs new file mode 100644 index 000000000..cb4f5b2d2 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/VariableProcessingState.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates.TemplateParameterProviders; + +/// +/// Used by to determine +/// the result of processing a variable with a set of parameters. +/// +public enum VariableProcessingState +{ + /// + /// Processing succeeded. + /// + Success, + + /// + /// The parameter was not present. + /// + NotProcessed, + + /// + /// The parameter was not valid for the given variable. + /// + Failure, +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/VariableSpecification.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/VariableSpecification.cs new file mode 100644 index 000000000..81b8f1705 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/TemplateParameterProviders/VariableSpecification.cs @@ -0,0 +1,149 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using Corvus.HighPerformance; + +namespace Corvus.UriTemplates.TemplateParameterProviders; + +/// +/// A variable specification. +/// +public ref struct VariableSpecification +{ + /// + /// Initializes a new instance of the struct. + /// + /// The operator info. + /// The variable name. + /// Whether to explode the variable. + /// The prefix length. + /// Whether this is the first variable in the template. + public VariableSpecification(OperatorInfo operatorInfo, ReadOnlySpan variableName, bool explode = false, int prefixLength = 0, bool first = true) + { + this.OperatorInfo = operatorInfo; + this.Explode = explode; + this.PrefixLength = prefixLength; + this.First = first; + this.VarName = variableName; + } + + /// + /// Gets the operation info. + /// + public OperatorInfo OperatorInfo { get; } + + /// + /// Gets or sets the variable name. + /// + public ReadOnlySpan VarName { get; set; } + + /// + /// Gets or sets a value indicating whether this variable is exploded. + /// + public bool Explode { get; set; } + + /// + /// Gets or sets the prefix length for the variable. + /// + public int PrefixLength { get; set; } + + /// + /// Gets or sets a value indicating whether this is the first variable in the template. + /// + public bool First { get; set; } + + /// + /// Copy the result to the output span. + /// + /// The span to which to copy the result. + public void CopyTo(ref ValueStringBuilder output) + { + if (this.First) + { + if (this.OperatorInfo.First != '\0') + { + output.Append(this.OperatorInfo.First); + } + } + + output.Append(this.VarName); + + if (this.Explode) + { + output.Append('*'); + } + + if (this.PrefixLength > 0) + { + output.Append(':'); + + int digits = this.PrefixLength == 0 ? 1 : (int)Math.Floor(Math.Log10(this.PrefixLength)) + 1; + + Span buffer = output.AppendSpan(digits); + +#if NET8_0_OR_GREATER + this.PrefixLength.TryFormat(buffer, out int charsWritten); +#else + TryFormat(this.PrefixLength, buffer, digits); +#endif + } + +#if !NET8_0_OR_GREATER + static void TryFormat(int value, Span span, int digits) + { + while (digits > 0) + { + digits--; + span[digits] = (char)((value % 10) + '0'); + value /= 10; + } + } +#endif + } + + /// + /// Gets the variable as a string. + /// + /// The variable specification as a string. + public override string ToString() + { + int maximumOutputLength = 3 + this.VarName.Length + this.PrefixLength; + const int MaximumSupportedOutputLength = 4096; + if (maximumOutputLength > MaximumSupportedOutputLength) + { + // Entire URIs typically shouldn't be longer than this, so something is odd if we exceed this. + // This is a safety-oriented policy. If someone turns out to come up with a reasonable + // scenario in which they genuinely need more, we can revisit this. + throw new InvalidOperationException($"The length of this variable would exceed {MaximumSupportedOutputLength} characters. This is unlikely to be correct, but if this limit is unacceptable to you please file an issue at https://github.com/corvus-dotnet/Corvus.UriTemplates/issues"); + } + + Span initialBuffer = stackalloc char[maximumOutputLength]; + ValueStringBuilder builder = new(initialBuffer); + if (this.First) + { + builder.Append(this.OperatorInfo.First); + } + +#if NET8_0_OR_GREATER + builder.Append(this.VarName); +#else + foreach (char c in this.VarName) + { + builder.Append(c); + } +#endif + if (this.Explode) + { + builder.Append('*'); + } + + if (this.PrefixLength > 0) + { + builder.Append(':'); + builder.Append(this.PrefixLength); + } + + return builder.CreateStringAndDispose(); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateAndVerbTable.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateAndVerbTable.cs new file mode 100644 index 000000000..2147599fb --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateAndVerbTable.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates; + +/// +/// Extensions to support matching with UriTemplate and Verb. +/// +/// +/// This uses as the result type of the UriTemplateTable match to provide an +/// additional indirection through a verb. +/// +public static class UriTemplateAndVerbTable +{ + /// + /// Creates an instance of the . + /// + /// The type of the match result. + /// An instance of a . + public static UriTemplateAndVerbTable.Builder CreateBuilder() => new(); +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateAndVerbTable{TMatch}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateAndVerbTable{TMatch}.cs new file mode 100644 index 000000000..8c2e96b84 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateAndVerbTable{TMatch}.cs @@ -0,0 +1,164 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Diagnostics.CodeAnalysis; + +namespace Corvus.UriTemplates; + +/// +/// Matches a URI against a table of URI templates and returns a result value. +/// +/// The type of the match result. +public class UriTemplateAndVerbTable +{ + private UriTemplateTable> innerTable; + + private UriTemplateAndVerbTable(UriTemplateTable> innerTable) + { + this.innerTable = innerTable; + } + + /// + /// Try to match the uri against the URI templates in the table. + /// + /// The URI to match. + /// The verb to match. + /// The matched result. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// if the URI matched a value in the table. + /// + /// + /// This will find the first match in the table. + /// + /// + /// While the result is you need only dispose it if the method returned . + /// It is, however, safe to dispose in either case. + /// + /// + public bool TryMatch(ReadOnlySpan uri, ReadOnlySpan verb, [MaybeNullWhen(false)] out TemplateMatchResult match, bool requiresRootedMatch = false) + { + if (this.innerTable.TryMatch(uri, out TemplateMatchResult> result, requiresRootedMatch)) + { + if (result.Result.TryMatch(verb, out TMatch? value)) + { + match = new TemplateMatchResult(value, result.Parser); + return true; + } + } + + // No result, so return the default. + match = default; + return false; + } + +#if !NET8_0_OR_GREATER + /// + /// Try to match the uri against the URI templates in the table. + /// + /// The URI to match. + /// The verb to match. + /// The matched result. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// if the URI matched a value in the table. + /// + /// + /// This will find the first match in the table. + /// + /// + /// While the result is you need only dispose it if the method returned . + /// It is, however, safe to dispose in either case. + /// + /// + public bool TryMatch(string uri, string verb, [MaybeNullWhen(false)] out TemplateMatchResult match, bool requiresRootedMatch = false) + { + return this.TryMatch(uri.AsSpan(), verb.AsSpan(), out match, requiresRootedMatch); + } +#endif + + /// + /// A factory for creating instances. + /// + public sealed class Builder + { + // These are the parsers we have created from strings. + private Dictionary createdParsers; + private Dictionary.Builder> parsersToVerbBuilders; + + /// + /// Initializes a new instance of the class. + /// + internal Builder() + { + this.createdParsers = new(); + this.parsersToVerbBuilders = new(); + } + + /// + /// Add a match for a uriTemplate and verb. + /// + /// The URI template for which to add the match. + /// The verb for which to add the match. + /// The match to add for this combination. + public void Add(ReadOnlySpan uriTemplate, string verb, TMatch match) + { + string uriTemplateString = uriTemplate.ToString(); + if (!this.createdParsers.TryGetValue(uriTemplateString, out IUriTemplateParser? value)) + { + value = UriTemplateParserFactory.CreateParser(uriTemplate); + this.createdParsers.Add(uriTemplateString, value); + } + + this.Add(value, verb, match); + } + + /// + /// Add a match for a uriTemplate and verb. + /// + /// The URI template for which to add the match. + /// The verb for which to add the match. + /// The match to add for this combination. + public void Add(string uriTemplate, string verb, TMatch match) + { + if (!this.createdParsers.TryGetValue(uriTemplate, out IUriTemplateParser? value)) + { + value = UriTemplateParserFactory.CreateParser(uriTemplate); + this.createdParsers.Add(uriTemplate, value); + } + + this.Add(value, verb, match); + } + + /// + /// Add a match for a uriTemplate and verb. + /// + /// The URI template for which to add the match. + /// The verb for which to add the match. + /// The match to add for this combination. + public void Add(IUriTemplateParser uriTemplate, string verb, TMatch match) + { + if (!this.parsersToVerbBuilders.TryGetValue(uriTemplate, out MatchWithVerb.Builder? builder)) + { + builder = MatchWithVerb.CreateBuilder(); + this.parsersToVerbBuilders.Add(uriTemplate, builder); + } + + builder.Add(verb, match); + } + + /// + /// Converts the builder into a table. + /// + /// The . + public UriTemplateAndVerbTable ToTable() + { + UriTemplateTable>.Builder innerTableBuilder = UriTemplateTable.CreateBuilder>(); + foreach (KeyValuePair.Builder> item in this.parsersToVerbBuilders) + { + innerTableBuilder.Add(item.Key, item.Value.ToMatchWithVerb()); + } + + return new(innerTableBuilder.ToTable()); + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParameters.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParameters.cs new file mode 100644 index 000000000..753a2735c --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParameters.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates; + +/// +/// A collection of parameters extracted from a URI template. +/// +/// +/// This avoids allocating individual strings for the parameter names and values. +/// +public class UriTemplateParameters : IDisposable +{ + private ParameterValue[]? items; + + /// + /// Creates a . + /// + /// + /// The parameter value array, which has been rented. Ownership passes to this instance, + /// which is why has to return it. + /// + internal UriTemplateParameters(ParameterValue[] items) + { + this.items = items; + } + + /// + public void Dispose() + { + if (this.items is not null) + { + ParameterByNameAndRangeCache.Return(this.items); + this.items = null; + } + } + + /// + /// Tests whether a parameter with the specified name is present. + /// + /// The parameter name. + /// True if the named parameter was found. + /// + /// Thrown if has already been called. + /// + public bool Has(ReadOnlySpan name) => this.TryGet(name, out _); + + /// + /// Retrieves the value of a parameter. + /// + /// The parameter name. + /// The parameter value. + /// True if the named parameter was found. + /// + /// Thrown if has already been called. + /// + public bool TryGet(ReadOnlySpan name, out ParameterValue value) + { + if (this.items is null) + { + throw new ObjectDisposedException(nameof(UriTemplateParameters)); + } + + for (int i = 0; i < this.items.Length; i++) + { + ref ParameterValue v = ref this.items[i]; + if (v.Name.Span.IsEmpty) + { + // The array is rented, so it will typically be larger than it + // needs to be. If we hit an empty entry there's no point looking + // any further. + break; + } + + if (v.Name.Span.Equals(name, StringComparison.Ordinal)) + { + value = v; + return true; + } + } + + value = default; + return false; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParserExtensions.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParserExtensions.cs new file mode 100644 index 000000000..611d5ae95 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParserExtensions.cs @@ -0,0 +1,135 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Diagnostics.CodeAnalysis; + +namespace Corvus.UriTemplates; + +/// +/// A callback for enumerating parameters from the cache. +/// +/// The type of the state for the callback. +/// The name of the parameter. +/// The value of the parameter. +/// The state for the callback. +public delegate void EnumerateParametersCallback(ReadOnlySpan name, ReadOnlySpan value, ref TState state); + +/// +/// A callback for enumerating parameters from the cache. +/// +/// The type of the state for the callback. +/// The parameter name. +/// The range in the input URI string at which the parameter was found. +/// The state for the callback. +public delegate void EnumerateParametersCallbackWithRange(ParameterName name, Range valueRange, ref TState state); + +/// +/// Extension methods for . +/// +public static class UriTemplateParserExtensions +{ + /// + /// Enumerate the parameters in the parser. + /// + /// The type of the state for the callback. + /// The parser to use. + /// The uri to parse. + /// The callback to receive the enumerated parameters. + /// The state for the callback. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// if the parser was successful, otherwise . + public static bool EnumerateParameters(this IUriTemplateParser parser, ReadOnlySpan uri, EnumerateParametersCallback callback, ref TState state, int initialCapacity = 10) + { + return ParameterCache.EnumerateParameters(parser, uri, initialCapacity, callback, ref state); + } + + /// + /// Enumerate the parameters in the parser. + /// + /// The type of the state for the callback. + /// The parser to use. + /// The uri to parse. + /// The callback to receive the enumerated parameters. + /// The state for the callback. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// if the parser was successful, otherwise . + public static bool EnumerateParameters(this IUriTemplateParser parser, string uri, EnumerateParametersCallback callback, ref TState state, int initialCapacity = 10) + { + return ParameterCache.EnumerateParameters(parser, uri.AsSpan(), initialCapacity, callback, ref state); + } + + /// + /// Enumerate the parameters in the parser. + /// + /// The type of the state for the callback. + /// The parser to use. + /// The uri to parse. + /// The callback to receive the enumerated parameters. + /// The state for the callback. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// if the parser was successful, otherwise . + public static bool EnumerateParameters(this IUriTemplateParser parser, ReadOnlySpan uri, EnumerateParametersCallbackWithRange callback, ref TState state, int initialCapacity = 10) + { + return ParameterByNameAndRangeCache.EnumerateParameters(parser, uri, initialCapacity, callback, ref state); + } + + /// + /// Enumerate the parameters in the parser. + /// + /// The type of the state for the callback. + /// The parser to use. + /// The uri to parse. + /// The callback to receive the enumerated parameters. + /// The state for the callback. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// if the parser was successful, otherwise . + public static bool EnumerateParameters(this IUriTemplateParser parser, string uri, EnumerateParametersCallbackWithRange callback, ref TState state, int initialCapacity = 10) + { + return ParameterByNameAndRangeCache.EnumerateParameters(parser, uri.AsSpan(), initialCapacity, callback, ref state); + } + + /// + /// Gets the parameters from the URI template. + /// + /// The parser to use. + /// The uri to parse. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// A . + /// if parsing succeeded. + public static bool TryGetUriTemplateParameters( + this IUriTemplateParser parser, + ReadOnlySpan uri, + int initialCapacity, + [NotNullWhen(true)] out UriTemplateParameters? templateParameters) + { + return ParameterByNameAndRangeCache.TryGetUriTemplateParameters( + parser, uri, initialCapacity, false, out templateParameters); + } + + /// + /// Gets the parameters from the URI template. + /// + /// The parser to use. + /// The uri to parse. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// A . + /// if parsing succeeded. + public static bool TryGetUriTemplateParameters( + this IUriTemplateParser parser, + ReadOnlySpan uri, + int initialCapacity, + bool requiresRootedMatch, + [NotNullWhen(true)] out UriTemplateParameters? templateParameters) + { + return ParameterByNameAndRangeCache.TryGetUriTemplateParameters( + parser, uri, initialCapacity, requiresRootedMatch, out templateParameters); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParserFactory.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParserFactory.cs new file mode 100644 index 000000000..d81c31bc0 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateParserFactory.cs @@ -0,0 +1,1072 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Buffers; +using System.Text.RegularExpressions; + +namespace Corvus.UriTemplates; + +/// +/// Parses a UriTemplate. +/// +public static class UriTemplateParserFactory +{ + //// Note that we use Regular Expressions to build parse sequences, not to parse the actual results. + //// That is done using a zero-allocation model, with callbacks for the parameters we find. + private const string Varname = "[a-zA-Z0-9_]*"; + private const string Op = "(?[+#./;?&]?)"; + private const string Var = "(?(?:(?" + Varname + ")[*]?,?)*)"; + private const string Varspec = "(?{" + Op + Var + "})"; + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); + private static readonly Regex FindParam = new(Varspec, RegexOptions.Compiled, DefaultTimeout); + private static readonly Regex TemplateConversion = new(@"([^{]|^)\?", RegexOptions.Compiled, DefaultTimeout); + + /// + /// A pattern element in a URI template. + /// + private interface IUriTemplatePatternElement + { + /// + /// Non-greedily consume the given segment. + /// + /// The type of the state from the caller. + /// The escaped URI template. (Enables us to avoid making copies of parameter names.) + /// The segment to consume. + /// The number of characters consumed. + /// The callback when a parameter is discovered. + /// The tail for this segment. + /// The state from the caller. + /// True if the segment was consumed successfully, otherwise false. + bool Consume(string escapedTemplate, ReadOnlySpan segment, out int charsConsumed, ParameterCallback? parameterCallback, ref Consumer tail, ref TState state); + + /// + /// Non-greedily consume the given segment. + /// + /// The type of the state from the caller. + /// The escaped URI template. (Enables us to avoid making copies of parameter names.) + /// The segment to consume. + /// The offset in the original URI at which starts. + /// The number of characters consumed. + /// The callback when a parameter is discovered. + /// The tail for this segment. + /// The state from the caller. + /// True if the segment was consumed successfully, otherwise false. + bool Consume(string escapedTemplate, ReadOnlySpan segment, int segmentOffset, out int charsConsumed, ParameterCallbackWithRange? parameterCallback, ref Consumer tail, ref TState state); + } + + /// + /// Create a URI parser for a URI template. + /// + /// The URI template for which to create the parser. + /// An instance of a parser for the given URI template. + /// + /// Note that this operation allocates memory, but + /// is a low-allocation method. Ideally, you should cache the results of calling this method for a given URI template. + /// + public static IUriTemplateParser CreateParser(ReadOnlySpan uriTemplate) + { + (string escapedUriTemplate, IUriTemplatePatternElement[] elements) = CreateParserElements(uriTemplate); + return new UriParser(escapedUriTemplate, elements); + } + + /// + /// Create a URI parser for a URI template. + /// + /// The URI template for which to create the parser. + /// An instance of a parser for the given URI template. + /// + /// Note that this operation allocates memory, but + /// is a low-allocation method. Ideally, you should cache the results of calling this method for a given URI template. + /// + public static IUriTemplateParser CreateParser(string uriTemplate) + { + (string escapedUriTemplate, IUriTemplatePatternElement[] elements) = CreateParserElements(uriTemplate.AsSpan()); + return new UriParser(escapedUriTemplate, elements); + } + + private static (string EscapedTemplate, IUriTemplatePatternElement[] Elements) CreateParserElements(ReadOnlySpan uriTemplate) + { + string template = TemplateConversion.Replace(uriTemplate.ToString(), @"$+\?"); + ReadOnlySpan templateSpan = template.AsSpan(); + List elements = new(); + + int lastIndex = 0; + foreach (Match match in FindParam.Matches(template).Cast()) + { + if (match.Index > lastIndex) + { + // There must be a literal sequence in this gap + AddLiteral(templateSpan, elements, lastIndex, match); + } + + elements.Add(Match(match)); + lastIndex = match.Index + match.Length; + } + + if (lastIndex < templateSpan.Length) + { + // There must also be a literal sequence at the end + elements.Add(new LiteralSequence(templateSpan[lastIndex..])); + } + + return (template, elements.ToArray()); + + static int UnescapeQuestionMarkInPlace(Span literal) + { + int readIndex = 0; + int writeIndex = 0; + + while (readIndex < literal.Length) + { + if (readIndex < (literal.Length - 1) && literal[readIndex] == '\\' && literal[readIndex + 1] == '?') + { + // Skip the escaping slash + readIndex++; + continue; + } + + literal[writeIndex] = literal[readIndex]; + ++readIndex; + ++writeIndex; + } + + return writeIndex; + } + + static IUriTemplatePatternElement Match(Match m) + { + CaptureCollection captures = m.Groups["lvar"].Captures; + Range[] paramNameRanges = ArrayPool.Shared.Rent(captures.Count); + try + { + int written = 0; + foreach (Capture capture in captures.Cast()) + { + if (!string.IsNullOrEmpty(capture.Value)) + { + paramNameRanges[written++] = new(capture.Index, capture.Index + capture.Length); + } + } + +#if NET8_0_OR_GREATER + Range[] paramNameRangesArray = paramNameRanges[0..written]; +#else + var paramNameRangesArray = new Range[written]; + paramNameRanges.AsSpan(0, written).CopyTo(paramNameRangesArray); +#endif + + string op = m.Groups["op"].Value; + return op switch + { + "?" => new QueryExpressionSequence(paramNameRangesArray, '?'), + "&" => new QueryExpressionSequence(paramNameRangesArray, '&'), + "#" => new ExpressionSequence(paramNameRangesArray, '#'), + "/" => new ExpressionSequence(paramNameRangesArray, '/'), + "+" => new ExpressionSequence(paramNameRangesArray, '\0'), + _ => new ExpressionSequence(paramNameRangesArray, '\0'), + }; + } + finally + { + ArrayPool.Shared.Return(paramNameRanges); + } + } + + static void AddLiteral(ReadOnlySpan templateSpan, List elements, int lastIndex, Match match) + { + ReadOnlySpan literal = templateSpan[lastIndex..match.Index]; + Span unescaped = stackalloc char[literal.Length]; + literal.CopyTo(unescaped); + int written = UnescapeQuestionMarkInPlace(unescaped); + elements.Add(new LiteralSequence(unescaped[..written])); + } + } + + private readonly ref struct Consumer + { + private readonly ReadOnlySpan elements; + private readonly int elementsLength; + + public Consumer(in ReadOnlySpan elements) + { + this.elements = elements; + this.elementsLength = elements.Length; + } + + public bool Consume(string escapedTemplate, bool requiresRootedMatch, in ReadOnlySpan segment, out int charsConsumed, in ParameterCallback? parameterCallback, ref TState state) + { + int segmentLength = segment.Length; + charsConsumed = 0; + + // First, we attempt to consume, advancing through the span until we reach a match + // (Recall that na UriTemplate is normally allowed to match the tail of a string - any prefix can be ignored.) + int consumedBySequence = 0; + while (charsConsumed < segmentLength && !this.ConsumeCore(escapedTemplate, segment[charsConsumed..], out consumedBySequence, parameterCallback, ref state)) + { + if (requiresRootedMatch) + { + charsConsumed = segmentLength; + } + else + { + // We didn't match at that location, so tell the parameter callback to reset the accumulated parameters, + // and advance a character + parameterCallback?.Invoke(true, default, default, ref state); + charsConsumed += consumedBySequence > 0 ? consumedBySequence : 1; + } + } + + if (charsConsumed == segmentLength) + { + // We didn't find a match, so we tell the parameter callback to reset the accumulated parameters, + // and reset the characters consumed. + parameterCallback?.Invoke(true, default, default, ref state); + charsConsumed = 0; + return false; + } + + charsConsumed += consumedBySequence; + return true; + } + + public bool Consume(string escapedTemplate, bool requiresRootedMatch, in ReadOnlySpan segment, out int charsConsumed, in ParameterCallbackWithRange? parameterCallback, ref TState state) + { + int segmentLength = segment.Length; + charsConsumed = 0; + + // First, we attempt to consume, advancing through the span until we reach a match + // (Recall that na UriTemplate is normally allowed to match the tail of a string - any prefix can be ignored.) + int consumedBySequence = 0; + while (charsConsumed < segmentLength && !this.ConsumeCore(escapedTemplate, segment[charsConsumed..], charsConsumed, out consumedBySequence, parameterCallback, ref state)) + { + if (requiresRootedMatch) + { + charsConsumed = segmentLength; + } + else + { + // We didn't match at that location, so tell the parameter callback to reset the accumulated parameters, + // and advance a character + parameterCallback?.Invoke(true, default, default, ref state); + charsConsumed += consumedBySequence > 0 ? consumedBySequence : 1; + } + } + + if (charsConsumed == segmentLength) + { + // We didn't find a match, so we tell the parameter callback to reset the accumulated parameters, + // and reset the characters consumed. + parameterCallback?.Invoke(true, default, default, ref state); + charsConsumed = 0; + return false; + } + + charsConsumed += consumedBySequence; + return true; + } + + public bool MatchesAsTail(string escapedTemplate, in ReadOnlySpan segment, out int charsConsumed, ref TState state) + { + return this.ConsumeCore(escapedTemplate, segment, out charsConsumed, default(ParameterCallback), ref state); + } + + private bool ConsumeCore(string escapedTemplate, in ReadOnlySpan segment, out int charsConsumed, in ParameterCallback? parameterCallback, ref TState state) + { + charsConsumed = 0; + + for (int i = 0; i < this.elementsLength; ++i) + { + Consumer tail = new(this.elementsLength > (i + 1) ? this.elements[(i + 1)..] : default); + if (!this.elements[i].Consume(escapedTemplate, segment[charsConsumed..], out int localConsumed, parameterCallback, ref tail, ref state)) + { + // We ensure that local consumed is set correctly by the target for where we can try again. + charsConsumed += localConsumed; + return false; + } + + charsConsumed += localConsumed; + } + + return true; + } + + private bool ConsumeCore(string escapedTemplate, in ReadOnlySpan segment, int segmentOffset, out int charsConsumed, in ParameterCallbackWithRange? parameterCallback, ref TState state) + { + charsConsumed = 0; + + for (int i = 0; i < this.elementsLength; ++i) + { + Consumer tail = new(this.elementsLength > (i + 1) ? this.elements[(i + 1)..] : default); + if (!this.elements[i].Consume(escapedTemplate, segment[charsConsumed..], segmentOffset + charsConsumed, out int localConsumed, parameterCallback, ref tail, ref state)) + { + // We ensure that local consumed is set correctly by the target for where we can try again. + charsConsumed += localConsumed; + return false; + } + + charsConsumed += localConsumed; + } + + return true; + } + } + + /// + /// Parses a uri using a set of . + /// + private sealed class UriParser : IUriTemplateParser + { + private readonly string escapedTemplate; + private readonly IUriTemplatePatternElement[] elements; + + public UriParser(string escapedTemplate, in IUriTemplatePatternElement[] elements) + { + this.escapedTemplate = escapedTemplate; + this.elements = elements; + } + + /// + public bool IsMatch(in ReadOnlySpan uri, bool requiresRootedMatch = false) + { + Consumer sequence = new(this.elements.AsSpan()); + int state = 0; + bool result = sequence.Consume(this.escapedTemplate, requiresRootedMatch, uri, out int charsConsumed, default(ParameterCallback), ref state); + + // We have successfully parsed the uri if all of our elements successfully consumed + // the contents they were expecting, and we have no characters left over. + return result && charsConsumed == uri.Length; + } + +#if !NET8_0_OR_GREATER + /// + public bool IsMatch(string uri, bool requiresRootedMatch = false) + { + return this.IsMatch(uri.AsSpan(), requiresRootedMatch); + } + + /// + public bool ParseUri(string uri, ParameterCallback parameterCallback, ref TState state, bool requiresRootedMatch = false) + { + return this.ParseUri(uri.AsSpan(), parameterCallback, ref state, requiresRootedMatch); + } + + /// + public bool ParseUri(string uri, ParameterCallbackWithRange parameterCallback, ref TState state, bool requiresRootedMatch = false) + { + return this.ParseUri(uri.AsSpan(), parameterCallback, ref state, requiresRootedMatch); + } +#endif + + /// + public bool ParseUri( + in ReadOnlySpan uri, + ParameterCallbackWithRange parameterCallback, + ref TState state, + bool requiresRootedMatch = false) + { + Consumer sequence = new(this.elements.AsSpan()); + bool result = sequence.Consume(this.escapedTemplate, requiresRootedMatch, uri, out int charsConsumed, parameterCallback, ref state); + + // We have successfully parsed the uri if all of our elements successfully consumed + // the contents they were expecting, and we have no characters left over. + return result && charsConsumed == uri.Length; + } + + /// + public bool ParseUri(in ReadOnlySpan uri, ParameterCallback parameterCallback, ref TState state, bool requiresRootedMatch = false) + { + Consumer sequence = new(this.elements.AsSpan()); + bool result = sequence.Consume(this.escapedTemplate, requiresRootedMatch, uri, out int charsConsumed, parameterCallback, ref state); + + // We have successfully parsed the uri if all of our elements successfully consumed + // the contents they were expecting, and we have no characters left over. + return result && charsConsumed == uri.Length; + } + } + + /// + /// Represents a literal sequence in a URI template. + /// + private sealed class LiteralSequence : IUriTemplatePatternElement + { + private readonly char[] sequence; + + public LiteralSequence(ReadOnlySpan sequence) + { + this.sequence = sequence.ToArray(); + } + + /// + public bool Consume(string escapedTemplate, ReadOnlySpan segment, out int charsConsumed, ParameterCallback? parameterCallback, ref Consumer tail, ref TState state) + { + return this.Consume(segment, out charsConsumed, ref tail); + } + + /// + public bool Consume(string escapedTemplate, ReadOnlySpan segment, int segmentOffset, out int charsConsumed, ParameterCallbackWithRange? parameterCallback, ref Consumer tail, ref TState state) + { + return this.Consume(segment, out charsConsumed, ref tail); + } + + private bool Consume(ReadOnlySpan segment, out int charsConsumed, ref Consumer tail) + { + int index = segment.IndexOf(this.sequence); + if (index == 0) + { + charsConsumed = this.sequence.Length; + return true; + } + + if (index == -1) + { + // No point in looking ahead as the literal sequence doesn't appear anywhere. + charsConsumed = this.sequence.Length; + return false; + } + + charsConsumed = 0; + return false; + } + } + + /// + /// Represents an expression sequence in a URI template. + /// + private sealed class ExpressionSequence : IUriTemplatePatternElement + { +#if NET8_0_OR_GREATER + private static readonly SearchValues FragmentTerminators = SearchValues.Create(","); + private static readonly SearchValues SlashTerminators = SearchValues.Create("/?"); + private static readonly SearchValues QueryTerminators = SearchValues.Create("&#"); + private static readonly SearchValues SemicolonTerminators = SearchValues.Create(";/?#"); + private static readonly SearchValues DotTerminators = SearchValues.Create("./?#"); + private static readonly SearchValues AllOtherTerminators = SearchValues.Create("/?&"); +#else + private const string FragmentTerminators = ","; + private const string SlashTerminators = "/?"; + private const string QueryTerminators = "&#"; + private const string SemicolonTerminators = ";/?#"; + private const string DotTerminators = "./?#"; + private const string AllOtherTerminators = "/?&"; +#endif + private readonly Range[] parameterNameRanges; + private readonly char prefix; + +#if NET8_0_OR_GREATER + private readonly SearchValues terminators; +#else + private readonly string terminators; +#endif + + public ExpressionSequence(Range[] parameterNameRanges, char prefix) + { + this.parameterNameRanges = parameterNameRanges; + this.prefix = prefix; + this.terminators = GetTerminators(prefix); + +#if NET8_0_OR_GREATER + static SearchValues GetTerminators(char prefix) + { + return prefix switch + { + '#' => FragmentTerminators, + '/' => SlashTerminators, + '?' or '&' => QueryTerminators, + ';' => SemicolonTerminators, + '.' => DotTerminators, + _ => AllOtherTerminators, + }; + } +#else + static string GetTerminators(char prefix) + { + return prefix switch + { + '#' => FragmentTerminators, + '/' => SlashTerminators, + '?' or '&' => QueryTerminators, + ';' => SemicolonTerminators, + '.' => DotTerminators, + _ => AllOtherTerminators, + }; + } +#endif + } + + private enum State + { + LookingForPrefix, + LookingForParams, + } + + /// + public bool Consume(string escapedTemplate, ReadOnlySpan segment, out int charsConsumed, ParameterCallback? parameterCallback, ref Consumer tail, ref TState callbackState) + { + charsConsumed = 0; + int parameterIndex = 0; + State state = this.prefix != '\0' ? State.LookingForPrefix : State.LookingForParams; + Range currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + ReadOnlySpan escapedTemplateSpan = escapedTemplate.AsSpan(); + ReadOnlySpan currentParameterName = escapedTemplateSpan[currentParameterNameRange]; + char currentPrefix = this.prefix; + bool foundMatches = false; + while (charsConsumed < segment.Length) + { + switch (state) + { + case State.LookingForPrefix: + if (segment[charsConsumed] == currentPrefix) + { + state = State.LookingForParams; + charsConsumed++; + + // If we are a fragment parameter, subsequent + // parameters in the sequence use the ',' + if (currentPrefix == '#') + { + currentPrefix = ','; + } + } + else + { + // If we found any matches, then that's good, and + // we return our chars consumed. + // + // On the other hand, if we found no matches before we reached the + // end of our search, we say we didn't consume any characters, + // but we still matched successfully, because these matches were all optional. + if (!foundMatches) + { + charsConsumed = 0; + } + + return true; + } + + break; + + case State.LookingForParams: + // We found the prefix, so we need to find the next block until the terminator. + int segmentStart = charsConsumed; + int segmentEnd = segmentStart; + + // Now we are looking ahead to the next terminator, or the end of the segment + while (segmentEnd < segment.Length) + { +#if NET8_0_OR_GREATER + if (this.terminators.Contains(segment[segmentEnd])) +#else + if (this.terminators.IndexOf(segment[segmentEnd]) >= 0) +#endif + { + // Break out of the while because we've found the end. + break; + } + + segmentEnd++; + } + + // Tell the world about this parameter (note that the span for the value could be empty). + parameterCallback?.Invoke(false, currentParameterName, segment[segmentStart..segmentEnd], ref callbackState); + charsConsumed = segmentEnd; + foundMatches = true; + + // Start looking for the next parameter. + parameterIndex++; + + // We've moved past the last parameter we're looking for. + if (parameterIndex >= this.parameterNameRanges.Length) + { + // We found at least this match! + return true; + } + + // If we match the tail (the remaining segments in the match) we don't want to consume the next one. + if (tail.MatchesAsTail(escapedTemplate, segment[charsConsumed..], out int tailConsumed, ref callbackState) && (tailConsumed + charsConsumed == segment.Length)) + { + // The tail matches the rest of the segment, so we will ignore our next parameter. + return true; + } + + // Otherwise, start looking for the next parameter + currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + currentParameterName = escapedTemplateSpan[currentParameterNameRange]; + + state = this.prefix != '\0' ? State.LookingForPrefix : State.LookingForParams; + break; + } + } + + return true; + } + + /// + public bool Consume( + string escapedUriTemplate, + ReadOnlySpan segment, + int segmentStartOffset, + out int charsConsumed, + ParameterCallbackWithRange? parameterCallback, + ref Consumer tail, + ref TState callbackState) + { + charsConsumed = 0; + int parameterIndex = 0; + State state = this.prefix != '\0' ? State.LookingForPrefix : State.LookingForParams; + Range currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + + char currentPrefix = this.prefix; + bool foundMatches = false; + while (charsConsumed < segment.Length) + { + switch (state) + { + case State.LookingForPrefix: + if (segment[charsConsumed] == currentPrefix) + { + state = State.LookingForParams; + charsConsumed++; + + // If we are a fragment parameter, subsequent + // parameters in the sequence use the ',' + if (currentPrefix == '#') + { + currentPrefix = ','; + } + } + else + { + // If we found any matches, then that's good, and + // we return our chars consumed. + // + // On the other hand, if we found no matches before we reached the + // end of our search, we say we didn't consume any characters, + // but we still matched successfully, because these matches were all optional. + if (!foundMatches) + { + charsConsumed = 0; + } + + return true; + } + + break; + + case State.LookingForParams: + // We found the prefix, so we need to find the next block until the terminator. + int segmentStart = charsConsumed; + int segmentEnd = segmentStart; + + // Now we are looking ahead to the next terminator, or the end of the segment + while (segmentEnd < segment.Length) + { +#if NET8_0_OR_GREATER + if (this.terminators.Contains(segment[segmentEnd])) +#else + if (this.terminators.IndexOf(segment[segmentEnd]) >= 0) +#endif + { + // Break out of the while because we've found the end. + break; + } + + segmentEnd++; + } + + // Tell the world about this parameter (note that the span for the value could be empty). + parameterCallback?.Invoke(false, new ParameterName(escapedUriTemplate, currentParameterNameRange), new Range(segmentStartOffset + segmentStart, segmentStartOffset + segmentEnd), ref callbackState); + charsConsumed = segmentEnd; + foundMatches = true; + + // Start looking for the next parameter. + parameterIndex++; + + // We've moved past the last parameter we're looking for. + if (parameterIndex >= this.parameterNameRanges.Length) + { + // We found at least this match! + return true; + } + + // If we match the tail (the remaining segments in the match) we don't want to consume the next one. + if (tail.MatchesAsTail(escapedUriTemplate, segment[charsConsumed..], out int tailConsumed, ref callbackState) && (tailConsumed + charsConsumed == segment.Length)) + { + // The tail matches the rest of the segment, so we will ignore our next parameter. + return true; + } + + // Otherwise, start looking for the next parameter + currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + + state = this.prefix != '\0' ? State.LookingForPrefix : State.LookingForParams; + break; + } + } + + return true; + } + } + + /// + /// Represents a query expression sequence in a URI template. + /// + private sealed class QueryExpressionSequence : IUriTemplatePatternElement + { +#if NET8_0_OR_GREATER + private static readonly SearchValues Terminators = SearchValues.Create("/?&"); +#else + private const string Terminators = "/?&"; +#endif + private readonly Range[] parameterNameRanges; + private readonly char prefix; + + public QueryExpressionSequence(Range[] parameterNameRanges, char prefix) + { + this.parameterNameRanges = parameterNameRanges; + this.prefix = prefix; + } + + private enum State + { + LookingForPrefix, + LookingForParams, + } + + /// + public bool Consume(string escapedTemplate, ReadOnlySpan segment, out int charsConsumed, ParameterCallback? parameterCallback, ref Consumer tail, ref TState callbackState) + { + charsConsumed = 0; + int parameterIndex = 0; + State state = State.LookingForPrefix; + Range currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + ReadOnlySpan escapedTemplateSpan = escapedTemplate.AsSpan(); + ReadOnlySpan currentParameterName = escapedTemplateSpan[currentParameterNameRange]; + char currentPrefix = this.prefix; + bool foundMatches = false; + while (charsConsumed < segment.Length) + { + switch (state) + { + case State.LookingForPrefix: + if (segment[charsConsumed] == currentPrefix) + { + state = State.LookingForParams; + charsConsumed++; + + // If we are a query parameter, subsequent + // parameters in the sequence use the '&' + if (currentPrefix == '?') + { + currentPrefix = '&'; + } + } + else + { + // If we found any matches, then that's good, and + // we return our chars consumed. + // + // On the other hand, if we found no matches before we reached the + // end of our search, we say we didn't consume any characters, + // but we still matched successfully, because these matches were all optional. + if (!foundMatches) + { + charsConsumed = 0; + } + + return true; + } + + break; + + case State.LookingForParams: + // Now check the rest of the characters + if (!segment[charsConsumed..].StartsWith(currentParameterName)) + { + parameterIndex++; + + // We've moved past the last parameter we're looking for. + if (parameterIndex >= this.parameterNameRanges.Length) + { + // If we found any matches, then that's good, and + // we return our chars consumed. + // + // On the other hand, if we found no matches before we reached the + // end of our search, we say we didn't consume any characters, + // but we still matched successfully, because these matches were all optional. + if (!foundMatches) + { + charsConsumed = 0; + } + else + { + // Back up so that we *didn't* consume the prefix character + // as this must be associated with the next segment + charsConsumed--; + } + + return true; + } + + // Go round again, but try the next parameter name. + currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + currentParameterName = escapedTemplateSpan[currentParameterNameRange]; + } + else + { + // We found our name, so let's see if the next character is '=' + if (segment[charsConsumed + currentParameterName.Length] != '=') + { + // If the next character wasn't '=' we don't match this segment at all + // so something has definitely gone awry! One possible case, for example, is that the + // current segment is a parameter that has a longer name, prefixed with our parameter name + // e.g. we are a value represented by '&foo=3' and it is '&fooBar=4' + parameterIndex++; + + // We've moved past the last parameter we're looking for. + if (parameterIndex >= this.parameterNameRanges.Length) + { + // If we found any matches, then that's good, and + // we return our chars consumed. + // + // On the other hand, if we found no matches before we reached the + // end of our search, we say we didn't consume any characters, + // but we still matched successfully, because these matches were all optional. + if (!foundMatches) + { + charsConsumed = 0; + } + else + { + // Back up so that we *didn't* consume the prefix character + // as this must be associated with the next segment + charsConsumed--; + } + + return true; + } + + currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + currentParameterName = escapedTemplateSpan[currentParameterNameRange]; + } + else + { + // The next character was '=' so now let's pick out the value + int segmentStart = charsConsumed + currentParameterName.Length + 1; + int segmentEnd = segmentStart; + + // So we did match the parameter and reach '=' now we are looking ahead to the next terminator, or the end of the segment + while (segmentEnd < segment.Length) + { +#if NET8_0_OR_GREATER + if (Terminators.Contains(segment[segmentEnd])) +#else + if (Terminators.IndexOf(segment[segmentEnd]) >= 0) +#endif + { + // Break out because we've found the end. + break; + } + + segmentEnd++; + } + + // Tell the world about this parameter (note that the span for the value could be empty). + parameterCallback?.Invoke(false, currentParameterName, segment[segmentStart..segmentEnd], ref callbackState); + charsConsumed = segmentEnd; + foundMatches = true; + + // Start looking for the next parameter. + parameterIndex++; + + // We've moved past the last parameter we're looking for. + if (parameterIndex >= this.parameterNameRanges.Length) + { + // We found at least this match! + return true; + } + + // Otherwise, start looking for the next parameter + currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + currentParameterName = escapedTemplateSpan[currentParameterNameRange]; + + state = State.LookingForPrefix; + } + } + + break; + } + } + + return true; + } + + /// + public bool Consume(string escapedTemplate, ReadOnlySpan segment, int segmentOffset, out int charsConsumed, ParameterCallbackWithRange? parameterCallback, ref Consumer tail, ref TState callbackState) + { + charsConsumed = 0; + int parameterIndex = 0; + State state = State.LookingForPrefix; + Range currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + ReadOnlySpan escapedTemplateSpan = escapedTemplate.AsSpan(); + ReadOnlySpan currentParameterName = escapedTemplateSpan[currentParameterNameRange]; + char currentPrefix = this.prefix; + bool foundMatches = false; + while (charsConsumed < segment.Length) + { + switch (state) + { + case State.LookingForPrefix: + if (segment[charsConsumed] == currentPrefix) + { + state = State.LookingForParams; + charsConsumed++; + + // If we are a query parameter, subsequent + // parameters in the sequence use the '&' + if (currentPrefix == '?') + { + currentPrefix = '&'; + } + } + else + { + // If we found any matches, then that's good, and + // we return our chars consumed. + // + // On the other hand, if we found no matches before we reached the + // end of our search, we say we didn't consume any characters, + // but we still matched successfully, because these matches were all optional. + if (!foundMatches) + { + charsConsumed = 0; + } + + return true; + } + + break; + + case State.LookingForParams: + // Now check the rest of the characters + if (!segment[charsConsumed..].StartsWith(currentParameterName)) + { + parameterIndex++; + + // We've moved past the last parameter we're looking for. + if (parameterIndex >= this.parameterNameRanges.Length) + { + // If we found any matches, then that's good, and + // we return our chars consumed. + // + // On the other hand, if we found no matches before we reached the + // end of our search, we say we didn't consume any characters, + // but we still matched successfully, because these matches were all optional. + if (!foundMatches) + { + charsConsumed = 0; + } + else + { + // Back up so that we *didn't* consume the prefix character + // as this must be associated with the next segment + charsConsumed--; + } + + return true; + } + + // Go round again, but try the next parameter name. + currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + currentParameterName = escapedTemplateSpan[currentParameterNameRange]; + } + else + { + // We found our name, so let's see if the next character is '=' + if (segment[charsConsumed + currentParameterName.Length] != '=') + { + // If the next character wasn't '=' we don't match this segment at all + // so something has definitely gone awry! One possible case, for example, is that the + // current segment is a parameter that has a longer name, prefixed with our parameter name + // e.g. we are a value represented by '&foo=3' and it is '&fooBar=4' + parameterIndex++; + + // We've moved past the last parameter we're looking for. + if (parameterIndex >= this.parameterNameRanges.Length) + { + // If we found any matches, then that's good, and + // we return our chars consumed. + // + // On the other hand, if we found no matches before we reached the + // end of our search, we say we didn't consume any characters, + // but we still matched successfully, because these matches were all optional. + if (!foundMatches) + { + charsConsumed = 0; + } + else + { + // Back up so that we *didn't* consume the prefix character + // as this must be associated with the next segment + charsConsumed--; + } + + return true; + } + + currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + currentParameterName = escapedTemplateSpan[currentParameterNameRange]; + } + else + { + // The next character was '=' so now let's pick out the value + int segmentStart = charsConsumed + currentParameterName.Length + 1; + int segmentEnd = segmentStart; + + // So we did match the parameter and reach '=' now we are looking ahead to the next terminator, or the end of the segment + while (segmentEnd < segment.Length) + { +#if NET8_0_OR_GREATER + if (Terminators.Contains(segment[segmentEnd])) +#else + if (Terminators.IndexOf(segment[segmentEnd]) >= 0) +#endif + { + // Break out because we've found the end. + break; + } + + segmentEnd++; + } + + // Tell the world about this parameter (note that the span for the value could be empty). + parameterCallback?.Invoke(false, new ParameterName(escapedTemplate, currentParameterNameRange), new Range(segmentOffset + segmentStart, segmentOffset + segmentEnd), ref callbackState); + charsConsumed = segmentEnd; + foundMatches = true; + + // Start looking for the next parameter. + parameterIndex++; + + // We've moved past the last parameter we're looking for. + if (parameterIndex >= this.parameterNameRanges.Length) + { + // We found at least this match! + return true; + } + + // Otherwise, start looking for the next parameter + currentParameterNameRange = this.parameterNameRanges[parameterIndex]; + currentParameterName = escapedTemplateSpan[currentParameterNameRange]; + + state = State.LookingForPrefix; + } + } + + break; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateRegexBuilder.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateRegexBuilder.cs new file mode 100644 index 000000000..9348bf858 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateRegexBuilder.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Buffers; +using System.Text; +using System.Text.RegularExpressions; +using Corvus.UriTemplates.Internal; + +namespace Corvus.UriTemplates; + +/// +/// Builds a regular expression that can parse the parameters from a UriTemplate. +/// +/// +/// Note that we have a non-regex-based (low-allocation) equivalent to this in . +/// This is provided for applications that specifically require a regex. +/// +public static class UriTemplateRegexBuilder +{ + private const string Varname = "[a-zA-Z0-9_]*"; + private const string Op = "(?[+#./;?&]?)"; + private const string Var = "(?(?:(?" + Varname + ")[*]?,?)*)"; + private const string Varspec = "(?{" + Op + Var + "})"; + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); + private static readonly Regex FindParam = new(Varspec, RegexOptions.Compiled, DefaultTimeout); + private static readonly Regex TemplateConversion = new(@"([^{]|^)\?", RegexOptions.Compiled, DefaultTimeout); + + /// + /// Creates a regular expression matching the given URI template. + /// + /// The uri template. + /// The regular expression string matching the URI template. + /// + /// As this is an allocation-heavy operation, you should ensure that you cache + /// the results in some way. Ideally, this can be done at code generation/compile time + /// for URI templates that can be discovered at the time (e.g. when processing OpenAPI documents.) + /// + public static string CreateMatchingRegex(ReadOnlySpan uriTemplate) + { + return CreateMatchingRegex(uriTemplate.ToString()); + } + + /// + /// Creates a regular expression matching the given URI template. + /// + /// The uri template. + /// The regular expression string matching the URI template. + /// + /// As this is an allocation-heavy operation, you should ensure that you cache + /// the results in some way. Ideally, this can be done at code generation/compile time + /// for URI templates that can be discovered at the time (e.g. when processing OpenAPI documents.) + /// + public static string CreateMatchingRegex(string uriTemplate) + { + string template = TemplateConversion.Replace(uriTemplate, @"$+\?"); + + MatchCollection matches = FindParam.Matches(template); + + string regex = FindParam.Replace(template, Match); + return regex + "$"; + + static string Match(Match m) + { + CaptureCollection captures = m.Groups["lvar"].Captures; + string[] paramNames = ArrayPool.Shared.Rent(captures.Count); + try + { + int written = 0; + foreach (Capture capture in captures.Cast()) + { + if (!string.IsNullOrEmpty(capture.Value)) + { + paramNames[written++] = capture.Value; + } + } + + ReadOnlySpan paramNamesSpan = paramNames.AsSpan()[0..written]; + + string op = m.Groups["op"].Value; + return op switch + { + "?" => GetQueryExpression(paramNamesSpan, prefix: "?"), + "&" => GetQueryExpression(paramNamesSpan, prefix: "&"), + "#" => GetExpression(paramNamesSpan, prefix: "#"), + "/" => GetExpression(paramNamesSpan, prefix: "/"), + "+" => GetExpression(paramNamesSpan), + _ => GetExpression(paramNamesSpan), + }; + } + finally + { + ArrayPool.Shared.Return(paramNames); + } + } + } + + private static string GetQueryExpression(ReadOnlySpan paramNames, string prefix) + { + StringBuilder sb = StringBuilderPool.Shared.Get(); + + try + { + foreach (string paramname in paramNames) + { + sb.Append('\\'); + sb.Append(prefix); + sb.Append('?'); + if (prefix == "?") + { + prefix = "&"; + } + + sb.Append("(?:"); + sb.Append(paramname); + sb.Append('='); + + sb.Append("(?<"); + sb.Append(paramname); + sb.Append('>'); + sb.Append("[^/?&]+"); + sb.Append(')'); + sb.Append(")?"); + } + + return sb.ToString(); + } + finally + { + StringBuilderPool.Shared.Return(sb); + } + } + + private static string GetExpression(ReadOnlySpan paramNames, string? prefix = null) + { + StringBuilder sb = StringBuilderPool.Shared.Get(); + + try + { + string paramDelim = prefix switch + { + "#" => "[^,]+", + "/" => "[^/?]+", + "?" or "&" => "[^&#]+", + ";" => "[^;/?#]+", + "." => "[^./?#]+", + _ => "[^/?&]+", + }; + + foreach (string paramname in paramNames) + { + if (string.IsNullOrEmpty(paramname)) + { + continue; + } + + if (prefix != null) + { + sb.Append('\\'); + sb.Append(prefix); + sb.Append('?'); + if (prefix == "#") + { + prefix = ","; + } + } + + sb.Append("(?<"); + sb.Append(paramname); + sb.Append('>'); + sb.Append(paramDelim); // Param Value + sb.Append(")?"); + } + + return sb.ToString(); + } + finally + { + StringBuilderPool.Shared.Return(sb); + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateResolver{TParameterProvider,TParameterPayload}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateResolver{TParameterProvider,TParameterPayload}.cs new file mode 100644 index 000000000..4efcfa363 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateResolver{TParameterProvider,TParameterPayload}.cs @@ -0,0 +1,353 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using Corvus.HighPerformance; +using Corvus.UriTemplates.TemplateParameterProviders; + +namespace Corvus.UriTemplates; + +/// +/// A delegate for a callback providing parameter names as they are discovered. +/// +/// The type of the state. +/// The parameter name. +/// The state passed by the caller. +public delegate void ParameterNameCallback(ReadOnlySpan name, ref TState state); + +/// +/// A delegate for a callback providing a resolved template. +/// +/// The type of the state. +/// The resolved template. +/// The state passed by the caller. +public delegate void ResolvedUriTemplateCallback(ReadOnlySpan resolvedTemplate, ref TState state); + +/// +/// Resolves a UriTemplate by (optionally, partially) applying parameters to the template, to create a URI (if fully resolved), or a partially resolved URI template. +/// +/// The type of the template parameter provider. +/// The type of the parameter payload. +public static class UriTemplateResolver + where TParameterProvider : ITemplateParameterProvider +{ + private static readonly OperatorInfo OpInfoZero = new(@default: true, first: '\0', separator: ',', named: false, ifEmpty: string.Empty, allowReserved: false); + private static readonly OperatorInfo OpInfoPlus = new(@default: false, first: '\0', separator: ',', named: false, ifEmpty: string.Empty, allowReserved: true); + private static readonly OperatorInfo OpInfoDot = new(@default: false, first: '.', separator: '.', named: false, ifEmpty: string.Empty, allowReserved: false); + private static readonly OperatorInfo OpInfoSlash = new(@default: false, first: '/', separator: '/', named: false, ifEmpty: string.Empty, allowReserved: false); + private static readonly OperatorInfo OpInfoSemicolon = new(@default: false, first: ';', separator: ';', named: true, ifEmpty: string.Empty, allowReserved: false); + private static readonly OperatorInfo OpInfoQuery = new(@default: false, first: '?', separator: '&', named: true, ifEmpty: "=", allowReserved: false); + private static readonly OperatorInfo OpInfoAmpersand = new(@default: false, first: '&', separator: '&', named: true, ifEmpty: "=", allowReserved: false); + private static readonly OperatorInfo OpInfoHash = new(@default: false, first: '#', separator: ',', named: false, ifEmpty: string.Empty, allowReserved: true); + + private enum States + { + CopyingLiterals, + ParsingExpression, + } + + /// + /// Resolve the template into an output result. + /// + /// The type of the state to pass to the callback. + /// An instance of the parameter provider. + /// The template to resolve. + /// If then partially resolve the result. + /// The parameters to apply to the template. + /// The callback which is provided with the resolved template. + /// An optional callback which is provided each parameter name as they are discovered. + /// The state to pass to the callback. + /// if the URI matched the template, and the parameters were resolved successfully. + public static bool TryResolveResult(TParameterProvider parameterProvider, ReadOnlySpan template, bool resolvePartially, in TParameterPayload parameters, ResolvedUriTemplateCallback callback, ParameterNameCallback? parameterNameCallback, ref TState state) + { + ValueStringBuilder builder = new(template.Length * 4); + + try + { + if (TryResolveResult(parameterProvider, template, ref builder, resolvePartially, parameters, parameterNameCallback, ref state)) + { + callback(builder.AsSpan(), ref state); + return true; + } + + return false; + } + finally + { + builder.Dispose(); + } + } + + /// + /// Resolve the template into an output result. + /// + /// The type of the callback state. + /// The parameter provider. + /// The template to resolve. + /// The output buffer into which to resolve the template. + /// If then partially resolve the result. + /// The parameters to apply to the template. + /// An optional callback which is provided each parameter name as they are discovered. + /// The callback state. + /// if the URI matched the template, and the parameters were resolved successfully. + public static bool TryResolveResult(TParameterProvider parameterProvider, ReadOnlySpan template, ref ValueStringBuilder output, bool resolvePartially, in TParameterPayload parameters, ParameterNameCallback? parameterNameCallback, ref TState state) + { + States currentState = States.CopyingLiterals; + int expressionStart = -1; + int expressionEnd = -1; + int index = 0; + + foreach (char character in template) + { + switch (currentState) + { + case States.CopyingLiterals: + if (character == '{') + { + if (expressionStart != -1) + { + output.Append(template[expressionStart..expressionEnd]); + } + + currentState = States.ParsingExpression; + expressionStart = index + 1; + expressionEnd = index + 1; + } + else if (character == '}') + { + return false; + } + else + { + if (expressionStart == -1) + { + expressionStart = index; + } + + expressionEnd = index + 1; + } + + break; + case States.ParsingExpression: + System.Diagnostics.Debug.Assert(expressionStart != -1, "The current expression must be set before parsing the expression."); + + if (character == '}') + { + if (!ProcessExpression(parameterProvider, template[expressionStart..expressionEnd], ref output, resolvePartially, parameters, parameterNameCallback, ref state)) + { + return false; + } + + expressionStart = -1; + expressionEnd = -1; + currentState = States.CopyingLiterals; + } + else + { + expressionEnd = index + 1; + } + + break; + } + + index++; + } + + if (currentState == States.ParsingExpression) + { + return false; + } + + if (expressionStart != -1) + { + output.Append(template[expressionStart..expressionEnd]); + } + + return true; + } + + private static OperatorInfo GetOperator(char operatorIndicator) + { + return operatorIndicator switch + { + '+' => OpInfoPlus, + ';' => OpInfoSemicolon, + '/' => OpInfoSlash, + '#' => OpInfoHash, + '&' => OpInfoAmpersand, + '?' => OpInfoQuery, + '.' => OpInfoDot, + _ => OpInfoZero, + }; + } + + private static bool IsVarNameChar(char c) + { + return (c >= 'A' && c <= 'z') //// Alpha + || (c >= '0' && c <= '9') //// Digit + || c == '_' + || c == '%' + || c == '.'; + } + + private static bool ProcessExpression(TParameterProvider parameterProvider, ReadOnlySpan currentExpression, ref ValueStringBuilder output, bool resolvePartially, in TParameterPayload parameters, ParameterNameCallback? parameterNameCallback, ref TState state) + { + if (currentExpression.Length == 0) + { + return false; + } + + OperatorInfo op = GetOperator(currentExpression[0]); + + int firstChar = op.Default ? 0 : 1; + bool multivariableExpression = false; + int varNameStart = -1; + int varNameEnd = -1; + VariableProcessingState success = VariableProcessingState.Success; + + var varSpec = new VariableSpecification(op, ReadOnlySpan.Empty); + for (int i = firstChar; i < currentExpression.Length; i++) + { + char currentChar = currentExpression[i]; + switch (currentChar) + { + case '*': + if (varSpec.PrefixLength == 0) + { + varSpec.Explode = true; + } + else + { + return false; + } + + break; + + case ':': // Parse Prefix Modifier + currentChar = currentExpression[++i]; + int prefixStart = i; + while (currentChar >= '0' && currentChar <= '9' && i < currentExpression.Length) + { + i++; + if (i < currentExpression.Length) + { + currentChar = currentExpression[i]; + } + } + + if (prefixStart == i) + { + // This is a malformed template + return false; + } + +#if NET8_0_OR_GREATER + varSpec.PrefixLength = int.Parse(currentExpression[prefixStart..i]); +#else + varSpec.PrefixLength = ParseInt(currentExpression[prefixStart..i]); +#endif + i--; + break; + + case ',': + varSpec.VarName = currentExpression[varNameStart..varNameEnd]; + multivariableExpression = true; + + success = ProcessVariable(parameterProvider, ref varSpec, ref output, multivariableExpression, resolvePartially, parameters, parameterNameCallback, ref state); + bool isFirst = varSpec.First; + + // Reset for new variable + varSpec = new VariableSpecification(op, ReadOnlySpan.Empty); + varNameStart = -1; + varNameEnd = -1; + if ((success == VariableProcessingState.Success) || !isFirst || resolvePartially) + { + varSpec.First = false; + } + + break; + + default: + if (IsVarNameChar(currentChar)) + { + if (varNameStart == -1) + { + varNameStart = i; + } + + varNameEnd = i + 1; + } + else + { + return false; + } + + break; + } + } + + if (varNameStart != -1) + { + varSpec.VarName = currentExpression[varNameStart..varNameEnd]; + } + + VariableProcessingState outerSuccess = ProcessVariable(parameterProvider, ref varSpec, ref output, multivariableExpression, resolvePartially, parameters, parameterNameCallback, ref state); + + return outerSuccess != VariableProcessingState.Failure; + +#if !NET8_0_OR_GREATER + static int ParseInt(ReadOnlySpan span) + { + int result = 0; + foreach (char c in span) + { + result = (result * 10) + (c - '0'); + } + + return result; + } +#endif + } + + private static VariableProcessingState ProcessVariable(TParameterProvider parameterProvider, ref VariableSpecification varSpec, ref ValueStringBuilder output, bool multiVariableExpression, bool resolvePartially, in TParameterPayload parameters, ParameterNameCallback? parameterNameCallback, ref TState state) + { + if (parameterNameCallback is ParameterNameCallback callback) + { + callback(varSpec.VarName, ref state); + } + + VariableProcessingState result = parameterProvider.ProcessVariable(ref varSpec, parameters, ref output); + + if (result == VariableProcessingState.NotProcessed) + { + if (resolvePartially) + { + if (multiVariableExpression) + { + if (varSpec.First) + { + output.Append("{"); + } + else + { + output.Append("{&"); + } + + varSpec.CopyTo(ref output); + + output.Append('}'); + } + else + { + output.Append('{'); + varSpec.CopyTo(ref output); + output.Append('}'); + } + } + + return VariableProcessingState.NotProcessed; + } + + return result; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateTable.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateTable.cs new file mode 100644 index 000000000..c5276c9d8 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateTable.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates; + +/// +/// Create builders for a . +/// +public static class UriTemplateTable +{ + /// + /// Create an instance of a URI template table builder. + /// + /// The type of the match values. + /// A builder for a URI template table. + public static UriTemplateTable.Builder CreateBuilder() + { + return new UriTemplateTable.Builder(); + } + + /// + /// Create an instance of a URI template table builder. + /// + /// The type of the match values. + /// The initial capacity of the table. + /// A builder for a URI template table. + public static UriTemplateTable.Builder CreateBuilder(int initialCapacity) + { + return new UriTemplateTable.Builder(initialCapacity); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateTable{TMatch}.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateTable{TMatch}.cs new file mode 100644 index 000000000..af9df2e7e --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Corvus.UriTemplates/UriTemplateTable{TMatch}.cs @@ -0,0 +1,164 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Diagnostics.CodeAnalysis; + +namespace Corvus.UriTemplates; + +/// +/// Matches a URI against a table of URI templates and returns a result value. +/// +/// The type of the value to be matched. +public sealed class UriTemplateTable +{ + private readonly IUriTemplateParser[] parsers; + private readonly TMatch[] matches; + + private UriTemplateTable(IUriTemplateParser[] parsers, TMatch[] matches) + { + this.parsers = parsers; + this.matches = matches; + } + + /// + /// Gets the number of entries in the table. + /// + public int Length => this.parsers.Length; + +#if !NET8_0_OR_GREATER + /// + /// Try to match the uri against the URI templates in the table. + /// + /// The URI to match. + /// The matched result. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// if the URI matched a value in the table. + /// + /// + /// This will find the first match in the table. + /// + /// + /// While the result is you need only dispose it if the method returned . + /// It is, however, safe to dispose in either case. + /// + /// + public bool TryMatch(string uri, [MaybeNullWhen(false)] out TemplateMatchResult match, in bool requiresRootedMatch = false) + { + return this.TryMatch(uri.AsSpan(), out match, requiresRootedMatch); + } +#endif + + /// + /// Try to match the uri against the URI templates in the table. + /// + /// The URI to match. + /// The matched result. + /// If true, then the template requires a rooted match and will not ignore prefixes. This is more efficient when using a fully-qualified template. + /// if the URI matched a value in the table. + /// + /// + /// This will find the first match in the table. + /// + /// + /// While the result is you need only dispose it if the method returned . + /// It is, however, safe to dispose in either case. + /// + /// + public bool TryMatch(ReadOnlySpan uri, [MaybeNullWhen(false)] out TemplateMatchResult match, in bool requiresRootedMatch = false) + { + for (int i = 0; i < this.parsers.Length; ++i) + { + IUriTemplateParser parser = this.parsers[i]; + if (parser.IsMatch(uri, requiresRootedMatch)) + { + match = new(this.matches[i], parser); + return true; + } + } + + match = default; + return false; + } + + /// + /// A builder for a . + /// + public class Builder + { + private readonly List parsers; + private readonly List matches; + + /// + /// Initializes a new instance of the struct. + /// + internal Builder() + { + this.parsers = new(); + this.matches = new(); + } + + /// + /// Initializes a new instance of the struct. + /// + /// The initial capacity of the table. + /// + /// Provide the initial capacity of the table if known. This helps minimize + /// the overhead of re-allocation. + /// + internal Builder(int initialCapacity) + { + this.parsers = new(initialCapacity); + this.matches = new(initialCapacity); + } + + /// + /// Gets the length of the table builder. + /// + public int Count => this.parsers.Count; + + /// + /// Add a uri template and its corresponding match. + /// + /// The URI template to add. + /// The corresponding match to provide if the parser matches. + public void Add(string uriTemplate, TMatch match) + { + this.parsers.Add(UriTemplateParserFactory.CreateParser(uriTemplate)); + this.matches.Add(match); + } + + /// + /// Add a uri template and its corresponding match. + /// + /// The URI template to add. + /// The corresponding match to provide if the parser matches. + public void Add(ReadOnlySpan uriTemplate, TMatch match) + { + this.parsers.Add(UriTemplateParserFactory.CreateParser(uriTemplate)); + this.matches.Add(match); + } + + /// + /// Add a parser and its corresponding match. + /// + /// The parser to add. + /// The corresponding match to provide if the parser matches. + public void Add(IUriTemplateParser parser, TMatch match) + { + this.parsers.Add(parser); + this.matches.Add(match); + } + + /// + /// Convert the builder into a table. + /// + /// The resulting table. + public UriTemplateTable ToTable() + { +#pragma warning disable SA1010 // Opening square brackets should be spaced correctly - analyzers not up to date + return new([.. this.parsers], [.. this.matches]); +#pragma warning restore SA1010 // Opening square brackets should be spaced correctly + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/IndexRange/Index.cs b/Solutions/Corvus.Json.SourceGeneratorTools/IndexRange/Index.cs new file mode 100644 index 000000000..27660bd61 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/IndexRange/Index.cs @@ -0,0 +1,157 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +#pragma warning disable + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if NETSTANDARD2_1 +[assembly: TypeForwardedTo(typeof(System.Index))] +#else +using System.Runtime.CompilerServices; + +namespace System; + +/// Represent a type can be used to index a collection either from the start or the end. +/// +/// Index is used by the C# compiler to support the new index syntax +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; +/// int lastElement = someArray[^1]; // lastElement = 5 +/// +/// +public readonly struct Index : IEquatable +{ + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// +#if !NET35 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. +#if !NET35 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Index FromStart(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. +#if !NET35 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// +#if !NET35 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return "^" + ((uint)Value).ToString(); + + return ((uint)Value).ToString(); + } +} +#endif \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/IndexRange/Range.cs b/Solutions/Corvus.Json.SourceGeneratorTools/IndexRange/Range.cs new file mode 100644 index 000000000..3f9638ea2 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/IndexRange/Range.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +#pragma warning disable + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if NETSTANDARD2_1 +[assembly: TypeForwardedTo(typeof(System.Range))] +#else +using System.Runtime.CompilerServices; + +namespace System; + +/// Represent a range has start and end indexes. +/// +/// Range is used by the C# compiler to support the range syntax. +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; +/// int[] subArray1 = someArray[0..2]; // { 1, 2 } +/// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } +/// +/// +public readonly struct Range : IEquatable +{ + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return Start.GetHashCode() * 31 + End.GetHashCode(); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { + return Start + ".." + End; + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// +#if !NET35 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + [CLSCompliant(false)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start = Start.GetOffset(length); + int end = End.GetOffset(length); + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return (start, end - start); + } +} +#endif \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/Metaschema.cs b/Solutions/Corvus.Json.SourceGeneratorTools/Metaschema.cs new file mode 100644 index 000000000..55400f122 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/Metaschema.cs @@ -0,0 +1,126 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Text.Json; + +namespace Corvus.Json.SourceGenerator; + +/// +/// Apply the metaschema to a document resolver. +/// +internal static class Metaschema +{ + /// + /// Adds metaschema to a document resolver. + /// + /// The document resolver to which to add the metaschema. + /// A reference to the document resolver once the operation has completed. + internal static IDocumentResolver AddMetaschema(this IDocumentResolver documentResolver) + { + var assembly = Assembly.GetAssembly(typeof(Metaschema)); + + Debug.Assert(assembly is not null, "The assembly containing this type must exist"); + + documentResolver.AddDocument( + "http://json-schema.org/draft-04/schema", + Parse(assembly, "metaschema.draft4.schema.json")); + + documentResolver.AddDocument( + "http://json-schema.org/draft-06/schema", + Parse(assembly, "metaschema.draft6.schema.json")); + + documentResolver.AddDocument( + "http://json-schema.org/draft-07/schema", + Parse(assembly, "metaschema.draft7.schema.json")); + + documentResolver.AddDocument( + "https://json-schema.org/draft/2019-09/schema", + Parse(assembly, "metaschema.draft2019_09.schema.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2019-09/meta/applicator", + Parse(assembly, "metaschema.draft2019_09.meta.applicator.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2019-09/meta/content", + Parse(assembly, "metaschema.draft2019_09.meta.content.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2019-09/meta/core", + Parse(assembly, "metaschema.draft2019_09.meta.core.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2019-09/meta/format", + Parse(assembly, "metaschema.draft2019_09.meta.format.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2019-09/meta/hyper-schema", + Parse(assembly, "metaschema.draft2019_09.meta.hyper-schema.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2019-09/meta/meta-data", + Parse(assembly, "metaschema.draft2019_09.meta.meta-data.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2019-09/meta/validation", + Parse(assembly, "metaschema.draft2019_09.meta.validation.json")); + + documentResolver.AddDocument( + "https://json-schema.org/draft/2020-12/schema", + Parse(assembly, "metaschema.draft2020_12.schema.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2020-12/meta/applicator", + Parse(assembly, "metaschema.draft2020_12.meta.applicator.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2020-12/meta/content", + Parse(assembly, "metaschema.draft2020_12.meta.content.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2020-12/meta/core", + Parse(assembly, "metaschema.draft2020_12.meta.core.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2020-12/meta/format-annotation", + Parse(assembly, "metaschema.draft2020_12.meta.format-annotation.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2020-12/meta/format-assertion", + Parse(assembly, "metaschema.draft2020_12.meta.format-assertion.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2020-12/meta/hyper-schema", + Parse(assembly, "metaschema.draft2020_12.meta.hyper-schema.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2020-12/meta/meta-data", + Parse(assembly, "metaschema.draft2020_12.meta.meta-data.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2020-12/meta/unevaluated", + Parse(assembly, "metaschema.draft2020_12.meta.unevaluated.json")); + documentResolver.AddDocument( + "https://json-schema.org/draft/2020-12/meta/validation", + Parse(assembly, "metaschema.draft2020_12.meta.validation.json")); + + documentResolver.AddDocument( + "https://corvus-oss.org/json-schema/2020-12/schema", + Parse(assembly, "metaschema.corvus.schema.json")); + documentResolver.AddDocument( + "https://corvus-oss.org/json-schema/2020-12/meta/corvus-extensions", + Parse(assembly, "metaschema.corvus.meta.corvus-extensions.json")); + + return documentResolver; + } + + private static JsonDocument Parse(Assembly assembly, string resourceName) + { + try + { + return JsonDocument.Parse(ReadResource(assembly, resourceName)); + } + catch (Exception ex) + { + throw new InvalidOperationException($"The metaschema could not be parsed: {resourceName}", ex); + } + } + + private static string ReadResource(Assembly assembly, string path) + { + string name = path; + using Stream? resourceStream = assembly.GetManifestResourceStream(name); + Debug.Assert(resourceStream is not null, $"The manifest resource stream {name} does not exist."); + using var reader = new StreamReader(resourceStream); + return reader.ReadToEnd(); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/README.md b/Solutions/Corvus.Json.SourceGeneratorTools/README.md new file mode 100644 index 000000000..f1856f50e --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/README.md @@ -0,0 +1,75 @@ +# Corvus.Json.SourceGeneratorTools + +This provides a set of self-contained libraries for use in Source Generators that wish to generate code from JSON Schema. + +It is only ever intended to be used as a _private asset_ in such generators, and is not intended to be referenced by end-user libraries. + +It minimizes external dependencies by compiling in the source from the following libraries: + +`Corvus.Json.CodeGeneration.*` +`Corvus.UriTemplates` +`Corvus.Json.JsonReference` +`Corvus.Json.ExtendedTypes` +`Corvus.Json.JsonSchema` + +It also references `8.x` and prior versions of the System libraries which simplifies referencing in your custom Source Generator or Analyzer. + +## Implementation Helpers + +We provide a helper class to simplify some of the most common code-generation requirements. + +For example, `SourceGeneratorHelpers.GenerateCode()` takes a `SourceProductionContext` and an instance of a type called `TypesToGenerate` which +describes the set of root types to generate (whose dependencies will be inferred and automatically generated if not explicitly specified). + +It also takes an instance of a `VocabularyRegistry`. This is a `Corvus.Json.CodeGeneration` type; a default instance can be retrieved by calling `SourceGeneratorHelpers.CreateVocabularyRegistry()` and passing it an `IDocumentResolver` that has been preloaded with the standard vocabulary metaschema. + +A suitable `IDocumentResolver` for this purpose can be retrieved by calling `SourceGeneratorHelpers.CreateMetaSchemaResolver()`. + +For code generation, you will also need a `PrepoulatedDocumentResolver` containing the relevant additional analyzer files. A method +called `SourceGeneratorHelpers.BuildDocumentResolver()` will create such a document resolver for you, if provided with an `ImmutableArray`. + +Exactly when you construct these entities and types depends on your approach to source generation, and its caching strategy. + +Your generator code may look something like: + +```csharp + private static readonly IDocumentResolver MetaSchemaResolver = CreateMetaSchemaResolver(); + private static readonly VocabularyRegistry VocabularyRegistry = CreateVocabularyRegistry(MetaSchemaResolver); + + public void Initialize(IncrementalGeneratorInitializationContext initializationContext) + { + // Get global options + IncrementalValueProvider globalOptions = initializationContext.AnalyzerConfigOptionsProvider.Select(GetGlobalOptions); + + IncrementalValuesProvider jsonSourceFiles = initializationContext.AdditionalTextsProvider.Where(p => p.Path.EndsWith(".json")); + + IncrementalValueProvider documentResolver = jsonSourceFiles.Collect().Select(SourceGeneratorHelpers.BuildDocumentResolver); + + IncrementalValueProvider generationContext = documentResolver.Combine(globalOptions).Select((r, c) => new SourceGeneratorHelpers.GenerationContext(r.Left, r.Right)); + + // Typically built from e.g. attributes or other syntax on partial classes. + IncrementalValuesProvider generationSpecifications = BuildGenerationSpecifications(); + + IncrementalValueProvider typesToGenerate = generationSpecifications.Collect().Combine(generationContext).Select((c, t) => new SourceGeneratorHelpers.TypesToGenerate(c.Left, c.Right)); + + initializationContext.RegisterSourceOutput(typesToGenerate, GenerateCode); + } + + private static void GenerateCode(SourceProductionContext context, SourceGeneratorHelpers.TypesToGenerate generationSource) + { + SourceGeneratorHelpers.GenerateCode(context, generationSource, VocabularyRegistry); + } + +``` + +## Compatibility and Interoperability + +The code *generated by your Source Generator* should be compiled into assemblies that reference `Corvus.Json.ExtendedTypes` at runtime in the usual way. + +Should you wish to use schema *in the implementation of your Source Generator itself*, you will need to compile that generated code into `netstandard2.0` libraries that reference the `Corvus.Json.SourceGeneratorTools` assembly. Typically, this will be your Source Generator assembly itself. + +We recommend using the command line tool to generate these types to avoid analyzer inception! (It works, but it is confusing!) + +For example, we provide generated code for the standard JSON Schema meta-schema, and OpenAPI 3.0 and 3.1, compiled into this library. You cannot reference the `Corvus.Json.JsonSchema.*` packages directly in your Source Generator. + + diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/SourceGeneratorHelpers.cs b/Solutions/Corvus.Json.SourceGeneratorTools/SourceGeneratorHelpers.cs new file mode 100644 index 000000000..712acb185 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/SourceGeneratorHelpers.cs @@ -0,0 +1,358 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Collections.Immutable; +using System.Text; +using Corvus.Json.CodeGeneration; +using Corvus.Json.CodeGeneration.CSharp; +using Corvus.Json.CodeGeneration.DocumentResolvers; +using Corvus.Json.SourceGenerator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Corvus.Json.SourceGeneratorTools; + +/// +/// Useful methods for building source generators for JSON Schema. +/// +public static class SourceGeneratorHelpers +{ + private static readonly DiagnosticDescriptor Crv1001ErrorGeneratingCSharpCode = + new( + id: "CRV1001", + title: "JSON Schema Type Generator Error", + messageFormat: "Error generating C# code: {0}", + category: "JsonSchemaCodeGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor Crv1000ErrorAddingTypeDeclarations = + new( + id: "CRV1000", + title: "JSON Schema Type Generator Error", + messageFormat: "Error adding type declarations for path '{0}': {1}", + category: "JsonSchemaCodeGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// Generate code into a source production context. + /// + /// The . + /// The types to generate. + /// The vocabulary registry. + public static void GenerateCode(SourceProductionContext context, TypesToGenerate typesToGenerate, VocabularyRegistry vocabularyRegistry) + { + if (typesToGenerate.GenerationSpecifications.Length == 0) + { + // Nothing to generate + return; + } + + List typeDeclarationsToGenerate = []; + List namedTypes = []; + JsonSchemaTypeBuilder typeBuilder = new(typesToGenerate.DocumentResolver, vocabularyRegistry); + + string? defaultNamespace = null; + + foreach (GenerationSpecification spec in typesToGenerate.GenerationSpecifications) + { + if (context.CancellationToken.IsCancellationRequested) + { + return; + } + + string schemaFile = spec.Location; + JsonReference reference = new(schemaFile); + TypeDeclaration rootType; + try + { + rootType = typeBuilder.AddTypeDeclarations(reference, typesToGenerate.FallbackVocabulary, spec.RebaseToRootPath, context.CancellationToken); + } + catch (Exception ex) + { + context.ReportDiagnostic( + Diagnostic.Create( + Crv1000ErrorAddingTypeDeclarations, + Location.None, + reference, + ex.Message)); + + return; + } + + typeDeclarationsToGenerate.Add(rootType); + + defaultNamespace ??= spec.Namespace; + + namedTypes.Add( + new CSharpLanguageProvider.NamedType( + rootType.ReducedTypeDeclaration().ReducedType.LocatedSchema.Location, + spec.TypeName, + spec.Namespace)); + } + + CSharpLanguageProvider.Options options = new( + defaultNamespace ?? "GeneratedTypes", + [.. namedTypes], + useOptionalNameHeuristics: typesToGenerate.UseOptionalNameHeuristics, + alwaysAssertFormat: typesToGenerate.AlwaysAssertFormat, + optionalAsNullable: typesToGenerate.OptionalAsNullable, + disabledNamingHeuristics: [.. typesToGenerate.DisabledNamingHeuristics], + fileExtension: ".g.cs"); + + var languageProvider = CSharpLanguageProvider.DefaultWithOptions(options); + + IReadOnlyCollection generatedCode; + + try + { + generatedCode = + typeBuilder.GenerateCodeUsing( + languageProvider, + context.CancellationToken, + typeDeclarationsToGenerate); + } + catch (Exception ex) + { + context.ReportDiagnostic( + Diagnostic.Create( + Crv1001ErrorGeneratingCSharpCode, + Location.None, + ex.Message)); + + return; + } + + foreach (GeneratedCodeFile codeFile in generatedCode) + { + if (!context.CancellationToken.IsCancellationRequested) + { + context.AddSource(codeFile.FileName, SourceText.From(codeFile.FileContent, Encoding.UTF8)); + } + } + } + + /// + /// Build a document resolver populated with the given array of additional text sources. + /// + /// The additional text source. + /// The cancellation token. + /// A compound document resolver containing the JSON documents registered as additional text sources. + public static PrepopulatedDocumentResolver BuildDocumentResolver(ImmutableArray source, CancellationToken token) + { + PrepopulatedDocumentResolver newResolver = new(); + foreach (AdditionalText additionalText in source) + { + if (token.IsCancellationRequested) + { + return newResolver; + } + + string? json = additionalText.GetText(token)?.ToString(); + if (json is string j) + { + try + { + var doc = JsonDocument.Parse(j); + if (SchemaReferenceNormalization.TryNormalizeSchemaReference(additionalText.Path, string.Empty, out string? normalizedReference)) + { + newResolver.AddDocument(normalizedReference, doc); + } + } + catch (JsonException) + { + // We just ignore bad JSON files. + } + } + } + + return newResolver; + } + + /// + /// Create a containing the + /// well-known JSON Schema meta-schema. + /// + /// A document resolver containing the meta-schema. + public static PrepopulatedDocumentResolver CreateMetaSchemaResolver() + { + PrepopulatedDocumentResolver metaSchemaResolver = new(); + metaSchemaResolver.AddMetaschema(); + + return metaSchemaResolver; + } + + /// + /// Creates a vocabulary registry pre-populated with the JSON schema draft vocabularies. + /// + /// The document resolver from which the meta-schema can + /// be resolved. (Typically created using . + /// An instance of the vocabulary registry. + public static VocabularyRegistry CreateVocabularyRegistry(IDocumentResolver documentResolver) + { + VocabularyRegistry vocabularyRegistry = new(); + + // Add support for the vocabularies we are interested in. + CodeGeneration.Draft202012.VocabularyAnalyser.RegisterAnalyser(documentResolver, vocabularyRegistry); + CodeGeneration.Draft201909.VocabularyAnalyser.RegisterAnalyser(documentResolver, vocabularyRegistry); + CodeGeneration.Draft7.VocabularyAnalyser.RegisterAnalyser(vocabularyRegistry); + CodeGeneration.Draft6.VocabularyAnalyser.RegisterAnalyser(vocabularyRegistry); + CodeGeneration.Draft4.VocabularyAnalyser.RegisterAnalyser(vocabularyRegistry); + CodeGeneration.OpenApi30.VocabularyAnalyser.RegisterAnalyser(vocabularyRegistry); + + // And register the custom vocabulary for Corvus extensions. + vocabularyRegistry.RegisterVocabularies( + CodeGeneration.CorvusVocabulary.SchemaVocabulary.DefaultInstance); + + return vocabularyRegistry; + } + + /// + /// Defines the types to generate and the context in which they should be generated. + /// + /// The generation specifications. + /// The generation context. + public readonly struct TypesToGenerate(ImmutableArray generationSpecifications, GenerationContext generationContext) + { + /// + /// Gets the generation specifications for the types to generate. + /// + public ImmutableArray GenerationSpecifications => generationSpecifications; + + /// + /// Gets the document resolver for the types to generate. + /// + public IDocumentResolver DocumentResolver => generationContext.DocumentResolver; + + /// + /// Gets the fallback vocabulary for the types to generate. + /// + public IVocabulary FallbackVocabulary => generationContext.FallbackVocabulary; + + /// + /// Gets a value indicating whether optional values should be treated as nullable. + /// + public bool OptionalAsNullable => generationContext.OptionalAsNullable; + + /// + /// Gets a value indicating whether optional name heuristics should be used. + /// + public bool UseOptionalNameHeuristics => generationContext.UseOptionalNameHeuristics; + + /// + /// Gets the names of the disabled naming heuristics. + /// + public ImmutableArray DisabledNamingHeuristics => generationContext.DisabledNamingHeuristics; + + /// + /// Gets a value indicating whether to assert format regardless of the vocabulary. + /// + public bool AlwaysAssertFormat => generationContext.AlwaysAssertFormat; + } + + /// + /// Defines the specification for generating a single type and its dependencies. + /// + /// The .NET name of the type. + /// The .NET namespace for the type. + /// The schema location of the type. + /// Indicates whether to rebase the schema as a document root. + public readonly struct GenerationSpecification(string typeName, string ns, string location, bool rebaseToRootPath) + { + /// + /// Gets the .NET name of the type. + /// + public string TypeName { get; } = typeName; + + /// + /// Gets the .NET namespace for the type. + /// + public string Namespace { get; } = ns; + + /// + /// Gets the schema location of the type. + /// + public string Location { get; } = location; + + /// + /// Gets a value indicating whether to rebase the schema as document root. + /// + public bool RebaseToRootPath { get; } = rebaseToRootPath; + } + + /// + /// Gets teh generation context for the types to generate. + /// + /// The document resolver. + /// The global options. + public readonly struct GenerationContext(IDocumentResolver resolver, GlobalOptions globalOptions) + { + /// + /// Gets the document resolver. + /// + public IDocumentResolver DocumentResolver { get; } = resolver; + + /// + /// Gets the global fallback vocabulary. This can be overridden for particular types to generate. + /// + public IVocabulary FallbackVocabulary { get; } = globalOptions.FallbackVocabulary; + + /// + /// Gets a value indicating whether optional values should be treated as nullable. + /// + public bool OptionalAsNullable { get; } = globalOptions.OptionalAsNullable; + + /// + /// Gets a value indicating whether optional name heuristics should be used. + /// + public bool UseOptionalNameHeuristics { get; } = globalOptions.UseOptionalNameHeuristics; + + /// + /// Gets the names of the disabled naming heuristics. + /// + public ImmutableArray DisabledNamingHeuristics { get; } = globalOptions.DisabledNamingHeuristics; + + /// + /// Gets a value indicating whether to assert format regardless of the vocabulary. + /// + public bool AlwaysAssertFormat { get; } = globalOptions.AlwaysAssertFormat; + } + + /// + /// Gets the global options for the source generator. + /// + /// The global fallback vocabulary. This can be overridden for particular types to generate. + /// Indicates whether optional values should be treated as nullable. + /// Indicates whether optional name heuristics should be used. + /// Indicates whether to assert format regardless of the vocabulary. + /// The names of the disabled naming heuristics. + public readonly struct GlobalOptions(IVocabulary fallbackVocabulary, bool optionalAsNullable, bool useOptionalNameHeuristics, bool alwaysAssertFormat, ImmutableArray disabledNamingHeuristics) + { + /// + /// Gets the global fallback vocabulary. This can be overridden for particular types to generate. + /// + public IVocabulary FallbackVocabulary { get; } = fallbackVocabulary; + + /// + /// Gets a value indicating whether optional values should be treated as nullable. + /// + public bool OptionalAsNullable { get; } = optionalAsNullable; + + /// + /// Gets a value indicating whether optional name heuristics should be used. + /// + public bool UseOptionalNameHeuristics { get; } = useOptionalNameHeuristics; + + /// + /// Gets the names of the disabled naming heuristics. + /// + public ImmutableArray DisabledNamingHeuristics { get; } = disabledNamingHeuristics; + + /// + /// Gets a value indicating whether to assert format regardless of the vocabulary. + /// + public bool AlwaysAssertFormat { get; } = alwaysAssertFormat; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/corvus/meta/corvus-extensions.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/corvus/meta/corvus-extensions.json new file mode 100644 index 000000000..12eef7d72 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/corvus/meta/corvus-extensions.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://corvus-oss.org/json-schema/2020-12/meta/corvus-extensions", + "$dynamicAnchor": "meta", + + "title": "Corvus vocabulary meta-schema for corvus extension keywords", + "type": [ "object", "boolean" ], + "properties": { + "$corvusTypeName": { "type": "string", "description": "The explicit type name for a schema during code generation." } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/corvus/schema.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/corvus/schema.json new file mode 100644 index 000000000..52a6c2cf7 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/corvus/schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://corvus-oss.org/json-schema/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true, + "https://corvus-oss.org/json-schema/2020-12/vocab/corvus-extensions": false + }, + "$dynamicAnchor": "meta", + + "title": "Core and Validation specifications meta-schema", + "allOf": [ + { "$ref": "https://json-schema.org/draft/2020-12/meta/core" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/applicator" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/unevaluated" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/validation" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/meta-data" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/format-annotation" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/content" }, + { "$ref": "https://corvus-oss.org/json-schema/2020-12/meta/corvus-extensions" } + ], + "type": [ "object", "boolean" ], + "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.", + "properties": { + "definitions": { + "$comment": "\"definitions\" has been replaced by \"$defs\".", + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "deprecated": true, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$dynamicRef": "#meta" }, + { "$ref": "meta/validation#/$defs/stringArray" } + ] + }, + "deprecated": true, + "default": {} + }, + "$recursiveAnchor": { + "$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".", + "$ref": "meta/core#/$defs/anchorString", + "deprecated": true + }, + "$recursiveRef": { + "$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".", + "$ref": "meta/core#/$defs/uriReferenceString", + "deprecated": true + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/applicator.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/applicator.json new file mode 100644 index 000000000..80a04ca13 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/applicator.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/applicator", + "$recursiveAnchor": true, + + "title": "Applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "additionalItems": { "$recursiveRef": "#" }, + "unevaluatedItems": { "$recursiveRef": "#" }, + "items": { + "anyOf": [ + { "$recursiveRef": "#" }, + { "$ref": "#/$defs/schemaArray" } + ] + }, + "contains": { "$recursiveRef": "#" }, + "additionalProperties": { "$recursiveRef": "#" }, + "unevaluatedProperties": { "$recursiveRef": "#" }, + "properties": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { + "$recursiveRef": "#" + } + }, + "propertyNames": { "$recursiveRef": "#" }, + "if": { "$recursiveRef": "#" }, + "then": { "$recursiveRef": "#" }, + "else": { "$recursiveRef": "#" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$recursiveRef": "#" } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$recursiveRef": "#" } + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/content.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/content.json new file mode 100644 index 000000000..7a9af9754 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/content.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/content", + "$recursiveAnchor": true, + + "title": "Content vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "contentSchema": { "$recursiveRef": "#" } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/core.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/core.json new file mode 100644 index 000000000..2cf1fedd3 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/core.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/core", + "$recursiveAnchor": true, + + "title": "Core vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$anchor": { + "type": "string", + "pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$recursiveRef": { + "type": "string", + "format": "uri-reference" + }, + "$recursiveAnchor": { + "type": "boolean", + "default": false + }, + "$vocabulary": { + "type": "object", + "propertyNames": { + "type": "string", + "format": "uri" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "$comment": { + "type": "string" + }, + "$defs": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/format.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/format.json new file mode 100644 index 000000000..87177f351 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/format.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/format", + "$recursiveAnchor": true, + + "title": "Format vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/hyper-schema.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/hyper-schema.json new file mode 100644 index 000000000..cd81bd875 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/hyper-schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/hyper-schema", + "$id": "https://json-schema.org/draft/2019-09/meta/hyper-schema", + "$recursiveAnchor": true, + + "title": "JSON Hyper-Schema Vocabulary Schema", + "type": ["object", "boolean"], + "properties": { + "base": { + "type": "string", + "format": "uri-template" + }, + "links": { + "type": "array", + "items": { + "$ref": "https://json-schema.org/draft/2019-09/links" + } + } + }, + "links": [ + { + "rel": "self", + "href": "{+%24id}" + } + ] +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/meta-data.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/meta-data.json new file mode 100644 index 000000000..28d3e8ab1 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/meta-data.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/meta-data", + "$recursiveAnchor": true, + + "title": "Meta-data vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/validation.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/validation.json new file mode 100644 index 000000000..5356df926 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/meta/validation.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/validation", + "$recursiveAnchor": true, + + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/schema.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/schema.json new file mode 100644 index 000000000..2248a0c80 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2019-09/schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/schema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true, + "https://json-schema.org/draft/2019-09/vocab/applicator": true, + "https://json-schema.org/draft/2019-09/vocab/validation": true, + "https://json-schema.org/draft/2019-09/vocab/meta-data": true, + "https://json-schema.org/draft/2019-09/vocab/format": false, + "https://json-schema.org/draft/2019-09/vocab/content": true + }, + "$recursiveAnchor": true, + + "title": "Core and Validation specifications meta-schema", + "allOf": [ + {"$ref": "meta/core"}, + {"$ref": "meta/applicator"}, + {"$ref": "meta/validation"}, + {"$ref": "meta/meta-data"}, + {"$ref": "meta/format"}, + {"$ref": "meta/content"} + ], + "type": ["object", "boolean"], + "properties": { + "definitions": { + "$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.", + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$recursiveRef": "#" }, + { "$ref": "meta/validation#/$defs/stringArray" } + ] + } + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/applicator.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/applicator.json new file mode 100644 index 000000000..f4775974a --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/applicator.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/applicator", + "$dynamicAnchor": "meta", + + "title": "Applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "prefixItems": { "$ref": "#/$defs/schemaArray" }, + "items": { "$dynamicRef": "#meta" }, + "contains": { "$dynamicRef": "#meta" }, + "additionalProperties": { "$dynamicRef": "#meta" }, + "properties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "default": {} + }, + "propertyNames": { "$dynamicRef": "#meta" }, + "if": { "$dynamicRef": "#meta" }, + "then": { "$dynamicRef": "#meta" }, + "else": { "$dynamicRef": "#meta" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$dynamicRef": "#meta" } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$dynamicRef": "#meta" } + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/content.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/content.json new file mode 100644 index 000000000..76e3760d2 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/content.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/content", + "$dynamicAnchor": "meta", + + "title": "Content vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "contentEncoding": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentSchema": { "$dynamicRef": "#meta" } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/core.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/core.json new file mode 100644 index 000000000..691862289 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/core.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/core", + "$dynamicAnchor": "meta", + + "title": "Core vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "$ref": "#/$defs/uriReferenceString", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$" + }, + "$schema": { "$ref": "#/$defs/uriString" }, + "$ref": { "$ref": "#/$defs/uriReferenceString" }, + "$anchor": { "$ref": "#/$defs/anchorString" }, + "$dynamicRef": { "$ref": "#/$defs/uriReferenceString" }, + "$dynamicAnchor": { "$ref": "#/$defs/anchorString" }, + "$vocabulary": { + "type": "object", + "propertyNames": { "$ref": "#/$defs/uriString" }, + "additionalProperties": { + "type": "boolean" + } + }, + "$comment": { + "type": "string" + }, + "$defs": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" } + } + }, + "$defs": { + "anchorString": { + "type": "string", + "pattern": "^[A-Za-z_][-A-Za-z0-9._]*$" + }, + "uriString": { + "type": "string", + "format": "uri" + }, + "uriReferenceString": { + "type": "string", + "format": "uri-reference" + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/format-annotation.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/format-annotation.json new file mode 100644 index 000000000..3479e6695 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/format-annotation.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/format-annotation", + "$dynamicAnchor": "meta", + + "title": "Format vocabulary meta-schema for annotation results", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/format-assertion.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/format-assertion.json new file mode 100644 index 000000000..1a4f106cf --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/format-assertion.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/format-assertion", + "$dynamicAnchor": "meta", + + "title": "Format vocabulary meta-schema for assertion results", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/hyper-schema.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/hyper-schema.json new file mode 100644 index 000000000..f0beb5a4f --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/hyper-schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/hyper-schema", + "$id": "https://json-schema.org/draft/2020-12/meta/hyper-schema", + "$dynamicAnchor": "meta", + + "title": "JSON Hyper-Schema Vocabulary Schema", + "type": ["object", "boolean"], + "properties": { + "base": { + "type": "string", + "format": "uri-template" + }, + "links": { + "type": "array", + "items": { + "$ref": "https://json-schema.org/draft/2020-12/links" + } + } + }, + "links": [ + { + "rel": "self", + "href": "{+%24id}" + } + ] +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/meta-data.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/meta-data.json new file mode 100644 index 000000000..4049ab21b --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/meta-data.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/meta-data", + "$dynamicAnchor": "meta", + + "title": "Meta-data vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/unevaluated.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/unevaluated.json new file mode 100644 index 000000000..93779e54e --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/unevaluated.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/unevaluated", + "$dynamicAnchor": "meta", + + "title": "Unevaluated applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "unevaluatedItems": { "$dynamicRef": "#meta" }, + "unevaluatedProperties": { "$dynamicRef": "#meta" } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/validation.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/validation.json new file mode 100644 index 000000000..ebb75db77 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/meta/validation.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/validation", + "$dynamicAnchor": "meta", + + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/schema.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/schema.json new file mode 100644 index 000000000..d5e2d31c3 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft2020-12/schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "$dynamicAnchor": "meta", + + "title": "Core and Validation specifications meta-schema", + "allOf": [ + {"$ref": "meta/core"}, + {"$ref": "meta/applicator"}, + {"$ref": "meta/unevaluated"}, + {"$ref": "meta/validation"}, + {"$ref": "meta/meta-data"}, + {"$ref": "meta/format-annotation"}, + {"$ref": "meta/content"} + ], + "type": ["object", "boolean"], + "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.", + "properties": { + "definitions": { + "$comment": "\"definitions\" has been replaced by \"$defs\".", + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "deprecated": true, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$dynamicRef": "#meta" }, + { "$ref": "meta/validation#/$defs/stringArray" } + ] + }, + "deprecated": true, + "default": {} + }, + "$recursiveAnchor": { + "$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".", + "$ref": "meta/core#/$defs/anchorString", + "deprecated": true + }, + "$recursiveRef": { + "$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".", + "$ref": "meta/core#/$defs/uriReferenceString", + "deprecated": true + } + } +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft4/schema.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft4/schema.json new file mode 100644 index 000000000..bcbb84743 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft4/schema.json @@ -0,0 +1,149 @@ +{ + "id": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] + }, + "simpleTypes": { + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "$schema": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { "$ref": "#/definitions/positiveInteger" }, + "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/positiveInteger" }, + "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { "$ref": "#/definitions/positiveInteger" }, + "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "dependencies": { + "exclusiveMaximum": [ "maximum" ], + "exclusiveMinimum": [ "minimum" ] + }, + "default": {} +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft6/schema.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft6/schema.json new file mode 100644 index 000000000..bd3e763bc --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft6/schema.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://json-schema.org/draft-06/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "examples": { + "type": "array", + "items": {} + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": {}, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": {} +} diff --git a/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft7/schema.json b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft7/schema.json new file mode 100644 index 000000000..fb92c7f75 --- /dev/null +++ b/Solutions/Corvus.Json.SourceGeneratorTools/metaschema/draft7/schema.json @@ -0,0 +1,172 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": { "$ref": "#" }, + "then": { "$ref": "#" }, + "else": { "$ref": "#" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} diff --git a/Solutions/Corvus.JsonSchema.sln b/Solutions/Corvus.JsonSchema.sln index b876e6849..7241ec2de 100644 --- a/Solutions/Corvus.JsonSchema.sln +++ b/Solutions/Corvus.JsonSchema.sln @@ -1,417 +1,217 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.3.32611.2 +VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.ExtendedTypes", "Corvus.Json.ExtendedTypes\Corvus.Json.ExtendedTypes.csproj", "{B0113059-9582-4B82-A591-5E36AB18923F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.Benchmarking", "Corvus.Json.Benchmarking\Corvus.Json.Benchmarking.csproj", "{D1227031-B358-4B5A-94BC-3CD28FEBADEC}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Specifications", "Specifications", "{D9BD60AE-2972-4608-8E0D-16929D42A0D9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration", "Corvus.Json.CodeGeneration\Corvus.Json.CodeGeneration.csproj", "{7F8BFC0B-3CBA-4202-8E51-63685EB3884A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.Specs", "Corvus.Json.Specs\Corvus.Json.Specs.csproj", "{20AFC7E7-5D24-4BB7-8F05-115D19474D05}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.201909", "Corvus.Json.CodeGeneration.201909\Corvus.Json.CodeGeneration.201909.csproj", "{D0DB7AA2-9D80-4BD2-8C26-8C3914D7F721}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.Patch", "Corvus.Json.Patch\Corvus.Json.Patch.csproj", "{1BE2E1C5-0726-418F-9041-18F9A9719FCA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.202012", "Corvus.Json.CodeGeneration.202012\Corvus.Json.CodeGeneration.202012.csproj", "{97DFF763-8EF1-48BE-849F-159F239B1040}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.Patch.SpecGenerator", "Corvus.Json.Patch.SpecGenerator\Corvus.Json.Patch.SpecGenerator.csproj", "{94B53FC6-F80E-4ADB-BB44-A7B12520177A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.4", "Corvus.Json.CodeGeneration.4\Corvus.Json.CodeGeneration.4.csproj", "{1C116DF6-5FB5-4188-B64F-C5D193272B58}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{911B0F4F-E689-48AA-8D4B-44AFBA1DF850}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.6", "Corvus.Json.CodeGeneration.6\Corvus.Json.CodeGeneration.6.csproj", "{5700545E-730A-417A-8B61-252B76345F07}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.JsonPatch.Benchmarking", "Corvus.JsonPatch.Benchmarking\Corvus.JsonPatch.Benchmarking.csproj", "{A67E1B7C-0990-4CA0-846A-F4DA4FCA20BF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.7", "Corvus.Json.CodeGeneration.7\Corvus.Json.CodeGeneration.7.csproj", "{5D0D6536-6350-43C6-BA2F-A488D265B3A9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.Benchmarking", "Corvus.Json.Benchmarking\Corvus.Json.Benchmarking.csproj", "{70C78E48-B257-4FD1-98E1-61D1F68831C2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.CorvusVocabulary", "Corvus.Json.CodeGeneration.CorvusVocabulary\Corvus.Json.CodeGeneration.CorvusVocabulary.csproj", "{486C2B04-DCCF-4957-A580-AD9FD26541C4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.CodeGeneration.202012", "Corvus.Json.CodeGeneration.202012\Corvus.Json.CodeGeneration.202012.csproj", "{D4C59DE5-AEA4-42EF-BED7-5457DF5BD465}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.CSharp", "Corvus.Json.CodeGeneration.CSharp\Corvus.Json.CodeGeneration.CSharp.csproj", "{90DC27A6-4526-40E8-9754-8428611E41F2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.CodeGenerator", "Corvus.Json.CodeGenerator\Corvus.Json.CodeGenerator.csproj", "{D878C49A-AD4B-4BBF-9B9C-168B99C83B00}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.CSharp.QuickStart", "Corvus.Json.CodeGeneration.CSharp.QuickStart\Corvus.Json.CodeGeneration.CSharp.QuickStart.csproj", "{8FA2FA0A-D651-4B70-85CF-CB2C9EEFFF76}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.JsonSchema.SpecGenerator", "Corvus.JsonSchema.SpecGenerator\Corvus.JsonSchema.SpecGenerator.csproj", "{428FA47D-641B-41B4-A49C-D39CEAF48B0A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.HttpClientDocumentResolver", "Corvus.Json.CodeGeneration.HttpClientDocumentResolver\Corvus.Json.CodeGeneration.HttpClientDocumentResolver.csproj", "{D24FD762-FD1D-4E0E-BF84-4C493E5F4912}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Code Generation", "Code Generation", "{95D2B90C-DBBA-4AF7-BAE8-4E933E050E75}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.OpenApi30", "Corvus.Json.CodeGeneration.OpenApi30\Corvus.Json.CodeGeneration.OpenApi30.csproj", "{85E4FD70-C057-45B5-963D-1F989CAC9596}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Generated Schema Models", "Generated Schema Models", "{3D0E15DD-5C66-4C2E-BAD1-F10A3B7B4A31}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.YamlPreProcessor", "Corvus.Json.CodeGeneration.YamlPreProcessor\Corvus.Json.CodeGeneration.YamlPreProcessor.csproj", "{0FE726E3-64DD-462B-8986-17C9101161FE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.JsonSchema.Draft201909", "Corvus.Json.JsonSchema.Draft201909\Corvus.Json.JsonSchema.Draft201909.csproj", "{DDD045D0-ECF3-4696-B4E0-28EC58B07D7B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGenerator", "Corvus.Json.CodeGenerator\Corvus.Json.CodeGenerator.csproj", "{25351BBD-5E6A-4E64-821C-4CE03C858B4D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.JsonSchema.Draft202012", "Corvus.Json.JsonSchema.Draft202012\Corvus.Json.JsonSchema.Draft202012.csproj", "{CD841AC2-88A2-40CA-995F-F8DABAED339C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.ExtendedTypes", "Corvus.Json.ExtendedTypes\Corvus.Json.ExtendedTypes.csproj", "{49266EDE-FDD4-4653-A5F2-3B7568CB8F1A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.JsonSchema.Draft6", "Corvus.Json.JsonSchema.Draft6\Corvus.Json.JsonSchema.Draft6.csproj", "{16DC75F0-A4B0-44A7-B0E5-BFA3B9F9AA59}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.JsonReference", "Corvus.Json.JsonReference\Corvus.Json.JsonReference.csproj", "{DB878532-48E7-4FE2-9D18-F6D5F5EC258C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.JsonSchema.Draft7", "Corvus.Json.JsonSchema.Draft7\Corvus.Json.JsonSchema.Draft7.csproj", "{C9C00BD6-0C15-45C2-B976-BF6111635BDF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.JsonSchema.Draft201909", "Corvus.Json.JsonSchema.Draft201909\Corvus.Json.JsonSchema.Draft201909.csproj", "{05ABCC12-D8B4-417E-BCDF-68A828BC8139}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Patch", "Patch", "{4B3FA353-F63F-42F8-9853-770030AF11F2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.JsonSchema.Draft202012", "Corvus.Json.JsonSchema.Draft202012\Corvus.Json.JsonSchema.Draft202012.csproj", "{4821127F-5A20-4519-8A99-99C75A996F5E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "JSON Extended Types", "JSON Extended Types", "{ADC45E09-87EA-482A-B306-C361184C13D4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.JsonSchema.Draft4", "Corvus.Json.JsonSchema.Draft4\Corvus.Json.JsonSchema.Draft4.csproj", "{7321ABCA-97C6-4741-A694-F942A32E5278}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Generators", "Generators", "{88A5B434-451F-4C83-852B-A140CA97C93C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.JsonSchema.Draft6", "Corvus.Json.JsonSchema.Draft6\Corvus.Json.JsonSchema.Draft6.csproj", "{CDA89973-296A-4C33-9DC0-5CB89D3ECD6A}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DevOps", "DevOps", "{12DBCC2E-9BA0-4470-83C6-BBA9682F0155}" - ProjectSection(SolutionItems) = preProject - ..\.github\workflows\auto_release.yml = ..\.github\workflows\auto_release.yml - ..\build.ps1 = ..\build.ps1 - ..\.github\workflows\build.yml = ..\.github\workflows\build.yml - ..\.github\workflows\dependabot_approve_and_label.yml = ..\.github\workflows\dependabot_approve_and_label.yml - ..\GitVersion.yml = ..\GitVersion.yml - EndProjectSection +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.JsonSchema.Draft7", "Corvus.Json.JsonSchema.Draft7\Corvus.Json.JsonSchema.Draft7.csproj", "{B911032D-6AFC-4E86-9093-A26358AFDDF2}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{2CE21CBD-4B31-4408-A0E1-01E673F6FADF}" - ProjectSection(SolutionItems) = preProject - ..\docs\GettingStartedWithJsonSchemaCodeGeneration.md = ..\docs\GettingStartedWithJsonSchemaCodeGeneration.md - ..\README.md = ..\README.md - EndProjectSection +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.JsonSchema.OpenApi30", "Corvus.Json.JsonSchema.OpenApi30\Corvus.Json.JsonSchema.OpenApi30.csproj", "{BB95436C-301F-48BC-8CA4-30849ECD121D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.JsonSchema.OpenApi30", "Corvus.Json.JsonSchema.OpenApi30\Corvus.Json.JsonSchema.OpenApi30.csproj", "{0DCAAEF5-28AB-4512-BCDE-0B80906469B4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.JsonSchema.OpenApi31", "Corvus.Json.JsonSchema.OpenApi31\Corvus.Json.JsonSchema.OpenApi31.csproj", "{5019694E-8FF8-47D2-86DA-73BBCC977466}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Vocabularies", "Vocabularies", "{61768B8B-B9E7-4B3D-BD95-BE8358CDDDB0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.Patch", "Corvus.Json.Patch\Corvus.Json.Patch.csproj", "{830F934B-EFA9-4A0F-9AEA-0FEE0DF08B73}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.JsonSchema.Draft4", "Corvus.Json.JsonSchema.Draft4\Corvus.Json.JsonSchema.Draft4.csproj", "{6326E721-2DFE-45C2-A30F-98D3AA5C202A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.Patch.SpecGenerator", "Corvus.Json.Patch.SpecGenerator\Corvus.Json.Patch.SpecGenerator.csproj", "{59BD9C7F-31DA-4D16-8FF3-CB72374F393A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.JsonSchema.OpenApi31", "Corvus.Json.JsonSchema.OpenApi31\Corvus.Json.JsonSchema.OpenApi31.csproj", "{F6FD6F8C-1CC7-43B2-9B6F-FDA696BBB840}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.SourceGenerator", "Corvus.Json.SourceGenerator\Corvus.Json.SourceGenerator.csproj", "{B991B39B-A9DE-4C46-A6BB-F5EEA44C8E1F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.CodeGeneration", "Corvus.Json.CodeGeneration\Corvus.Json.CodeGeneration.csproj", "{70DFB4D9-A21F-4F91-93BF-C89B94933F49}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.Specs", "Corvus.Json.Specs\Corvus.Json.Specs.csproj", "{37D6B3AE-7C92-4C20-8DD0-A711EDEB0AC1}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Language Providers", "Language Providers", "{B6733A7C-2B94-4F52-B86D-75C8713691FB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.Validator", "Corvus.Json.Validator\Corvus.Json.Validator.csproj", "{1132630C-DA9B-47A8-942A-EB93F5CDDD8C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.CodeGeneration.CSharp", "Corvus.Json.CodeGeneration.CSharp\Corvus.Json.CodeGeneration.CSharp.csproj", "{1FD125E5-A9AB-40CC-B09B-B2E9745243EA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.JsonPatch.Benchmarking", "Corvus.JsonPatch.Benchmarking\Corvus.JsonPatch.Benchmarking.csproj", "{4CF2D345-EC8F-4A1A-980C-B6668836B143}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.CodeGeneration.201909", "Corvus.Json.CodeGeneration.201909\Corvus.Json.CodeGeneration.201909.csproj", "{4C3FE42C-B802-46C9-B062-110CE9F716CD}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.JsonSchema.SpecGenerator", "Corvus.JsonSchema.SpecGenerator\Corvus.JsonSchema.SpecGenerator.csproj", "{27686E80-726F-458B-AEAB-7CF7B6ABA5D4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.CodeGeneration.7", "Corvus.Json.CodeGeneration.7\Corvus.Json.CodeGeneration.7.csproj", "{1B7A4E77-52F0-412E-9220-74EFA35F7C02}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox", "Sandbox\Sandbox.csproj", "{C2A65E18-0842-4DC6-8741-4523FA5A6D91}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.CodeGeneration.6", "Corvus.Json.CodeGeneration.6\Corvus.Json.CodeGeneration.6.csproj", "{C74C1DB2-77CB-42F3-BC99-D998BFC91EA3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox.SourceGenerator", "Sandbox.SourceGenerator\Sandbox.SourceGenerator.csproj", "{4CEC0727-4132-4227-96D0-7A75F33E7F21}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.CodeGeneration.4", "Corvus.Json.CodeGeneration.4\Corvus.Json.CodeGeneration.4.csproj", "{667C2A80-D723-4DF7-86AC-E9978D8640A6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.CodeGeneration.OpenApi30", "Corvus.Json.CodeGeneration.OpenApi30\Corvus.Json.CodeGeneration.OpenApi30.csproj", "{C991F65F-AA10-4D3F-A7C8-FE80738DABDA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "Sandbox\Sandbox.csproj", "{0771CCDA-52A7-406E-90F2-CEC4C17C96B6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Json.CodeGeneration.CorvusVocabulary", "Corvus.Json.CodeGeneration.CorvusVocabulary\Corvus.Json.CodeGeneration.CorvusVocabulary.csproj", "{52FC50E3-4944-4B08-BDAC-F12CF132057B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{BAD825DF-26C7-48EB-BF02-1DB4C4BA4AAE}" - ProjectSection(SolutionItems) = preProject - coretypesgeneratorconfig.json = coretypesgeneratorconfig.json - generatetypes-release.ps1 = generatetypes-release.ps1 - generatetypes.ps1 = generatetypes.ps1 - json-extended-types.json = json-extended-types.json - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.Validator", "Corvus.Json.Validator\Corvus.Json.Validator.csproj", "{654C49D4-2F9A-4FA6-B7E0-EF372DC349BC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.CSharp.QuickStart", "Corvus.Json.CodeGeneration.CSharp.QuickStart\Corvus.Json.CodeGeneration.CSharp.QuickStart.csproj", "{6C13CDA3-46B4-4624-8170-8DD74C18B215}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.JsonReference", "Corvus.Json.JsonReference\Corvus.Json.JsonReference.csproj", "{2C9BC284-F481-4C94-A40E-F818055DB352}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.HttpClientDocumentResolver", "Corvus.Json.CodeGeneration.HttpClientDocumentResolver\Corvus.Json.CodeGeneration.HttpClientDocumentResolver.csproj", "{75D43E9A-AA35-407F-85A2-E681962AA71E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGenerator", "SourceGenerator", "{63B6708D-861F-4BAA-B35C-F52D68E6D84A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.SourceGenerator", "Corvus.Json.SourceGenerator\Corvus.Json.SourceGenerator.csproj", "{30AB9BB0-925D-4B05-8E31-EDEC238869D5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox.SourceGenerator", "Sandbox.SourceGenerator\Sandbox.SourceGenerator.csproj", "{95BCD5E7-B37F-4E36-AC00-21708F4E87BA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.CodeGeneration.YamlPreProcessor", "Corvus.Json.CodeGeneration.YamlPreProcessor\Corvus.Json.CodeGeneration.YamlPreProcessor.csproj", "{C26876E9-CED9-46F0-B1F6-4DF7F03C9584}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.Json.SourceGeneratorTools", "Corvus.Json.SourceGeneratorTools\Corvus.Json.SourceGeneratorTools.csproj", "{A7D7CAA6-0747-4CE4-9E7D-EB649C1B4246}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B0113059-9582-4B82-A591-5E36AB18923F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B0113059-9582-4B82-A591-5E36AB18923F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B0113059-9582-4B82-A591-5E36AB18923F}.Debug|x64.ActiveCfg = Debug|Any CPU - {B0113059-9582-4B82-A591-5E36AB18923F}.Debug|x64.Build.0 = Debug|Any CPU - {B0113059-9582-4B82-A591-5E36AB18923F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B0113059-9582-4B82-A591-5E36AB18923F}.Release|Any CPU.Build.0 = Release|Any CPU - {B0113059-9582-4B82-A591-5E36AB18923F}.Release|x64.ActiveCfg = Release|Any CPU - {B0113059-9582-4B82-A591-5E36AB18923F}.Release|x64.Build.0 = Release|Any CPU - {20AFC7E7-5D24-4BB7-8F05-115D19474D05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20AFC7E7-5D24-4BB7-8F05-115D19474D05}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20AFC7E7-5D24-4BB7-8F05-115D19474D05}.Debug|x64.ActiveCfg = Debug|Any CPU - {20AFC7E7-5D24-4BB7-8F05-115D19474D05}.Debug|x64.Build.0 = Debug|Any CPU - {20AFC7E7-5D24-4BB7-8F05-115D19474D05}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20AFC7E7-5D24-4BB7-8F05-115D19474D05}.Release|Any CPU.Build.0 = Release|Any CPU - {20AFC7E7-5D24-4BB7-8F05-115D19474D05}.Release|x64.ActiveCfg = Release|Any CPU - {20AFC7E7-5D24-4BB7-8F05-115D19474D05}.Release|x64.Build.0 = Release|Any CPU - {1BE2E1C5-0726-418F-9041-18F9A9719FCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1BE2E1C5-0726-418F-9041-18F9A9719FCA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1BE2E1C5-0726-418F-9041-18F9A9719FCA}.Debug|x64.ActiveCfg = Debug|Any CPU - {1BE2E1C5-0726-418F-9041-18F9A9719FCA}.Debug|x64.Build.0 = Debug|Any CPU - {1BE2E1C5-0726-418F-9041-18F9A9719FCA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1BE2E1C5-0726-418F-9041-18F9A9719FCA}.Release|Any CPU.Build.0 = Release|Any CPU - {1BE2E1C5-0726-418F-9041-18F9A9719FCA}.Release|x64.ActiveCfg = Release|Any CPU - {1BE2E1C5-0726-418F-9041-18F9A9719FCA}.Release|x64.Build.0 = Release|Any CPU - {94B53FC6-F80E-4ADB-BB44-A7B12520177A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {94B53FC6-F80E-4ADB-BB44-A7B12520177A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {94B53FC6-F80E-4ADB-BB44-A7B12520177A}.Debug|x64.ActiveCfg = Debug|Any CPU - {94B53FC6-F80E-4ADB-BB44-A7B12520177A}.Debug|x64.Build.0 = Debug|Any CPU - {94B53FC6-F80E-4ADB-BB44-A7B12520177A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {94B53FC6-F80E-4ADB-BB44-A7B12520177A}.Release|Any CPU.Build.0 = Release|Any CPU - {94B53FC6-F80E-4ADB-BB44-A7B12520177A}.Release|x64.ActiveCfg = Release|Any CPU - {94B53FC6-F80E-4ADB-BB44-A7B12520177A}.Release|x64.Build.0 = Release|Any CPU - {A67E1B7C-0990-4CA0-846A-F4DA4FCA20BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A67E1B7C-0990-4CA0-846A-F4DA4FCA20BF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A67E1B7C-0990-4CA0-846A-F4DA4FCA20BF}.Debug|x64.ActiveCfg = Debug|Any CPU - {A67E1B7C-0990-4CA0-846A-F4DA4FCA20BF}.Debug|x64.Build.0 = Debug|Any CPU - {A67E1B7C-0990-4CA0-846A-F4DA4FCA20BF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A67E1B7C-0990-4CA0-846A-F4DA4FCA20BF}.Release|Any CPU.Build.0 = Release|Any CPU - {A67E1B7C-0990-4CA0-846A-F4DA4FCA20BF}.Release|x64.ActiveCfg = Release|Any CPU - {A67E1B7C-0990-4CA0-846A-F4DA4FCA20BF}.Release|x64.Build.0 = Release|Any CPU - {70C78E48-B257-4FD1-98E1-61D1F68831C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {70C78E48-B257-4FD1-98E1-61D1F68831C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70C78E48-B257-4FD1-98E1-61D1F68831C2}.Debug|x64.ActiveCfg = Debug|Any CPU - {70C78E48-B257-4FD1-98E1-61D1F68831C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {70C78E48-B257-4FD1-98E1-61D1F68831C2}.Release|Any CPU.Build.0 = Release|Any CPU - {70C78E48-B257-4FD1-98E1-61D1F68831C2}.Release|x64.ActiveCfg = Release|Any CPU - {D4C59DE5-AEA4-42EF-BED7-5457DF5BD465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4C59DE5-AEA4-42EF-BED7-5457DF5BD465}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4C59DE5-AEA4-42EF-BED7-5457DF5BD465}.Debug|x64.ActiveCfg = Debug|Any CPU - {D4C59DE5-AEA4-42EF-BED7-5457DF5BD465}.Debug|x64.Build.0 = Debug|Any CPU - {D4C59DE5-AEA4-42EF-BED7-5457DF5BD465}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4C59DE5-AEA4-42EF-BED7-5457DF5BD465}.Release|Any CPU.Build.0 = Release|Any CPU - {D4C59DE5-AEA4-42EF-BED7-5457DF5BD465}.Release|x64.ActiveCfg = Release|Any CPU - {D4C59DE5-AEA4-42EF-BED7-5457DF5BD465}.Release|x64.Build.0 = Release|Any CPU - {D878C49A-AD4B-4BBF-9B9C-168B99C83B00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D878C49A-AD4B-4BBF-9B9C-168B99C83B00}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D878C49A-AD4B-4BBF-9B9C-168B99C83B00}.Debug|x64.ActiveCfg = Debug|Any CPU - {D878C49A-AD4B-4BBF-9B9C-168B99C83B00}.Debug|x64.Build.0 = Debug|Any CPU - {D878C49A-AD4B-4BBF-9B9C-168B99C83B00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D878C49A-AD4B-4BBF-9B9C-168B99C83B00}.Release|Any CPU.Build.0 = Release|Any CPU - {D878C49A-AD4B-4BBF-9B9C-168B99C83B00}.Release|x64.ActiveCfg = Release|Any CPU - {D878C49A-AD4B-4BBF-9B9C-168B99C83B00}.Release|x64.Build.0 = Release|Any CPU - {428FA47D-641B-41B4-A49C-D39CEAF48B0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {428FA47D-641B-41B4-A49C-D39CEAF48B0A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {428FA47D-641B-41B4-A49C-D39CEAF48B0A}.Debug|x64.ActiveCfg = Debug|Any CPU - {428FA47D-641B-41B4-A49C-D39CEAF48B0A}.Debug|x64.Build.0 = Debug|Any CPU - {428FA47D-641B-41B4-A49C-D39CEAF48B0A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {428FA47D-641B-41B4-A49C-D39CEAF48B0A}.Release|Any CPU.Build.0 = Release|Any CPU - {428FA47D-641B-41B4-A49C-D39CEAF48B0A}.Release|x64.ActiveCfg = Release|Any CPU - {428FA47D-641B-41B4-A49C-D39CEAF48B0A}.Release|x64.Build.0 = Release|Any CPU - {DDD045D0-ECF3-4696-B4E0-28EC58B07D7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DDD045D0-ECF3-4696-B4E0-28EC58B07D7B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DDD045D0-ECF3-4696-B4E0-28EC58B07D7B}.Debug|x64.ActiveCfg = Debug|Any CPU - {DDD045D0-ECF3-4696-B4E0-28EC58B07D7B}.Debug|x64.Build.0 = Debug|Any CPU - {DDD045D0-ECF3-4696-B4E0-28EC58B07D7B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DDD045D0-ECF3-4696-B4E0-28EC58B07D7B}.Release|Any CPU.Build.0 = Release|Any CPU - {DDD045D0-ECF3-4696-B4E0-28EC58B07D7B}.Release|x64.ActiveCfg = Release|Any CPU - {DDD045D0-ECF3-4696-B4E0-28EC58B07D7B}.Release|x64.Build.0 = Release|Any CPU - {CD841AC2-88A2-40CA-995F-F8DABAED339C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD841AC2-88A2-40CA-995F-F8DABAED339C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD841AC2-88A2-40CA-995F-F8DABAED339C}.Debug|x64.ActiveCfg = Debug|Any CPU - {CD841AC2-88A2-40CA-995F-F8DABAED339C}.Debug|x64.Build.0 = Debug|Any CPU - {CD841AC2-88A2-40CA-995F-F8DABAED339C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD841AC2-88A2-40CA-995F-F8DABAED339C}.Release|Any CPU.Build.0 = Release|Any CPU - {CD841AC2-88A2-40CA-995F-F8DABAED339C}.Release|x64.ActiveCfg = Release|Any CPU - {CD841AC2-88A2-40CA-995F-F8DABAED339C}.Release|x64.Build.0 = Release|Any CPU - {16DC75F0-A4B0-44A7-B0E5-BFA3B9F9AA59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {16DC75F0-A4B0-44A7-B0E5-BFA3B9F9AA59}.Debug|Any CPU.Build.0 = Debug|Any CPU - {16DC75F0-A4B0-44A7-B0E5-BFA3B9F9AA59}.Debug|x64.ActiveCfg = Debug|Any CPU - {16DC75F0-A4B0-44A7-B0E5-BFA3B9F9AA59}.Debug|x64.Build.0 = Debug|Any CPU - {16DC75F0-A4B0-44A7-B0E5-BFA3B9F9AA59}.Release|Any CPU.ActiveCfg = Release|Any CPU - {16DC75F0-A4B0-44A7-B0E5-BFA3B9F9AA59}.Release|Any CPU.Build.0 = Release|Any CPU - {16DC75F0-A4B0-44A7-B0E5-BFA3B9F9AA59}.Release|x64.ActiveCfg = Release|Any CPU - {16DC75F0-A4B0-44A7-B0E5-BFA3B9F9AA59}.Release|x64.Build.0 = Release|Any CPU - {C9C00BD6-0C15-45C2-B976-BF6111635BDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9C00BD6-0C15-45C2-B976-BF6111635BDF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9C00BD6-0C15-45C2-B976-BF6111635BDF}.Debug|x64.ActiveCfg = Debug|Any CPU - {C9C00BD6-0C15-45C2-B976-BF6111635BDF}.Debug|x64.Build.0 = Debug|Any CPU - {C9C00BD6-0C15-45C2-B976-BF6111635BDF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9C00BD6-0C15-45C2-B976-BF6111635BDF}.Release|Any CPU.Build.0 = Release|Any CPU - {C9C00BD6-0C15-45C2-B976-BF6111635BDF}.Release|x64.ActiveCfg = Release|Any CPU - {C9C00BD6-0C15-45C2-B976-BF6111635BDF}.Release|x64.Build.0 = Release|Any CPU - {0DCAAEF5-28AB-4512-BCDE-0B80906469B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DCAAEF5-28AB-4512-BCDE-0B80906469B4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0DCAAEF5-28AB-4512-BCDE-0B80906469B4}.Debug|x64.ActiveCfg = Debug|Any CPU - {0DCAAEF5-28AB-4512-BCDE-0B80906469B4}.Debug|x64.Build.0 = Debug|Any CPU - {0DCAAEF5-28AB-4512-BCDE-0B80906469B4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DCAAEF5-28AB-4512-BCDE-0B80906469B4}.Release|Any CPU.Build.0 = Release|Any CPU - {0DCAAEF5-28AB-4512-BCDE-0B80906469B4}.Release|x64.ActiveCfg = Release|Any CPU - {0DCAAEF5-28AB-4512-BCDE-0B80906469B4}.Release|x64.Build.0 = Release|Any CPU - {6326E721-2DFE-45C2-A30F-98D3AA5C202A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6326E721-2DFE-45C2-A30F-98D3AA5C202A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6326E721-2DFE-45C2-A30F-98D3AA5C202A}.Debug|x64.ActiveCfg = Debug|Any CPU - {6326E721-2DFE-45C2-A30F-98D3AA5C202A}.Debug|x64.Build.0 = Debug|Any CPU - {6326E721-2DFE-45C2-A30F-98D3AA5C202A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6326E721-2DFE-45C2-A30F-98D3AA5C202A}.Release|Any CPU.Build.0 = Release|Any CPU - {6326E721-2DFE-45C2-A30F-98D3AA5C202A}.Release|x64.ActiveCfg = Release|Any CPU - {6326E721-2DFE-45C2-A30F-98D3AA5C202A}.Release|x64.Build.0 = Release|Any CPU - {F6FD6F8C-1CC7-43B2-9B6F-FDA696BBB840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6FD6F8C-1CC7-43B2-9B6F-FDA696BBB840}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F6FD6F8C-1CC7-43B2-9B6F-FDA696BBB840}.Debug|x64.ActiveCfg = Debug|Any CPU - {F6FD6F8C-1CC7-43B2-9B6F-FDA696BBB840}.Debug|x64.Build.0 = Debug|Any CPU - {F6FD6F8C-1CC7-43B2-9B6F-FDA696BBB840}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F6FD6F8C-1CC7-43B2-9B6F-FDA696BBB840}.Release|Any CPU.Build.0 = Release|Any CPU - {F6FD6F8C-1CC7-43B2-9B6F-FDA696BBB840}.Release|x64.ActiveCfg = Release|Any CPU - {F6FD6F8C-1CC7-43B2-9B6F-FDA696BBB840}.Release|x64.Build.0 = Release|Any CPU - {70DFB4D9-A21F-4F91-93BF-C89B94933F49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {70DFB4D9-A21F-4F91-93BF-C89B94933F49}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70DFB4D9-A21F-4F91-93BF-C89B94933F49}.Debug|x64.ActiveCfg = Debug|Any CPU - {70DFB4D9-A21F-4F91-93BF-C89B94933F49}.Debug|x64.Build.0 = Debug|Any CPU - {70DFB4D9-A21F-4F91-93BF-C89B94933F49}.Release|Any CPU.ActiveCfg = Release|Any CPU - {70DFB4D9-A21F-4F91-93BF-C89B94933F49}.Release|Any CPU.Build.0 = Release|Any CPU - {70DFB4D9-A21F-4F91-93BF-C89B94933F49}.Release|x64.ActiveCfg = Release|Any CPU - {70DFB4D9-A21F-4F91-93BF-C89B94933F49}.Release|x64.Build.0 = Release|Any CPU - {1FD125E5-A9AB-40CC-B09B-B2E9745243EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1FD125E5-A9AB-40CC-B09B-B2E9745243EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1FD125E5-A9AB-40CC-B09B-B2E9745243EA}.Debug|x64.ActiveCfg = Debug|Any CPU - {1FD125E5-A9AB-40CC-B09B-B2E9745243EA}.Debug|x64.Build.0 = Debug|Any CPU - {1FD125E5-A9AB-40CC-B09B-B2E9745243EA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1FD125E5-A9AB-40CC-B09B-B2E9745243EA}.Release|Any CPU.Build.0 = Release|Any CPU - {1FD125E5-A9AB-40CC-B09B-B2E9745243EA}.Release|x64.ActiveCfg = Release|Any CPU - {1FD125E5-A9AB-40CC-B09B-B2E9745243EA}.Release|x64.Build.0 = Release|Any CPU - {4C3FE42C-B802-46C9-B062-110CE9F716CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C3FE42C-B802-46C9-B062-110CE9F716CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C3FE42C-B802-46C9-B062-110CE9F716CD}.Debug|x64.ActiveCfg = Debug|Any CPU - {4C3FE42C-B802-46C9-B062-110CE9F716CD}.Debug|x64.Build.0 = Debug|Any CPU - {4C3FE42C-B802-46C9-B062-110CE9F716CD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C3FE42C-B802-46C9-B062-110CE9F716CD}.Release|Any CPU.Build.0 = Release|Any CPU - {4C3FE42C-B802-46C9-B062-110CE9F716CD}.Release|x64.ActiveCfg = Release|Any CPU - {4C3FE42C-B802-46C9-B062-110CE9F716CD}.Release|x64.Build.0 = Release|Any CPU - {1B7A4E77-52F0-412E-9220-74EFA35F7C02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B7A4E77-52F0-412E-9220-74EFA35F7C02}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B7A4E77-52F0-412E-9220-74EFA35F7C02}.Debug|x64.ActiveCfg = Debug|Any CPU - {1B7A4E77-52F0-412E-9220-74EFA35F7C02}.Debug|x64.Build.0 = Debug|Any CPU - {1B7A4E77-52F0-412E-9220-74EFA35F7C02}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B7A4E77-52F0-412E-9220-74EFA35F7C02}.Release|Any CPU.Build.0 = Release|Any CPU - {1B7A4E77-52F0-412E-9220-74EFA35F7C02}.Release|x64.ActiveCfg = Release|Any CPU - {1B7A4E77-52F0-412E-9220-74EFA35F7C02}.Release|x64.Build.0 = Release|Any CPU - {C74C1DB2-77CB-42F3-BC99-D998BFC91EA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C74C1DB2-77CB-42F3-BC99-D998BFC91EA3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C74C1DB2-77CB-42F3-BC99-D998BFC91EA3}.Debug|x64.ActiveCfg = Debug|Any CPU - {C74C1DB2-77CB-42F3-BC99-D998BFC91EA3}.Debug|x64.Build.0 = Debug|Any CPU - {C74C1DB2-77CB-42F3-BC99-D998BFC91EA3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C74C1DB2-77CB-42F3-BC99-D998BFC91EA3}.Release|Any CPU.Build.0 = Release|Any CPU - {C74C1DB2-77CB-42F3-BC99-D998BFC91EA3}.Release|x64.ActiveCfg = Release|Any CPU - {C74C1DB2-77CB-42F3-BC99-D998BFC91EA3}.Release|x64.Build.0 = Release|Any CPU - {667C2A80-D723-4DF7-86AC-E9978D8640A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {667C2A80-D723-4DF7-86AC-E9978D8640A6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {667C2A80-D723-4DF7-86AC-E9978D8640A6}.Debug|x64.ActiveCfg = Debug|Any CPU - {667C2A80-D723-4DF7-86AC-E9978D8640A6}.Debug|x64.Build.0 = Debug|Any CPU - {667C2A80-D723-4DF7-86AC-E9978D8640A6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {667C2A80-D723-4DF7-86AC-E9978D8640A6}.Release|Any CPU.Build.0 = Release|Any CPU - {667C2A80-D723-4DF7-86AC-E9978D8640A6}.Release|x64.ActiveCfg = Release|Any CPU - {667C2A80-D723-4DF7-86AC-E9978D8640A6}.Release|x64.Build.0 = Release|Any CPU - {C991F65F-AA10-4D3F-A7C8-FE80738DABDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C991F65F-AA10-4D3F-A7C8-FE80738DABDA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C991F65F-AA10-4D3F-A7C8-FE80738DABDA}.Debug|x64.ActiveCfg = Debug|Any CPU - {C991F65F-AA10-4D3F-A7C8-FE80738DABDA}.Debug|x64.Build.0 = Debug|Any CPU - {C991F65F-AA10-4D3F-A7C8-FE80738DABDA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C991F65F-AA10-4D3F-A7C8-FE80738DABDA}.Release|Any CPU.Build.0 = Release|Any CPU - {C991F65F-AA10-4D3F-A7C8-FE80738DABDA}.Release|x64.ActiveCfg = Release|Any CPU - {C991F65F-AA10-4D3F-A7C8-FE80738DABDA}.Release|x64.Build.0 = Release|Any CPU - {0771CCDA-52A7-406E-90F2-CEC4C17C96B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0771CCDA-52A7-406E-90F2-CEC4C17C96B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0771CCDA-52A7-406E-90F2-CEC4C17C96B6}.Debug|x64.ActiveCfg = Debug|Any CPU - {0771CCDA-52A7-406E-90F2-CEC4C17C96B6}.Debug|x64.Build.0 = Debug|Any CPU - {0771CCDA-52A7-406E-90F2-CEC4C17C96B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0771CCDA-52A7-406E-90F2-CEC4C17C96B6}.Release|Any CPU.Build.0 = Release|Any CPU - {0771CCDA-52A7-406E-90F2-CEC4C17C96B6}.Release|x64.ActiveCfg = Release|Any CPU - {0771CCDA-52A7-406E-90F2-CEC4C17C96B6}.Release|x64.Build.0 = Release|Any CPU - {52FC50E3-4944-4B08-BDAC-F12CF132057B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52FC50E3-4944-4B08-BDAC-F12CF132057B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52FC50E3-4944-4B08-BDAC-F12CF132057B}.Debug|x64.ActiveCfg = Debug|Any CPU - {52FC50E3-4944-4B08-BDAC-F12CF132057B}.Debug|x64.Build.0 = Debug|Any CPU - {52FC50E3-4944-4B08-BDAC-F12CF132057B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52FC50E3-4944-4B08-BDAC-F12CF132057B}.Release|Any CPU.Build.0 = Release|Any CPU - {52FC50E3-4944-4B08-BDAC-F12CF132057B}.Release|x64.ActiveCfg = Release|Any CPU - {52FC50E3-4944-4B08-BDAC-F12CF132057B}.Release|x64.Build.0 = Release|Any CPU - {654C49D4-2F9A-4FA6-B7E0-EF372DC349BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {654C49D4-2F9A-4FA6-B7E0-EF372DC349BC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {654C49D4-2F9A-4FA6-B7E0-EF372DC349BC}.Debug|x64.ActiveCfg = Debug|Any CPU - {654C49D4-2F9A-4FA6-B7E0-EF372DC349BC}.Debug|x64.Build.0 = Debug|Any CPU - {654C49D4-2F9A-4FA6-B7E0-EF372DC349BC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {654C49D4-2F9A-4FA6-B7E0-EF372DC349BC}.Release|Any CPU.Build.0 = Release|Any CPU - {654C49D4-2F9A-4FA6-B7E0-EF372DC349BC}.Release|x64.ActiveCfg = Release|Any CPU - {654C49D4-2F9A-4FA6-B7E0-EF372DC349BC}.Release|x64.Build.0 = Release|Any CPU - {6C13CDA3-46B4-4624-8170-8DD74C18B215}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6C13CDA3-46B4-4624-8170-8DD74C18B215}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6C13CDA3-46B4-4624-8170-8DD74C18B215}.Debug|x64.ActiveCfg = Debug|Any CPU - {6C13CDA3-46B4-4624-8170-8DD74C18B215}.Debug|x64.Build.0 = Debug|Any CPU - {6C13CDA3-46B4-4624-8170-8DD74C18B215}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6C13CDA3-46B4-4624-8170-8DD74C18B215}.Release|Any CPU.Build.0 = Release|Any CPU - {6C13CDA3-46B4-4624-8170-8DD74C18B215}.Release|x64.ActiveCfg = Release|Any CPU - {6C13CDA3-46B4-4624-8170-8DD74C18B215}.Release|x64.Build.0 = Release|Any CPU - {2C9BC284-F481-4C94-A40E-F818055DB352}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2C9BC284-F481-4C94-A40E-F818055DB352}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2C9BC284-F481-4C94-A40E-F818055DB352}.Debug|x64.ActiveCfg = Debug|Any CPU - {2C9BC284-F481-4C94-A40E-F818055DB352}.Debug|x64.Build.0 = Debug|Any CPU - {2C9BC284-F481-4C94-A40E-F818055DB352}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2C9BC284-F481-4C94-A40E-F818055DB352}.Release|Any CPU.Build.0 = Release|Any CPU - {2C9BC284-F481-4C94-A40E-F818055DB352}.Release|x64.ActiveCfg = Release|Any CPU - {2C9BC284-F481-4C94-A40E-F818055DB352}.Release|x64.Build.0 = Release|Any CPU - {75D43E9A-AA35-407F-85A2-E681962AA71E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {75D43E9A-AA35-407F-85A2-E681962AA71E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {75D43E9A-AA35-407F-85A2-E681962AA71E}.Debug|x64.ActiveCfg = Debug|Any CPU - {75D43E9A-AA35-407F-85A2-E681962AA71E}.Debug|x64.Build.0 = Debug|Any CPU - {75D43E9A-AA35-407F-85A2-E681962AA71E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {75D43E9A-AA35-407F-85A2-E681962AA71E}.Release|Any CPU.Build.0 = Release|Any CPU - {75D43E9A-AA35-407F-85A2-E681962AA71E}.Release|x64.ActiveCfg = Release|Any CPU - {75D43E9A-AA35-407F-85A2-E681962AA71E}.Release|x64.Build.0 = Release|Any CPU - {30AB9BB0-925D-4B05-8E31-EDEC238869D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30AB9BB0-925D-4B05-8E31-EDEC238869D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30AB9BB0-925D-4B05-8E31-EDEC238869D5}.Debug|x64.ActiveCfg = Debug|Any CPU - {30AB9BB0-925D-4B05-8E31-EDEC238869D5}.Debug|x64.Build.0 = Debug|Any CPU - {30AB9BB0-925D-4B05-8E31-EDEC238869D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30AB9BB0-925D-4B05-8E31-EDEC238869D5}.Release|Any CPU.Build.0 = Release|Any CPU - {30AB9BB0-925D-4B05-8E31-EDEC238869D5}.Release|x64.ActiveCfg = Release|Any CPU - {30AB9BB0-925D-4B05-8E31-EDEC238869D5}.Release|x64.Build.0 = Release|Any CPU - {95BCD5E7-B37F-4E36-AC00-21708F4E87BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {95BCD5E7-B37F-4E36-AC00-21708F4E87BA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {95BCD5E7-B37F-4E36-AC00-21708F4E87BA}.Debug|x64.ActiveCfg = Debug|Any CPU - {95BCD5E7-B37F-4E36-AC00-21708F4E87BA}.Debug|x64.Build.0 = Debug|Any CPU - {95BCD5E7-B37F-4E36-AC00-21708F4E87BA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {95BCD5E7-B37F-4E36-AC00-21708F4E87BA}.Release|Any CPU.Build.0 = Release|Any CPU - {95BCD5E7-B37F-4E36-AC00-21708F4E87BA}.Release|x64.ActiveCfg = Release|Any CPU - {95BCD5E7-B37F-4E36-AC00-21708F4E87BA}.Release|x64.Build.0 = Release|Any CPU - {C26876E9-CED9-46F0-B1F6-4DF7F03C9584}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C26876E9-CED9-46F0-B1F6-4DF7F03C9584}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C26876E9-CED9-46F0-B1F6-4DF7F03C9584}.Debug|x64.ActiveCfg = Debug|Any CPU - {C26876E9-CED9-46F0-B1F6-4DF7F03C9584}.Debug|x64.Build.0 = Debug|Any CPU - {C26876E9-CED9-46F0-B1F6-4DF7F03C9584}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C26876E9-CED9-46F0-B1F6-4DF7F03C9584}.Release|Any CPU.Build.0 = Release|Any CPU - {C26876E9-CED9-46F0-B1F6-4DF7F03C9584}.Release|x64.ActiveCfg = Release|Any CPU - {C26876E9-CED9-46F0-B1F6-4DF7F03C9584}.Release|x64.Build.0 = Release|Any CPU + {D1227031-B358-4B5A-94BC-3CD28FEBADEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1227031-B358-4B5A-94BC-3CD28FEBADEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1227031-B358-4B5A-94BC-3CD28FEBADEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1227031-B358-4B5A-94BC-3CD28FEBADEC}.Release|Any CPU.Build.0 = Release|Any CPU + {7F8BFC0B-3CBA-4202-8E51-63685EB3884A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F8BFC0B-3CBA-4202-8E51-63685EB3884A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F8BFC0B-3CBA-4202-8E51-63685EB3884A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F8BFC0B-3CBA-4202-8E51-63685EB3884A}.Release|Any CPU.Build.0 = Release|Any CPU + {D0DB7AA2-9D80-4BD2-8C26-8C3914D7F721}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0DB7AA2-9D80-4BD2-8C26-8C3914D7F721}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0DB7AA2-9D80-4BD2-8C26-8C3914D7F721}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0DB7AA2-9D80-4BD2-8C26-8C3914D7F721}.Release|Any CPU.Build.0 = Release|Any CPU + {97DFF763-8EF1-48BE-849F-159F239B1040}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97DFF763-8EF1-48BE-849F-159F239B1040}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97DFF763-8EF1-48BE-849F-159F239B1040}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97DFF763-8EF1-48BE-849F-159F239B1040}.Release|Any CPU.Build.0 = Release|Any CPU + {1C116DF6-5FB5-4188-B64F-C5D193272B58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C116DF6-5FB5-4188-B64F-C5D193272B58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C116DF6-5FB5-4188-B64F-C5D193272B58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C116DF6-5FB5-4188-B64F-C5D193272B58}.Release|Any CPU.Build.0 = Release|Any CPU + {5700545E-730A-417A-8B61-252B76345F07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5700545E-730A-417A-8B61-252B76345F07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5700545E-730A-417A-8B61-252B76345F07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5700545E-730A-417A-8B61-252B76345F07}.Release|Any CPU.Build.0 = Release|Any CPU + {5D0D6536-6350-43C6-BA2F-A488D265B3A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D0D6536-6350-43C6-BA2F-A488D265B3A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D0D6536-6350-43C6-BA2F-A488D265B3A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D0D6536-6350-43C6-BA2F-A488D265B3A9}.Release|Any CPU.Build.0 = Release|Any CPU + {486C2B04-DCCF-4957-A580-AD9FD26541C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {486C2B04-DCCF-4957-A580-AD9FD26541C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {486C2B04-DCCF-4957-A580-AD9FD26541C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {486C2B04-DCCF-4957-A580-AD9FD26541C4}.Release|Any CPU.Build.0 = Release|Any CPU + {90DC27A6-4526-40E8-9754-8428611E41F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90DC27A6-4526-40E8-9754-8428611E41F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90DC27A6-4526-40E8-9754-8428611E41F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90DC27A6-4526-40E8-9754-8428611E41F2}.Release|Any CPU.Build.0 = Release|Any CPU + {8FA2FA0A-D651-4B70-85CF-CB2C9EEFFF76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FA2FA0A-D651-4B70-85CF-CB2C9EEFFF76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FA2FA0A-D651-4B70-85CF-CB2C9EEFFF76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FA2FA0A-D651-4B70-85CF-CB2C9EEFFF76}.Release|Any CPU.Build.0 = Release|Any CPU + {D24FD762-FD1D-4E0E-BF84-4C493E5F4912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D24FD762-FD1D-4E0E-BF84-4C493E5F4912}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D24FD762-FD1D-4E0E-BF84-4C493E5F4912}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D24FD762-FD1D-4E0E-BF84-4C493E5F4912}.Release|Any CPU.Build.0 = Release|Any CPU + {85E4FD70-C057-45B5-963D-1F989CAC9596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85E4FD70-C057-45B5-963D-1F989CAC9596}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85E4FD70-C057-45B5-963D-1F989CAC9596}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85E4FD70-C057-45B5-963D-1F989CAC9596}.Release|Any CPU.Build.0 = Release|Any CPU + {0FE726E3-64DD-462B-8986-17C9101161FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FE726E3-64DD-462B-8986-17C9101161FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FE726E3-64DD-462B-8986-17C9101161FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FE726E3-64DD-462B-8986-17C9101161FE}.Release|Any CPU.Build.0 = Release|Any CPU + {25351BBD-5E6A-4E64-821C-4CE03C858B4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25351BBD-5E6A-4E64-821C-4CE03C858B4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25351BBD-5E6A-4E64-821C-4CE03C858B4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25351BBD-5E6A-4E64-821C-4CE03C858B4D}.Release|Any CPU.Build.0 = Release|Any CPU + {49266EDE-FDD4-4653-A5F2-3B7568CB8F1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49266EDE-FDD4-4653-A5F2-3B7568CB8F1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49266EDE-FDD4-4653-A5F2-3B7568CB8F1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49266EDE-FDD4-4653-A5F2-3B7568CB8F1A}.Release|Any CPU.Build.0 = Release|Any CPU + {DB878532-48E7-4FE2-9D18-F6D5F5EC258C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB878532-48E7-4FE2-9D18-F6D5F5EC258C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB878532-48E7-4FE2-9D18-F6D5F5EC258C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB878532-48E7-4FE2-9D18-F6D5F5EC258C}.Release|Any CPU.Build.0 = Release|Any CPU + {05ABCC12-D8B4-417E-BCDF-68A828BC8139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05ABCC12-D8B4-417E-BCDF-68A828BC8139}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05ABCC12-D8B4-417E-BCDF-68A828BC8139}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05ABCC12-D8B4-417E-BCDF-68A828BC8139}.Release|Any CPU.Build.0 = Release|Any CPU + {4821127F-5A20-4519-8A99-99C75A996F5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4821127F-5A20-4519-8A99-99C75A996F5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4821127F-5A20-4519-8A99-99C75A996F5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4821127F-5A20-4519-8A99-99C75A996F5E}.Release|Any CPU.Build.0 = Release|Any CPU + {7321ABCA-97C6-4741-A694-F942A32E5278}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7321ABCA-97C6-4741-A694-F942A32E5278}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7321ABCA-97C6-4741-A694-F942A32E5278}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7321ABCA-97C6-4741-A694-F942A32E5278}.Release|Any CPU.Build.0 = Release|Any CPU + {CDA89973-296A-4C33-9DC0-5CB89D3ECD6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDA89973-296A-4C33-9DC0-5CB89D3ECD6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDA89973-296A-4C33-9DC0-5CB89D3ECD6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDA89973-296A-4C33-9DC0-5CB89D3ECD6A}.Release|Any CPU.Build.0 = Release|Any CPU + {B911032D-6AFC-4E86-9093-A26358AFDDF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B911032D-6AFC-4E86-9093-A26358AFDDF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B911032D-6AFC-4E86-9093-A26358AFDDF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B911032D-6AFC-4E86-9093-A26358AFDDF2}.Release|Any CPU.Build.0 = Release|Any CPU + {BB95436C-301F-48BC-8CA4-30849ECD121D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB95436C-301F-48BC-8CA4-30849ECD121D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB95436C-301F-48BC-8CA4-30849ECD121D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB95436C-301F-48BC-8CA4-30849ECD121D}.Release|Any CPU.Build.0 = Release|Any CPU + {5019694E-8FF8-47D2-86DA-73BBCC977466}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5019694E-8FF8-47D2-86DA-73BBCC977466}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5019694E-8FF8-47D2-86DA-73BBCC977466}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5019694E-8FF8-47D2-86DA-73BBCC977466}.Release|Any CPU.Build.0 = Release|Any CPU + {830F934B-EFA9-4A0F-9AEA-0FEE0DF08B73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {830F934B-EFA9-4A0F-9AEA-0FEE0DF08B73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {830F934B-EFA9-4A0F-9AEA-0FEE0DF08B73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {830F934B-EFA9-4A0F-9AEA-0FEE0DF08B73}.Release|Any CPU.Build.0 = Release|Any CPU + {59BD9C7F-31DA-4D16-8FF3-CB72374F393A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59BD9C7F-31DA-4D16-8FF3-CB72374F393A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59BD9C7F-31DA-4D16-8FF3-CB72374F393A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59BD9C7F-31DA-4D16-8FF3-CB72374F393A}.Release|Any CPU.Build.0 = Release|Any CPU + {B991B39B-A9DE-4C46-A6BB-F5EEA44C8E1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B991B39B-A9DE-4C46-A6BB-F5EEA44C8E1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B991B39B-A9DE-4C46-A6BB-F5EEA44C8E1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B991B39B-A9DE-4C46-A6BB-F5EEA44C8E1F}.Release|Any CPU.Build.0 = Release|Any CPU + {37D6B3AE-7C92-4C20-8DD0-A711EDEB0AC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37D6B3AE-7C92-4C20-8DD0-A711EDEB0AC1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37D6B3AE-7C92-4C20-8DD0-A711EDEB0AC1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37D6B3AE-7C92-4C20-8DD0-A711EDEB0AC1}.Release|Any CPU.Build.0 = Release|Any CPU + {1132630C-DA9B-47A8-942A-EB93F5CDDD8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1132630C-DA9B-47A8-942A-EB93F5CDDD8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1132630C-DA9B-47A8-942A-EB93F5CDDD8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1132630C-DA9B-47A8-942A-EB93F5CDDD8C}.Release|Any CPU.Build.0 = Release|Any CPU + {4CF2D345-EC8F-4A1A-980C-B6668836B143}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4CF2D345-EC8F-4A1A-980C-B6668836B143}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4CF2D345-EC8F-4A1A-980C-B6668836B143}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4CF2D345-EC8F-4A1A-980C-B6668836B143}.Release|Any CPU.Build.0 = Release|Any CPU + {27686E80-726F-458B-AEAB-7CF7B6ABA5D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27686E80-726F-458B-AEAB-7CF7B6ABA5D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27686E80-726F-458B-AEAB-7CF7B6ABA5D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27686E80-726F-458B-AEAB-7CF7B6ABA5D4}.Release|Any CPU.Build.0 = Release|Any CPU + {C2A65E18-0842-4DC6-8741-4523FA5A6D91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2A65E18-0842-4DC6-8741-4523FA5A6D91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2A65E18-0842-4DC6-8741-4523FA5A6D91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2A65E18-0842-4DC6-8741-4523FA5A6D91}.Release|Any CPU.Build.0 = Release|Any CPU + {4CEC0727-4132-4227-96D0-7A75F33E7F21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4CEC0727-4132-4227-96D0-7A75F33E7F21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4CEC0727-4132-4227-96D0-7A75F33E7F21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4CEC0727-4132-4227-96D0-7A75F33E7F21}.Release|Any CPU.Build.0 = Release|Any CPU + {A7D7CAA6-0747-4CE4-9E7D-EB649C1B4246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7D7CAA6-0747-4CE4-9E7D-EB649C1B4246}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7D7CAA6-0747-4CE4-9E7D-EB649C1B4246}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7D7CAA6-0747-4CE4-9E7D-EB649C1B4246}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {B0113059-9582-4B82-A591-5E36AB18923F} = {ADC45E09-87EA-482A-B306-C361184C13D4} - {20AFC7E7-5D24-4BB7-8F05-115D19474D05} = {D9BD60AE-2972-4608-8E0D-16929D42A0D9} - {1BE2E1C5-0726-418F-9041-18F9A9719FCA} = {4B3FA353-F63F-42F8-9853-770030AF11F2} - {94B53FC6-F80E-4ADB-BB44-A7B12520177A} = {88A5B434-451F-4C83-852B-A140CA97C93C} - {A67E1B7C-0990-4CA0-846A-F4DA4FCA20BF} = {911B0F4F-E689-48AA-8D4B-44AFBA1DF850} - {70C78E48-B257-4FD1-98E1-61D1F68831C2} = {911B0F4F-E689-48AA-8D4B-44AFBA1DF850} - {D4C59DE5-AEA4-42EF-BED7-5457DF5BD465} = {61768B8B-B9E7-4B3D-BD95-BE8358CDDDB0} - {428FA47D-641B-41B4-A49C-D39CEAF48B0A} = {88A5B434-451F-4C83-852B-A140CA97C93C} - {DDD045D0-ECF3-4696-B4E0-28EC58B07D7B} = {3D0E15DD-5C66-4C2E-BAD1-F10A3B7B4A31} - {CD841AC2-88A2-40CA-995F-F8DABAED339C} = {3D0E15DD-5C66-4C2E-BAD1-F10A3B7B4A31} - {16DC75F0-A4B0-44A7-B0E5-BFA3B9F9AA59} = {3D0E15DD-5C66-4C2E-BAD1-F10A3B7B4A31} - {C9C00BD6-0C15-45C2-B976-BF6111635BDF} = {3D0E15DD-5C66-4C2E-BAD1-F10A3B7B4A31} - {88A5B434-451F-4C83-852B-A140CA97C93C} = {D9BD60AE-2972-4608-8E0D-16929D42A0D9} - {0DCAAEF5-28AB-4512-BCDE-0B80906469B4} = {3D0E15DD-5C66-4C2E-BAD1-F10A3B7B4A31} - {61768B8B-B9E7-4B3D-BD95-BE8358CDDDB0} = {95D2B90C-DBBA-4AF7-BAE8-4E933E050E75} - {6326E721-2DFE-45C2-A30F-98D3AA5C202A} = {3D0E15DD-5C66-4C2E-BAD1-F10A3B7B4A31} - {F6FD6F8C-1CC7-43B2-9B6F-FDA696BBB840} = {3D0E15DD-5C66-4C2E-BAD1-F10A3B7B4A31} - {70DFB4D9-A21F-4F91-93BF-C89B94933F49} = {95D2B90C-DBBA-4AF7-BAE8-4E933E050E75} - {B6733A7C-2B94-4F52-B86D-75C8713691FB} = {95D2B90C-DBBA-4AF7-BAE8-4E933E050E75} - {1FD125E5-A9AB-40CC-B09B-B2E9745243EA} = {B6733A7C-2B94-4F52-B86D-75C8713691FB} - {4C3FE42C-B802-46C9-B062-110CE9F716CD} = {61768B8B-B9E7-4B3D-BD95-BE8358CDDDB0} - {1B7A4E77-52F0-412E-9220-74EFA35F7C02} = {61768B8B-B9E7-4B3D-BD95-BE8358CDDDB0} - {C74C1DB2-77CB-42F3-BC99-D998BFC91EA3} = {61768B8B-B9E7-4B3D-BD95-BE8358CDDDB0} - {667C2A80-D723-4DF7-86AC-E9978D8640A6} = {61768B8B-B9E7-4B3D-BD95-BE8358CDDDB0} - {C991F65F-AA10-4D3F-A7C8-FE80738DABDA} = {61768B8B-B9E7-4B3D-BD95-BE8358CDDDB0} - {52FC50E3-4944-4B08-BDAC-F12CF132057B} = {61768B8B-B9E7-4B3D-BD95-BE8358CDDDB0} - {BAD825DF-26C7-48EB-BF02-1DB4C4BA4AAE} = {95D2B90C-DBBA-4AF7-BAE8-4E933E050E75} - {6C13CDA3-46B4-4624-8170-8DD74C18B215} = {2CE21CBD-4B31-4408-A0E1-01E673F6FADF} - {2C9BC284-F481-4C94-A40E-F818055DB352} = {ADC45E09-87EA-482A-B306-C361184C13D4} - {75D43E9A-AA35-407F-85A2-E681962AA71E} = {95D2B90C-DBBA-4AF7-BAE8-4E933E050E75} - {30AB9BB0-925D-4B05-8E31-EDEC238869D5} = {63B6708D-861F-4BAA-B35C-F52D68E6D84A} - {95BCD5E7-B37F-4E36-AC00-21708F4E87BA} = {63B6708D-861F-4BAA-B35C-F52D68E6D84A} - {C26876E9-CED9-46F0-B1F6-4DF7F03C9584} = {95D2B90C-DBBA-4AF7-BAE8-4E933E050E75} - EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {D744631F-1DD6-4290-BF2D-67925BA2341D} + SolutionGuid = {A9FB3137-5BE5-44D0-86AC-4BC1DA4BA066} EndGlobalSection EndGlobal