diff --git a/CHANGELOG.md b/CHANGELOG.md index ef1ae24c9f..4039250434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed serialization of scalar members in union types for Python. [#2828](https://github.com/microsoft/kiota/issues/2828) - Fixed a bug where scalar error mappings would be generated even though it's not supported by the http request adapter. [#4018](https://github.com/microsoft/kiota/issues/4018) - Switched to proxy generation for TypeScript, leading to about ~44% bundle sizes reduction. [#3642](https://github.com/microsoft/kiota/issues/3642) +- Required query parameters are now projected as `{baseurl+}foo/bar?required={required}` instead of `{baseurl+}foo/bar{?required}` so they are automatically populated if no value is provided. [#3989](https://github.com/microsoft/kiota/issues/3989) - Fixed a bug where TypeScript models factory methods would be missing return types. - Fixed a bug where generated paths would possibly get too long. [#3854](https://github.com/microsoft/kiota/issues/3854) - The vscode extension now also displays the children nodes when filtering. [#3998](https://github.com/microsoft/kiota/issues/3998) diff --git a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs index 9e45a8ba83..11a4e9d30e 100644 --- a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs @@ -194,16 +194,23 @@ public static string GetUrlTemplate(this OpenApiUrlTreeNode currentNode) pathItem.Operations .SelectMany(static x => x.Value.Parameters) .Where(static x => x.In == ParameterLocation.Query)) - .DistinctBy(static x => x.Name) + .DistinctBy(static x => x.Name, StringComparer.Ordinal) + .OrderBy(static x => x.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); if (parameters.Length != 0) - queryStringParameters = "{?" + - parameters.Select(static x => + { + var requiredParameters = string.Join("&", parameters.Where(static x => x.Required) + .Select(static x => + $"{x.Name}={{{x.Name.SanitizeParameterNameForUrlTemplate()}}}")); + var optionalParameters = string.Join(",", parameters.Where(static x => !x.Required) + .Select(static x => x.Name.SanitizeParameterNameForUrlTemplate() + (x.Explode ? - "*" : string.Empty)) - .Aggregate(static (x, y) => $"{x},{y}") + - '}'; + "*" : string.Empty))); + var hasRequiredParameters = !string.IsNullOrEmpty(requiredParameters); + var hasOptionalParameters = !string.IsNullOrEmpty(optionalParameters); + queryStringParameters = $"{(hasRequiredParameters ? "?" : string.Empty)}{requiredParameters}{(hasOptionalParameters ? "{" : string.Empty)}{(hasOptionalParameters && hasRequiredParameters ? "&" : string.Empty)}{(hasOptionalParameters && !hasRequiredParameters ? "?" : string.Empty)}{optionalParameters}{(hasOptionalParameters ? "}" : string.Empty)}"; + } } var pathReservedPathParametersIds = currentNode.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pItem) ? pItem.Parameters diff --git a/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs index 8d662fb894..2ad8c46606 100644 --- a/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs +++ b/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs @@ -131,13 +131,13 @@ public void GetUrlTemplateSelectsDistinctQueryParameters() { var doc = new OpenApiDocument { - Paths = new(), + Paths = [], }; doc.Paths.Add("{param-with-dashes}\\existing-segment", new() { Operations = new Dictionary { { OperationType.Get, new() { - Parameters = new List { + Parameters = [ new() { Name = "param-with-dashes", In = ParameterLocation.Path, @@ -155,12 +155,12 @@ public void GetUrlTemplateSelectsDistinctQueryParameters() }, Style = ParameterStyle.Simple, } - } + ] } }, { OperationType.Put, new() { - Parameters = new List { + Parameters = [ new() { Name = "param-with-dashes", In = ParameterLocation.Path, @@ -178,7 +178,7 @@ public void GetUrlTemplateSelectsDistinctQueryParameters() }, Style = ParameterStyle.Simple, } - } + ] } } } @@ -187,19 +187,280 @@ public void GetUrlTemplateSelectsDistinctQueryParameters() Assert.Equal("{+baseurl}/{param%2Dwith%2Ddashes}/existing-segment{?%24select}", node.Children.First().Value.GetUrlTemplate()); // the query parameters will be decoded by a middleware at runtime before the request is executed } + [Fact] + public void GeneratesRequiredQueryParametersAndOptionalMixInPathItem() + { + var doc = new OpenApiDocument + { + Paths = [], + }; + doc.Paths.Add("users\\{id}\\manager", new() + { + Parameters = { + new OpenApiParameter { + Name = "id", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "filter", + In = ParameterLocation.Query, + Required = false, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "apikey", + In = ParameterLocation.Query, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + } + } + }, + Operations = new Dictionary { + { OperationType.Get, new() { + } + }, + } + }); + var node = OpenApiUrlTreeNode.Create(doc, Label); + Assert.Equal("{+baseurl}/users/{id}/manager?apikey={apikey}{&filter*}", node.Children.First().Value.GetUrlTemplate()); + } + [Fact] + public void GeneratesRequiredQueryParametersAndOptionalMixInOperation() + { + var doc = new OpenApiDocument + { + Paths = [], + }; + doc.Paths.Add("users\\{id}\\manager", new() + { + Operations = new Dictionary { + { OperationType.Get, new() { + Parameters = { + new OpenApiParameter { + Name = "id", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "filter", + In = ParameterLocation.Query, + Required = false, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "apikey", + In = ParameterLocation.Query, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + } + } + }, + } + }, + } + }); + var node = OpenApiUrlTreeNode.Create(doc, Label); + Assert.Equal("{+baseurl}/users/{id}/manager?apikey={apikey}{&filter*}", node.Children.First().Value.GetUrlTemplate()); + } + [Fact] + public void GeneratesOnlyOptionalQueryParametersInPathItem() + { + var doc = new OpenApiDocument + { + Paths = [], + }; + doc.Paths.Add("users\\{id}\\manager", new() + { + Parameters = { + new OpenApiParameter { + Name = "id", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "filter", + In = ParameterLocation.Query, + Required = false, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "apikey", + In = ParameterLocation.Query, + Schema = new OpenApiSchema { + Type = "string" + } + } + }, + Operations = new Dictionary { + { OperationType.Get, new() { + } + }, + } + }); + var node = OpenApiUrlTreeNode.Create(doc, Label); + Assert.Equal("{+baseurl}/users/{id}/manager{?apikey*,filter*}", node.Children.First().Value.GetUrlTemplate()); + } + [Fact] + public void GeneratesOnlyOptionalQueryParametersInOperation() + { + var doc = new OpenApiDocument + { + Paths = [], + }; + doc.Paths.Add("users\\{id}\\manager", new() + { + Operations = new Dictionary { + { OperationType.Get, new() { + Parameters = { + new OpenApiParameter { + Name = "id", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "filter", + In = ParameterLocation.Query, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "apikey", + In = ParameterLocation.Query, + Schema = new OpenApiSchema { + Type = "string" + } + } + }, + } + }, + } + }); + var node = OpenApiUrlTreeNode.Create(doc, Label); + Assert.Equal("{+baseurl}/users/{id}/manager{?apikey*,filter*}", node.Children.First().Value.GetUrlTemplate()); + } + [Fact] + public void GeneratesOnlyRequiredQueryParametersInPathItem() + { + var doc = new OpenApiDocument + { + Paths = [], + }; + doc.Paths.Add("users\\{id}\\manager", new() + { + Parameters = { + new OpenApiParameter { + Name = "id", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "filter", + In = ParameterLocation.Query, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "apikey", + Required = true, + In = ParameterLocation.Query, + Schema = new OpenApiSchema { + Type = "string" + } + } + }, + Operations = new Dictionary { + { OperationType.Get, new() { + } + }, + } + }); + var node = OpenApiUrlTreeNode.Create(doc, Label); + Assert.Equal("{+baseurl}/users/{id}/manager?apikey={apikey}&filter={filter}", node.Children.First().Value.GetUrlTemplate()); + } + [Fact] + public void GeneratesOnlyRequiredQueryParametersInOperation() + { + var doc = new OpenApiDocument + { + Paths = [], + }; + doc.Paths.Add("users\\{id}\\manager", new() + { + Operations = new Dictionary { + { OperationType.Get, new() { + Parameters = { + new OpenApiParameter { + Name = "id", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "filter", + Required = true, + In = ParameterLocation.Query, + Schema = new OpenApiSchema { + Type = "string" + } + }, + new OpenApiParameter { + Name = "apikey", + Required = true, + In = ParameterLocation.Query, + Schema = new OpenApiSchema { + Type = "string" + } + } + }, + } + }, + } + }); + var node = OpenApiUrlTreeNode.Create(doc, Label); + Assert.Equal("{+baseurl}/users/{id}/manager?apikey={apikey}&filter={filter}", node.Children.First().Value.GetUrlTemplate()); + } [Fact] public void GetUrlTemplateCleansInvalidParameters() { var doc = new OpenApiDocument { - Paths = new(), + Paths = [], }; doc.Paths.Add("{param-with-dashes}\\existing-segment", new() { Operations = new Dictionary { { OperationType.Get, new() { - Parameters = new List { + Parameters = [ new() { Name = "param-with-dashes", In = ParameterLocation.Path, @@ -241,13 +502,13 @@ public void GetUrlTemplateCleansInvalidParameters() }, Style = ParameterStyle.Simple, } - } + ] } } } }); var node = OpenApiUrlTreeNode.Create(doc, Label); - Assert.Equal("{+baseurl}/{param%2Dwith%2Ddashes}/existing-segment{?%24select,api%2Dversion,api%7Etopic,api%2Eencoding}", node.Children.First().Value.GetUrlTemplate()); + Assert.Equal("{+baseurl}/{param%2Dwith%2Ddashes}/existing-segment{?%24select,api%2Dversion,api%2Eencoding,api%7Etopic}", node.Children.First().Value.GetUrlTemplate()); // the query parameters will be decoded by a middleware at runtime before the request is executed } [InlineData("\\reviews\\search.json", "reviews.searchJson")]