diff --git a/src/NSwag.CodeGeneration.CSharp.Tests/QueryParameterTests.cs b/src/NSwag.CodeGeneration.CSharp.Tests/QueryParameterTests.cs index 844971b300..1e5332949e 100644 --- a/src/NSwag.CodeGeneration.CSharp.Tests/QueryParameterTests.cs +++ b/src/NSwag.CodeGeneration.CSharp.Tests/QueryParameterTests.cs @@ -1,4 +1,7 @@ -using Xunit; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using Xunit; namespace NSwag.CodeGeneration.CSharp.Tests { @@ -78,5 +81,164 @@ public void When_query_parameter_is_set_to_explode_and_style_is_form_object_para "urlBuilder_.Append(System.Uri.EscapeDataString(\"limit\") + \"=\").Append(System.Uri.EscapeDataString(ConvertToString(paging.Limit, System.Globalization.CultureInfo.InvariantCulture))).Append(\"&\");", code); } + + [Theory] + [InlineData("form", ",")] + [InlineData("spaceDelimited", "%20")] + [InlineData("pipeDelimited", "|")] + public void When_query_parameter_is_set_to_not_explode_style_should_be_respected_and_array_parameters_delimited(string style, string delimiter) + { + var spec = $@"{{ + ""openapi"": ""3.0.0"", + ""info"": {{ + ""version"": ""1.0.0"", + ""title"": ""Query params tests"" + }}, + ""servers"": [ + {{ + ""url"": ""http://localhost:8080"" + }} + ], + ""paths"": {{ + ""/settings"": {{ + ""get"": {{ + ""summary"": ""List all settings"", + ""operationId"": ""listSettings"", + ""parameters"": [ + {{ + ""name"": ""paging"", + ""in"": ""query"", + ""description"": ""list setting filter"", + ""required"": false, + ""style"": ""{style}"", + ""explode"": false, + ""type"": ""array"", + ""items"": {{ + ""type"": ""integer"", + ""format"": ""int32"" + }} + }} + ], + ""responses"": {{ + ""200"": {{ + ""description"": ""A paged array of settings"" + }} + }} + }} + }} + }} +}} +"; + + + + var document = OpenApiDocument.FromJsonAsync(spec).Result; + + //// Act + var generator = new CSharpClientGenerator(document, new CSharpClientGeneratorSettings()); + var code = generator.GenerateFile(); + + //// Assert + + var expected = + $@"{{ + bool first_ = true; + foreach (var item_ in paging) + {{ + if (first_) + {{ + urlBuilder_.Append(System.Uri.EscapeDataString(""paging"")).Append('='); + first_ = false; + }} + else + urlBuilder_.Append({(delimiter.Length == 1 ? $"'{delimiter}'" : $"\"{delimiter}\"")}); + + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture))); + }} + if (!first_) + urlBuilder_.Append('&'); +}}"; + + AssertCodeContains(expected, code); + } + + [Theory] + [InlineData("form")] + [InlineData("spaceDelimited")] + [InlineData("pipeDelimited")] + public void When_query_parameter_is_set_to_explode_style_is_ignored_and_array_parameters_are_exploded(string style) + { + var spec = $@"{{ + ""openapi"": ""3.0.0"", + ""info"": {{ + ""version"": ""1.0.0"", + ""title"": ""Query params tests"" + }}, + ""servers"": [ + {{ + ""url"": ""http://localhost:8080"" + }} + ], + ""paths"": {{ + ""/settings"": {{ + ""get"": {{ + ""summary"": ""List all settings"", + ""operationId"": ""listSettings"", + ""parameters"": [ + {{ + ""name"": ""paging"", + ""in"": ""query"", + ""description"": ""list setting filter"", + ""required"": false, + ""style"": ""{style}"", + ""explode"": true, + ""type"": ""array"", + ""items"": {{ + ""type"": ""integer"", + ""format"": ""int32"" + }} + }} + ], + ""responses"": {{ + ""200"": {{ + ""description"": ""A paged array of settings"" + }} + }} + }} + }} + }} +}} +"; + + + + var document = OpenApiDocument.FromJsonAsync(spec).Result; + + //// Act + var generator = new CSharpClientGenerator(document, new CSharpClientGeneratorSettings()); + var code = generator.GenerateFile(); + + //// Assert + + var expected = "foreach (var item_ in paging) { urlBuilder_.Append(System.Uri.EscapeDataString(\"paging\") + \"=\").Append((item_ == null) ? \"\" : System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture))).Append(\"&\"); }"; + + AssertCodeContains(expected, code); + } + + private static void AssertCodeContains(string expected, string actual) + { + var reader = new StringReader(expected); + + const string regexLinePrefix = @"^\s+"; + const string regexLineSuffix = @"\r?\n"; + + StringBuilder regexPattern = new StringBuilder(); + while (reader.ReadLine() is string line) + regexPattern.AppendLine(regexLinePrefix + Regex.Escape(line) + regexLineSuffix); + + Regex regex = new Regex(regexPattern.ToString(), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace); + + Assert.True(regex.IsMatch(actual), $"Unable to find:\r\n{expected}\r\n in:\r\n{actual}"); + } } } \ No newline at end of file diff --git a/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.QueryParameter.liquid b/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.QueryParameter.liquid index b58f0a265f..c295763927 100644 --- a/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.QueryParameter.liquid +++ b/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.QueryParameter.liquid @@ -7,7 +7,63 @@ urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}") + "=").Ap {% elseif parameter.IsDate -%} urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}") + "=").Append(System.Uri.EscapeDataString({% if parameter.IsNullable and parameter.IsRequired %}{{ parameter.VariableName }} != null ? {% endif %}{{ parameter.VariableName }}{% if parameter.IsSystemNullable %}.Value{% endif %}.ToString("{{ ParameterDateFormat }}", System.Globalization.CultureInfo.InvariantCulture){% if parameter.IsNullable and parameter.IsRequired %} : "{{ QueryNullValue }}"{% endif %})).Append("&"); {% elseif parameter.IsArray -%} +{% if parameter.Explode -%} foreach (var item_ in {{ parameter.VariableName }}) { urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}") + "=").Append(System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); } +{% elseif parameter.IsForm -%} +{ + bool first_ = true; + foreach (var item_ in {{ parameter.VariableName }}) + { + if (first_) + { + urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}")).Append('='); + first_ = false; + } + else + urlBuilder_.Append(','); + + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture))); + } + if (!first_) + urlBuilder_.Append('&'); +} +{% elseif parameter.IsSpaceDelimited -%} +{ + bool first_ = true; + foreach (var item_ in {{ parameter.VariableName }}) + { + if (first_) + { + urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}")).Append('='); + first_ = false; + } + else + urlBuilder_.Append("%20"); + + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture))); + } + if (!first_) + urlBuilder_.Append('&'); +} +{% elseif parameter.IsPipeDelimited -%} +{ + bool first_ = true; + foreach (var item_ in {{ parameter.VariableName }}) + { + if (first_) + { + urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}")).Append('='); + first_ = false; + } + else + urlBuilder_.Append('|'); + + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture))); + } + if (!first_) + urlBuilder_.Append('&'); +} +{% endif -%} {% elseif parameter.IsDeepObject -%} {% for property in parameter.PropertyNames -%} if ({{parameter.Name}}.{{property.Name}} != null) diff --git a/src/NSwag.CodeGeneration/Models/ParameterModelBase.cs b/src/NSwag.CodeGeneration/Models/ParameterModelBase.cs index 26a2635caa..126c9d6485 100644 --- a/src/NSwag.CodeGeneration/Models/ParameterModelBase.cs +++ b/src/NSwag.CodeGeneration/Models/ParameterModelBase.cs @@ -90,16 +90,26 @@ public string Default public OpenApiParameterKind Kind => _parameter.Kind; /// Gets the parameter style. - public OpenApiParameterStyle Style => _parameter.Style; + public OpenApiParameterStyle Style => _parameter.ActualStyle; /// Gets the the value indicating if the parameter values should be exploded when included in the query string. - public bool Explode => _parameter.Explode; + public bool Explode => _parameter.ActualExplode; /// Gets a value indicating whether the parameter is a deep object (OpenAPI 3). - public bool IsDeepObject => _parameter.Style == OpenApiParameterStyle.DeepObject; + public bool IsDeepObject => _parameter.ActualStyle == OpenApiParameterStyle.DeepObject; /// Gets a value indicating whether the parameter has form style. - public bool IsForm => _parameter.Style == OpenApiParameterStyle.Form; + public bool IsForm => _parameter.ActualStyle == OpenApiParameterStyle.Form; + + /// + /// Gets a value indicating whether this array parameter has space-delimited style (OpenAPI 3). + /// + public bool IsSpaceDelimited => this._parameter.ActualStyle == OpenApiParameterStyle.SpaceDelimeted; + + /// + /// Gets a value indicating whether this array parameter has pipe-delimited style (OpenAPI 3). + /// + public bool IsPipeDelimited => this._parameter.ActualStyle == OpenApiParameterStyle.PipeDelimited; /// Gets the contained value property names (OpenAPI 3). public IEnumerable PropertyNames diff --git a/src/NSwag.Core.Tests/Serialization/QueryParameterSerializationTests.cs b/src/NSwag.Core.Tests/Serialization/QueryParameterSerializationTests.cs new file mode 100644 index 0000000000..699c65e14e --- /dev/null +++ b/src/NSwag.Core.Tests/Serialization/QueryParameterSerializationTests.cs @@ -0,0 +1,85 @@ +using System.Threading.Tasks; +using Newtonsoft.Json; +using NJsonSchema; +using Xunit; + +namespace NSwag.Core.Tests.Serialization +{ + public class QueryParameterSerializationTests + { + [Fact] + public async Task When_style_is_form_then_explode_should_serialize_as_true_by_default() + { + //// Arrange + var parameter = new OpenApiParameter(); + parameter.Name = "Foo"; + parameter.Kind = OpenApiParameterKind.Query; + parameter.Style = OpenApiParameterStyle.Form; + parameter.Schema = new JsonSchema + { + Type = JsonObjectType.Array, + Item = new JsonSchema + { + Type = JsonObjectType.String + } + }; + + //// Act + var json = parameter.ToJson(Formatting.Indented); + + //// Assert + Assert.Equal( + @"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""name"": ""Foo"", + ""in"": ""query"", + ""style"": ""form"", + ""explode"": true, + ""schema"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } +}".Replace("\r", ""), json.Replace("\r", "")); + } + + [Fact] + public async Task When_style_is_form_then_explode_should_serialize_as_false_when_set() + { + //// Arrange + var parameter = new OpenApiParameter(); + parameter.Name = "Foo"; + parameter.Kind = OpenApiParameterKind.Query; + parameter.Style = OpenApiParameterStyle.Form; + parameter.Explode = false; + parameter.Schema = new JsonSchema + { + Type = JsonObjectType.Array, + Item = new JsonSchema + { + Type = JsonObjectType.String + } + }; + + //// Act + var json = parameter.ToJson(Formatting.Indented); + + //// Assert + Assert.Equal( + @"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""name"": ""Foo"", + ""in"": ""query"", + ""style"": ""form"", + ""explode"": false, + ""schema"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } +}".Replace("\r", ""), json.Replace("\r", "")); + } + } +} \ No newline at end of file diff --git a/src/NSwag.Core/OpenApiParameter.cs b/src/NSwag.Core/OpenApiParameter.cs index 4f1ec64bf1..bc40d08851 100644 --- a/src/NSwag.Core/OpenApiParameter.cs +++ b/src/NSwag.Core/OpenApiParameter.cs @@ -24,7 +24,7 @@ public class OpenApiParameter : JsonSchema private bool _isRequired = false; private JsonSchema _schema; private IDictionary _examples; - private bool _explode; + private bool? _explode; private int? _position; private static readonly Regex AppJsonRegex = new Regex(@"application\/(\S+?)?\+?json;?(\S+)?"); @@ -56,6 +56,37 @@ public OpenApiParameterKind Kind } } + /// Gets the actual style of the parameter (OpenAPI only). + [JsonIgnore] + public OpenApiParameterStyle ActualStyle + { + get + { + OpenApiParameterStyle ret; + if (_style == OpenApiParameterStyle.Undefined) + { + switch (Kind) + { + case OpenApiParameterKind.Query: + case OpenApiParameterKind.Cookie: + ret = OpenApiParameterStyle.Form; + break; + case OpenApiParameterKind.Path: + case OpenApiParameterKind.Header: + ret = OpenApiParameterStyle.Simple; + break; + default: + ret = OpenApiParameterStyle.Undefined; + break; + } + } + else + ret = _style; + + return ret; + } + } + /// Gets or sets the style of the parameter (OpenAPI only). [JsonProperty(PropertyName = "style", DefaultValueHandling = DefaultValueHandling.Ignore)] public OpenApiParameterStyle Style @@ -68,11 +99,15 @@ public OpenApiParameterStyle Style } } + /// Gets the actual explode setting for the parameter (OpenAPI only). + [JsonIgnore] + public bool ActualExplode => _explode ?? ActualStyle == OpenApiParameterStyle.Form; + /// Gets or sets the explode setting for the parameter (OpenAPI only). - [JsonProperty(PropertyName = "explode", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonProperty(PropertyName = "explode", DefaultValueHandling = DefaultValueHandling.Include)] public bool Explode { - get => _explode; + get => _explode ?? ActualExplode; set { _explode = value;