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/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..6439ebc 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, )",
@@ -97,14 +81,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 +163,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 +215,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 +316,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",
@@ -526,11 +503,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 +1126,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.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