diff --git a/README.md b/README.md index 0d13c8d..270d801 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,8 @@ As you can see, there is a significant benefit to using the Corvus implementatio | ExtractParametersCorvusTavis | 495.2 ns | NA | 0.50 | 0.1450 | 608 B | 0.55 | | ExtractParametersCorvus | 174.6 ns | NA | 0.18 | - | - | 0.00 | -## Parameter Extraction - -### Using the Tavis API +## Tavis API +### Parameter Extraction ```csharp UriTemplate template = new("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"); @@ -43,52 +42,7 @@ Uri uri = new ("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=12 IDictionary? result = template.GetParameters(uri); ``` -### Using the low-allocation API directly - -The lowest-level access makes use of a callback, which is fed the parameters as they are found. - -If the `reset` flag is set, you should disregard any parameters that have previously been sent, and start again. (This is typically the case where a partial match fails, and is restarted.) - -In order to manage the cache/reset process for you, we provide a `ParameterCache` type. You can rent an instance, and use it to accumulate the results for you. You can then enumerate the result set, and return the resource that have been rented for you. - -```csharp -var state = ParameterCache.Rent(5); -IUriTemplateParser corvusTemplate = UriTemplateParserFactory.CreateParser("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"); - -corvusTemplate!.ParseUri("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", ParameterCache.HandleParameters, ref state); - -state.EnumerateParameters(HandleFinalParameterSet); - -state.Return(); - -void HandleFinalParameterSet(ReadOnlySpan name, ReadOnlySpan value) -{ - if (name.SequenceEqual("parentRequestId")) - { - Assert.True(value.SequenceEqual("123232323"), $"parentRequestId was {value}"); - count++; - } - else if (name.SequenceEqual("hash")) - { - Assert.True(value.SequenceEqual("23ADE34FAE"), $"hash was {value}"); - count++; - } - else if (name.SequenceEqual("callback")) - { - Assert.True(value.SequenceEqual("http%3A%2F%2Fexample.com%2Fcallback"), $"callback was {value}"); - count++; - } - else - { - Assert.True(false, $"Unexpected parameter: (name: '{name}', value: '{value}')"); - } -} -``` - - -## URI Resolution - -### Using the Tavis API +### URI Resolution Replacing a path segment parameter, @@ -186,47 +140,100 @@ public void TestExtremeEncoding() } ``` -### Using the low-allocation API directly +Our `Corvus.UriTemplates.TavisApi` implementation is built over an underlying low-allocation API. + +## Low allocation API + +### Extracting parameter values from a URI by matching it to a URI template + +To create an instance of a parser for a URI template, call one of the `CreateParser()` overloads, passing it your URI template. -The low-allocation library provides a generic class `UriTemplateResolver` for URI template resolution. +```csharp +IUriTemplateParser UriTemplateParserFactory.CreateParser(string uriTemplate); +``` -The `TParameterProvider` is a type which implements `ITemplateParameterProvider`, to process a parameter payload according to a variable specification. +or -This allows you to process parameters as efficiently as possible, based on the types you need to support. +```csharp +IUriTemplateParser UriTemplateParserFactory.CreateParser(ReadOnlySpan uriTemplate); +``` -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. +You would typically have some initialization code that is called once to build your parsers from your templates (either derived statically or from some configuration) ```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); +private const string UriTemplate = "http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"; -static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) +private static readonly IUriTemplateParser CorvusTemplate = CreateParser(); + +private static IUriTemplateParser CreateParser() { - // 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.) + return + UriTemplateParserFactory.CreateParser(UriTemplate) } ``` -There are also overloads of `TryResolveResult` which will write to an `IBufferWriter` instead of providing the `ReadOnlySpan` to a callback. +You can then make use of that parser to extract parameter values from a URI. + +The parser uses a callback model to deliver the parameters to you (to avoid allocations). If you are used to low allocation code, you will probably recognize the pattern. + +You call `EnumerateParmaeters()`, passing the URI you wish to parse (as a `ReadOnlySpan`), a callback, and the initial value of a state object, which will be passed to that callback. + +The callback itself is called by the parser each time a matched parameter is discovered. -Similarly, a resolver that takes parameters from a `Dictionary` can be found in the package `Corvus.UriTemplates.Resolvers.DictionaryOfObject`. +It is given `ReadOnlySpan` instances for the name and value pairs, along with the current version of the state object. This state is passed by `ref`, so you can update its value to keep track of whatever processing you are doing with the parameters you have been passed. + +Here's an example that just counts the parameters it has seen. ```csharp -using JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }"); +int state = 0; + +CorvusTemplate.EnumerateParameters(Uri, HandleParameters, ref state); + +static void HandleParameters(ReadOnlySpan name, ReadOnlySpan value, ref int state) +{ + state++; +} +``` + +> There is a defaulted optional parameter to this method that lets you specific an initial capacity for the cache; if you know how many parameters you are going to match, you can tune this to minimize the amount of re-allocation required. + +### Resolving a template by substituting parameter values and producing a URI + +The other basic scenario is injecting parameter values into a URI template to produce a URI (or another URI template if we haven't replaced all the parameters in the template). + +The underlying type that does the work is called `UriTemplateResolver`. + +The `TParameterProvider` is an `ITemplateParameterProvider` - an interface implemented by types which convert from a source of parameter values (the `TParameterPayload`), on behalf of the `UriTemplateResolver`. + +We offer two of these providers "out of the box" - the `JsonTemplateParameterProvider` (which adapts to a `JsonElement`) and the `DictionaryTemplateParameterProvider` (which adapts to an `IDictionary` and is used by the underlying Tavis-compatible API). + +To save you having to work directly with the `UriTemplateResolver` plugging in all the necessary generic parameters, most `ITemplateParameterProvider` implements will offer a convenience type, and these are no exception. + +`JsonUriTemplateResolver` and `DictionaryUriTemplateResolver` give you strongly typed `TryResolveResult` and `TryGetParameterNames` methods which you can use in your code. + +Here's an example. + +```csharp +const string uriTemplate = "http://example.org/location{?value*}"; + +using var jsonValues = JsonDocument.Parse("{\"value\": { \"foo\": \"bar\", \"bar\": 3.4, \"baz\": null }}"); +Dictionary value = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } }; +Dictionary parameters = new() { { "value", value } }; + object? nullState = default; -JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState); + +JsonUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState); +DictionaryUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, parameters, 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.) + Console.WriteLine(resolvedTemplate.ToString()); } ``` -You should examine the implementations of those types if you wish to implement your own low-allocation parameter providers. +Notice how we can use the exact same callback that receives the resolved template, for both resolvers - the callback is not dependent on the particular parameter provider. + +> The Dictionary provider is somewhat faster than the JSON provider, largely because it has less work to do to extract parameter names and values. However, the JSON parameter provider offers direct support for all JSON value kinds (including encoding serialized "deeply nested" JSON values). ## Build and test diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj b/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj index 5f26a56..56f6935 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj +++ b/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj @@ -12,6 +12,7 @@ + diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs index b6e803f..69b98ff 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs +++ b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs @@ -63,7 +63,30 @@ public void ExtractParametersTavis() [Benchmark] public void ExtractParametersCorvusTavis() { - this.corvusTavisTemplate!.GetParameters(TavisUri); + IDictionary? result = this.corvusTavisTemplate!.GetParameters(TavisUri); + } + + /// + /// Extract parameters from a URI template using the Corvus implementation of the Tavis API. + /// + [Benchmark] + public void ExtractParametersCorvusTavisWithParameterCache() + { + int state = 0; + + if (this.corvusTemplate!.EnumerateParameters(Uri, HandleParameters, ref state)) + { + // We can use the state + } + else + { + // We can't use the state + } + + static void HandleParameters(ReadOnlySpan name, ReadOnlySpan value, ref int state) + { + state++; + } } /// @@ -72,12 +95,19 @@ public void ExtractParametersCorvusTavis() [Benchmark] public void ExtractParametersCorvus() { - object? state = default; - this.corvusTemplate!.ParseUri(Uri, HandleParameters, ref state); + int state = 0; + this.corvusTemplate!.ParseUri(Uri, HandleParameterMatching, ref state); - static void HandleParameters(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref object? state) + static void HandleParameterMatching(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref int state) { - // NOP + if (reset) + { + state = 0; + } + else + { + state++; + } } } } \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs index 56ed73a..fae0bdd 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs +++ b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs @@ -14,9 +14,10 @@ namespace Corvus.UriTemplates.Benchmarking; 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 Dictionary Value = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } }; + private static readonly Dictionary Parameters = new() { { "value", Value } }; - private readonly JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }"); + private readonly JsonDocument jsonValues = JsonDocument.Parse("{\"value\": { \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }}"); private Tavis.UriTemplates.UriTemplate? tavisTemplate; private TavisApi.UriTemplate? corvusTavisTemplate; @@ -49,7 +50,7 @@ public Task GlobalCleanup() [Benchmark(Baseline = true)] public void ResolveUriTavis() { - this.tavisTemplate!.SetParameter("value", Values); + this.tavisTemplate!.SetParameter("value", Value); this.tavisTemplate!.Resolve(); } @@ -59,7 +60,7 @@ public void ResolveUriTavis() [Benchmark] public void ResolveUriCorvusTavis() { - this.corvusTavisTemplate!.SetParameter("value", Values); + this.corvusTavisTemplate!.SetParameter("value", Value); this.corvusTavisTemplate!.Resolve(); } @@ -67,7 +68,7 @@ public void ResolveUriCorvusTavis() /// Resolve a URI from a template and parameter values using Corvus.UriTemplateResolver. /// [Benchmark] - public void ResolveUriCorvus() + public void ResolveUriCorvusJson() { object? nullState = default; JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, this.jsonValues.RootElement, HandleResult, ref nullState); @@ -76,4 +77,18 @@ static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) // NOP } } + + /// + /// Resolve a URI from a template and parameter values using Corvus.UriTemplateResolver. + /// + [Benchmark] + public void ResolveUriCorvusDictionary() + { + object? nullState = default; + DictionaryUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, Parameters, HandleResult, ref nullState); + static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) + { + // NOP + } + } } \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json b/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json index 2b2a00e..30db83a 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json +++ b/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json @@ -42,6 +42,21 @@ "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, )", @@ -345,6 +360,11 @@ "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", @@ -1107,6 +1127,16 @@ "System.Collections.Immutable": "[7.0.0-rc.1.22426.10, )" } }, + "corvus.uritemplates.resolvers.dictionaryofobject": { + "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, )" + } + }, "corvus.uritemplates.resolvers.json": { "type": "Project", "dependencies": { diff --git a/Solutions/Corvus.UriTemplate.TavisApi.Tests/UsageTests.cs b/Solutions/Corvus.UriTemplate.TavisApi.Tests/UsageTests.cs index fcc0484..437eb6c 100644 --- a/Solutions/Corvus.UriTemplate.TavisApi.Tests/UsageTests.cs +++ b/Solutions/Corvus.UriTemplate.TavisApi.Tests/UsageTests.cs @@ -11,20 +11,15 @@ public class UsageTests [Fact] public void ShouldRetrieveParameters() { - var state = ParameterCache.Rent(5); IUriTemplateParser corvusTemplate = UriTemplateParserFactory.CreateParser("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"); - corvusTemplate!.ParseUri("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", ParameterCache.HandleParameters, ref state); + int state = 0; - int count = 0; + corvusTemplate!.EnumerateParameters("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", Callback, ref state); - state.EnumerateParameters(Callback); + Assert.Equal(3, state); - Assert.Equal(3, count); - - state.Return(); - - void Callback(ReadOnlySpan name, ReadOnlySpan value) + void Callback(ReadOnlySpan name, ReadOnlySpan value, ref int count) { if (name.SequenceEqual("parentRequestId")) { @@ -51,20 +46,14 @@ void Callback(ReadOnlySpan name, ReadOnlySpan value) [Fact] public void ShouldRetrieveParametersWhenReallocationIsRequired() { - var state = ParameterCache.Rent(1); IUriTemplateParser corvusTemplate = UriTemplateParserFactory.CreateParser("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"); - corvusTemplate!.ParseUri("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", ParameterCache.HandleParameters, ref state); - - int count = 0; - - state.EnumerateParameters(Callback); - - Assert.Equal(3, count); + int state = 0; + corvusTemplate!.EnumerateParameters("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", Callback, ref state, 1); - state.Return(); + Assert.Equal(3, state); - void Callback(ReadOnlySpan name, ReadOnlySpan value) + void Callback(ReadOnlySpan name, ReadOnlySpan value, ref int count) { if (name.SequenceEqual("parentRequestId")) { diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs index d7e5e3a..990adea 100644 --- a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs @@ -136,8 +136,7 @@ private static bool ProcessRawText(ReadOnlySpan rawInput, { if (sourceArray != null) { - sourceUnescaped.Clear(); - ArrayPool.Shared.Return(sourceArray); + ArrayPool.Shared.Return(sourceArray, true); } } } @@ -169,8 +168,7 @@ private static bool ProcessRawText(ReadOnlySpan rawInput, { if (pooledName != null) { - utf8Unescaped.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -203,8 +201,7 @@ private static bool ProcessDecodedText(ReadOnlySpan decod if (sourceTranscodedArray != null) { - sourceTranscoded.Clear(); - ArrayPool.Shared.Return(sourceTranscodedArray); + ArrayPool.Shared.Return(sourceTranscodedArray, true); } return success; diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs index c22434a..689aeca 100644 --- a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs @@ -206,8 +206,7 @@ private static bool ProcessRawText(ReadOnlySpan rawInput, { if (sourceArray != null) { - sourceUnescaped.Clear(); - ArrayPool.Shared.Return(sourceArray); + ArrayPool.Shared.Return(sourceArray, true); } } } @@ -239,8 +238,7 @@ private static bool ProcessRawText(ReadOnlySpan rawInput, { if (pooledName != null) { - utf8Unescaped.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -273,8 +271,7 @@ private static bool ProcessDecodedText(ReadOnlySpan decod if (sourceTranscodedArray != null) { - sourceTranscoded.Clear(); - ArrayPool.Shared.Return(sourceTranscodedArray); + ArrayPool.Shared.Return(sourceTranscodedArray, true); } return success; @@ -343,14 +340,12 @@ private static bool ProcessEncodedNameAndEncodedValue(ReadOnlyS { if (pooledValue is not null) { - utf8UnescapedValue.Clear(); - ArrayPool.Shared.Return(pooledValue); + ArrayPool.Shared.Return(pooledValue, true); } if (pooledName is not null) { - utf8UnescapedName.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -377,8 +372,7 @@ private static bool ProcessDecodedNameAndEncodedValue(ReadOnlyS { if (pooledValue != null) { - utf8UnescapedValue.Clear(); - ArrayPool.Shared.Return(pooledValue); + ArrayPool.Shared.Return(pooledValue, true); } } } @@ -405,8 +399,7 @@ private static bool ProcessEncodedNameAndDecodedValue(ReadOnlyS { if (pooledName != null) { - utf8UnescapedName.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -467,14 +460,12 @@ private static bool ProcessEncodedNameAndEncodedValue(ReadOnlyS { if (pooledValue is not null) { - utf8UnescapedValue.Clear(); - ArrayPool.Shared.Return(pooledValue); + ArrayPool.Shared.Return(pooledValue, true); } if (pooledName is not null) { - utf8UnescapedName.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -501,8 +492,7 @@ private static bool ProcessDecodedNameAndEncodedValue(ReadOnlyS { if (pooledValue != null) { - utf8UnescapedValue.Clear(); - ArrayPool.Shared.Return(pooledValue); + ArrayPool.Shared.Return(pooledValue, true); } } } @@ -529,8 +519,7 @@ private static bool ProcessEncodedNameAndDecodedValue(ReadOnlyS { if (pooledName != null) { - utf8UnescapedName.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -566,14 +555,12 @@ private static bool ProcessDecodedNameAndValue(ReadOnlySpan.Shared.Return(transcodedNameArray); + ArrayPool.Shared.Return(transcodedNameArray, true); } if (transcodedValueArray != null) { - transcodedValue.Clear(); - ArrayPool.Shared.Return(transcodedValueArray); + ArrayPool.Shared.Return(transcodedValueArray, true); } return success; diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs index 331c6b4..48d8994 100644 --- a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs @@ -34,8 +34,7 @@ public static bool CanDecodeBase64(ReadOnlySpan utf8Unescaped) { if (pooledArray != null) { - byteSpan.Clear(); - ArrayPool.Shared.Return(pooledArray); + ArrayPool.Shared.Return(pooledArray, true); } return false; @@ -44,8 +43,7 @@ public static bool CanDecodeBase64(ReadOnlySpan utf8Unescaped) if (pooledArray != null) { - byteSpan.Clear(); - ArrayPool.Shared.Return(pooledArray); + ArrayPool.Shared.Return(pooledArray, true); } return true; @@ -69,8 +67,7 @@ public static ReadOnlySpan GetUnescapedSpan(ReadOnlySpan utf8Source, if (pooledName != null) { - new Span(pooledName, 0, written).Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } return propertyName; diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs index 8a9b470..755777a 100644 --- a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs @@ -54,7 +54,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.GetArrayLength() == 0); + AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, false); } AppendArray(output, variableSpecification.OperatorInfo, variableSpecification.Explode, variableSpecification.VarName, value); @@ -68,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, !HasProperties(value)); + AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, false); } AppendObject(output, variableSpecification.OperatorInfo, variableSpecification.Explode, value); @@ -88,7 +88,7 @@ public static VariableProcessingState ProcessVariable(ref VariableSpecification { if (variableSpecification.OperatorInfo.Named) { - AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, IsNullOrUndefined(value)); + AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, false); } AppendValue(output, value, variableSpecification.PrefixLength, variableSpecification.OperatorInfo.AllowReserved); diff --git a/Solutions/Corvus.UriTemplates.sln b/Solutions/Corvus.UriTemplates.sln index 33f97f6..7eca243 100644 --- a/Solutions/Corvus.UriTemplates.sln +++ b/Solutions/Corvus.UriTemplates.sln @@ -9,9 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.UriTemplates", "Corv 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.UriTemplates.Resolvers.DictionaryOfObject", "Corvus.UriTemplates.Resolvers.DictionaryOfObject\Corvus.UriTemplates.Resolvers.DictionaryOfObject.csproj", "{C77AE102-65C4-4C64-8B53-B2476EC80280}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox", "Sandbox\Sandbox.csproj", "{302F8634-2171-43DB-B5DD-5B01BE6026C2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,6 +41,10 @@ Global {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 + {302F8634-2171-43DB-B5DD-5B01BE6026C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {302F8634-2171-43DB-B5DD-5B01BE6026C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {302F8634-2171-43DB-B5DD-5B01BE6026C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {302F8634-2171-43DB-B5DD-5B01BE6026C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs index 304df15..9d24937 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs @@ -10,7 +10,7 @@ namespace Corvus.UriTemplates; /// /// A cache for parameters extracted from a URI template. /// -public struct ParameterCache +internal struct ParameterCache { private readonly int bufferIncrement; private CacheEntry[] items; @@ -25,11 +25,34 @@ private ParameterCache(int initialCapacity) } /// - /// A callback for enumerating parameters from the cache. + /// Enumerate the parameters in the parser. /// - /// The name of the parameter. - /// The value of the parameter. - public delegate void ParameterCacheCallback(ReadOnlySpan name, ReadOnlySpan value); + /// The type of the state for the callback. + /// The parser to use. + /// The uri to parse. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// The callback to recieve the enumerated parameters. + /// The state for the callback. + /// if the parser was successful, otherwise . + public static bool EnumerateParameters(IUriTemplateParser parser, ReadOnlySpan uri, int initialCapacity, EnumerateParametersCallback callback, ref TState state) + { + ParameterCache cache = Rent(initialCapacity); + if (parser.ParseUri(uri, HandleParameters, ref cache)) + { + for (int i = 0; i < cache.written; ++i) + { + CacheEntry item = cache.items[i]; + callback(item.Name, item.Value, ref state); + } + + cache.Return(); + return true; + } + + cache.Return(); + return false; + } /// /// Rent an instance of a parameter cache. @@ -37,7 +60,7 @@ private ParameterCache(int initialCapacity) /// The initial capacity of the cache. /// An instance of a parameter cache. /// When you have finished with the cache, call to relinquish any internal resources. - public static ParameterCache Rent(int initialCapacity) + private static ParameterCache Rent(int initialCapacity) { return new(initialCapacity); } @@ -49,7 +72,8 @@ public static ParameterCache Rent(int initialCapacity) /// The name of the parameter. /// The value of the parameter. /// The parameter cache. - public static void HandleParameters(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref ParameterCache state) + /// Pass this to , as the callback. + private static void HandleParameters(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref ParameterCache state) { if (!reset) { @@ -61,56 +85,39 @@ public static void HandleParameters(bool reset, ReadOnlySpan name, ReadOnl } } - /// - /// Reset the items written. - /// - public void Reset() - { - this.ResetItems(); - this.written = 0; - } - - /// - /// Enumerate the parameters in the cache. - /// - /// The callback to recieve the enumerated parameters. - public void EnumerateParameters(ParameterCacheCallback callback) - { - if (this.items is not null) - { - for (int i = 0; i < this.written; ++i) - { - CacheEntry item = this.items[i]; - callback(item.Name.AsSpan(0, item.NameLength), item.Value.AsSpan(0, item.ValueLength)); - } - } - } - /// /// Add a parameter to the cache. /// /// The name of the parameter to add. /// The value of the parameter to add. - public void Add(ReadOnlySpan name, ReadOnlySpan value) + private void Add(ReadOnlySpan name, ReadOnlySpan value) { - char[] nameArray = name.Length > 0 ? ArrayPool.Shared.Rent(name.Length) : Array.Empty(); - name.CopyTo(nameArray); - char[] valueArray = value.Length > 0 ? ArrayPool.Shared.Rent(value.Length) : Array.Empty(); - value.CopyTo(valueArray); + char[] entryArray = name.Length + value.Length > 0 ? ArrayPool.Shared.Rent(name.Length + value.Length) : Array.Empty(); + name.CopyTo(entryArray); + value.CopyTo(entryArray.AsSpan(name.Length)); if (this.written == this.items.Length) { ArrayPool.Shared.Resize(ref this.items, this.items.Length + this.bufferIncrement); } - this.items[this.written] = (nameArray, name.Length, valueArray, value.Length); + this.items[this.written] = new(entryArray, name.Length, value.Length); this.written++; } + /// + /// Reset the items written. + /// + private void Reset() + { + this.ResetItems(); + this.written = 0; + } + /// /// Return the resources used by the cache. /// - public void Return() + private void Return() { if (!this.returned) { @@ -124,47 +131,33 @@ private void ResetItems() { for (int i = 0; i < this.written; ++i) { - CacheEntry item = this.items[i]; - if (item.Name.Length > 0) - { - item.Name.AsSpan().Clear(); - ArrayPool.Shared.Return(item.Name); - } - - if (item.Value.Length > 0) - { - item.Value.AsSpan().Clear(); - ArrayPool.Shared.Return(item.Value); - } + this.items[i].Return(); } } private readonly struct CacheEntry { - public CacheEntry(char[] name, int nameLength, char[] value, int valueLength) + private readonly char[] entry; + private readonly int nameLength; + private readonly int valueLength; + + public CacheEntry(in char[] entry, int nameLength, int valueLength) { - this.Name = name; - this.NameLength = nameLength; - this.Value = value; - this.ValueLength = valueLength; + this.entry = entry; + this.nameLength = nameLength; + this.valueLength = valueLength; } - public char[] Name { get; } - - public int NameLength { get; } + public Span Name => this.nameLength > 0 ? this.entry.AsSpan(0, this.nameLength) : Span.Empty; - public char[] Value { get; } + public Span Value => this.valueLength > 0 ? this.entry.AsSpan(this.nameLength, this.valueLength) : Span.Empty; - public int ValueLength { get; } - - public static implicit operator (char[] Name, int NameLength, char[] Value, int ValueLength)(CacheEntry value) - { - return (value.Name, value.NameLength, value.Value, value.ValueLength); - } - - public static implicit operator CacheEntry((char[] Name, int NameLength, char[] Value, int ValueLength) value) + public void Return() { - return new CacheEntry(value.Name, value.NameLength, value.Value, value.ValueLength); + if (this.entry.Length > 0) + { + ArrayPool.Shared.Return(this.entry); + } } } } \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/UriExtensions.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/UriExtensions.cs index b977a1f..c30b281 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/UriExtensions.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/UriExtensions.cs @@ -168,8 +168,7 @@ static void ExecuteCallback(QueryStringParameterCallback callback, ReadO { if (pooledArray is not null) { - lowerName.Clear(); - ArrayPool.Shared.Return(pooledArray); + ArrayPool.Shared.Return(pooledArray, true); } } } diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserExtensions.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserExtensions.cs new file mode 100644 index 0000000..b66e35a --- /dev/null +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserExtensions.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates; + +/// +/// A callback for enumerating parameters from the cache. +/// +/// The type of the state for the callback. +/// The name of the parameter. +/// The value of the parameter. +/// The state for the callback. +public delegate void EnumerateParametersCallback(ReadOnlySpan name, ReadOnlySpan value, ref TState state); + +/// +/// Extension methods for . +/// +public static class UriTemplateParserExtensions +{ + /// + /// Enumerate the parameters in the parser. + /// + /// The type of the state for the callback. + /// The parser to use. + /// The uri to parse. + /// The callback to recieve the enumerated parameters. + /// The state for the callback. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// if the parser was successful, otherwise . + public static bool EnumerateParameters(this IUriTemplateParser parser, ReadOnlySpan uri, EnumerateParametersCallback callback, ref TState state, int initialCapacity = 10) + { + return ParameterCache.EnumerateParameters(parser, uri, initialCapacity, callback, ref state); + } +} \ No newline at end of file diff --git a/Solutions/Sandbox/Program.cs b/Solutions/Sandbox/Program.cs new file mode 100644 index 0000000..0461860 --- /dev/null +++ b/Solutions/Sandbox/Program.cs @@ -0,0 +1,18 @@ +using System.Text.Json; +using Corvus.UriTemplates; + +const string uriTemplate = "http://example.org/location{?value*}"; + +using var jsonValues = JsonDocument.Parse("{\"value\": { \"foo\": \"bar\", \"bar\": 3.4, \"baz\": null }}"); +Dictionary value = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } }; +Dictionary parameters = new() { { "value", value } }; + +object? nullState = default; + +JsonUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState); +DictionaryUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, parameters, HandleResult, ref nullState); + +static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) +{ + Console.WriteLine(resolvedTemplate.ToString()); +} diff --git a/Solutions/Sandbox/Sandbox.csproj b/Solutions/Sandbox/Sandbox.csproj new file mode 100644 index 0000000..d7d47fb --- /dev/null +++ b/Solutions/Sandbox/Sandbox.csproj @@ -0,0 +1,16 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + +