diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index c9ebf81771..1425ca2472 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -55,6 +55,11 @@ parameters: os: "linux" image: "ubuntu-latest" pool: Azure-Pipelines-1ESPT-ExDShared + - architecture: "linux-arm64" + jobPrefix: "linux_arm64" + os: "linux" + image: "ubuntu-latest" + pool: Azure-Pipelines-1ESPT-ExDShared - architecture: "osx-x64" jobPrefix: "osx_x64" os: "macOS" @@ -575,6 +580,7 @@ extends: win_x64_build_binaries, win_x86_build_binaries, linux_x64_build_binaries, + linux_arm64_build_binaries, osx_x64_build_binaries, osx_arm64_build_binaries, ] diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b71c4288ab..8be50c9b54 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build image - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 with: push: false platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 @@ -94,7 +94,7 @@ jobs: run: echo "date=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT - name: Push to GitHub Packages - Nightly if: contains(github.ref, env.PREVIEW_BRANCH) - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 with: push: true platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 @@ -104,7 +104,7 @@ jobs: # we can't get the sequence number from ADO so we default it back to github run number - name: Push to GitHub Packages - Release if: contains(github.ref, 'refs/tags/v') - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 with: push: true platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 diff --git a/.github/workflows/idempotency-tests.yml b/.github/workflows/idempotency-tests.yml index cf479de6d4..074325f488 100644 --- a/.github/workflows/idempotency-tests.yml +++ b/.github/workflows/idempotency-tests.yml @@ -63,6 +63,7 @@ jobs: - ruby - php - python + - dart description: - "./tests/Kiota.Builder.IntegrationTests/InheritingErrors.yaml" - "./tests/Kiota.Builder.IntegrationTests/NoUnderscoresInModel.yaml" diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 487b069e4e..7e0ef9ffff 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -43,6 +43,7 @@ jobs: - ruby - php - python + - dart description: - "./tests/Kiota.Builder.IntegrationTests/InheritingErrors.yaml" - "./tests/Kiota.Builder.IntegrationTests/EnumHandling.yaml" @@ -109,6 +110,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" + - name: Setup Dart + if: matrix.language == 'dart' + uses: dart-lang/setup-dart@v1 + with: + sdk: "stable" - name: Check if test is suppressed id: check-suppressed diff --git a/.vscode/launch.json b/.vscode/launch.json index f4bcd561b3..2638c90765 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -170,6 +170,27 @@ "console": "internalConsole", "stopAtEntry": false }, + { + "name": "Launch Dart", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/kiota/bin/Debug/net8.0/kiota.dll", + "args": [ + "generate", + "--openapi", + "https://raw.githubusercontent.com/microsoftgraph/msgraph-sdk-powershell/dev/openApiDocs/v1.0/Mail.yml", + "--language", + "dart", + "-o", + "${workspaceFolder}/samples/msgraph-mail/java/utilities/src/main/java/graphjavav4/utilities", + "-n", + "graphdart4.utilities" + ], + "cwd": "${workspaceFolder}/src/kiota", + "console": "internalConsole", + "stopAtEntry": false + }, { "name": "Launch CLI (CSharp)", "type": "coreclr", @@ -385,4 +406,4 @@ "processId": "${command:pickProcess}" } ] -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 49acfd4d8b..3baef197e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +## [1.22.1] - 2025-01-10 + +### Added + +### Changed +- Fixed a bug in the VS Code extension plugin generation [#5978](https://github.com/microsoft/kiota/issues/5978) + +## [1.22.0] - 2025-01-09 + +### Added + +- Adds code generation for the `Dart` programming language.Credit to @ricardoboss, @joanne-ter-maat, @Kees-Schotanus, @andreaTP, @baywet[#3745](https://github.com/microsoft/kiota/issues/3745) +- Added new linux arm64 binary builds for Kiota CLI + +### Changed + - Fixed a bug in generation when a referenced schema in an allOf was a primitive [#5701](https://github.com/microsoft/kiota/issues/5701). - Fixed a bug where inherited error models would be missing interface declarations. [#5888](https://github.com/microsoft/kiota/issues/5888) - Fixed a bug where oneOf/anyOf schemas with single references to inheritance or intersections would be missing properties. [#5921](https://github.com/microsoft/kiota/issues/5921) @@ -1522,3 +1538,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial GitHub release + diff --git a/it/Readme.md b/it/Readme.md index aac690c34d..262e5b88c0 100644 --- a/it/Readme.md +++ b/it/Readme.md @@ -17,5 +17,5 @@ Generate the code: And finally run the test: ```bash -./it/exec-cmd.ps1 -language ${LANG} +./it/exec-cmd.ps1 -descriptionUrl ${FILE/URL} -language ${LANG} ``` diff --git a/it/dart/.gitignore b/it/dart/.gitignore new file mode 100644 index 0000000000..4f66d2275a --- /dev/null +++ b/it/dart/.gitignore @@ -0,0 +1,3 @@ +pubspec.lock +.dart_tool/ +src/ \ No newline at end of file diff --git a/it/dart/basic/test/api_client_test.dart b/it/dart/basic/test/api_client_test.dart new file mode 100644 index 0000000000..6bc18fd191 --- /dev/null +++ b/it/dart/basic/test/api_client_test.dart @@ -0,0 +1,24 @@ +import 'package:microsoft_kiota_abstractions/microsoft_kiota_abstractions.dart'; +import 'package:microsoft_kiota_http/microsoft_kiota_http.dart'; +import 'package:test/test.dart'; +import '../lib/api_client.dart'; +import '../lib/models/error.dart'; + +void main() { + group('apiclient', () { + test('basic endpoint test', () { + final requestAdapter = HttpClientRequestAdapter( + client: KiotaClientFactory.createClient(), + authProvider: AnonymousAuthenticationProvider(), + pNodeFactory: ParseNodeFactoryRegistry.defaultInstance, + sWriterFactory: SerializationWriterFactoryRegistry.defaultInstance, + ); + requestAdapter.baseUrl = "http://localhost:1080"; + var client = ApiClient(requestAdapter); + expect( + () => client.api.v1.topics.getAsync(), + throwsA(predicate( + (e) => e is Error && e.id == 'my-sample-id' && e.code == 123))); + }); + }); +} diff --git a/it/dart/pubspec.yaml b/it/dart/pubspec.yaml new file mode 100644 index 0000000000..8becd341e7 --- /dev/null +++ b/it/dart/pubspec.yaml @@ -0,0 +1,22 @@ +name: kiota_dart_generate +description: api generation +version: 0.0.1 +publish_to: none + +environment: + sdk: ^3.6.0 + +# Add regular dependencies here. +dependencies: + microsoft_kiota_abstractions: ^0.0.1 + microsoft_kiota_http: ^0.0.1 + microsoft_kiota_serialization_form: ^0.0.1 + microsoft_kiota_serialization_text: ^0.0.1 + microsoft_kiota_serialization_json: ^0.0.1 + microsoft_kiota_serialization_multipart: ^0.0.1 + http: ^1.2.2 + uuid: ^4.5.1 + +dev_dependencies: + lints: ^5.1.1 + test: ^1.25.14 diff --git a/it/exec-cmd.ps1 b/it/exec-cmd.ps1 index ee6faf9440..0bd7595493 100755 --- a/it/exec-cmd.ps1 +++ b/it/exec-cmd.ps1 @@ -202,6 +202,29 @@ elseif ($language -eq "python") { Pop-Location } } +elseif ($language -eq "dart") { + Invoke-Call -ScriptBlock { + dart pub get + dart analyze lib/ + } -ErrorAction Stop + + if ($mockServerTest) { + Push-Location $itTestPath + + $itTestPathSources = Join-Path -Path $testPath -ChildPath "lib" + $itTestPathDest = Join-Path -Path $itTestPath -ChildPath "lib" + if (Test-Path $itTestPathDest) { + Remove-Item $itTestPathDest -Force -Recurse + } + Copy-Item -Path $itTestPathSources -Destination $itTestPathDest -Recurse + + Invoke-Call -ScriptBlock { + dart test + } -ErrorAction Stop + + Pop-Location + } +} Pop-Location if (!([string]::IsNullOrEmpty($mockSeverITFolder))) { diff --git a/it/get-additional-arguments.ps1 b/it/get-additional-arguments.ps1 index aa1e6db286..e96e3c4a0c 100755 --- a/it/get-additional-arguments.ps1 +++ b/it/get-additional-arguments.ps1 @@ -23,6 +23,9 @@ if ($language -eq "csharp") { elseif ($language -eq "java") { $command = " --output `"./it/$language/src/apisdk`"" } +elseif ($language -eq "dart") { + $command = " --output `"./it/$language/lib`"" +} elseif ($language -eq "go") { $command = " --output `"./it/$language/client`" --namespace-name `"integrationtest/client`"" } diff --git a/it/typescript/package-lock.json b/it/typescript/package-lock.json index 70a26854fc..9ace36ad33 100644 --- a/it/typescript/package-lock.json +++ b/it/typescript/package-lock.json @@ -10,13 +10,13 @@ "license": "MIT", "dependencies": { "@azure/identity": "^4.5.0", - "@microsoft/kiota-abstractions": "^1.0.0-preview.78", - "@microsoft/kiota-authentication-azure": "^1.0.0-preview.78", - "@microsoft/kiota-http-fetchlibrary": "^1.0.0-preview.78", - "@microsoft/kiota-serialization-form": "^1.0.0-preview.78", - "@microsoft/kiota-serialization-json": "^1.0.0-preview.78", - "@microsoft/kiota-serialization-multipart": "^1.0.0-preview.78", - "@microsoft/kiota-serialization-text": "^1.0.0-preview.78", + "@microsoft/kiota-abstractions": "^1.0.0-preview.79", + "@microsoft/kiota-authentication-azure": "^1.0.0-preview.79", + "@microsoft/kiota-http-fetchlibrary": "^1.0.0-preview.79", + "@microsoft/kiota-serialization-form": "^1.0.0-preview.79", + "@microsoft/kiota-serialization-json": "^1.0.0-preview.79", + "@microsoft/kiota-serialization-multipart": "^1.0.0-preview.79", + "@microsoft/kiota-serialization-text": "^1.0.0-preview.79", "express": "^5.0.1", "node-fetch": "^2.7.0" }, @@ -782,9 +782,9 @@ } }, "node_modules/@microsoft/kiota-abstractions": { - "version": "1.0.0-preview.78", - "resolved": "https://registry.npmjs.org/@microsoft/kiota-abstractions/-/kiota-abstractions-1.0.0-preview.78.tgz", - "integrity": "sha512-xQDr3zG/i7rpwoqHLOD4zYfq4Aw39LcZoqOBN0B7m7L35v5dEPR6f7a6d0MJ4AehBqQokrIOpU/atFcG0waZTA==", + "version": "1.0.0-preview.79", + "resolved": "https://registry.npmjs.org/@microsoft/kiota-abstractions/-/kiota-abstractions-1.0.0-preview.79.tgz", + "integrity": "sha512-gHLJmYwoacLlROWm8jliY5y8GZjHOSUULTFmS7iiegXFB6vJU2Hk258LSACTm/CeE6YbJZFRiH2fXgOF5ygGHw==", "dependencies": { "@opentelemetry/api": "^1.7.0", "@std-uritemplate/std-uritemplate": "^2.0.0", @@ -806,59 +806,59 @@ } }, "node_modules/@microsoft/kiota-authentication-azure": { - "version": "1.0.0-preview.78", - "resolved": "https://registry.npmjs.org/@microsoft/kiota-authentication-azure/-/kiota-authentication-azure-1.0.0-preview.78.tgz", - "integrity": "sha512-iE67HXY6OrDHZPe3esnBnqsqhFrQj/jc2vpNjIcBLG6BwWU7LHrnbhKa3zRVN+XlQBkUvajAyRqRRV0zKa7aKA==", + "version": "1.0.0-preview.79", + "resolved": "https://registry.npmjs.org/@microsoft/kiota-authentication-azure/-/kiota-authentication-azure-1.0.0-preview.79.tgz", + "integrity": "sha512-iIfi+kFCnhCorsXr0a2ogFH42wsI2aYzUbpOqOC6Idbo8Sfowb/ZSXWqjCDNmOF9v8hKRMEFqgBPYeuetsMR6Q==", "dependencies": { "@azure/core-auth": "^1.5.0", - "@microsoft/kiota-abstractions": "^1.0.0-preview.78", + "@microsoft/kiota-abstractions": "^1.0.0-preview.79", "@opentelemetry/api": "^1.7.0", "tslib": "^2.6.2" } }, "node_modules/@microsoft/kiota-http-fetchlibrary": { - "version": "1.0.0-preview.78", - "resolved": "https://registry.npmjs.org/@microsoft/kiota-http-fetchlibrary/-/kiota-http-fetchlibrary-1.0.0-preview.78.tgz", - "integrity": "sha512-eJwwWlwna0QiB3+g6GPmdoZRShWOJh49/Ngp+q+rFQX2t2vZ1nEyagcFi9A5tV4f/qhsmrbXxRp/lmqZNl8i6Q==", + "version": "1.0.0-preview.79", + "resolved": "https://registry.npmjs.org/@microsoft/kiota-http-fetchlibrary/-/kiota-http-fetchlibrary-1.0.0-preview.79.tgz", + "integrity": "sha512-f8cB+erl7Wsdzvj1CFbQ97M6U3fgbOjzC4c4tzNCDWxNOI11SbXh0/9Qa+3mLn4fvIFJbgZ3tOM1lFEsyzGhvQ==", "dependencies": { - "@microsoft/kiota-abstractions": "^1.0.0-preview.78", + "@microsoft/kiota-abstractions": "^1.0.0-preview.79", "@opentelemetry/api": "^1.7.0", "tslib": "^2.6.2" } }, "node_modules/@microsoft/kiota-serialization-form": { - "version": "1.0.0-preview.78", - "resolved": "https://registry.npmjs.org/@microsoft/kiota-serialization-form/-/kiota-serialization-form-1.0.0-preview.78.tgz", - "integrity": "sha512-MeHAexViBpl8x82ukA1ffBMlDEap3J1v5ZJMUD1K+GKGX8ifj4jlznlYcmbIQqWZRW8+yUrtqKjQNAsCpJL/gQ==", + "version": "1.0.0-preview.79", + "resolved": "https://registry.npmjs.org/@microsoft/kiota-serialization-form/-/kiota-serialization-form-1.0.0-preview.79.tgz", + "integrity": "sha512-aGnoyOAW9f6lkxKeoA2UlQDk1CvqVzaT0GR6TbgTKSKav5ik4fI1z0bGsEwZBAq360v+mVbdYtSNBp8p6W465g==", "dependencies": { - "@microsoft/kiota-abstractions": "^1.0.0-preview.78", + "@microsoft/kiota-abstractions": "^1.0.0-preview.79", "tslib": "^2.6.2" } }, "node_modules/@microsoft/kiota-serialization-json": { - "version": "1.0.0-preview.78", - "resolved": "https://registry.npmjs.org/@microsoft/kiota-serialization-json/-/kiota-serialization-json-1.0.0-preview.78.tgz", - "integrity": "sha512-Hrk6DrrrcyDiTjxOxvS7J8SOVEUScyyBIPOtXpSPJAeoeUmoQ7UGyDu7NzLqPzVCIo+B2Ygxct3tdc5ZDoh4ng==", + "version": "1.0.0-preview.79", + "resolved": "https://registry.npmjs.org/@microsoft/kiota-serialization-json/-/kiota-serialization-json-1.0.0-preview.79.tgz", + "integrity": "sha512-90lLvKjVCknuSQQLRPJV6SBIQgcVUaCpfb8H1Dmm9I7Id+aespPDwF7F0c4JjgZGi0addzdQzCGj2ACyhnAKOg==", "dependencies": { - "@microsoft/kiota-abstractions": "^1.0.0-preview.78", + "@microsoft/kiota-abstractions": "^1.0.0-preview.79", "tslib": "^2.6.2" } }, "node_modules/@microsoft/kiota-serialization-multipart": { - "version": "1.0.0-preview.78", - "resolved": "https://registry.npmjs.org/@microsoft/kiota-serialization-multipart/-/kiota-serialization-multipart-1.0.0-preview.78.tgz", - "integrity": "sha512-CpVKtPesh2wgMSF1cJoBus+AZuaOoofaIb9YkkdPfm+L+PH/3cFCposQ+a/jgF9f4FCTe2SxxCgiP/gzh/aRdQ==", + "version": "1.0.0-preview.79", + "resolved": "https://registry.npmjs.org/@microsoft/kiota-serialization-multipart/-/kiota-serialization-multipart-1.0.0-preview.79.tgz", + "integrity": "sha512-B5lpF2iUrG5u0bwt/JyvXmIi6XWBqD9eFurFdfVt1tRlIPzoRCbkJnQOm76+tjhW5lLtbxuGbSHVCNgAn5Q3xQ==", "dependencies": { - "@microsoft/kiota-abstractions": "^1.0.0-preview.78", + "@microsoft/kiota-abstractions": "^1.0.0-preview.79", "tslib": "^2.6.2" } }, "node_modules/@microsoft/kiota-serialization-text": { - "version": "1.0.0-preview.78", - "resolved": "https://registry.npmjs.org/@microsoft/kiota-serialization-text/-/kiota-serialization-text-1.0.0-preview.78.tgz", - "integrity": "sha512-TlAF4YHwyn0CeGh1snLlB/9PeGl5+K/9OLko9S99QWIbeIlGJnU4hrJFfIBK7JIqLFZw1pPuYMVe3vh5xDlxCQ==", + "version": "1.0.0-preview.79", + "resolved": "https://registry.npmjs.org/@microsoft/kiota-serialization-text/-/kiota-serialization-text-1.0.0-preview.79.tgz", + "integrity": "sha512-dDodJTQwmIvyczZHfRPHSNorvQcKCAMu4fSETkApAWdIEIo3sDWI3+NRQnNlwNJ1dxmOV75Ks5/xPMCpsizucg==", "dependencies": { - "@microsoft/kiota-abstractions": "^1.0.0-preview.78", + "@microsoft/kiota-abstractions": "^1.0.0-preview.79", "tslib": "^2.6.2" } }, diff --git a/it/typescript/package.json b/it/typescript/package.json index 64708174e0..19489724a1 100644 --- a/it/typescript/package.json +++ b/it/typescript/package.json @@ -32,13 +32,13 @@ }, "dependencies": { "@azure/identity": "^4.5.0", - "@microsoft/kiota-abstractions": "^1.0.0-preview.78", - "@microsoft/kiota-authentication-azure": "^1.0.0-preview.78", - "@microsoft/kiota-http-fetchlibrary": "^1.0.0-preview.78", - "@microsoft/kiota-serialization-form": "^1.0.0-preview.78", - "@microsoft/kiota-serialization-json": "^1.0.0-preview.78", - "@microsoft/kiota-serialization-multipart": "^1.0.0-preview.78", - "@microsoft/kiota-serialization-text": "^1.0.0-preview.78", + "@microsoft/kiota-abstractions": "^1.0.0-preview.79", + "@microsoft/kiota-authentication-azure": "^1.0.0-preview.79", + "@microsoft/kiota-http-fetchlibrary": "^1.0.0-preview.79", + "@microsoft/kiota-serialization-form": "^1.0.0-preview.79", + "@microsoft/kiota-serialization-json": "^1.0.0-preview.79", + "@microsoft/kiota-serialization-multipart": "^1.0.0-preview.79", + "@microsoft/kiota-serialization-text": "^1.0.0-preview.79", "express": "^5.0.1", "node-fetch": "^2.7.0" } diff --git a/src/Kiota.Builder/GenerationLanguage.cs b/src/Kiota.Builder/GenerationLanguage.cs index dbd236ade7..278bdcd8b2 100644 --- a/src/Kiota.Builder/GenerationLanguage.cs +++ b/src/Kiota.Builder/GenerationLanguage.cs @@ -9,5 +9,6 @@ public enum GenerationLanguage Go, Swift, Ruby, - CLI + CLI, + Dart, } diff --git a/src/Kiota.Builder/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj index 63eb4d6f87..70463a727c 100644 --- a/src/Kiota.Builder/Kiota.Builder.csproj +++ b/src/Kiota.Builder/Kiota.Builder.csproj @@ -1,4 +1,4 @@ - + Latest enable @@ -15,7 +15,7 @@ Microsoft.OpenApi.Kiota.Builder Microsoft.OpenApi.Kiota.Builder ./nupkg - 1.22.0 + 1.23.0 $(VersionSuffix) https://github.com/microsoft/kiota/releases diff --git a/src/Kiota.Builder/PathSegmenters/DartPathSegmenter.cs b/src/Kiota.Builder/PathSegmenters/DartPathSegmenter.cs new file mode 100644 index 0000000000..4869be49e6 --- /dev/null +++ b/src/Kiota.Builder/PathSegmenters/DartPathSegmenter.cs @@ -0,0 +1,20 @@ +using System; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; +using Kiota.Builder.Writers.Go; + +namespace Kiota.Builder.PathSegmenters; + +public class DartPathSegmenter(string rootPath, string clientNamespaceName) : CommonPathSegmenter(rootPath, clientNamespaceName) +{ + public override string FileSuffix => ".dart"; + + public override string NormalizeNamespaceSegment(string segmentName) => segmentName.ToCamelCase(); + + public override string NormalizeFileName(CodeElement currentElement) => GetLastFileNameSegment(currentElement).ToSnakeCase(); + + internal string GetRelativeFileName(CodeNamespace @namespace, CodeElement element) + { + return NormalizeFileName(element); + } +} diff --git a/src/Kiota.Builder/Refiners/DartExceptionsReservedNamesProvider.cs b/src/Kiota.Builder/Refiners/DartExceptionsReservedNamesProvider.cs new file mode 100644 index 0000000000..892d157958 --- /dev/null +++ b/src/Kiota.Builder/Refiners/DartExceptionsReservedNamesProvider.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Kiota.Builder.Refiners; +public class DartExceptionsReservedNamesProvider : IReservedNamesProvider +{ + private readonly Lazy> _reservedNames = new(static () => new(StringComparer.OrdinalIgnoreCase) + { + "toString" + }); + public HashSet ReservedNames => _reservedNames.Value; +} diff --git a/src/Kiota.Builder/Refiners/DartRefiner.cs b/src/Kiota.Builder/Refiners/DartRefiner.cs new file mode 100644 index 0000000000..054415c1ad --- /dev/null +++ b/src/Kiota.Builder/Refiners/DartRefiner.cs @@ -0,0 +1,520 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Configuration; +using Kiota.Builder.Extensions; +using Kiota.Builder.Writers.Dart; + + +namespace Kiota.Builder.Refiners; +public class DartRefiner : CommonLanguageRefiner, ILanguageRefiner +{ + private const string MultipartBodyClassName = "MultipartBody"; + private const string AbstractionsNamespaceName = "microsoft_kiota_abstractions/microsoft_kiota_abstractions"; + private const string SerializationNamespaceName = "microsoft_kiota_serialization"; + private static readonly CodeUsingDeclarationNameComparer usingComparer = new(); + + protected static readonly AdditionalUsingEvaluator[] defaultUsingEvaluators = { + new (static x => x is CodeProperty prop && prop.IsOfKind(CodePropertyKind.RequestAdapter), + AbstractionsNamespaceName, "RequestAdapter"), + new (static x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.RequestGenerator), + AbstractionsNamespaceName, "Method", "RequestInformation", "RequestOption"), + new (static x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.Serializer), + AbstractionsNamespaceName, "SerializationWriter"), + new (static x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.Deserializer), + AbstractionsNamespaceName, "ParseNode"), + new (static x => x is CodeClass @class && @class.IsOfKind(CodeClassKind.Model), + AbstractionsNamespaceName, "Parsable"), + new (static x => x is CodeClass @class && @class.IsOfKind(CodeClassKind.Model) && @class.Properties.Any(x => x.IsOfKind(CodePropertyKind.AdditionalData)), + AbstractionsNamespaceName, "AdditionalDataHolder"), + new (static x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.RequestExecutor), + AbstractionsNamespaceName, "Parsable"), + new (static x => x is CodeProperty prop && prop.IsOfKind(CodePropertyKind.QueryParameter) && !string.IsNullOrEmpty(prop.SerializationName), + AbstractionsNamespaceName, "QueryParameterAttribute"), + new (static x => x is CodeClass @class && @class.OriginalComposedType is CodeIntersectionType intersectionType && intersectionType.Types.Any(static y => !y.IsExternal), + AbstractionsNamespaceName, "ParseNodeHelper"), + new (static x => x is CodeProperty prop && prop.IsOfKind(CodePropertyKind.Headers), + AbstractionsNamespaceName, "RequestHeaders"), + new (static x => x is CodeProperty prop && prop.IsOfKind(CodePropertyKind.Custom) && prop.Type.Name.Equals(KiotaBuilder.UntypedNodeName, StringComparison.OrdinalIgnoreCase), + AbstractionsNamespaceName, KiotaBuilder.UntypedNodeName), + new (static x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.RequestExecutor, CodeMethodKind.RequestGenerator) && method.Parameters.Any(static y => y.IsOfKind(CodeParameterKind.RequestBody) && y.Type.Name.Equals(MultipartBodyClassName, StringComparison.OrdinalIgnoreCase)), + AbstractionsNamespaceName, MultipartBodyClassName), + }; + + + public DartRefiner(GenerationConfiguration configuration) : base(configuration) { } + public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken cancellationToken) + { + return Task.Run(() => + { + cancellationToken.ThrowIfCancellationRequested(); + var defaultConfiguration = new GenerationConfiguration(); + + ConvertUnionTypesToWrapper(generatedCode, + _configuration.UsesBackingStore, + static s => s.ToFirstCharacterLowerCase(), + false); + ReplaceIndexersByMethodsWithParameter(generatedCode, + false, + static x => $"by{x.ToPascalCase('_')}", + static x => x.ToCamelCase('_'), + GenerationLanguage.Dart); + CorrectCommonNames(generatedCode); + var reservedNamesProvider = new DartReservedNamesProvider(); + cancellationToken.ThrowIfCancellationRequested(); + CorrectNames(generatedCode, s => + { + if (s.Contains('_', StringComparison.OrdinalIgnoreCase) && + s.ToPascalCase(UnderscoreArray) is string refinedName && + !reservedNamesProvider.ReservedNames.Contains(s) && + !reservedNamesProvider.ReservedNames.Contains(refinedName)) + return refinedName; + else + return s; + }); + CorrectCoreType(generatedCode, CorrectMethodType, CorrectPropertyType, CorrectImplements); + ReplacePropertyNames(generatedCode, + [ + CodePropertyKind.Custom, + CodePropertyKind.AdditionalData, + CodePropertyKind.QueryParameter, + CodePropertyKind.RequestBuilder, + ], + static s => s.ToCamelCase(UnderscoreArray)); + + AddQueryParameterExtractorMethod(generatedCode); + // This adds the BaseRequestBuilder class as a superclass + MoveRequestBuilderPropertiesToBaseType(generatedCode, + new CodeUsing + { + Name = "BaseRequestBuilder", + Declaration = new CodeType + { + Name = AbstractionsNamespaceName, + IsExternal = true, + } + }, addCurrentTypeAsGenericTypeParameter: true); + RemoveRequestConfigurationClasses(generatedCode, + new CodeUsing + { + Name = "RequestConfiguration", + Declaration = new CodeType + { + Name = AbstractionsNamespaceName, + IsExternal = true + } + }, new CodeType + { + Name = "DefaultQueryParameters", + IsExternal = true, + }); + MoveQueryParameterClass(generatedCode); + AddDefaultImports(generatedCode, defaultUsingEvaluators); + AddPropertiesAndMethodTypesImports(generatedCode, true, true, true, codeTypeFilter); + AddParsableImplementsForModelClasses(generatedCode, "Parsable"); + AddConstructorsForDefaultValues(generatedCode, true); + AddConstructorForErrorClass(generatedCode); + cancellationToken.ThrowIfCancellationRequested(); + AddAsyncSuffix(generatedCode); + AddDiscriminatorMappingsUsingsToParentClasses(generatedCode, "ParseNode", addUsings: true, includeParentNamespace: true); + + ReplaceReservedNames(generatedCode, reservedNamesProvider, x => $"{x}_"); + ReplaceReservedModelTypes(generatedCode, reservedNamesProvider, x => $"{x}Object"); + ReplaceReservedExceptionPropertyNames( + generatedCode, + new DartExceptionsReservedNamesProvider(), + static x => $"{x.ToFirstCharacterLowerCase()}_" + ); + + ReplaceDefaultSerializationModules( + generatedCode, + defaultConfiguration.Serializers, + new(StringComparer.OrdinalIgnoreCase) { + $"{SerializationNamespaceName}_json/{SerializationNamespaceName}_json.JsonSerializationWriterFactory", + $"{SerializationNamespaceName}_text/{SerializationNamespaceName}_text.TextSerializationWriterFactory", + $"{SerializationNamespaceName}_form/{SerializationNamespaceName}_form.FormSerializationWriterFactory", + $"{SerializationNamespaceName}_multipart/{SerializationNamespaceName}_multipart.MultipartSerializationWriterFactory", + } + ); + ReplaceDefaultDeserializationModules( + generatedCode, + defaultConfiguration.Deserializers, + new(StringComparer.OrdinalIgnoreCase) { + $"{SerializationNamespaceName}_json/{SerializationNamespaceName}_json.JsonParseNodeFactory", + $"{SerializationNamespaceName}_form/{SerializationNamespaceName}_form.FormParseNodeFactory", + $"{SerializationNamespaceName}_text/{SerializationNamespaceName}_text.TextParseNodeFactory" + } + ); + AddSerializationModulesImport(generatedCode, + [$"{AbstractionsNamespaceName}.ApiClientBuilder", + $"{AbstractionsNamespaceName}.SerializationWriterFactoryRegistry"], + [$"{AbstractionsNamespaceName}.ParseNodeFactoryRegistry"]); + cancellationToken.ThrowIfCancellationRequested(); + + AddParentClassToErrorClasses( + generatedCode, + "ApiException", + AbstractionsNamespaceName + ); + DeduplicateErrorMappings(generatedCode); + RemoveCancellationParameter(generatedCode); + DisambiguatePropertiesWithClassNames(generatedCode); + RemoveMethodByKind(generatedCode, CodeMethodKind.RawUrlBuilder); + AddCustomMethods(generatedCode); + EscapeStringValues(generatedCode); + AliasUsingWithSameSymbol(generatedCode); + }, cancellationToken); + } + + ///error classes should always have a constructor for the copyWith method + private void AddConstructorForErrorClass(CodeElement currentElement) + { + if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition && !codeClass.Methods.Where(static x => x.IsOfKind(CodeMethodKind.Constructor)).Any()) + { + codeClass.AddMethod(new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + IsAsync = false, + IsStatic = false, + Documentation = new(new() { + {"TypeName", new CodeType { + IsExternal = false, + TypeDefinition = codeClass, + } + } + }) + { + DescriptionTemplate = "Instantiates a new {TypeName} and sets the default values.", + }, + Access = AccessModifier.Public, + ReturnType = new CodeType { Name = "void", IsExternal = true }, + Parent = codeClass, + }); + } + CrawlTree(currentElement, element => AddConstructorForErrorClass(element)); + } + + /// + /// Corrects common names so they can be used with Dart. + /// This normally comes down to changing the first character to lower case. + /// GetFieldDeserializers is corrected to getFieldDeserializers + /// + private static void CorrectCommonNames(CodeElement currentElement) + { + if (currentElement is CodeMethod m && + currentElement.Parent is CodeClass parentClass) + { + parentClass.RenameChildElement(m.Name, m.Name.ToFirstCharacterLowerCase()); + parentClass.Name = parentClass.Name.ToFirstCharacterUpperCase(); + } + else if (currentElement is CodeIndexer i) + { + i.IndexParameter.Name = i.IndexParameter.Name.ToFirstCharacterLowerCase(); + } + else if (currentElement is CodeEnum e) + { + var options = e.Options.ToList(); + foreach (var option in options) + { + option.Name = DartConventionService.getCorrectedEnumName(option.Name); + option.SerializationName = option.SerializationName.Replace("'", "\\'", StringComparison.OrdinalIgnoreCase); + } + ///ensure enum options with the same corrected name get a unique name + var nameGroups = options.Select((Option, index) => new { Option, index }).GroupBy(s => s.Option.Name).ToList(); + foreach (var group in nameGroups.Where(g => g.Count() > 1)) + { + foreach (var entry in group.Skip(1).Select((g, i) => new { g, i })) + { + options[entry.g.index].Name = options[entry.g.index].Name + entry.i; + } + } + } + else if (currentElement is CodeProperty p && p.Type is CodeType propertyType && propertyType.TypeDefinition is CodeEnum && !string.IsNullOrEmpty(p.DefaultValue)) + { + p.DefaultValue = DartConventionService.getCorrectedEnumName(p.DefaultValue.Trim('"').CleanupSymbolName()); + if (new DartReservedNamesProvider().ReservedNames.Contains(p.DefaultValue)) + { + p.DefaultValue += "_"; + } + } + CrawlTree(currentElement, element => CorrectCommonNames(element)); + } + + private static void CorrectMethodType(CodeMethod currentMethod) + { + if (currentMethod.IsOfKind(CodeMethodKind.Serializer)) + currentMethod.Parameters.Where(x => x.IsOfKind(CodeParameterKind.Serializer)).ToList().ForEach(x => + { + x.Optional = false; + x.Type.IsNullable = true; + if (x.Type.Name.StartsWith('I')) + x.Type.Name = x.Type.Name[1..]; + }); + else if (currentMethod.IsOfKind(CodeMethodKind.Deserializer)) + { + currentMethod.ReturnType.Name = "Map"; + currentMethod.Name = "getFieldDeserializers"; + } + else if (currentMethod.IsOfKind(CodeMethodKind.RawUrlConstructor, CodeMethodKind.ClientConstructor)) + { + currentMethod.Parameters.Where(x => x.IsOfKind(CodeParameterKind.RequestAdapter, CodeParameterKind.BackingStore)) + .Where(x => x.Type.Name.StartsWith('I')) + .ToList() + .ForEach(x => x.Type.Name = x.Type.Name[1..]); // removing the "I" + } + CorrectCoreTypes(currentMethod.Parent as CodeClass, DateTypesReplacements, currentMethod.Parameters + .Select(static x => x.Type) + .Union(new[] { currentMethod.ReturnType }) + .ToArray()); + currentMethod.Parameters.ToList().ForEach(static x => x.Name = x.Name.ToFirstCharacterLowerCase()); + } + + private static void CorrectPropertyType(CodeProperty currentProperty) + { + ArgumentNullException.ThrowIfNull(currentProperty); + + if (currentProperty.IsOfKind(CodePropertyKind.Options)) + currentProperty.DefaultValue = "List()"; + else if (currentProperty.IsOfKind(CodePropertyKind.Headers)) + currentProperty.DefaultValue = $"{currentProperty.Type.Name.ToFirstCharacterLowerCase()}()"; + else if (currentProperty.IsOfKind(CodePropertyKind.RequestAdapter)) + { + currentProperty.Type.Name = "RequestAdapter"; + currentProperty.Type.IsNullable = true; + } + else if (currentProperty.IsOfKind(CodePropertyKind.BackingStore)) + { + currentProperty.Type.Name = currentProperty.Type.Name[1..]; // removing the "I" + currentProperty.Name = currentProperty.Name.ToFirstCharacterLowerCase(); + } + else if (currentProperty.IsOfKind(CodePropertyKind.QueryParameter)) + { + currentProperty.DefaultValue = $"{currentProperty.Type.Name.ToFirstCharacterUpperCase()}()"; + } + else if (currentProperty.IsOfKind(CodePropertyKind.AdditionalData)) + { + currentProperty.Type.Name = "Map"; + currentProperty.DefaultValue = "{}"; + currentProperty.Name = currentProperty.Name.ToFirstCharacterLowerCase(); + } + else if (currentProperty.IsOfKind(CodePropertyKind.UrlTemplate)) + { + currentProperty.Type.IsNullable = true; + } + else if (currentProperty.IsOfKind(CodePropertyKind.PathParameters)) + { + currentProperty.Type.IsNullable = true; + currentProperty.Type.Name = "Map"; + if (!string.IsNullOrEmpty(currentProperty.DefaultValue)) + currentProperty.DefaultValue = "{}"; + } + else + { + currentProperty.Name = currentProperty.Name.ToFirstCharacterLowerCase(); + } + currentProperty.Type.Name = currentProperty.Type.Name.ToFirstCharacterUpperCase(); + CorrectCoreTypes(currentProperty.Parent as CodeClass, DateTypesReplacements, currentProperty.Type); + } + + private static void CorrectImplements(ProprietableBlockDeclaration block) + { + block.Implements.Where(x => "IAdditionalDataHolder".Equals(x.Name, StringComparison.OrdinalIgnoreCase) || "IBackedModel".Equals(x.Name, StringComparison.OrdinalIgnoreCase)).ToList().ForEach(x => x.Name = x.Name[1..]); // skipping the I + } + public static IEnumerable codeTypeFilter(IEnumerable usingsToAdd) + { + var result = usingsToAdd.OfType().Except(usingsToAdd.Where(static codeType => codeType.Parent is ClassDeclaration declaration && declaration.Parent is CodeClass codeClass && codeClass.IsErrorDefinition)); + var genericParameterTypes = usingsToAdd.OfType().Where( + static codeType => codeType.Parent is CodeParameter parameter + && parameter.IsOfKind(CodeParameterKind.RequestConfiguration)).Select(x => x.GenericTypeParameterValues.First()); + + return result.Union(genericParameterTypes); + } + protected static void AddAsyncSuffix(CodeElement currentElement) + { + if (currentElement is CodeMethod currentMethod && currentMethod.IsAsync) + currentMethod.Name += "Async"; + CrawlTree(currentElement, AddAsyncSuffix); + } + private void AddQueryParameterExtractorMethod(CodeElement currentElement, string methodName = "toMap") + { + if (currentElement is CodeClass currentClass && + currentClass.IsOfKind(CodeClassKind.QueryParameters)) + { + currentClass.StartBlock.AddImplements(new CodeType + { + IsExternal = true, + Name = "AbstractQueryParameters" + }); + currentClass.AddMethod(new CodeMethod + { + Name = methodName, + Access = AccessModifier.Public, + ReturnType = new CodeType + { + Name = "Map", + IsNullable = false, + }, + IsAsync = false, + IsStatic = false, + Kind = CodeMethodKind.QueryParametersMapper, + Documentation = new() + { + DescriptionTemplate = "Extracts the query parameters into a map for the URI template parsing.", + }, + }); + currentClass.AddUsing(new CodeUsing + { + Name = "AbstractQueryParameters", + Declaration = new CodeType { Name = AbstractionsNamespaceName, IsExternal = true }, + }); + } + CrawlTree(currentElement, x => AddQueryParameterExtractorMethod(x, methodName)); + } + + private void MoveQueryParameterClass(CodeElement currentElement) + { + if (currentElement is CodeClass currentClass && + currentClass.IsOfKind(CodeClassKind.RequestBuilder)) + { + var parentNamespace = currentClass.GetImmediateParentOfType(); + var nestedClasses = currentClass.InnerClasses.Where(x => x.IsOfKind(CodeClassKind.QueryParameters)); + foreach (CodeClass nestedClass in nestedClasses) + { + parentNamespace.AddClass(nestedClass); + currentClass.RemoveChildElementByName(nestedClass.Name); + } + } + CrawlTree(currentElement, x => MoveQueryParameterClass(x)); + } + + protected static void DisambiguatePropertiesWithClassNames(CodeElement currentElement) + { + if (currentElement is CodeClass currentClass) + { + var sameNameProperty = currentClass.Properties + .FirstOrDefault(x => x.Name.Equals(currentClass.Name, StringComparison.OrdinalIgnoreCase)); + if (sameNameProperty != null) + { + currentClass.RemoveChildElement(sameNameProperty); + if (string.IsNullOrEmpty(sameNameProperty.SerializationName)) + sameNameProperty.SerializationName = sameNameProperty.Name; + sameNameProperty.Name = $"{sameNameProperty.Name}Prop"; + currentClass.AddProperty(sameNameProperty); + } + } + CrawlTree(currentElement, DisambiguatePropertiesWithClassNames); + } + private void AddCustomMethods(CodeElement currentElement) + { + if (currentElement is CodeClass currentClass) + { + if (currentClass.IsOfKind(CodeClassKind.RequestBuilder)) + { + currentClass.AddMethod(new CodeMethod + { + Name = "clone", + Access = AccessModifier.Public, + ReturnType = new CodeType + { + Name = currentClass.Name, + IsNullable = false, + }, + IsAsync = false, + IsStatic = false, + + Kind = CodeMethodKind.Custom, + Documentation = new() + { + DescriptionTemplate = "Clones the requestbuilder.", + }, + }); + } + if (currentClass.IsOfKind(CodeClassKind.Model) && currentClass.IsErrorDefinition) + { + currentClass.AddMethod(new CodeMethod + { + Name = "copyWith", + Access = AccessModifier.Public, + ReturnType = new CodeType + { + Name = currentClass.Name, + IsNullable = false, + }, + IsAsync = false, + IsStatic = false, + + Kind = CodeMethodKind.Custom, + Documentation = new() + { + DescriptionTemplate = "Creates a copy of the object.", + }, + }); + } + } + CrawlTree(currentElement, x => AddCustomMethods(x)); + } + + private void EscapeStringValues(CodeElement currentElement) + { + if (currentElement is CodeProperty property) + { + if (!String.IsNullOrEmpty(property.SerializationName) && property.SerializationName.Contains('$', StringComparison.Ordinal)) + { + property.SerializationName = property.SerializationName.Replace("$", "\\$", StringComparison.Ordinal); + } + if (property.DefaultValue.Contains('$', StringComparison.Ordinal)) + { + property.DefaultValue = property.DefaultValue.Replace("$", "\\$", StringComparison.Ordinal); + } + } + else if (currentElement is CodeMethod method && method.HasUrlTemplateOverride) + { + method.UrlTemplateOverride = method.UrlTemplateOverride.Replace("$", "\\$", StringComparison.Ordinal); + } + CrawlTree(currentElement, EscapeStringValues); + } + + private static readonly Dictionary DateTypesReplacements = new(StringComparer.OrdinalIgnoreCase) { + + {"TimeSpan", ("Duration", null)}, + {"DateTimeOffset", ("DateTime", null)}, + {"Guid", ("UuidValue", new CodeUsing { + Name = "UuidValue", + Declaration = new CodeType { + Name = "uuid/uuid", + IsExternal = true, + }, + })}, + }; + private static void AliasUsingWithSameSymbol(CodeElement currentElement) + { + if (currentElement is CodeClass currentClass && currentClass.StartBlock != null && currentClass.StartBlock.Usings.Any(x => !x.IsExternal)) + { + var duplicatedSymbolsUsings = currentClass.StartBlock.Usings + .Distinct(usingComparer) + .Where(static x => !string.IsNullOrEmpty(x.Declaration?.Name) && x.Declaration.TypeDefinition != null) + .GroupBy(static x => x.Declaration!.Name, StringComparer.OrdinalIgnoreCase) + .Where(x => x.Count() > 1) + .SelectMany(x => x) + .Union(currentClass.StartBlock + .Usings + .Where(x => !x.IsExternal) + .Where(x => x.Declaration! + .Name + .Equals(currentClass.Name, StringComparison.OrdinalIgnoreCase))); + foreach (var usingElement in duplicatedSymbolsUsings) + { + var replacement = string.Join("_", usingElement.Declaration!.TypeDefinition!.GetImmediateParentOfType().Name + .Split(".", StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.ToLowerInvariant()) + .ToArray()); + usingElement.Alias = $"{(string.IsNullOrEmpty(replacement) ? string.Empty : $"{replacement}")}_{usingElement.Declaration!.TypeDefinition!.Name.ToLowerInvariant()}"; + } + } + CrawlTree(currentElement, AliasUsingWithSameSymbol); + } +} diff --git a/src/Kiota.Builder/Refiners/DartReservedNamesProvider.cs b/src/Kiota.Builder/Refiners/DartReservedNamesProvider.cs new file mode 100644 index 0000000000..18d1abeecf --- /dev/null +++ b/src/Kiota.Builder/Refiners/DartReservedNamesProvider.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; + +namespace Kiota.Builder.Refiners; +public class DartReservedNamesProvider : IReservedNamesProvider +{ + private readonly Lazy> _reservedNames = new(() => new(StringComparer.OrdinalIgnoreCase) { + "abstract", + "as", + "assert", + "async", + "await", + "base", + "bool", + "break", + "case", + "catch", + "class", + "const", + "continue", + "covariant", + "default", + "deferred", + "do", + "double", + "dynamic", + "else", + "enum", + "export", + "extends", + "extension", + "external", + "factory", + "false", + "final", + "finally", + "for", + "Function", + "get", + "hide", + "if", + "implements", + "import", + "in", + "index", + "int", + "interface", + "is", + "late", + "library", + "mixin", + "new", + "null", + "of", + "on", + "operator", + "part", + "required", + "rethrow", + "return", + "sealed", + "set", + "show", + "static", + "stream", + "string", + "super", + "switch", + "sync", + "this", + "throw", + "true", + "try", + "type", + "typedef", + "var", + "void", + "when", + "with", + "while", + "yield", + "BaseRequestBuilder", + "clone" + }); + public HashSet ReservedNames => _reservedNames.Value; +} diff --git a/src/Kiota.Builder/Refiners/ILanguageRefiner.cs b/src/Kiota.Builder/Refiners/ILanguageRefiner.cs index 2b1b4df7d8..37b42e51cb 100644 --- a/src/Kiota.Builder/Refiners/ILanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/ILanguageRefiner.cs @@ -40,6 +40,9 @@ public static async Task RefineAsync(GenerationConfiguration config, CodeNamespa case GenerationLanguage.Python: await new PythonRefiner(config).RefineAsync(generatedCode, cancellationToken).ConfigureAwait(false); break; + case GenerationLanguage.Dart: + await new DartRefiner(config).RefineAsync(generatedCode, cancellationToken).ConfigureAwait(false); + break; } } } diff --git a/src/Kiota.Builder/Writers/Dart/CodeBlockEndWriter.cs b/src/Kiota.Builder/Writers/Dart/CodeBlockEndWriter.cs new file mode 100644 index 0000000000..caf0f81346 --- /dev/null +++ b/src/Kiota.Builder/Writers/Dart/CodeBlockEndWriter.cs @@ -0,0 +1,12 @@ +using System; +using Kiota.Builder.CodeDOM; + +namespace Kiota.Builder.Writers.Dart; +public class CodeBlockEndWriter : ICodeElementWriter +{ + public void WriteCodeElement(BlockEnd codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(writer); + writer.CloseBlock(); + } +} diff --git a/src/Kiota.Builder/Writers/Dart/CodeClassDeclarationWriter.cs b/src/Kiota.Builder/Writers/Dart/CodeClassDeclarationWriter.cs new file mode 100644 index 0000000000..0499688ac6 --- /dev/null +++ b/src/Kiota.Builder/Writers/Dart/CodeClassDeclarationWriter.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.PathSegmenters; + +namespace Kiota.Builder.Writers.Dart; +public class CodeClassDeclarationWriter : BaseElementWriter +{ + private readonly RelativeImportManager relativeImportManager; + + public CodeClassDeclarationWriter(DartConventionService conventionService, string clientNamespaceName, DartPathSegmenter pathSegmenter) : base(conventionService) + { + ArgumentNullException.ThrowIfNull(pathSegmenter); + relativeImportManager = new RelativeImportManager(clientNamespaceName, '.', pathSegmenter.GetRelativeFileName); + } + + public override void WriteCodeElement(ClassDeclaration codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + + if (codeElement.Parent is not CodeClass parentClass) + throw new InvalidOperationException($"The provided code element {codeElement.Name} doesn't have a parent of type {nameof(CodeClass)}"); + conventions.WriteLintingMessage(writer); + var currentNamespace = codeElement.GetImmediateParentOfType(); + + if (codeElement.Parent?.Parent is CodeNamespace) + { + foreach (var externalPath in codeElement.Usings + .Where(x => (x.Declaration?.IsExternal ?? true) || !x.Declaration.Name.Equals(codeElement.Name, StringComparison.OrdinalIgnoreCase)) // needed for circular requests patterns like message folder + .Where(static x => x.IsExternal) + .DistinctBy(static x => x.Declaration!.Name, StringComparer.Ordinal) + .OrderBy(static x => x.Declaration!.Name, StringComparer.Ordinal)) + writer.WriteLine($"import 'package:{externalPath.Declaration!.Name}.dart';"); + + foreach (var relativePath in codeElement.Usings + .Where(static x => !x.IsExternal) + .DistinctBy(static x => $"{x.Name}{x.Declaration?.Name}", StringComparer.OrdinalIgnoreCase) + .Select(x => x.Declaration?.Name?.StartsWith('.') ?? false ? + (string.Empty, x.Alias, x.Declaration.Name) : + relativeImportManager.GetRelativeImportPathForUsing(x, currentNamespace)) + .OrderBy(static x => x.Item3, StringComparer.Ordinal)) + writer.WriteLine($"import '{relativePath.Item3}.dart'{GetAlias(relativePath.Item2)};"); + + writer.WriteLine(); + + } + + var derivedTypes = (codeElement.Inherits is null ? Enumerable.Empty() : [conventions.GetTypeString(codeElement.Inherits, parentClass)]).ToArray(); + var derivation = derivedTypes.Length != 0 ? " extends " + derivedTypes.Aggregate(static (x, y) => $"{x}, {y}") : string.Empty; + var implements = !codeElement.Implements.Any() ? string.Empty : $" implements {codeElement.Implements.Select(static x => x.Name).Aggregate(static (x, y) => x + ", " + y)}"; + + conventions.WriteAutogeneratedMessage(writer); + conventions.WriteLongDescription(parentClass, writer); + conventions.WriteDeprecationAttribute(parentClass, writer); + writer.StartBlock($"class {codeElement.Name}{derivation}{implements} {{"); + } + + private static String GetAlias(string alias) + { + return string.IsNullOrEmpty(alias) ? string.Empty : $" as {alias}"; + } +} diff --git a/src/Kiota.Builder/Writers/Dart/CodeEnumWriter.cs b/src/Kiota.Builder/Writers/Dart/CodeEnumWriter.cs new file mode 100644 index 0000000000..d736fb80c5 --- /dev/null +++ b/src/Kiota.Builder/Writers/Dart/CodeEnumWriter.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; + +using Kiota.Builder.CodeDOM; + +namespace Kiota.Builder.Writers.Dart; +public class CodeEnumWriter : BaseElementWriter +{ + public CodeEnumWriter(DartConventionService conventionService) : base(conventionService) { } + public override void WriteCodeElement(CodeEnum codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + if (!codeElement.Options.Any()) + return; + var enumName = codeElement.Name; + conventions.WriteLintingMessage(writer); + conventions.WriteAutogeneratedMessage(writer); + conventions.WriteShortDescription(codeElement, writer); + conventions.WriteDeprecationAttribute(codeElement, writer); + writer.StartBlock($"enum {enumName} {{"); + + var options = codeElement.Options; + var lastOption = options.Last(); + + foreach (var option in options) + { + conventions.WriteShortDescription(option, writer); + + var serializationName = option.SerializationName; + writer.WriteLine($"{option.Name}('{serializationName}'){(option == lastOption ? ";" : ",")}"); + } + writer.WriteLine($"const {enumName}(this.value);"); + writer.WriteLine("final String value;"); + } +} diff --git a/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs new file mode 100644 index 0000000000..83ff8ccbb3 --- /dev/null +++ b/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs @@ -0,0 +1,865 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; +using Kiota.Builder.OrderComparers; +using static Kiota.Builder.CodeDOM.CodeTypeBase; + +namespace Kiota.Builder.Writers.Dart; +public class CodeMethodWriter : BaseElementWriter +{ + public CodeMethodWriter(DartConventionService conventionService) : base(conventionService) + { + + } + public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + if (codeElement.ReturnType == null) throw new InvalidOperationException($"{nameof(codeElement.ReturnType)} should not be null"); + ArgumentNullException.ThrowIfNull(writer); + if (codeElement.Parent is not CodeClass parentClass) throw new InvalidOperationException("the parent of a method should be a class"); + + var returnType = conventions.GetTypeString(codeElement.ReturnType, codeElement); + var inherits = parentClass.StartBlock.Inherits != null && !parentClass.IsErrorDefinition; + var isVoid = conventions.VoidTypeName.Equals(returnType, StringComparison.OrdinalIgnoreCase); + WriteMethodDocumentation(codeElement, writer); + WriteMethodPrototype(codeElement, parentClass, writer, returnType, inherits, isVoid); + writer.IncreaseIndent(); + + HandleMethodKind(codeElement, writer, inherits, parentClass, isVoid); + var isConstructor = codeElement.IsOfKind(CodeMethodKind.Constructor, CodeMethodKind.ClientConstructor, CodeMethodKind.RawUrlConstructor); + + if (HasEmptyConstructorBody(codeElement, parentClass, isConstructor)) + { + writer.DecreaseIndent(); + } + else + { + if (isConstructor && !inherits && parentClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData).Any() && !parentClass.IsErrorDefinition && !parentClass.Properties.Where(static x => x.Kind is CodePropertyKind.BackingStore).Any()) + { + writer.DecreaseIndent(); + } + else if (isConstructor && parentClass.IsErrorDefinition) + { + if (parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.AdditionalData)).Any() && parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.BackingStore)).Any()) + { + writer.CloseBlock("}) {additionalData = {};}"); + } + else + { + writer.CloseBlock("});"); + } + } + else + { + writer.CloseBlock(); + } + } + } + + private static bool HasEmptyConstructorBody(CodeMethod codeElement, CodeClass parentClass, bool isConstructor) + { + if (parentClass.IsOfKind(CodeClassKind.Model) && codeElement.IsOfKind(CodeMethodKind.Constructor) && !parentClass.IsErrorDefinition) + { + return parentClass.Properties.All(prop => string.IsNullOrEmpty(prop.DefaultValue)); + } + var hasBody = codeElement.Parameters.Any(p => !p.IsOfKind(CodeParameterKind.RequestAdapter) && !p.IsOfKind(CodeParameterKind.PathParameters)); + return isConstructor && parentClass.IsOfKind(CodeClassKind.RequestBuilder) && !codeElement.IsOfKind(CodeMethodKind.ClientConstructor) && (!hasBody || codeElement.IsOfKind(CodeMethodKind.RawUrlConstructor)); + } + + protected virtual void HandleMethodKind(CodeMethod codeElement, LanguageWriter writer, bool doesInherit, CodeClass parentClass, bool isVoid) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + ArgumentNullException.ThrowIfNull(parentClass); + var returnType = conventions.GetTypeString(codeElement.ReturnType, codeElement); + var returnTypeWithoutCollectionInformation = conventions.GetTypeString(codeElement.ReturnType, codeElement, false); + var requestBodyParam = codeElement.Parameters.OfKind(CodeParameterKind.RequestBody); + var requestConfig = codeElement.Parameters.OfKind(CodeParameterKind.RequestConfiguration); + var requestContentType = codeElement.Parameters.OfKind(CodeParameterKind.RequestBodyContentType); + var requestParams = new RequestParams(requestBodyParam, requestConfig, requestContentType); + + switch (codeElement.Kind) + { + case CodeMethodKind.Serializer: + WriteSerializerBody(doesInherit, codeElement, parentClass, writer); + break; + case CodeMethodKind.RequestGenerator: + WriteRequestGeneratorBody(codeElement, requestParams, parentClass, writer); + break; + case CodeMethodKind.RequestExecutor: + WriteRequestExecutorBody(codeElement, requestParams, parentClass, isVoid, returnTypeWithoutCollectionInformation, writer); + break; + case CodeMethodKind.Deserializer: + WriteDeserializerBody(doesInherit, codeElement, parentClass, writer); + break; + case CodeMethodKind.ClientConstructor: + WriteConstructorBody(parentClass, codeElement, writer); + WriteApiConstructorBody(parentClass, codeElement, writer); + break; + case CodeMethodKind.RawUrlBuilder: + WriteRawUrlBuilderBody(parentClass, codeElement, writer); + break; + case CodeMethodKind.Constructor: + case CodeMethodKind.RawUrlConstructor: + WriteConstructorBody(parentClass, codeElement, writer); + break; + case CodeMethodKind.IndexerBackwardCompatibility: + case CodeMethodKind.RequestBuilderWithParameters: + WriteRequestBuilderBody(parentClass, codeElement, writer); + break; + case CodeMethodKind.QueryParametersMapper: + WriteQueryparametersBody(parentClass, writer); + break; + case CodeMethodKind.Getter: + case CodeMethodKind.Setter: + throw new InvalidOperationException("getters and setters are automatically added on fields in Dart"); + case CodeMethodKind.RequestBuilderBackwardCompatibility: + throw new InvalidOperationException("RequestBuilderBackwardCompatibility is not supported as the request builders are implemented by properties."); + case CodeMethodKind.ErrorMessageOverride: + throw new InvalidOperationException("ErrorMessageOverride is not supported as the error message is implemented by a property."); + case CodeMethodKind.CommandBuilder: + throw new InvalidOperationException("CommandBuilder methods are not implemented in this SDK. They're currently only supported in the shell language."); + case CodeMethodKind.Factory: + WriteFactoryMethodBody(codeElement, parentClass, writer); + break; + case CodeMethodKind.Custom: + WriteCustomMethodBody(codeElement, parentClass, writer); + break; + case CodeMethodKind.ComposedTypeMarker: + throw new InvalidOperationException("ComposedTypeMarker is not required as interface is explicitly implemented."); + default: + writer.WriteLine("return null;"); + break; + } + } + private void WriteRawUrlBuilderBody(CodeClass parentClass, CodeMethod codeElement, LanguageWriter writer) + { + var rawUrlParameter = codeElement.Parameters.OfKind(CodeParameterKind.RawUrl) ?? throw new InvalidOperationException("RawUrlBuilder method should have a RawUrl parameter"); + var requestAdapterProperty = parentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter) ?? throw new InvalidOperationException("RawUrlBuilder method should have a RequestAdapter property"); + writer.WriteLine($"return {parentClass.Name}.withUrl({rawUrlParameter.Name}, {requestAdapterProperty.Name});"); + } + private static readonly CodePropertyTypeComparer CodePropertyTypeForwardComparer = new(); + private static readonly CodePropertyTypeComparer CodePropertyTypeBackwardComparer = new(true); + private void WriteFactoryMethodBodyForInheritedModel(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + writer.StartBlock($"return switch({DiscriminatorMappingVarName}) {{"); + foreach (var mappedType in parentClass.DiscriminatorInformation.DiscriminatorMappings) + { + writer.WriteLine($"'{mappedType.Key}' => {conventions.GetTypeString(mappedType.Value.AllTypes.First(), codeElement)}(),"); + } + writer.WriteLine($"_ => {parentClass.Name}(),"); + writer.CloseBlock("};"); + } + private const string ResultVarName = "result"; + private void WriteFactoryMethodBodyForUnionModel(CodeMethod codeElement, CodeClass parentClass, CodeParameter parseNodeParameter, LanguageWriter writer) + { + writer.WriteLine($"var {ResultVarName} = {parentClass.Name}();"); + + if (parentClass.GetPropertiesOfKind(CodePropertyKind.Custom).Where(static x => x.Type is CodeType cType && cType.TypeDefinition is CodeClass && !cType.IsCollection).Any()) + { + var discriminatorPropertyName = parentClass.DiscriminatorInformation.DiscriminatorPropertyName; + discriminatorPropertyName = discriminatorPropertyName.StartsWith('$') ? "\\" + discriminatorPropertyName : discriminatorPropertyName; + writer.WriteLine($"var {DiscriminatorMappingVarName} = {parseNodeParameter.Name}.getChildNode('{discriminatorPropertyName}')?.getStringValue();"); + } + var includeElse = false; + foreach (var property in parentClass.GetPropertiesOfKind(CodePropertyKind.Custom) + .OrderBy(static x => x, CodePropertyTypeForwardComparer) + .ThenBy(static x => x.Name, StringComparer.Ordinal)) + { + if (property.Type is CodeType propertyType) + if (propertyType.TypeDefinition is CodeClass && !propertyType.IsCollection) + { + var mappedType = parentClass.DiscriminatorInformation.DiscriminatorMappings.FirstOrDefault(x => x.Value.Name.Equals(propertyType.Name, StringComparison.OrdinalIgnoreCase)); + writer.StartBlock($"{(includeElse ? "else " : string.Empty)}if('{mappedType.Key}' == {DiscriminatorMappingVarName}) {{"); + writer.WriteLine($"{ResultVarName}.{property.Name} = {conventions.GetTypeString(propertyType, codeElement)}();"); + writer.CloseBlock(); + } + else if (propertyType.TypeDefinition is CodeClass && propertyType.IsCollection || propertyType.TypeDefinition is null || propertyType.TypeDefinition is CodeEnum) + { + var typeName = conventions.GetTypeString(propertyType, codeElement, true, false); + var check = propertyType.IsCollection ? ".isNotEmpty" : $" is {typeName}"; + writer.StartBlock($"{(includeElse ? "else " : string.Empty)}if({parseNodeParameter.Name}.{GetDeserializationMethodName(propertyType, codeElement)}{check}) {{"); + writer.WriteLine($"{ResultVarName}.{property.Name} = {parseNodeParameter.Name}.{GetDeserializationMethodName(propertyType, codeElement)};"); + writer.CloseBlock(); + } + if (!includeElse) + includeElse = true; + } + writer.WriteLine($"return {ResultVarName};"); + } + private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, CodeClass parentClass, CodeParameter parseNodeParameter, LanguageWriter writer) + { + writer.WriteLine($"var {ResultVarName} = {parentClass.Name}();"); + var includeElse = false; + foreach (var property in parentClass.GetPropertiesOfKind(CodePropertyKind.Custom) + .Where(static x => x.Type is not CodeType propertyType || propertyType.IsCollection || propertyType.TypeDefinition is not CodeClass) + .OrderBy(static x => x, CodePropertyTypeBackwardComparer) + .ThenBy(static x => x.Name, StringComparer.Ordinal)) + { + if (property.Type is CodeType propertyType) + { + var check = propertyType.IsCollection ? ".isNotEmpty" : " != null"; + writer.StartBlock($"{(includeElse ? "else " : string.Empty)}if({parseNodeParameter.Name}.{GetDeserializationMethodName(propertyType, codeElement)}{check}) {{"); + writer.WriteLine($"{ResultVarName}.{property.Name} = {parseNodeParameter.Name}.{GetDeserializationMethodName(propertyType, codeElement)};"); + writer.CloseBlock(); + } + if (!includeElse) + includeElse = true; + } + var complexProperties = parentClass.GetPropertiesOfKind(CodePropertyKind.Custom) + .Where(static x => x.Type is CodeType xType && xType.TypeDefinition is CodeClass && !xType.IsCollection) + .Select(static x => new Tuple(x, (CodeType)x.Type)) + .ToArray(); + if (complexProperties.Length != 0) + { + if (includeElse) + { + writer.StartBlock("else {"); + } + foreach (var property in complexProperties) + writer.WriteLine($"{ResultVarName}.{property.Item1.Name} = {conventions.GetTypeString(property.Item2, codeElement)}();"); + if (includeElse) + { + writer.CloseBlock(); + } + } + writer.WriteLine($"return {ResultVarName};"); + } + + private const string DiscriminatorMappingVarName = "mappingValue"; + private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + var parseNodeParameter = codeElement.Parameters.OfKind(CodeParameterKind.ParseNode) ?? throw new InvalidOperationException("Factory method should have a ParseNode parameter"); + + if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForInheritedType) + { + var discriminatorPropertyName = parentClass.DiscriminatorInformation.DiscriminatorPropertyName; + discriminatorPropertyName = discriminatorPropertyName.StartsWith('$') ? "\\" + discriminatorPropertyName : discriminatorPropertyName; + writer.WriteLine($"var {DiscriminatorMappingVarName} = {parseNodeParameter.Name}.getChildNode('{discriminatorPropertyName}')?.getStringValue();"); + WriteFactoryMethodBodyForInheritedModel(codeElement, parentClass, writer); + } + else if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForUnionType) + WriteFactoryMethodBodyForUnionModel(codeElement, parentClass, parseNodeParameter, writer); + else if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForIntersectionType) + WriteFactoryMethodBodyForIntersectionModel(codeElement, parentClass, parseNodeParameter, writer); + else if (parentClass.IsErrorDefinition && parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.AdditionalData)).Any() && !parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.BackingStore)).Any()) + { + writer.WriteLine($"return {parentClass.Name}(additionalData: {{}});"); + } + else + writer.WriteLine($"return {parentClass.Name}();"); + } + + 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(static x => x.IsOfKind(CodeParameterKind.Path)), customParameters: codeElement.Parameters.Where(static x => x.IsOfKind(CodeParameterKind.Custom))); + } + private static void WriteApiConstructorBody(CodeClass parentClass, CodeMethod method, LanguageWriter writer) + { + if (parentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter) is not CodeProperty requestAdapterProperty) return; + var pathParametersProperty = parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters); + var backingStoreParameter = method.Parameters.OfKind(CodeParameterKind.BackingStore); + var requestAdapterPropertyName = requestAdapterProperty.Name; + WriteSerializationRegistration(method.SerializerModules, writer, "registerDefaultSerializer"); + WriteSerializationRegistration(method.DeserializerModules, writer, "registerDefaultDeserializer"); + if (!string.IsNullOrEmpty(method.BaseUrl)) + { + writer.StartBlock($"if ({requestAdapterPropertyName}.baseUrl == null || {requestAdapterPropertyName}.baseUrl!.isEmpty) {{"); + writer.WriteLine($"{requestAdapterPropertyName}.baseUrl = '{method.BaseUrl}';"); + writer.CloseBlock(); + if (pathParametersProperty != null) + writer.WriteLine($"{pathParametersProperty.Name}['baseurl'] = {requestAdapterPropertyName}.baseUrl;"); + } + if (backingStoreParameter != null) + { + writer.StartBlock($"if ({backingStoreParameter.Name} != null) {{"); + writer.WriteLine($"{requestAdapterPropertyName}.enableBackingStore({backingStoreParameter.Name});"); + writer.CloseBlock(); + } + } + private static void WriteSerializationRegistration(HashSet serializationClassNames, LanguageWriter writer, string methodName) + { + if (serializationClassNames != null) + foreach (var serializationClassName in serializationClassNames) + writer.WriteLine($"ApiClientBuilder.{methodName}({serializationClassName}.new);"); + } + private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer) + { + if (parentClass.IsErrorDefinition) + { + WriteErrorClassConstructor(parentClass, writer); + } + else + { + var separator = ','; + var propWithDefaults = parentClass.Properties + .Where(static x => !string.IsNullOrEmpty(x.DefaultValue) && !x.IsOfKind(CodePropertyKind.UrlTemplate, CodePropertyKind.PathParameters, CodePropertyKind.BackingStore)) + // do not apply the default value if the type is composed as the default value may not necessarily which type to use + .Where(static x => x.Type is not CodeType propType || propType.TypeDefinition is not CodeClass propertyClass || propertyClass.OriginalComposedType is null) + .OrderByDescending(static x => x.Kind) + .ThenBy(static x => x.Name).ToArray(); + var lastOption = propWithDefaults.LastOrDefault(); + + foreach (var propWithDefault in propWithDefaults) + { + var defaultValue = propWithDefault.DefaultValue; + if (propWithDefault == lastOption) + { + separator = ';'; + } + if (propWithDefault.Type is CodeType propertyType && propertyType.TypeDefinition is CodeEnum) + { + defaultValue = $"{conventions.GetTypeString(propWithDefault.Type, currentMethod).TrimEnd('?')}.{defaultValue}"; + } + else if (propWithDefault.Type is CodeType propertyType2) + { + defaultValue = defaultValue.Trim('"'); + if (propertyType2.Name.Equals("String", StringComparison.Ordinal)) + { + defaultValue = $"'{defaultValue}'"; + } + } + writer.WriteLine($"{propWithDefault.Name} = {defaultValue}{separator}"); + } + if (parentClass.IsOfKind(CodeClassKind.RequestBuilder) && + parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is CodeProperty pathParametersProp && + currentMethod.IsOfKind(CodeMethodKind.Constructor) && + currentMethod.Parameters.OfKind(CodeParameterKind.PathParameters) is CodeParameter pathParametersParam) + { + var pathParameters = currentMethod.Parameters.Where(static x => x.IsOfKind(CodeParameterKind.Path)); + if (pathParameters.Any()) + conventions.AddParametersAssignment(writer, + pathParametersParam.Type, + pathParametersParam.Name, + pathParametersProp.Name, + currentMethod.Parameters + .Where(static x => x.IsOfKind(CodeParameterKind.Path)) + .Select(static x => (x.Type, string.IsNullOrEmpty(x.SerializationName) ? x.Name : x.SerializationName, x.Name)) + .ToArray()); + } + } + } + + private void WriteErrorClassConstructor(CodeClass parentClass, LanguageWriter writer) + { + foreach (string prop in DartConventionService.ErrorClassProperties) + { + writer.WriteLine($"super.{prop},"); + } + if (!parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.BackingStore)).Any()) + { + foreach (CodeProperty prop in parentClass.GetPropertiesOfKind(CodePropertyKind.Custom, CodePropertyKind.AdditionalData)) + { + var required = prop.Type.IsNullable ? "" : "required "; + + if (!conventions.ErrorClassPropertyExistsInSuperClass(prop)) + { + writer.WriteLine($"{required}this.{prop.Name},"); + } + } + } + } + + private string DefaultDeserializerReturnInstance => $""; + private void WriteDeserializerBody(bool shouldHide, CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForUnionType) + WriteDeserializerBodyForUnionModel(codeElement, parentClass, writer); + else if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForIntersectionType) + WriteDeserializerBodyForIntersectionModel(parentClass, writer); + else + WriteDeserializerBodyForInheritedModel(shouldHide, codeElement, parentClass, writer); + } + private void WriteDeserializerBodyForUnionModel(CodeMethod method, CodeClass parentClass, LanguageWriter writer) + { + var includeElse = false; + foreach (var otherPropName in parentClass + .GetPropertiesOfKind(CodePropertyKind.Custom) + .Where(static x => !x.ExistsInBaseType) + .Where(static x => x.Type is CodeType propertyType && !propertyType.IsCollection && propertyType.TypeDefinition is CodeClass) + .OrderBy(static x => x, CodePropertyTypeForwardComparer) + .ThenBy(static x => x.Name) + .Select(static x => x.Name)) + { + writer.StartBlock($"{(includeElse ? "else " : string.Empty)}if({otherPropName} != null) {{"); + writer.WriteLine($"return {otherPropName}!.{method.Name}();"); + writer.CloseBlock(); + if (!includeElse) + includeElse = true; + } + writer.WriteLine($"return {DefaultDeserializerReturnInstance}{{}};"); + } + private const string DeserializerName = "deserializers"; + + private void WriteDeserializerBodyForIntersectionModel(CodeClass parentClass, LanguageWriter writer) + { + + writer.WriteLine($"var {DeserializerName} = {DefaultDeserializerReturnInstance}{{}};"); + var complexProperties = parentClass.GetPropertiesOfKind(CodePropertyKind.Custom) + .Where(static x => x.Type is CodeType propType && propType.TypeDefinition is CodeClass && !x.Type.IsCollection) + .ToArray(); + foreach (CodeProperty prop in complexProperties) + { + writer.WriteLine($"if({prop.Name} != null){{{prop.Name}!.getFieldDeserializers().forEach((k,v) => {DeserializerName}.putIfAbsent(k, ()=>v));}}"); + } + writer.WriteLine($"return {DeserializerName};"); + } + private const string DeserializerVarName = "deserializerMap"; + private void WriteDeserializerBodyForInheritedModel(bool shouldHide, CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + var fieldToSerialize = parentClass.GetPropertiesOfKind(CodePropertyKind.Custom, CodePropertyKind.ErrorMessageOverride).ToArray(); + if (shouldHide) + { + writer.WriteLine($"var {DeserializerVarName} = " + "super.getFieldDeserializers();"); + } + else + { + writer.WriteLine($"var {DeserializerVarName} = {DefaultDeserializerReturnInstance}{{}};"); + } + + if (fieldToSerialize.Length != 0) + { + fieldToSerialize + .Where(x => !x.ExistsInBaseType && !conventions.ErrorClassPropertyExistsInSuperClass(x)) + .OrderBy(static x => x.Name) + .Select(x => + $"{DeserializerVarName}['{x.WireName}'] = (node) => {x.Name} = node.{GetDeserializationMethodName(x.Type, codeElement)};") + .ToList() + .ForEach(x => writer.WriteLine(x)); + } + writer.WriteLine($"return {DeserializerVarName};"); + } + 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) + { + var collectionMethod = ""; + if (currentType.TypeDefinition == null) + return $"getCollectionOfPrimitiveValues<{propertyType.TrimEnd(DartConventionService.NullableMarker)}>(){collectionMethod}"; + else if (currentType.TypeDefinition is CodeEnum enumType) + { + var typeName = enumType.Name; + return $"getCollectionOfEnumValues<{typeName}>((stringValue) => {typeName}.values.where((enumVal) => enumVal.value == stringValue).firstOrNull)"; + } + else + return $"getCollectionOfObjectValues<{propertyType}>({propertyType}.createFromDiscriminatorValue){collectionMethod}"; + } + else if (currentType.TypeDefinition is CodeEnum enumType) + { + var typeName = enumType.Name; + return $"getEnumValue<{typeName}>((stringValue) => {typeName}.values.where((enumVal) => enumVal.value == stringValue).firstOrNull)"; + } + } + return propertyType switch + { + "Iterable" => "getCollectionOfPrimitiveValues()", + "UuidValue" => "getGuidValue()", + "byte[]" => "getByteArrayValue()", + _ when conventions.IsPrimitiveType(propertyType) => $"get{propertyType.TrimEnd(DartConventionService.NullableMarker).ToFirstCharacterUpperCase()}Value()", + _ => $"getObjectValue<{propertyType.ToFirstCharacterUpperCase()}>({propertyType}.createFromDiscriminatorValue)", + }; + } + protected void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass parentClass, bool isVoid, string returnTypeWithoutCollectionInformation, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(requestParams); + ArgumentNullException.ThrowIfNull(parentClass); + ArgumentNullException.ThrowIfNull(writer); + if (codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); + + var generatorMethodName = parentClass + .Methods + .FirstOrDefault(x => x.IsOfKind(CodeMethodKind.RequestGenerator) && x.HttpMethod == codeElement.HttpMethod) + ?.Name; + var parametersList = new CodeParameter?[] { requestParams.requestBody, requestParams.requestContentType, requestParams.requestConfiguration } + .Select(static x => x?.Name).Where(static x => x != null).Aggregate(static (x, y) => $"{x}, {y}"); + writer.WriteLine($"var requestInfo = {generatorMethodName}({parametersList});"); + var errorMappingVarName = "{}"; + if (codeElement.ErrorMappings.Any()) + { + errorMappingVarName = "errorMapping"; + writer.StartBlock($"final {errorMappingVarName} = >{{"); + foreach (var errorMapping in codeElement.ErrorMappings.Where(errorMapping => errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass)) + { + writer.WriteLine($"'{errorMapping.Key.ToUpperInvariant()}' : {conventions.GetTypeString(errorMapping.Value, codeElement, false)}.createFromDiscriminatorValue,"); + } + writer.CloseBlock("};"); + } + var returnTypeCodeType = codeElement.ReturnType as CodeType; + var returnTypeFactory = returnTypeCodeType?.TypeDefinition is CodeClass || (returnTypeCodeType != null && returnTypeCodeType.Name.Equals(KiotaBuilder.UntypedNodeName, StringComparison.OrdinalIgnoreCase)) + ? $", {returnTypeWithoutCollectionInformation}.createFromDiscriminatorValue" + : null; + writer.WriteLine($"return await requestAdapter.{GetSendRequestMethodName(isVoid, codeElement, codeElement.ReturnType)}(requestInfo{returnTypeFactory}, {errorMappingVarName});"); + } + 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"); + if (currentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is not CodeProperty urlTemplateParamsProperty) throw new InvalidOperationException("path parameters property cannot be null"); + if (currentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate) is not CodeProperty urlTemplateProperty) throw new InvalidOperationException("url template property cannot be null"); + + var operationName = codeElement.HttpMethod.ToString(); + var urlTemplateValue = codeElement.HasUrlTemplateOverride ? $"'{codeElement.UrlTemplateOverride}'" : GetPropertyCall(urlTemplateProperty, "''"); + writer.WriteLine($"var {RequestInfoVarName} = RequestInformation(httpMethod : HttpMethod.{operationName?.ToLowerInvariant()}, {urlTemplateProperty.Name} : {urlTemplateValue}, {urlTemplateParamsProperty.Name} : {GetPropertyCall(urlTemplateParamsProperty, "string.Empty")});"); + + if (requestParams.requestConfiguration != null && requestParams.requestConfiguration.Type is CodeType paramType) + { + var parameterClassName = paramType.GenericTypeParameterValues.First().Name; + writer.WriteLine($"{RequestInfoVarName}.configure<{parameterClassName}>({requestParams.requestConfiguration.Name}, () => {parameterClassName}());"); + } + + if (codeElement.ShouldAddAcceptHeader) + writer.WriteLine($"{RequestInfoVarName}.headers.put('Accept', '{codeElement.AcceptHeaderValue}');"); + if (requestParams.requestBody != null) + { + var suffix = requestParams.requestBody.Type.IsCollection ? "Collection" : string.Empty; + if (requestParams.requestBody.Type.Name.Equals(conventions.StreamTypeName, StringComparison.OrdinalIgnoreCase)) + { + if (requestParams.requestContentType is not null) + writer.WriteLine($"{RequestInfoVarName}.setStreamContent({requestParams.requestBody.Name}, {requestParams.requestContentType.Name});"); + else if (!string.IsNullOrEmpty(codeElement.RequestBodyContentType)) + writer.WriteLine($"{RequestInfoVarName}.setStreamContent({requestParams.requestBody.Name}, '{codeElement.RequestBodyContentType}');"); + } + else if (currentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter) is CodeProperty requestAdapterProperty) + if (requestParams.requestBody.Type is CodeType bodyType && (bodyType.TypeDefinition is CodeClass || bodyType.Name.Equals("MultipartBody", StringComparison.OrdinalIgnoreCase))) + writer.WriteLine($"{RequestInfoVarName}.setContentFromParsable{suffix}({requestAdapterProperty.Name}, '{codeElement.RequestBodyContentType}', {requestParams.requestBody.Name});"); + else + writer.WriteLine($"{RequestInfoVarName}.setContentFromScalar{suffix}({requestAdapterProperty.Name}, '{codeElement.RequestBodyContentType}', {requestParams.requestBody.Name});"); + } + + writer.WriteLine($"return {RequestInfoVarName};"); + } + private static string GetPropertyCall(CodeProperty property, string defaultValue) => property == null ? defaultValue : $"{property.Name}"; + private void WriteSerializerBody(bool shouldHide, CodeMethod method, CodeClass parentClass, LanguageWriter writer) + { + if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForUnionType) + WriteSerializerBodyForUnionModel(method, parentClass, writer); + else if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForIntersectionType) + WriteSerializerBodyForIntersectionModel(method, parentClass, writer); + else + WriteSerializerBodyForInheritedModel(shouldHide, method, parentClass, writer); + + if (parentClass.GetPropertyOfKind(CodePropertyKind.AdditionalData) is CodeProperty additionalDataProperty) + writer.WriteLine($"writer.writeAdditionalData({additionalDataProperty.Name});"); + } + private void WriteSerializerBodyForInheritedModel(bool shouldHide, CodeMethod method, CodeClass parentClass, LanguageWriter writer) + { + if (shouldHide) + writer.WriteLine("super.serialize(writer);"); + foreach (var otherProp in parentClass + .GetPropertiesOfKind(CodePropertyKind.Custom, CodePropertyKind.ErrorMessageOverride) + .Where(x => !x.ExistsInBaseType && !x.ReadOnly && !conventions.ErrorClassPropertyExistsInSuperClass(x)) + .OrderBy(static x => x.Name)) + { + var serializationMethodName = GetSerializationMethodName(otherProp.Type, method); + var booleanValue = serializationMethodName == "writeBoolValue" ? "value:" : ""; + var secondArgument = ""; + if (otherProp.Type is CodeType currentType && currentType.TypeDefinition is CodeEnum enumType) + { + secondArgument = $", (e) => e?.value"; + } + writer.WriteLine($"writer.{serializationMethodName}('{otherProp.WireName}', {booleanValue}{otherProp.Name}{secondArgument});"); + } + } + private void WriteSerializerBodyForUnionModel(CodeMethod method, CodeClass parentClass, LanguageWriter writer) + { + var includeElse = false; + foreach (var otherProp in parentClass + .GetPropertiesOfKind(CodePropertyKind.Custom) + .Where(static x => !x.ExistsInBaseType) + .OrderBy(static x => x, CodePropertyTypeForwardComparer) + .ThenBy(static x => x.Name)) + { + var serializationMethodName = GetSerializationMethodName(otherProp.Type, method); + var booleanValue = serializationMethodName == "writeBoolValue" ? "value:" : ""; + writer.StartBlock($"{(includeElse ? "else " : string.Empty)}if({otherProp.Name} != null) {{"); + writer.WriteLine($"writer.{GetSerializationMethodName(otherProp.Type, method)}(null, {booleanValue}{otherProp.Name});"); + writer.CloseBlock(); + if (!includeElse) + includeElse = true; + } + } + private void WriteSerializerBodyForIntersectionModel(CodeMethod method, CodeClass parentClass, LanguageWriter writer) + { + var includeElse = false; + foreach (var otherProp in parentClass + .GetPropertiesOfKind(CodePropertyKind.Custom) + .Where(static x => !x.ExistsInBaseType) + .Where(static x => x.Type is not CodeType propertyType || propertyType.IsCollection || propertyType.TypeDefinition is not CodeClass) + .OrderBy(static x => x, CodePropertyTypeBackwardComparer) + .ThenBy(static x => x.Name)) + { + var serializationMethodName = GetSerializationMethodName(otherProp.Type, method); + var booleanValue = serializationMethodName == "writeBoolValue" ? "value:" : ""; + writer.StartBlock($"{(includeElse ? "else " : string.Empty)}if({otherProp.Name} != null) {{"); + writer.WriteLine($"writer.{GetSerializationMethodName(otherProp.Type, method)}(null, {booleanValue}{otherProp.Name});"); + writer.CloseBlock(); + if (!includeElse) + includeElse = true; + } + var complexProperties = parentClass.GetPropertiesOfKind(CodePropertyKind.Custom) + .Where(static x => x.Type is CodeType propType && propType.TypeDefinition is CodeClass && !x.Type.IsCollection) + .ToArray(); + if (complexProperties.Length != 0) + { + if (includeElse) + { + writer.StartBlock("else {"); + } + var firstPropertyName = complexProperties.First().Name; + var propertiesNames = complexProperties.Skip(1).Any() ? complexProperties.Skip(1) + .Select(static x => x.Name) + .OrderBy(static x => x) + .Aggregate(static (x, y) => $"{x}, {y}") : string.Empty; + var propertiesList = string.IsNullOrEmpty(propertiesNames) ? "" : $", [{propertiesNames}]"; + writer.WriteLine($"writer.{GetSerializationMethodName(complexProperties.First().Type, method)}(null, {firstPropertyName}{propertiesList});"); + if (includeElse) + { + writer.CloseBlock(); + } + } + } + + protected string GetSendRequestMethodName(bool isVoid, CodeElement currentElement, CodeTypeBase returnType) + { + ArgumentNullException.ThrowIfNull(returnType); + var returnTypeName = conventions.GetTypeString(returnType, currentElement, false); + var isStream = conventions.StreamTypeName.Equals(returnTypeName, StringComparison.OrdinalIgnoreCase); + var isEnum = returnType is CodeType codeType && codeType.TypeDefinition is CodeEnum; + if (isVoid) return "sendNoContent"; + else if (isStream || conventions.IsPrimitiveType(returnTypeName) || isEnum) + if (returnType.IsCollection) + return $"sendPrimitiveCollection<{returnTypeName.TrimEnd('?')}>"; + else + return $"sendPrimitive<{returnTypeName}>"; + else if (returnType.IsCollection) return $"sendCollection<{returnTypeName}>"; + else if (returnType.Name.EqualsIgnoreCase("binary")) return "sendPrimitiveCollection"; + else return $"send<{returnTypeName}>"; + } + private void WriteMethodDocumentation(CodeMethod code, LanguageWriter writer) + { + conventions.WriteLongDescription(code, writer); + foreach (var paramWithDescription in code.Parameters + .Where(static x => x.Documentation.DescriptionAvailable) + .OrderBy(static x => x.Name, StringComparer.OrdinalIgnoreCase)) + conventions.WriteParameterDescription(paramWithDescription, writer); + conventions.WriteDeprecationAttribute(code, writer); + } + private static readonly BaseCodeParameterOrderComparer parameterOrderComparer = new(); + private static string GetBaseSuffix(bool isConstructor, bool inherits, CodeClass parentClass, CodeMethod currentMethod) + { + if (isConstructor && inherits) + { + if (parentClass.IsOfKind(CodeClassKind.RequestBuilder) && parentClass.Properties.FirstOrDefaultOfKind(CodePropertyKind.UrlTemplate) is CodeProperty urlTemplateProperty && + !string.IsNullOrEmpty(urlTemplateProperty.DefaultValue)) + { + var thirdParameterName = string.Empty; + if (currentMethod.Parameters.OfKind(CodeParameterKind.PathParameters) is CodeParameter pathParametersParameter) + thirdParameterName = $", {pathParametersParameter.Name}"; + else if (currentMethod.Parameters.OfKind(CodeParameterKind.RawUrl) is CodeParameter rawUrlParameter) + thirdParameterName = $", {{RequestInformation.rawUrlKey : {rawUrlParameter.Name}}}"; + else if (parentClass.Properties.FirstOrDefaultOfKind(CodePropertyKind.PathParameters) is CodeProperty pathParametersProperty && !string.IsNullOrEmpty(pathParametersProperty.DefaultValue)) + thirdParameterName = $", {pathParametersProperty.DefaultValue}"; + if (currentMethod.Parameters.OfKind(CodeParameterKind.RequestAdapter) is CodeParameter requestAdapterParameter) + { + return $" : super({requestAdapterParameter.Name}, {urlTemplateProperty.DefaultValue}{thirdParameterName})"; + } + else if (parentClass.StartBlock?.Inherits?.Name?.Contains("CliRequestBuilder", StringComparison.Ordinal) == true) + { + // CLI uses a different base class. + return $" : super({urlTemplateProperty.DefaultValue}{thirdParameterName})"; + } + } + return " : super()"; + } + else if (isConstructor && parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.AdditionalData)).Any() && !parentClass.IsErrorDefinition && !parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.BackingStore)).Any()) + { + return " : "; + } + + return string.Empty; + } + private void WriteMethodPrototype(CodeMethod code, CodeClass parentClass, LanguageWriter writer, string returnType, bool inherits, bool isVoid) + { + var staticModifier = code.IsStatic ? "static " : string.Empty; + if (code.IsOfKind(CodeMethodKind.Serializer, CodeMethodKind.Deserializer, CodeMethodKind.QueryParametersMapper) || code.IsOfKind(CodeMethodKind.Custom)) + { + writer.WriteLine("@override"); + } + + var genericTypeSuffix = code.IsAsync && !isVoid ? ">" : string.Empty; + var isConstructor = code.IsOfKind(CodeMethodKind.Constructor, CodeMethodKind.ClientConstructor, CodeMethodKind.RawUrlConstructor); + var voidCorrectedTaskReturnType = code.IsAsync && isVoid ? "void" : returnType; + var async = code.IsAsync ? " async" : string.Empty; + if (code.ReturnType.IsArray && code.IsOfKind(CodeMethodKind.RequestExecutor)) + voidCorrectedTaskReturnType = $"IEnumerable<{voidCorrectedTaskReturnType.StripArraySuffix()}>"; + else if (code.IsOfKind(CodeMethodKind.RequestExecutor) && code.IsAsync) + voidCorrectedTaskReturnType = $"Future<{voidCorrectedTaskReturnType.StripArraySuffix()}>"; + // TODO: Task type should be moved into the refiner + var completeReturnType = isConstructor ? + string.Empty : voidCorrectedTaskReturnType + " "; + var baseSuffix = GetBaseSuffix(isConstructor, inherits, parentClass, code); + var parameters = string.Join(", ", code.Parameters.OrderBy(static x => x, parameterOrderComparer).Select(p => conventions.GetParameterSignature(p, code)).ToList()); + var methodName = GetMethodName(code, parentClass, isConstructor); + var includeNullableReferenceType = code.IsOfKind(CodeMethodKind.RequestExecutor, CodeMethodKind.RequestGenerator); + var openingBracket = baseSuffix.Equals(" : ", StringComparison.Ordinal) ? "" : "{"; + var closingparenthesis = (isConstructor && parentClass.IsErrorDefinition) ? string.Empty : ")"; + // Constuctors (except for ClientConstructor) don't need a body but a closing statement + if (HasEmptyConstructorBody(code, parentClass, isConstructor)) + { + openingBracket = ";"; + } + + if (includeNullableReferenceType) + { + var completeReturnTypeWithNullable = isConstructor || string.IsNullOrEmpty(genericTypeSuffix) ? completeReturnType : $"{completeReturnType[..^2].TrimEnd('?')}?{genericTypeSuffix} "; + var nullableParameters = string.Join(", ", code.Parameters.Order(parameterOrderComparer) + .Select(p => p.IsOfKind(CodeParameterKind.RequestConfiguration) ? + $"[{GetParameterSignatureWithNullableRefType(p, code)}]" : + conventions.GetParameterSignature(p, code)) + .ToList()); + writer.WriteLine($"{staticModifier}{completeReturnTypeWithNullable}{conventions.GetAccessModifier(code.Access)}{methodName}({nullableParameters}){baseSuffix}{async} {{"); + } + else if (parentClass.IsOfKind(CodeClassKind.Model) && code.IsOfKind(CodeMethodKind.Custom) && code.Name.EqualsIgnoreCase("copyWith")) + { + var parentParameters = "int? statusCode, String? message, Map>? responseHeaders, Iterable? innerExceptions, "; + var ownParameters = string.Join(", ", parentClass.GetPropertiesOfKind(CodePropertyKind.Custom, CodePropertyKind.AdditionalData) + .Where(p => !conventions.ErrorClassPropertyExistsInSuperClass(p)) + .Select(p => $"{GetPrefix(p)}{conventions.TranslateType(p.Type)}{getSuffix(p)}? {p.Name}")); + writer.WriteLine($"{staticModifier}{completeReturnType}{conventions.GetAccessModifier(code.Access)}{methodName}({openingBracket}{parentParameters}{ownParameters} }}){{"); + } + else + { + writer.WriteLine($"{staticModifier}{completeReturnType}{conventions.GetAccessModifier(code.Access)}{methodName}({parameters}{closingparenthesis}{baseSuffix}{async} {openingBracket}"); + } + } + + private string getSuffix(CodeProperty property) + { + return property.Type.CollectionKind == CodeTypeCollectionKind.Complex ? ">" : string.Empty; + } + + private string GetPrefix(CodeProperty property) + { + return property.Type.CollectionKind == CodeTypeCollectionKind.Complex ? "Iterable<" : string.Empty; + } + + private static string GetMethodName(CodeMethod code, CodeClass parentClass, bool isConstructor) + { + if (code.IsOfKind(CodeMethodKind.RawUrlConstructor)) + { + return parentClass.Name + ".withUrl"; + } + return isConstructor ? parentClass.Name : code.Name; + } + + private string GetParameterSignatureWithNullableRefType(CodeParameter parameter, CodeElement targetElement) + { + var signatureSegments = conventions.GetParameterSignature(parameter, targetElement).Split(" ", StringSplitOptions.RemoveEmptyEntries); + if (signatureSegments.Length > 1 && signatureSegments[1].StartsWith("Function", StringComparison.Ordinal)) + signatureSegments[1] += "?"; + return $"{string.Join(" ", signatureSegments)}"; + } + + + private void WriteQueryparametersBody(CodeClass parentClass, LanguageWriter writer) + { + writer.StartBlock("return {"); + foreach (CodeProperty property in parentClass.Properties) + { + var key = property.IsNameEscaped ? property.SerializationName : property.Name; + writer.WriteLine($"'{key}' : {property.Name},"); + } + writer.CloseBlock("};"); + } + + private string GetSerializationMethodName(CodeTypeBase propType, CodeMethod method, bool includeNullableRef = false) + { + 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) + return $"writeCollectionOfPrimitiveValues<{propertyType}>"; + else if (currentType.TypeDefinition is CodeEnum) + return $"writeCollectionOfEnumValues<{propertyType.TrimEnd('?')}>"; + else + return $"writeCollectionOfObjectValues<{propertyType}>"; + else if (currentType.TypeDefinition is CodeEnum enumType) + return $"writeEnumValue<{enumType.Name}>"; + + } + + return propertyType switch + { + "byte[]" => "writeByteArrayValue", + "String" => "writeStringValue", + "Iterable" => "writeCollectionOfPrimitiveValues", + "UuidValue" => "writeUuidValue", + _ when conventions.IsPrimitiveType(propertyType) => $"write{propertyType.TrimEnd(DartConventionService.NullableMarker).ToFirstCharacterUpperCase()}Value", + _ => $"writeObjectValue<{propertyType.ToFirstCharacterUpperCase()}{(includeNullableRef ? "?" : "")}>", + }; + } + private void WriteCustomMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + if (codeElement.Name.Equals("clone", StringComparison.OrdinalIgnoreCase)) + { + var constructor = parentClass.GetMethodsOffKind(CodeMethodKind.Constructor, CodeMethodKind.ClientConstructor).Where(static x => x.Parameters.Any()).FirstOrDefault(); + var argumentList = constructor?.Parameters.OrderBy(static x => x, new BaseCodeParameterOrderComparer()) + .Select(static x => x.Type.Parent is CodeParameter param && param.IsOfKind(CodeParameterKind.RequestAdapter, CodeParameterKind.PathParameters) + ? x.Name : + x.Optional ? "null" : x.DefaultValue) + .Aggregate(static (x, y) => $"{x}, {y}"); + writer.WriteLine($"return {parentClass.Name}({argumentList});"); + } + if (codeElement.Name.Equals("copyWith", StringComparison.Ordinal)) + { + var hasBackingStore = parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.BackingStore)).Any(); + var resultName = hasBackingStore ? "result" : string.Empty; + + if (hasBackingStore) + { + writer.WriteLine($"var {resultName} = {parentClass.Name}("); + } + else + { + writer.WriteLine($"return {parentClass.Name}("); + } + foreach (string prop in DartConventionService.ErrorClassProperties) + { + writer.WriteLine($"{prop} : {prop} ?? this.{prop}, "); + } + if (hasBackingStore) + { + writer.WriteLine(");"); + } + foreach (CodeProperty prop in parentClass.GetPropertiesOfKind(CodePropertyKind.Custom, CodePropertyKind.AdditionalData)) + { + var propertyname = prop.Name; + var separator = hasBackingStore ? "=" : ":"; + var ending = hasBackingStore ? ";" : ","; + var resultPropertyName = string.IsNullOrEmpty(resultName) ? propertyname : $"{resultName}.{propertyname}"; + if (!conventions.ErrorClassPropertyExistsInSuperClass(prop)) + { + writer.WriteLine($"{resultPropertyName} {separator} {propertyname} ?? this.{propertyname}{ending} "); + } + } + if (hasBackingStore) + { + writer.WriteLine($"return {resultName}; "); + } + else + { + writer.WriteLine($");"); + } + } + } +} diff --git a/src/Kiota.Builder/Writers/Dart/CodePropertyWriter.cs b/src/Kiota.Builder/Writers/Dart/CodePropertyWriter.cs new file mode 100644 index 0000000000..16ceee96eb --- /dev/null +++ b/src/Kiota.Builder/Writers/Dart/CodePropertyWriter.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using Kiota.Builder.CodeDOM; + +namespace Kiota.Builder.Writers.Dart; +public class CodePropertyWriter : BaseElementWriter +{ + public CodePropertyWriter(DartConventionService conventionService) : base(conventionService) { } + public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + if (codeElement.ExistsInExternalBaseType || conventions.ErrorClassPropertyExistsInSuperClass(codeElement)) return; + var propertyType = conventions.GetTypeString(codeElement.Type, codeElement); + var isNullableReferenceType = !propertyType.EndsWith('?') + && codeElement.IsOfKind( + CodePropertyKind.Custom, + CodePropertyKind.QueryParameter, CodePropertyKind.ErrorMessageOverride); + conventions.WriteShortDescription(codeElement, writer); + conventions.WriteDeprecationAttribute(codeElement, writer); + if (isNullableReferenceType) + { + WritePropertyInternal(codeElement, writer, $"{propertyType}?"); + } + else + { + WritePropertyInternal(codeElement, writer, propertyType); + } + } + + private void WritePropertyInternal(CodeProperty codeElement, LanguageWriter writer, string propertyType) + { + if (codeElement.Parent is not CodeClass parentClass) + throw new InvalidOperationException("The parent of a property should be a class"); + + var backingStoreProperty = parentClass.GetBackingStoreProperty(); + var defaultValue = string.Empty; + var getterModifier = string.Empty; + + var propertyName = codeElement.Name; + switch (codeElement.Kind) + { + case CodePropertyKind.RequestBuilder: + writer.StartBlock($"{propertyType} get {conventions.GetAccessModifierPrefix(codeElement.Access)}{propertyName} {{"); + conventions.AddRequestBuilderBody(parentClass, propertyType, writer, prefix: "return "); + writer.CloseBlock(); + break; + case CodePropertyKind.AdditionalData when backingStoreProperty != null: + case CodePropertyKind.Custom when backingStoreProperty != null: + var backingStoreKey = codeElement.WireName; + var defaultIfNotNullable = propertyType.EndsWith('?') ? string.Empty : codeElement.IsOfKind(CodePropertyKind.AdditionalData) ? " ?? {}" : $" ?? {codeElement.DefaultValue}"; + writer.StartBlock($"{propertyType} get {conventions.GetAccessModifierPrefix(codeElement.Access)}{propertyName} {{"); + writer.WriteLine($"return {backingStoreProperty.Name}.get<{propertyType}>('{backingStoreKey}'){defaultIfNotNullable};"); + writer.CloseBlock(); + writer.WriteLine(); + writer.StartBlock($"set {codeElement.Name}({propertyType} value) {{"); + writer.WriteLine($"{backingStoreProperty.Name}.set('{backingStoreKey}', value);"); + writer.CloseBlock(); + break; + case CodePropertyKind.ErrorMessageOverride when parentClass.IsErrorDefinition: + writer.WriteLine("@override"); + goto default; + case CodePropertyKind.QueryParameter when codeElement.IsNameEscaped: + writer.WriteLine($"/// @QueryParameter('{codeElement.SerializationName}')"); + goto default; + case CodePropertyKind.QueryParameters: + defaultValue = $" = {propertyType}()"; + goto default; + case CodePropertyKind.AdditionalData: + if (parentClass.StartBlock.Implements.Where(static x => x.Name.Equals("AdditionalDataHolder", StringComparison.Ordinal)).Any()) + writer.WriteLine("@override"); + goto default; + case CodePropertyKind.BackingStore: + defaultValue = " = BackingStoreFactorySingleton.instance.createBackingStore()"; + goto default; + default: + writer.WriteLine($"{propertyType} {getterModifier}{conventions.GetAccessModifierPrefix(codeElement.Access)}{codeElement.Name}{defaultValue};"); + break; + } + } +} diff --git a/src/Kiota.Builder/Writers/Dart/DartConventionService.cs b/src/Kiota.Builder/Writers/Dart/DartConventionService.cs new file mode 100644 index 0000000000..13434eb98b --- /dev/null +++ b/src/Kiota.Builder/Writers/Dart/DartConventionService.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; + +using static Kiota.Builder.CodeDOM.CodeTypeBase; + +namespace Kiota.Builder.Writers.Dart; +public class DartConventionService : CommonLanguageConventionService +{ + private static string AutoGenerationHeader => "/// auto generated"; + internal static readonly HashSet ErrorClassProperties = new(StringComparer.OrdinalIgnoreCase) { "message", "statusCode", "responseHeaders", "innerExceptions" }; + public override string StreamTypeName => "Stream"; + public override string VoidTypeName => "void"; + public override string DocCommentPrefix => "/// "; + private static readonly HashSet NullableTypes = new(StringComparer.OrdinalIgnoreCase) { "int", "bool", "double", "string", "datetime", "dateonly", "timeonly", "backingstorefactory" }; + public const char NullableMarker = '?'; + public static string NullableMarkerAsString => "?"; + public override string ParseNodeInterfaceName => "ParseNode"; + + private const string ReferenceTypePrefix = "["; + private const string ReferenceTypeSuffix = "]"; + + public override bool WriteShortDescription(IDocumentedElement element, LanguageWriter writer, string prefix = "", string suffix = "") + { + ArgumentNullException.ThrowIfNull(writer); + ArgumentNullException.ThrowIfNull(element); + if (!element.Documentation.DescriptionAvailable) return false; + if (element is not CodeElement codeElement) return false; + + var description = element.Documentation.GetDescription(x => GetTypeReferenceForDocComment(x, codeElement), ReferenceTypePrefix, ReferenceTypeSuffix); + writer.WriteLine($"{DocCommentPrefix} {description}"); + + return true; + } + public void WriteParameterDescription(CodeParameter element, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(writer); + ArgumentNullException.ThrowIfNull(element); + var description = element.Documentation.GetDescription(x => GetTypeReferenceForDocComment(x, element), ReferenceTypePrefix, ReferenceTypeSuffix); + writer.WriteLine($"{DocCommentPrefix} {ReferenceTypePrefix}{element.Name}{ReferenceTypeSuffix} {description}"); + } + + public void WriteAutogeneratedMessage(LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteLine(AutoGenerationHeader); + } + + + public void WriteLintingMessage(LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteLine("// ignore_for_file: type=lint"); + } + + public void WriteLongDescription(CodeElement element, LanguageWriter writer, IEnumerable? additionalRemarks = default) + { + ArgumentNullException.ThrowIfNull(writer); + if (element is not IDocumentedElement documentedElement || documentedElement.Documentation is not CodeDocumentation documentation) return; + if (additionalRemarks == default) + additionalRemarks = []; + var remarks = additionalRemarks.Where(static x => !string.IsNullOrEmpty(x)).ToArray(); + if (documentation.DescriptionAvailable || documentation.ExternalDocumentationAvailable || remarks.Length != 0) + { + if (documentation.DescriptionAvailable) + { + var description = documentedElement.Documentation.GetDescription(x => GetTypeReferenceForDocComment(x, element), ReferenceTypePrefix, ReferenceTypeSuffix); + writer.WriteLine($"{DocCommentPrefix}{description}"); + } + foreach (var additionalRemark in remarks) + writer.WriteLine($"{DocCommentPrefix}{additionalRemark}"); + if (element is IDeprecableElement deprecableElement && deprecableElement.Deprecation is not null && deprecableElement.Deprecation.IsDeprecated) + foreach (var additionalComment in GetDeprecationInformationForDocumentationComment(deprecableElement)) + writer.WriteLine($"{DocCommentPrefix}{additionalComment}"); + + if (documentation.ExternalDocumentationAvailable) + writer.WriteLine($"{DocCommentPrefix} [{documentation.DocumentationLabel}]({documentation.DocumentationLink})"); + } + } + + internal string GetTypeReferenceForDocComment(CodeTypeBase code, CodeElement targetElement) + { + if (code is CodeType codeType && codeType.TypeDefinition is CodeMethod method) + return $"{GetTypeString(new CodeType { TypeDefinition = method.Parent, IsExternal = false }, targetElement)}#{GetTypeString(code, targetElement)}"; + return $"{GetTypeString(code, targetElement)}"; + } + + private string[] GetDeprecationInformationForDocumentationComment(IDeprecableElement element) + { + if (element.Deprecation is null || !element.Deprecation.IsDeprecated) return Array.Empty(); + + var versionComment = string.IsNullOrEmpty(element.Deprecation.Version) ? string.Empty : $" as of {element.Deprecation.Version}"; + var dateComment = element.Deprecation.Date is null ? string.Empty : $" on {element.Deprecation.Date.Value.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}"; + var removalComment = element.Deprecation.RemovalDate is null ? string.Empty : $" and will be removed {element.Deprecation.RemovalDate.Value.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}"; + return [ + $"@deprecated", + $"{element.Deprecation.GetDescription(type => GetTypeString(type, (element as CodeElement)!))}{versionComment}{dateComment}{removalComment}" + ]; + } + + public override string GetAccessModifier(AccessModifier access) + { + // Dart does not support access modifiers + return access == AccessModifier.Private ? "_" : string.Empty; + } + +#pragma warning disable CA1822 // Method should be static + public string GetAccessModifierPrefix(AccessModifier access) + { + return access switch + { + AccessModifier.Private => "_", + _ => string.Empty, + }; + } + + internal void AddRequestBuilderBody(CodeClass parentClass, string returnType, LanguageWriter writer, string? urlTemplateVarName = default, string? prefix = default, IEnumerable? pathParameters = default, IEnumerable? customParameters = default) + { + if (parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is CodeProperty pathParametersProp && + parentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter) is CodeProperty requestAdapterProp) + { + var pathParametersSuffix = !(pathParameters?.Any() ?? false) ? string.Empty : + $", {string.Join(", ", pathParameters.Select(x => x.Optional ? $"{x.Name.ToFirstCharacterLowerCase()} : {x.Name.ToFirstCharacterLowerCase()}" : $"{x.Name.ToFirstCharacterLowerCase()}"))}"; + var urlTplRef = string.IsNullOrEmpty(urlTemplateVarName) ? pathParametersProp.Name.ToFirstCharacterLowerCase() : urlTemplateVarName; + if (customParameters?.Any() ?? false) + { + urlTplRef = TempDictionaryVarName; + writer.WriteLine($"var {urlTplRef} = Map.of({pathParametersProp.Name.ToFirstCharacterLowerCase()});"); + foreach (var param in customParameters) + writer.WriteLine($"{urlTplRef}.putIfAbsent('{param.SerializationName}', () => {param.Name.ToFirstCharacterLowerCase()});"); + } + writer.WriteLine($"{prefix}{returnType}({urlTplRef}, {requestAdapterProp.Name.ToFirstCharacterLowerCase()}{pathParametersSuffix});"); + } + } + public override string TempDictionaryVarName => "urlTplParams"; + internal void AddParametersAssignment(LanguageWriter writer, CodeTypeBase pathParametersType, string pathParametersReference, string varName = "", params (CodeTypeBase, string, string)[] parameters) + { + if (pathParametersType == null) return; + if (string.IsNullOrEmpty(varName)) + { + varName = TempDictionaryVarName; + writer.WriteLine($"var {varName} = {pathParametersType.Name}({pathParametersReference});"); + } + if (parameters.Length != 0) + { + writer.WriteLines(parameters.Select(p => + { + var (ct, name, identName) = p; + string nullCheck = string.Empty; + if (ct.CollectionKind == CodeTypeCollectionKind.None && ct.IsNullable) + { + if (nameof(String).Equals(ct.Name, StringComparison.OrdinalIgnoreCase)) + nullCheck = $"if ({identName}!= null && {identName}.isNotEmpty) "; + else + nullCheck = $"if ({identName} != null) "; + } + return $"{nullCheck}{varName}[\"{name}\"]={identName};"; + }).ToArray()); + } + } +#pragma warning restore CA1822 // Method should be static + private static bool ShouldTypeHaveNullableMarker(CodeTypeBase propType, string propTypeName) + { + return propType.IsNullable && (NullableTypes.Contains(propTypeName) || (propType is CodeType codeType && codeType.TypeDefinition is CodeEnum)); + } + + public override string GetTypeString(CodeTypeBase code, CodeElement targetElement, bool includeCollectionInformation = true, LanguageWriter? writer = null) + { + return GetTypeString(code, targetElement, includeCollectionInformation, true); + } + public string GetTypeString(CodeTypeBase code, CodeElement targetElement, bool includeCollectionInformation, bool includeNullableInformation, bool includeActionInformation = true) + { + ArgumentNullException.ThrowIfNull(targetElement); + if (code is CodeComposedTypeBase) + throw new InvalidOperationException($"Dart does not support union types, the union type {code.Name} should have been filtered out by the refiner"); + if (code is CodeType currentType) + { + var typeName = TranslateTypeAndAvoidUsingNamespaceSegmentNames(currentType); + var alias = GetTypeAlias(currentType, targetElement); + if (!string.IsNullOrEmpty(alias)) + { + typeName = alias + "." + typeName; + } + var nullableSuffix = ShouldTypeHaveNullableMarker(code, typeName) && includeNullableInformation ? NullableMarkerAsString : string.Empty; + var collectionPrefix = currentType.CollectionKind == CodeTypeCollectionKind.Complex && includeCollectionInformation ? "Iterable<" : string.Empty; + if (currentType.CollectionKind == CodeTypeCollectionKind.Array && includeCollectionInformation) + { + collectionPrefix = "List<"; + } + var collectionSuffix = currentType.CollectionKind == CodeTypeCollectionKind.None || !includeCollectionInformation ? string.Empty : ">"; + var genericParameters = currentType.GenericTypeParameterValues.Any() ? + $"<{string.Join(", ", currentType.GenericTypeParameterValues.Select(x => GetTypeString(x, targetElement, includeCollectionInformation)))}>" : + string.Empty; + if (currentType.ActionOf && includeActionInformation) + { + return $"void Function({collectionPrefix}{typeName}{genericParameters}{nullableSuffix}{collectionSuffix})"; + } + + return $"{collectionPrefix}{typeName}{genericParameters}{collectionSuffix}{nullableSuffix}"; + } + + throw new InvalidOperationException($"type of type {code?.GetType()} is unknown"); + } + + private static string GetTypeAlias(CodeType targetType, CodeElement targetElement) + { + if (targetElement.GetImmediateParentOfType() is IBlock parentBlock && + parentBlock.Usings + .FirstOrDefault(x => !x.IsExternal && + x.Declaration?.TypeDefinition != null && + x.Declaration.TypeDefinition == targetType.TypeDefinition && + !string.IsNullOrEmpty(x.Alias)) is CodeUsing aliasedUsing) + return aliasedUsing.Alias; + return string.Empty; + } + + private string TranslateTypeAndAvoidUsingNamespaceSegmentNames(CodeType currentType) => TranslateType(currentType); + + public override string TranslateType(CodeType type) + { + ArgumentNullException.ThrowIfNull(type); + return type.Name.ToLowerInvariant() switch + { + "integer" or "sbyte" or "byte" or "int64" => "int", + "boolean" => "bool", + "string" => "String", + "double" or "float" or "decimal" => "double", + "object" or "void" => type.Name.ToLowerInvariant(),// little casing hack + "binary" or "base64" or "base64url" => "Iterable", + string s when s.Contains("RequestConfiguration", StringComparison.OrdinalIgnoreCase) => "RequestConfiguration", + "iparsenode" => "ParseNode", + "iserializationwriter" => "SerializationWriter", + _ => type.Name.ToFirstCharacterUpperCase() is string typeName && !string.IsNullOrEmpty(typeName) ? typeName : "Object", + }; + } + + public bool IsPrimitiveType(string typeName) + { + if (string.IsNullOrEmpty(typeName)) return false; + typeName = typeName.StripArraySuffix().TrimEnd('?').ToLowerInvariant(); + return typeName switch + { + "string" or "dateonly" or "timeonly" or "datetime" or "duration" => true, + _ when NullableTypes.Contains(typeName) => true, + _ => false, + }; + } + public override string GetParameterSignature(CodeParameter parameter, CodeElement targetElement, LanguageWriter? writer = null) + { + ArgumentNullException.ThrowIfNull(parameter); + var parameterType = GetTypeString(parameter.Type, targetElement, true, parameter.Optional); + var defaultValue = parameter switch + { + _ when !string.IsNullOrEmpty(parameter.DefaultValue) => $" = {parameter.DefaultValue}", + _ when nameof(String).Equals(parameterType, StringComparison.OrdinalIgnoreCase) && parameter.Optional => " = \"\"", + _ => string.Empty, + }; + var open = !string.IsNullOrEmpty(defaultValue) ? "{" : ""; + var close = !string.IsNullOrEmpty(defaultValue) ? "}" : ""; + return $"{GetDeprecationInformation(parameter)}{open}{parameterType} {parameter.Name.ToFirstCharacterLowerCase()}{defaultValue}{close}"; + } + private string GetDeprecationInformation(IDeprecableElement element) + { + if (element.Deprecation is null || !element.Deprecation.IsDeprecated) return string.Empty; + + var versionComment = string.IsNullOrEmpty(element.Deprecation.Version) ? string.Empty : $" as of {element.Deprecation.Version}"; + var dateComment = element.Deprecation.Date is null ? string.Empty : $" on {element.Deprecation.Date.Value.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}"; + var removalComment = element.Deprecation.RemovalDate is null ? string.Empty : $" and will be removed {element.Deprecation.RemovalDate.Value.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}"; + return $"@Deprecated(\"{element.Deprecation.GetDescription(type => GetTypeString(type, (element as CodeElement)!))}{versionComment}{dateComment}{removalComment}\")"; + } + internal void WriteDeprecationAttribute(IDeprecableElement element, LanguageWriter writer) + { + var deprecationMessage = GetDeprecationInformation(element); + if (!string.IsNullOrEmpty(deprecationMessage)) + writer.WriteLine(deprecationMessage); + } + + public bool ErrorClassPropertyExistsInSuperClass(CodeProperty codeElement) + { + return codeElement?.Parent is CodeClass parentClass && parentClass.IsErrorDefinition && ErrorClassProperties.Contains(codeElement.Name); + } + + public static string getCorrectedEnumName(string name) + { + ArgumentNullException.ThrowIfNull(name); + var correctedName = ""; + if (name.Contains('_', StringComparison.Ordinal)) + { + correctedName = name.ToLowerInvariant().ToCamelCase('_'); + } + else + { + correctedName = name.All(c => char.IsUpper(c) || char.IsAsciiDigit(c)) ? name.ToLowerInvariant() : name.ToFirstCharacterLowerCase(); + } + return correctedName; + } +} diff --git a/src/Kiota.Builder/Writers/Dart/DartWriter.cs b/src/Kiota.Builder/Writers/Dart/DartWriter.cs new file mode 100644 index 0000000000..f368db63a8 --- /dev/null +++ b/src/Kiota.Builder/Writers/Dart/DartWriter.cs @@ -0,0 +1,18 @@ +using Kiota.Builder.PathSegmenters; + +namespace Kiota.Builder.Writers.Dart; +public class DartWriter : LanguageWriter +{ + public DartWriter(string rootPath, string clientNamespaceName) + { + PathSegmenter = new DartPathSegmenter(rootPath, clientNamespaceName); + var conventionService = new DartConventionService(); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService, clientNamespaceName, (DartPathSegmenter)PathSegmenter)); + AddOrReplaceCodeElementWriter(new CodeBlockEndWriter()); + 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 3a4a8886e0..9ccf0652ab 100644 --- a/src/Kiota.Builder/Writers/LanguageWriter.cs +++ b/src/Kiota.Builder/Writers/LanguageWriter.cs @@ -8,6 +8,7 @@ using Kiota.Builder.PathSegmenters; using Kiota.Builder.Writers.Cli; using Kiota.Builder.Writers.CSharp; +using Kiota.Builder.Writers.Dart; using Kiota.Builder.Writers.Go; using Kiota.Builder.Writers.Java; using Kiota.Builder.Writers.Php; @@ -192,6 +193,7 @@ public static LanguageWriter GetLanguageWriter(GenerationLanguage language, stri GenerationLanguage.Go => new GoWriter(outputPath, clientNamespaceName, excludeBackwardCompatible), GenerationLanguage.CLI => new CliWriter(outputPath, clientNamespaceName), GenerationLanguage.Swift => new SwiftWriter(outputPath, clientNamespaceName), + GenerationLanguage.Dart => new DartWriter(outputPath, clientNamespaceName), _ => throw new InvalidEnumArgumentException($"{language} language currently not supported."), }; } diff --git a/src/kiota/Properties/launchSettings.json b/src/kiota/Properties/launchSettings.json index 627716184d..f08c0d3028 100644 --- a/src/kiota/Properties/launchSettings.json +++ b/src/kiota/Properties/launchSettings.json @@ -2,7 +2,11 @@ "profiles": { "kiota": { "commandName": "Project", - "commandLineArgs": "generate --openapi https://localhost:3000/swagger.json -o E:\\OSS\\kiota\\Output\\local_3 -c GraphClient --log-level Information -l CSharp --dsv" + "commandLineArgs": "--openapi C:\\src\\msgraph-sdk-powershell\\openApiDocs\\v1.0\\mail.yml -o C:\\Users\\darrmi\\source\\github\\darrelmiller\\OpenApiClient\\Generated -c GraphClient --loglevel Information" + }, + "dart": { + "commandName": "Project", + "commandLineArgs": "generate --openapi D:\\swagger.json -o D:\\generated --log-level Debug -l Dart" } } } diff --git a/src/kiota/appsettings.json b/src/kiota/appsettings.json index a53ec54fe0..a332489f69 100644 --- a/src/kiota/appsettings.json +++ b/src/kiota/appsettings.json @@ -385,6 +385,48 @@ } ], "DependencyInstallCommand": "dotnet add package {0} --version {1}" + }, + "Dart": { + "MaturityLevel": "Experimental", + "SupportExperience": "Community", + "Dependencies": [ + { + "Name": "microsoft_kiota_abstractions", + "Version": "0.0.2", + "Type": "Abstractions" + }, + { + "Name": "microsoft_kiota_http", + "Version": "0.0.2", + "Type": "Http" + }, + { + "Name": "microsoft_kiota_serialization_form", + "Version": "0.0.2", + "Type": "Serialization" + }, + { + "Name": "microsoft_kiota_serialization_text", + "Version": "0.0.2", + "Type": "Serialization" + }, + { + "Name": "microsoft_kiota_serialization_json", + "Version": "0.0.2", + "Type": "Serialization" + }, + { + "Name": "microsoft_kiota_serialization_multipart", + "Version": "0.0.2", + "Type": "Serialization" + }, + { + "Name": "microsoft_kiota_bundle", + "Version": "0.0.2", + "Type": "Bundle" + } + ], + "DependencyInstallCommand": "" } } } \ No newline at end of file diff --git a/src/kiota/kiota.csproj b/src/kiota/kiota.csproj index 8a313c04d5..2b87a93385 100644 --- a/src/kiota/kiota.csproj +++ b/src/kiota/kiota.csproj @@ -1,4 +1,4 @@ - + Exe Latest @@ -15,7 +15,7 @@ Microsoft.OpenApi.Kiota Microsoft.OpenApi.Kiota ./nupkg - 1.22.0 + 1.23.0 $(VersionSuffix) https://github.com/microsoft/kiota/releases diff --git a/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs b/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs index 6f189e6514..d79948aa93 100644 --- a/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs +++ b/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs @@ -20,11 +20,13 @@ public void Dispose() [InlineData(GenerationLanguage.Java, false)] [InlineData(GenerationLanguage.TypeScript, false)] [InlineData(GenerationLanguage.Go, false)] + [InlineData(GenerationLanguage.Dart, false)] [InlineData(GenerationLanguage.Ruby, false)] [InlineData(GenerationLanguage.CSharp, true)] [InlineData(GenerationLanguage.Java, true)] [InlineData(GenerationLanguage.PHP, false)] [InlineData(GenerationLanguage.TypeScript, true)] + [InlineData(GenerationLanguage.Dart, true)] [Theory] public async Task GeneratesTodoAsync(GenerationLanguage language, bool backingStore) { @@ -46,11 +48,13 @@ public async Task GeneratesTodoAsync(GenerationLanguage language, bool backingSt [InlineData(GenerationLanguage.Java, false)] [InlineData(GenerationLanguage.TypeScript, false)] [InlineData(GenerationLanguage.Go, false)] + [InlineData(GenerationLanguage.Dart, false)] [InlineData(GenerationLanguage.Ruby, false)] [InlineData(GenerationLanguage.CSharp, true)] [InlineData(GenerationLanguage.Java, true)] [InlineData(GenerationLanguage.PHP, false)] [InlineData(GenerationLanguage.TypeScript, true)] + [InlineData(GenerationLanguage.Dart, true)] [Theory] public async Task GeneratesModelWithDictionaryAsync(GenerationLanguage language, bool backingStore) { @@ -72,11 +76,13 @@ public async Task GeneratesModelWithDictionaryAsync(GenerationLanguage language, [InlineData(GenerationLanguage.Java, false)] [InlineData(GenerationLanguage.TypeScript, false)] [InlineData(GenerationLanguage.Go, false)] + [InlineData(GenerationLanguage.Dart, false)] [InlineData(GenerationLanguage.Ruby, false)] [InlineData(GenerationLanguage.CSharp, true)] [InlineData(GenerationLanguage.Java, true)] [InlineData(GenerationLanguage.PHP, false)] [InlineData(GenerationLanguage.TypeScript, true)] + [InlineData(GenerationLanguage.Dart, true)] [Theory] public async Task GeneratesResponseWithMultipleReturnFormatsAsync(GenerationLanguage language, bool backingStore) { @@ -97,6 +103,7 @@ public async Task GeneratesResponseWithMultipleReturnFormatsAsync(GenerationLang [InlineData(GenerationLanguage.CSharp)] [InlineData(GenerationLanguage.Java)] [InlineData(GenerationLanguage.Go)] + [InlineData(GenerationLanguage.Dart)] [InlineData(GenerationLanguage.Ruby)] [InlineData(GenerationLanguage.Python)] [InlineData(GenerationLanguage.TypeScript)] @@ -119,6 +126,7 @@ public async Task GeneratesErrorsInliningParentsAsync(GenerationLanguage languag [InlineData(GenerationLanguage.CSharp)] [InlineData(GenerationLanguage.Java)] [InlineData(GenerationLanguage.Go)] + [InlineData(GenerationLanguage.Dart)] [InlineData(GenerationLanguage.Ruby)] [InlineData(GenerationLanguage.Python)] [InlineData(GenerationLanguage.TypeScript)] @@ -167,6 +175,7 @@ public async Task GeneratesIdiomaticChildrenNamesAsync(GenerationLanguage langua } [InlineData(GenerationLanguage.CSharp)] [InlineData(GenerationLanguage.Go)] + [InlineData(GenerationLanguage.Dart)] [InlineData(GenerationLanguage.Java)] [InlineData(GenerationLanguage.PHP)] [InlineData(GenerationLanguage.Python)] @@ -201,6 +210,9 @@ public async Task GeneratesUritemplateHintsAsync(GenerationLanguage language) case GenerationLanguage.CSharp: Assert.Contains("[QueryParameter(\"startDateTime\")]", fullText); break; + case GenerationLanguage.Dart: + Assert.Contains("'EndDateTime' : endDateTime", fullText); + break; case GenerationLanguage.Go: Assert.Contains("`uriparametername:\"startDateTime\"`", fullText); break; diff --git a/tests/Kiota.Builder.IntegrationTests/Kiota.Builder.IntegrationTests.csproj b/tests/Kiota.Builder.IntegrationTests/Kiota.Builder.IntegrationTests.csproj index 683a0b4ab9..fac873450c 100644 --- a/tests/Kiota.Builder.IntegrationTests/Kiota.Builder.IntegrationTests.csproj +++ b/tests/Kiota.Builder.IntegrationTests/Kiota.Builder.IntegrationTests.csproj @@ -17,8 +17,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Kiota.Builder.Tests/Kiota.Builder.Tests.csproj b/tests/Kiota.Builder.Tests/Kiota.Builder.Tests.csproj index a087921e6d..b54c040283 100644 --- a/tests/Kiota.Builder.Tests/Kiota.Builder.Tests.csproj +++ b/tests/Kiota.Builder.Tests/Kiota.Builder.Tests.csproj @@ -19,8 +19,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Kiota.Builder.Tests/Refiners/DartLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/DartLanguageRefinerTests.cs new file mode 100644 index 0000000000..7310b4b2ba --- /dev/null +++ b/tests/Kiota.Builder.Tests/Refiners/DartLanguageRefinerTests.cs @@ -0,0 +1,435 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Configuration; +using Kiota.Builder.Extensions; +using Kiota.Builder.Refiners; + +using Xunit; + +namespace Kiota.Builder.Tests.Refiners; +public class DartLanguageRefinerTests +{ + private readonly CodeNamespace root = CodeNamespace.InitRootNamespace(); + #region CommonLanguageRefinerTests + [Fact] + public async Task AddsExceptionInheritanceOnErrorClasses() + { + var model = root.AddClass(new CodeClass + { + Name = "somemodel", + Kind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + + var declaration = model.StartBlock; + + Assert.Contains("ApiException", declaration.Usings.Select(x => x.Name)); + Assert.Equal("ApiException", declaration.Inherits.Name); + } + [Fact] + public async Task AddsUsingsForErrorTypesForRequestExecutor() + { + var requestBuilder = root.AddClass(new CodeClass + { + Name = "somerequestbuilder", + Kind = CodeClassKind.RequestBuilder, + }).First(); + var subNS = root.AddNamespace($"{root.Name}.subns"); // otherwise the import gets trimmed + var errorClass = subNS.AddClass(new CodeClass + { + Name = "Error4XX", + Kind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + var requestExecutor = requestBuilder.AddMethod(new CodeMethod + { + Name = "get", + Kind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType + { + Name = "string" + }, + }).First(); + requestExecutor.AddErrorMapping("4XX", new CodeType + { + Name = "Error4XX", + TypeDefinition = errorClass, + }); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + + var declaration = requestBuilder.StartBlock; + + Assert.Contains("Error4XX", declaration.Usings.Select(x => x.Declaration?.Name)); + } + [Fact] + public async Task EscapesReservedKeywordsInInternalDeclaration() + { + var model = root.AddClass(new CodeClass + { + Name = "break", + Kind = CodeClassKind.Model + }).First(); + var nUsing = new CodeUsing + { + Name = "some.ns", + }; + nUsing.Declaration = new CodeType + { + IsExternal = false, + TypeDefinition = model + }; + model.AddUsing(nUsing); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + Assert.NotEqual("break", nUsing.Declaration.Name, StringComparer.OrdinalIgnoreCase); + Assert.Contains("_", nUsing.Declaration.Name); + } + [Fact] + public async Task EscapesReservedKeywords() + { + var model = root.AddClass(new CodeClass + { + Name = "break", + Kind = CodeClassKind.Model + }).First(); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + Assert.NotEqual("break", model.Name, StringComparer.OrdinalIgnoreCase); + Assert.Contains("_", model.Name); + } + [Fact] + public async Task AddsDefaultImports() + { + var model = root.AddClass(new CodeClass + { + Name = "model", + Kind = CodeClassKind.Model + }).First(); + var requestBuilder = root.AddClass(new CodeClass + { + Name = "rb", + Kind = CodeClassKind.RequestBuilder, + }).First(); + requestBuilder.AddMethod(new CodeMethod + { + Name = "get", + Kind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType + { + Name = "string", + }, + }); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + Assert.NotEmpty(model.StartBlock.Usings); + Assert.NotEmpty(requestBuilder.StartBlock.Usings); + } + [Fact] + public async Task ReplacesDateTimeOffsetByNativeType() + { + var model = root.AddClass(new CodeClass + { + Name = "model", + Kind = CodeClassKind.Model + }).First(); + var method = model.AddMethod(new CodeMethod + { + Name = "method", + ReturnType = new CodeType + { + Name = "DateTimeOffset" + }, + }).First(); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + Assert.NotEmpty(model.StartBlock.Usings); + Assert.Equal("DateTime", method.ReturnType.Name); + } + [Fact] + public async Task ReplacesTimeSpanByNativeType() + { + var model = root.AddClass(new CodeClass + { + Name = "model", + Kind = CodeClassKind.Model + }).First(); + var method = model.AddMethod(new CodeMethod + { + Name = "method", + ReturnType = new CodeType + { + Name = "TimeSpan" + }, + }).First(); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + Assert.NotEmpty(model.StartBlock.Usings); + Assert.Equal("Duration", method.ReturnType.Name); + } + [Fact] + public async Task ReplacesIndexersByMethodsWithParameter() + { + var model = root.AddClass(new CodeClass + { + Name = "model", + Kind = CodeClassKind.Model + }).First(); + var collectionNS = root.AddNamespace("collection"); + var itemsNs = collectionNS.AddNamespace($"{collectionNS.Name}.items"); + var requestBuilder = itemsNs.AddClass(new CodeClass + { + Name = "requestBuilder", + Kind = CodeClassKind.RequestBuilder + }).First(); + requestBuilder.AddProperty(new CodeProperty + { + Name = "urlTemplate", + DefaultValue = "path", + Kind = CodePropertyKind.UrlTemplate, + Type = new CodeType + { + Name = "string", + } + }); + requestBuilder.AddIndexer(new CodeIndexer + { + Name = "idx", + ReturnType = new CodeType + { + Name = requestBuilder.Name, + TypeDefinition = requestBuilder, + }, + IndexParameter = new() + { + Name = "id", + Type = new CodeType + { + Name = "string", + }, + } + }); + var collectionRequestBuilder = collectionNS.AddClass(new CodeClass + { + Name = "CollectionRequestBuilder", + Kind = CodeClassKind.RequestBuilder, + }).First(); + collectionRequestBuilder.AddProperty(new CodeProperty + { + Name = "collection", + Kind = CodePropertyKind.RequestBuilder, + Type = new CodeType + { + Name = requestBuilder.Name, + TypeDefinition = requestBuilder, + }, + }); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + Assert.Single(requestBuilder.Properties); + Assert.Empty(requestBuilder.GetChildElements(true).OfType()); + Assert.Single(requestBuilder.Methods, static x => x.IsOfKind(CodeMethodKind.IndexerBackwardCompatibility)); + Assert.Single(collectionRequestBuilder.Properties); + } + [Fact] + public async Task DoesNotKeepCancellationParametersInRequestExecutors() + { + var model = root.AddClass(new CodeClass + { + Name = "model", + Kind = CodeClassKind.RequestBuilder + }).First(); + var method = model.AddMethod(new CodeMethod + { + Name = "getMethod", + Kind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType + { + Name = "string" + } + }).First(); + var cancellationParam = new CodeParameter + { + Name = "cancellationToken", + Optional = true, + Kind = CodeParameterKind.Cancellation, + Documentation = new() + { + DescriptionTemplate = "Cancellation token to use when cancelling requests", + }, + Type = new CodeType { Name = "CancellationToken", IsExternal = true }, + }; + method.AddParameter(cancellationParam); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + Assert.False(method.Parameters.Any()); + Assert.DoesNotContain(cancellationParam, method.Parameters); + } + [Fact] + public async Task NormalizeInheritedClassesNames() + { + var parentModel = root.AddClass(new CodeClass + { + Name = "parent_Model", + Kind = CodeClassKind.Model, + }).First(); + var implementsModel = root.AddClass(new CodeClass + { + Name = "implements_Model", + Kind = CodeClassKind.Model, + }).First(); + var childModel = root.AddClass(new CodeClass + { + Name = "childModel", + Kind = CodeClassKind.Model, + }).First(); + childModel.StartBlock.Inherits = new CodeType + { + Name = "parent_Model", + TypeDefinition = parentModel, + }; + childModel.StartBlock.AddImplements(new CodeType + { + Name = "implements_Model", + TypeDefinition = implementsModel, + }); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + Assert.Equal("ParentModel", childModel.StartBlock.Inherits.Name); + Assert.Equal("ImplementsModel", childModel.StartBlock.Implements.First().Name); + } + #endregion + #region DartLanguageRefinerTests + [Fact] + public async Task CorrectsCoreType() + { + const string requestAdapterDefaultName = "IRequestAdapter"; + const string factoryDefaultName = "ISerializationWriterFactory"; + const string deserializeDefaultName = "IDictionary>"; + const string dateTimeOffsetDefaultName = "DateTimeOffset"; + const string additionalDataDefaultName = "new Dictionary()"; + var model = root.AddClass(new CodeClass + { + Name = "model", + Kind = CodeClassKind.Model + }).First(); + model.AddProperty(new() + { + Name = "core", + Kind = CodePropertyKind.RequestAdapter, + Type = new CodeType + { + Name = requestAdapterDefaultName + } + }, new() + { + Name = "someDate", + Kind = CodePropertyKind.Custom, + Type = new CodeType + { + Name = dateTimeOffsetDefaultName, + } + }, new() + { + Name = "additionalData", + Kind = CodePropertyKind.AdditionalData, + Type = new CodeType + { + Name = additionalDataDefaultName + } + }); + const string additionalDataHolderDefaultName = "IAdditionalDataHolder"; + model.StartBlock.AddImplements(new CodeType + { + Name = additionalDataHolderDefaultName, + IsExternal = true, + }); + var executorMethod = model.AddMethod(new CodeMethod + { + Name = "executor", + Kind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType + { + Name = "string" + } + }, new() + { + Name = "deserializeFields", + ReturnType = new CodeType + { + Name = deserializeDefaultName, + }, + Kind = CodeMethodKind.Deserializer + }).First(); + const string serializerDefaultName = "ISerializationWriter"; + var serializationMethod = model.AddMethod(new CodeMethod + { + Name = "serialization", + Kind = CodeMethodKind.Serializer, + ReturnType = new CodeType + { + Name = "string" + } + }).First(); + serializationMethod.AddParameter(new CodeParameter + { + Name = "handler", + Kind = CodeParameterKind.Serializer, + Type = new CodeType + { + Name = serializerDefaultName, + } + }); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + Assert.DoesNotContain(model.Properties, static x => requestAdapterDefaultName.Equals(x.Type.Name)); + Assert.DoesNotContain(model.Properties, static x => factoryDefaultName.Equals(x.Type.Name)); + Assert.DoesNotContain(model.Properties, static x => dateTimeOffsetDefaultName.Equals(x.Type.Name)); + Assert.DoesNotContain(model.Properties, static x => additionalDataDefaultName.Equals(x.Type.Name)); + Assert.DoesNotContain(model.Methods, static x => deserializeDefaultName.Equals(x.ReturnType.Name)); + Assert.DoesNotContain(model.Methods.SelectMany(static x => x.Parameters), static x => serializerDefaultName.Equals(x.Type.Name)); + Assert.DoesNotContain(model.StartBlock.Implements, static x => additionalDataHolderDefaultName.Equals(x.Name, StringComparison.OrdinalIgnoreCase)); + Assert.Contains(additionalDataHolderDefaultName[1..], model.StartBlock.Implements.Select(static x => x.Name).ToList()); + } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task AddsUsingForUntypedNode(bool usesBackingStore) + { + var model = root.AddClass(new CodeClass + { + Name = "model", + Kind = CodeClassKind.Model + }).First(); + var property = model.AddProperty(new CodeProperty + { + Name = "property", + Type = new CodeType + { + Name = KiotaBuilder.UntypedNodeName, + IsExternal = true + }, + }).First(); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart, UsesBackingStore = usesBackingStore }, root); + Assert.Equal(KiotaBuilder.UntypedNodeName, property.Type.Name); + Assert.NotEmpty(model.StartBlock.Usings); + var nodeUsing = model.StartBlock.Usings.Where(static declaredUsing => declaredUsing.Name.Equals(KiotaBuilder.UntypedNodeName, StringComparison.OrdinalIgnoreCase)).ToArray(); + Assert.Single(nodeUsing); + Assert.Equal("microsoft_kiota_abstractions/microsoft_kiota_abstractions", nodeUsing[0].Declaration.Name); + } + [Fact] + public async Task AddsCustomMethods() + { + var builder = root.AddClass(new CodeClass + { + Name = "builder", + Kind = CodeClassKind.RequestBuilder + }).First(); + var model = root.AddClass(new CodeClass + { + Name = "model", + Kind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + var buildermethods = builder.Methods; + var modelmethods = model.Methods; + Assert.Contains(buildermethods, x => x.IsOfKind(CodeMethodKind.Custom) && x.Name.Equals("clone", StringComparison.Ordinal)); + Assert.Contains(modelmethods, x => x.IsOfKind(CodeMethodKind.Custom) && x.Name.Equals("copyWith", StringComparison.Ordinal)); + } + #endregion +} diff --git a/tests/Kiota.Builder.Tests/Writers/Dart/CodeClassDeclarationWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Dart/CodeClassDeclarationWriterTests.cs new file mode 100644 index 0000000000..316990a799 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/Dart/CodeClassDeclarationWriterTests.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; + +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Writers; +using Kiota.Builder.Writers.Dart; + +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Dart; +public sealed class CodeClassDeclarationWriterTests : IDisposable +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private const string DefaultNameSpace = "ns"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeClassDeclarationWriter codeElementWriter; + private readonly CodeClass parentClass; + private readonly CodeNamespace root; + + public CodeClassDeclarationWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Dart, DefaultPath, DefaultName); + codeElementWriter = new CodeClassDeclarationWriter(new DartConventionService(), DefaultNameSpace, (Builder.PathSegmenters.DartPathSegmenter)writer.PathSegmenter); + tw = new StringWriter(); + writer.SetTextWriter(tw); + root = CodeNamespace.InitRootNamespace(); + parentClass = new() + { + Name = "parentClass" + }; + root.AddClass(parentClass); + } + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + [Fact] + public void WritesSimpleDeclaration() + { + codeElementWriter.WriteCodeElement(parentClass.StartBlock, writer); + var result = tw.ToString(); + Assert.Contains("class", result); + } + [Fact] + public void WritesImplementation() + { + var declaration = parentClass.StartBlock; + declaration.AddImplements(new CodeType + { + Name = "someInterface" + }); + codeElementWriter.WriteCodeElement(declaration, writer); + var result = tw.ToString(); + Assert.Contains("implements someInterface", result); + } + [Fact] + public void WritesInheritance() + { + var declaration = parentClass.StartBlock; + declaration.Inherits = new() + { + Name = "someParent" + }; + codeElementWriter.WriteCodeElement(declaration, writer); + var result = tw.ToString(); + Assert.Contains("extends", result); + Assert.Contains("SomeParent", result); + } + [Fact] + public void WritesImports() + { + var declaration = parentClass.StartBlock; + CodeClass messageClass = new() + { + Name = "Message" + }; + root.AddClass(messageClass); + declaration.AddUsings(new CodeUsing() + { + Name = "project.graph", + Declaration = new() + { + Name = "Message", + TypeDefinition = messageClass + } + }); + codeElementWriter.WriteCodeElement(declaration, writer); + var result = tw.ToString(); + Assert.Contains("import './message.dart';", result); + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/Dart/CodeClassEndWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Dart/CodeClassEndWriterTests.cs new file mode 100644 index 0000000000..c590b2bc7f --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/Dart/CodeClassEndWriterTests.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Linq; + +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Writers; +using Kiota.Builder.Writers.Dart; + +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Dart; +public sealed class CodeClassEndWriterTests : IDisposable +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeBlockEndWriter codeElementWriter; + private readonly CodeClass parentClass; + public CodeClassEndWriterTests() + { + codeElementWriter = new CodeBlockEndWriter(); + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Dart, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + var root = CodeNamespace.InitRootNamespace(); + parentClass = new CodeClass + { + Name = "parentClass" + }; + root.AddClass(parentClass); + } + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + [Fact] + public void ClosesNonNestedClasses() + { + codeElementWriter.WriteCodeElement(parentClass.EndBlock, writer); + var result = tw.ToString(); + Assert.Equal(1, result.Count(x => x == '}')); + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/Dart/CodeEnumWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Dart/CodeEnumWriterTests.cs new file mode 100644 index 0000000000..37ce1e96f9 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/Dart/CodeEnumWriterTests.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.Linq; + +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Writers; + +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Dart; +public sealed class CodeEnumWriterTests : IDisposable +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeEnum currentEnum; + private const string EnumName = "SomeEnum"; + public CodeEnumWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Dart, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + var root = CodeNamespace.InitRootNamespace(); + currentEnum = root.AddEnum(new CodeEnum + { + Name = EnumName, + }).First(); + } + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + [Fact] + public void WritesEnum() + { + const string optionName = "option1"; + currentEnum.AddOption(new CodeEnumOption { Name = optionName, SerializationName = optionName }); + writer.Write(currentEnum); + var result = tw.ToString(); + Assert.Contains("enum", result); + Assert.Contains(EnumName, result); + Assert.Contains($"{optionName}('{optionName}')", result); + Assert.Contains($"const {EnumName}(this.value);", result); + Assert.Contains("final String value;", result); + } + [Fact] + public void DoesntWriteAnythingOnNoOption() + { + writer.Write(currentEnum); + var result = tw.ToString(); + Assert.Empty(result); + } + [Fact] + public void WritesEnumOptionDescription() + { + var option = new CodeEnumOption + { + Documentation = new() + { + DescriptionTemplate = "Some option description", + }, + Name = "option1", + }; + currentEnum.AddOption(option); + writer.Write(currentEnum); + var result = tw.ToString(); + Console.WriteLine(result); + Assert.Contains($"/// {option.Documentation.DescriptionTemplate}", result); + } + [Fact] + public void WritesEnumSerializationValue() + { + var OptionName = "plus1"; + var SerializationValue = "+1"; + var option = new CodeEnumOption + { + Name = OptionName, + SerializationName = SerializationValue + }; + currentEnum.AddOption(option); + writer.Write(currentEnum); + var result = tw.ToString(); + Assert.Contains($"{OptionName}('{SerializationValue}')", result); + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/Dart/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Dart/CodeMethodWriterTests.cs new file mode 100644 index 0000000000..7426330ec5 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/Dart/CodeMethodWriterTests.cs @@ -0,0 +1,1555 @@ +using System; +using System.IO; +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; +using Kiota.Builder.Writers; +using Kiota.Builder.Writers.Dart; + +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Dart; +public sealed class CodeMethodWriterTests : IDisposable +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private CodeMethod method; + private CodeClass parentClass; + private readonly CodeNamespace root; + private const string ExecuterExceptionVar = "executionException"; + 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 CodeMethodWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Dart, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + root = CodeNamespace.InitRootNamespace(); + } + private void setup(bool withInheritance = false) + { + if (parentClass != null) + throw new InvalidOperationException("setup() must only be called once"); + CodeClass baseClass = default; + if (withInheritance) + { + baseClass = root.AddClass(new CodeClass + { + Name = "SomeParentClass", + }).First(); + baseClass.AddProperty(new CodeProperty + { + Name = "definedInParent", + Type = new CodeType + { + Name = "String" + }, + Kind = CodePropertyKind.Custom, + }); + } + parentClass = new CodeClass + { + Name = "ParentClass" + }; + if (withInheritance) + { + parentClass.StartBlock.Inherits = new CodeType + { + Name = "SomeParentClass", + TypeDefinition = baseClass + }; + } + root.AddClass(parentClass); + method = new CodeMethod + { + Name = MethodName, + ReturnType = new CodeType + { + Name = ReturnTypeName + } + }; + parentClass.AddMethod(method); + } + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + private void AddRequestProperties() + { + parentClass.StartBlock.Inherits = new CodeType + { + Name = "BaseRequestBuilder", + IsExternal = true, + }; + parentClass.AddProperty(new CodeProperty + { + Name = "requestAdapter", + Kind = CodePropertyKind.RequestAdapter, + Type = new CodeType + { + Name = "RequestAdapter", + } + }); + parentClass.AddProperty(new CodeProperty + { + Name = "pathParameters", + Kind = CodePropertyKind.PathParameters, + Type = new CodeType + { + Name = "String", + } + }); + parentClass.AddProperty(new CodeProperty + { + Name = "urlTemplate", + Kind = CodePropertyKind.UrlTemplate, + Type = new CodeType + { + Name = "String", + } + }); + } + private void AddSerializationProperties() + { + parentClass.AddProperty(new CodeProperty + { + Name = "additionalData", + Kind = CodePropertyKind.AdditionalData, + Type = new CodeType + { + Name = "String" + }, + Getter = new CodeMethod + { + Name = "getAdditionalData", + ReturnType = new CodeType + { + Name = "String" + } + }, + Setter = new CodeMethod + { + Name = "setAdditionalData", + ReturnType = new CodeType + { + Name = "String" + } + } + }); + parentClass.AddProperty(new CodeProperty + { + Name = "dummyProp", + Type = new CodeType + { + Name = "String" + }, + Getter = new CodeMethod + { + Name = "getDummyProp", + ReturnType = new CodeType + { + Name = "String" + }, + }, + Setter = new CodeMethod + { + Name = "setDummyProp", + ReturnType = new CodeType + { + Name = "void" + } + }, + }); + parentClass.AddProperty(new CodeProperty + { + Name = "noAccessors", + Kind = CodePropertyKind.Custom, + Type = new CodeType + { + Name = "String" + } + }); + parentClass.AddProperty(new CodeProperty + { + Name = "dummyColl", + Type = new CodeType + { + Name = "String", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + }, + Getter = new CodeMethod + { + Name = "getDummyColl", + ReturnType = new CodeType + { + Name = "String", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + }, + }, + Setter = new CodeMethod + { + Name = "setDummyColl", + ReturnType = new CodeType + { + Name = "void", + } + }, + }); + parentClass.AddProperty(new CodeProperty + { + Name = "dummyComplexColl", + Type = new CodeType + { + Name = "Complex", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + TypeDefinition = new CodeClass + { + Name = "SomeComplexType" + } + }, + Getter = new CodeMethod + { + Name = "getDummyComplexColl", + ReturnType = new CodeType + { + Name = "String", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + }, + }, + Setter = new CodeMethod + { + Name = "setDummyComplexColl", + ReturnType = new CodeType + { + Name = "void" + } + } + }); + parentClass.AddProperty(new CodeProperty + { + Name = "dummyEnumCollection", + Type = new CodeType + { + Name = "SomeEnum", + TypeDefinition = new CodeEnum + { + Name = "EnumType" + } + }, + Getter = new CodeMethod + { + Name = "getDummyEnumCollection", + ReturnType = new CodeType + { + Name = "String" + }, + }, + Setter = new CodeMethod + { + Name = "setDummyEnumCollection", + ReturnType = new CodeType + { + Name = "void" + } + } + }); + } + private CodeClass AddUnionType() + { + var complexType1 = root.AddClass(new CodeClass + { + Name = "ComplexType1", + Kind = CodeClassKind.Model, + }).First(); + var complexType2 = root.AddClass(new CodeClass + { + Name = "ComplexType2", + Kind = CodeClassKind.Model, + }).First(); + var unionType = root.AddClass(new CodeClass + { + Name = "UnionType", + Kind = CodeClassKind.Model, + OriginalComposedType = new CodeUnionType + { + Name = "UnionType", + }, + DiscriminatorInformation = new() + { + DiscriminatorPropertyName = "@odata.type", + }, + }).First(); + var cType1 = new CodeType + { + Name = "ComplexType1", + TypeDefinition = complexType1 + }; + var cType2 = new CodeType + { + Name = "ComplexType2", + TypeDefinition = complexType2, + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Complex, + }; + var sType = new CodeType + { + Name = "String", + }; + unionType.DiscriminatorInformation.AddDiscriminatorMapping("#kiota.complexType1", new CodeType + { + Name = "ComplexType1", + TypeDefinition = cType1 + }); + unionType.DiscriminatorInformation.AddDiscriminatorMapping("#kiota.complexType2", new CodeType + { + Name = "ComplexType2", + TypeDefinition = cType2 + }); + unionType.OriginalComposedType.AddType(cType1); + unionType.OriginalComposedType.AddType(cType2); + unionType.OriginalComposedType.AddType(sType); + unionType.AddProperty(new CodeProperty + { + Name = "complexType1Value", + Type = cType1, + Kind = CodePropertyKind.Custom, + Setter = new CodeMethod + { + Name = "setComplexType1Value", + ReturnType = new CodeType + { + Name = "void" + }, + Kind = CodeMethodKind.Setter, + }, + Getter = new CodeMethod + { + Name = "getComplexType1Value", + ReturnType = cType1, + Kind = CodeMethodKind.Getter, + } + }); + unionType.AddProperty(new CodeProperty + { + Name = "complexType2Value", + Type = cType2, + Kind = CodePropertyKind.Custom, + Setter = new CodeMethod + { + Name = "setComplexType2Value", + ReturnType = new CodeType + { + Name = "void" + }, + Kind = CodeMethodKind.Setter, + }, + Getter = new CodeMethod + { + Name = "getComplexType2Value", + ReturnType = cType2, + Kind = CodeMethodKind.Getter, + } + }); + unionType.AddProperty(new CodeProperty + { + Name = "stringValue", + Type = sType, + Kind = CodePropertyKind.Custom, + Setter = new CodeMethod + { + Name = "setStringValue", + ReturnType = new CodeType + { + Name = "void" + }, + Kind = CodeMethodKind.Setter, + }, + Getter = new CodeMethod + { + Name = "getStringValue", + ReturnType = sType, + Kind = CodeMethodKind.Getter, + } + }); + return unionType; + } + private CodeClass AddIntersectionType() + { + var complexType1 = root.AddClass(new CodeClass + { + Name = "ComplexType1", + Kind = CodeClassKind.Model, + }).First(); + var complexType2 = root.AddClass(new CodeClass + { + Name = "ComplexType2", + Kind = CodeClassKind.Model, + }).First(); + var complexType3 = root.AddClass(new CodeClass + { + Name = "ComplexType3", + Kind = CodeClassKind.Model, + }).First(); + var intersectionType = root.AddClass(new CodeClass + { + Name = "IntersectionType", + Kind = CodeClassKind.Model, + OriginalComposedType = new CodeIntersectionType + { + Name = "IntersectionType", + }, + DiscriminatorInformation = new() + { + DiscriminatorPropertyName = "@odata.type", + }, + }).First(); + var cType1 = new CodeType + { + Name = "ComplexType1", + TypeDefinition = complexType1 + }; + var cType2 = new CodeType + { + Name = "ComplexType2", + TypeDefinition = complexType2, + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Complex, + }; + var cType3 = new CodeType + { + Name = "ComplexType3", + TypeDefinition = complexType3 + }; + intersectionType.DiscriminatorInformation.AddDiscriminatorMapping("#kiota.complexType1", new CodeType + { + Name = "ComplexType1", + TypeDefinition = cType1 + }); + intersectionType.DiscriminatorInformation.AddDiscriminatorMapping("#kiota.complexType2", new CodeType + { + Name = "ComplexType2", + TypeDefinition = cType2 + }); + intersectionType.DiscriminatorInformation.AddDiscriminatorMapping("#kiota.complexType3", new CodeType + { + Name = "ComplexType3", + TypeDefinition = cType3 + }); + var sType = new CodeType + { + Name = "String", + }; + intersectionType.OriginalComposedType.AddType(cType1); + intersectionType.OriginalComposedType.AddType(cType2); + intersectionType.OriginalComposedType.AddType(cType3); + intersectionType.OriginalComposedType.AddType(sType); + intersectionType.AddProperty(new CodeProperty + { + Name = "complexType1Value", + Type = cType1, + Kind = CodePropertyKind.Custom, + Setter = new CodeMethod + { + Name = "setComplexType1Value", + ReturnType = new CodeType + { + Name = "void" + }, + Kind = CodeMethodKind.Setter, + }, + Getter = new CodeMethod + { + Name = "getComplexType1Value", + ReturnType = cType1, + Kind = CodeMethodKind.Getter, + } + }); + intersectionType.AddProperty(new CodeProperty + { + Name = "complexType2Value", + Type = cType2, + Kind = CodePropertyKind.Custom, + Setter = new CodeMethod + { + Name = "setComplexType2Value", + ReturnType = new CodeType + { + Name = "void" + }, + Kind = CodeMethodKind.Setter, + }, + Getter = new CodeMethod + { + Name = "getComplexType2Value", + ReturnType = cType2, + Kind = CodeMethodKind.Getter, + } + }); + intersectionType.AddProperty(new CodeProperty + { + Name = "complexType3Value", + Type = cType3, + Kind = CodePropertyKind.Custom, + Setter = new CodeMethod + { + Name = "setComplexType3Value", + ReturnType = new CodeType + { + Name = "void" + }, + Kind = CodeMethodKind.Setter, + }, + Getter = new CodeMethod + { + Name = "getComplexType3Value", + ReturnType = cType3, + Kind = CodeMethodKind.Getter, + } + }); + intersectionType.AddProperty(new CodeProperty + { + Name = "stringValue", + Type = sType, + Kind = CodePropertyKind.Custom, + Setter = new CodeMethod + { + Name = "setStringValue", + ReturnType = new CodeType + { + Name = "void" + }, + Kind = CodeMethodKind.Setter, + }, + Getter = new CodeMethod + { + Name = "getStringValue", + ReturnType = sType, + Kind = CodeMethodKind.Getter, + } + }); + return intersectionType; + } + private void AddRequestBodyParameters(bool useComplexTypeForBody = false) + { + var stringType = new CodeType + { + Name = "String", + }; + var requestConfigClass = parentClass.AddInnerClass(new CodeClass + { + Name = "RequestConfig", + Kind = CodeClassKind.RequestConfiguration, + }).First(); + requestConfigClass.AddProperty(new() + { + Name = "h", + Kind = CodePropertyKind.Headers, + Type = stringType, + }, + new() + { + Name = "q", + Kind = CodePropertyKind.QueryParameters, + Type = stringType, + }, + new() + { + Name = "o", + Kind = CodePropertyKind.Options, + Type = stringType, + }); + method.AddParameter(new CodeParameter + { + Name = "b", + Kind = CodeParameterKind.RequestBody, + Type = useComplexTypeForBody ? new CodeType + { + Name = "SomeComplexTypeForRequestBody", + TypeDefinition = root.AddClass(new CodeClass + { + Name = "SomeComplexTypeForRequestBody", + Kind = CodeClassKind.Model, + }).First(), + } : stringType, + }); + var configType = new CodeType + { + Name = "RequestConfig", + TypeDefinition = requestConfigClass, + ActionOf = true, + }; + configType.AddGenericTypeParameterValue(new CodeType { Name = "DefaultQueryParameters" }); + method.AddParameter(new CodeParameter + { + Name = "c", + Kind = CodeParameterKind.RequestConfiguration, + Type = configType, + Optional = true, + }); + } + [Fact] + public void WritesVoidTypeForExecutor() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + method.AddParameter(new CodeParameter() + { + Type = new CodeType(), + Kind = CodeParameterKind.RequestConfiguration + }); + method.ReturnType = new CodeType + { + Name = "void", + }; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("Future", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestBuilder() + { + setup(); + method.Kind = CodeMethodKind.RequestBuilderBackwardCompatibility; + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void WritesRequestBodiesThrowOnNullHttpMethod() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + Assert.Throws(() => writer.Write(method)); + method.Kind = CodeMethodKind.RequestGenerator; + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void WritesRequestExecutorBody() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + }).First(); + var error5XX = root.AddClass(new CodeClass + { + Name = "Error5XX", + }).First(); + var error401 = root.AddClass(new CodeClass + { + Name = "Error401", + }).First(); + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }); + method.AddErrorMapping("5XX", new CodeType { Name = "Error5XX", TypeDefinition = error5XX }); + method.AddErrorMapping("401", new CodeType { Name = "Error401", TypeDefinition = error401 }); + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("var requestInfo", result); + Assert.Contains("final errorMapping = >{", result); + Assert.Contains("'401' : Error401.createFromDiscriminatorValue,", result); + Assert.Contains("'4XX' : Error4XX.createFromDiscriminatorValue,", result); + Assert.Contains("'5XX' : Error5XX.createFromDiscriminatorValue,", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void DoesntCreateDictionaryOnEmptyErrorMapping() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("Map> errorMapping = {", result); + Assert.Contains("{}", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesModelFactoryBody() + { + setup(); + var parentModel = root.AddClass(new CodeClass + { + Name = "ParentModel", + Kind = CodeClassKind.Model, + }).First(); + var childModel = root.AddClass(new CodeClass + { + Name = "ChildModel", + Kind = CodeClassKind.Model, + }).First(); + childModel.StartBlock.Inherits = new CodeType + { + Name = "ParentModel", + TypeDefinition = parentModel, + }; + var factoryMethod = parentModel.AddMethod(new CodeMethod + { + Name = "factory", + Kind = CodeMethodKind.Factory, + ReturnType = new CodeType + { + Name = "ParentModel", + TypeDefinition = parentModel, + }, + IsStatic = true, + }).First(); + parentModel.DiscriminatorInformation.AddDiscriminatorMapping("ns.childmodel", new CodeType + { + Name = "childModel", + TypeDefinition = childModel, + }); + parentModel.DiscriminatorInformation.DiscriminatorPropertyName = "@odata.type"; + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + TypeDefinition = new CodeClass + { + Name = "ParseNode", + }, + IsExternal = true, + }, + Optional = false, + }); + writer.Write(factoryMethod); + var result = tw.ToString(); + Assert.Contains("var mappingValue = parseNode.getChildNode('@odata.type')", result); + Assert.Contains("return switch(mappingValue) {", result); + Assert.Contains("'ns.childmodel' => ChildModel(),", result); + Assert.Contains("_ => ParentModel()", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void DoesntWriteFactorySwitchOnMissingParameter() + { + setup(); + var parentModel = root.AddClass(new CodeClass + { + Name = "parentModel", + Kind = CodeClassKind.Model, + }).First(); + var childModel = root.AddClass(new CodeClass + { + Name = "childModel", + Kind = CodeClassKind.Model, + }).First(); + childModel.StartBlock.Inherits = new CodeType + { + Name = "parentModel", + TypeDefinition = parentModel, + }; + var factoryMethod = parentModel.AddMethod(new CodeMethod + { + Name = "factory", + Kind = CodeMethodKind.Factory, + ReturnType = new CodeType + { + Name = "parentModel", + TypeDefinition = parentModel, + }, + IsStatic = true, + }).First(); + parentModel.DiscriminatorInformation.AddDiscriminatorMapping("ns.childmodel", new CodeType + { + Name = "childModel", + TypeDefinition = childModel, + }); + parentModel.DiscriminatorInformation.DiscriminatorPropertyName = "@odata.type"; + Assert.Throws(() => writer.Write(factoryMethod)); + } + [Fact] + public void DoesntWriteFactorySwitchOnEmptyPropertyName() + { + setup(); + var parentModel = root.AddClass(new CodeClass + { + Name = "ParentModel", + Kind = CodeClassKind.Model, + }).First(); + var childModel = root.AddClass(new CodeClass + { + Name = "ChildModel", + Kind = CodeClassKind.Model, + }).First(); + childModel.StartBlock.Inherits = new CodeType + { + Name = "parentModel", + TypeDefinition = parentModel, + }; + var factoryMethod = parentModel.AddMethod(new CodeMethod + { + Name = "factory", + Kind = CodeMethodKind.Factory, + ReturnType = new CodeType + { + Name = "ParentModel", + TypeDefinition = parentModel, + }, + IsStatic = true, + }).First(); + parentModel.DiscriminatorInformation.AddDiscriminatorMapping("ns.childmodel", new CodeType + { + Name = "ChildModel", + TypeDefinition = childModel, + }); + parentModel.DiscriminatorInformation.DiscriminatorPropertyName = string.Empty; + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + TypeDefinition = new CodeClass + { + Name = "ParseNode", + }, + IsExternal = true, + }, + Optional = false, + }); + writer.Write(factoryMethod); + var result = tw.ToString(); + Assert.DoesNotContain("var mappingValue = parseNode.getChildNode('@odata.type')", result); + Assert.DoesNotContain("return switch(mappingValue) {", result); + Assert.DoesNotContain("'ns.childmodel' => ChildModel(),", result); + Assert.Contains("return ParentModel()", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void DoesntWriteFactorySwitchOnEmptyMappings() + { + setup(); + var parentModel = root.AddClass(new CodeClass + { + Name = "ParentModel", + Kind = CodeClassKind.Model, + }).First(); + var factoryMethod = parentModel.AddMethod(new CodeMethod + { + Name = "factory", + Kind = CodeMethodKind.Factory, + ReturnType = new CodeType + { + Name = "ParentModel", + TypeDefinition = parentModel, + }, + IsStatic = true, + }).First(); + parentModel.DiscriminatorInformation.DiscriminatorPropertyName = "@odata.type"; + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + TypeDefinition = new CodeClass + { + Name = "ParseNode", + }, + IsExternal = true, + }, + Optional = false, + }); + writer.Write(factoryMethod); + var result = tw.ToString(); + Assert.DoesNotContain("final ParseNode mappingValueNode = parseNode.getChildNode(\"@odata.type\")", result); + Assert.DoesNotContain("if (mappingValueNode != null) {", result); + Assert.DoesNotContain("final String mappingValue = mappingValueNode.getStringValue()", result); + Assert.DoesNotContain("switch (mappingValue) {", result); + Assert.DoesNotContain("case \"ns.childmodel\": return new ChildModel();", result); + Assert.Contains("return ParentModel()", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestGeneratorBodyForMultipart() + { + setup(); + method.Kind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Post; + AddRequestProperties(); + AddRequestBodyParameters(); + method.Parameters.First(static x => x.IsOfKind(CodeParameterKind.RequestBody)).Type = new CodeType { Name = "MultipartBody", IsExternal = true }; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("setContentFromParsable", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestExecutorBodyForCollections() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + method.ReturnType.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("sendCollection", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + + //vanaf hier, heeft ook configure nodig, dus wel generics + [Fact] + public void WritesRequestGeneratorBodyForScalar() + { + setup(); + method.Kind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Get; + AddRequestProperties(); + AddRequestBodyParameters(); + method.AcceptedResponseTypes.Add("application/json"); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("var requestInfo = RequestInformation(httpMethod : HttpMethod.get, urlTemplate : urlTemplate, pathParameters : pathParameters);", result); + Assert.Contains("requestInfo.configure(c, () => DefaultQueryParameters());", result); + Assert.Contains("requestInfo.headers.put('Accept', 'application/json');", result); + Assert.Contains("requestInfo.setContentFromScalar(requestAdapter, '', b)", result); + Assert.Contains("return requestInfo;", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestGeneratorBodyForScalarCollection() + { + setup(); + method.Kind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Get; + AddRequestProperties(); + AddRequestBodyParameters(); + method.AcceptedResponseTypes.Add("application/json"); + var bodyParameter = method.Parameters.OfKind(CodeParameterKind.RequestBody); + bodyParameter.Type.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Complex; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("var requestInfo = RequestInformation(httpMethod : HttpMethod.get, urlTemplate : urlTemplate, pathParameters : pathParameters)", result); + Assert.Contains("requestInfo.configure(c, () => DefaultQueryParameters());", result); + Assert.Contains("requestInfo.headers.put('Accept', 'application/json');", result); + Assert.Contains("setContentFromScalarCollection", result); + Assert.Contains("return requestInfo;", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestGeneratorBodyForParsable() + { + setup(); + method.Kind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Get; + AddRequestProperties(); + AddRequestBodyParameters(true); + method.AcceptedResponseTypes.Add("application/json"); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("var requestInfo = RequestInformation(httpMethod : HttpMethod.get, urlTemplate : urlTemplate, pathParameters : pathParameters)", result); + Assert.Contains("requestInfo.configure(c, () => DefaultQueryParameters());", result); + Assert.Contains("requestInfo.headers.put('Accept', 'application/json')", result); + Assert.Contains("setContentFromParsable", result); + Assert.Contains("return requestInfo;", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestGeneratorBodyWhenUrlTemplateIsOverrode() + { + setup(); + method.Kind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Get; + AddRequestProperties(); + AddRequestBodyParameters(true); + method.AcceptedResponseTypes.Add("application/json"); + method.UrlTemplateOverride = "{baseurl+}/foo/bar"; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("var requestInfo = RequestInformation(httpMethod : HttpMethod.get, urlTemplate : '{baseurl+}/foo/bar', pathParameters : pathParameters)", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesUnionDeSerializerBody() + { + setup(); + var wrapper = AddUnionType(); + var deserializationMethod = wrapper.AddMethod(new CodeMethod + { + Name = "getFieldDeserializers", + Kind = CodeMethodKind.Deserializer, + IsAsync = false, + ReturnType = new CodeType + { + Name = "Map", + }, + }).First(); + writer.Write(deserializationMethod); + var result = tw.ToString(); + Assert.Contains("complexType1Value != null", result); + Assert.Contains("return complexType1Value!.getFieldDeserializers()", result); + Assert.Contains("{}", result); + AssertExtensions.Before("return complexType1Value!.getFieldDeserializers()", "{}", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesIntersectionDeSerializerBody() + { + setup(); + var wrapper = AddIntersectionType(); + var deserializationMethod = wrapper.AddMethod(new CodeMethod + { + Name = "getFieldDeserializers", + Kind = CodeMethodKind.Deserializer, + IsAsync = false, + ReturnType = new CodeType + { + Name = "Map", + }, + }).First(); + writer.Write(deserializationMethod); + var result = tw.ToString(); + Assert.Contains("var deserializers = {};", result); + Assert.Contains("if(complexType1Value != null){complexType1Value!.getFieldDeserializers().forEach((k,v) => deserializers.putIfAbsent(k, ()=>v));}", result); + Assert.Contains("if(complexType3Value != null){complexType3Value!.getFieldDeserializers().forEach((k,v) => deserializers.putIfAbsent(k, ()=>v));}", result); + Assert.Contains("return deserializers", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesInheritedDeSerializerBody() + { + setup(true); + method.Kind = CodeMethodKind.Deserializer; + method.IsAsync = false; + AddSerializationProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("super.getFieldDeserializers()", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesDeSerializerBody() + { + setup(); + method.Kind = CodeMethodKind.Deserializer; + method.IsAsync = false; + AddSerializationProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("getStringValue", result); + Assert.Contains("getCollectionOfPrimitiveValues", result); + Assert.Contains("getCollectionOfObjectValues", result); + Assert.Contains("getEnumValue", result); + Assert.DoesNotContain("definedInParent", result, StringComparison.OrdinalIgnoreCase); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesInheritedSerializerBody() + { + setup(true); + method.Kind = CodeMethodKind.Serializer; + method.IsAsync = false; + AddSerializationProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("super.serialize", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesUnionSerializerBody() + { + setup(); + var wrapper = AddUnionType(); + var serializationMethod = wrapper.AddMethod(new CodeMethod + { + Name = "factory", + Kind = CodeMethodKind.Serializer, + IsAsync = false, + ReturnType = new CodeType + { + Name = "void", + }, + }).First(); + serializationMethod.AddParameter(new CodeParameter + { + Name = "writer", + Kind = CodeParameterKind.Serializer, + Type = new CodeType + { + Name = "SerializationWriter" + } + }); + writer.Write(serializationMethod); + var result = tw.ToString(); + Assert.DoesNotContain("super.serialize(writer)", result); + Assert.Contains("if(complexType1Value != null) {", result); + Assert.Contains("writer.writeObjectValue(null, complexType1Value)", result); + Assert.Contains("stringValue != null", result); + Assert.Contains("writer.writeStringValue(null, stringValue)", result); + Assert.Contains("complexType2Value != null", result); + Assert.Contains("writer.writeCollectionOfObjectValues(null, complexType2Value)", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesIntersectionSerializerBody() + { + setup(); + var wrapper = AddIntersectionType(); + var serializationMethod = wrapper.AddMethod(new CodeMethod + { + Name = "factory", + Kind = CodeMethodKind.Serializer, + IsAsync = false, + ReturnType = new CodeType + { + Name = "void", + }, + }).First(); + serializationMethod.AddParameter(new CodeParameter + { + Name = "writer", + Kind = CodeParameterKind.Serializer, + Type = new CodeType + { + Name = "SerializationWriter" + } + }); + writer.Write(serializationMethod); + var result = tw.ToString(); + Assert.DoesNotContain("super.serialize(writer)", result); + Assert.DoesNotContain("complexType1Value != null) {", result); + Assert.Contains("writer.writeObjectValue(null, complexType1Value, [complexType3Value])", result); + Assert.Contains("stringValue != null", result); + Assert.Contains("writer.writeStringValue(null, stringValue)", result); + Assert.Contains("complexType2Value != null", result); + Assert.Contains("writer.writeCollectionOfObjectValues(null, complexType2Value)", result); + AssertExtensions.Before("writer.writeStringValue(null, stringValue)", "writer.writeObjectValue(null, complexType1Value, [complexType3Value])", result); + AssertExtensions.Before("writer.writeCollectionOfObjectValues(null, complexType2Value)", "writer.writeObjectValue(null, complexType1Value, [complexType3Value])", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesSerializerBody() + { + setup(); + method.Kind = CodeMethodKind.Serializer; + method.IsAsync = false; + AddSerializationProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("writeStringValue", result); + Assert.Contains("writeCollectionOfPrimitiveValues", result); + Assert.Contains("writeCollectionOfObjectValues", result); + Assert.Contains("writeEnumValue", result); + Assert.Contains("writeAdditionalData(additionalData);", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesMethodDescriptionLink() + { + setup(); + method.Documentation.DescriptionTemplate = MethodDescription; + method.Documentation.DocumentationLabel = "see more"; + method.Documentation.DocumentationLink = new("https://foo.org/docs"); + method.IsAsync = false; + var parameter = new CodeParameter + { + Documentation = new() + { + DescriptionTemplate = ParamDescription, + }, + Name = ParamName, + Type = new CodeType + { + Name = "String" + } + }; + method.AddParameter(parameter); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("[see more](https://foo.org/docs)", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void Defensive() + { + setup(); + var codeMethodWriter = new CodeMethodWriter(new DartConventionService()); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(null, writer)); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, null)); + var originalParent = method.Parent; + method.Parent = CodeNamespace.InitRootNamespace(); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); + method.Parent = originalParent; + } + [Fact] + public void ThrowsIfParentIsNotClass() + { + setup(); + method.Parent = CodeNamespace.InitRootNamespace(); + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void WritesReturnType() + { + setup(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"{ReturnTypeName} {MethodName}", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesPublicMethodByDefault() + { + setup(); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("_", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesPrivateMethod() + { + setup(); + method.Access = AccessModifier.Private; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("_", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesPathParameterRequestBuilder() + { + setup(); + AddRequestProperties(); + method.Kind = CodeMethodKind.RequestBuilderWithParameters; + method.AddParameter(new CodeParameter + { + Name = "pathParam", + Kind = CodeParameterKind.Path, + Type = new CodeType + { + Name = "String" + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("requestAdapter", result); + Assert.Contains("pathParameters", result); + Assert.Contains("pathParam", result); + Assert.Contains("return", result); + } + [Fact] + public void WritesConstructor() + { + setup(); + method.Kind = CodeMethodKind.Constructor; + var defaultValue = "'someVal'"; + var propName = "propWithDefaultValue"; + parentClass.Kind = CodeClassKind.RequestBuilder; + parentClass.AddProperty(new CodeProperty + { + Name = propName, + DefaultValue = defaultValue, + Kind = CodePropertyKind.Custom, + Type = new CodeType + { + Name = "String" + } + }); + var defaultValueNull = "'null'"; + var nullPropName = "propWithDefaultNullValue"; + parentClass.AddProperty(new CodeProperty + { + Name = nullPropName, + DefaultValue = defaultValueNull, + Kind = CodePropertyKind.Custom, + Type = new CodeType + { + Name = "int", + IsNullable = true + } + }); + var defaultValueBool = "true"; + var boolPropName = "propWithDefaultBoolValue"; + parentClass.AddProperty(new CodeProperty + { + Name = boolPropName, + DefaultValue = defaultValueBool, + Kind = CodePropertyKind.Custom, + Type = new CodeType + { + Name = "Boolean", + IsNullable = true + } + }); + AddRequestProperties(); + method.AddParameter(new CodeParameter + { + Name = "pathParameters", + Kind = CodeParameterKind.PathParameters, + Type = new CodeType + { + Name = "Map" + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name, result); + Assert.Contains($"{propName} = '{defaultValue}'", result); + Assert.Contains($"{nullPropName} = {defaultValueNull}", result); + Assert.Contains($"{boolPropName} = {defaultValueBool}", result); + Assert.Contains("super", result); + } + [Fact] + public void WritesWithUrl() + { + setup(); + method.Kind = CodeMethodKind.RawUrlBuilder; + Assert.Throws(() => writer.Write(method)); + method.AddParameter(new CodeParameter + { + Name = "rawUrl", + Kind = CodeParameterKind.RawUrl, + Type = new CodeType + { + Name = "String" + }, + }); + Assert.Throws(() => writer.Write(method)); + AddRequestProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"return {parentClass.Name}", result); + } + [Fact] + public void DoesNotWriteConstructorWithDefaultFromComposedType() + { + setup(); + method.Kind = CodeMethodKind.Constructor; + var defaultValue = "Test Value"; + var propName = "size"; + var unionType = root.AddClass(new CodeClass + { + Name = "UnionType", + Kind = CodeClassKind.Model, + OriginalComposedType = new CodeUnionType + { + Name = "UnionType", + }, + DiscriminatorInformation = new() + { + DiscriminatorPropertyName = "@odata.type", + }, + }).First(); + parentClass.AddProperty(new CodeProperty + { + Name = propName, + DefaultValue = defaultValue, + Kind = CodePropertyKind.Custom, + Type = new CodeType { TypeDefinition = unionType } + }); + var sType = new CodeType + { + Name = "String", + }; + var arrayType = new CodeType + { + Name = "array", + }; + unionType.OriginalComposedType.AddType(sType); + unionType.OriginalComposedType.AddType(arrayType); + + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name, result); + Assert.DoesNotContain(defaultValue, result);//ensure the composed type is not referenced + } + [Fact] + public void WritesRawUrlConstructor() + { + setup(); + method.Kind = CodeMethodKind.RawUrlConstructor; + var defaultValue = "\"someVal\""; + var propName = "propWithDefaultValue"; + parentClass.Kind = CodeClassKind.RequestBuilder; + parentClass.AddProperty(new CodeProperty + { + Name = propName, + DefaultValue = defaultValue, + Kind = CodePropertyKind.Custom, + Type = new CodeType + { + Name = "String" + } + }); + AddRequestProperties(); + method.AddParameter(new CodeParameter + { + Name = "rawUrl", + Kind = CodeParameterKind.RawUrl, + Type = new CodeType + { + Name = "String" + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name, result); + Assert.Contains($"{propName} = '{defaultValue.TrimQuotes()}'", result); + Assert.Contains("super", result); + } + [Fact] + public void WritesApiConstructor() + { + setup(); + method.Kind = CodeMethodKind.ClientConstructor; + method.BaseUrl = "https://graph.microsoft.com/v1.0"; + parentClass.AddProperty(new CodeProperty + { + Name = "pathParameters", + Kind = CodePropertyKind.PathParameters, + Type = new CodeType + { + Name = "Dictionary", + IsExternal = true, + } + }); + var coreProp = parentClass.AddProperty(new CodeProperty + { + Name = "core", + Kind = CodePropertyKind.RequestAdapter, + Type = new CodeType + { + Name = "HttpCore", + IsExternal = true, + } + }).First(); + method.AddParameter(new CodeParameter + { + Name = "core", + Kind = CodeParameterKind.RequestAdapter, + Type = coreProp.Type, + }); + method.DeserializerModules = new() { "com.microsoft.kiota.serialization.Deserializer" }; + method.SerializerModules = new() { "com.microsoft.kiota.serialization.Serializer" }; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name, result); + Assert.Contains("registerDefaultSerializer", result); + Assert.Contains("registerDefaultDeserializer", result); + Assert.Contains($"pathParameters['baseurl'] = core.baseUrl", result); + Assert.Contains($"core.baseUrl = '{method.BaseUrl}'", result); + } + [Fact] + public void WritesApiConstructorWithBackingStore() + { + setup(); + method.Kind = CodeMethodKind.ClientConstructor; + var coreProp = parentClass.AddProperty(new CodeProperty + { + Name = "core", + Kind = CodePropertyKind.RequestAdapter, + Type = new CodeType + { + Name = "HttpCore", + IsExternal = true, + } + }).First(); + method.AddParameter(new CodeParameter + { + Name = "core", + Kind = CodeParameterKind.RequestAdapter, + Type = coreProp.Type, + }); + var backingStoreParam = new CodeParameter + { + Name = "backingStore", + Kind = CodeParameterKind.BackingStore, + Type = new CodeType + { + Name = "BackingStore", + IsExternal = true, + } + }; + method.AddParameter(backingStoreParam); + var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.Dart, DefaultPath, DefaultName); + tempWriter.SetTextWriter(tw); + tempWriter.Write(method); + var result = tw.ToString(); + Assert.Contains("enableBackingStore", result); + } + [Fact] + public void DoesntWriteReadOnlyPropertiesInSerializerBody() + { + setup(true); + method.Kind = CodeMethodKind.Serializer; + AddSerializationProperties(); + parentClass.AddProperty(new CodeProperty + { + Name = "readOnlyProperty", + ReadOnly = true, + Type = new CodeType + { + Name = "String", + }, + }); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("readOnlyProperty", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesDeprecationInformation() + { + setup(); + method.Deprecation = new("This method is deprecated", DateTimeOffset.Parse("2020-01-01T00:00:00Z"), DateTimeOffset.Parse("2021-01-01T00:00:00Z"), "v2.0"); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("This method is deprecated", result); + Assert.Contains("2020-01-01", result); + Assert.Contains("2021-01-01", result); + Assert.Contains("v2.0", result); + Assert.Contains("@Deprecated", result); + } + [Fact] + public void WritesDeprecationInformationFromBuilder() + { + setup(); + var newMethod = method.Clone() as CodeMethod; + newMethod.Name = "NewAwesomeMethod";// new method replacement + method.Deprecation = new("This method is obsolete. Use NewAwesomeMethod instead.", IsDeprecated: true, TypeReferences: new() { { "TypeName", new CodeType { TypeDefinition = newMethod, IsExternal = false } } }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("This method is obsolete. Use NewAwesomeMethod instead.", result); + } + [Fact] + public void WritesRequestGeneratorAcceptHeaderQuotes() + { + setup(); + method.Kind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Get; + AddRequestProperties(); + method.AcceptedResponseTypes.Add("application/json; profile=\"CamelCase\""); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("requestInfo.headers.put('Accept', 'application/json; profile=\"CamelCase\"')", result); + } + + [Fact] + public void WritesRequestGeneratorContentTypeQuotes() + { + setup(); + method.Kind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Post; + AddRequestProperties(); + AddRequestBodyParameters(); + method.RequestBodyContentType = "application/json; profile=\"CamelCase\""; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("'application/json; profile=\"CamelCase\"'", result); + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/Dart/CodePropertyWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Dart/CodePropertyWriterTests.cs new file mode 100644 index 0000000000..a8c7e6223c --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/Dart/CodePropertyWriterTests.cs @@ -0,0 +1,134 @@ +using System; +using System.IO; +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Writers; + +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Dart; +public sealed class CodePropertyWriterTests : IDisposable +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeProperty property; + private readonly CodeClass parentClass; + private const string PropertyName = "propertyName"; + private const string TypeName = "Somecustomtype"; + public CodePropertyWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Dart, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + var root = CodeNamespace.InitRootNamespace(); + parentClass = new CodeClass + { + Name = "parentClass" + }; + root.AddClass(parentClass); + property = new CodeProperty + { + Name = PropertyName, + Type = new CodeType + { + Name = TypeName + } + }; + parentClass.AddProperty(property, new() + { + Name = "pathParameters", + Kind = CodePropertyKind.PathParameters, + Type = new CodeType + { + Name = "PathParameters", + }, + }, new() + { + Name = "requestAdapter", + Kind = CodePropertyKind.RequestAdapter, + Type = new CodeType + { + Name = "RequestAdapter", + }, + }); + } + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + [Fact] + public void WritesRequestBuilder() + { + property.Kind = CodePropertyKind.RequestBuilder; + writer.Write(property); + var result = tw.ToString(); + Assert.Contains($"return {TypeName}", result); + Assert.Contains("requestAdapter", result); + Assert.Contains("pathParameters", result); + } + [Fact] + public void WritesCustomProperty() + { + property.Kind = CodePropertyKind.Custom; + writer.Write(property); + var result = tw.ToString(); + Assert.Contains($"{TypeName}? {PropertyName}", result); + } + [Fact] + public void MapsCustomPropertiesToBackingStore() + { + parentClass.AddBackingStoreProperty(); + property.Kind = CodePropertyKind.Custom; + writer.Write(property); + var result = tw.ToString(); + Assert.Contains("return backingStore.get('propertyName');", result); + Assert.Contains("backingStore.set('propertyName', value);", result); + } + [Fact] + public void MapsAdditionalDataPropertiesToBackingStore() + { + parentClass.AddBackingStoreProperty(); + property.Kind = CodePropertyKind.AdditionalData; + writer.Write(property); + var result = tw.ToString(); + Assert.Contains("return backingStore.get('propertyName') ?? {};", result); + Assert.Contains("backingStore.set('propertyName', value);", result); + } + [Fact] + public void DoesntWritePropertiesExistingInParentType() + { + parentClass.AddProperty(new CodeProperty + { + Name = "definedInParent", + Type = new CodeType + { + Name = "string" + }, + Kind = CodePropertyKind.Custom, + }); + var subClass = (parentClass.Parent as CodeNamespace).AddClass(new CodeClass + { + Name = "BaseClass", + }).First(); + subClass.StartBlock.Inherits = new CodeType + { + Name = "BaseClass", + TypeDefinition = parentClass + }; + var propertyToWrite = subClass.AddProperty(new CodeProperty + { + Name = "definedInParent", + Type = new CodeType + { + Name = "string" + }, + Kind = CodePropertyKind.Custom, + }).First(); + writer.Write(propertyToWrite); + var result = tw.ToString(); + Assert.Empty(result); + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/Dart/DartWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Dart/DartWriterTests.cs new file mode 100644 index 0000000000..7eef46c8a7 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/Dart/DartWriterTests.cs @@ -0,0 +1,18 @@ +using System; + +using Kiota.Builder.Writers.Dart; + +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Dart; +public class DartWriterTests +{ + [Fact] + public void Instantiates() + { + var writer = new DartWriter("./", "graph"); + Assert.NotNull(writer.PathSegmenter); + Assert.Throws(() => new DartWriter(null, "graph")); + Assert.Throws(() => new DartWriter("./", null)); + } +} diff --git a/tests/Kiota.Tests/Kiota.Tests.csproj b/tests/Kiota.Tests/Kiota.Tests.csproj index b81a22368a..0f13a84124 100644 --- a/tests/Kiota.Tests/Kiota.Tests.csproj +++ b/tests/Kiota.Tests/Kiota.Tests.csproj @@ -12,8 +12,8 @@ all - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/vscode/microsoft-kiota/CHANGELOG.md b/vscode/microsoft-kiota/CHANGELOG.md index 94c3f08309..e1a6bf8573 100644 --- a/vscode/microsoft-kiota/CHANGELOG.md +++ b/vscode/microsoft-kiota/CHANGELOG.md @@ -8,6 +8,13 @@ All notable changes to this project will be documented in this file. ### Changed +## [1.22.100000001] - 2025-01-10 + +### Added + +### Changed +- Fixed a bug in the VS Code extension plugin generation [#5978](https://github.com/microsoft/kiota/issues/5978) + ## [1.20.000000001] - 2024-11-07 ## [1.19.100000001] - 2024-10-03 diff --git a/vscode/microsoft-kiota/package-lock.json b/vscode/microsoft-kiota/package-lock.json index d3a6ae335f..d687db7328 100644 --- a/vscode/microsoft-kiota/package-lock.json +++ b/vscode/microsoft-kiota/package-lock.json @@ -30,12 +30,12 @@ "@vscode/test-electron": "^2.4.1", "chai": "^5.1.2", "eslint": "^9.17.0", - "glob": "^11.0.0", + "glob": "^11.0.1", "mocha": "^11.0.1", "sinon": "^19.0.2", "ts-loader": "^9.5.1", "typemoq": "^2.1.0", - "typescript": "^5.7.2", + "typescript": "^5.7.3", "webpack": "^5.97.1", "webpack-cli": "^6.0.1" }, @@ -2812,11 +2812,10 @@ } }, "node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", "dev": true, - "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^4.0.1", @@ -5339,9 +5338,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/vscode/microsoft-kiota/package.json b/vscode/microsoft-kiota/package.json index 4680ed82bd..26f68e1a7f 100644 --- a/vscode/microsoft-kiota/package.json +++ b/vscode/microsoft-kiota/package.json @@ -504,12 +504,12 @@ "@vscode/test-electron": "^2.4.1", "chai": "^5.1.2", "eslint": "^9.17.0", - "glob": "^11.0.0", + "glob": "^11.0.1", "mocha": "^11.0.1", "sinon": "^19.0.2", "ts-loader": "^9.5.1", "typemoq": "^2.1.0", - "typescript": "^5.7.2", + "typescript": "^5.7.3", "webpack": "^5.97.1", "webpack-cli": "^6.0.1" }, @@ -534,6 +534,10 @@ "platformId": "linux-x64", "sha256": "730C0A5469D83DBC9CDC206C9A3837DBE5892A7A3F0F09AF5A1EAC4CED4B033E" }, + { + "platformId": "linux-arm64", + "sha256": "E26CE40838E9F5D87476D53893332CFA58E5E691622772569B7A526C292958D4" + }, { "platformId": "osx-x64", "sha256": "3446A580FFF5F8567E37E9E04D6E944A8B8E83F0B5584A5683527F501E6181FD" @@ -543,4 +547,4 @@ "sha256": "2525D4A8C3ED4972C05E57E4DB2D244E1A708325CE4A5CFB24822D9FD7BB7ED3" } ] -} +} \ No newline at end of file diff --git a/vscode/microsoft-kiota/src/commands/generate/generateClientCommand.ts b/vscode/microsoft-kiota/src/commands/generate/generateClientCommand.ts index e65cebaf85..849426bdf5 100644 --- a/vscode/microsoft-kiota/src/commands/generate/generateClientCommand.ts +++ b/vscode/microsoft-kiota/src/commands/generate/generateClientCommand.ts @@ -201,7 +201,7 @@ export class GenerateClientCommand extends Command { settings.cleanOutput, settings.disableValidationRules, ConsumerOperation.Add, - undefined, + null, '', config.workingDirectory ); @@ -247,7 +247,7 @@ export class GenerateClientCommand extends Command { settings.cleanOutput, settings.disableValidationRules, ConsumerOperation.Add, - undefined, + null, '', config.workingDirectory ); diff --git a/vscode/microsoft-kiota/src/commands/generate/generation-util.ts b/vscode/microsoft-kiota/src/commands/generate/generation-util.ts index c372cb916f..ec5dcfc1d8 100644 --- a/vscode/microsoft-kiota/src/commands/generate/generation-util.ts +++ b/vscode/microsoft-kiota/src/commands/generate/generation-util.ts @@ -1,17 +1,20 @@ import * as vscode from "vscode"; +import * as fs from 'fs'; import { KIOTA_WORKSPACE_FILE, treeViewId } from "../../constants"; import { OpenApiTreeProvider } from "../../providers/openApiTreeProvider"; -import { updateTreeViewIcons } from "../../util"; +import { getWorkspaceJsonPath, updateTreeViewIcons } from "../../util"; export async function displayGenerationResults(openApiTreeProvider: OpenApiTreeProvider, config: any) { const clientNameOrPluginName = config.clientClassName || config.pluginName; const workspaceJson = vscode.workspace.textDocuments.find(doc => doc.fileName.endsWith(KIOTA_WORKSPACE_FILE)); if (workspaceJson) { - const content = workspaceJson.getText(); + const content = await fs.promises.readFile(getWorkspaceJsonPath(), 'utf-8'); const workspace = JSON.parse(content); const clientOrPluginObject = workspace.plugins[clientNameOrPluginName] || workspace.clients[clientNameOrPluginName]; - await openApiTreeProvider.loadEditPaths(clientNameOrPluginName, clientOrPluginObject); + if (clientOrPluginObject) { + await openApiTreeProvider.loadEditPaths(clientNameOrPluginName, clientOrPluginObject); + } } openApiTreeProvider.resetInitialState(); await updateTreeViewIcons(treeViewId, false, true);