From 0436897ee81020384a5bc925f67f8ccda4afe415 Mon Sep 17 00:00:00 2001 From: mspearey Date: Thu, 30 May 2024 15:22:24 +0100 Subject: [PATCH 1/3] fix missing models when no multipart encoding exists Model declarations not created when multipart/form-data exists with no encoding in mime content Added check for multipart/form-data with additional check if no other mime content --- .../Extensions/OpenApiOperationExtensions.cs | 9 + src/Kiota.Builder/KiotaBuilder.cs | 40 ++- .../Kiota.Builder.Tests/KiotaBuilderTests.cs | 281 +++++++++++++++++- 3 files changed, 315 insertions(+), 15 deletions(-) diff --git a/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs index 6d14c22441..aca62940e2 100644 --- a/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs @@ -47,6 +47,15 @@ internal static bool IsMultipartFormDataSchema(this IDictionary source, StructuredMimeTypesCollection structuredMimeTypes) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(structuredMimeTypes); + if (structuredMimeTypes.Count == 0) return false; + if (!source.ContainsKey(multipartMimeTypes.First())) return false; + if (source.Count == 1) return true; + return structuredMimeTypes.First() == multipartMimeTypes.First(); + } internal static IEnumerable GetValidSchemas(this IDictionary source, StructuredMimeTypesCollection structuredMimeTypes) { ArgumentNullException.ThrowIfNull(source); diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index ef05625247..2ef10354c5 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1473,26 +1473,37 @@ private void AddRequestBuilderMethodParameters(OpenApiUrlTreeNode currentNode, O if (operation.GetRequestSchema(config.StructuredMimeTypes) is OpenApiSchema requestBodySchema) { CodeTypeBase requestBodyType; - if (operation.RequestBody.Content.IsMultipartFormDataSchema(config.StructuredMimeTypes)) + if (operation.RequestBody.Content.IsMultipartFormDataSchema(config.StructuredMimeTypes) + && operation.RequestBody.Content.IsMultipartTopMimeType(config.StructuredMimeTypes)) { - requestBodyType = new CodeType - { - Name = "MultipartBody", - IsExternal = true, - }; var mediaType = operation.RequestBody.Content.First(x => x.Value.Schema == requestBodySchema).Value; - foreach (var encodingEntry in mediaType.Encoding - .Where(x => !string.IsNullOrEmpty(x.Value.ContentType) && - config.StructuredMimeTypes.Contains(x.Value.ContentType))) + if (mediaType.Encoding.Any()) + { + requestBodyType = new CodeType { Name = "MultipartBody", IsExternal = true, }; + foreach (var encodingEntry in mediaType.Encoding + .Where(x => !string.IsNullOrEmpty(x.Value.ContentType) && + config.StructuredMimeTypes.Contains(x.Value.ContentType))) + { + if (CreateModelDeclarations(currentNode, requestBodySchema.Properties[encodingEntry.Key], + operation, method, $"{operationType}RequestBody", + isRequestBody: true) is CodeType propertyType && + propertyType.TypeDefinition is not null) + multipartPropertiesModels.TryAdd(propertyType.TypeDefinition, true); + } + } + else { - if (CreateModelDeclarations(currentNode, requestBodySchema.Properties[encodingEntry.Key], operation, method, $"{operationType}RequestBody", isRequestBody: true) is CodeType propertyType && - propertyType.TypeDefinition is not null) - multipartPropertiesModels.TryAdd(propertyType.TypeDefinition, true); + requestBodyType = CreateModelDeclarations(currentNode, requestBodySchema, operation, method, + $"{operationType}RequestBody", isRequestBody: true) ?? + throw new InvalidSchemaException(); } } else - requestBodyType = CreateModelDeclarations(currentNode, requestBodySchema, operation, method, $"{operationType}RequestBody", isRequestBody: true) ?? - throw new InvalidSchemaException(); + { + requestBodyType = CreateModelDeclarations(currentNode, requestBodySchema, operation, method, + $"{operationType}RequestBody", isRequestBody: true) ?? + throw new InvalidSchemaException(); + } method.AddParameter(new CodeParameter { Name = "body", @@ -1550,6 +1561,7 @@ private void AddRequestBuilderMethodParameters(OpenApiUrlTreeNode currentNode, O PossibleValues = contentTypes.ToList() }); } + method.AddParameter(new CodeParameter { Name = "requestConfiguration", diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 19c2d8a249..6d008c1f2a 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -7180,7 +7180,7 @@ public async Task CleanupSymbolNameDoesNotCauseNameConflictsInQueryParameters() Assert.Equal("int64", select.Type.Name); } [Fact] - public async Task SupportsMultiPartFormAsRequestBody() + public async Task SupportsMultiPartFormAsRequestBodyWithDefaultMimeTypes() { var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); await using var fs = await GetDocumentStream(@"openapi: 3.0.1 @@ -7246,6 +7246,285 @@ public async Task SupportsMultiPartFormAsRequestBody() Assert.NotNull(addressClass); } [Fact] + public async Task SupportsMultiPartFormAsRequestBodyWithoutEncodingWithDefaultMimeTypes() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: Example + description: Example + version: 1.0.1 +servers: + - url: https://example.org +paths: + /directoryObject: + post: + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + id: + type: string + format: uuid + address: + $ref: '#/components/schemas/address' + profileImage: + type: string + format: binary + responses: + '204': + content: + application/json: + schema: + type: string +components: + schemas: + address: + type: object + properties: + street: + type: string + city: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false}, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + Assert.NotNull(codeModel); + var rbClass = codeModel.FindChildByName("directoryObjectRequestBuilder"); + Assert.NotNull(rbClass); + var postMethod = rbClass.FindChildByName("Post", false); + Assert.NotNull(postMethod); + var bodyParameter = postMethod.Parameters.FirstOrDefault(static x => x.IsOfKind(CodeParameterKind.RequestBody)); + Assert.NotNull(bodyParameter); + Assert.Equal("directoryObjectPostRequestBody", bodyParameter.Type.Name, StringComparer.OrdinalIgnoreCase); + var addressClass = codeModel.FindChildByName("Address"); + Assert.NotNull(addressClass); + } + [Fact] + public async Task SupportsMultipleContentTypesAsRequestBodyWithDefaultMimeTypes() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: Example + description: Example + version: 1.0.1 +servers: + - url: https://example.org +paths: + /directoryObject: + post: + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + id: + type: string + format: uuid + address: + $ref: '#/components/schemas/address' + profileImage: + type: string + format: binary + application/json: + schema: + type: object + properties: + id: + type: string + format: uuid + address: + $ref: '#/components/schemas/address' + profileImage: + type: string + format: binary + responses: + '204': + content: + application/json: + schema: + type: string +components: + schemas: + address: + type: object + properties: + street: + type: string + city: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false}, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + Assert.NotNull(codeModel); + var rbClass = codeModel.FindChildByName("directoryObjectRequestBuilder"); + Assert.NotNull(rbClass); + var postMethod = rbClass.FindChildByName("Post", false); + Assert.NotNull(postMethod); + var bodyParameter = postMethod.Parameters.FirstOrDefault(static x => x.IsOfKind(CodeParameterKind.RequestBody)); + Assert.NotNull(bodyParameter); + Assert.Equal("directoryObjectPostRequestBody", bodyParameter.Type.Name, StringComparer.OrdinalIgnoreCase); + var addressClass = codeModel.FindChildByName("Address"); + Assert.NotNull(addressClass); + } + [Fact] + public async Task SupportsMultipleContentTypesAsRequestBodyWithMultipartPriorityNoEncoding() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: Example + description: Example + version: 1.0.1 +servers: + - url: https://example.org +paths: + /directoryObject: + post: + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + id: + type: string + format: uuid + address: + $ref: '#/components/schemas/address' + profileImage: + type: string + format: binary + application/json: + schema: + type: object + properties: + id: + type: string + format: uuid + address: + $ref: '#/components/schemas/address' + profileImage: + type: string + format: binary + responses: + '204': + content: + application/json: + schema: + type: string +components: + schemas: + address: + type: object + properties: + street: + type: string + city: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false, StructuredMimeTypes = new StructuredMimeTypesCollection {"multipart/form-data;q=1", "application/json;q=0.1"}}, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + Assert.NotNull(codeModel); + var rbClass = codeModel.FindChildByName("directoryObjectRequestBuilder"); + Assert.NotNull(rbClass); + var postMethod = rbClass.FindChildByName("Post", false); + Assert.NotNull(postMethod); + var bodyParameter = postMethod.Parameters.FirstOrDefault(static x => x.IsOfKind(CodeParameterKind.RequestBody)); + Assert.NotNull(bodyParameter); + Assert.Equal("directoryObjectPostRequestBody", bodyParameter.Type.Name, StringComparer.OrdinalIgnoreCase); + var addressClass = codeModel.FindChildByName("Address"); + Assert.NotNull(addressClass); + } + [Fact] + public async Task SupportsMultipleContentTypesAsRequestBodyWithMultipartPriorityAndEncoding() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: Example + description: Example + version: 1.0.1 +servers: + - url: https://example.org +paths: + /directoryObject: + post: + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + id: + type: string + format: uuid + address: + $ref: '#/components/schemas/address' + profileImage: + type: string + format: binary + encoding: + id: + contentType: text/plain + address: + contentType: application/json + profileImage: + contentType: image/png + application/json: + schema: + type: object + properties: + id: + type: string + format: uuid + address: + $ref: '#/components/schemas/address' + profileImage: + type: string + format: binary + responses: + '204': + content: + application/json: + schema: + type: string +components: + schemas: + address: + type: object + properties: + street: + type: string + city: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false, StructuredMimeTypes = new StructuredMimeTypesCollection {"multipart/form-data;q=1", "application/json;q=0.1"}}, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + Assert.NotNull(codeModel); + var rbClass = codeModel.FindChildByName("directoryObjectRequestBuilder"); + Assert.NotNull(rbClass); + var postMethod = rbClass.FindChildByName("Post", false); + Assert.NotNull(postMethod); + var bodyParameter = postMethod.Parameters.FirstOrDefault(static x => x.IsOfKind(CodeParameterKind.RequestBody)); + Assert.NotNull(bodyParameter); + Assert.Equal("MultipartBody", bodyParameter.Type.Name, StringComparer.OrdinalIgnoreCase); + var addressClass = codeModel.FindChildByName("Address"); + Assert.NotNull(addressClass); + } + [Fact] public async Task ComplexInheritanceStructures() { var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); From 9b8ccec1f24ab2dc3cbc3ee03354896cf8ff1392 Mon Sep 17 00:00:00 2001 From: Mathew Spearey Date: Thu, 30 May 2024 15:32:02 +0100 Subject: [PATCH 2/3] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a91a69048..34e10fa314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixes a bug where request executors would be missing Untyped parameters in dotnet [#4692](https://github.com/microsoft/kiota/issues/4692) - Fixes a bug where indexers in include/exclude patters were not normalized if the indexer was the last segment without a slash at the end [#4715](https://github.com/microsoft/kiota/issues/4715) - Fixes a bug where CLI generation doesnot handle parameters of type string array. [#4707](https://github.com/microsoft/kiota/issues/4707) +- Fixed a bug where models would not be created when a multipart content schema existed with no encoding [#4734](https://github.com/microsoft/kiota/issues/4734) ## [1.14.0] - 2024-05-02 From 8e482eac84d72c00240dd0b3addcecfaf925349e Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Fri, 31 May 2024 09:48:57 +0300 Subject: [PATCH 3/3] Fix format --- src/Kiota.Builder/KiotaBuilder.cs | 2 +- tests/Kiota.Builder.Tests/KiotaBuilderTests.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 2ef10354c5..81613e912f 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1561,7 +1561,7 @@ private void AddRequestBuilderMethodParameters(OpenApiUrlTreeNode currentNode, O PossibleValues = contentTypes.ToList() }); } - + method.AddParameter(new CodeParameter { Name = "requestConfiguration", diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 6d008c1f2a..e70aff7d30 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -7289,7 +7289,7 @@ public async Task SupportsMultiPartFormAsRequestBodyWithoutEncodingWithDefaultMi city: type: string"); var mockLogger = new Mock>(); - var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false}, _httpClient); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false }, _httpClient); var document = await builder.CreateOpenApiDocumentAsync(fs); var node = builder.CreateUriSpace(document); var codeModel = builder.CreateSourceModel(node); @@ -7360,7 +7360,7 @@ public async Task SupportsMultipleContentTypesAsRequestBodyWithDefaultMimeTypes( city: type: string"); var mockLogger = new Mock>(); - var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false}, _httpClient); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false }, _httpClient); var document = await builder.CreateOpenApiDocumentAsync(fs); var node = builder.CreateUriSpace(document); var codeModel = builder.CreateSourceModel(node); @@ -7431,7 +7431,7 @@ public async Task SupportsMultipleContentTypesAsRequestBodyWithMultipartPriority city: type: string"); var mockLogger = new Mock>(); - var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false, StructuredMimeTypes = new StructuredMimeTypesCollection {"multipart/form-data;q=1", "application/json;q=0.1"}}, _httpClient); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false, StructuredMimeTypes = new StructuredMimeTypesCollection { "multipart/form-data;q=1", "application/json;q=0.1" } }, _httpClient); var document = await builder.CreateOpenApiDocumentAsync(fs); var node = builder.CreateUriSpace(document); var codeModel = builder.CreateSourceModel(node); @@ -7509,7 +7509,7 @@ public async Task SupportsMultipleContentTypesAsRequestBodyWithMultipartPriority city: type: string"); var mockLogger = new Mock>(); - var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false, StructuredMimeTypes = new StructuredMimeTypesCollection {"multipart/form-data;q=1", "application/json;q=0.1"}}, _httpClient); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false, StructuredMimeTypes = new StructuredMimeTypesCollection { "multipart/form-data;q=1", "application/json;q=0.1" } }, _httpClient); var document = await builder.CreateOpenApiDocumentAsync(fs); var node = builder.CreateUriSpace(document); var codeModel = builder.CreateSourceModel(node);