From 10c64c1bd9103b10262d3b5c1330e53c449b6526 Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Fri, 3 Feb 2023 16:09:44 +0300 Subject: [PATCH 1/4] Add related links to CLI documentation --- .../Writers/Shell/ShellCodeMethodWriter.cs | 46 +++++++++- .../Shell/ShellCodeMethodWriterTests.cs | 89 +++++++++++++++++++ 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs b/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs index 9a1c69ad26..2080250868 100644 --- a/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs @@ -314,9 +314,11 @@ private static List WriteExecutableCommandOptions(LanguageWriter writer, optionBuilder.Append($", getDefaultValue: ()=> {defaultValue}"); } - if (!string.IsNullOrEmpty(option.Documentation.Description)) + var help = BuildDescriptionForElement(option); + + if (!string.IsNullOrEmpty(help)) { - optionBuilder.Append($", description: \"{option.Documentation.Description}\""); + optionBuilder.Append($", description: \"{help}\""); } optionBuilder.Append(") {"); @@ -348,8 +350,44 @@ private static List WriteExecutableCommandOptions(LanguageWriter writer, private static void WriteCommandDescription(CodeMethod codeElement, LanguageWriter writer) { - if (!string.IsNullOrWhiteSpace(codeElement.Documentation.Description)) - writer.WriteLine($"command.Description = \"{codeElement.Documentation.Description}\";"); + var help = BuildDescriptionForElement(codeElement); + if (!string.IsNullOrWhiteSpace(help)) + writer.WriteLine($"command.Description = \"{help}\";"); + } + + private static string? BuildDescriptionForElement(CodeElement element) + { + var documentation = element switch + { + CodeMethod doc when element is CodeMethod => doc.Documentation, + CodeProperty prop when element is CodeProperty => prop.Documentation, + CodeIndexer prop when element is CodeIndexer => prop.Documentation, + CodeParameter prop when element is CodeParameter => prop.Documentation, + _ => null, + }; + if (documentation is null) return null; + var helpDescBuilder = new StringBuilder(); + if (documentation.DescriptionAvailable) + { + helpDescBuilder.Append(documentation.Description); + } + + if (documentation.DocumentationLink is not null) + { + if (element is CodeParameter) + { + if (documentation.DescriptionAvailable) helpDescBuilder.Append("\\n"); + helpDescBuilder.Append("See: "); + } + else + { + if (documentation.DescriptionAvailable) helpDescBuilder.Append("\\n\\n"); + helpDescBuilder.Append("Related Links:\\n "); + } + helpDescBuilder.Append(documentation.DocumentationLink); + } + + return helpDescBuilder.ToString(); } private void WriteContainerCommand(CodeMethod codeElement, LanguageWriter writer, CodeClass parent, string name) diff --git a/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs index 573ff025d2..dc48952f33 100644 --- a/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs @@ -353,6 +353,94 @@ public void WritesContainerCommandWithConflictingTypes() Assert.Contains("return command;", result); } + [Fact] + public void WritesExecutableCommandWithRelatedLinksInDescription() + { + method.Kind = CodeMethodKind.CommandBuilder; + method.Documentation.Description = "Test description"; + method.Documentation.DocumentationLink = new Uri("https://test.com/help/description"); + method.SimpleName = "User"; + method.HttpMethod = HttpMethod.Get; + var stringType = new CodeType + { + Name = "string", + }; + var generatorMethod = new CodeMethod + { + Kind = CodeMethodKind.RequestGenerator, + Name = "CreateGetRequestInformation", + HttpMethod = method.HttpMethod, + ReturnType = stringType, + }; + method.OriginalMethod = new CodeMethod + { + Kind = CodeMethodKind.RequestExecutor, + HttpMethod = method.HttpMethod, + ReturnType = stringType, + Parent = method.Parent + }; + var codeClass = method.Parent as CodeClass; + codeClass.AddMethod(generatorMethod); + + AddRequestProperties(); + AddRequestBodyParameters(method.OriginalMethod); + AddPathQueryAndHeaderParameters(generatorMethod); + generatorMethod.AddPathQueryOrHeaderParameter(new CodeParameter + { + Name = "testDoc", + Kind = CodeParameterKind.QueryParameter, + Type = new CodeType + { + Name = "string", + IsNullable = true, + }, + Documentation = new() { + DocumentationLink = new Uri("https://test.com/help/description") + } + }); + generatorMethod.AddPathQueryOrHeaderParameter(new CodeParameter + { + Name = "testDoc2", + Kind = CodeParameterKind.QueryParameter, + Type = new CodeType + { + Name = "string", + IsNullable = true, + }, + Documentation = new() { + Description = "Documentation label", + DocumentationLink = new Uri("https://test.com/help/description") + } + }); + + writer.Write(method); + var result = tw.ToString(); + + Assert.Contains("var command = new Command(\"user\");", result); + Assert.Contains("command.Description = \"Test description\\n\\nRelated Links:\\n https://test.com/help/description\";", result); + Assert.Contains("var qOption = new Option(\"-q\", getDefaultValue: ()=> \"test\", description: \"The q option\")", result); + Assert.Contains("qOption.IsRequired = false;", result); + Assert.Contains("command.AddOption(qOption);", result); + Assert.Matches("var testHeaderOption = new Option\\(\"--test-header\", description: \"The test header\"\\) {\\s+Arity = ArgumentArity.OneOrMore", result); + Assert.Contains("testHeaderOption.IsRequired = true;", result); + Assert.Contains("command.AddOption(testHeaderOption);", result); + // Should generated code have Option instead? Currently for the CLI, it doesn't matter since GetValueForOption always returns nullable types + Assert.Contains("var testDocOption = new Option(\"--test-doc\", description: \"See: https://test.com/help/description\")", result); + Assert.Contains("var testDoc2Option = new Option(\"--test-doc2\", description: \"Documentation label\\nSee: https://test.com/help/description\")", result); + Assert.Contains("command.SetHandler(async (invocationContext) => {", result); + Assert.Contains("var q = invocationContext.ParseResult.GetValueForOption(qOption);", result); + Assert.Contains("var testHeader = invocationContext.ParseResult.GetValueForOption(testHeaderOption);", result); + Assert.Contains("var requestInfo = CreateGetRequestInformation", result); + Assert.Contains("if (testPath is not null) requestInfo.PathParameters.Add(\"test%2Dpath\", testPath);", result); + Assert.Contains("if (testHeader is not null) requestInfo.Headers.Add(\"Test-Header\", testHeader);", result); + Assert.Contains("var response = await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping: default, cancellationToken: cancellationToken) ?? Stream.Null;", result); + Assert.Contains("IOutputFormatterFactory outputFormatterFactory = invocationContext.BindingContext.GetRequiredService();", result); + Assert.Contains("var formatter = outputFormatterFactory.GetFormatter(FormatterType.TEXT);", result); + Assert.Contains("await formatter.WriteOutputAsync(response, null, cancellationToken);", result); + Assert.Contains("});", result); + Assert.Contains("return command;", result); + } + [Fact] public void WritesExecutableCommandForGetRequestPrimitive() { @@ -405,6 +493,7 @@ public void WritesExecutableCommandForGetRequestPrimitive() Assert.Contains("command.AddOption(qOption);", result); Assert.Matches("var testHeaderOption = new Option\\(\"--test-header\", description: \"The test header\"\\) {\\s+Arity = ArgumentArity.OneOrMore", result); Assert.Contains("testHeaderOption.IsRequired = true;", result); + Assert.Contains("var countOption = new Option(\"--count\")", result); Assert.Contains("command.AddOption(testHeaderOption);", result); Assert.Contains("command.SetHandler(async (invocationContext) => {", result); Assert.Contains("var q = invocationContext.ParseResult.GetValueForOption(qOption);", result); From aa0b9b453a05ff18ea3ca12f711685e9fe24d85c Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Fri, 3 Feb 2023 16:13:18 +0300 Subject: [PATCH 2/4] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a34d9c158..6991ed804b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for Composed types (De)Serialization for PHP Generation. [#1814](https://github.com/microsoft/kiota/issues/1814) - Added support for backing store in Go. [466](https://github.com/microsoft/kiota/issues/466) - Added support for inherited error types by inlining the parents. [2194](https://github.com/microsoft/kiota/issues/2194) +- Added support for documentation links in CLI's help commands. ### Changed From e54db303ca5921a500cfe51694723da8651045ae Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Fri, 3 Feb 2023 16:39:17 +0300 Subject: [PATCH 3/4] Fix formatting. --- .../Writers/Shell/ShellCodeMethodWriterTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs index dc48952f33..7b93c51ad9 100644 --- a/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs @@ -394,7 +394,8 @@ public void WritesExecutableCommandWithRelatedLinksInDescription() Name = "string", IsNullable = true, }, - Documentation = new() { + Documentation = new() + { DocumentationLink = new Uri("https://test.com/help/description") } }); @@ -407,7 +408,8 @@ public void WritesExecutableCommandWithRelatedLinksInDescription() Name = "string", IsNullable = true, }, - Documentation = new() { + Documentation = new() + { Description = "Documentation label", DocumentationLink = new Uri("https://test.com/help/description") } From 6c9ec9f5cebf5ac32c5f8e606857da8df1f0e71a Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Fri, 3 Feb 2023 20:38:41 +0300 Subject: [PATCH 4/4] Include documentation label in description. Optimize description builder function --- .../Writers/Shell/ShellCodeMethodWriter.cs | 50 +++++++++++++------ .../Shell/ShellCodeMethodWriterTests.cs | 21 +++++++- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs b/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs index 2080250868..d4305ed049 100644 --- a/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs @@ -314,11 +314,11 @@ private static List WriteExecutableCommandOptions(LanguageWriter writer, optionBuilder.Append($", getDefaultValue: ()=> {defaultValue}"); } - var help = BuildDescriptionForElement(option); + var builder = BuildDescriptionForElement(option); - if (!string.IsNullOrEmpty(help)) + if (builder?.Length > 0) { - optionBuilder.Append($", description: \"{help}\""); + optionBuilder.Append($", description: \"{builder}\""); } optionBuilder.Append(") {"); @@ -350,12 +350,12 @@ private static List WriteExecutableCommandOptions(LanguageWriter writer, private static void WriteCommandDescription(CodeMethod codeElement, LanguageWriter writer) { - var help = BuildDescriptionForElement(codeElement); - if (!string.IsNullOrWhiteSpace(help)) - writer.WriteLine($"command.Description = \"{help}\";"); + var builder = BuildDescriptionForElement(codeElement); + if (builder?.Length > 0) + writer.WriteLine($"command.Description = \"{builder}\";"); } - private static string? BuildDescriptionForElement(CodeElement element) + private static StringBuilder? BuildDescriptionForElement(CodeElement element) { var documentation = element switch { @@ -365,29 +365,47 @@ private static void WriteCommandDescription(CodeMethod codeElement, LanguageWrit CodeParameter prop when element is CodeParameter => prop.Documentation, _ => null, }; + // Optimization, don't allocate if (documentation is null) return null; - var helpDescBuilder = new StringBuilder(); + var builder = new StringBuilder(); if (documentation.DescriptionAvailable) { - helpDescBuilder.Append(documentation.Description); + builder.Append(documentation.Description); } if (documentation.DocumentationLink is not null) { - if (element is CodeParameter) + string newLine = string.Empty; + if (documentation.DescriptionAvailable) { - if (documentation.DescriptionAvailable) helpDescBuilder.Append("\\n"); - helpDescBuilder.Append("See: "); + newLine = element switch + { + _ when element is CodeParameter => "\\n", + _ => "\\n\\n", + }; + } + string title; + if (!string.IsNullOrWhiteSpace(documentation.DocumentationLabel)) + { + title = documentation.DocumentationLabel; } else { - if (documentation.DescriptionAvailable) helpDescBuilder.Append("\\n\\n"); - helpDescBuilder.Append("Related Links:\\n "); + title = element is CodeParameter ? "See" : "Related Links"; } - helpDescBuilder.Append(documentation.DocumentationLink); + string titleSuffix = element switch + { + _ when element is CodeParameter => ": ", + _ => ":\\n ", + }; + + builder.Append(newLine); + builder.Append(title); + builder.Append(titleSuffix); + builder.Append(documentation.DocumentationLink); } - return helpDescBuilder.ToString(); + return builder; } private void WriteContainerCommand(CodeMethod codeElement, LanguageWriter writer, CodeClass parent, string name) diff --git a/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs index 7b93c51ad9..7daa8ebd11 100644 --- a/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs @@ -410,7 +410,23 @@ public void WritesExecutableCommandWithRelatedLinksInDescription() }, Documentation = new() { - Description = "Documentation label", + Description = "Documentation label2", + DocumentationLink = new Uri("https://test.com/help/description") + } + }); + generatorMethod.AddPathQueryOrHeaderParameter(new CodeParameter + { + Name = "testDoc3", + Kind = CodeParameterKind.QueryParameter, + Type = new CodeType + { + Name = "string", + IsNullable = true, + }, + Documentation = new() + { + Description = "Documentation label3", + DocumentationLabel = "Test label", DocumentationLink = new Uri("https://test.com/help/description") } }); @@ -428,7 +444,8 @@ public void WritesExecutableCommandWithRelatedLinksInDescription() Assert.Contains("command.AddOption(testHeaderOption);", result); // Should generated code have Option instead? Currently for the CLI, it doesn't matter since GetValueForOption always returns nullable types Assert.Contains("var testDocOption = new Option(\"--test-doc\", description: \"See: https://test.com/help/description\")", result); - Assert.Contains("var testDoc2Option = new Option(\"--test-doc2\", description: \"Documentation label\\nSee: https://test.com/help/description\")", result); + Assert.Contains("var testDoc2Option = new Option(\"--test-doc2\", description: \"Documentation label2\\nSee: https://test.com/help/description\")", result); + Assert.Contains("var testDoc3Option = new Option(\"--test-doc3\", description: \"Documentation label3\\nTest label: https://test.com/help/description\")", result); Assert.Contains("command.SetHandler(async (invocationContext) => {", result); Assert.Contains("var q = invocationContext.ParseResult.GetValueForOption(qOption);", result); Assert.Contains("var testHeader = invocationContext.ParseResult.GetValueForOption(testHeaderOption);", result);