diff --git a/.gitignore b/.gitignore index 3699d25..bad3b67 100644 --- a/.gitignore +++ b/.gitignore @@ -346,4 +346,5 @@ coverage.cobertura.xml # Scripted build artifacts /_codeCoverage -/_packages \ No newline at end of file +/_packages +*.sbom.* \ No newline at end of file diff --git a/README.md b/README.md index 37e4440..0d13c8d 100644 --- a/README.md +++ b/README.md @@ -194,19 +194,39 @@ The `TParameterProvider` is a type which implements `ITemplateParameterProvider< This allows you to process parameters as efficiently as possible, based on the types you need to support. -The benchmarks contain an example built over the low-allocation [Corvus.JsonSchema.ExtendedTypes](https://github.com/corvus-dotnet/Corvus.JsonSchema) called `JsonTemplateParameterProvider` that takes a parameter set based on a JSON object, supporting all JSON element types as parameter values. +The package `Corvus.UriTemplates.Resolvers.Json` contains a `JsonTemplateResolver` that takes a parameter set based on a `System.Text.Json.JsonElement` of `JsonValueKind.Object`. Its properties become the named parameters. ```csharp +using JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }"); object? nullState = default; -JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, JsonValues, HandleResult, ref nullState); +JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState); + static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) { - Do what you want with the resolved template! + // Do what you want with the resolved template + // (Typically, you use the state you have passed in to pprovide the resolved template + // to the outside world in some form.) } ``` There are also overloads of `TryResolveResult` which will write to an `IBufferWriter` instead of providing the `ReadOnlySpan` to a callback. +Similarly, a resolver that takes parameters from a `Dictionary` can be found in the package `Corvus.UriTemplates.Resolvers.DictionaryOfObject`. + +```csharp +using JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }"); +object? nullState = default; +JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState); + +static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) +{ + // Do what you want with the resolved template + // (Typically, you use the state you have passed in to pprovide the resolved template + // to the outside world in some form.) +} +``` + +You should examine the implementations of those types if you wish to implement your own low-allocation parameter providers. ## Build and test diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj b/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj index 8a205ae..5f26a56 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj +++ b/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj @@ -12,13 +12,13 @@ + - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs index eb41f62..56ed73a 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs +++ b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs @@ -2,8 +2,8 @@ // Copyright (c) Endjin Limited. All rights reserved. // +using System.Text.Json; using BenchmarkDotNet.Attributes; -using Corvus.Json; namespace Corvus.UriTemplates.Benchmarking; @@ -15,8 +15,8 @@ public class UriTemplateParameterSetting { private const string UriTemplate = "http://example.org/location{?value*}"; private static readonly Dictionary Values = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } }; - private static readonly JsonAny JsonValues = JsonAny.FromProperties(("foo", "bar"), ("bar", "baz"), ("baz", "bob")).AsJsonElementBackedValue(); + private readonly JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }"); private Tavis.UriTemplates.UriTemplate? tavisTemplate; private TavisApi.UriTemplate? corvusTavisTemplate; @@ -39,6 +39,7 @@ public Task GlobalSetup() [GlobalCleanup] public Task GlobalCleanup() { + this.jsonValues.Dispose(); return Task.CompletedTask; } @@ -69,7 +70,7 @@ public void ResolveUriCorvusTavis() public void ResolveUriCorvus() { object? nullState = default; - JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, JsonValues, HandleResult, ref nullState); + JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, this.jsonValues.RootElement, HandleResult, ref nullState); static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) { // NOP diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json b/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json index 3158952..2b2a00e 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json +++ b/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json @@ -32,22 +32,6 @@ "Microsoft.Diagnostics.Tracing.TraceEvent": "3.0.2" } }, - "Corvus.Json.ExtendedTypes": { - "type": "Direct", - "requested": "[1.0.0-v1-pre1.174, )", - "resolved": "1.0.0-v1-pre1.174", - "contentHash": "u/bjziXGWKnkIuZgvwTE0/ngpImRLbXThszBUjxqNXZT21JR50lJue1HiHs1Ckw/0oFsTq65n8ijZAmRpxLdGg==", - "dependencies": { - "CommunityToolkit.HighPerformance": "8.0.0", - "Corvus.Extensions": "1.1.4", - "Microsoft.Extensions.Http": "7.0.0-rc.1.22426.10", - "Microsoft.Extensions.ObjectPool": "7.0.0-rc.1.22427.2", - "NodaTime": "3.1.2", - "System.Buffers": "4.5.1", - "System.Collections.Immutable": "7.0.0-rc.1.22426.10", - "System.Text.Json": "7.0.0-rc.1.22426.10" - } - }, "Endjin.RecommendedPractices.GitHub": { "type": "Direct", "requested": "[2.1.2, )", @@ -58,21 +42,6 @@ "Microsoft.SourceLink.GitHub": "1.1.1" } }, - "Roslynator.Analyzers": { - "type": "Direct", - "requested": "[4.1.1, )", - "resolved": "4.1.1", - "contentHash": "3cPVlrB1PytlO1ztZZBOExDKQWpMZgI15ZDa0BqLu0l6xv+xIRfEpqjNRcpvUy3aLxWTkPgSKZbbaO+VoFEJ1g==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.435, )", - "resolved": "1.2.0-beta.435", - "contentHash": "TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.435" - } - }, "Tavis.UriTemplates": { "type": "Direct", "requested": "[1.1.1, )", @@ -97,14 +66,6 @@ "resolved": "8.0.0", "contentHash": "S5Iv1d5UJZNJLJbe/xzJmLqYJ2mhefbLAvhXCZEh3G4sFadUBuQZhQioE4oJG4enY69QMuJX3AX9+6P9rH1bMw==" }, - "Corvus.Extensions": { - "type": "Transitive", - "resolved": "1.1.4", - "contentHash": "WGwNzQDNrlxfH82iRSSXcG92yKhE8xlBMWoSC4dycp0MnH2Mle0TF+Y4keRgDAdDwXg8VC+3paZx64jVG1Jazg==", - "dependencies": { - "System.Interactive": "3.2.0" - } - }, "Endjin.RecommendedPractices": { "type": "Transitive", "resolved": "2.1.2", @@ -187,45 +148,50 @@ "resolved": "3.1.6", "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" }, - "Microsoft.Extensions.DependencyInjection": { + "Microsoft.Extensions.Configuration": { "type": "Transitive", - "resolved": "7.0.0-rc.1.22426.10", - "contentHash": "P7COzZFQKK9YzxomJUl9zXOKkt+5JJ4BIwHl/sN7+gHWAGjY9bD3yqV0Vzf5moGahBVrvL7dWyX1AN2MeoL68g==", + "resolved": "2.1.1", + "contentHash": "LjVKO6P2y52c5ZhTLX/w8zc5H4Y3J/LJsgqTBj49TtFq/hAtVNue/WA0F6/7GMY90xhD7K0MDZ4qpOeWXbLvzg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0-rc.1.22426.10" + "Microsoft.Extensions.Configuration.Abstractions": "2.1.1" } }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { + "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", - "resolved": "7.0.0-rc.1.22426.10", - "contentHash": "DJfKuxPF9IF1BnFxpEYLLXthLg6QEXopGtxGfuSCc3/fWJdnwPwSCTA6DOl5LHtRHogJlq6haL3Ehh+RNVpW7Q==" + "resolved": "2.1.1", + "contentHash": "VfuZJNa0WUshZ/+8BFZAhwFKiKuu/qOUCFntfdLpHj7vcRnsGHqd3G2Hse78DM+pgozczGM63lGPRLmy+uhUOA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "2.1.1" + } }, - "Microsoft.Extensions.Http": { + "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", - "resolved": "7.0.0-rc.1.22426.10", - "contentHash": "qcNuBOGM2mu+yAUiqCDoRL6vLcW3vfVze/5QAmfWUIg/duUXd2D4kiLvyXiCt4cNWMS5X2Zw5+tlt8c/MnBq1g==", + "resolved": "2.1.1", + "contentHash": "fcLCTS03poWE4v9tSNBr3pWn0QwGgAn1vzqHXlXgvqZeOc7LvQNzaWcKRQZTdEc3+YhQKwMsOtm3VKSA2aWQ8w==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0-rc.1.22426.10", - "Microsoft.Extensions.Logging": "7.0.0-rc.1.22426.10", - "Microsoft.Extensions.Logging.Abstractions": "7.0.0-rc.1.22426.10", - "Microsoft.Extensions.Options": "7.0.0-rc.1.22426.10" + "Microsoft.Extensions.Configuration": "2.1.1" } }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "MgYpU5cwZohUMKKg3sbPhvGG+eAZ/59E9UwPwlrUkyXU+PGzqwZg9yyQNjhxuAWmoNoFReoemeCku50prYSGzA==" + }, "Microsoft.Extensions.Logging": { "type": "Transitive", - "resolved": "7.0.0-rc.1.22426.10", - "contentHash": "8RkgzXx5vTJGFd60GBlk5vybpEBUXTxfgcRgQw1uaM2kAjovc/Q5xezT+u58xDsy4rLDfd0pGZ6EisV5GOcy4w==", + "resolved": "2.1.1", + "contentHash": "hh+mkOAQDTp6XH80xJt3+wwYVzkbwYQl9XZRCz4Um0JjP/o7N9vHM3rZ6wwwtr+BBe/L6iBO2sz0px6OWBzqZQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "7.0.0-rc.1.22426.10", - "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0-rc.1.22426.10", - "Microsoft.Extensions.Logging.Abstractions": "7.0.0-rc.1.22426.10", - "Microsoft.Extensions.Options": "7.0.0-rc.1.22426.10" + "Microsoft.Extensions.Configuration.Binder": "2.1.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1", + "Microsoft.Extensions.Logging.Abstractions": "2.1.1", + "Microsoft.Extensions.Options": "2.1.1" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "7.0.0-rc.1.22426.10", - "contentHash": "ElrDslqtljpyFZTyygITf3VmYEWUE/ZsTaf7jltzbFohp5u51CLQ2OlO0dRSA8CsFqX/Axiu9vMwyUmt8NJo2g==" + "resolved": "2.1.1", + "contentHash": "XRzK7ZF+O6FzdfWrlFTi1Rgj2080ZDsd46vzOjadHUB0Cz5kOvDG8vI7caa5YFrsHQpcfn0DxtjS4E46N4FZsA==" }, "Microsoft.Extensions.ObjectPool": { "type": "Transitive", @@ -234,17 +200,21 @@ }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "7.0.0-rc.1.22426.10", - "contentHash": "DQ0guZczQIwu7kGsKy7072ncZOdbHC8r1bWLbh+/NaL/cVC1mrlK8UA8fQP+trtp2YYfOrfaF0wqffYkRdpQKg==", + "resolved": "2.1.1", + "contentHash": "V7lXCU78lAbzaulCGFKojcCyG8RTJicEbiBkPJjFqiqXwndEBBIehdXRMWEVU3UtzQ1yDvphiWUL9th6/4gJ7w==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0-rc.1.22426.10", - "Microsoft.Extensions.Primitives": "7.0.0-rc.1.22426.10" + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1", + "Microsoft.Extensions.Primitives": "2.1.1" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "7.0.0-rc.1.22426.10", - "contentHash": "BELLSkZWsX/Y4uNqbCiAnwBH5NnVfMAxs50SXUaV+edkO4rRJdskxHTLMgLCqtnTcy4UnWsERsnj/3zONGlogA==" + "resolved": "2.1.1", + "contentHash": "scJ1GZNIxMmjpENh0UZ8XCQ6vzr/LzeF9WvEA51Ix2OQGAs9WPgPu8ABVUdvpKPLuor/t05gm6menJK3PwqOXg==", + "dependencies": { + "System.Memory": "4.5.1", + "System.Runtime.CompilerServices.Unsafe": "4.5.1" + } }, "Microsoft.NETCore.Platforms": { "type": "Transitive", @@ -331,14 +301,6 @@ "System.Xml.XDocument": "4.0.11" } }, - "NodaTime": { - "type": "Transitive", - "resolved": "3.1.2", - "contentHash": "KAlnzQ2EtrrRhFoVePf2kMc24CXX3mAslfp+LmVQnk6HSZ8whgsHNpfUfO+jWCdMgGKQQKMjNCDgmTopjTPFFA==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.7.1" - } - }, "Perfolizer": { "type": "Transitive", "resolved": "0.2.1", @@ -383,11 +345,6 @@ "Microsoft.NETCore.Targets": "1.0.1" } }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.435", - "contentHash": "ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg==" - }, "System.AppContext": { "type": "Transitive", "resolved": "4.1.0", @@ -526,11 +483,6 @@ "System.Runtime.InteropServices": "4.1.0" } }, - "System.Interactive": { - "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "hoXiC7r+WvT/oQ/QcsCgIJMEcXKXyM26BvIcFVRgEMzXk9URu8oR2ADqrnHwIRiJmxQC/q8b3KTQSkdoFRO4TA==" - }, "System.IO": { "type": "Transitive", "resolved": "4.1.0", @@ -1154,6 +1106,17 @@ "System.Buffers": "[4.5.1, )", "System.Collections.Immutable": "[7.0.0-rc.1.22426.10, )" } + }, + "corvus.uritemplates.resolvers.json": { + "type": "Project", + "dependencies": { + "CommunityToolkit.HighPerformance": "[8.0.0, )", + "Corvus.UriTemplates": "[1.0.0, )", + "Microsoft.Extensions.ObjectPool": "[7.0.0-rc.1.22427.2, )", + "System.Buffers": "[4.5.1, )", + "System.Collections.Immutable": "[7.0.0-rc.1.22426.10, )", + "System.Text.Json": "[7.0.0-rc.1.22426.10, )" + } } } } diff --git a/Solutions/Corvus.UriTemplate.TavisApi.Tests/UriTemplateTests.csproj b/Solutions/Corvus.UriTemplate.TavisApi.Tests/UriTemplateTests.csproj index ba5391b..c503766 100644 --- a/Solutions/Corvus.UriTemplate.TavisApi.Tests/UriTemplateTests.csproj +++ b/Solutions/Corvus.UriTemplate.TavisApi.Tests/UriTemplateTests.csproj @@ -16,6 +16,7 @@ + diff --git a/Solutions/Corvus.UriTemplates.Resolvers.DictionaryOfObject/Corvus.UriTemplates.Resolvers.DictionaryOfObject.csproj b/Solutions/Corvus.UriTemplates.Resolvers.DictionaryOfObject/Corvus.UriTemplates.Resolvers.DictionaryOfObject.csproj new file mode 100644 index 0000000..d411b25 --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.DictionaryOfObject/Corvus.UriTemplates.Resolvers.DictionaryOfObject.csproj @@ -0,0 +1,38 @@ + + + + + net6.0;net7.0 + enable + enable + + + + Apache-2.0 + Dictionary of object parameter provider for a low allocation implementation of URI template functions conforming to http://tools.ietf.org/html/rfc6570. + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + diff --git a/Solutions/Corvus.UriTemplates.Resolvers.DictionaryOfObject/Corvus.UriTemplates/DictionaryTemplateParameterProvider.cs b/Solutions/Corvus.UriTemplates.Resolvers.DictionaryOfObject/Corvus.UriTemplates/DictionaryTemplateParameterProvider.cs new file mode 100644 index 0000000..ef0b1d6 --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.DictionaryOfObject/Corvus.UriTemplates/DictionaryTemplateParameterProvider.cs @@ -0,0 +1,296 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Buffers; +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using CommunityToolkit.HighPerformance; +using Corvus.UriTemplates.TemplateParameterProviders; + +namespace Corvus.UriTemplates; + +/// +/// 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. +#if NET6_0 + public VariableProcessingState ProcessVariable(ref VariableSpecification variableSpecification, in IDictionary parameters, IBufferWriter output) +#else + public static VariableProcessingState ProcessVariable(ref VariableSpecification variableSpecification, in IDictionary parameters, IBufferWriter output) +#endif + { + 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.Write(variableSpecification.OperatorInfo.First); + } + } + else + { + output.Write(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(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, list.Count == 0); + } + + AppendArray(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(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, dictionary.Count == 0); + } + + AppendObject(output, variableSpecification.OperatorInfo, variableSpecification.Explode, dictionary); + } + else if (value is string stringValue) + { + if (variableSpecification.OperatorInfo.Named) + { + AppendNameAndStringValue(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, stringValue, variableSpecification.PrefixLength, variableSpecification.OperatorInfo.AllowReserved); + } + else + { + AppendValue(output, stringValue, variableSpecification.PrefixLength, variableSpecification.OperatorInfo.AllowReserved); + } + } + else + { + // Fallback to string + string? fallbackStringValue = value?.ToString(); + if (variableSpecification.OperatorInfo.Named) + { + AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, string.IsNullOrEmpty(fallbackStringValue)); + } + + AppendValue(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(IBufferWriter output, in OperatorInfo op, bool explode, ReadOnlySpan variable, in IList array) + { + bool isFirst = true; + foreach (object item in array) + { + if (!isFirst) + { + output.Write(explode ? op.Separator : ','); + } + else + { + isFirst = false; + } + + if (op.Named && explode) + { + output.Write(variable); + output.Write('='); + } + + AppendValue(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(IBufferWriter output, in OperatorInfo op, bool explode, in IDictionary instance) + { + bool isFirst = true; + foreach (KeyValuePair value in instance) + { + if (!isFirst) + { + if (explode) + { + output.Write(op.Separator); + } + else + { + output.Write(','); + } + } + else + { + isFirst = false; + } + + WriteEncodedPropertyName(value.Key.AsSpan(), output, op.AllowReserved, out bool decoded); + + if (explode) + { + output.Write('='); + } + else + { + output.Write(','); + } + + AppendValue(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, IBufferWriter output, bool allowReserved, out bool result) + { + TemplateParameterProvider.Encode(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(IBufferWriter output, ReadOnlySpan variable, string ifEmpty, bool valueIsEmpty) + { + output.Write(variable); + + if (valueIsEmpty) + { + output.Write(ifEmpty); + } + else + { + output.Write('='); + } + } + + /// + /// 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(IBufferWriter output, ReadOnlySpan variable, string ifEmpty, string? value, int prefixLength, bool allowReserved) + { + output.Write(variable); + + ReadOnlySpan span = value.AsSpan(); + + // Write the name separator + if (span.Length == 0) + { + output.Write(ifEmpty); + } + else + { + output.Write('='); + } + + WriteStringValue(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(IBufferWriter output, string? value, int prefixLength, bool allowReserved) + { + WriteStringValue(output, value.AsSpan(), prefixLength, allowReserved); + } + + private static void WriteStringValue(IBufferWriter 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(output, valueString, allowReserved); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates.Resolvers.DictionaryOfObject/Corvus.UriTemplates/DictionaryUriTemplateResolver.cs b/Solutions/Corvus.UriTemplates.Resolvers.DictionaryOfObject/Corvus.UriTemplates/DictionaryUriTemplateResolver.cs new file mode 100644 index 0000000..a90d52b --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.DictionaryOfObject/Corvus.UriTemplates/DictionaryUriTemplateResolver.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace Corvus.UriTemplates; + +/// +/// A wrapper around +/// for a . +/// +public static class DictionaryUriTemplateResolver +{ + private static readonly Dictionary EmptyDictionary = new(); +#if NET6_0 + private static readonly DictionaryTemplateParameterProvider ParameterProvider = new(); +#endif + + /// + /// 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) + { +#if NET6_0 + return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, resolvePartially, parameters, callback, parameterNameCallback, ref state); +#else + return UriTemplateResolver>.TryResolveResult(template, resolvePartially, parameters, callback, parameterNameCallback, ref state); +#endif + } + + /// + /// 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) + { +#if NET6_0 + return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, resolvePartially, parameters, callback, null, ref state); +#else + return UriTemplateResolver>.TryResolveResult(template, resolvePartially, parameters, callback, null, ref state); +#endif + } + + /// + /// 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, IBufferWriter output, bool resolvePartially, in IDictionary parameters) + { + object? nullState = default; +#if NET6_0 + return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, output, resolvePartially, parameters, null, ref nullState); +#else + return UriTemplateResolver>.TryResolveResult(template, output, resolvePartially, parameters, null, ref nullState); +#endif + } + + /// + /// 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) + { +#if NET6_0 + return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, true, EmptyDictionary, Nop, callback, ref state); +#else + return UriTemplateResolver>.TryResolveResult(template, true, EmptyDictionary, Nop, callback, ref state); +#endif + + static void Nop(ReadOnlySpan value, ref TState state) + { + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates.Resolvers.Json.csproj b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates.Resolvers.Json.csproj new file mode 100644 index 0000000..70fe974 --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates.Resolvers.Json.csproj @@ -0,0 +1,39 @@ + + + + + net6.0;net7.0 + enable + enable + + + + Apache-2.0 + JSON parameter provider for a low allocation implementation of URI template functions conforming to http://tools.ietf.org/html/rfc6570. + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonConstants.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonConstants.cs new file mode 100644 index 0000000..c9429a8 --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonConstants.cs @@ -0,0 +1,119 @@ +// +// 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. + +#pragma warning disable + +using System; + +namespace Corvus.UriTemplates +{ + internal static class JsonConstants + { + public const byte OpenBrace = (byte)'{'; + public const byte CloseBrace = (byte)'}'; + public const byte OpenBracket = (byte)'['; + public const byte CloseBracket = (byte)']'; + public const byte Space = (byte)' '; + public const byte CarriageReturn = (byte)'\r'; + public const byte LineFeed = (byte)'\n'; + public const byte Tab = (byte)'\t'; + public const byte ListSeparator = (byte)','; + public const byte KeyValueSeperator = (byte)':'; + public const byte Quote = (byte)'"'; + public const byte BackSlash = (byte)'\\'; + public const byte Slash = (byte)'/'; + public const byte BackSpace = (byte)'\b'; + public const byte FormFeed = (byte)'\f'; + public const byte Asterisk = (byte)'*'; + public const byte Colon = (byte)':'; + public const byte Period = (byte)'.'; + public const byte Plus = (byte)'+'; + public const byte Hyphen = (byte)'-'; + public const byte UtcOffsetToken = (byte)'Z'; + public const byte TimePrefix = (byte)'T'; + + // \u2028 and \u2029 are considered respectively line and paragraph separators + // UTF-8 representation for them is E2, 80, A8/A9 + public const byte StartingByteOfNonStandardSeparator = 0xE2; + + public static ReadOnlySpan Utf8Bom => new byte[] { 0xEF, 0xBB, 0xBF }; + public static ReadOnlySpan TrueValue => new byte[] { (byte)'t', (byte)'r', (byte)'u', (byte)'e' }; + public static ReadOnlySpan FalseValue => new byte[] { (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' }; + public static ReadOnlySpan NullValue => new byte[] { (byte)'n', (byte)'u', (byte)'l', (byte)'l' }; + + public static ReadOnlySpan NaNValue => new byte[] { (byte)'N', (byte)'a', (byte)'N' }; + public static ReadOnlySpan PositiveInfinityValue => new byte[] { (byte)'I', (byte)'n', (byte)'f', (byte)'i', (byte)'n', (byte)'i', (byte)'t', (byte)'y' }; + public static ReadOnlySpan NegativeInfinityValue => new byte[] { (byte)'-', (byte)'I', (byte)'n', (byte)'f', (byte)'i', (byte)'n', (byte)'i', (byte)'t', (byte)'y' }; + + // Used to search for the end of a number + public static ReadOnlySpan Delimiters => new byte[] { ListSeparator, CloseBrace, CloseBracket, Space, LineFeed, CarriageReturn, Tab, Slash }; + + // Explicitly skipping ReverseSolidus since that is handled separately + public static ReadOnlySpan EscapableChars => new byte[] { Quote, (byte)'n', (byte)'r', (byte)'t', Slash, (byte)'u', (byte)'b', (byte)'f' }; + + public const int SpacesPerIndent = 2; + public const int MaxWriterDepth = 1_000; + public const int RemoveFlagsBitMask = 0x7FFFFFFF; + + public const int StackallocThreshold = 256; + + // In the worst case, an ASCII character represented as a single utf-8 byte could expand 6x when escaped. + // For example: '+' becomes '\u0043' + // Escaping surrogate pairs (represented by 3 or 4 utf-8 bytes) would expand to 12 bytes (which is still <= 6x). + // The same factor applies to utf-16 characters. + public const int MaxExpansionFactorWhileEscaping = 6; + + // In the worst case, a single UTF-16 character could be expanded to 3 UTF-8 bytes. + // Only surrogate pairs expand to 4 UTF-8 bytes but that is a transformation of 2 UTF-16 characters goign to 4 UTF-8 bytes (factor of 2). + // All other UTF-16 characters can be represented by either 1 or 2 UTF-8 bytes. + public const int MaxExpansionFactorWhileTranscoding = 3; + + public const int MaxEscapedTokenSize = 1_000_000_000; // Max size for already escaped value. + public const int MaxUnescapedTokenSize = MaxEscapedTokenSize / MaxExpansionFactorWhileEscaping; // 166_666_666 bytes + public const int MaxBase64ValueTokenSize = (MaxEscapedTokenSize >> 2) * 3 / MaxExpansionFactorWhileEscaping; // 125_000_000 bytes + public const int MaxCharacterTokenSize = MaxEscapedTokenSize / MaxExpansionFactorWhileEscaping; // 166_666_666 characters + + public const int MaximumFormatBooleanLength = 5; + public const int MaximumFormatInt64Length = 20; // 19 + sign (i.e. -9223372036854775808) + public const int MaximumFormatUInt64Length = 20; // i.e. 18446744073709551615 + public const int MaximumFormatDoubleLength = 128; // default (i.e. 'G'), using 128 (rather than say 32) to be future-proof. + public const int MaximumFormatSingleLength = 128; // default (i.e. 'G'), using 128 (rather than say 32) to be future-proof. + public const int MaximumFormatDecimalLength = 31; // default (i.e. 'G') + public const int MaximumFormatGuidLength = 36; // default (i.e. 'D'), 8 + 4 + 4 + 4 + 12 + 4 for the hyphens (e.g. 094ffa0a-0442-494d-b452-04003fa755cc) + public const int MaximumEscapedGuidLength = MaxExpansionFactorWhileEscaping * MaximumFormatGuidLength; + public const int MaximumFormatDateTimeLength = 27; // StandardFormat 'O', e.g. 2017-06-12T05:30:45.7680000 + public const int MaximumFormatDateTimeOffsetLength = 33; // StandardFormat 'O', e.g. 2017-06-12T05:30:45.7680000-07:00 + public const int MaxDateTimeUtcOffsetHours = 14; // The UTC offset portion of a TimeSpan or DateTime can be no more than 14 hours and no less than -14 hours. + public const int DateTimeNumFractionDigits = 7; // TimeSpan and DateTime formats allow exactly up to many digits for specifying the fraction after the seconds. + public const int MaxDateTimeFraction = 9_999_999; // The largest fraction expressible by TimeSpan and DateTime formats + public const int DateTimeParseNumFractionDigits = 16; // The maximum number of fraction digits the Json DateTime parser allows + public const int MaximumDateTimeOffsetParseLength = (MaximumFormatDateTimeOffsetLength + + (DateTimeParseNumFractionDigits - DateTimeNumFractionDigits)); // Like StandardFormat 'O' for DateTimeOffset, but allowing 9 additional (up to 16) fraction digits. + public const int MinimumDateTimeParseLength = 10; // YYYY-MM-DD + public const int MaximumEscapedDateTimeOffsetParseLength = MaxExpansionFactorWhileEscaping * MaximumDateTimeOffsetParseLength; + + // Encoding Helpers + public const char HighSurrogateStart = '\ud800'; + public const char HighSurrogateEnd = '\udbff'; + public const char LowSurrogateStart = '\udc00'; + public const char LowSurrogateEnd = '\udfff'; + + public const int UnicodePlane01StartValue = 0x10000; + public const int HighSurrogateStartValue = 0xD800; + public const int HighSurrogateEndValue = 0xDBFF; + public const int LowSurrogateStartValue = 0xDC00; + public const int LowSurrogateEndValue = 0xDFFF; + public const int BitShiftBy10 = 0x400; + + // The maximum number of parameters a constructor can have where it can be considered + // for a path on deserialization where we don't box the constructor arguments. + public const int UnboxedParameterCountThreshold = 4; + + // The maximum number of parameters a constructor can have where it can be supported. + public const int MaxParameterCount = 64; + } +} \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs new file mode 100644 index 0000000..d7e5e3a --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs @@ -0,0 +1,247 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; + +namespace Corvus.UriTemplates; + +/// +/// Extensions to JsonElement to provide raw string processing. +/// +internal static class JsonElementExtensions +{ + /// + /// Attempts to represent the current JSON string as the given type. + /// + /// The type of the parser state. + /// The type with which to represent the JSON string. + /// The JSON element to extend. + /// A delegate to the method that parses the JSON string. + /// The state for the parser. + /// Receives the value. + /// + /// This method does not create a representation of values other than JSON strings. + /// + /// + /// if the string can be represented as the given type, + /// otherwise. + /// + /// + /// This value's is not . + /// + /// + /// The parent has been disposed. + /// + public static bool TryGetValue(this JsonElement element, in Utf8Parser parser, in TState state, [NotNullWhen(true)] out TResult? value) + { + return element.TryGetValue(parser, state, true, out value); + } + + /// + /// Attempts to represent the current JSON string as the given type. + /// + /// The type of the parser state. + /// The type with which to represent the JSON string. + /// The JSON element to extend. + /// A delegate to the method that parses the JSON string. + /// The state for the parser. + /// Indicates whether the UTF8 JSON string should be decoded. + /// Receives the value. + /// + /// This method does not create a representation of values other than JSON strings. + /// + /// + /// if the string can be represented as the given type, + /// otherwise. + /// + /// + /// This value's is not . + /// + /// + /// The parent has been disposed. + /// + public static bool TryGetValue(this JsonElement element, in Utf8Parser parser, in TState state, bool decode, [NotNullWhen(true)] out TResult? value) + { + if (element.ValueKind != JsonValueKind.String) + { + throw new InvalidOperationException(); + } + + return element.ProcessRawText(new Utf8ParserStateWrapper(parser, state, decode), ProcessRawText, out value); + } + + /// + /// Attempts to represent the current JSON string as the given type. + /// + /// The type of the parser state. + /// The type with which to represent the JSON string. + /// The JSON element to extend. + /// A delegate to the method that parses the JSON string. + /// The state for the parser. + /// Receives the value. + /// + /// This method does not create a representation of values other than JSON strings. + /// + /// + /// if the string can be represented as the given type, + /// otherwise. + /// + /// + /// This value's is not . + /// + /// + /// The parent has been disposed. + /// + public static bool TryGetValue(this JsonElement element, in Parser parser, in TState state, [NotNullWhen(true)] out TResult? value) + { + if (element.ValueKind != JsonValueKind.String) + { + throw new InvalidOperationException(); + } + + return element.ProcessRawText(new ParserStateWrapper(parser, state), ProcessRawText, out value); + } + + private static bool ProcessRawText(ReadOnlySpan rawInput, in Utf8ParserStateWrapper state, [NotNullWhen(true)] out TResult? value) + { + if (!state.Decode) + { + return state.Parser(rawInput, state.State, out value); + } + else + { + int idx = rawInput.IndexOf(JsonConstants.BackSlash); + + if (idx < 0) + { + return state.Parser(rawInput, state.State, out value); + } + + byte[]? sourceArray = null; + int length = rawInput.Length; + Span sourceUnescaped = length <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (sourceArray = ArrayPool.Shared.Rent(length)); + JsonReaderHelper.Unescape(rawInput, sourceUnescaped, 0, out int written); + sourceUnescaped = sourceUnescaped[..written]; + + try + { + return state.Parser(sourceUnescaped, state.State, out value); + } + finally + { + if (sourceArray != null) + { + sourceUnescaped.Clear(); + ArrayPool.Shared.Return(sourceArray); + } + } + } + } + + private static bool ProcessRawText(ReadOnlySpan rawInput, in ParserStateWrapper state, [NotNullWhen(true)] out TResult? result) + { + int idx = rawInput.IndexOf(JsonConstants.BackSlash); + + if (idx >= 0) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int length = rawInput.Length; + byte[]? pooledName = null; + + Span utf8Unescaped = + length <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledName = ArrayPool.Shared.Rent(length)); + + JsonReaderHelper.Unescape(rawInput, utf8Unescaped, idx, out int written); + utf8Unescaped = utf8Unescaped[..written]; + + try + { + return ProcessDecodedText(utf8Unescaped, state, out result); + } + finally + { + if (pooledName != null) + { + utf8Unescaped.Clear(); + ArrayPool.Shared.Return(pooledName); + } + } + } + else + { + return ProcessDecodedText(rawInput, state, out result); + } + } + + private static bool ProcessDecodedText(ReadOnlySpan decodedUtf8String, in ParserStateWrapper state, [NotNullWhen(true)] out TResult? value) + { + char[]? sourceTranscodedArray = null; + int length = checked(decodedUtf8String.Length * JsonConstants.MaxExpansionFactorWhileTranscoding); + Span sourceTranscoded = length <= JsonConstants.StackallocThreshold ? + stackalloc char[JsonConstants.StackallocThreshold] : + (sourceTranscodedArray = ArrayPool.Shared.Rent(length)); + int writtenTranscoded = JsonReaderHelper.TranscodeHelper(decodedUtf8String, sourceTranscoded); + sourceTranscoded = sourceTranscoded[..writtenTranscoded]; + + bool success = false; + if (state.Parser(sourceTranscoded, state.State, out TResult? tmp)) + { + value = tmp; + success = true; + } + else + { + value = default; + } + + if (sourceTranscodedArray != null) + { + sourceTranscoded.Clear(); + ArrayPool.Shared.Return(sourceTranscodedArray); + } + + return success; + } + + /// + /// Wraps up the state for the UTF8 parser and the parser's native state into a compound state entity. + /// + private readonly struct Utf8ParserStateWrapper + { + public Utf8ParserStateWrapper(Utf8Parser parser, in TState state, bool decode) + { + this.Parser = parser; + this.State = state; + this.Decode = decode; + } + + public Utf8Parser Parser { get; } + + public TState State { get; } + + public bool Decode { get; } + } + + /// + /// Wraps up the state for the parser and the parser's native state into a compound state entity. + /// + private readonly struct ParserStateWrapper + { + public ParserStateWrapper(Parser parser, in TState state) + { + this.Parser = parser; + this.State = state; + } + + public Parser Parser { get; } + + public TState State { get; } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonHelpers.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonHelpers.cs new file mode 100644 index 0000000..304588e --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonHelpers.cs @@ -0,0 +1,34 @@ +// +// 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. + +#pragma warning disable + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Corvus.UriTemplates +{ + internal static partial class JsonHelpers + { + // Copy of Array.MaxArrayLength. For byte arrays the limit is slightly larger + private const int MaxArrayLength = 0X7FEFFFFF; + + /// + /// Returns if is between + /// and , inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) + => (value - lowerBound) <= (upperBound - lowerBound); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs new file mode 100644 index 0000000..c22434a --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs @@ -0,0 +1,651 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; + +namespace Corvus.UriTemplates; + +/// +/// Extensions to JsonProperty to provide raw string processing. +/// +internal static class JsonPropertyExtensions +{ + /// + /// Attempts to represent the JSON property name as the given type. + /// + /// The type of the parser state. + /// The type with which to represent the JSON string. + /// The JSON element to extend. + /// A delegate to the method that parses the JSON string. + /// The state for the parser. + /// Receives the value. + /// + /// if the string can be represented as the given type, + /// otherwise. + /// + /// + /// The parent has been disposed. + /// + public static bool TryGetName(this JsonProperty property, in Utf8Parser parser, in TState state, [NotNullWhen(true)] out TResult? value) + { + return property.TryGetName(parser, state, true, out value); + } + + /// + /// Attempts to represent the current JSON string as the given type. + /// + /// The type of the parser state. + /// The type with which to represent the JSON string. + /// The JSON element to extend. + /// A delegate to the method that parses the JSON string. + /// The state for the parser. + /// Indicates whether the UTF8 JSON string should be decoded. + /// Receives the value. + /// + /// This method does not create a representation of values other than JSON strings. + /// + /// + /// if the string can be represented as the given type, + /// otherwise. + /// + /// + /// The parent has been disposed. + /// + public static bool TryGetName(this JsonProperty property, in Utf8Parser parser, in TState state, bool decode, [NotNullWhen(true)] out TResult? value) + { + return property.ProcessRawTextForName(new Utf8ParserStateWrapper(parser, state, decode), ProcessRawText, out value); + } + + /// + /// Attempts to represent the current JSON string as the given type. + /// + /// The type of the parser state. + /// The type with which to represent the JSON string. + /// The JSON element to extend. + /// A delegate to the method that parses the JSON string. + /// The state for the parser. + /// Receives the value. + /// + /// This method does not create a representation of values other than JSON strings. + /// + /// + /// if the string can be represented as the given type, + /// otherwise. + /// + /// + /// The parent has been disposed. + /// + public static bool TryGetName(this JsonProperty property, in Parser parser, in TState state, [NotNullWhen(true)] out TResult? value) + { + return property.ProcessRawTextForName(new ParserStateWrapper(parser, state), ProcessRawText, out value); + } + + /// + /// Attempts to represent the current JSON string property as the given type. + /// + /// The type of the parser state. + /// The type with which to represent the JSON string. + /// The JSON property to extend. + /// A delegate to the method that parses the JSON string. + /// The state for the parser. + /// Receives the value. + /// + /// This method does not create a representation of values other than JSON strings. + /// + /// + /// if the string can be represented as the given type, + /// otherwise. + /// + /// + /// This value's is not . + /// + /// + /// The parent has been disposed. + /// + public static bool TryGetNameAndStringValue(this JsonProperty property, in Utf8PropertyParser parser, in TState state, [NotNullWhen(true)] out TResult? value) + { + return property.TryGetNameAndStringValue(parser, state, true, out value); + } + + /// + /// Attempts to represent the current JSON string property as the given type. + /// + /// The type of the parser state. + /// The type with which to represent the JSON string. + /// The JSON element to extend. + /// A delegate to the method that parses the JSON string. + /// The state for the parser. + /// Indicates whether the UTF8 JSON string should be decoded. + /// Receives the value. + /// + /// This method does not create a representation of values other than JSON strings. + /// + /// + /// if the string can be represented as the given type, + /// otherwise. + /// + /// + /// This value's is not . + /// + /// + /// The parent has been disposed. + /// + public static bool TryGetNameAndStringValue(this JsonProperty property, in Utf8PropertyParser parser, in TState state, bool decode, [NotNullWhen(true)] out TResult? value) + { + if (property.Value.ValueKind != JsonValueKind.String) + { + throw new InvalidOperationException(); + } + + return property.ProcessRawTextForNameAndString(new Utf8PropertyParserStateWrapper(parser, state, decode), ProcessRawTextForNameAndString, out value); + } + + /// + /// Attempts to represent the current JSON string as the given type. + /// + /// The type of the parser state. + /// The type with which to represent the JSON string. + /// The JSON element to extend. + /// A delegate to the method that parses the JSON string. + /// The state for the parser. + /// Receives the value. + /// + /// This method does not create a representation of values other than JSON strings. + /// + /// + /// if the string can be represented as the given type, + /// otherwise. + /// + /// + /// This value's is not . + /// + /// + /// The parent has been disposed. + /// + public static bool TryGetNameAndStringValue(this JsonProperty property, in PropertyParser parser, in TState state, [NotNullWhen(true)] out TResult? value) + { + if (property.Value.ValueKind != JsonValueKind.String) + { + throw new InvalidOperationException(); + } + + return property.ProcessRawTextForNameAndString(new PropertyParserStateWrapper(parser, state), ProcessRawText, out value); + } + + private static bool ProcessRawText(ReadOnlySpan rawInput, in Utf8ParserStateWrapper state, [NotNullWhen(true)] out TResult? value) + { + if (!state.Decode) + { + return state.Parser(rawInput, state.State, out value); + } + else + { + int idx = rawInput.IndexOf(JsonConstants.BackSlash); + + if (idx < 0) + { + return state.Parser(rawInput, state.State, out value); + } + + byte[]? sourceArray = null; + int length = rawInput.Length; + Span sourceUnescaped = length <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (sourceArray = ArrayPool.Shared.Rent(length)); + JsonReaderHelper.Unescape(rawInput, sourceUnescaped, 0, out int written); + sourceUnescaped = sourceUnescaped[..written]; + + try + { + return state.Parser(sourceUnescaped, state.State, out value); + } + finally + { + if (sourceArray != null) + { + sourceUnescaped.Clear(); + ArrayPool.Shared.Return(sourceArray); + } + } + } + } + + private static bool ProcessRawText(ReadOnlySpan rawInput, in ParserStateWrapper state, [NotNullWhen(true)] out TResult? result) + { + int idx = rawInput.IndexOf(JsonConstants.BackSlash); + + if (idx >= 0) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int length = rawInput.Length; + byte[]? pooledName = null; + + Span utf8Unescaped = + length <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledName = ArrayPool.Shared.Rent(length)); + + JsonReaderHelper.Unescape(rawInput, utf8Unescaped, idx, out int written); + utf8Unescaped = utf8Unescaped[..written]; + + try + { + return ProcessDecodedText(utf8Unescaped, state, out result); + } + finally + { + if (pooledName != null) + { + utf8Unescaped.Clear(); + ArrayPool.Shared.Return(pooledName); + } + } + } + else + { + return ProcessDecodedText(rawInput, state, out result); + } + } + + private static bool ProcessDecodedText(ReadOnlySpan decodedUtf8String, in ParserStateWrapper state, [NotNullWhen(true)] out TResult? value) + { + char[]? sourceTranscodedArray = null; + int length = checked(decodedUtf8String.Length * JsonConstants.MaxExpansionFactorWhileTranscoding); + Span sourceTranscoded = length <= JsonConstants.StackallocThreshold ? + stackalloc char[JsonConstants.StackallocThreshold] : + (sourceTranscodedArray = ArrayPool.Shared.Rent(length)); + int writtenTranscoded = JsonReaderHelper.TranscodeHelper(decodedUtf8String, sourceTranscoded); + sourceTranscoded = sourceTranscoded[..writtenTranscoded]; + + bool success = false; + if (state.Parser(sourceTranscoded, state.State, out TResult? tmp)) + { + value = tmp; + success = true; + } + else + { + value = default; + } + + if (sourceTranscodedArray != null) + { + sourceTranscoded.Clear(); + ArrayPool.Shared.Return(sourceTranscodedArray); + } + + return success; + } + + private static bool ProcessRawTextForNameAndString(ReadOnlySpan rawName, ReadOnlySpan rawValue, in Utf8PropertyParserStateWrapper state, [NotNullWhen(true)] out TResult? value) + { + if (!state.Decode) + { + return state.Parser(rawName, rawValue, state.State, out value); + } + else + { + int idx = rawValue.IndexOf(JsonConstants.BackSlash); + int idx2 = rawName.IndexOf(JsonConstants.BackSlash); + + if (idx >= 0 && idx2 >= 0) + { + return ProcessEncodedNameAndEncodedValue(rawName, rawValue, state, out value, idx, idx2); + } + else if (idx >= 0 && idx2 < 0) + { + return ProcessDecodedNameAndEncodedValue(rawName, rawValue, state, out value, idx); + } + else if (idx2 >= 0 && idx < 0) + { + return ProcessEncodedNameAndDecodedValue(rawName, rawValue, state, out value, idx2); + } + else + { + return state.Parser(rawName, rawValue, state.State, out value); + } + } + } + + private static bool ProcessEncodedNameAndEncodedValue(ReadOnlySpan encodedName, ReadOnlySpan encodedValue, Utf8PropertyParserStateWrapper state, out TResult? result, int idx, int idx2) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int nameLength = encodedName.Length; + byte[]? pooledName = null; + + Span utf8UnescapedName = + nameLength <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledName = ArrayPool.Shared.Rent(nameLength)); + + JsonReaderHelper.Unescape(encodedName, utf8UnescapedName, idx, out int writtenName); + utf8UnescapedName = utf8UnescapedName[..writtenName]; + + int valueLength = encodedValue.Length; + byte[]? pooledValue = null; + + Span utf8UnescapedValue = + valueLength <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledValue = ArrayPool.Shared.Rent(valueLength)); + + JsonReaderHelper.Unescape(encodedValue, utf8UnescapedValue, idx2, out int writtenValue); + utf8UnescapedValue = utf8UnescapedValue[..writtenValue]; + + try + { + return state.Parser(utf8UnescapedName, utf8UnescapedValue, state.State, out result); + } + finally + { + if (pooledValue is not null) + { + utf8UnescapedValue.Clear(); + ArrayPool.Shared.Return(pooledValue); + } + + if (pooledName is not null) + { + utf8UnescapedName.Clear(); + ArrayPool.Shared.Return(pooledName); + } + } + } + + private static bool ProcessDecodedNameAndEncodedValue(ReadOnlySpan decodedName, ReadOnlySpan encodedValue, Utf8PropertyParserStateWrapper state, out TResult? result, int idx) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int length = encodedValue.Length; + byte[]? pooledValue = null; + + Span utf8UnescapedValue = + length <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledValue = ArrayPool.Shared.Rent(length)); + + JsonReaderHelper.Unescape(encodedValue, utf8UnescapedValue, idx, out int written); + utf8UnescapedValue = utf8UnescapedValue[..written]; + + try + { + return state.Parser(decodedName, utf8UnescapedValue, state.State, out result); + } + finally + { + if (pooledValue != null) + { + utf8UnescapedValue.Clear(); + ArrayPool.Shared.Return(pooledValue); + } + } + } + + private static bool ProcessEncodedNameAndDecodedValue(ReadOnlySpan encodedName, ReadOnlySpan decodedValue, Utf8PropertyParserStateWrapper state, out TResult? result, int idx) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int length = encodedName.Length; + byte[]? pooledName = null; + + Span utf8UnescapedName = + length <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledName = ArrayPool.Shared.Rent(length)); + + JsonReaderHelper.Unescape(encodedName, utf8UnescapedName, idx, out int written); + utf8UnescapedName = utf8UnescapedName[..written]; + + try + { + return state.Parser(utf8UnescapedName, decodedValue, state.State, out result); + } + finally + { + if (pooledName != null) + { + utf8UnescapedName.Clear(); + ArrayPool.Shared.Return(pooledName); + } + } + } + + private static bool ProcessRawText(ReadOnlySpan rawName, ReadOnlySpan rawInput, in PropertyParserStateWrapper state, [NotNullWhen(true)] out TResult? result) + { + int idx = rawInput.IndexOf(JsonConstants.BackSlash); + int idx2 = rawName.IndexOf(JsonConstants.BackSlash); + + if (idx >= 0 && idx2 >= 0) + { + return ProcessEncodedNameAndEncodedValue(rawName, rawInput, state, out result, idx, idx2); + } + else if (idx >= 0 && idx2 < 0) + { + return ProcessDecodedNameAndEncodedValue(rawName, rawInput, state, out result, idx); + } + else if (idx2 >= 0 && idx < 0) + { + return ProcessEncodedNameAndDecodedValue(rawName, rawInput, state, out result, idx2); + } + else + { + return ProcessDecodedNameAndValue(rawName, rawInput, state, out result); + } + } + + private static bool ProcessEncodedNameAndEncodedValue(ReadOnlySpan encodedName, ReadOnlySpan encodedValue, PropertyParserStateWrapper state, out TResult? result, int idx, int idx2) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int nameLength = encodedName.Length; + byte[]? pooledName = null; + + Span utf8UnescapedName = + nameLength <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledName = ArrayPool.Shared.Rent(nameLength)); + + JsonReaderHelper.Unescape(encodedName, utf8UnescapedName, idx, out int writtenName); + utf8UnescapedName = utf8UnescapedName[..writtenName]; + + int valueLength = encodedValue.Length; + byte[]? pooledValue = null; + + Span utf8UnescapedValue = + valueLength <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledValue = ArrayPool.Shared.Rent(valueLength)); + + JsonReaderHelper.Unescape(encodedValue, utf8UnescapedValue, idx2, out int writtenValue); + utf8UnescapedValue = utf8UnescapedValue[..writtenValue]; + + try + { + return ProcessDecodedNameAndValue(utf8UnescapedName, utf8UnescapedValue, state, out result); + } + finally + { + if (pooledValue is not null) + { + utf8UnescapedValue.Clear(); + ArrayPool.Shared.Return(pooledValue); + } + + if (pooledName is not null) + { + utf8UnescapedName.Clear(); + ArrayPool.Shared.Return(pooledName); + } + } + } + + private static bool ProcessDecodedNameAndEncodedValue(ReadOnlySpan decodedName, ReadOnlySpan encodedValue, PropertyParserStateWrapper state, out TResult? result, int idx) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int length = encodedValue.Length; + byte[]? pooledValue = null; + + Span utf8UnescapedValue = + length <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledValue = ArrayPool.Shared.Rent(length)); + + JsonReaderHelper.Unescape(encodedValue, utf8UnescapedValue, idx, out int written); + utf8UnescapedValue = utf8UnescapedValue[..written]; + + try + { + return ProcessDecodedNameAndValue(decodedName, utf8UnescapedValue, state, out result); + } + finally + { + if (pooledValue != null) + { + utf8UnescapedValue.Clear(); + ArrayPool.Shared.Return(pooledValue); + } + } + } + + private static bool ProcessEncodedNameAndDecodedValue(ReadOnlySpan encodedName, ReadOnlySpan decodedValue, PropertyParserStateWrapper state, out TResult? result, int idx) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int length = encodedName.Length; + byte[]? pooledName = null; + + Span utf8UnescapedName = + length <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledName = ArrayPool.Shared.Rent(length)); + + JsonReaderHelper.Unescape(encodedName, utf8UnescapedName, idx, out int written); + utf8UnescapedName = utf8UnescapedName[..written]; + + try + { + return ProcessDecodedNameAndValue(utf8UnescapedName, decodedValue, state, out result); + } + finally + { + if (pooledName != null) + { + utf8UnescapedName.Clear(); + ArrayPool.Shared.Return(pooledName); + } + } + } + + private static bool ProcessDecodedNameAndValue(ReadOnlySpan decodedName, ReadOnlySpan decodedValue, in PropertyParserStateWrapper state, [NotNullWhen(true)] out TResult? value) + { + char[]? transcodedNameArray = null; + int nameLength = checked(decodedName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding); + Span transcodedName = nameLength <= JsonConstants.StackallocThreshold ? + stackalloc char[JsonConstants.StackallocThreshold] : + (transcodedNameArray = ArrayPool.Shared.Rent(nameLength)); + int writtenName = JsonReaderHelper.TranscodeHelper(decodedName, transcodedName); + transcodedName = transcodedName[..writtenName]; + + char[]? transcodedValueArray = null; + int valueLength = checked(decodedValue.Length * JsonConstants.MaxExpansionFactorWhileTranscoding); + Span transcodedValue = valueLength <= JsonConstants.StackallocThreshold ? + stackalloc char[JsonConstants.StackallocThreshold] : + (transcodedValueArray = ArrayPool.Shared.Rent(valueLength)); + int writtenValue = JsonReaderHelper.TranscodeHelper(decodedValue, transcodedValue); + transcodedValue = transcodedValue[..writtenValue]; + + bool success = false; + if (state.Parser(transcodedName, transcodedValue, state.State, out TResult? tmp)) + { + value = tmp; + success = true; + } + else + { + value = default; + } + + if (transcodedNameArray != null) + { + transcodedName.Clear(); + ArrayPool.Shared.Return(transcodedNameArray); + } + + if (transcodedValueArray != null) + { + transcodedValue.Clear(); + ArrayPool.Shared.Return(transcodedValueArray); + } + + return success; + } + + /// + /// Wraps up the state for the UTF8 parser and the parser's native state into a compound state entity. + /// + private readonly struct Utf8PropertyParserStateWrapper + { + public Utf8PropertyParserStateWrapper(Utf8PropertyParser parser, in TState state, bool decode) + { + this.Parser = parser; + this.State = state; + this.Decode = decode; + } + + public Utf8PropertyParser Parser { get; } + + public TState State { get; } + + public bool Decode { get; } + } + + /// + /// Wraps up the state for the parser and the parser's native state into a compound state entity. + /// + private readonly struct PropertyParserStateWrapper + { + public PropertyParserStateWrapper(PropertyParser parser, in TState state) + { + this.Parser = parser; + this.State = state; + } + + public PropertyParser Parser { get; } + + public TState State { get; } + } + + /// + /// Wraps up the state for the UTF8 parser and the parser's native state into a compound state entity. + /// + private readonly struct Utf8ParserStateWrapper + { + public Utf8ParserStateWrapper(Utf8Parser parser, in TState state, bool decode) + { + this.Parser = parser; + this.State = state; + this.Decode = decode; + } + + public Utf8Parser Parser { get; } + + public TState State { get; } + + public bool Decode { get; } + } + + /// + /// Wraps up the state for the parser and the parser's native state into a compound state entity. + /// + private readonly struct ParserStateWrapper + { + public ParserStateWrapper(Parser parser, in TState state) + { + this.Parser = parser; + this.State = state; + } + + public Parser Parser { get; } + + public TState State { get; } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs new file mode 100644 index 0000000..331c6b4 --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs @@ -0,0 +1,214 @@ +// +// 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. + +#pragma warning disable + +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Corvus.UriTemplates +{ + internal static partial class JsonReaderHelper + { + public static readonly UTF8Encoding s_utf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + + public static bool CanDecodeBase64(ReadOnlySpan utf8Unescaped) + { + byte[]? pooledArray = null; + + Span byteSpan = utf8Unescaped.Length <= JsonConstants.StackallocThreshold ? + stackalloc byte[JsonConstants.StackallocThreshold] : + (pooledArray = ArrayPool.Shared.Rent(utf8Unescaped.Length)); + + OperationStatus status = Base64.DecodeFromUtf8(utf8Unescaped, byteSpan, out int bytesConsumed, out int bytesWritten); + + if (status != OperationStatus.Done) + { + if (pooledArray != null) + { + byteSpan.Clear(); + ArrayPool.Shared.Return(pooledArray); + } + + return false; + } + Debug.Assert(bytesConsumed == utf8Unescaped.Length); + + if (pooledArray != null) + { + byteSpan.Clear(); + ArrayPool.Shared.Return(pooledArray); + } + + return true; + } + + public static ReadOnlySpan GetUnescapedSpan(ReadOnlySpan utf8Source, int idx) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int length = utf8Source.Length; + byte[]? pooledName = null; + + Span utf8Unescaped = length <= JsonConstants.StackallocThreshold ? + stackalloc byte[length] : + (pooledName = ArrayPool.Shared.Rent(length)); + + Unescape(utf8Source, utf8Unescaped, idx, out int written); + Debug.Assert(written > 0); + + ReadOnlySpan propertyName = utf8Unescaped.Slice(0, written).ToArray(); + Debug.Assert(!propertyName.IsEmpty); + + if (pooledName != null) + { + new Span(pooledName, 0, written).Clear(); + ArrayPool.Shared.Return(pooledName); + } + + return propertyName; + } + + internal static void Unescape(ReadOnlySpan source, Span destination, int idx, out int written) + { + Debug.Assert(idx >= 0 && idx < source.Length); + Debug.Assert(source[idx] == JsonConstants.BackSlash); + Debug.Assert(destination.Length >= source.Length); + + source.Slice(0, idx).CopyTo(destination); + written = idx; + + for (; idx < source.Length; idx++) + { + byte currentByte = source[idx]; + if (currentByte == JsonConstants.BackSlash) + { + idx++; + currentByte = source[idx]; + + if (currentByte == JsonConstants.Quote) + { + destination[written++] = JsonConstants.Quote; + } + else if (currentByte == 'n') + { + destination[written++] = JsonConstants.LineFeed; + } + else if (currentByte == 'r') + { + destination[written++] = JsonConstants.CarriageReturn; + } + else if (currentByte == JsonConstants.BackSlash) + { + destination[written++] = JsonConstants.BackSlash; + } + else if (currentByte == JsonConstants.Slash) + { + destination[written++] = JsonConstants.Slash; + } + else if (currentByte == 't') + { + destination[written++] = JsonConstants.Tab; + } + else if (currentByte == 'b') + { + destination[written++] = JsonConstants.BackSpace; + } + else if (currentByte == 'f') + { + destination[written++] = JsonConstants.FormFeed; + } + else if (currentByte == 'u') + { + // The source is known to be valid JSON, and hence if we see a \u, it is guaranteed to have 4 hex digits following it + // Otherwise, the Utf8JsonReader would have alreayd thrown an exception. + Debug.Assert(source.Length >= idx + 5); + + bool result = Utf8Parser.TryParse(source.Slice(idx + 1, 4), out int scalar, out int bytesConsumed, 'x'); + Debug.Assert(result); + Debug.Assert(bytesConsumed == 4); + idx += bytesConsumed; // The loop iteration will increment idx past the last hex digit + + if (JsonHelpers.IsInRangeInclusive((uint)scalar, JsonConstants.HighSurrogateStartValue, JsonConstants.LowSurrogateEndValue)) + { + // The first hex value cannot be a low surrogate. + if (scalar >= JsonConstants.LowSurrogateStartValue) + { + throw new InvalidOperationException($"Read Invalid UTF16: {scalar}"); + } + + Debug.Assert(JsonHelpers.IsInRangeInclusive((uint)scalar, JsonConstants.HighSurrogateStartValue, JsonConstants.HighSurrogateEndValue)); + + idx += 3; // Skip the last hex digit and the next \u + + // We must have a low surrogate following a high surrogate. + if (source.Length < idx + 4 || source[idx - 2] != '\\' || source[idx - 1] != 'u') + { + throw new InvalidOperationException("Read Invalid UTF16"); + } + + // The source is known to be valid JSON, and hence if we see a \u, it is guaranteed to have 4 hex digits following it + // Otherwise, the Utf8JsonReader would have alreayd thrown an exception. + result = Utf8Parser.TryParse(source.Slice(idx, 4), out int lowSurrogate, out bytesConsumed, 'x'); + Debug.Assert(result); + Debug.Assert(bytesConsumed == 4); + + // If the first hex value is a high surrogate, the next one must be a low surrogate. + if (!JsonHelpers.IsInRangeInclusive((uint)lowSurrogate, JsonConstants.LowSurrogateStartValue, JsonConstants.LowSurrogateEndValue)) + { + throw new InvalidOperationException($"Read Invalid UTF16: {lowSurrogate}"); + } + + idx += bytesConsumed - 1; // The loop iteration will increment idx past the last hex digit + + // To find the unicode scalar: + // (0x400 * (High surrogate - 0xD800)) + Low surrogate - 0xDC00 + 0x10000 + scalar = (JsonConstants.BitShiftBy10 * (scalar - JsonConstants.HighSurrogateStartValue)) + + (lowSurrogate - JsonConstants.LowSurrogateStartValue) + + JsonConstants.UnicodePlane01StartValue; + } + + var rune = new Rune(scalar); + int bytesWritten = rune.EncodeToUtf8(destination.Slice(written)); + Debug.Assert(bytesWritten <= 4); + written += bytesWritten; + } + } + else + { + destination[written++] = currentByte; + } + } + } + + public static int TranscodeHelper(ReadOnlySpan utf8Unescaped, Span destination) + { + try + { + return s_utf8Encoding.GetChars(utf8Unescaped, destination); + } + catch (DecoderFallbackException dfe) + { + // We want to be consistent with the exception being thrown + // so the user only has to catch a single exception. + // Since we already throw InvalidOperationException for mismatch token type, + // and while unescaping, using that exception for failure to decode invalid UTF-8 bytes as well. + // Therefore, wrapping the DecoderFallbackException around an InvalidOperationException. + throw new InvalidOperationException("Cannot transcode invalid UTF8 bytes.", dfe); + } + catch (ArgumentException) + { + // Destination buffer was too small; clear it up since the encoder might have not. + destination.Clear(); + throw; + } + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonStringValueParsers.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonStringValueParsers.cs new file mode 100644 index 0000000..497d387 --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonStringValueParsers.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Diagnostics.CodeAnalysis; + +namespace Corvus.UriTemplates; + +/// +/// A delegate to a method that attempts to represent a JSON string as a given type. +/// +/// The type of the state for the parser. +/// The type of the resulting value. +/// The UTF8-encoded JSON string. This may be encoded or decoded depending on context. +/// The state for the parser. +/// The resulting value. +/// +/// This method does not create a representation of values other than JSON strings. +/// +/// +/// if the string can be represented as the given type, +/// otherwise. +/// +internal delegate bool Utf8Parser(ReadOnlySpan span, in TState state, [NotNullWhen(true)] out TResult? value); + +/// +/// A delegate to a method that attempts to represent a JSON string as a given type. +/// +/// The type of the state for the parser. +/// The type of the resulting value. +/// The JSON string. This will always be in its decoded form. +/// The state for the parser. +/// The resulting value. +/// +/// This method does not create a representation of values other than JSON strings. +/// +/// +/// if the string can be represented as the given type, +/// otherwise. +/// +internal delegate bool Parser(ReadOnlySpan span, in TState state, [NotNullWhen(true)] out TResult? value); + +/// +/// A delegate to a method that attempts to represent a JSON string as a given type. +/// +/// The type of the state for the parser. +/// The type of the resulting value. +/// The UTF8-encoded JSON property name. This may be encoded or decoded depending on context. +/// The UTF8-encoded JSON string. This may be encoded or decoded depending on context. +/// The state for the parser. +/// The resulting value. +/// +/// This method does not create a representation of values other than JSON strings. +/// +/// +/// if the string can be represented as the given type, +/// otherwise. +/// +internal delegate bool Utf8PropertyParser(ReadOnlySpan name, ReadOnlySpan span, in TState state, [NotNullWhen(true)] out TResult? value); + +/// +/// A delegate to a method that attempts to represent a JSON string as a given type. +/// +/// The type of the state for the parser. +/// The type of the resulting value. +/// The JSON property name. This may be encoded or decoded depending on context. +/// The JSON string. This will always be in its decoded form. +/// The state for the parser. +/// The resulting value. +/// +/// This method does not create a representation of values other than JSON strings. +/// +/// +/// if the string can be represented as the given type, +/// otherwise. +/// +internal delegate bool PropertyParser(ReadOnlySpan name, ReadOnlySpan span, in TState state, [NotNullWhen(true)] out TResult? value); \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/JsonTemplateParameterProvider.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs similarity index 80% rename from Solutions/Corvus.UriTemplate.Benchmarking/JsonTemplateParameterProvider.cs rename to Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs index 73fd8e1..8a9b470 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/JsonTemplateParameterProvider.cs +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs @@ -5,15 +5,14 @@ using System.Buffers; using System.Text.Json; using CommunityToolkit.HighPerformance; -using Corvus.Json; using Corvus.UriTemplates.TemplateParameterProviders; -namespace Corvus.UriTemplates.Benchmarking; +namespace Corvus.UriTemplates; /// /// Implements a parameter provider over a JsonAny. /// -internal class JsonTemplateParameterProvider : ITemplateParameterProvider +internal class JsonTemplateParameterProvider : ITemplateParameterProvider { /// /// Process the given variable. @@ -25,12 +24,16 @@ internal class JsonTemplateParameterProvider : ITemplateParameterProvider 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 static VariableProcessingState ProcessVariable(ref VariableSpecification variableSpecification, in JsonAny parameters, IBufferWriter output) +#if NET6_0 + public VariableProcessingState ProcessVariable(ref VariableSpecification variableSpecification, in JsonElement parameters, IBufferWriter output) +#else + public static VariableProcessingState ProcessVariable(ref VariableSpecification variableSpecification, in JsonElement parameters, IBufferWriter output) +#endif { - if (!parameters.TryGetProperty(variableSpecification.VarName, out JsonAny value) - || value.IsNullOrUndefined() + if (!parameters.TryGetProperty(variableSpecification.VarName, out JsonElement value) + || IsNullOrUndefined(value) || (value.ValueKind == JsonValueKind.Array && value.GetArrayLength() == 0) - || (value.ValueKind == JsonValueKind.Object && !value.HasProperties())) + || (value.ValueKind == JsonValueKind.Object && !HasProperties(value))) { return VariableProcessingState.NotProcessed; } @@ -65,7 +68,7 @@ public static VariableProcessingState ProcessVariable(ref VariableSpecification if (variableSpecification.OperatorInfo.Named && !variableSpecification.Explode) //// exploding will prefix with list name { - AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, !value.HasProperties()); + AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, !HasProperties(value)); } AppendObject(output, variableSpecification.OperatorInfo, variableSpecification.Explode, value); @@ -85,7 +88,7 @@ public static VariableProcessingState ProcessVariable(ref VariableSpecification { if (variableSpecification.OperatorInfo.Named) { - AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, value.IsNullOrUndefined()); + AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, IsNullOrUndefined(value)); } AppendValue(output, value, variableSpecification.PrefixLength, variableSpecification.OperatorInfo.AllowReserved); @@ -94,6 +97,19 @@ public static VariableProcessingState ProcessVariable(ref VariableSpecification return VariableProcessingState.Success; } + private static bool IsNullOrUndefined(JsonElement value) + { + return + value.ValueKind == JsonValueKind.Undefined || + value.ValueKind == JsonValueKind.Null; + } + + private static bool HasProperties(JsonElement value) + { + using JsonElement.ObjectEnumerator enumerator = value.EnumerateObject(); + return enumerator.MoveNext(); + } + /// /// Append an array to the result. /// @@ -102,10 +118,10 @@ public static VariableProcessingState ProcessVariable(ref VariableSpecification /// Whether to explode the array. /// The variable name. /// The array to add. - private static void AppendArray(IBufferWriter output, in OperatorInfo op, bool explode, ReadOnlySpan variable, in JsonAny array) + private static void AppendArray(IBufferWriter output, in OperatorInfo op, bool explode, ReadOnlySpan variable, in JsonElement array) { bool isFirst = true; - foreach (JsonAny item in array.EnumerateArray()) + foreach (JsonElement item in array.EnumerateArray()) { if (!isFirst) { @@ -133,10 +149,10 @@ private static void AppendArray(IBufferWriter output, in OperatorInfo op, /// The operator info. /// Whether to explode the object. /// The object instance to append. - private static void AppendObject(IBufferWriter output, in OperatorInfo op, bool explode, in JsonAny instance) + private static void AppendObject(IBufferWriter output, in OperatorInfo op, bool explode, in JsonElement instance) { bool isFirst = true; - foreach (JsonObjectProperty value in instance.EnumerateObject()) + foreach (JsonProperty value in instance.EnumerateObject()) { if (!isFirst) { @@ -213,18 +229,10 @@ private static void AppendName(IBufferWriter output, ReadOnlySpan va /// The value to append. /// The prefix length. /// Whether to allow reserved characters. - private static void AppendNameAndStringValue(IBufferWriter output, ReadOnlySpan variable, string ifEmpty, JsonAny value, int prefixLength, bool allowReserved) + private static void AppendNameAndStringValue(IBufferWriter output, ReadOnlySpan variable, string ifEmpty, JsonElement value, int prefixLength, bool allowReserved) { output.Write(variable); - - if (value.HasJsonElementBacking) - { - value.AsJsonElement.TryGetValue(ProcessString, new AppendNameAndValueState(output, ifEmpty, prefixLength, allowReserved), out bool _); - } - else - { - ProcessString(value.AsSpan(), new AppendNameAndValueState(output, ifEmpty, prefixLength, allowReserved), out bool _); - } + value.TryGetValue(ProcessString, new AppendNameAndValueState(output, ifEmpty, prefixLength, allowReserved), out bool _); } /// @@ -234,18 +242,11 @@ private static void AppendNameAndStringValue(IBufferWriter output, ReadOnl /// The value to append. /// The prefix length. /// Whether to allow reserved characters. - private static void AppendValue(IBufferWriter output, JsonAny value, int prefixLength, bool allowReserved) + private static void AppendValue(IBufferWriter output, JsonElement value, int prefixLength, bool allowReserved) { if (value.ValueKind == JsonValueKind.String) { - if (value.HasJsonElementBacking) - { - value.AsJsonElement.TryGetValue(ProcessString, new AppendValueState(output, prefixLength, allowReserved), out bool _); - } - else - { - ProcessString(value.AsSpan(), new AppendValueState(output, prefixLength, allowReserved), out bool _); - } + value.TryGetValue(ProcessString, new AppendValueState(output, prefixLength, allowReserved), out bool _); } else if (value.ValueKind == JsonValueKind.True) { @@ -261,7 +262,7 @@ private static void AppendValue(IBufferWriter output, JsonAny value, int p } else if (value.ValueKind == JsonValueKind.Number) { - double valueNumber = (double)value; + double valueNumber = value.GetDouble(); // The maximum number of digits in a double precision number is 1074; we allocate a little above this Span buffer = stackalloc char[1100]; @@ -310,8 +311,21 @@ private static void WriteStringValue(IBufferWriter output, ReadOnlySpan Output, int PrefixLength, bool AllowReserved) + private readonly struct AppendValueState { + public AppendValueState(IBufferWriter output, int prefixLength, bool allowReserved) + { + this.Output = output; + this.PrefixLength = prefixLength; + this.AllowReserved = allowReserved; + } + + public IBufferWriter Output { get; } + + public int PrefixLength { get; } + + public bool AllowReserved { get; } + public static implicit operator (IBufferWriter Output, int PrefixLength, bool AllowReserved)(AppendValueState value) { return (value.Output, value.PrefixLength, value.AllowReserved); @@ -323,8 +337,24 @@ public static implicit operator AppendValueState((IBufferWriter Output, in } } - private readonly record struct AppendNameAndValueState(IBufferWriter Output, string IfEmpty, int PrefixLength, bool AllowReserved) + private readonly struct AppendNameAndValueState { + public AppendNameAndValueState(IBufferWriter output, string ifEmpty, int prefixLength, bool allowReserved) + { + this.Output = output; + this.IfEmpty = ifEmpty; + this.PrefixLength = prefixLength; + this.AllowReserved = allowReserved; + } + + public IBufferWriter Output { get; } + + public string IfEmpty { get; } + + public int PrefixLength { get; } + + public bool AllowReserved { get; } + public static implicit operator (IBufferWriter Output, string IfEmpty, int PrefixLength, bool AllowReserved)(AppendNameAndValueState value) { return (value.Output, value.IfEmpty, value.PrefixLength, value.AllowReserved); @@ -336,8 +366,18 @@ public static implicit operator AppendNameAndValueState((IBufferWriter Out } } - private readonly record struct WriteEncodedPropertyNameState(IBufferWriter Output, bool AllowReserved) + private readonly struct WriteEncodedPropertyNameState { + public WriteEncodedPropertyNameState(IBufferWriter output, bool allowReserved) + { + this.Output = output; + this.AllowReserved = allowReserved; + } + + public IBufferWriter Output { get; } + + public bool AllowReserved { get; } + public static implicit operator (IBufferWriter Output, bool AllowReserved)(WriteEncodedPropertyNameState value) { return (value.Output, value.AllowReserved); diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/JsonUriTemplateResolver.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonUriTemplateResolver.cs similarity index 69% rename from Solutions/Corvus.UriTemplate.Benchmarking/JsonUriTemplateResolver.cs rename to Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonUriTemplateResolver.cs index 7c2a70a..f8ddbfe 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/JsonUriTemplateResolver.cs +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonUriTemplateResolver.cs @@ -4,9 +4,9 @@ using System.Buffers; using System.Runtime.CompilerServices; -using Corvus.Json; +using System.Text.Json; -namespace Corvus.UriTemplates.Benchmarking; +namespace Corvus.UriTemplates; /// /// A wrapper around @@ -14,6 +14,10 @@ namespace Corvus.UriTemplates.Benchmarking; /// public static class JsonUriTemplateResolver { +#if NET6_0 + private static readonly JsonTemplateParameterProvider ParameterProvider = new(); +#endif + /// /// Resolve the template into an output result. /// @@ -26,9 +30,13 @@ public static class JsonUriTemplateResolver /// 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 JsonAny parameters, ParameterNameCallback? parameterNameCallback, ResolvedUriTemplateCallback callback, ref TState state) + public static bool TryResolveResult(ReadOnlySpan template, bool resolvePartially, in JsonElement parameters, ParameterNameCallback? parameterNameCallback, ResolvedUriTemplateCallback callback, ref TState state) { - return UriTemplateResolver.TryResolveResult(template, resolvePartially, parameters, callback, parameterNameCallback, ref state); +#if NET6_0 + return UriTemplateResolver.TryResolveResult(ParameterProvider, template, resolvePartially, parameters, callback, parameterNameCallback, ref state); +#else + return UriTemplateResolver.TryResolveResult(template, resolvePartially, parameters, callback, parameterNameCallback, ref state); +#endif } /// @@ -42,9 +50,13 @@ public static bool TryResolveResult(ReadOnlySpan template, bool re /// 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 JsonAny parameters, ResolvedUriTemplateCallback callback, ref TState state) + public static bool TryResolveResult(ReadOnlySpan template, bool resolvePartially, in JsonElement parameters, ResolvedUriTemplateCallback callback, ref TState state) { - return UriTemplateResolver.TryResolveResult(template, resolvePartially, parameters, callback, null, ref state); +#if NET6_0 + return UriTemplateResolver.TryResolveResult(ParameterProvider, template, resolvePartially, parameters, callback, null, ref state); +#else + return UriTemplateResolver.TryResolveResult(template, resolvePartially, parameters, callback, null, ref state); +#endif } /// @@ -56,10 +68,14 @@ public static bool TryResolveResult(ReadOnlySpan template, bool re /// 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, IBufferWriter output, bool resolvePartially, in JsonAny parameters) + public static bool TryResolveResult(ReadOnlySpan template, IBufferWriter output, bool resolvePartially, in JsonElement parameters) { object? nullState = default; - return UriTemplateResolver.TryResolveResult(template, output, resolvePartially, parameters, null, ref nullState); +#if NET6_0 + return UriTemplateResolver.TryResolveResult(ParameterProvider, template, output, resolvePartially, parameters, null, ref nullState); +#else + return UriTemplateResolver.TryResolveResult(template, output, resolvePartially, parameters, null, ref nullState); +#endif } /// @@ -73,7 +89,11 @@ public static bool TryResolveResult(ReadOnlySpan template, IBufferWriter(ReadOnlySpan template, ParameterNameCallback callback, ref TState state) { - return UriTemplateResolver.TryResolveResult(template, true, JsonAny.Null, Nop, callback, ref state); +#if NET6_0 + return UriTemplateResolver.TryResolveResult(ParameterProvider, template, true, default, Nop, callback, ref state); +#else + return UriTemplateResolver.TryResolveResult(template, true, default, Nop, callback, ref state); +#endif static void Nop(ReadOnlySpan value, ref TState state) { diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/LowAllocJsonUtils.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/LowAllocJsonUtils.cs new file mode 100644 index 0000000..8e91413 --- /dev/null +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/LowAllocJsonUtils.cs @@ -0,0 +1,201 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Diagnostics.CodeAnalysis; +using System.Text.Encodings.Web; +using System.Text.Json; +using CommunityToolkit.HighPerformance.Buffers; +using Microsoft.Extensions.ObjectPool; + +namespace Corvus.UriTemplates; + +/// +/// Utilies for low allocation access to JSON values. +/// +/// +/// These adapters give access to the underlying UTF8 bytes of JSON string +/// values, unless/until https://github.com/dotnet/runtime/issues/74028 lands. +/// +internal static class LowAllocJsonUtils +{ + private static readonly ObjectPool WriterPool = new DefaultObjectPoolProvider().Create(new Utf8JsonWriterPooledObjectPolicy()); + + /// + /// Process raw JSON text. + /// + /// The type of the state for the processor. + /// The type of the result of processing. + /// The json element to process. + /// The state passed to the processor. + /// The processing callback. + /// The result of processing. + /// True if the processing succeeded, otherwise false. + public static bool ProcessRawText( + this JsonElement element, + in TState state, + in Utf8Parser callback, + [NotNullWhen(true)] out TResult? result) + { + 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); + } + } + } + + /// + /// Process raw JSON text for a property name. + /// + /// The type of the state for the processor. + /// The type of the result of processing. + /// The json property to process. + /// The state passed to the processor. + /// The processing callback. + /// The result of processing. + /// True if the processing succeeded, otherwise false. + public static bool ProcessRawTextForName( + this JsonProperty property, + in TState state, + in Utf8Parser callback, + [NotNullWhen(true)] out TResult? result) + { + PooledWriter? writerPair = null; + try + { + writerPair = WriterPool.Get(); + (Utf8JsonWriter w, ArrayPoolBufferWriter writer) = writerPair.Get(); + property.WriteTo(w); + w.Flush(); + int endOfName = writer.WrittenSpan[1..].IndexOf(JsonConstants.Quote) + 1; + return callback(writer.WrittenSpan[1..endOfName], state, out result); + } + finally + { + if (writerPair is not null) + { + WriterPool.Return(writerPair); + } + } + } + + /// + /// Process raw JSON text. + /// + /// The type of the state for the processor. + /// The type of the result of processing. + /// The json element to process. + /// The state passed to the processor. + /// The processing callback. + /// The result of processing. + /// True if the processing succeeded, otherwise false. + public static bool ProcessRawTextForNameAndString( + this JsonProperty element, + in TState state, + in Utf8PropertyParser callback, + [NotNullWhen(true)] out TResult? result) + { + PooledWriter? writerPair = null; + try + { + writerPair = WriterPool.Get(); + (Utf8JsonWriter w, ArrayPoolBufferWriter writer) = writerPair.Get(); + element.WriteTo(w); + w.Flush(); + + // Find the name and the property value + int endOfName = writer.WrittenSpan[1..].IndexOf(JsonConstants.Quote) + 1; + int startOfSpan = writer.WrittenSpan[(endOfName + 1)..].IndexOf(JsonConstants.Quote) + endOfName + 1; + return callback(writer.WrittenSpan[1..endOfName], writer.WrittenSpan[(startOfSpan + 1)..^1], state, out result); + } + finally + { + if (writerPair is not null) + { + WriterPool.Return(writerPair); + } + } + } + + private class PooledWriter : IDisposable + { + private static readonly ObjectPool> ArrayPoolWriterPool = + new DefaultObjectPoolProvider().Create>(); + + private static readonly JsonWriterOptions Options = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Indented = false, SkipValidation = true }; + + private Utf8JsonWriter? writer; + private (Utf8JsonWriter JsonWriter, ArrayPoolBufferWriter BufferWriter)? value; + + public void Dispose() + { + if (this.value.HasValue) + { + ArrayPoolWriterPool.Return(this.value.Value.BufferWriter); + this.value.Value.JsonWriter.Dispose(); + this.value = null; + this.writer?.Dispose(); + this.writer = null; + } + } + + public (Utf8JsonWriter JsonWriter, ArrayPoolBufferWriter BufferWriter) Get() + { + if (this.value.HasValue) + { + return this.value.Value; + } + + ArrayPoolBufferWriter bufferWriter = ArrayPoolWriterPool.Get(); + bufferWriter.Clear(); + if (this.writer is null) + { + this.writer = new(bufferWriter, Options); + } + else + { + this.writer.Reset(bufferWriter); + } + + (Utf8JsonWriter Writer, ArrayPoolBufferWriter BufferWriter) result = (this.writer, bufferWriter); + this.value = result; + return result; + } + + internal void Reset() + { + if (this.value.HasValue) + { + this.value.Value.BufferWriter.Clear(); + ArrayPoolWriterPool.Return(this.value.Value.BufferWriter); + this.writer!.Reset(); + this.value = null; + } + } + } + + private class Utf8JsonWriterPooledObjectPolicy : PooledObjectPolicy + { + public override PooledWriter Create() + { + return new PooledWriter(); + } + + public override bool Return(PooledWriter obj) + { + obj.Reset(); + return true; + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates.sln b/Solutions/Corvus.UriTemplates.sln index ea155f8..33f97f6 100644 --- a/Solutions/Corvus.UriTemplates.sln +++ b/Solutions/Corvus.UriTemplates.sln @@ -5,10 +5,14 @@ VisualStudioVersion = 17.3.32611.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.UriTemplate.Benchmarking", "Corvus.UriTemplate.Benchmarking\Corvus.UriTemplate.Benchmarking.csproj", "{5921287D-255C-46D0-8864-5644ABD710D9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.UriTemplates", "Corvus.UriTemplates\Corvus.UriTemplates.csproj", "{E6BB4B60-3481-4645-8810-836EFC0CF35B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.UriTemplates", "Corvus.UriTemplates\Corvus.UriTemplates.csproj", "{E6BB4B60-3481-4645-8810-836EFC0CF35B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UriTemplateTests", "Corvus.UriTemplate.TavisApi.Tests\UriTemplateTests.csproj", "{32E0D248-3274-4C77-B1D4-7035E184958B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.UriTemplates.Resolvers.Json", "Corvus.UriTemplates.Resolvers.Json\Corvus.UriTemplates.Resolvers.Json.csproj", "{69C2149D-21D8-48FA-B201-7AFD59EBB5EA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.UriTemplates.Resolvers.DictionaryOfObject", "Corvus.UriTemplates.Resolvers.DictionaryOfObject\Corvus.UriTemplates.Resolvers.DictionaryOfObject.csproj", "{C77AE102-65C4-4C64-8B53-B2476EC80280}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +31,14 @@ Global {32E0D248-3274-4C77-B1D4-7035E184958B}.Debug|Any CPU.Build.0 = Debug|Any CPU {32E0D248-3274-4C77-B1D4-7035E184958B}.Release|Any CPU.ActiveCfg = Release|Any CPU {32E0D248-3274-4C77-B1D4-7035E184958B}.Release|Any CPU.Build.0 = Release|Any CPU + {69C2149D-21D8-48FA-B201-7AFD59EBB5EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69C2149D-21D8-48FA-B201-7AFD59EBB5EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69C2149D-21D8-48FA-B201-7AFD59EBB5EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69C2149D-21D8-48FA-B201-7AFD59EBB5EA}.Release|Any CPU.Build.0 = Release|Any CPU + {C77AE102-65C4-4C64-8B53-B2476EC80280}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C77AE102-65C4-4C64-8B53-B2476EC80280}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C77AE102-65C4-4C64-8B53-B2476EC80280}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C77AE102-65C4-4C64-8B53-B2476EC80280}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates.csproj b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates.csproj index 0f71d7b..990e866 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates.csproj +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates.csproj @@ -2,7 +2,7 @@ - netstandard2.1;net7.0 + net6.0;net7.0 enable enable diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/DictionaryTemplateParameterProvider.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/DictionaryTemplateParameterProvider.cs index 21fdb76..3221a76 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/DictionaryTemplateParameterProvider.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/DictionaryTemplateParameterProvider.cs @@ -25,7 +25,7 @@ internal class DictionaryTemplateParameterProvider : ITemplateParameterProvider< /// 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. -#if NETSTANDARD2_1 +#if NET6_0 public VariableProcessingState ProcessVariable(ref VariableSpecification variableSpecification, in IDictionary parameters, IBufferWriter output) #else public static VariableProcessingState ProcessVariable(ref VariableSpecification variableSpecification, in IDictionary parameters, IBufferWriter output) @@ -187,7 +187,7 @@ private static void AppendObject(IBufferWriter output, in OperatorInfo op, isFirst = false; } - WriteEncodedPropertyName(value.Key.AsSpan(), new WriteEncodedPropertyNameState(output, op.AllowReserved), out bool decoded); + WriteEncodedPropertyName(value.Key.AsSpan(), output, op.AllowReserved, out bool decoded); if (explode) { @@ -206,12 +206,13 @@ private static void AppendObject(IBufferWriter output, in OperatorInfo op, /// Encoded and write the property name to the output. /// /// The name to write. - /// The state for the writer. + /// 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, in WriteEncodedPropertyNameState state, out bool result) + private static bool WriteEncodedPropertyName(ReadOnlySpan name, IBufferWriter output, bool allowReserved, out bool result) { - TemplateParameterProvider.Encode(state.Output, name, state.AllowReserved); + TemplateParameterProvider.Encode(output, name, allowReserved); result = true; return true; } @@ -249,7 +250,20 @@ private static void AppendName(IBufferWriter output, ReadOnlySpan va private static void AppendNameAndStringValue(IBufferWriter output, ReadOnlySpan variable, string ifEmpty, string? value, int prefixLength, bool allowReserved) { output.Write(variable); - ProcessString(value.AsSpan(), new AppendNameAndValueState(output, ifEmpty, prefixLength, allowReserved), out bool _); + + ReadOnlySpan span = value.AsSpan(); + + // Write the name separator + if (span.Length == 0) + { + output.Write(ifEmpty); + } + else + { + output.Write('='); + } + + WriteStringValue(output, span, prefixLength, allowReserved); } /// @@ -261,31 +275,7 @@ private static void AppendNameAndStringValue(IBufferWriter output, ReadOnl /// Whether to allow reserved characters. private static void AppendValue(IBufferWriter output, string? value, int prefixLength, bool allowReserved) { - ProcessString(value.AsSpan(), new AppendValueState(output, prefixLength, allowReserved), out bool _); - } - - private static bool ProcessString(ReadOnlySpan span, in AppendValueState state, out bool encoded) - { - WriteStringValue(state.Output, span, state.PrefixLength, state.AllowReserved); - encoded = true; - return true; - } - - private static bool ProcessString(ReadOnlySpan span, in AppendNameAndValueState state, out bool encoded) - { - // Write the name separator - if (span.Length == 0) - { - state.Output.Write(state.IfEmpty); - } - else - { - state.Output.Write('='); - } - - WriteStringValue(state.Output, span, state.PrefixLength, state.AllowReserved); - encoded = true; - return true; + WriteStringValue(output, value.AsSpan(), prefixLength, allowReserved); } private static void WriteStringValue(IBufferWriter output, ReadOnlySpan span, int prefixLength, bool allowReserved) @@ -303,82 +293,4 @@ private static void WriteStringValue(IBufferWriter output, ReadOnlySpan output, int prefixLength, bool allowReserved) - { - this.Output = output; - this.PrefixLength = prefixLength; - this.AllowReserved = allowReserved; - } - - public IBufferWriter Output { get; } - - public int PrefixLength { get; } - - public bool AllowReserved { get; } - - public static implicit operator (IBufferWriter Output, int PrefixLength, bool AllowReserved)(AppendValueState value) - { - return (value.Output, value.PrefixLength, value.AllowReserved); - } - - public static implicit operator AppendValueState((IBufferWriter Output, int PrefixLength, bool AllowReserved) value) - { - return new AppendValueState(value.Output, value.PrefixLength, value.AllowReserved); - } - } - - private readonly struct AppendNameAndValueState - { - public AppendNameAndValueState(IBufferWriter output, string ifEmpty, int prefixLength, bool allowReserved) - { - this.Output = output; - this.IfEmpty = ifEmpty; - this.PrefixLength = prefixLength; - this.AllowReserved = allowReserved; - } - - public IBufferWriter Output { get; } - - public string IfEmpty { get; } - - public int PrefixLength { get; } - - public bool AllowReserved { get; } - - public static implicit operator (IBufferWriter Output, string IfEmpty, int PrefixLength, bool AllowReserved)(AppendNameAndValueState value) - { - return (value.Output, value.IfEmpty, value.PrefixLength, value.AllowReserved); - } - - public static implicit operator AppendNameAndValueState((IBufferWriter Output, string IfEmpty, int PrefixLength, bool AllowReserved) value) - { - return new AppendNameAndValueState(value.Output, value.IfEmpty, value.PrefixLength, value.AllowReserved); - } - } - - private readonly struct WriteEncodedPropertyNameState - { - public WriteEncodedPropertyNameState(IBufferWriter output, bool allowReserved) - { - this.Output = output; - this.AllowReserved = allowReserved; - } - - public IBufferWriter Output { get; } - - public bool AllowReserved { get; } - - public static implicit operator (IBufferWriter Output, bool AllowReserved)(WriteEncodedPropertyNameState value) - { - return (value.Output, value.AllowReserved); - } - - public static implicit operator WriteEncodedPropertyNameState((IBufferWriter Output, bool AllowReserved) value) - { - return new WriteEncodedPropertyNameState(value.Output, value.AllowReserved); - } - } } \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/DictionaryUriTemplateResolver.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/DictionaryUriTemplateResolver.cs index 5f57605..af919d4 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/DictionaryUriTemplateResolver.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/DictionaryUriTemplateResolver.cs @@ -14,7 +14,7 @@ namespace Corvus.UriTemplates.TavisApi; internal static class DictionaryUriTemplateResolver { private static readonly Dictionary EmptyDictionary = new(); -#if NETSTANDARD2_1 +#if NET6_0 private static readonly DictionaryTemplateParameterProvider ParameterProvider = new(); #endif @@ -32,7 +32,7 @@ internal static class DictionaryUriTemplateResolver [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryResolveResult(ReadOnlySpan template, bool resolvePartially, in IDictionary parameters, ParameterNameCallback parameterNameCallback, ResolvedUriTemplateCallback callback, ref TState state) { -#if NETSTANDARD2_1 +#if NET6_0 return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, resolvePartially, parameters, callback, parameterNameCallback, ref state); #else return UriTemplateResolver>.TryResolveResult(template, resolvePartially, parameters, callback, parameterNameCallback, ref state); @@ -52,7 +52,7 @@ public static bool TryResolveResult(ReadOnlySpan template, bool re [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryResolveResult(ReadOnlySpan template, bool resolvePartially, in IDictionary parameters, ResolvedUriTemplateCallback callback, ref TState state) { -#if NETSTANDARD2_1 +#if NET6_0 return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, resolvePartially, parameters, callback, null, ref state); #else return UriTemplateResolver>.TryResolveResult(template, resolvePartially, parameters, callback, null, ref state); @@ -71,7 +71,7 @@ public static bool TryResolveResult(ReadOnlySpan template, bool re public static bool TryResolveResult(ReadOnlySpan template, IBufferWriter output, bool resolvePartially, in IDictionary parameters) { object? nullState = default; -#if NETSTANDARD2_1 +#if NET6_0 return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, output, resolvePartially, parameters, null, ref nullState); #else return UriTemplateResolver>.TryResolveResult(template, output, resolvePartially, parameters, null, ref nullState); @@ -89,7 +89,7 @@ public static bool TryResolveResult(ReadOnlySpan template, IBufferWriter(ReadOnlySpan template, ParameterNameCallback callback, ref TState state) { -#if NETSTANDARD2_1 +#if NET6_0 return UriTemplateResolver>.TryResolveResult(ParameterProvider, template, true, EmptyDictionary, Nop, callback, ref state); #else return UriTemplateResolver>.TryResolveResult(template, true, EmptyDictionary, Nop, callback, ref state); diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TemplateParameterProviders/ITemplateParameterProvider.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TemplateParameterProviders/ITemplateParameterProvider.cs index 8c6db7a..7e801ff 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TemplateParameterProviders/ITemplateParameterProvider.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TemplateParameterProviders/ITemplateParameterProvider.cs @@ -26,7 +26,7 @@ public interface ITemplateParameterProvider /// 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. -#if NETSTANDARD2_1 +#if NET6_0 VariableProcessingState ProcessVariable(ref VariableSpecification variableSpecification, in TParameterPayload parameters, IBufferWriter output); #else diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TemplateParameterProviders/TemplateParameterProvider.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TemplateParameterProviders/TemplateParameterProvider.cs index ff658ee..0a470b7 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TemplateParameterProviders/TemplateParameterProvider.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TemplateParameterProviders/TemplateParameterProvider.cs @@ -76,7 +76,7 @@ private static bool IsEscapeSequence(ReadOnlySpan value, int i) private static bool IsHex(char v) { -#if NETSTANDARD2_1 +#if NET6_0 Span vSpan = stackalloc char[1]; vSpan[0] = v; return PossibleHexChars.Span.Contains(vSpan, StringComparison.Ordinal); diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserFactory.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserFactory.cs index a90c4e7..608cb90 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserFactory.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserFactory.cs @@ -371,7 +371,7 @@ public bool Consume(ReadOnlySpan segment, out int charsConsumed, P // Now we are looking ahead to the next terminator, or the end of the segment while (segmentEnd < segment.Length) { -#if NETSTANDARD2_1 +#if NET6_0 if (terminatorsSpan.Contains(segment.Slice(segmentEnd, 1), StringComparison.Ordinal)) #else if (terminatorsSpan.Contains(segment[segmentEnd])) diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateResolver{TParameterProvider,TParameterPayload}.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateResolver{TParameterProvider,TParameterPayload}.cs index 340faf0..fe28d00 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateResolver{TParameterProvider,TParameterPayload}.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateResolver{TParameterProvider,TParameterPayload}.cs @@ -52,7 +52,7 @@ private enum States ParsingExpression, } -#if NETSTANDARD2_1 +#if NET6_0 /// /// Resolve the template into an output result. /// @@ -84,7 +84,7 @@ public static bool TryResolveResult(ReadOnlySpan template, bool re ArrayPoolBufferWriter abw = ArrayPoolWriterPool.Get(); try { -#if NETSTANDARD2_1 +#if NET6_0 if (TryResolveResult(parameterProvider, template, abw, resolvePartially, parameters, parameterNameCallback, ref state)) #else if (TryResolveResult(template, abw, resolvePartially, parameters, parameterNameCallback, ref state)) @@ -103,7 +103,7 @@ public static bool TryResolveResult(ReadOnlySpan template, bool re } } -#if NETSTANDARD2_1 +#if NET6_0 /// /// Resolve the template into an output result. /// @@ -173,7 +173,7 @@ public static bool TryResolveResult(ReadOnlySpan template, IBuffer if (character == '}') { -#if NETSTANDARD2_1 +#if NET6_0 if (!ProcessExpression(parameterProvider, template[expressionStart..expressionEnd], output, resolvePartially, parameters, parameterNameCallback, ref state)) #else if (!ProcessExpression(template[expressionStart..expressionEnd], output, resolvePartially, parameters, parameterNameCallback, ref state)) @@ -234,7 +234,7 @@ private static bool IsVarNameChar(char c) || c == '.'; } -#if NETSTANDARD2_1 +#if NET6_0 private static bool ProcessExpression(TParameterProvider parameterProvider, ReadOnlySpan currentExpression, IBufferWriter output, bool resolvePartially, in TParameterPayload parameters, ParameterNameCallback? parameterNameCallback, ref TState state) #else private static bool ProcessExpression(ReadOnlySpan currentExpression, IBufferWriter output, bool resolvePartially, in TParameterPayload parameters, ParameterNameCallback? parameterNameCallback, ref TState state) @@ -295,7 +295,7 @@ private static bool ProcessExpression(ReadOnlySpan currentExpressi case ',': varSpec.VarName = currentExpression[varNameStart..varNameEnd]; multivariableExpression = true; -#if NETSTANDARD2_1 +#if NET6_0 VariableProcessingState success = ProcessVariable(parameterProvider, ref varSpec, output, multivariableExpression, resolvePartially, parameters, parameterNameCallback, ref state); #else VariableProcessingState success = ProcessVariable(ref varSpec, output, multivariableExpression, resolvePartially, parameters, parameterNameCallback, ref state); @@ -342,7 +342,7 @@ private static bool ProcessExpression(ReadOnlySpan currentExpressi varSpec.VarName = currentExpression[varNameStart..varNameEnd]; } -#if NETSTANDARD2_1 +#if NET6_0 VariableProcessingState outerSuccess = ProcessVariable(parameterProvider, ref varSpec, output, multivariableExpression, resolvePartially, parameters, parameterNameCallback, ref state); #else VariableProcessingState outerSuccess = ProcessVariable(ref varSpec, output, multivariableExpression, resolvePartially, parameters, parameterNameCallback, ref state); @@ -361,7 +361,7 @@ private static bool ProcessExpression(ReadOnlySpan currentExpressi return true; } -#if NETSTANDARD2_1 +#if NET6_0 private static VariableProcessingState ProcessVariable(TParameterProvider parameterProvider, ref VariableSpecification varSpec, IBufferWriter output, bool multiVariableExpression, bool resolvePartially, in TParameterPayload parameters, ParameterNameCallback? parameterNameCallback, ref TState state) #else private static VariableProcessingState ProcessVariable(ref VariableSpecification varSpec, IBufferWriter output, bool multiVariableExpression, bool resolvePartially, in TParameterPayload parameters, ParameterNameCallback? parameterNameCallback, ref TState state) @@ -372,7 +372,7 @@ private static VariableProcessingState ProcessVariable(ref VariableSpeci callback(varSpec.VarName, ref state); } -#if NETSTANDARD2_1 +#if NET6_0 VariableProcessingState result = parameterProvider.ProcessVariable(ref varSpec, parameters, output); #else VariableProcessingState result = TParameterProvider.ProcessVariable(ref varSpec, parameters, output); diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 49eea3c..2b3e761 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -21,5 +21,6 @@ jobs: service_connection_nuget_org: $(Endjin_Service_Connection_NuGet_Org) service_connection_github: $(Endjin_Service_Connection_GitHub) solution_to_build: $(Endjin_Solution_To_Build) - netSdkVersion: '7.x' + additionalNetSdkVersions: + - '7.x' includeNetSdkPreviewVersions: 'true' diff --git a/build.ps1 b/build.ps1 index 71e8677..44f47b6 100644 --- a/build.ps1 +++ b/build.ps1 @@ -30,6 +30,12 @@ The path to import the Endjin.RecommendedPractices.Build module from. This is useful when testing pre-release versions of the Endjin.RecommendedPractices.Build that are not yet available in the PowerShell Gallery. +.PARAMETER BuildModuleVersion + The version of the Endjin.RecommendedPractices.Build module to import. This is useful when + testing pre-release versions of the Endjin.RecommendedPractices.Build that are not yet + available in the PowerShell Gallery. +.PARAMETER InvokeBuildModuleVersion + The version of the InvokeBuild module to be used. #> [CmdletBinding()] param ( @@ -37,7 +43,7 @@ param ( [string[]] $Tasks = @("."), [Parameter()] - [string] $Configuration = "Release", + [string] $Configuration = "Debug", [Parameter()] [string] $BuildRepositoryUri = "", @@ -62,17 +68,23 @@ param ( [switch] $Clean, [Parameter()] - [string] $BuildModulePath + [string] $BuildModulePath, + + [Parameter()] + [version] $BuildModuleVersion = "0.2.10", + + [Parameter()] + [version] $InvokeBuildModuleVersion = "5.7.1" ) $ErrorActionPreference = $ErrorActionPreference ? $ErrorActionPreference : 'Stop' -$InformationPreference = $InformationAction ? $InformationAction : 'Continue' +$InformationPreference = 'Continue' $here = Split-Path -Parent $PSCommandPath #region InvokeBuild setup if (!(Get-Module -ListAvailable InvokeBuild)) { - Install-Module InvokeBuild -RequiredVersion 5.7.1 -Scope CurrentUser -Force -Repository PSGallery + Install-Module InvokeBuild -RequiredVersion $InvokeBuildModuleVersion -Scope CurrentUser -Force -Repository PSGallery } Import-Module InvokeBuild # This handles calling the build engine when this file is run like a normal PowerShell script @@ -89,68 +101,103 @@ if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') { } #endregion -# Import shared tasks and initialise build framework +#region Import shared tasks and initialise build framework if (!($BuildModulePath)) { - if (!(Get-Module -ListAvailable Endjin.RecommendedPractices.Build)) { + if (!(Get-Module -ListAvailable Endjin.RecommendedPractices.Build | ? { $_.Version -eq $BuildModuleVersion })) { Write-Information "Installing 'Endjin.RecommendedPractices.Build' module..." - Install-Module Endjin.RecommendedPractices.Build -RequiredVersion 0.1.1 -AllowPrerelease -Scope CurrentUser -Force -Repository PSGallery + Install-Module Endjin.RecommendedPractices.Build -RequiredVersion $BuildModuleVersion -Scope CurrentUser -Force -Repository PSGallery } $BuildModulePath = "Endjin.RecommendedPractices.Build" } else { Write-Information "BuildModulePath: $BuildModulePath" } -Import-Module $BuildModulePath -Force +Import-Module $BuildModulePath -RequiredVersion $BuildModuleVersion -Force # Load the build process & tasks . Endjin.RecommendedPractices.Build.tasks +#endregion + # # Build process control options # +$SkipInit = $false $SkipVersion = $false $SkipBuild = $false -$CleanBuild = $false +$CleanBuild = $Clean $SkipTest = $false -$SkipTestReport = $false +$SkipTestReport = $true $SkipPackage = $false +$SkipAnalysis = $false -# Advanced build settings -$EnableGitVersionAdoVariableWorkaround = $false # # Build process configuration # -$SolutionToBuild = (Resolve-Path (Join-Path $here ".\Solutions\Corvus.JsonSchema.sln")).Path - +$SolutionToBuild = (Resolve-Path (Join-Path $here ".\Solutions\Corvus.UriTemplates.sln")).Path +$ProjectsToPublish = @( + # "Solutions/MySolution/MyWebSite/MyWebSite.csproj" +) +$NuSpecFilesToPackage = @( + # "Solutions/MySolution/MyProject/MyProject.nuspec" +) # # Update to the latest report generator versions -# $ReportGeneratorToolVersion = "5.1.10" # -# Temporarily skip the test report -# -$SkipTestReport = $true - -# -# Specify files to exclude from test coverage +# Specify files to exclude from code coverage # This option is for excluding generated code +# - Use file path or directory path with globbing (e.g dir1/*.cs) +# - Use single or multiple paths (separated by comma) (e.g. **/dir1/class1.cs,**/dir2/*.cs,**/dir3/**/*.cs) +# $ExcludeFilesFromCodeCoverage = "" +# +# Temporarily skip the test report +# +$SkipTestReport = $true # Synopsis: Build, Test and Package task . FullBuild # build extensibility tasks +task RunFirst {} +task PreInit { + Write-Host "Initialising submodule" + exec { & git submodule init } + exec { & git submodule update } +} +task PostInit {} +task PreVersion {} +task PostVersion {} task PreBuild {} task PostBuild {} -task PreTest {} -task PostTest {} +task PreTest { + # .net 7 bug workaround - ref: https://github.com/microsoft/vstest/issues/4014 + Write-Host "Set temporary ENV vars for MSBuild" + $env:CollectCoverage = $EnableCoverage + $env:CoverletOutputFormat = "cobertura" +} +task PostTest { + Get-ChildItem env:/CollectCoverage + Get-ChildItem env:/CoverletOutputFormat + + # cleanup .net 7 bug workaround + Write-Host "Clean-up temporary ENV vars for MSBuild" + Remove-Item env:/CollectCoverage + Remove-Item env:/CoverletOutputFormat +} task PreTestReport {} task PostTestReport {} +task PreAnalysis {} +task PostAnalysis {} task PrePackage {} task PostPackage {} +task PrePublish {} +task PostPublish {} +task RunLast {}