From 9721c367fefb72394b8be218c7479098826b53fb Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Thu, 21 Nov 2024 09:35:44 -0600 Subject: [PATCH] ability to use array types as query string values in Wolverine.HTTP. Closes GH-1137 --- .../using_querystring_parameters.cs | 78 +++++++++++++++++++ .../CodeGen/ParsedArrayQueryStringValue.cs | 56 +++++++++++++ src/Http/Wolverine.Http/HttpChain.cs | 16 ++++ .../WolverineWebApi/QuerystringEndpoints.cs | 44 +++++++++++ src/Http/WolverineWebApi/TestEndpoints.cs | 26 +------ 5 files changed, 197 insertions(+), 23 deletions(-) create mode 100644 src/Http/Wolverine.Http/CodeGen/ParsedArrayQueryStringValue.cs create mode 100644 src/Http/WolverineWebApi/QuerystringEndpoints.cs diff --git a/src/Http/Wolverine.Http.Tests/using_querystring_parameters.cs b/src/Http/Wolverine.Http.Tests/using_querystring_parameters.cs index ed14ecba9..79310f273 100644 --- a/src/Http/Wolverine.Http.Tests/using_querystring_parameters.cs +++ b/src/Http/Wolverine.Http.Tests/using_querystring_parameters.cs @@ -1,4 +1,8 @@ +using JasperFx.CodeGeneration.Frames; using Shouldly; +using Wolverine.Http.CodeGen; +using Wolverine.Runtime; +using WolverineWebApi; namespace Wolverine.Http.Tests; @@ -198,6 +202,68 @@ public async Task use_parsed_enum_collection() body.ReadAsText().ShouldBe($"North,East,South"); } + [Fact] + public async Task using_string_array_completely_hit() + { + var body = await Scenario(x => + { + x.Get + .Url("/querystring/stringarray") + .QueryString("values", "foo") + .QueryString("values", "bar") + .QueryString("values", "baz"); + + x.Header("content-type").SingleValueShouldEqual("text/plain"); + }); + + body.ReadAsText().ShouldBe("foo,bar,baz"); + } + + [Fact] + public async Task using_string_array_completely_miss() + { + var body = await Scenario(x => + { + x.Get + .Url("/querystring/stringarray"); + + x.Header("content-type").SingleValueShouldEqual("text/plain"); + }); + + body.ReadAsText().ShouldBe("none"); + } + + [Fact] + public async Task using_int_array_completely_hit() + { + var body = await Scenario(x => + { + x.Get + .Url("/querystring/intarray") + .QueryString("values", "4") + .QueryString("values", "2") + .QueryString("values", "1"); + + x.Header("content-type").SingleValueShouldEqual("text/plain"); + }); + + body.ReadAsText().ShouldBe("1,2,4"); + } + + [Fact] + public async Task using_int_array_completely_miss() + { + var body = await Scenario(x => + { + x.Get + .Url("/querystring/intarray"); + + x.Header("content-type").SingleValueShouldEqual("text/plain"); + }); + + body.ReadAsText().ShouldBe("none"); + } + #region sample_query_string_usage [Fact] @@ -238,4 +304,16 @@ public async Task use_decimal_querystring_hit() } #endregion + + [Fact] + public void trouble_shoot_querystring_matching() + { + var method = new MethodCall(typeof(QuerystringEndpoints), "IntArray"); + var chain = new HttpChain(method, new HttpGraph(new WolverineOptions(), ServiceContainer.Empty())); + + var parameter = method.Method.GetParameters().Single(); + + var variable = chain.TryFindOrCreateQuerystringValue(parameter); + variable.Creator.ShouldBeOfType(); + } } \ No newline at end of file diff --git a/src/Http/Wolverine.Http/CodeGen/ParsedArrayQueryStringValue.cs b/src/Http/Wolverine.Http/CodeGen/ParsedArrayQueryStringValue.cs new file mode 100644 index 000000000..0fb5a1905 --- /dev/null +++ b/src/Http/Wolverine.Http/CodeGen/ParsedArrayQueryStringValue.cs @@ -0,0 +1,56 @@ +using System.Reflection; +using JasperFx.CodeGeneration; +using JasperFx.CodeGeneration.Frames; +using JasperFx.Core.Reflection; + +namespace Wolverine.Http.CodeGen; + +internal class ParsedArrayQueryStringValue : SyncFrame +{ + public ParsedArrayQueryStringValue(ParameterInfo parameter) + { + Variable = new QuerystringVariable(parameter.ParameterType, parameter.Name!, this); + } + + public QuerystringVariable Variable { get; } + + public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) + { + var elementType = Variable.VariableType.GetElementType(); + if (elementType == typeof(string)) + { + writer.Write($"var {Variable.Usage} = httpContext.Request.Query[\"{Variable.Usage}\"].ToArray();"); + } + else + { + var collectionAlias = typeof(List<>).MakeGenericType(elementType).FullNameInCode(); + var elementAlias = elementType.FullNameInCode(); + + writer.Write($"var {Variable.Usage}_List = new {collectionAlias}();"); + + writer.Write($"BLOCK:foreach (var {Variable.Usage}Value in httpContext.Request.Query[\"{Variable.Usage}\"])"); + + if (elementType.IsEnum) + { + writer.Write($"BLOCK:if ({elementAlias}.TryParse<{elementAlias}>({Variable.Usage}Value, out var {Variable.Usage}ValueParsed))"); + } + else if (elementType.IsBoolean()) + { + writer.Write($"BLOCK:if ({elementAlias}.TryParse({Variable.Usage}Value, out var {Variable.Usage}ValueParsed))"); + } + else + { + writer.Write($"BLOCK:if ({elementAlias}.TryParse({Variable.Usage}Value, System.Globalization.CultureInfo.InvariantCulture, out var {Variable.Usage}ValueParsed))"); + } + + writer.Write($"{Variable.Usage}_List.Add({Variable.Usage}ValueParsed);"); + writer.FinishBlock(); // parsing block + + writer.FinishBlock(); // foreach blobck + + writer.Write($"var {Variable.Usage} = {Variable.Usage}_List.ToArray();"); + } + + Next?.GenerateCode(method, writer); + } +} \ No newline at end of file diff --git a/src/Http/Wolverine.Http/HttpChain.cs b/src/Http/Wolverine.Http/HttpChain.cs index 172e3453b..d1f58880b 100644 --- a/src/Http/Wolverine.Http/HttpChain.cs +++ b/src/Http/Wolverine.Http/HttpChain.cs @@ -317,6 +317,14 @@ private void applyMetadata() if (parameter.ParameterType == typeof(string)) { variable = new ReadStringQueryStringValue(key).Variable; + variable.Name = key; + _querystringVariables.Add(variable); + } + + if (parameter.ParameterType == typeof(string[])) + { + variable = new ParsedArrayQueryStringValue(parameter).Variable; + variable.Name = key; _querystringVariables.Add(variable); } @@ -330,10 +338,18 @@ private void applyMetadata() _querystringVariables.Add(variable); } } + + if (parameter.ParameterType.IsArray && RouteParameterStrategy.CanParse(parameter.ParameterType.GetElementType())) + { + variable = new ParsedArrayQueryStringValue(parameter).Variable; + variable.Name = key; + _querystringVariables.Add(variable); + } if (ParsedCollectionQueryStringValue.CanParse(parameter.ParameterType)) { variable = new ParsedCollectionQueryStringValue(parameter).Variable; + variable.Name = key; _querystringVariables.Add(variable); } diff --git a/src/Http/WolverineWebApi/QuerystringEndpoints.cs b/src/Http/WolverineWebApi/QuerystringEndpoints.cs new file mode 100644 index 000000000..de276639a --- /dev/null +++ b/src/Http/WolverineWebApi/QuerystringEndpoints.cs @@ -0,0 +1,44 @@ +using JasperFx.Core; +using Marten; +using Microsoft.AspNetCore.Mvc; +using Wolverine.Http; + +namespace WolverineWebApi; + +public static class QuerystringEndpoints +{ + + [WolverineGet("/querystring/enum")] + public static string UsingEnumQuerystring(Direction direction) + { + return direction.ToString(); + } + + [WolverineGet("/querystring/explicit")] + public static string UsingEnumQuerystring([FromQuery(Name = "name")]string value) + { + return value ?? ""; + } + + [WolverineGet("/querystring/enum/nullable")] + public static string UsingNullableEnumQuerystring(Direction? direction) + { + return direction?.ToString() ?? "none"; + } + + [WolverineGet("/querystring/stringarray")] + public static string StringArray(string[]? values) + { + if (values == null || values.IsEmpty()) return "none"; + + return values.Join(","); + } + + [WolverineGet("/querystring/intarray")] + public static string IntArray(int[]? values) + { + if (values == null || values.IsEmpty()) return "none"; + + return values.OrderBy(x => x).Select(x => x.ToString()).Join(","); + } +} \ No newline at end of file diff --git a/src/Http/WolverineWebApi/TestEndpoints.cs b/src/Http/WolverineWebApi/TestEndpoints.cs index f27be4e48..1ee01092b 100644 --- a/src/Http/WolverineWebApi/TestEndpoints.cs +++ b/src/Http/WolverineWebApi/TestEndpoints.cs @@ -1,7 +1,7 @@ using System.Globalization; using Marten; -using Microsoft.AspNetCore.Mvc; using Wolverine.Http; +using JasperFx.Core; namespace WolverineWebApi; @@ -136,28 +136,8 @@ public static string UsingEnumCollection(IEnumerable collection) { return string.Join(",", collection); } -} - -public static class QuerystringEndpoints -{ - - [WolverineGet("/querystring/enum")] - public static string UsingEnumQuerystring(Direction direction) - { - return direction.ToString(); - } - - [WolverineGet("/querystring/explicit")] - public static string UsingEnumQuerystring([FromQuery(Name = "name")]string value) - { - return value ?? ""; - } - - [WolverineGet("/querystring/enum/nullable")] - public static string UsingNullableEnumQuerystring(Direction? direction) - { - return direction?.ToString() ?? "none"; - } + + } public class ArithmeticResults