diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8641561f54..be4ec0c376 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -109,3 +109,10 @@ updates: open-pull-requests-limit: 10 labels: - TypeScript +- package-ecosystem: nuget + directory: "/cli/commons" + schedule: + interval: daily + open-pull-requests-limit: 10 + labels: + - CLI diff --git a/.github/workflows/cli-commons.yml b/.github/workflows/cli-commons.yml new file mode 100644 index 0000000000..99f5269d08 --- /dev/null +++ b/.github/workflows/cli-commons.yml @@ -0,0 +1,64 @@ +name: CLI commons + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: ['cli/commons/**', '.github/workflows/**'] + pull_request: + paths: ['cli/commons/**', '.github/workflows/**'] + +jobs: + build: + runs-on: ubuntu-latest + env: + relativePath: ./cli/commons + solutionName: Microsoft.Kiota.Cli.Commons.sln + steps: + - uses: actions/checkout@v2.4.0 + - name: Setup .NET + uses: actions/setup-dotnet@v1.9.0 + with: + dotnet-version: 6.0.x + - name: Restore dependencies + run: dotnet restore ${{ env.solutionName }} + working-directory: ${{ env.relativePath }} + - name: Build + run: dotnet build ${{ env.solutionName }} --no-restore -c Release + working-directory: ${{ env.relativePath }} + - name: Test + run: dotnet test ${{ env.solutionName }} --no-build --verbosity normal -c Release /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=opencover + working-directory: ${{ env.relativePath }} + - name: Publish + run: dotnet publish ${{ env.solutionName }} --no-restore --no-build --verbosity normal -c Release + working-directory: ${{ env.relativePath }} + - name: Pack + run: dotnet pack ${{ env.solutionName }} --no-restore --no-build --verbosity normal -c Release + working-directory: ${{ env.relativePath }} + - name: Upload Coverage Results + uses: actions/upload-artifact@v2 + with: + name: codeCoverage + path: | + ${{ env.relativePath }}src/Microsoft.Kiota.Cli.Commons.Tests/TestResults + - name: Upload Nuget Package + uses: actions/upload-artifact@v2 + with: + name: drop + path: | + ${{ env.relativePath }}/src/bin/Release/*.nupkg + deploy: + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + environment: + name: staging_feeds + runs-on: ubuntu-latest + needs: [build] + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v1.9.0 + with: + dotnet-version: 6.0.x + - uses: actions/download-artifact@v2 + with: + name: drop + - run: dotnet nuget push "*.nupkg" --skip-duplicate -s https://nuget.pkg.github.com/microsoft/index.json -k ${{ secrets.PUBLISH_GH_TOKEN }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fc908e7e9e..927dbd2b35 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -15,10 +15,10 @@ on: workflow_dispatch: push: branches: [ main ] - path-ignore: ['abstractions/**', 'authentication/**', 'serialization/**', 'http/**', '**.md', '.vscode/**', '**.svg'] + path-ignore: ['abstractions/**', 'authentication/**', 'serialization/**', 'http/**', 'cli/**', '**.md', '.vscode/**', '**.svg'] pull_request: # The branches below must be a subset of the branches above - path-ignore: ['abstractions/**', 'authentication/**', 'serialization/**', 'http/**', '**.md', '.vscode/**', '**.svg'] + path-ignore: ['abstractions/**', 'authentication/**', 'serialization/**', 'http/**', 'cli/**', '**.md', '.vscode/**', '**.svg'] schedule: - cron: '20 9 * * 5' diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e7790b5fb2..5d95adc35b 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -4,10 +4,10 @@ on: push: branches: - main - path-ignore: ['abstractions/**', 'authentication/**', 'serialization/**', 'http/**', '**.md', '.vscode/**', '**.svg'] + path-ignore: ['abstractions/**', 'authentication/**', 'serialization/**', 'http/**', 'cli/**', '**.md', '.vscode/**', '**.svg'] pull_request: types: [opened, synchronize, reopened] - path-ignore: ['abstractions/**', 'authentication/**', 'serialization/**', 'http/**', '**.md', '.vscode/**', '**.svg'] + path-ignore: ['abstractions/**', 'authentication/**', 'serialization/**', 'http/**', 'cli/**', '**.md', '.vscode/**', '**.svg'] env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/README.md b/README.md index 10542c884a..c5e5279ee9 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The following table provides an overview of the languages supported by Kiota and | Python | [▶](https://github.com/microsoft/kiota/projects/3) | [✔](./abstractions/python) | ❌ | [Anonymous](./abstractions/python/kiota/abstractions/authentication/anonymous_authentication_provider.py), [Azure](./authentication/python/azure) | ❌ | | | Ruby | [✔](https://github.com/microsoft/kiota/projects/6) | [✔](./abstractions/ruby) | [JSON](./serialization/ruby/json/microsoft_kiota_serialization) | [Anonymous](./abstractions/ruby/microsoft_kiota_abstractions/lib/microsoft_kiota_abstractions/authentication/anonymous_authentication_provider.rb), [❌ Azure](https://github.com/microsoft/kiota/issues/421) | [✔](./http/ruby/nethttp/microsoft_kiota_nethttplibrary)| [link](https://microsoft.github.io/kiota/get-started/ruby) | | TypeScript/JavaScript | [✔](https://github.com/microsoft/kiota/projects/2) | [✔](./abstractions/typescript) | [JSON](./serialization/typescript/json) | [Anonymous](./abstractions/typescript/src/authentication/anonymousAuthenticationProvider.ts), [Azure](./authentication/typescript/azure) | [✔](./http/typescript/fetch) | [link](https://microsoft.github.io/kiota/get-started/typescript) | +| Shell | [✔](https://github.com/microsoft/kiota/projects/10) | [✔](./abstractions/dotnet), [✔](./cli/commonc) | [JSON](./serialization/dotnet/json) | [Anonymous](./abstractions/dotnet/src/authentication/AnonymousAuthenticationProvider.cs), [Azure](./authentication/dotnet/azure) | [✔](./http/dotnet/httpclient) | [link](https://microsoft.github.io/kiota/get-started/dotnet) | > Legend: ✔ -> in preview, ❌ -> not started, ▶ -> in progress. diff --git a/cli/commons/Microsoft.Kiota.Cli.Commons.sln b/cli/commons/Microsoft.Kiota.Cli.Commons.sln new file mode 100644 index 0000000000..1608855473 --- /dev/null +++ b/cli/commons/Microsoft.Kiota.Cli.Commons.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8DEB9AF3-BEA6-4E73-BB5E-EBC1DFE6AF22}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Kiota.Cli.Commons", "src\Microsoft.Kiota.Cli.Commons\Microsoft.Kiota.Cli.Commons.csproj", "{23DD14C5-3060-4498-B2F9-85B68770AE0B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Kiota.Cli.Commons.Tests", "src\Microsoft.Kiota.Cli.Commons.Tests\Microsoft.Kiota.Cli.Commons.Tests.csproj", "{D1228DD9-C98F-46C1-911A-65AE2D34DBE5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {23DD14C5-3060-4498-B2F9-85B68770AE0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23DD14C5-3060-4498-B2F9-85B68770AE0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23DD14C5-3060-4498-B2F9-85B68770AE0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23DD14C5-3060-4498-B2F9-85B68770AE0B}.Release|Any CPU.Build.0 = Release|Any CPU + {D1228DD9-C98F-46C1-911A-65AE2D34DBE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1228DD9-C98F-46C1-911A-65AE2D34DBE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1228DD9-C98F-46C1-911A-65AE2D34DBE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1228DD9-C98F-46C1-911A-65AE2D34DBE5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {23DD14C5-3060-4498-B2F9-85B68770AE0B} = {8DEB9AF3-BEA6-4E73-BB5E-EBC1DFE6AF22} + {D1228DD9-C98F-46C1-911A-65AE2D34DBE5} = {8DEB9AF3-BEA6-4E73-BB5E-EBC1DFE6AF22} + EndGlobalSection +EndGlobal diff --git a/cli/commons/README.md b/cli/commons/README.md new file mode 100644 index 0000000000..a2ee63c92a --- /dev/null +++ b/cli/commons/README.md @@ -0,0 +1,3 @@ +# Kiota CLI Commons Package + +Contains CLI specific types that are referenced in code generated by the shell language. \ No newline at end of file diff --git a/cli/commons/src/Microsoft.Kiota.Cli.Commons.Tests/Microsoft.Kiota.Cli.Commons.Tests.csproj b/cli/commons/src/Microsoft.Kiota.Cli.Commons.Tests/Microsoft.Kiota.Cli.Commons.Tests.csproj new file mode 100644 index 0000000000..5f88768850 --- /dev/null +++ b/cli/commons/src/Microsoft.Kiota.Cli.Commons.Tests/Microsoft.Kiota.Cli.Commons.Tests.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/cli/commons/src/Microsoft.Kiota.Cli.Commons/IO/FormatterType.cs b/cli/commons/src/Microsoft.Kiota.Cli.Commons/IO/FormatterType.cs new file mode 100644 index 0000000000..96618b0a82 --- /dev/null +++ b/cli/commons/src/Microsoft.Kiota.Cli.Commons/IO/FormatterType.cs @@ -0,0 +1,8 @@ +namespace Microsoft.Kiota.Cli.Commons.IO; + +public enum FormatterType +{ + JSON, + TABLE, + NONE +} diff --git a/cli/commons/src/Microsoft.Kiota.Cli.Commons/IO/IOutputFormatter.cs b/cli/commons/src/Microsoft.Kiota.Cli.Commons/IO/IOutputFormatter.cs new file mode 100644 index 0000000000..924b8a754a --- /dev/null +++ b/cli/commons/src/Microsoft.Kiota.Cli.Commons/IO/IOutputFormatter.cs @@ -0,0 +1,10 @@ +using System.CommandLine; + +namespace Microsoft.Kiota.Cli.Commons.IO; + +public interface IOutputFormatter +{ + void WriteOutput(string content, IConsole console); + + void WriteOutput(Stream content, IConsole console); +} diff --git a/cli/commons/src/Microsoft.Kiota.Cli.Commons/IO/IOutputFormatterFactory.cs b/cli/commons/src/Microsoft.Kiota.Cli.Commons/IO/IOutputFormatterFactory.cs new file mode 100644 index 0000000000..4d8026a99a --- /dev/null +++ b/cli/commons/src/Microsoft.Kiota.Cli.Commons/IO/IOutputFormatterFactory.cs @@ -0,0 +1,6 @@ +namespace Microsoft.Kiota.Cli.Commons.IO; + +public interface IOutputFormatterFactory +{ + IOutputFormatter GetFormatter(FormatterType formatterType); +} diff --git a/cli/commons/src/Microsoft.Kiota.Cli.Commons/Microsoft.Kiota.Cli.Commons.csproj b/cli/commons/src/Microsoft.Kiota.Cli.Commons/Microsoft.Kiota.Cli.Commons.csproj new file mode 100644 index 0000000000..062e460d3f --- /dev/null +++ b/cli/commons/src/Microsoft.Kiota.Cli.Commons/Microsoft.Kiota.Cli.Commons.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + 0.1.0 + + + + + + + diff --git a/src/Kiota.Builder/CodeDOM/CodeMethod.cs b/src/Kiota.Builder/CodeDOM/CodeMethod.cs index 048b437b30..4cc1faff2e 100644 --- a/src/Kiota.Builder/CodeDOM/CodeMethod.cs +++ b/src/Kiota.Builder/CodeDOM/CodeMethod.cs @@ -20,6 +20,7 @@ public enum CodeMethodKind RequestBuilderWithParameters, RawUrlConstructor, NullCheck, + CommandBuilder, } public enum HttpMethod { Get, @@ -48,6 +49,11 @@ public class CodeMethod : CodeTerminal, ICloneable, IDocumentedElement public void RemoveParametersByKind(params CodeParameterKind[] kinds) { parameters.RemoveAll(p => p.IsOfKind(kinds)); } + + public void ClearParameters() + { + parameters.Clear(); + } public IEnumerable Parameters { get => parameters; } public bool IsStatic {get;set;} = false; public bool IsAsync {get;set;} = true; @@ -55,7 +61,28 @@ public void RemoveParametersByKind(params CodeParameterKind[] kinds) { /// /// The property this method accesses to when it's a getter or setter. /// - public CodeProperty AccessedProperty { get; set; } + public CodeProperty AccessedProperty { get; set; + } + /// + /// The combination of the path and query parameters for the current URL. + /// Only use this property if the language you are generating for doesn't support fluent API style (e.g. Shell/CLI) + /// + public IEnumerable PathAndQueryParameters + { + get; private set; + } + public void AddPathOrQueryParameter(params CodeParameter[] parameters) + { + if (parameters == null || !parameters.Any()) return; + foreach (var parameter in parameters) + { + EnsureElementsAreChildren(parameter); + } + if (PathAndQueryParameters == null) + PathAndQueryParameters = new List(parameters); + else if (PathAndQueryParameters is List cast) + cast.AddRange(parameters); + } public bool IsOfKind(params CodeMethodKind[] kinds) { return kinds?.Contains(MethodKind) ?? false; } @@ -83,7 +110,15 @@ public bool IsSerializationMethod { /// The base url for every request read from the servers property on the description. /// Only provided for constructor on Api client /// - public string BaseUrl { get; set; } + public string BaseUrl { get; set; + } + + /// + /// This is currently used for CommandBuilder methods to get the original name without the Build prefix & Command suffix. + /// Avoids regex operations + /// + public string SimpleName { get; set; } = String.Empty; + /// /// Mapping of the error code and response types for this method. /// diff --git a/src/Kiota.Builder/Extensions/CodeParametersEnumerableExtensions.cs b/src/Kiota.Builder/Extensions/CodeParametersEnumerableExtensions.cs index a7904ce4e9..a336c9e48b 100644 --- a/src/Kiota.Builder/Extensions/CodeParametersEnumerableExtensions.cs +++ b/src/Kiota.Builder/Extensions/CodeParametersEnumerableExtensions.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Collections.Generic; namespace Kiota.Builder { diff --git a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs index 929d30beb9..a9376951ab 100644 --- a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs @@ -120,8 +120,12 @@ private static string SanitizePathParameterNames(string original) { if(string.IsNullOrEmpty(original) || !original.Contains('{')) return original; var parameters = pathParamMatcher.Matches(original); foreach(var value in parameters.Select(x => x.Value)) - original = original.Replace(value, value.Replace('-', '_')); + original = original.SanitizePathParameterName(); return original; } + public static string SanitizePathParameterName(this string original) { + if(string.IsNullOrEmpty(original)) return original; + return original.Replace('-', '_'); + } } } diff --git a/src/Kiota.Builder/Extensions/StringExtensions.cs b/src/Kiota.Builder/Extensions/StringExtensions.cs index fd0e2b6cee..99b948ca7a 100644 --- a/src/Kiota.Builder/Extensions/StringExtensions.cs +++ b/src/Kiota.Builder/Extensions/StringExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Security.Cryptography; using System.Text; @@ -26,7 +26,7 @@ public static string ReplaceValueIdentifier(this string original) => public static string TrimQuotes(this string original) => original?.Trim('\'', '"'); - public static string ToSnakeCase(this string name) + public static string ToSnakeCase(this string name, char separator = '_') { if(string.IsNullOrEmpty(name)) return name; var chunks = name.Split('-', StringSplitOptions.RemoveEmptyEntries); @@ -40,7 +40,7 @@ public static string ToSnakeCase(this string name) foreach (var item in identifier[1..]) { if(char.IsUpper(item)) { - sb.Append('_'); + sb.Append(separator); sb.Append(char.ToLowerInvariant(item)); } else { sb.Append(item); diff --git a/src/Kiota.Builder/GenerationLanguage.cs b/src/Kiota.Builder/GenerationLanguage.cs index 22ddcb5397..021f50d2e5 100644 --- a/src/Kiota.Builder/GenerationLanguage.cs +++ b/src/Kiota.Builder/GenerationLanguage.cs @@ -1,4 +1,4 @@ -namespace Kiota.Builder { +namespace Kiota.Builder { public enum GenerationLanguage { CSharp, Java, @@ -7,5 +7,6 @@ public enum GenerationLanguage { Python, Go, Ruby, + Shell } } diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 79a7a73bdc..4a67f82d1b 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -626,10 +626,40 @@ private void CreateOperationMethods(OpenApiUrlTreeNode currentNode, OperationTyp Description = operation.Description ?? operation.Summary, ReturnType = new CodeType { Name = "RequestInformation", IsNullable = false, IsExternal = true}, }; + if (config.Language == GenerationLanguage.Shell) + SetPathAndQueryParameters(generatorMethod, currentNode, operation); parentClass.AddMethod(generatorMethod); AddRequestBuilderMethodParameters(currentNode, operation, parameterClass, generatorMethod); logger.LogTrace("Creating method {name} of {type}", generatorMethod.Name, generatorMethod.ReturnType); } + private static void SetPathAndQueryParameters(CodeMethod target, OpenApiUrlTreeNode currentNode, OpenApiOperation operation) + { + var pathAndQueryParameters = currentNode + .PathItems[Constants.DefaultOpenApiLabel] + .Parameters + .Where(x => x.In == ParameterLocation.Path || x.In == ParameterLocation.Query) + .Select(x => new CodeParameter + { + Name = x.Name.TrimStart('$').SanitizePathParameterName(), + Type = GetQueryParameterType(x.Schema), + Description = x.Description, + ParameterKind = x.In == ParameterLocation.Path ? CodeParameterKind.Path : CodeParameterKind.QueryParameter, + Optional = !x.Required + }) + .Union(operation + .Parameters + .Where(x => x.In == ParameterLocation.Path || x.In == ParameterLocation.Query) + .Select(x => new CodeParameter + { + Name = x.Name.TrimStart('$').SanitizePathParameterName(), + Type = GetQueryParameterType(x.Schema), + Description = x.Description, + ParameterKind = x.In == ParameterLocation.Path ? CodeParameterKind.Path : CodeParameterKind.QueryParameter, + Optional = !x.Required + })) + .ToArray(); + target.AddPathOrQueryParameter(pathAndQueryParameters); + } private void AddRequestBuilderMethodParameters(OpenApiUrlTreeNode currentNode, OpenApiOperation operation, CodeClass parameterClass, CodeMethod method) { var nonBinaryRequestBody = operation.RequestBody?.Content?.FirstOrDefault(x => !RequestBodyBinaryContentType.Equals(x.Key, StringComparison.OrdinalIgnoreCase)); if (nonBinaryRequestBody.HasValue && nonBinaryRequestBody.Value.Value != null) @@ -979,6 +1009,13 @@ private CodeClass CreateOperationParameter(OpenApiUrlTreeNode node, OperationTyp return parameterClass; } else return null; } + private static CodeType GetQueryParameterType(OpenApiSchema schema) => + new() + { + IsExternal = true, + Name = schema.Items?.Type ?? schema.Type, + CollectionKind = schema.IsArray() ? CodeType.CodeTypeCollectionKind.Array : default, + }; private static string FixQueryParameterIdentifier(OpenApiParameter parameter) { diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 2a73363c08..38707d80ab 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -39,7 +39,7 @@ public override void Refine(CodeNamespace generatedCode) "Microsoft.Kiota.Abstractions" ); } - private static void DisambiguatePropertiesWithClassNames(CodeElement currentElement) { + protected static void DisambiguatePropertiesWithClassNames(CodeElement currentElement) { if(currentElement is CodeClass currentClass) { var sameNameProperty = currentClass.Properties .FirstOrDefault(x => x.Name.Equals(currentClass.Name, StringComparison.OrdinalIgnoreCase)); @@ -52,7 +52,7 @@ private static void DisambiguatePropertiesWithClassNames(CodeElement currentElem } CrawlTree(currentElement, DisambiguatePropertiesWithClassNames); } - private static void MakeEnumPropertiesNullable(CodeElement currentElement) { + protected static void MakeEnumPropertiesNullable(CodeElement currentElement) { if(currentElement is CodeClass currentClass && currentClass.IsOfKind(CodeClassKind.Model)) currentClass.Properties .Where(x => x.Type is CodeType propType && propType.TypeDefinition is CodeEnum) @@ -61,7 +61,7 @@ private static void MakeEnumPropertiesNullable(CodeElement currentElement) { CrawlTree(currentElement, MakeEnumPropertiesNullable); } - private static readonly AdditionalUsingEvaluator[] defaultUsingEvaluators = new AdditionalUsingEvaluator[] { + protected static readonly AdditionalUsingEvaluator[] defaultUsingEvaluators = new AdditionalUsingEvaluator[] { new (x => x is CodeProperty prop && prop.IsOfKind(CodePropertyKind.RequestAdapter), "Microsoft.Kiota.Abstractions", "IRequestAdapter"), new (x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.RequestGenerator), @@ -94,21 +94,21 @@ private static void MakeEnumPropertiesNullable(CodeElement currentElement) { new (x => x is CodeProperty prop && prop.IsOfKind(CodePropertyKind.BackingStore), "Microsoft.Kiota.Abstractions.Store", "IBackingStore", "IBackedModel", "BackingStoreFactorySingleton" ), }; - private static void CapitalizeNamespacesFirstLetters(CodeElement current) { + protected static void CapitalizeNamespacesFirstLetters(CodeElement current) { if(current is CodeNamespace currentNamespace) currentNamespace.Name = currentNamespace.Name?.Split('.')?.Select(x => x.ToFirstCharacterUpperCase())?.Aggregate((x, y) => $"{x}.{y}"); CrawlTree(current, CapitalizeNamespacesFirstLetters); } - private static void AddAsyncSuffix(CodeElement currentElement) { + protected static void AddAsyncSuffix(CodeElement currentElement) { if(currentElement is CodeMethod currentMethod && currentMethod.IsAsync) currentMethod.Name += "Async"; CrawlTree(currentElement, AddAsyncSuffix); } - private static void CorrectPropertyType(CodeProperty currentProperty) + protected static void CorrectPropertyType(CodeProperty currentProperty) { CorrectDateTypes(currentProperty.Parent as CodeClass, DateTypesReplacements, currentProperty.Type); } - private static void CorrectMethodType(CodeMethod currentMethod) + protected static void CorrectMethodType(CodeMethod currentMethod) { CorrectDateTypes(currentMethod.Parent as CodeClass, DateTypesReplacements, currentMethod.Parameters .Select(x => x.Type) diff --git a/src/Kiota.Builder/Refiners/ILanguageRefiner.cs b/src/Kiota.Builder/Refiners/ILanguageRefiner.cs index 04a0026878..e97583c905 100644 --- a/src/Kiota.Builder/Refiners/ILanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/ILanguageRefiner.cs @@ -1,4 +1,4 @@ -namespace Kiota.Builder.Refiners +namespace Kiota.Builder.Refiners { public interface ILanguageRefiner { @@ -24,6 +24,9 @@ public static void Refine(GenerationConfiguration config, CodeNamespace generate case GenerationLanguage.Go: new GoRefiner(config).Refine(generatedCode); break; + case GenerationLanguage.Shell: + new ShellRefiner(config).Refine(generatedCode); + break; default: break; //Do nothing } diff --git a/src/Kiota.Builder/Refiners/ShellRefiner.cs b/src/Kiota.Builder/Refiners/ShellRefiner.cs new file mode 100644 index 0000000000..4da99c323f --- /dev/null +++ b/src/Kiota.Builder/Refiners/ShellRefiner.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Kiota.Builder.Extensions; + +namespace Kiota.Builder.Refiners +{ + public class ShellRefiner : CSharpRefiner, ILanguageRefiner + { + public ShellRefiner(GenerationConfiguration configuration) : base(configuration) { } + public override void Refine(CodeNamespace generatedCode) + { + AddDefaultImports(generatedCode, defaultUsingEvaluators); + AddDefaultImports(generatedCode, additionalUsingEvaluators); + CorrectCoreType(generatedCode, CorrectMethodType, CorrectPropertyType); + MoveClassesWithNamespaceNamesUnderNamespace(generatedCode); + ConvertUnionTypesToWrapper(generatedCode, _configuration.UsesBackingStore); + AddPropertiesAndMethodTypesImports(generatedCode, false, false, false); + AddInnerClasses(generatedCode, false); + AddParsableInheritanceForModelClasses(generatedCode, "IParsable"); + CapitalizeNamespacesFirstLetters(generatedCode); + ReplaceBinaryByNativeType(generatedCode, "Stream", "System.IO"); + MakeEnumPropertiesNullable(generatedCode); + CreateCommandBuilders(generatedCode); + /* Exclude the following as their names will be capitalized making the change unnecessary in this case sensitive language + * code classes, class declarations, property names, using declarations, namespace names + * Exclude CodeMethod as the return type will also be capitalized (excluding the CodeType is not enough since this is evaluated at the code method level) + */ + ReplaceReservedNames( + generatedCode, + new CSharpReservedNamesProvider(), x => $"@{x.ToFirstCharacterUpperCase()}", + new HashSet { typeof(CodeClass), typeof(CodeClass.Declaration), typeof(CodeProperty), typeof(CodeUsing), typeof(CodeNamespace), typeof(CodeMethod) } + ); + DisambiguatePropertiesWithClassNames(generatedCode); + AddConstructorsForDefaultValues(generatedCode, false); + AddSerializationModulesImport(generatedCode); + } + + private static void CreateCommandBuilders(CodeElement currentElement) + { + if (currentElement is CodeClass currentClass && currentClass.IsOfKind(CodeClassKind.RequestBuilder)) + { + // Replace Nav Properties with BuildXXXCommand methods + var navProperties = currentClass.GetChildElements().OfType().Where(e => e.IsOfKind(CodePropertyKind.RequestBuilder)); + foreach (var navProp in navProperties) + { + var method = CreateBuildCommandMethod(navProp, currentClass); + currentClass.AddMethod(method); + currentClass.RemoveChildElement(navProp); + } + + // Build command for indexers + var indexers = currentClass.GetChildElements().OfType(); + var classHasIndexers = indexers.Any(); + CreateCommandBuildersFromIndexers(currentClass, indexers); + + // Clone executors & convert to build command + var requestExecutors = currentClass.GetChildElements().OfType().Where(e => e.IsOfKind(CodeMethodKind.RequestExecutor)); + CreateCommandBuildersFromRequestExecutors(currentClass, classHasIndexers, requestExecutors); + + // Build root command + var clientConstructor = currentClass.GetChildElements().OfType().FirstOrDefault(m => m.IsOfKind(CodeMethodKind.ClientConstructor)); + if (clientConstructor != null) + { + var rootMethod = new CodeMethod + { + Name = "BuildRootCommand", + Description = clientConstructor.Description, + IsAsync = false, + MethodKind = CodeMethodKind.CommandBuilder, + ReturnType = new CodeType { Name = "Command", IsExternal = true }, + OriginalMethod = clientConstructor, + }; + currentClass.AddMethod(rootMethod); + } + } + CrawlTree(currentElement, CreateCommandBuilders); + } + + private static void CreateCommandBuildersFromRequestExecutors(CodeClass currentClass, bool classHasIndexers, IEnumerable requestMethods) + { + foreach (var requestMethod in requestMethods) + { + CodeMethod clone = requestMethod.Clone() as CodeMethod; + var cmdName = clone.HttpMethod switch + { + HttpMethod.Get when classHasIndexers => "List", + HttpMethod.Post when classHasIndexers => "Create", + _ => clone.Name, + }; + + clone.IsAsync = false; + clone.Name = $"Build{cmdName}Command"; + clone.Description = requestMethod.Description; + clone.ReturnType = CreateCommandType(); + clone.MethodKind = CodeMethodKind.CommandBuilder; + clone.OriginalMethod = requestMethod; + clone.SimpleName = cmdName; + clone.ClearParameters(); + currentClass.AddMethod(clone); + currentClass.RemoveChildElement(requestMethod); + } + } + + private static void CreateCommandBuildersFromIndexers(CodeClass currentClass, IEnumerable indexers) + { + foreach (var indexer in indexers) + { + var method = new CodeMethod + { + Name = "BuildCommand", + IsAsync = false, + MethodKind = CodeMethodKind.CommandBuilder, + OriginalIndexer = indexer + }; + + // ReturnType setter assigns the parent + method.ReturnType = CreateCommandType(); + method.ReturnType.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Complex; + currentClass.AddMethod(method); + currentClass.RemoveChildElement(indexer); + } + } + + private static CodeType CreateCommandType() + { + return new CodeType + { + Name = "Command", + IsExternal = true, + }; + } + + private static CodeMethod CreateBuildCommandMethod(CodeProperty navProperty, CodeClass parent) + { + var codeMethod = new CodeMethod + { + IsAsync = false, + Name = $"Build{navProperty.Name.ToFirstCharacterUpperCase()}Command", + MethodKind = CodeMethodKind.CommandBuilder + }; + codeMethod.ReturnType = CreateCommandType(); + codeMethod.AccessedProperty = navProperty; + codeMethod.SimpleName = navProperty.Name; + codeMethod.Parent = parent; + return codeMethod; + } + + private static readonly AdditionalUsingEvaluator[] additionalUsingEvaluators = new AdditionalUsingEvaluator[] { + new (x => x is CodeClass @class && @class.IsOfKind(CodeClassKind.RequestBuilder), + "System.CommandLine", "Command", "RootCommand", "IConsole"), + new (x => x is CodeClass @class && @class.IsOfKind(CodeClassKind.RequestBuilder), + "Microsoft.Kiota.Cli.Commons.IO", "IOutputFormatter", "IOutputFormatterFactory", "FormatterType"), + new (x => x is CodeClass @class && @class.IsOfKind(CodeClassKind.RequestBuilder), + "System.Text", "Encoding"), + }; + } +} diff --git a/src/Kiota.Builder/Writers/CSharp/CSharpWriter.cs b/src/Kiota.Builder/Writers/CSharp/CSharpWriter.cs index d37bc33762..ea5e72aebc 100644 --- a/src/Kiota.Builder/Writers/CSharp/CSharpWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CSharpWriter.cs @@ -6,13 +6,13 @@ public CSharpWriter(string rootPath, string clientNamespaceName) { PathSegmenter = new CSharpPathSegmenter(rootPath, clientNamespaceName); var conventionService = new CSharpConventionService(); - AddCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); - AddCodeElementWriter(new CodeClassEndWriter(conventionService)); - AddCodeElementWriter(new CodeEnumWriter(conventionService)); - AddCodeElementWriter(new CodeIndexerWriter(conventionService)); - AddCodeElementWriter(new CodeMethodWriter(conventionService)); - AddCodeElementWriter(new CodePropertyWriter(conventionService)); - AddCodeElementWriter(new CodeTypeWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassEndWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeEnumWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeIndexerWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeTypeWriter(conventionService)); } } diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 668c0862dd..a4933014d8 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -6,13 +6,13 @@ namespace Kiota.Builder.Writers.CSharp; public class CodeMethodWriter : BaseElementWriter { - public CodeMethodWriter(CSharpConventionService conventionService): base(conventionService) { } + public CodeMethodWriter(CSharpConventionService conventionService) : base(conventionService) { } public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter writer) { - if(codeElement == null) throw new ArgumentNullException(nameof(codeElement)); - if(codeElement.ReturnType == null) throw new InvalidOperationException($"{nameof(codeElement.ReturnType)} should not be null"); - if(writer == null) throw new ArgumentNullException(nameof(writer)); - if(!(codeElement.Parent is CodeClass)) throw new InvalidOperationException("the parent of a method should be a class"); + if (codeElement == null) throw new ArgumentNullException(nameof(codeElement)); + if (codeElement.ReturnType == null) throw new InvalidOperationException($"{nameof(codeElement.ReturnType)} should not be null"); + if (writer == null) throw new ArgumentNullException(nameof(writer)); + if (!(codeElement.Parent is CodeClass)) throw new InvalidOperationException("the parent of a method should be a class"); var returnType = conventions.GetTypeString(codeElement.ReturnType, codeElement); var parentClass = codeElement.Parent as CodeClass; @@ -21,22 +21,31 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri WriteMethodDocumentation(codeElement, writer); WriteMethodPrototype(codeElement, writer, returnType, inherits, isVoid); writer.IncreaseIndent(); - var requestBodyParam = codeElement.Parameters.OfKind(CodeParameterKind.RequestBody); - var queryStringParam = codeElement.Parameters.OfKind(CodeParameterKind.QueryParameter); - var headersParam = codeElement.Parameters.OfKind(CodeParameterKind.Headers); - var optionsParam = codeElement.Parameters.OfKind(CodeParameterKind.Options); - var requestParams = new RequestParams(requestBodyParam, queryStringParam, headersParam, optionsParam); - foreach(var parameter in codeElement.Parameters.Where(x => !x.Optional).OrderBy(x => x.Name)) { + foreach (var parameter in codeElement.Parameters.Where(x => !x.Optional).OrderBy(x => x.Name)) + { var parameterName = parameter.Name.ToFirstCharacterLowerCase(); - if(nameof(String).Equals(parameter.Type.Name, StringComparison.OrdinalIgnoreCase)) + if (nameof(String).Equals(parameter.Type.Name, StringComparison.OrdinalIgnoreCase)) writer.WriteLine($"if(string.IsNullOrEmpty({parameterName})) throw new ArgumentNullException(nameof({parameterName}));"); else writer.WriteLine($"_ = {parameterName} ?? throw new ArgumentNullException(nameof({parameterName}));"); } - switch(codeElement.MethodKind) { + HandleMethodKind(codeElement, writer, inherits, parentClass, isVoid); + writer.CloseBlock(); + } + + protected virtual void HandleMethodKind(CodeMethod codeElement, LanguageWriter writer, bool inherits, CodeClass parentClass, bool isVoid) + { + var returnType = conventions.GetTypeString(codeElement.ReturnType, codeElement); + var requestBodyParam = codeElement.Parameters.OfKind(CodeParameterKind.RequestBody); + var queryStringParam = codeElement.Parameters.OfKind(CodeParameterKind.QueryParameter); + var headersParam = codeElement.Parameters.OfKind(CodeParameterKind.Headers); + var optionsParam = codeElement.Parameters.OfKind(CodeParameterKind.Options); + var requestParams = new RequestParams(requestBodyParam, queryStringParam, headersParam, optionsParam); + switch (codeElement.MethodKind) + { case CodeMethodKind.Serializer: WriteSerializerBody(inherits, codeElement, parentClass, writer); - break; + break; case CodeMethodKind.RequestGenerator: WriteRequestGeneratorBody(codeElement, requestParams, parentClass, writer); break; @@ -62,45 +71,55 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri throw new InvalidOperationException("getters and setters are automatically added on fields in dotnet"); case CodeMethodKind.RequestBuilderBackwardCompatibility: throw new InvalidOperationException("RequestBuilderBackwardCompatibility is not supported as the request builders are implemented by properties."); + case CodeMethodKind.CommandBuilder: + var origParams = codeElement.OriginalMethod?.Parameters ?? codeElement.Parameters; + requestBodyParam = origParams.OfKind(CodeParameterKind.RequestBody); + requestParams = new RequestParams(requestBodyParam, null, null, null); + WriteCommandBuilderBody(codeElement, requestParams, isVoid, returnType, writer); + break; default: writer.WriteLine("return null;"); - break; + break; } - writer.DecreaseIndent(); - writer.WriteLine("}"); } private void WriteRequestBuilderBody(CodeClass parentClass, CodeMethod codeElement, LanguageWriter writer) { var importSymbol = conventions.GetTypeString(codeElement.ReturnType, parentClass); conventions.AddRequestBuilderBody(parentClass, importSymbol, writer, prefix: "return ", pathParameters: codeElement.Parameters.Where(x => x.IsOfKind(CodeParameterKind.Path))); } - private static void WriteApiConstructorBody(CodeClass parentClass, CodeMethod method, LanguageWriter writer) { + private static void WriteApiConstructorBody(CodeClass parentClass, CodeMethod method, LanguageWriter writer) + { var requestAdapterProperty = parentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter); var backingStoreParameter = method.Parameters.FirstOrDefault(x => x.IsOfKind(CodeParameterKind.BackingStore)); var requestAdapterPropertyName = requestAdapterProperty.Name.ToFirstCharacterUpperCase(); WriteSerializationRegistration(method.SerializerModules, writer, "RegisterDefaultSerializer"); WriteSerializationRegistration(method.DeserializerModules, writer, "RegisterDefaultDeserializer"); writer.WriteLine($"{requestAdapterPropertyName}.BaseUrl = \"{method.BaseUrl}\";"); - if(backingStoreParameter != null) + if (backingStoreParameter != null) writer.WriteLine($"{requestAdapterPropertyName}.EnableBackingStore({backingStoreParameter.Name});"); } - private static void WriteSerializationRegistration(List serializationClassNames, LanguageWriter writer, string methodName) { - if(serializationClassNames != null) - foreach(var serializationClassName in serializationClassNames) + private static void WriteSerializationRegistration(List serializationClassNames, LanguageWriter writer, string methodName) + { + if (serializationClassNames != null) + foreach (var serializationClassName in serializationClassNames) writer.WriteLine($"ApiClientBuilder.{methodName}<{serializationClassName}>();"); } - private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer) { - foreach(var propWithDefault in parentClass + private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer) + { + foreach (var propWithDefault in parentClass .Properties .Where(x => !string.IsNullOrEmpty(x.DefaultValue)) .OrderByDescending(x => x.PropertyKind) - .ThenBy(x => x.Name)) { + .ThenBy(x => x.Name)) + { writer.WriteLine($"{propWithDefault.Name.ToFirstCharacterUpperCase()} = {propWithDefault.DefaultValue};"); } - if(parentClass.IsOfKind(CodeClassKind.RequestBuilder)) { - if(currentMethod.IsOfKind(CodeMethodKind.Constructor)) { + if (parentClass.IsOfKind(CodeClassKind.RequestBuilder)) + { + if (currentMethod.IsOfKind(CodeMethodKind.Constructor)) + { var pathParametersParam = currentMethod.Parameters.FirstOrDefault(x => x.IsOfKind(CodeParameterKind.PathParameters)); - conventions.AddParametersAssignment(writer, + conventions.AddParametersAssignment(writer, pathParametersParam.Type, pathParametersParam.Name.ToFirstCharacterLowerCase(), currentMethod.Parameters @@ -109,7 +128,8 @@ private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMetho .ToArray()); AssignPropertyFromParameter(parentClass, currentMethod, CodeParameterKind.PathParameters, CodePropertyKind.PathParameters, writer, conventions.TempDictionaryVarName); } - else if(currentMethod.IsOfKind(CodeMethodKind.RawUrlConstructor)) { + else if (currentMethod.IsOfKind(CodeMethodKind.RawUrlConstructor)) + { var pathParametersProp = parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters); var rawUrlParam = currentMethod.Parameters.FirstOrDefault(x => x.IsOfKind(CodeParameterKind.RawUrl)); conventions.AddParametersAssignment(writer, @@ -121,35 +141,41 @@ private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMetho AssignPropertyFromParameter(parentClass, currentMethod, CodeParameterKind.RequestAdapter, CodePropertyKind.RequestAdapter, writer); } } - private static void AssignPropertyFromParameter(CodeClass parentClass, CodeMethod currentMethod, CodeParameterKind parameterKind, CodePropertyKind propertyKind, LanguageWriter writer, string variableName = default) { + private static void AssignPropertyFromParameter(CodeClass parentClass, CodeMethod currentMethod, CodeParameterKind parameterKind, CodePropertyKind propertyKind, LanguageWriter writer, string variableName = default) + { var property = parentClass.GetPropertyOfKind(propertyKind); - if(property != null) { + if (property != null) + { var parameter = currentMethod.Parameters.FirstOrDefault(x => x.IsOfKind(parameterKind)); - if(!string.IsNullOrEmpty(variableName)) + if (!string.IsNullOrEmpty(variableName)) writer.WriteLine($"{property.Name.ToFirstCharacterUpperCase()} = {variableName};"); - else if(parameter != null) + else if (parameter != null) writer.WriteLine($"{property.Name.ToFirstCharacterUpperCase()} = {parameter.Name};"); } } - private void WriteDeserializerBody(bool shouldHide, CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { + private void WriteDeserializerBody(bool shouldHide, CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { var parentSerializationInfo = shouldHide ? $"(base.{codeElement.Name.ToFirstCharacterUpperCase()}())" : string.Empty; writer.WriteLine($"return new Dictionary>{parentSerializationInfo} {{"); writer.IncreaseIndent(); - foreach(var otherProp in parentClass + foreach (var otherProp in parentClass .Properties .Where(x => x.IsOfKind(CodePropertyKind.Custom)) - .OrderBy(x => x.Name)) { + .OrderBy(x => x.Name)) + { writer.WriteLine($"{{\"{otherProp.SerializationName ?? otherProp.Name.ToFirstCharacterLowerCase()}\", (o,n) => {{ (o as {parentClass.Name.ToFirstCharacterUpperCase()}).{otherProp.Name.ToFirstCharacterUpperCase()} = n.{GetDeserializationMethodName(otherProp.Type, codeElement)}(); }} }},"); } writer.DecreaseIndent(); writer.WriteLine("};"); } - private string GetDeserializationMethodName(CodeTypeBase propType, CodeMethod method) { + private string GetDeserializationMethodName(CodeTypeBase propType, CodeMethod method) + { var isCollection = propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None; var propertyType = conventions.GetTypeString(propType, method, false); - if(propType is CodeType currentType) { - if(isCollection) - if(currentType.TypeDefinition == null) + if (propType is CodeType currentType) + { + if (isCollection) + if (currentType.TypeDefinition == null) return $"GetCollectionOfPrimitiveValues<{propertyType}>().ToList"; else if (currentType.TypeDefinition is CodeEnum enumType) return $"GetCollectionOfEnumValues<{enumType.Name.ToFirstCharacterUpperCase()}>().ToList"; @@ -165,23 +191,26 @@ _ when conventions.IsPrimitiveType(propertyType) => $"Get{propertyType.TrimEnd(C _ => $"GetObjectValue<{propertyType.ToFirstCharacterUpperCase()}>", }; } - private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requestParams, bool isVoid, string returnType, LanguageWriter writer) { - if(codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); - + protected void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requestParams, bool isVoid, string returnType, LanguageWriter writer) + { + if (codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); + var isStream = conventions.StreamTypeName.Equals(returnType, StringComparison.OrdinalIgnoreCase); var generatorMethodName = (codeElement.Parent as CodeClass) .Methods .FirstOrDefault(x => x.IsOfKind(CodeMethodKind.RequestGenerator) && x.HttpMethod == codeElement.HttpMethod) ?.Name; var parametersList = new CodeParameter[] { requestParams.requestBody, requestParams.queryString, requestParams.headers, requestParams.options } - .Select(x => x?.Name).Where(x => x != null).Aggregate((x,y) => $"{x}, {y}"); + .Select(x => x?.Name).Where(x => x != null).Aggregate((x, y) => $"{x}, {y}"); writer.WriteLine($"var requestInfo = {generatorMethodName}({parametersList});"); var errorMappingVarName = "default"; - if(codeElement.ErrorMappings.Any()) { + if (codeElement.ErrorMappings.Any()) + { errorMappingVarName = "errorMapping"; writer.WriteLine($"var {errorMappingVarName} = new Dictionary> {{"); writer.IncreaseIndent(); - foreach(var errorMapping in codeElement.ErrorMappings) { + foreach (var errorMapping in codeElement.ErrorMappings) + { writer.WriteLine($"{{\"{errorMapping.Key.ToUpperInvariant()}\", () => new {errorMapping.Value.Name.ToFirstCharacterUpperCase()}()}},"); } writer.CloseBlock("};"); @@ -189,9 +218,10 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ writer.WriteLine($"{(isVoid ? string.Empty : "return ")}await RequestAdapter.{GetSendRequestMethodName(isVoid, isStream, codeElement.ReturnType.IsCollection, returnType)}(requestInfo, responseHandler, {errorMappingVarName}, cancellationToken);"); } private const string RequestInfoVarName = "requestInfo"; - private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass currentClass, LanguageWriter writer) { - if(codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); - + private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass currentClass, LanguageWriter writer) + { + if (codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); + var operationName = codeElement.HttpMethod.ToString(); var urlTemplateParamsProperty = currentClass.GetPropertyOfKind(CodePropertyKind.PathParameters); var urlTemplateProperty = currentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate); @@ -203,13 +233,15 @@ private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams req $"PathParameters = {GetPropertyCall(urlTemplateParamsProperty, "string.Empty")},"); writer.DecreaseIndent(); writer.WriteLine("};"); - if(requestParams.requestBody != null) { - if(requestParams.requestBody.Type.Name.Equals(conventions.StreamTypeName, StringComparison.OrdinalIgnoreCase)) + if (requestParams.requestBody != null) + { + if (requestParams.requestBody.Type.Name.Equals(conventions.StreamTypeName, StringComparison.OrdinalIgnoreCase)) writer.WriteLine($"{RequestInfoVarName}.SetStreamContent({requestParams.requestBody.Name});"); else writer.WriteLine($"{RequestInfoVarName}.SetContentFromParsable({requestAdapterProperty.Name.ToFirstCharacterUpperCase()}, \"{codeElement.ContentType}\", {requestParams.requestBody.Name});"); } - if(requestParams.queryString != null) { + if (requestParams.queryString != null) + { writer.WriteLine($"if ({requestParams.queryString.Name} != null) {{"); writer.IncreaseIndent(); writer.WriteLines($"var qParams = new {operationName?.ToFirstCharacterUpperCase()}QueryParameters();", @@ -218,84 +250,98 @@ private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams req writer.DecreaseIndent(); writer.WriteLine("}"); } - if(requestParams.headers != null) + if (requestParams.headers != null) writer.WriteLine($"{requestParams.headers.Name}?.Invoke({RequestInfoVarName}.Headers);"); - if(requestParams.options != null) + if (requestParams.options != null) writer.WriteLine($"{RequestInfoVarName}.AddRequestOptions({requestParams.options.Name}?.ToArray());"); writer.WriteLine($"return {RequestInfoVarName};"); } private static string GetPropertyCall(CodeProperty property, string defaultValue) => property == null ? defaultValue : $"{property.Name.ToFirstCharacterUpperCase()}"; - private void WriteSerializerBody(bool shouldHide, CodeMethod method, CodeClass parentClass, LanguageWriter writer) { + private void WriteSerializerBody(bool shouldHide, CodeMethod method, CodeClass parentClass, LanguageWriter writer) + { var additionalDataProperty = parentClass.GetPropertyOfKind(CodePropertyKind.AdditionalData); - if(shouldHide) + if (shouldHide) writer.WriteLine("base.Serialize(writer);"); - foreach(var otherProp in parentClass + foreach (var otherProp in parentClass .Properties .Where(x => x.IsOfKind(CodePropertyKind.Custom)) - .OrderBy(x => x.Name)) { + .OrderBy(x => x.Name)) + { writer.WriteLine($"writer.{GetSerializationMethodName(otherProp.Type, method)}(\"{otherProp.SerializationName ?? otherProp.Name.ToFirstCharacterLowerCase()}\", {otherProp.Name.ToFirstCharacterUpperCase()});"); } - if(additionalDataProperty != null) + if (additionalDataProperty != null) writer.WriteLine($"writer.WriteAdditionalData({additionalDataProperty.Name});"); } - private string GetSendRequestMethodName(bool isVoid, bool isStream, bool isCollection, string returnType) { - if(isVoid) return "SendNoContentAsync"; - else if(isStream || conventions.IsPrimitiveType(returnType)) - if(isCollection) + + protected virtual void WriteCommandBuilderBody(CodeMethod codeElement, RequestParams requestParams, bool isVoid, string returnType, LanguageWriter writer) + { + throw new InvalidOperationException("CommandBuilder methods are not implemented in this SDK. They're currently only supported in the shell language."); + } + + protected string GetSendRequestMethodName(bool isVoid, bool isStream, bool isCollection, string returnType) + { + if (isVoid) return "SendNoContentAsync"; + else if (isStream || conventions.IsPrimitiveType(returnType)) + if (isCollection) return $"SendPrimitiveCollectionAsync<{returnType.StripArraySuffix()}>"; else return $"SendPrimitiveAsync<{returnType}>"; else if (isCollection) return $"SendCollectionAsync<{returnType.StripArraySuffix()}>"; else return $"SendAsync<{returnType}>"; } - private void WriteMethodDocumentation(CodeMethod code, LanguageWriter writer) { + private void WriteMethodDocumentation(CodeMethod code, LanguageWriter writer) + { var isDescriptionPresent = !string.IsNullOrEmpty(code.Description); var parametersWithDescription = code.Parameters.Where(x => !string.IsNullOrEmpty(code.Description)); - if (isDescriptionPresent || parametersWithDescription.Any()) { + if (isDescriptionPresent || parametersWithDescription.Any()) + { writer.WriteLine($"{conventions.DocCommentPrefix}"); - if(isDescriptionPresent) + if (isDescriptionPresent) writer.WriteLine($"{conventions.DocCommentPrefix}{code.Description}"); - foreach(var paramWithDescription in parametersWithDescription.OrderBy(x => x.Name)) + foreach (var paramWithDescription in parametersWithDescription.OrderBy(x => x.Name)) writer.WriteLine($"{conventions.DocCommentPrefix}{paramWithDescription.Description}"); writer.WriteLine($"{conventions.DocCommentPrefix}"); } } private static readonly CodeParameterOrderComparer parameterOrderComparer = new(); - private void WriteMethodPrototype(CodeMethod code, LanguageWriter writer, string returnType, bool inherits, bool isVoid) { + private void WriteMethodPrototype(CodeMethod code, LanguageWriter writer, string returnType, bool inherits, bool isVoid) + { var staticModifier = code.IsStatic ? "static " : string.Empty; var hideModifier = inherits && code.IsSerializationMethod ? "new " : string.Empty; var genericTypePrefix = isVoid ? string.Empty : "<"; - var genericTypeSuffix = code.IsAsync && !isVoid ? ">": string.Empty; + var genericTypeSuffix = code.IsAsync && !isVoid ? ">" : string.Empty; var isConstructor = code.IsOfKind(CodeMethodKind.Constructor, CodeMethodKind.ClientConstructor, CodeMethodKind.RawUrlConstructor); var asyncPrefix = code.IsAsync ? "async Task" + genericTypePrefix : string.Empty; var voidCorrectedTaskReturnType = code.IsAsync && isVoid ? string.Empty : returnType; - if(code.ReturnType.IsArray && code.IsOfKind(CodeMethodKind.RequestExecutor)) + if (code.ReturnType.IsArray && code.IsOfKind(CodeMethodKind.RequestExecutor)) voidCorrectedTaskReturnType = $"IEnumerable<{voidCorrectedTaskReturnType.StripArraySuffix()}>"; // TODO: Task type should be moved into the refiner var completeReturnType = isConstructor ? string.Empty : $"{asyncPrefix}{voidCorrectedTaskReturnType}{genericTypeSuffix} "; var baseSuffix = string.Empty; - if(isConstructor && inherits) + if (isConstructor && inherits) baseSuffix = " : base()"; - var parameters = string.Join(", ", code.Parameters.OrderBy(x => x, parameterOrderComparer).Select(p=> conventions.GetParameterSignature(p, code)).ToList()); + var parameters = string.Join(", ", code.Parameters.OrderBy(x => x, parameterOrderComparer).Select(p => conventions.GetParameterSignature(p, code)).ToList()); var methodName = isConstructor ? code.Parent.Name.ToFirstCharacterUpperCase() : code.Name.ToFirstCharacterUpperCase(); writer.WriteLine($"{conventions.GetAccessModifier(code.Access)} {staticModifier}{hideModifier}{completeReturnType}{methodName}({parameters}){baseSuffix} {{"); } - private string GetSerializationMethodName(CodeTypeBase propType, CodeMethod method) { + private string GetSerializationMethodName(CodeTypeBase propType, CodeMethod method) + { var isCollection = propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None; var propertyType = conventions.GetTypeString(propType, method, false); - if(propType is CodeType currentType) { - if(isCollection) - if(currentType.TypeDefinition == null) + if (propType is CodeType currentType) + { + if (isCollection) + if (currentType.TypeDefinition == null) return $"WriteCollectionOfPrimitiveValues<{propertyType}>"; - else if(currentType.TypeDefinition is CodeEnum enumType) + else if (currentType.TypeDefinition is CodeEnum enumType) return $"WriteCollectionOfEnumValues<{enumType.Name.ToFirstCharacterUpperCase()}>"; else return $"WriteCollectionOfObjectValues<{propertyType}>"; else if (currentType.TypeDefinition is CodeEnum enumType) return $"WriteEnumValue<{enumType.Name.ToFirstCharacterUpperCase()}>"; - + } return propertyType switch { diff --git a/src/Kiota.Builder/Writers/Go/GoWriter.cs b/src/Kiota.Builder/Writers/Go/GoWriter.cs index dd90d6b4c8..185efd13cd 100644 --- a/src/Kiota.Builder/Writers/Go/GoWriter.cs +++ b/src/Kiota.Builder/Writers/Go/GoWriter.cs @@ -4,11 +4,11 @@ public GoWriter(string rootPath, string clientNamespaceName) { PathSegmenter = new GoPathSegmenter(rootPath, clientNamespaceName); var conventionService = new GoConventionService(); - AddCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); - AddCodeElementWriter(new CodeClassEndWriter()); - AddCodeElementWriter(new CodePropertyWriter(conventionService)); - AddCodeElementWriter(new CodeEnumWriter(conventionService)); - AddCodeElementWriter(new CodeMethodWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassEndWriter()); + AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeEnumWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService)); } } } diff --git a/src/Kiota.Builder/Writers/Java/JavaWriter.cs b/src/Kiota.Builder/Writers/Java/JavaWriter.cs index b34b5d0441..537a2af81c 100644 --- a/src/Kiota.Builder/Writers/Java/JavaWriter.cs +++ b/src/Kiota.Builder/Writers/Java/JavaWriter.cs @@ -6,12 +6,12 @@ public JavaWriter(string rootPath, string clientNamespaceName) { PathSegmenter = new JavaPathSegmenter(rootPath, clientNamespaceName); var conventionService = new JavaConventionService(); - AddCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); - AddCodeElementWriter(new CodeClassEndWriter()); - AddCodeElementWriter(new CodeEnumWriter(conventionService)); - AddCodeElementWriter(new CodeMethodWriter(conventionService)); - AddCodeElementWriter(new CodePropertyWriter(conventionService)); - AddCodeElementWriter(new CodeTypeWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassEndWriter()); + AddOrReplaceCodeElementWriter(new CodeEnumWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeTypeWriter(conventionService)); } } } diff --git a/src/Kiota.Builder/Writers/LanguageWriter.cs b/src/Kiota.Builder/Writers/LanguageWriter.cs index c1dbda7265..34df4c970d 100644 --- a/src/Kiota.Builder/Writers/LanguageWriter.cs +++ b/src/Kiota.Builder/Writers/LanguageWriter.cs @@ -7,6 +7,7 @@ using Kiota.Builder.Writers.Go; using Kiota.Builder.Writers.Java; using Kiota.Builder.Writers.Ruby; +using Kiota.Builder.Writers.Shell; using Kiota.Builder.Writers.TypeScript; using Kiota.Builder.Writers.Php; @@ -111,8 +112,9 @@ public void Write(T code) where T : CodeElement else if(!(code is CodeClass) && !(code is CodeNamespace.BlockDeclaration) && !(code is CodeNamespace.BlockEnd)) throw new InvalidOperationException($"Dispatcher missing for type {code.GetType()}"); } - protected void AddCodeElementWriter(ICodeElementWriter writer) where T: CodeElement { - Writers.Add(typeof(T), writer); + protected void AddOrReplaceCodeElementWriter(ICodeElementWriter writer) where T: CodeElement { + if (!Writers.TryAdd(typeof(T), writer)) + Writers[typeof(T)] = writer; } private readonly Dictionary Writers = new(); // we have to type as object because dotnet doesn't have type capture i.e eq for `? extends CodeElement` public static LanguageWriter GetLanguageWriter(GenerationLanguage language, string outputPath, string clientNamespaceName) { @@ -124,6 +126,7 @@ public static LanguageWriter GetLanguageWriter(GenerationLanguage language, stri GenerationLanguage.Ruby => new RubyWriter(outputPath, clientNamespaceName), GenerationLanguage.PHP => new PhpWriter(outputPath, clientNamespaceName), GenerationLanguage.Go => new GoWriter(outputPath, clientNamespaceName), + GenerationLanguage.Shell => new ShellWriter(outputPath, clientNamespaceName), _ => throw new InvalidEnumArgumentException($"{language} language currently not supported."), }; } diff --git a/src/Kiota.Builder/Writers/Php/PhpWriter.cs b/src/Kiota.Builder/Writers/Php/PhpWriter.cs index eabdd71465..5e241dff8f 100644 --- a/src/Kiota.Builder/Writers/Php/PhpWriter.cs +++ b/src/Kiota.Builder/Writers/Php/PhpWriter.cs @@ -6,11 +6,11 @@ public PhpWriter(string rootPath, string clientNamespaceName, bool useBackingSto { PathSegmenter = new PhpPathSegmenter(rootPath, clientNamespaceName); var conventionService = new PhpConventionService(); - AddCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); - AddCodeElementWriter(new CodePropertyWriter(conventionService)); - AddCodeElementWriter(new CodeMethodWriter(conventionService)); - AddCodeElementWriter(new CodeClassEndWriter()); - AddCodeElementWriter(new CodeEnumWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassEndWriter()); + AddOrReplaceCodeElementWriter(new CodeEnumWriter(conventionService)); } } } diff --git a/src/Kiota.Builder/Writers/Ruby/RubyWriter.cs b/src/Kiota.Builder/Writers/Ruby/RubyWriter.cs index 32a7ee8acc..16ebc32a32 100644 --- a/src/Kiota.Builder/Writers/Ruby/RubyWriter.cs +++ b/src/Kiota.Builder/Writers/Ruby/RubyWriter.cs @@ -6,12 +6,12 @@ public RubyWriter(string rootPath, string clientNamespaceName) { PathSegmenter = new RubyPathSegmenter(rootPath, clientNamespaceName); var conventionService = new RubyConventionService(); - AddCodeElementWriter(new CodeClassDeclarationWriter(conventionService, clientNamespaceName)); - AddCodeElementWriter(new CodeClassEndWriter(conventionService)); - AddCodeElementWriter(new CodeNamespaceWriter(conventionService)); - AddCodeElementWriter(new CodeEnumWriter(conventionService)); - AddCodeElementWriter(new CodeMethodWriter(conventionService)); - AddCodeElementWriter(new CodePropertyWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService, clientNamespaceName)); + AddOrReplaceCodeElementWriter(new CodeClassEndWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeNamespaceWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeEnumWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService)); } } } diff --git a/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs b/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs new file mode 100644 index 0000000000..3fb24e70e0 --- /dev/null +++ b/src/Kiota.Builder/Writers/Shell/ShellCodeMethodWriter.cs @@ -0,0 +1,441 @@ +using System; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Kiota.Builder.Writers.CSharp; +using Kiota.Builder.Extensions; +using System.Collections.Generic; + +namespace Kiota.Builder.Writers.Shell +{ + class ShellCodeMethodWriter : CodeMethodWriter + { + private static Regex delimitedRegex = new Regex("(?<=[a-z])[-_\\.]+([A-Za-z])", RegexOptions.Compiled); + private static Regex camelCaseRegex = new Regex("(?<=[a-z])([A-Z])", RegexOptions.Compiled); + private static Regex identifierRegex = new Regex("(?:[-_\\.]([a-zA-Z]))", RegexOptions.Compiled); + private static Regex uppercaseRegex = new Regex("([A-Z])", RegexOptions.Compiled); + private const string cancellationTokenParamType = "CancellationToken"; + private const string cancellationTokenParamName = "cancellationToken"; + private const string consoleParamType = "IConsole"; + private const string consoleParamName = "console"; + private const string fileParamType = "FileInfo"; + private const string fileParamName = "file"; + private const string outputFormatParamType = "FormatterType"; + private const string outputFormatParamName = "output"; + private const string outputFormatterFactoryParamType = "IOutputFormatterFactory"; + private const string outputFormatterFactoryParamName = "outputFormatterFactory"; + + public ShellCodeMethodWriter(CSharpConventionService conventionService) : base(conventionService) + { + } + + protected override void WriteCommandBuilderBody(CodeMethod codeElement, RequestParams requestParams, bool isVoid, string returnType, LanguageWriter writer) + { + var parent = codeElement.Parent as CodeClass; + var classMethods = parent.Methods; + var name = codeElement.SimpleName; + name = uppercaseRegex.Replace(name, "-$1").TrimStart('-').ToLower(); + + if (codeElement.HttpMethod == null) + { + // Build method + // Puts together the BuildXXCommand objects. Needs a nav property name e.g. users + // Command("users") -> Command("get") + if (string.IsNullOrWhiteSpace(name)) + { + // BuildCommand function + WriteUnnamedBuildCommand(codeElement, writer, parent, classMethods); + } + else + { + WriteContainerCommand(codeElement, writer, parent, name); + } + } else + { + WriteExecutableCommand(codeElement, requestParams, writer, name); + } + } + + private void WriteExecutableCommand(CodeMethod codeElement, RequestParams requestParams, LanguageWriter writer, string name) + { + var generatorMethod = (codeElement.Parent as CodeClass) + .Methods + .FirstOrDefault(x => x.IsOfKind(CodeMethodKind.RequestGenerator) && x.HttpMethod == codeElement.HttpMethod); + var pathAndQueryParams = generatorMethod.PathAndQueryParameters; + var originalMethod = codeElement.OriginalMethod; + var origParams = originalMethod.Parameters; + var parametersList = pathAndQueryParams?.Where(p => !string.IsNullOrWhiteSpace(p.Name))?.ToList() ?? new List(); + if (origParams.Any(p => p.IsOfKind(CodeParameterKind.RequestBody))) + { + parametersList.Add(origParams.OfKind(CodeParameterKind.RequestBody)); + } + writer.WriteLine($"var command = new Command(\"{name}\");"); + WriteCommandDescription(codeElement, writer); + writer.WriteLine("// Create options for all the parameters"); + // investigate exploding query params + // Check the possible formatting options for headers in a cli. + // -h A=b -h + // -h A:B,B:C + // -h {"A": "B"} + var availableOptions = WriteExecutableCommandOptions(writer, parametersList); + + var paramTypes = parametersList.Select(x => + { + var codeType = x.Type as CodeType; + if (x.IsOfKind(CodeParameterKind.RequestBody) && codeType.TypeDefinition is CodeClass) + { + return "string"; + } + else if (conventions.StreamTypeName.Equals(x.Type?.Name, StringComparison.OrdinalIgnoreCase)) + { + return "FileInfo"; + } + + return conventions.GetTypeString(x.Type, x); + }).ToList(); + var paramNames = parametersList.Select(x => NormalizeToIdentifier(x.Name)).ToList(); + var isHandlerVoid = conventions.VoidTypeName.Equals(originalMethod.ReturnType.Name, StringComparison.OrdinalIgnoreCase); + var returnType = conventions.GetTypeString(originalMethod.ReturnType, originalMethod); + if (conventions.StreamTypeName.Equals(returnType, StringComparison.OrdinalIgnoreCase)) + { + var fileOptionName = "fileOption"; + writer.WriteLine($"var {fileOptionName} = new Option<{fileParamType}>(\"--{fileParamName}\");"); + writer.WriteLine($"command.AddOption({fileOptionName});"); + paramTypes.Add(fileParamType); + paramNames.Add(fileParamName); + availableOptions.Add(fileOptionName); + } + + // Add output type param + if (!isHandlerVoid) + { + var outputOptionName = "outputOption"; + writer.WriteLine($"var {outputOptionName} = new Option<{outputFormatParamType}>(\"--{outputFormatParamName}\", () => FormatterType.JSON){{"); + writer.IncreaseIndent(); + writer.WriteLine("IsRequired = true"); + writer.CloseBlock("};"); + writer.WriteLine($"command.AddOption({outputOptionName});"); + paramTypes.Add(outputFormatParamType); + paramNames.Add(outputFormatParamName); + availableOptions.Add(outputOptionName); + } + + // Add output formatter factory param + paramTypes.Add(outputFormatterFactoryParamType); + paramNames.Add(outputFormatterFactoryParamName); + + // Add console param + paramTypes.Add(consoleParamType); + paramNames.Add(consoleParamName); + + // Add CancellationToken param + paramTypes.Add(cancellationTokenParamType); + paramNames.Add(cancellationTokenParamName); + + var zipped = paramTypes.Zip(paramNames); + var projected = zipped.Select((x, y) => $"{x.First} {x.Second}"); + var handlerParams = string.Join(", ", projected); + writer.WriteLine($"command.SetHandler(async ({handlerParams}) => {{"); + writer.IncreaseIndent(); + WriteCommandHandlerBody(originalMethod, requestParams, isHandlerVoid, returnType, writer); + // Get request generator method. To call it + get path & query parameters see WriteRequestExecutorBody in CSharp + if (isHandlerVoid) + { + writer.WriteLine($"{consoleParamName}.WriteLine(\"Success\");"); + } + else + { + var type = originalMethod.ReturnType as CodeType; + var typeString = conventions.GetTypeString(type, originalMethod); + var formatterVar = "formatter"; + + writer.WriteLine($"var {formatterVar} = {outputFormatterFactoryParamName}.GetFormatter({outputFormatParamName});"); + if (typeString != "Stream") + { + writer.WriteLine($"{formatterVar}.WriteOutput(response, {consoleParamName});"); + } + else + { + writer.WriteLine($"if ({fileParamName} == null) {{"); + writer.IncreaseIndent(); + writer.WriteLine($"{formatterVar}.WriteOutput(response, {consoleParamName});"); + writer.CloseBlock(); + writer.WriteLine("else {"); + writer.IncreaseIndent(); + writer.WriteLine($"using var writeStream = {fileParamName}.OpenWrite();"); + writer.WriteLine("await response.CopyToAsync(writeStream);"); + writer.WriteLine($"{consoleParamName}.WriteLine($\"Content written to {{{fileParamName}.FullName}}.\");"); + writer.CloseBlock(); + } + } + writer.DecreaseIndent(); + var delimiter = ""; + if (availableOptions.Any()) + { + delimiter = ", "; + } + writer.WriteLine($"}}{delimiter}{string.Join(", ", availableOptions)});"); + writer.WriteLine("return command;"); + } + + private List WriteExecutableCommandOptions(LanguageWriter writer, List parametersList) + { + var availableOptions = new List(); + foreach (var option in parametersList) + { + var type = option.Type as CodeType; + var optionName = $"{NormalizeToIdentifier(option.Name)}Option"; + var optionType = conventions.GetTypeString(option.Type, option); + if (option.ParameterKind == CodeParameterKind.RequestBody && type.TypeDefinition is CodeClass) optionType = "string"; + + // Binary body handling + if (option.IsOfKind(CodeParameterKind.RequestBody) && conventions.StreamTypeName.Equals(option.Type?.Name, StringComparison.OrdinalIgnoreCase)) + { + option.Name = "file"; + } + + var optionBuilder = new StringBuilder("new Option"); + if (!String.IsNullOrEmpty(optionType)) + { + optionBuilder.Append($"<{optionType}>"); + } + optionBuilder.Append("(\""); + if (option.Name.Length > 1) optionBuilder.Append('-'); + optionBuilder.Append($"-{NormalizeToOption(option.Name)}\""); + if (option.DefaultValue != null) + { + var defaultValue = optionType == "string" ? $"\"{option.DefaultValue}\"" : option.DefaultValue; + optionBuilder.Append($", getDefaultValue: ()=> {defaultValue}"); + } + + if (!string.IsNullOrEmpty(option.Description)) + { + optionBuilder.Append($", description: \"{option.Description}\""); + } + + optionBuilder.Append(") {"); + var strValue = $"{optionBuilder}"; + writer.WriteLine($"var {optionName} = {strValue}"); + writer.IncreaseIndent(); + var isRequired = !option.Optional || option.IsOfKind(CodeParameterKind.Path); + + if (option.Type.IsCollection) + { + var arity = isRequired ? "OneOrMore" : "ZeroOrMore"; + writer.WriteLine($"Arity = ArgumentArity.{arity}"); + } + + writer.DecreaseIndent(); + writer.WriteLine("};"); + writer.WriteLine($"{optionName}.IsRequired = {isRequired.ToString().ToFirstCharacterLowerCase()};"); + writer.WriteLine($"command.AddOption({optionName});"); + availableOptions.Add(optionName); + } + + return availableOptions; + } + + private static void WriteCommandDescription(CodeMethod codeElement, LanguageWriter writer) + { + if (!string.IsNullOrWhiteSpace(codeElement.Description)) + writer.WriteLine($"command.Description = \"{codeElement.Description}\";"); + } + + private void WriteContainerCommand(CodeMethod codeElement, LanguageWriter writer, CodeClass parent, string name) + { + writer.WriteLine($"var command = new Command(\"{name}\");"); + WriteCommandDescription(codeElement, writer); + + if ((codeElement.AccessedProperty?.Type) is CodeType codeReturnType) + { + // Include namespace to avoid type ambiguity on similarly named classes. Currently, if we have namespaces A and A.B where both namespaces have type T, + // Trying to use type A.B.T in namespace A without using the fully qualified name will break the build. + var targetClass = string.Join(".", codeReturnType.TypeDefinition.Parent.Name, conventions.GetTypeString(codeReturnType, codeElement)); + var builderMethods = codeReturnType.TypeDefinition.GetChildElements(true).OfType() + .Where(m => m.IsOfKind(CodeMethodKind.CommandBuilder)) + .OrderBy(m => m.Name) + .ThenBy(m => m.ReturnType.IsCollection); + conventions.AddRequestBuilderBody(parent, targetClass, writer, prefix: "var builder = ", pathParameters: codeElement.Parameters.Where(x => x.IsOfKind(CodeParameterKind.Path))); + + foreach (var method in builderMethods) + { + if (method.ReturnType.IsCollection) + { + writer.WriteLine($"foreach (var cmd in builder.{method.Name}()) {{"); + writer.IncreaseIndent(); + writer.WriteLine($"command.AddCommand(cmd);"); + writer.CloseBlock(); + } + else + { + writer.WriteLine($"command.AddCommand(builder.{method.Name}());"); + } + } + // SubCommands + } + + writer.WriteLine("return command;"); + } + + private void WriteUnnamedBuildCommand(CodeMethod codeElement, LanguageWriter writer, CodeClass parent, IEnumerable classMethods) + { + if (codeElement.OriginalMethod?.MethodKind == CodeMethodKind.ClientConstructor) + { + var commandBuilderMethods = classMethods.Where(m => m.MethodKind == CodeMethodKind.CommandBuilder && m != codeElement).OrderBy(m => m.Name); + writer.WriteLine($"var command = new RootCommand();"); + WriteCommandDescription(codeElement, writer); + foreach (var method in commandBuilderMethods) + { + writer.WriteLine($"command.AddCommand({method.Name}());"); + } + + writer.WriteLine("return command;"); + } + else if (codeElement.OriginalIndexer != null) + { + var targetClass = conventions.GetTypeString(codeElement.OriginalIndexer.ReturnType, codeElement); + var builderMethods = (codeElement.OriginalIndexer.ReturnType as CodeType).TypeDefinition.GetChildElements(true).OfType() + .Where(m => m.IsOfKind(CodeMethodKind.CommandBuilder)) + .OrderBy(m => m.Name); + conventions.AddRequestBuilderBody(parent, targetClass, writer, prefix: "var builder = ", pathParameters: codeElement.Parameters.Where(x => x.IsOfKind(CodeParameterKind.Path))); + writer.WriteLine("var commands = new List();"); + + foreach (var method in builderMethods) + { + if (method.ReturnType.IsCollection) + { + writer.WriteLine($"commands.AddRange(builder.{method.Name}());"); + } + else + { + writer.WriteLine($"commands.Add(builder.{method.Name}());"); + } + } + + writer.WriteLine("return commands;"); + } + } + + protected virtual void WriteCommandHandlerBody(CodeMethod codeElement, RequestParams requestParams, bool isVoid, string returnType, LanguageWriter writer) + { + if (codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); + + var generatorMethod = (codeElement.Parent as CodeClass) + .Methods + .FirstOrDefault(x => x.IsOfKind(CodeMethodKind.RequestGenerator) && x.HttpMethod == codeElement.HttpMethod); + var requestBodyParam = requestParams.requestBody; + if (requestBodyParam != null) + { + var requestBodyParamType = requestBodyParam?.Type as CodeType; + if (requestBodyParamType?.TypeDefinition is CodeClass) + { + writer.WriteLine($"using var stream = new MemoryStream(Encoding.UTF8.GetBytes({requestBodyParam.Name}));"); + writer.WriteLine($"var parseNode = ParseNodeFactoryRegistry.DefaultInstance.GetRootParseNode(\"{generatorMethod.ContentType}\", stream);"); + + var typeString = conventions.GetTypeString(requestBodyParamType, requestBodyParam, false); + + if (requestBodyParamType.IsCollection) + { + writer.WriteLine($"var model = parseNode.GetCollectionOfObjectValues<{typeString}>();"); + } + else + { + writer.WriteLine($"var model = parseNode.GetObjectValue<{typeString}>();"); + } + + requestBodyParam.Name = "model"; + } + else if (conventions.StreamTypeName.Equals(requestBodyParamType?.Name, StringComparison.OrdinalIgnoreCase)) + { + var name = requestBodyParam.Name; + requestBodyParam.Name = "stream"; + writer.WriteLine($"using var {requestBodyParam.Name} = {name}.OpenRead();"); + } + } + + var parametersList = new CodeParameter[] { requestParams.requestBody, requestParams.queryString, requestParams.headers, requestParams.options } + .Select(x => x?.Name).Where(x => x != null).DefaultIfEmpty().Aggregate((x, y) => $"{x}, {y}"); + var separator = string.IsNullOrWhiteSpace(parametersList) ? "" : ", "; + WriteRequestInformation(writer, generatorMethod, parametersList, separator); + + var errorMappingVarName = "default"; + if (codeElement.ErrorMappings.Any()) + { + errorMappingVarName = "errorMapping"; + writer.WriteLine($"var {errorMappingVarName} = new Dictionary> {{"); + writer.IncreaseIndent(); + foreach (var errorMapping in codeElement.ErrorMappings) + { + writer.WriteLine($"{{\"{errorMapping.Key.ToUpperInvariant()}\", () => new {errorMapping.Value.Name.ToFirstCharacterUpperCase()}()}},"); + } + writer.CloseBlock("};"); + } + + var requestMethod = "SendPrimitiveAsync"; + if (isVoid) + { + requestMethod = "SendNoContentAsync"; + } + + writer.WriteLine($"{(isVoid ? string.Empty : "var response = ")}await RequestAdapter.{requestMethod}(requestInfo, errorMapping: {errorMappingVarName}, cancellationToken: {cancellationTokenParamName});"); + } + + private static void WriteRequestInformation(LanguageWriter writer, CodeMethod generatorMethod, string parametersList, string separator) + { + writer.WriteLine($"var requestInfo = {generatorMethod?.Name}({parametersList}{separator}q => {{"); + if (generatorMethod?.PathAndQueryParameters != null) + { + writer.IncreaseIndent(); + foreach (var param in generatorMethod.PathAndQueryParameters.Where(p => p.IsOfKind(CodeParameterKind.QueryParameter))) + { + var paramName = NormalizeToIdentifier(param.Name); + bool isStringParam = "string".Equals(param.Type.Name, StringComparison.OrdinalIgnoreCase) && !param.Type.IsCollection; + bool indentParam = true; + if (isStringParam) + { + writer.Write($"if (!String.IsNullOrEmpty({paramName})) "); + indentParam = false; + } + + writer.Write($"q.{param.Name.ToFirstCharacterUpperCase()} = {paramName};", indentParam); + + writer.WriteLine(); + } + writer.CloseBlock("});"); + + foreach (var paramName in generatorMethod.PathAndQueryParameters.Where(p => p.IsOfKind(CodeParameterKind.PathParameters)).Select(p => p.Name)) + { + writer.WriteLine($"requestInfo.PathParameters.Add(\"{paramName}\", {NormalizeToIdentifier(paramName)});"); + } + } + else + { + writer.WriteLine("});"); + } + } + + /// + /// Converts delimited string into camel case for use as identifiers + /// + /// + /// + private static string NormalizeToIdentifier(string input) + { + return identifierRegex.Replace(input, m => m.Groups[1].Value.ToUpper()); + } + + /// + /// Converts camel-case or delimited string to '-' delimited string for use as a command option + /// + /// + /// + private static string NormalizeToOption(string input) + { + var result = camelCaseRegex.Replace(input, "-$1"); + // 2 passes for cases like "singleValueLegacyExtendedProperty_id" + result = delimitedRegex.Replace(result, "-$1"); + + return result.ToLower(); + } + } +} diff --git a/src/Kiota.Builder/Writers/Shell/ShellWriter.cs b/src/Kiota.Builder/Writers/Shell/ShellWriter.cs new file mode 100644 index 0000000000..b1c3bc3812 --- /dev/null +++ b/src/Kiota.Builder/Writers/Shell/ShellWriter.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Kiota.Builder.Writers.CSharp; + +namespace Kiota.Builder.Writers.Shell +{ + class ShellWriter : CSharpWriter + { + public ShellWriter(string rootPath, string clientNamespaceName) : base(rootPath, clientNamespaceName) + { + var conventionService = new CSharpConventionService(); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassEndWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeEnumWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeIndexerWriter(conventionService)); + AddOrReplaceCodeElementWriter(new ShellCodeMethodWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeTypeWriter(conventionService)); + } + } +} diff --git a/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs b/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs index 269cfcf981..234259fa94 100644 --- a/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs @@ -6,12 +6,12 @@ public TypeScriptWriter(string rootPath, string clientNamespaceName) { PathSegmenter = new TypeScriptPathSegmenter(rootPath,clientNamespaceName); var conventionService = new TypeScriptConventionService(null); - AddCodeElementWriter(new CodeClassDeclarationWriter(conventionService, clientNamespaceName)); - AddCodeElementWriter(new CodeClassEndWriter()); - AddCodeElementWriter(new CodeEnumWriter(conventionService)); - AddCodeElementWriter(new CodeMethodWriter(conventionService)); - AddCodeElementWriter(new CodePropertyWriter(conventionService)); - AddCodeElementWriter(new CodeTypeWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService, clientNamespaceName)); + AddOrReplaceCodeElementWriter(new CodeClassEndWriter()); + AddOrReplaceCodeElementWriter(new CodeEnumWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeTypeWriter(conventionService)); } } } diff --git a/tests/Kiota.Builder.Tests/Refiners/ShellRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/ShellRefinerTests.cs new file mode 100644 index 0000000000..b9cfa4dead --- /dev/null +++ b/tests/Kiota.Builder.Tests/Refiners/ShellRefinerTests.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; +using Xunit; + +namespace Kiota.Builder.Refiners.Tests; + +public class ShellRefinerTests { + private readonly CodeNamespace root = CodeNamespace.InitRootNamespace(); + + [Fact] + public void AddsUsingsForCommandTypesUsedInCommandBuilder() { + var requestBuilder = root.AddClass(new CodeClass { + Name = "somerequestbuilder", + ClassKind = CodeClassKind.RequestBuilder, + }).First(); + var subNS = root.AddNamespace($"{root.Name}.subns"); // otherwise the import gets trimmed + var commandBuilder = requestBuilder.AddMethod(new CodeMethod { + Name = "GetCommand", + MethodKind = CodeMethodKind.CommandBuilder, + ReturnType = new CodeType { + Name = "Command", + IsExternal = true + } + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Shell }, root); + + var declaration = requestBuilder.StartBlock as CodeClass.Declaration; + + Assert.Contains("System.CommandLine", declaration.Usings.Select(x => x.Declaration?.Name)); + } + + [Fact] + public void CreatesCommandBuilders() { + var requestBuilder = root.AddClass(new CodeClass { + Name = "somerequestbuilder", + ClassKind = CodeClassKind.RequestBuilder, + }).First(); + var subNS = root.AddNamespace($"{root.Name}.subns"); // otherwise the import gets trimmed + // Add nav props + requestBuilder.AddProperty(new CodeProperty { + Name = "User", + PropertyKind = CodePropertyKind.RequestBuilder + }); + + // Add indexer + requestBuilder.SetIndexer(new CodeIndexer { + Name = "Users", + ReturnType = new CodeType { + Name = "Address" + } + }); + + // Add request executor + requestBuilder.AddMethod(new CodeMethod { + Name = "GetExecutor", + ReturnType = new CodeType { + Name = "User" + }, + MethodKind = CodeMethodKind.RequestExecutor, + HttpMethod = HttpMethod.Get + }); + + // Add client constructor + requestBuilder.AddMethod(new CodeMethod { + Name = "constructor", + MethodKind = CodeMethodKind.ClientConstructor, + ReturnType = new CodeType { + Name = "void" + }, + DeserializerModules = new() {"com.microsoft.kiota.serialization.Deserializer"}, + SerializerModules = new() {"com.microsoft.kiota.serialization.Serializer"} + }); + + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Shell }, root); + + var methods = root.GetChildElements().OfType().SelectMany(c => c.GetChildElements().OfType()); + var methodNames = methods.Select(m => m.Name); + + Assert.Contains("BuildCommand", methodNames); + Assert.Contains("BuildUserCommand", methodNames); + Assert.Contains("BuildListCommand", methodNames); + Assert.Contains("BuildRootCommand", methodNames); + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs new file mode 100644 index 0000000000..3c3d19b003 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/Shell/ShellCodeMethodWriterTests.cs @@ -0,0 +1,403 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Kiota.Builder.Writers; +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Shell; + +public class ShellCodeMethodWriterTests : IDisposable +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeMethod method; + private readonly CodeClass parentClass; + private readonly CodeNamespace root; + private const string MethodName = "methodName"; + private const string ReturnTypeName = "Somecustomtype"; + private const string MethodDescription = "some description"; + private const string ParamDescription = "some parameter description"; + private const string ParamName = "paramName"; + + public ShellCodeMethodWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Shell, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + root = CodeNamespace.InitRootNamespace(); + parentClass = new CodeClass + { + Name = "parentClass" + }; + root.AddClass(parentClass); + method = new CodeMethod + { + Name = MethodName + }; + method.ReturnType = new CodeType + { + Name = ReturnTypeName + }; + parentClass.AddMethod(method); + } + + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + + private void AddRequestProperties() { + parentClass.AddProperty(new CodeProperty { + Name = "RequestAdapter", + PropertyKind = CodePropertyKind.RequestAdapter, + }); + parentClass.AddProperty(new CodeProperty { + Name = "pathParameters", + PropertyKind = CodePropertyKind.PathParameters, + }); + parentClass.AddProperty(new CodeProperty { + Name = "urlTemplate", + PropertyKind = CodePropertyKind.UrlTemplate, + }); + } + + private static void AddRequestBodyParameters(CodeMethod method) { + var stringType = new CodeType { + Name = "string", + }; + method.AddParameter(new CodeParameter { + Name = "h", + ParameterKind = CodeParameterKind.Headers, + Type = stringType, + }); + method.AddParameter(new CodeParameter{ + Name = "q", + ParameterKind = CodeParameterKind.QueryParameter, + Type = stringType, + }); + method.AddParameter(new CodeParameter{ + Name = "b", + ParameterKind = CodeParameterKind.RequestBody, + Type = stringType, + }); + method.AddParameter(new CodeParameter{ + Name = "r", + ParameterKind = CodeParameterKind.ResponseHandler, + Type = stringType, + }); + method.AddParameter(new CodeParameter { + Name = "o", + ParameterKind = CodeParameterKind.Options, + Type = stringType, + }); + method.AddParameter(new CodeParameter + { + Name = "c", + ParameterKind = CodeParameterKind.Cancellation, + Type = stringType, + }); + } + + private static void AddPathAndQueryParameters(CodeMethod method) { + var stringType = new CodeType { + Name = "string", + }; + method.AddPathOrQueryParameter(new CodeParameter{ + Name = "q", + ParameterKind = CodeParameterKind.QueryParameter, + Type = stringType, + DefaultValue = "test", + Description = "The q option", + Optional = true + }); + method.AddPathOrQueryParameter(new CodeParameter { + Name = "p", + ParameterKind = CodeParameterKind.Path, + Type = stringType + }); + } + + [Fact] + public void WritesRootCommand() + { + method.MethodKind = CodeMethodKind.CommandBuilder; + method.OriginalMethod = new CodeMethod + { + MethodKind = CodeMethodKind.ClientConstructor + }; + + writer.Write(method); + + var result = tw.ToString(); + + Assert.Contains("var command = new RootCommand();", result); + + Assert.Contains("return command;", result); + } + + [Fact] + public void WritesRootCommandWithCommandBuilderMethods() + { + method.MethodKind = CodeMethodKind.CommandBuilder; + method.OriginalMethod = new CodeMethod + { + MethodKind = CodeMethodKind.ClientConstructor + }; + parentClass.AddMethod(new CodeMethod { + Name = "BuildUserCommand", + MethodKind = CodeMethodKind.CommandBuilder + }); + + writer.Write(method); + + var result = tw.ToString(); + + Assert.Contains("var command = new RootCommand();", result); + Assert.Contains("command.AddCommand(BuildUserCommand());", result); + Assert.Contains("return command;", result); + } + + [Fact] + public void WritesIndexerCommands() { + method.MethodKind = CodeMethodKind.CommandBuilder; + var type = new CodeClass { Name = "TestClass", ClassKind = CodeClassKind.RequestBuilder }; + type.AddMethod(new CodeMethod { MethodKind = CodeMethodKind.CommandBuilder, Name = "BuildTestMethod1", ReturnType = new CodeType() }); + type.AddMethod(new CodeMethod { MethodKind = CodeMethodKind.CommandBuilder, Name = "BuildTestMethod2", ReturnType = new CodeType {CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array} }); + method.OriginalIndexer = new CodeIndexer { + ReturnType = new CodeType { + Name = "TestRequestBuilder", + TypeDefinition = type + } + }; + + AddRequestProperties(); + + writer.Write(method); + var result = tw.ToString(); + + Assert.Contains("var builder = new TestRequestBuilder", result); + Assert.Contains("var commands = new List();", result); + Assert.Contains("commands.Add(builder.BuildTestMethod1());", result); + Assert.Contains("commands.AddRange(builder.BuildTestMethod2());", result); + Assert.Contains("return commands;", result); + } + + [Fact] + public void WritesContainerCommands() { + method.MethodKind = CodeMethodKind.CommandBuilder; + method.SimpleName = "User"; + var type = new CodeClass { Name = "TestClass", ClassKind = CodeClassKind.RequestBuilder }; + type.AddMethod(new CodeMethod { MethodKind = CodeMethodKind.CommandBuilder, Name = "BuildTestMethod1", ReturnType = new CodeType() }); + type.AddMethod(new CodeMethod { MethodKind = CodeMethodKind.CommandBuilder, Name = "BuildTestMethod2", ReturnType = new CodeType {CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array} }); + type.Parent = new CodeType { + Name = "Test.Name" + }; + method.AccessedProperty = new CodeProperty { + Type = new CodeType { + Name = "TestRequestBuilder", + TypeDefinition = type + } + }; + + AddRequestProperties(); + + writer.Write(method); + var result = tw.ToString(); + + Assert.Contains("var command = new Command(\"user\");", result); + Assert.Contains("var builder = new Test.Name.TestRequestBuilder", result); + Assert.Contains("command.AddCommand(builder.BuildTestMethod1());", result); + Assert.Contains("foreach (var cmd in builder.BuildTestMethod2()) {", result); + Assert.Contains("command.AddCommand(cmd);", result); + Assert.Contains("return command;", result); + } + + [Fact] + public void WritesExecutableCommandForGetRequest() { + + method.MethodKind = CodeMethodKind.CommandBuilder; + method.SimpleName = "User"; + method.HttpMethod = HttpMethod.Get; + var stringType = new CodeType { + Name = "string", + }; + var generatorMethod = new CodeMethod { + MethodKind = CodeMethodKind.RequestGenerator, + Name = "CreateGetRequestInformation", + HttpMethod = method.HttpMethod + }; + method.OriginalMethod = new CodeMethod { + MethodKind = CodeMethodKind.RequestExecutor, + HttpMethod = method.HttpMethod, + ReturnType = stringType, + Parent = method.Parent + }; + var codeClass = method.Parent as CodeClass; + codeClass.AddMethod(generatorMethod); + + AddRequestProperties(); + AddRequestBodyParameters(method.OriginalMethod); + AddPathAndQueryParameters(generatorMethod); + + writer.Write(method); + var result = tw.ToString(); + + Assert.Contains("var command = new Command(\"user\");", 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.Contains("command.AddOption(outputOption);", result); + Assert.Contains("var requestInfo = CreateGetRequestInformation", result); + Assert.Contains("var response = await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping: default, cancellationToken: cancellationToken);", result); + Assert.Contains("return command;", result); + } + + [Fact] + public void WritesExecutableCommandForPostRequest() { + + method.MethodKind = CodeMethodKind.CommandBuilder; + method.SimpleName = "User"; + method.HttpMethod = HttpMethod.Post; + var stringType = new CodeType { + Name = "string", + }; + var generatorMethod = new CodeMethod { + MethodKind = CodeMethodKind.RequestGenerator, + Name = "CreatePostRequestInformation", + HttpMethod = method.HttpMethod + }; + method.OriginalMethod = new CodeMethod { + MethodKind = CodeMethodKind.RequestExecutor, + HttpMethod = method.HttpMethod, + ReturnType = stringType, + Parent = method.Parent + }; + method.OriginalMethod.AddParameter(new CodeParameter{ + Name = "body", + ParameterKind = CodeParameterKind.RequestBody, + Type = stringType, + }); + var codeClass = method.Parent as CodeClass; + codeClass.AddMethod(generatorMethod); + + AddRequestProperties(); + AddPathAndQueryParameters(generatorMethod); + + writer.Write(method); + var result = tw.ToString(); + + Assert.Contains("var command = new Command(\"user\");", 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.Contains("var bodyOption = new Option(\"--body\")", result); + Assert.Contains("bodyOption.IsRequired = true;", result); + Assert.Contains("command.AddOption(bodyOption);", result); + Assert.Contains("command.AddOption(outputOption);", result); + Assert.Contains("var requestInfo = CreatePostRequestInformation", result); + Assert.Contains("var response = await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping: default, cancellationToken: cancellationToken);", result); + Assert.Contains("return command;", result); + } + + [Fact] + public void WritesExecutableCommandForGetStreamRequest() + { + + method.MethodKind = CodeMethodKind.CommandBuilder; + method.SimpleName = "User"; + method.HttpMethod = HttpMethod.Get; + var streamType = new CodeType + { + Name = "stream", + }; + var generatorMethod = new CodeMethod + { + MethodKind = CodeMethodKind.RequestGenerator, + Name = "CreateGetRequestInformation", + HttpMethod = method.HttpMethod + }; + method.OriginalMethod = new CodeMethod + { + MethodKind = CodeMethodKind.RequestExecutor, + HttpMethod = method.HttpMethod, + ReturnType = streamType, + Parent = method.Parent + }; + var codeClass = method.Parent as CodeClass; + codeClass.AddMethod(generatorMethod); + + AddRequestProperties(); + AddRequestBodyParameters(method.OriginalMethod); + AddPathAndQueryParameters(generatorMethod); + + writer.Write(method); + var result = tw.ToString(); + + Assert.Contains("var command = new Command(\"user\");", 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.Contains("command.AddOption(outputOption);", result); + Assert.Contains("var fileOption = new Option(\"--file\");", result); + Assert.Contains("command.AddOption(fileOption);", result); + Assert.Contains("var requestInfo = CreateGetRequestInformation", result); + Assert.Contains("var response = await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping: default, cancellationToken: cancellationToken);", result); + Assert.Contains("return command;", result); + } + + [Fact] + public void WritesExecutableCommandForPostVoidRequest() { + + method.MethodKind = CodeMethodKind.CommandBuilder; + method.SimpleName = "User"; + method.HttpMethod = HttpMethod.Post; + var stringType = new CodeType { + Name = "string", + }; + var voidType = new CodeType { + Name = "void", + }; + var generatorMethod = new CodeMethod { + MethodKind = CodeMethodKind.RequestGenerator, + Name = "CreatePostRequestInformation", + HttpMethod = method.HttpMethod + }; + method.OriginalMethod = new CodeMethod { + MethodKind = CodeMethodKind.RequestExecutor, + HttpMethod = method.HttpMethod, + ReturnType = voidType, + Parent = method.Parent + }; + method.OriginalMethod.AddParameter(new CodeParameter{ + Name = "body", + ParameterKind = CodeParameterKind.RequestBody, + Type = stringType, + }); + var codeClass = method.Parent as CodeClass; + codeClass.AddMethod(generatorMethod); + + AddRequestProperties(); + AddPathAndQueryParameters(generatorMethod); + + writer.Write(method); + var result = tw.ToString(); + + Assert.Contains("var command = new Command(\"user\");", 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.Contains("var bodyOption = new Option(\"--body\")", result); + Assert.Contains("bodyOption.IsRequired = true;", result); + Assert.Contains("command.AddOption(bodyOption);", result); + Assert.Contains("var requestInfo = CreatePostRequestInformation", result); + Assert.Contains("await RequestAdapter.SendNoContentAsync(requestInfo, errorMapping: default, cancellationToken: cancellationToken);", result); + Assert.Contains("console.WriteLine(\"Success\");", result); + Assert.Contains("return command;", result); + } +}