From 4a86b7c606d315a6d0817fe38a6da81437f67eba Mon Sep 17 00:00:00 2001 From: Jonathan Goldman Date: Sat, 6 Jul 2019 11:37:12 -0400 Subject: [PATCH] Make parameter formatting more generic (#94) --- .gitignore | 4 +- CHANGELOG.md | 4 + .../Bodies/FileUploadBodySpecs.cs | 4 +- .../DefaultQuerySerializerSpecs.cs | 120 ++++++++++++++++++ .../HttpClientSettingsSpecs.cs | 7 +- .../Middleware/RequestMiddlewareSpecs.cs | 2 +- .../MultipleValuesArrayFormatterSpecs.cs | 21 --- .../ParameterSpecs.cs | 88 +------------ .../RequestRunnerSpecs.cs | 2 +- .../Requests/GetRequestSpecs.cs | 4 +- .../Requests/PostRequestSpecs.cs | 8 +- src/SpeakEasy.Specifications/ResourceSpecs.cs | 10 +- .../CommaSeparatedArrayFormatter.cs | 16 --- .../MultipleValuesArrayFormatter.cs | 17 --- src/SpeakEasy/Bodies/FileUploadBody.cs | 2 +- src/SpeakEasy/Bodies/NullRequestBody.cs | 2 +- src/SpeakEasy/Bodies/ObjectRequestBody.cs | 2 +- src/SpeakEasy/Bodies/PostRequestBody.cs | 4 +- src/SpeakEasy/DefaultQuerySerializer.cs | 61 +++++++++ src/SpeakEasy/HttpClient.cs | 2 +- src/SpeakEasy/HttpClientSettings.cs | 13 +- src/SpeakEasy/IArrayFormatter.cs | 23 ---- src/SpeakEasy/IHttpRequest.cs | 2 +- src/SpeakEasy/IQuerySerializer.cs | 13 ++ src/SpeakEasy/IRequestBody.cs | 4 +- src/SpeakEasy/Middleware/RequestMiddleware.cs | 10 +- src/SpeakEasy/Parameter.cs | 32 ----- src/SpeakEasy/RequestRunner.cs | 4 +- src/SpeakEasy/Requests/HttpRequest.cs | 4 +- src/SpeakEasy/Resource.cs | 23 ++-- 30 files changed, 253 insertions(+), 255 deletions(-) create mode 100644 src/SpeakEasy.Specifications/DefaultQuerySerializerSpecs.cs delete mode 100644 src/SpeakEasy.Specifications/MultipleValuesArrayFormatterSpecs.cs delete mode 100644 src/SpeakEasy/ArrayFormatters/CommaSeparatedArrayFormatter.cs delete mode 100644 src/SpeakEasy/ArrayFormatters/MultipleValuesArrayFormatter.cs create mode 100644 src/SpeakEasy/DefaultQuerySerializer.cs delete mode 100644 src/SpeakEasy/IArrayFormatter.cs create mode 100644 src/SpeakEasy/IQuerySerializer.cs diff --git a/.gitignore b/.gitignore index be4672e..96237bf 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,6 @@ packages/ #resticle targets -..\src\CommonAssemblyInfo.cs \ No newline at end of file +..\src\CommonAssemblyInfo.cs + +src/.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4da093e..6766c65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 1.3.0 + + * Ability to change the way in which the query string is formatted + ## 1.2.3 * Enabling post requests using form-data for a file diff --git a/src/SpeakEasy.Specifications/Bodies/FileUploadBodySpecs.cs b/src/SpeakEasy.Specifications/Bodies/FileUploadBodySpecs.cs index dd6cdfd..13ef22e 100644 --- a/src/SpeakEasy.Specifications/Bodies/FileUploadBodySpecs.cs +++ b/src/SpeakEasy.Specifications/Bodies/FileUploadBodySpecs.cs @@ -34,7 +34,7 @@ class when_serializing static IContent serializable; Because of = () => - serializable = body.Serialize(transmissionSettings, An()); + serializable = body.Serialize(transmissionSettings, An()); It should_have_content_type_for_multipart_form_data = () => serializable.ShouldBeOfExactType(); @@ -69,7 +69,7 @@ class when_serializing_a_multipart_form body = new FileUploadBody(resource, new[] { An() }); transmissionSettings = An(); - serializable = body.Serialize(transmissionSettings, An()); + serializable = body.Serialize(transmissionSettings, An()); }; It should_have_content_type_for_multipart_form_data = () => diff --git a/src/SpeakEasy.Specifications/DefaultQuerySerializerSpecs.cs b/src/SpeakEasy.Specifications/DefaultQuerySerializerSpecs.cs new file mode 100644 index 0000000..5cccc2c --- /dev/null +++ b/src/SpeakEasy.Specifications/DefaultQuerySerializerSpecs.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using Machine.Specifications; +using SpeakEasy.Serializers; + +namespace SpeakEasy.Specifications +{ + [Subject(typeof(DefaultQuerySerializer))] + class DefaultQuerySerializerSpecs + { + static DefaultQuerySerializer serializer; + + static IEnumerable formatted; + + Establish context = () => + serializer = new DefaultQuerySerializer(); + + class in_general + { + It should_expand_array_values = () => + serializer.ExpandArrayValues.ShouldBeTrue(); + } + + class when_converting_to_query_string + { + Because of = () => + formatted = serializer.Serialize(new [] + { + new Parameter("name", "value") + }); + + It should_format_as_query_string = () => + formatted.ShouldContain("name=value"); + } + + class when_expanding_array_values + { + class when_converting_to_query_string_with_int_array_value_and_multiple_values_array_formatter + { + Establish context = () => + serializer.ExpandArrayValues = true; + + Because of = () => + formatted = serializer.Serialize(new [] + { + new Parameter("name", new[] { 3, 4, 5 }) + }); + + It should_format_as_query_string = () => + formatted.ShouldContain("name=3", "name=4", "name=5"); + } + } + + class when_not_expanding_array_values + { + Establish context = () => + serializer.ExpandArrayValues = false; + + class when_converting_to_query_string_with_string_array_value + { + Because of = () => + formatted = serializer.Serialize(new [] + { + new Parameter("name", new[] { "value1", "value2" }) + }); + + It should_format_as_query_string = () => + formatted.ShouldContain("name=value1,value2"); + } + + class when_converting_to_query_string_with_int_array_value + { + Because of = () => + formatted = serializer.Serialize(new [] + { + new Parameter("name", new[] { 3, 4, 5 }) + }); + + It should_format_as_query_string = () => + formatted.ShouldContain("name=3,4,5"); + } + } + + class when_converting_to_query_string_with_date_time + { + Because of = () => + formatted = serializer.Serialize(new[] + { + new Parameter("name", new DateTime(2013, 10, 15, 14, 30, 44)) + }); + + It should_format_as_query_string = () => + formatted.ShouldContain("name=2013-10-15T14:30:44.0000000"); + } + + class when_converting_to_query_string_with_nullable_date_time + { + Because of = () => + formatted = serializer.Serialize(new [] + { + new Parameter("name", (DateTime?)new DateTime(2013, 10, 15, 14, 30, 44, DateTimeKind.Utc)) + }); + + It should_format_as_query_string = () => + formatted.ShouldContain("name=2013-10-15T14:30:44.0000000Z"); + } + + class when_converting_values_containing_slashes_and_ampersands + { + Because of = () => + formatted = serializer.Serialize(new [] + { + new Parameter("name", "value/this&that") + }); + + It should_properly_escape_special_characters = () => + formatted.ShouldContain("name=value%2Fthis%26that"); + } + } +} diff --git a/src/SpeakEasy.Specifications/HttpClientSettingsSpecs.cs b/src/SpeakEasy.Specifications/HttpClientSettingsSpecs.cs index e4608ea..0f97d23 100644 --- a/src/SpeakEasy.Specifications/HttpClientSettingsSpecs.cs +++ b/src/SpeakEasy.Specifications/HttpClientSettingsSpecs.cs @@ -2,7 +2,6 @@ using System.Threading; using System.Threading.Tasks; using Machine.Specifications; -using SpeakEasy.ArrayFormatters; using SpeakEasy.Authenticators; using SpeakEasy.Middleware; using SpeakEasy.Serializers; @@ -28,8 +27,8 @@ class in_general It should_have_default_user_agent = () => settings.Middleware.Has().ShouldBeTrue(); - It should_have_default_array_formatter = () => - settings.ArrayFormatter.ShouldBeOfExactType(); + It should_have_default_query_serializer = () => + settings.QuerySerializer.ShouldBeOfExactType(); It should_be_valid = () => settings.IsValid.ShouldBeTrue(); @@ -50,7 +49,7 @@ class default_settings_in_general class without_array_formatter { Because of = () => - settings.ArrayFormatter = null; + settings.QuerySerializer = null; It should_not_be_valid = () => settings.IsValid.ShouldBeFalse(); diff --git a/src/SpeakEasy.Specifications/Middleware/RequestMiddlewareSpecs.cs b/src/SpeakEasy.Specifications/Middleware/RequestMiddlewareSpecs.cs index c375a25..602e3d9 100644 --- a/src/SpeakEasy.Specifications/Middleware/RequestMiddlewareSpecs.cs +++ b/src/SpeakEasy.Specifications/Middleware/RequestMiddlewareSpecs.cs @@ -20,7 +20,7 @@ class RequestMiddlewareSpecs : WithFakes Establish context = () => { - middleware = new RequestMiddleware(new SystemHttpClient(), The(), The(), new CookieContainer()); + middleware = new RequestMiddleware(new SystemHttpClient(), The(), The(), new CookieContainer()); }; class when_building_web_request_with_get_request diff --git a/src/SpeakEasy.Specifications/MultipleValuesArrayFormatterSpecs.cs b/src/SpeakEasy.Specifications/MultipleValuesArrayFormatterSpecs.cs deleted file mode 100644 index 2dae51e..0000000 --- a/src/SpeakEasy.Specifications/MultipleValuesArrayFormatterSpecs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Machine.Fakes; -using Machine.Specifications; -using SpeakEasy.ArrayFormatters; - -namespace SpeakEasy.Specifications -{ - [Subject(typeof(MultipleValuesArrayFormatter))] - class MultipleValuesArrayFormatterSpecs : WithSubject - { - class when_formatting - { - static string formatted; - - Because of = () => - formatted = Subject.FormatParameter("field", new[] { "a", "b", "c" }, t => t.ToString()); - - It should_format_each_item = () => - formatted.ShouldEqual("field=a&field=b&field=c"); - } - } -} diff --git a/src/SpeakEasy.Specifications/ParameterSpecs.cs b/src/SpeakEasy.Specifications/ParameterSpecs.cs index 3e7e6ec..50297b6 100644 --- a/src/SpeakEasy.Specifications/ParameterSpecs.cs +++ b/src/SpeakEasy.Specifications/ParameterSpecs.cs @@ -1,6 +1,6 @@ using System; using Machine.Specifications; -using SpeakEasy.ArrayFormatters; +using SpeakEasy.Serializers; namespace SpeakEasy.Specifications { @@ -9,80 +9,6 @@ class ParameterSpecs { static Parameter parameter; - static string formatted; - - class when_converting_to_query_string - { - Establish context = () => - parameter = new Parameter("name", "value"); - - Because of = () => - formatted = parameter.ToQueryString(new CommaSeparatedArrayFormatter()); - - It should_format_as_query_string = () => - formatted.ShouldEqual("name=value"); - } - - class when_converting_to_query_string_with_string_array_value - { - Establish context = () => - parameter = new Parameter("name", new[] { "value1", "value2" }); - - Because of = () => - formatted = parameter.ToQueryString(new CommaSeparatedArrayFormatter()); - - It should_format_as_query_string = () => - formatted.ShouldEqual("name=value1,value2"); - } - - class when_converting_to_query_string_with_int_array_value - { - Establish context = () => - parameter = new Parameter("name", new[] { 3, 4, 5 }); - - Because of = () => - formatted = parameter.ToQueryString(new CommaSeparatedArrayFormatter()); - - It should_format_as_query_string = () => - formatted.ShouldEqual("name=3,4,5"); - } - - class when_converting_to_query_string_with_int_array_value_and_multiple_values_array_formatter - { - Establish context = () => - parameter = new Parameter("name", new[] { 3, 4, 5 }); - - Because of = () => - formatted = parameter.ToQueryString(new MultipleValuesArrayFormatter()); - - It should_format_as_query_string = () => - formatted.ShouldEqual("name=3&name=4&name=5"); - } - - class when_converting_to_query_string_with_date_time - { - Establish context = () => - parameter = new Parameter("name", new DateTime(2013, 10, 15, 14, 30, 44)); - - Because of = () => - formatted = parameter.ToQueryString(new CommaSeparatedArrayFormatter()); - - It should_format_as_query_string = () => - formatted.ShouldEqual("name=2013-10-15T14:30:44.0000000"); - } - - class when_converting_to_query_string_with_nullable_date_time - { - Establish context = () => - parameter = new Parameter("name", (DateTime?)new DateTime(2013, 10, 15, 14, 30, 44, DateTimeKind.Utc)); - - Because of = () => - formatted = parameter.ToQueryString(new CommaSeparatedArrayFormatter()); - - It should_format_as_query_string = () => - formatted.ShouldEqual("name=2013-10-15T14:30:44.0000000Z"); - } - class when_value_is_nullable { Establish context = () => @@ -91,17 +17,5 @@ class when_value_is_nullable It should_not_have_value = () => parameter.HasValue.ShouldBeFalse(); } - - class when_converting_values_containing_slashes_and_ampersands - { - Establish context = () => - parameter = new Parameter("name", "value/this&that"); - - Because of = () => - formatted = parameter.ToQueryString(new CommaSeparatedArrayFormatter()); - - It should_properly_escape_special_characters = () => - formatted.ShouldEqual("name=value%2Fthis%26that"); - } } } diff --git a/src/SpeakEasy.Specifications/RequestRunnerSpecs.cs b/src/SpeakEasy.Specifications/RequestRunnerSpecs.cs index a28e870..fd5edbd 100644 --- a/src/SpeakEasy.Specifications/RequestRunnerSpecs.cs +++ b/src/SpeakEasy.Specifications/RequestRunnerSpecs.cs @@ -12,7 +12,7 @@ class RequestRunnerSpecs : WithSubject Establish context = () => { request = An(); - request.WhenToldTo(r => r.BuildRequestUrl(Param.IsAny())).Return("http://example.com"); + request.WhenToldTo(r => r.BuildRequestUrl(Param.IsAny())).Return("http://example.com"); request.WhenToldTo(r => r.HttpMethod).Return(HttpMethod.Get); }; diff --git a/src/SpeakEasy.Specifications/Requests/GetRequestSpecs.cs b/src/SpeakEasy.Specifications/Requests/GetRequestSpecs.cs index f2eb1da..3c06e82 100644 --- a/src/SpeakEasy.Specifications/Requests/GetRequestSpecs.cs +++ b/src/SpeakEasy.Specifications/Requests/GetRequestSpecs.cs @@ -1,6 +1,6 @@ using Machine.Specifications; -using SpeakEasy.ArrayFormatters; using SpeakEasy.Requests; +using SpeakEasy.Serializers; namespace SpeakEasy.Specifications.Requests { @@ -30,7 +30,7 @@ class when_building_web_request_with_parameters }; Because of = () => - url = request.BuildRequestUrl(new CommaSeparatedArrayFormatter()); + url = request.BuildRequestUrl(new DefaultQuerySerializer()); It should_set_url = () => url.ShouldEqual("http://example.com/companies?filter=ftse&starred=True"); diff --git a/src/SpeakEasy.Specifications/Requests/PostRequestSpecs.cs b/src/SpeakEasy.Specifications/Requests/PostRequestSpecs.cs index 49520f0..3aeda82 100644 --- a/src/SpeakEasy.Specifications/Requests/PostRequestSpecs.cs +++ b/src/SpeakEasy.Specifications/Requests/PostRequestSpecs.cs @@ -5,9 +5,9 @@ using System.Text; using Machine.Fakes; using Machine.Specifications; -using SpeakEasy.ArrayFormatters; using SpeakEasy.Bodies; using SpeakEasy.Requests; +using SpeakEasy.Serializers; namespace SpeakEasy.Specifications.Requests { @@ -36,7 +36,7 @@ class when_building_request_url_with_object_body }; It should_generate_query_params = () => - request.BuildRequestUrl(new CommaSeparatedArrayFormatter()).ShouldEqual("http://example.com/companies?makemoney=allday"); + request.BuildRequestUrl(new DefaultQuerySerializer()).ShouldEqual("http://example.com/companies?makemoney=allday"); } class when_building_request_url_with_post_request_body @@ -50,7 +50,7 @@ class when_building_request_url_with_post_request_body }; It should_not_generate_query_params = () => - request.BuildRequestUrl(new CommaSeparatedArrayFormatter()).ShouldEqual("http://example.com/companies"); + request.BuildRequestUrl(new DefaultQuerySerializer()).ShouldEqual("http://example.com/companies"); } class when_building_request_url_with_post_request_body_of_a_file @@ -85,7 +85,7 @@ class when_building_request_url_with_post_request_body_of_a_file Because of = () => { var httpRequest = new HttpRequestMessage(HttpMethod.Post, ""); - serializable = body.Serialize(An(), An()); + serializable = body.Serialize(An(), An()); serializable.WriteTo(httpRequest).Wait(); content = (MultipartFormDataContent) httpRequest.Content; }; diff --git a/src/SpeakEasy.Specifications/ResourceSpecs.cs b/src/SpeakEasy.Specifications/ResourceSpecs.cs index 732c5ae..8f9594f 100644 --- a/src/SpeakEasy.Specifications/ResourceSpecs.cs +++ b/src/SpeakEasy.Specifications/ResourceSpecs.cs @@ -1,5 +1,5 @@ using Machine.Specifications; -using SpeakEasy.ArrayFormatters; +using SpeakEasy.Serializers; namespace SpeakEasy.Specifications { @@ -97,7 +97,7 @@ class when_creating_resource_with_parameters root = Resource.Create("company/:name"); It should_create_resource_with_parameter = () => - root.HasSegment("name"); + root.HasSegment("name").ShouldBeTrue(); It should_have_one_parameter = () => root.NumSegments.ShouldEqual(1); @@ -115,7 +115,7 @@ class when_getting_encoded_parameters }; Because of = () => - encoded = root.GetEncodedParameters(new CommaSeparatedArrayFormatter()); + encoded = root.GetEncodedParameters(new DefaultQuerySerializer()); It should_encode_parameters = () => encoded.ShouldEqual("name=jim&age=26"); @@ -133,7 +133,7 @@ class when_getting_encoded_parameters_with_null_parameters }; Because of = () => - encoded = root.GetEncodedParameters(new CommaSeparatedArrayFormatter()); + encoded = root.GetEncodedParameters(new DefaultQuerySerializer()); It should_encode_parameters = () => encoded.ShouldEqual("name=jim"); @@ -151,7 +151,7 @@ class when_getting_encoded_parameters_with_all_null_parameters }; Because of = () => - encoded = root.GetEncodedParameters(new CommaSeparatedArrayFormatter()); + encoded = root.GetEncodedParameters(new DefaultQuerySerializer()); It should_encode_parameters = () => encoded.ShouldBeEmpty(); diff --git a/src/SpeakEasy/ArrayFormatters/CommaSeparatedArrayFormatter.cs b/src/SpeakEasy/ArrayFormatters/CommaSeparatedArrayFormatter.cs deleted file mode 100644 index 66215ff..0000000 --- a/src/SpeakEasy/ArrayFormatters/CommaSeparatedArrayFormatter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Linq; - -namespace SpeakEasy.ArrayFormatters -{ - public class CommaSeparatedArrayFormatter : IArrayFormatter - { - public string FormatParameter(string name, Array values, Func valueFormatter) - { - var items = string.Join(",", values.Cast() - .Select(valueFormatter)); - - return string.Concat(name, "=", items); - } - } -} diff --git a/src/SpeakEasy/ArrayFormatters/MultipleValuesArrayFormatter.cs b/src/SpeakEasy/ArrayFormatters/MultipleValuesArrayFormatter.cs deleted file mode 100644 index ca3db89..0000000 --- a/src/SpeakEasy/ArrayFormatters/MultipleValuesArrayFormatter.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Linq; - -namespace SpeakEasy.ArrayFormatters -{ - public class MultipleValuesArrayFormatter : IArrayFormatter - { - public string FormatParameter(string name, Array values, Func valueFormatter) - { - var items = values - .Cast() - .Select(s => string.Concat(name, "=", valueFormatter(s))); - - return string.Join("&", items); - } - } -} diff --git a/src/SpeakEasy/Bodies/FileUploadBody.cs b/src/SpeakEasy/Bodies/FileUploadBody.cs index 4a6b5ff..0f4862f 100644 --- a/src/SpeakEasy/Bodies/FileUploadBody.cs +++ b/src/SpeakEasy/Bodies/FileUploadBody.cs @@ -16,7 +16,7 @@ public FileUploadBody(Resource resource, IFile[] files) public bool ConsumesResourceParameters { get; } = true; - public IContent Serialize(ITransmissionSettings transmissionSettings, IArrayFormatter arrayFormatter) + public IContent Serialize(ITransmissionSettings transmissionSettings, IQuerySerializer querySerializer) { return new MultipartMimeContent(resource, files); } diff --git a/src/SpeakEasy/Bodies/NullRequestBody.cs b/src/SpeakEasy/Bodies/NullRequestBody.cs index 25ee805..a1d10c1 100644 --- a/src/SpeakEasy/Bodies/NullRequestBody.cs +++ b/src/SpeakEasy/Bodies/NullRequestBody.cs @@ -6,7 +6,7 @@ internal class NullRequestBody : IRequestBody { public bool ConsumesResourceParameters { get; } = false; - public IContent Serialize(ITransmissionSettings transmissionSettings, IArrayFormatter arrayFormatter) + public IContent Serialize(ITransmissionSettings transmissionSettings, IQuerySerializer querySerializer) { return new NullContent(transmissionSettings); } diff --git a/src/SpeakEasy/Bodies/ObjectRequestBody.cs b/src/SpeakEasy/Bodies/ObjectRequestBody.cs index bd87994..a557e30 100644 --- a/src/SpeakEasy/Bodies/ObjectRequestBody.cs +++ b/src/SpeakEasy/Bodies/ObjectRequestBody.cs @@ -15,7 +15,7 @@ public ObjectRequestBody(object body) public bool ConsumesResourceParameters { get; } = false; - public IContent Serialize(ITransmissionSettings transmissionSettings, IArrayFormatter arrayFormatter) + public IContent Serialize(ITransmissionSettings transmissionSettings, IQuerySerializer querySerializer) { return new StreamableContent( transmissionSettings.DefaultSerializerContentType, diff --git a/src/SpeakEasy/Bodies/PostRequestBody.cs b/src/SpeakEasy/Bodies/PostRequestBody.cs index 6866188..afa587c 100644 --- a/src/SpeakEasy/Bodies/PostRequestBody.cs +++ b/src/SpeakEasy/Bodies/PostRequestBody.cs @@ -15,7 +15,7 @@ public PostRequestBody(Resource resource) public bool ConsumesResourceParameters { get; } = true; - public IContent Serialize(ITransmissionSettings transmissionSettings, IArrayFormatter arrayFormatter) + public IContent Serialize(ITransmissionSettings transmissionSettings, IQuerySerializer querySerializer) { if (!resource.HasParameters) { @@ -27,7 +27,7 @@ public IContent Serialize(ITransmissionSettings transmissionSettings, IArrayForm return new MultipartFileFormDataContent(resource); } - var parameters = resource.GetEncodedParameters(arrayFormatter); + var parameters = resource.GetEncodedParameters(querySerializer); var content = Encoding.UTF8.GetBytes(parameters); return new ByteArrayContent("application/x-www-form-urlencoded", content); diff --git a/src/SpeakEasy/DefaultQuerySerializer.cs b/src/SpeakEasy/DefaultQuerySerializer.cs new file mode 100644 index 0000000..e0e370c --- /dev/null +++ b/src/SpeakEasy/DefaultQuerySerializer.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace SpeakEasy +{ + public class DefaultQuerySerializer : IQuerySerializer + { + public bool ExpandArrayValues { get; set; } = true; + + public IEnumerable Serialize(IEnumerable parameters) + { + return parameters + .Where(p => p.HasValue) + .SelectMany(ToQueryString); + } + + private IEnumerable ToQueryString(Parameter parameter) + { + if (!parameter.HasValue) + { + throw new NotSupportedException($"Could not convert the parameter {parameter.Name} to a query string because it did not have a value"); + } + + if (parameter.Value is Array array) + { + if (ExpandArrayValues) + { + foreach (var item in array) + { + yield return string.Concat(parameter.Name, "=", ToQueryStringValue(item)); + } + } + else + { + var items = string.Join(",", array.Cast() + .Select(ToQueryStringValue)); + + yield return string.Concat(parameter.Name, "=", items); + } + } + + var value = ToQueryStringValue(parameter.Value); + + yield return string.Concat(parameter.Name, "=", value); + } + + private string ToQueryStringValue(object value) + { + if (value is DateTime time) + { + return time.ToString("o", CultureInfo.InvariantCulture); + } + + var raw = value.ToString(); + + return Uri.EscapeDataString(raw); + } + } +} diff --git a/src/SpeakEasy/HttpClient.cs b/src/SpeakEasy/HttpClient.cs index 39e5253..2e22344 100644 --- a/src/SpeakEasy/HttpClient.cs +++ b/src/SpeakEasy/HttpClient.cs @@ -49,7 +49,7 @@ internal HttpClient(string rootUrl, HttpClientSettings settings) requestRunner = new RequestRunner( client, new TransmissionSettings(settings.Serializers), - settings.ArrayFormatter, + settings.QuerySerializer, cookieContainer, settings.Middleware); diff --git a/src/SpeakEasy/HttpClientSettings.cs b/src/SpeakEasy/HttpClientSettings.cs index e352b41..762b2d0 100644 --- a/src/SpeakEasy/HttpClientSettings.cs +++ b/src/SpeakEasy/HttpClientSettings.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using SpeakEasy.ArrayFormatters; using SpeakEasy.Authenticators; using SpeakEasy.Middleware; using SpeakEasy.Serializers; @@ -21,7 +20,7 @@ public HttpClientSettings() Serializers = new List(); Authenticator = new NullAuthenticator(); NamingConvention = new DefaultNamingConvention(); - ArrayFormatter = new MultipleValuesArrayFormatter(); + QuerySerializer = new DefaultQuerySerializer(); Serializers.Add(new DefaultJsonSerializer()); Serializers.Add(new TextPlainSerializer()); @@ -36,7 +35,7 @@ public HttpClientSettings() public IAuthenticator Authenticator { get; set; } /// - /// The available serialiazers + /// The available serializers /// public List Serializers { get; set; } @@ -46,9 +45,9 @@ public HttpClientSettings() public MiddlewareCollection Middleware { get; } = new MiddlewareCollection(); /// - /// The array formatter that will be used to format query string array paramters + /// The query formatter that will be used to format query string array parameters /// - public IArrayFormatter ArrayFormatter { get; set; } + public IQuerySerializer QuerySerializer { get; set; } /// /// The default serializer @@ -68,10 +67,10 @@ public HttpClientSettings() /// /// Indicates whether or not the http client settings are valid /// - public bool IsValid => Serializers.Any() && ArrayFormatter != null; + public bool IsValid => Serializers.Any() && QuerySerializer != null; /// - /// The default timeout for the HttpClient to 30 minutes, + /// The default timeout for the HttpClient to 30 minutes, /// to use the system default (100 seconds) set this property to null. /// public TimeSpan? DefaultTimeout { get; set; } = TimeSpan.FromMinutes(30); diff --git a/src/SpeakEasy/IArrayFormatter.cs b/src/SpeakEasy/IArrayFormatter.cs deleted file mode 100644 index 15fe8d5..0000000 --- a/src/SpeakEasy/IArrayFormatter.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace SpeakEasy -{ - /// - /// An array parameter formatter is used to customize the way in which - /// array parameters are formatted, for example you may want an array ints such - /// as this one: - /// - /// new []{1, 2, 3 } - /// - /// to be formatted to the query string like so: - /// - /// ?items=1,2,3 - /// - public interface IArrayFormatter - { - /// - /// Formats a single parameter - /// - string FormatParameter(string name, Array values, Func valueFormatter); - } -} \ No newline at end of file diff --git a/src/SpeakEasy/IHttpRequest.cs b/src/SpeakEasy/IHttpRequest.cs index 323b194..52f4af7 100644 --- a/src/SpeakEasy/IHttpRequest.cs +++ b/src/SpeakEasy/IHttpRequest.cs @@ -30,7 +30,7 @@ public interface IHttpRequest /// Builds the method specific request url /// /// A url - string BuildRequestUrl(IArrayFormatter arrayFormatter); + string BuildRequestUrl(IQuerySerializer querySerializer); void AddHeader(string header, string value); diff --git a/src/SpeakEasy/IQuerySerializer.cs b/src/SpeakEasy/IQuerySerializer.cs new file mode 100644 index 0000000..1087192 --- /dev/null +++ b/src/SpeakEasy/IQuerySerializer.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace SpeakEasy +{ + /// + /// A query formatter is used to customize the way in which query parameters + /// are formatted to the url. + /// + public interface IQuerySerializer + { + IEnumerable Serialize(IEnumerable parameters); + } +} diff --git a/src/SpeakEasy/IRequestBody.cs b/src/SpeakEasy/IRequestBody.cs index 553848e..660bf53 100644 --- a/src/SpeakEasy/IRequestBody.cs +++ b/src/SpeakEasy/IRequestBody.cs @@ -18,8 +18,8 @@ public interface IRequestBody /// Serializes the request body /// /// The transmission settings - /// The array formatter for array parameters + /// The query serializer for array parameters /// A byte array with the contents of this body - IContent Serialize(ITransmissionSettings transmissionSettings, IArrayFormatter arrayFormatter); + IContent Serialize(ITransmissionSettings transmissionSettings, IQuerySerializer querySerializer); } } diff --git a/src/SpeakEasy/Middleware/RequestMiddleware.cs b/src/SpeakEasy/Middleware/RequestMiddleware.cs index c59eca6..594a166 100644 --- a/src/SpeakEasy/Middleware/RequestMiddleware.cs +++ b/src/SpeakEasy/Middleware/RequestMiddleware.cs @@ -17,7 +17,7 @@ internal class RequestMiddleware : IHttpMiddleware private readonly ITransmissionSettings transmissionSettings; - private readonly IArrayFormatter arrayFormatter; + private readonly IQuerySerializer querySerializer; private readonly CookieContainer cookieContainer; @@ -26,11 +26,11 @@ internal class RequestMiddleware : IHttpMiddleware public RequestMiddleware( SystemHttpClient client, ITransmissionSettings transmissionSettings, - IArrayFormatter arrayFormatter, + IQuerySerializer querySerializer, CookieContainer cookieContainer) { this.transmissionSettings = transmissionSettings; - this.arrayFormatter = arrayFormatter; + this.querySerializer = querySerializer; this.cookieContainer = cookieContainer; this.client = client; } @@ -43,7 +43,7 @@ public IHttpMiddleware Next public async Task Invoke(IHttpRequest request, CancellationToken cancellationToken) { - var serializedBody = request.Body.Serialize(transmissionSettings, arrayFormatter); + var serializedBody = request.Body.Serialize(transmissionSettings, querySerializer); using (var httpRequest = BuildHttpRequestMessage(request)) { @@ -65,7 +65,7 @@ public HttpRequestMessage BuildHttpRequestMessage(IHttpRequest httpRequest) { var message = new HttpRequestMessage( httpRequest.HttpMethod, - httpRequest.BuildRequestUrl(arrayFormatter)); + httpRequest.BuildRequestUrl(querySerializer)); foreach (var mediaType in transmissionSettings.DeserializableMediaTypes) { diff --git a/src/SpeakEasy/Parameter.cs b/src/SpeakEasy/Parameter.cs index ff159f5..5abcdf6 100644 --- a/src/SpeakEasy/Parameter.cs +++ b/src/SpeakEasy/Parameter.cs @@ -1,6 +1,3 @@ -using System; -using System.Globalization; - namespace SpeakEasy { public class Parameter @@ -16,34 +13,5 @@ public Parameter(string name, object value) public object Value { get; } public bool HasValue => Value != null; - - public string ToQueryString(IArrayFormatter arrayFormatter) - { - if (!HasValue) - { - throw new NotSupportedException($"Could not convert the parameter {Name} to a query string because it did not have a value"); - } - - if (Value is Array array) - { - return arrayFormatter.FormatParameter(Name, array, ToQueryStringValue); - } - - var value = ToQueryStringValue(Value); - - return string.Concat(Name, "=", value); - } - - private string ToQueryStringValue(object value) - { - if (value is DateTime time) - { - return time.ToString("o", CultureInfo.InvariantCulture); - } - - var raw = value.ToString(); - - return Uri.EscapeDataString(raw); - } } } diff --git a/src/SpeakEasy/RequestRunner.cs b/src/SpeakEasy/RequestRunner.cs index 8d54739..5807408 100644 --- a/src/SpeakEasy/RequestRunner.cs +++ b/src/SpeakEasy/RequestRunner.cs @@ -15,11 +15,11 @@ internal class RequestRunner : IRequestRunner public RequestRunner( SystemHttpClient client, ITransmissionSettings transmissionSettings, - IArrayFormatter arrayFormatter, + IQuerySerializer querySerializer, CookieContainer cookieContainer, MiddlewareCollection middleware) { - var defaultMiddleware = new RequestMiddleware(client, transmissionSettings, arrayFormatter, cookieContainer); + var defaultMiddleware = new RequestMiddleware(client, transmissionSettings, querySerializer, cookieContainer); middleware.Append(defaultMiddleware); middlewareHead = middleware.BuildMiddlewareChain(); } diff --git a/src/SpeakEasy/Requests/HttpRequest.cs b/src/SpeakEasy/Requests/HttpRequest.cs index 1a6d9dc..27806c3 100644 --- a/src/SpeakEasy/Requests/HttpRequest.cs +++ b/src/SpeakEasy/Requests/HttpRequest.cs @@ -21,14 +21,14 @@ protected HttpRequest(Resource resource, IRequestBody body) public abstract HttpMethod HttpMethod { get; } - public string BuildRequestUrl(IArrayFormatter arrayFormatter) + public string BuildRequestUrl(IQuerySerializer querySerializer) { if (!Resource.HasParameters || Body.ConsumesResourceParameters) { return Resource.Path; } - var queryString = Resource.GetEncodedParameters(arrayFormatter); + var queryString = Resource.GetEncodedParameters(querySerializer); return string.Concat(Resource.Path, "?", queryString); } diff --git a/src/SpeakEasy/Resource.cs b/src/SpeakEasy/Resource.cs index c84210d..294bf42 100644 --- a/src/SpeakEasy/Resource.cs +++ b/src/SpeakEasy/Resource.cs @@ -7,11 +7,11 @@ namespace SpeakEasy { public class Resource { - private static ConcurrentDictionary segmentsCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary SegmentsCache = new ConcurrentDictionary(); public static Resource Create(string path) { - var segments = segmentsCache.GetOrAdd(path, GetSegments); + var segments = SegmentsCache.GetOrAdd(path, GetSegments); return new Resource(path.TrimEnd('/'), segments); } @@ -47,7 +47,7 @@ private Resource(string path, string[] segmentNames) public bool HasParameters => parameters != null && parameters.Any(); - public int NumParameters => parameters == null ? 0 : parameters.Count; + public int NumParameters => parameters?.Count ?? 0; public bool HasSegment(string name) { @@ -79,23 +79,18 @@ public Resource Append(string resource) return Append(Create(resource)); } - public string GetEncodedParameters(IArrayFormatter arrayFormatter) + public string GetEncodedParameters(IQuerySerializer querySerializer) { - var formattedParameters = GetFormattedParameters(arrayFormatter); + var formattedParameters = GetFormattedParameters(querySerializer); return string.Join("&", formattedParameters); } - private IEnumerable GetFormattedParameters(IArrayFormatter arrayFormatter) + private IEnumerable GetFormattedParameters(IQuerySerializer querySerializer) { - if (parameters == null) - { - return Enumerable.Empty(); - } - - return parameters - .Where(p => p.HasValue) - .Select(p => p.ToQueryString(arrayFormatter)); + return parameters == null + ? Enumerable.Empty() + : querySerializer.Serialize(parameters); } public override string ToString()