From 4a1c64c5ef3d9b26ac00cebf5e68c9858eb30fb5 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 13 Oct 2024 23:12:32 -0500 Subject: [PATCH 1/4] fix: ts support for param paths with trailing slash The typescript writer has a conflict when there is more than 1 code file in a single namespace. --- .../Refiners/TypeScriptRefiner.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs index 825c94a1c3..3e22a7c32e 100644 --- a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs +++ b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs @@ -167,6 +167,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken GenerateReusableModelsCodeFiles(modelsNamespace); GenerateRequestBuilderCodeFiles(modelsNamespace); GroupReusableModelsInSingleFile(modelsNamespace); + MergeConflictingBuilderCodeFiles(modelsNamespace); RemoveSelfReferencingUsings(generatedCode); AddAliasToCodeFileUsings(generatedCode); CorrectSerializerParameters(generatedCode); @@ -301,6 +302,36 @@ parentNamespace.Parent is CodeNamespace parentLevelNamespace && CrawlTree(currentElement, AddDownwardsConstantsImports); } + private static void MergeConflictingBuilderCodeFiles(CodeNamespace modelsNamespace) + { + if (modelsNamespace.Parent is not CodeNamespace mainNamespace) return; + var elementsToConsider = mainNamespace.Namespaces.Except([modelsNamespace]).OfType().ToArray(); + foreach (var element in elementsToConsider) + MergeConflictingBuilderCodeFilesForElement(element); + } + private static void MergeConflictingBuilderCodeFilesForElement(CodeElement currentElement) + { + if (currentElement is CodeNamespace currentNamespace && currentNamespace.Files.Count() > 1) + { + var targetFile = currentNamespace.Files.First(); + foreach (var fileToMerge in currentNamespace.Files.Skip(1)) + { + if (fileToMerge.Classes.Any()) + targetFile.AddElements(fileToMerge.Classes.ToArray()); + if (fileToMerge.Constants.Any()) + targetFile.AddElements(fileToMerge.Constants.ToArray()); + if (fileToMerge.Enums.Any()) + targetFile.AddElements(fileToMerge.Enums.ToArray()); + if (fileToMerge.Interfaces.Any()) + targetFile.AddElements(fileToMerge.Interfaces.ToArray()); + if (fileToMerge.Usings.Any()) + targetFile.AddElements(fileToMerge.Usings.ToArray()); + currentNamespace.RemoveChildElement(fileToMerge); + } + } + CrawlTree(currentElement, MergeConflictingBuilderCodeFilesForElement); + } + private static CodeFile? GenerateModelCodeFile(CodeInterface codeInterface, CodeNamespace codeNamespace) { var functions = GetSerializationAndFactoryFunctions(codeInterface, codeNamespace).ToArray(); From 1a6c287640892409b09288c23b3d84c4cb8e5475 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:37:26 -0500 Subject: [PATCH 2/4] fix: use the right UriTemplate for requests metadata --- src/Kiota.Builder/CodeDOM/CodeConstant.cs | 1 + src/Kiota.Builder/Refiners/TypeScriptRefiner.cs | 6 +++--- .../Writers/TypeScript/CodeConstantWriter.cs | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Kiota.Builder/CodeDOM/CodeConstant.cs b/src/Kiota.Builder/CodeDOM/CodeConstant.cs index 96d8eb93b1..10b1fa9389 100644 --- a/src/Kiota.Builder/CodeDOM/CodeConstant.cs +++ b/src/Kiota.Builder/CodeDOM/CodeConstant.cs @@ -95,6 +95,7 @@ public CodeDocumentation Documentation Name = $"{codeClass.Name.ToFirstCharacterLowerCase()}{RequestsMetadataSuffix}", Kind = CodeConstantKind.RequestsMetadata, OriginalCodeElement = codeClass, + UriTemplate = $"{codeClass.Name.ToFirstCharacterLowerCase()}{UriTemplateSuffix}", }; result.Documentation.DescriptionTemplate = "Metadata for all the requests in the request builder."; if (usingsToAdd is { Length: > 0 } usingsToAddList) diff --git a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs index 3e22a7c32e..a910d82f8f 100644 --- a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs +++ b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs @@ -311,10 +311,10 @@ private static void MergeConflictingBuilderCodeFiles(CodeNamespace modelsNamespa } private static void MergeConflictingBuilderCodeFilesForElement(CodeElement currentElement) { - if (currentElement is CodeNamespace currentNamespace && currentNamespace.Files.Count() > 1) + if (currentElement is CodeNamespace currentNamespace && currentNamespace.Files.ToArray() is { Length: > 1 } codeFiles) { - var targetFile = currentNamespace.Files.First(); - foreach (var fileToMerge in currentNamespace.Files.Skip(1)) + var targetFile = codeFiles.First(); + foreach (var fileToMerge in codeFiles.Skip(1)) { if (fileToMerge.Classes.Any()) targetFile.AddElements(fileToMerge.Classes.ToArray()); diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs index 9e4cda57f8..d9ce4d018b 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs @@ -92,8 +92,20 @@ private void WriteRequestsMetadataConstant(CodeConstant codeElement, LanguageWri .OrderBy(static x => x.Name, StringComparer.OrdinalIgnoreCase) .ToArray() is not { Length: > 0 } executorMethods) return; - var uriTemplateConstant = codeElement.Parent is CodeFile parentFile && parentFile.Constants.FirstOrDefault(static x => x.Kind is CodeConstantKind.UriTemplate) is CodeConstant tplct ? - tplct : throw new InvalidOperationException("Couldn't find the associated uri template constant for the requests metadata constant"); + CodeConstant? uriTemplateConstant = null; + if (codeElement.Parent is CodeFile parentFile) + { + var uriTemplates = parentFile.Constants.Where(static x => x.Kind is CodeConstantKind.UriTemplate).ToArray(); + var uriTemplate = uriTemplates.Length == 1 + ? uriTemplates.First() + : uriTemplates.FirstOrDefault(x => x.Name == codeElement.UriTemplate); + if (uriTemplate is CodeConstant tplct) + { + uriTemplateConstant = tplct; + } + } + if (uriTemplateConstant == null) + throw new InvalidOperationException("Couldn't find the associated uri template constant for the requests metadata constant"); writer.StartBlock($"export const {codeElement.Name.ToFirstCharacterUpperCase()}: RequestsMetadata = {{"); foreach (var executorMethod in executorMethods) { From 49e8e19494d86cd02e4e339898d6af613c50c84e Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 20 Oct 2024 12:36:18 -0500 Subject: [PATCH 3/4] feat: support non-param paths with trailing slash --- src/Kiota.Builder/Extensions/StringExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Kiota.Builder/Extensions/StringExtensions.cs b/src/Kiota.Builder/Extensions/StringExtensions.cs index 9707295a57..e87e31d923 100644 --- a/src/Kiota.Builder/Extensions/StringExtensions.cs +++ b/src/Kiota.Builder/Extensions/StringExtensions.cs @@ -266,6 +266,11 @@ private static string NormalizeSymbolsBeforeCleanup(string original) result = result.Replace("+", "_plus_", StringComparison.OrdinalIgnoreCase); } + if (result.Contains('\\', StringComparison.OrdinalIgnoreCase)) + { + result = result.Replace(@"\", "Slash", StringComparison.OrdinalIgnoreCase); + } + return result; } /// From 4373e5845bf9cb1ccca47bf750386fbcb3cf7801 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Mon, 21 Oct 2024 00:14:54 -0500 Subject: [PATCH 4/4] test: fix failing, test trailing slash handling behavior --- .../Extensions/StringExtensionsTests.cs | 5 +- .../TrailingSlashSampleYml.cs | 132 ++++++++++++++++++ .../TypeScriptLanguageRefinerTests.cs | 49 +++++++ .../Writers/CSharp/CodeEnumWriterTests.cs | 2 +- .../TypeScript/CodeConstantWriterTests.cs | 27 ++++ 5 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 tests/Kiota.Builder.Tests/OpenApiSampleFiles/TrailingSlashSampleYml.cs diff --git a/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs index 6e9fce63a3..ebc156cf70 100644 --- a/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs +++ b/tests/Kiota.Builder.Tests/Extensions/StringExtensionsTests.cs @@ -85,7 +85,7 @@ public void NormalizeNameSpaceName() Assert.Equal("Toto", "toto".NormalizeNameSpaceName("-")); Assert.Equal("Microsoft_Graph_Message_Content", "microsoft.Graph.Message.Content".NormalizeNameSpaceName("_")); } - [InlineData("\" !#$%&'()*+,./:;<=>?@[]\\^`{}|~-", "plus")] + [InlineData("\" !#$%&'()*+,./:;<=>?@[]^`{}|~-", "plus")] [InlineData("unchanged", "unchanged")] [InlineData("@odata.changed", "OdataChanged")] [InlineData("specialLast@", "specialLast")] @@ -98,6 +98,9 @@ public void NormalizeNameSpaceName() [InlineData("-1-", "minus_1")] [InlineData("-1-1", "minus_11")] [InlineData("-", "minus")] + [InlineData("\\", "Slash")] + [InlineData("Param\\", "ParamSlash")] + [InlineData("Param\\RequestBuilder", "ParamSlashRequestBuilder")] [Theory] public void CleansUpSymbolNames(string input, string expected) { diff --git a/tests/Kiota.Builder.Tests/OpenApiSampleFiles/TrailingSlashSampleYml.cs b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/TrailingSlashSampleYml.cs new file mode 100644 index 0000000000..db58ed9047 --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/TrailingSlashSampleYml.cs @@ -0,0 +1,132 @@ +namespace Kiota.Builder.Tests.OpenApiSampleFiles; + +public static class TrailingSlashSampleYml +{ + /** + * An OpenAPI 3.0.0 sample document with trailing slashes on some paths. + */ + public static readonly string OpenApiYaml = @" +openapi: 3.0.0 +info: + title: Sample API + description: A sample API that uses trailing slashes. + version: 1.0.0 +servers: + - url: https://api.example.com/v1 +paths: + /foo: + get: + summary: Get foo + description: Returns foo. + responses: + '200': + description: foo + content: + text/plain: + schema: + type: string + /foo/: + get: + summary: Get foo slash + description: Returns foo slash. + responses: + '200': + description: foo slash + content: + text/plain: + schema: + type: string + /message/{id}: + get: + summary: Get a Message + description: Returns a single Message object. + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: A Message object + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + /message/{id}/: + get: + summary: Get replies to a Message + description: Returns a list of Message object replies for a Message. + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: A list of Message objects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Message' + /bucket/{name}/: + get: + summary: List items in a bucket + description: Returns a list of BucketFiles in a bucket. + parameters: + - name: name + in: path + required: true + schema: + type: string + responses: + '200': + description: A list of BucketFile objects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/BucketFile' + /bucket/{name}/{id}: + get: + summary: Get a bucket item + description: Returns a single BucketFile object. + parameters: + - name: name + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: A BucketFile object + content: + application/json: + schema: + $ref: '#/components/schemas/BucketFile' +components: + schemas: + Message: + type: object + properties: + Guid: + type: string + required: + - Guid + BucketFile: + type: object + properties: + Guid: + type: string + required: + - Guid"; +} diff --git a/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs index 540f49f10d..7289ec93c1 100644 --- a/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs @@ -961,6 +961,55 @@ public async Task ParsesAndRefinesUnionOfPrimitiveValuesAsync() var unionType = modelCodeFile.GetChildElements().Where(x => x is CodeFunction function && TypeScriptRefiner.GetOriginalComposedType(function.OriginalLocalMethod.ReturnType) is not null).ToList(); Assert.True(unionType.Count > 0); } + [Fact] + public async Task ParsesAndRefinesPathsWithTrailingSlashAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await File.WriteAllTextAsync(tempFilePath, TrailingSlashSampleYml.OpenApiYaml); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Testing", Serializers = ["none"], Deserializers = ["none"] }, _httpClient); + await using var fs = new FileStream(tempFilePath, FileMode.Open); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + builder.SetApiRootUrl(); + var codeModel = builder.CreateSourceModel(node); + var rootNS = codeModel.FindNamespaceByName("ApiSdk"); + Assert.NotNull(rootNS); + await ILanguageRefiner.RefineAsync(generationConfiguration, rootNS); + Assert.NotNull(rootNS); + + var fooNS = rootNS.FindNamespaceByName("ApiSdk.foo"); + Assert.NotNull(fooNS); + var fooCodeFile = fooNS.FindChildByName("fooRequestBuilder", false); + Assert.NotNull(fooCodeFile); + var fooRequestBuilder = fooCodeFile.FindChildByName("fooRequestBuilder", false); + var fooSlashRequestBuilder = fooCodeFile.FindChildByName("fooSlashRequestBuilder", false); + Assert.NotNull(fooRequestBuilder); + Assert.NotNull(fooSlashRequestBuilder); + + var messageNS = rootNS.FindNamespaceByName("ApiSdk.message"); + Assert.NotNull(messageNS); + var messageCodeFile = messageNS.FindChildByName("messageRequestBuilder", false); + Assert.NotNull(messageCodeFile); + var messageRequestBuilder = messageCodeFile.FindChildByName("messageRequestBuilder", false); + Assert.NotNull(messageRequestBuilder); + var messageWithIdSlashMethod = messageRequestBuilder.FindChildByName("withIdSlash", false); + var messageByIdMethod = messageRequestBuilder.FindChildByName("byId", false); + Assert.NotNull(messageWithIdSlashMethod); + Assert.NotNull(messageByIdMethod); + + var bucketNS = rootNS.FindNamespaceByName("ApiSdk.bucket"); + Assert.NotNull(bucketNS); + var bucketItemNS = bucketNS.FindChildByName("ApiSdk.bucket.item", false); + Assert.NotNull(bucketItemNS); + var bucketItemCodeFile = bucketItemNS.FindChildByName("WithNameItemRequestBuilder", false); + Assert.NotNull(bucketItemCodeFile); + var bucketWithNameItemRequestBuilder = bucketItemCodeFile.FindChildByName("WithNameItemRequestBuilder", false); + var bucketWithNameSlashRequestBuilder = bucketItemCodeFile.FindChildByName("WithNameSlashRequestBuilder", false); + Assert.NotNull(bucketWithNameItemRequestBuilder); + Assert.NotNull(bucketWithNameSlashRequestBuilder); + } [Fact] public void GetOriginalComposedType_ReturnsNull_WhenElementIsNull() diff --git a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeEnumWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeEnumWriterTests.cs index 0a76a2e617..54732b0ed7 100644 --- a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeEnumWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeEnumWriterTests.cs @@ -82,7 +82,7 @@ public void NamesDontDiffer_DoesntWriteEnumMember() } [Theory] - [InlineData("\\", "BackSlash")] + [InlineData("\\", "Slash")] [InlineData("?", "QuestionMark")] [InlineData("$", "Dollar")] [InlineData("~", "Tilde")] diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs index 759ea67dc8..781b178b33 100644 --- a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs @@ -477,6 +477,33 @@ public void WritesRequestExecutorBody() Assert.Contains("403: createError403FromDiscriminatorValue as ParsableFactory", result); } [Fact] + public void WritesRequestsMetadataWithCorrectUriTemplate() + { + parentClass.Kind = CodeClassKind.RequestBuilder; + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + AddRequestProperties(); + AddRequestBodyParameters(); + var constant = CodeConstant.FromRequestBuilderToRequestsMetadata(parentClass); + var codeFile = root.TryAddCodeFile("foo", constant); + codeFile.AddElements(new CodeConstant + { + Name = "firstAndWrongUriTemplate", + Kind = CodeConstantKind.UriTemplate, + UriTemplate = "{+baseurl}/foo/" + }); + codeFile.AddElements(new CodeConstant + { + Name = "parentClassUriTemplate", + Kind = CodeConstantKind.UriTemplate, + UriTemplate = "{+baseurl}/foo" + }); + writer.Write(constant); + var result = tw.ToString(); + Assert.Contains("uriTemplate: ParentClassUriTemplate", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] public void WritesIndexer() { AddRequestProperties();