Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add related links to CLI documentation #2251

Merged
merged 4 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
64 changes: 60 additions & 4 deletions src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,11 @@ private static List<string> WriteExecutableCommandOptions(LanguageWriter writer,
optionBuilder.Append($", getDefaultValue: ()=> {defaultValue}");
}

if (!string.IsNullOrEmpty(option.Documentation.Description))
var builder = BuildDescriptionForElement(option);

if (builder?.Length > 0)
{
optionBuilder.Append($", description: \"{option.Documentation.Description}\"");
optionBuilder.Append($", description: \"{builder}\"");
}

optionBuilder.Append(") {");
Expand Down Expand Up @@ -348,8 +350,62 @@ private static List<string> 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 builder = BuildDescriptionForElement(codeElement);
if (builder?.Length > 0)
writer.WriteLine($"command.Description = \"{builder}\";");
}

private static StringBuilder? 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,
};
// Optimization, don't allocate
if (documentation is null) return null;
var builder = new StringBuilder();
if (documentation.DescriptionAvailable)
{
builder.Append(documentation.Description);
}

if (documentation.DocumentationLink is not null)
{
string newLine = string.Empty;
if (documentation.DescriptionAvailable)
{
newLine = element switch
{
_ when element is CodeParameter => "\\n",
_ => "\\n\\n",
};
}
string title;
if (!string.IsNullOrWhiteSpace(documentation.DocumentationLabel))
{
title = documentation.DocumentationLabel;
}
else
{
title = element is CodeParameter ? "See" : "Related Links";
}
string titleSuffix = element switch
{
_ when element is CodeParameter => ": ",
_ => ":\\n ",
};

builder.Append(newLine);
builder.Append(title);
builder.Append(titleSuffix);
builder.Append(documentation.DocumentationLink);
}

return builder;
}

private void WriteContainerCommand(CodeMethod codeElement, LanguageWriter writer, CodeClass parent, string name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,113 @@ 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 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")
}
});

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<string>(\"-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<string\\[]>\\(\"--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<string?> instead? Currently for the CLI, it doesn't matter since GetValueForOption always returns nullable types
Assert.Contains("var testDocOption = new Option<string>(\"--test-doc\", description: \"See: https://test.com/help/description\")", result);
Assert.Contains("var testDoc2Option = new Option<string>(\"--test-doc2\", description: \"Documentation label2\\nSee: https://test.com/help/description\")", result);
Assert.Contains("var testDoc3Option = new Option<string>(\"--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);
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<Stream>(requestInfo, errorMapping: default, cancellationToken: cancellationToken) ?? Stream.Null;", result);
Assert.Contains("IOutputFormatterFactory outputFormatterFactory = invocationContext.BindingContext.GetRequiredService<IOutputFormatterFactory>();", 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()
{
Expand Down Expand Up @@ -405,6 +512,7 @@ public void WritesExecutableCommandForGetRequestPrimitive()
Assert.Contains("command.AddOption(qOption);", result);
Assert.Matches("var testHeaderOption = new Option<string\\[]>\\(\"--test-header\", description: \"The test header\"\\) {\\s+Arity = ArgumentArity.OneOrMore", result);
Assert.Contains("testHeaderOption.IsRequired = true;", result);
Assert.Contains("var countOption = new Option<bool?>(\"--count\")", result);
Assert.Contains("command.AddOption(testHeaderOption);", result);
Assert.Contains("command.SetHandler(async (invocationContext) => {", result);
Assert.Contains("var q = invocationContext.ParseResult.GetValueForOption(qOption);", result);
Expand Down