diff --git a/.chronus/changes/ReleaseDashboardsStep1-2024-11-5-14-40-39.md b/.chronus/changes/ReleaseDashboardsStep1-2024-11-5-14-40-39.md new file mode 100644 index 0000000000..e3e74262ca --- /dev/null +++ b/.chronus/changes/ReleaseDashboardsStep1-2024-11-5-14-40-39.md @@ -0,0 +1,8 @@ +--- +changeKind: internal +packages: + - "@typespec/http-specs" + - "@typespec/spector" +--- + +Adding scripts to package.json \ No newline at end of file diff --git a/.chronus/changes/nullable-2024-11-6-1-23-27.md b/.chronus/changes/nullable-2024-11-6-1-23-27.md new file mode 100644 index 0000000000..5aa6b8945c --- /dev/null +++ b/.chronus/changes/nullable-2024-11-6-1-23-27.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/http-server-csharp" +--- + +Fix nullable types, anonymous types, and safeInt diff --git a/.chronus/changes/synced-with-versioning-removed-in-cadl-ranch-2024-10-28-15-49-29.md b/.chronus/changes/synced-with-versioning-removed-in-cadl-ranch-2024-10-28-15-49-29.md new file mode 100644 index 0000000000..3704e00677 --- /dev/null +++ b/.chronus/changes/synced-with-versioning-removed-in-cadl-ranch-2024-10-28-15-49-29.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-specs" +--- + +update code in versioning/removed and removed type/model/templated. diff --git a/eng/common/config/labels.ts b/eng/common/config/labels.ts index cb9c284ec0..ce4ec6b35d 100644 --- a/eng/common/config/labels.ts +++ b/eng/common/config/labels.ts @@ -194,6 +194,10 @@ export default defineConfig({ misc: { description: "Misc labels", labels: { + "1_0_E2E": { + color: "5319E7", + description: "", + }, "Client Emitter Migration": { color: "FD92F0", description: "", diff --git a/package.json b/package.json index 786d4a21ec..adb2207254 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,11 @@ "test:e2e": "pnpm -r run test:e2e", "update-latest-docs": "pnpm -r run update-latest-docs", "watch": "tsc --build ./tsconfig.ws.json --watch", - "sync-labels": "tsx ./eng/common/scripts/labels/sync-labels.ts --config ./eng/common/config/labels.ts" + "sync-labels": "tsx ./eng/common/scripts/labels/sync-labels.ts --config ./eng/common/config/labels.ts", + "validate-scenarios": "pnpm -r --filter=@typespec/http-specs run validate-scenarios", + "validate-mock-apis": "pnpm -r --filter=@typespec/http-specs run validate-mock-apis", + "generate-scenarios-summary": "pnpm -r --filter=@typespec/http-specs run generate-scenarios-summary", + "upload-manifest": "pnpm -r --filter=@typespec/http-specs run upload-manifest" }, "devDependencies": { "@chronus/chronus": "^0.13.0", diff --git a/packages/http-client-csharp/eng/scripts/Generate.ps1 b/packages/http-client-csharp/eng/scripts/Generate.ps1 index ee44432ff6..05134142c8 100644 --- a/packages/http-client-csharp/eng/scripts/Generate.ps1 +++ b/packages/http-client-csharp/eng/scripts/Generate.ps1 @@ -101,6 +101,14 @@ foreach ($directory in $directories) { continue } + # srv-driven contains two separate specs, for two separate clients. We need to generate both. + if ($folders.Contains("srv-driven")) { + Generate-Srv-Driven $directory.FullName $generationDir -generateStub $stubbed + $cadlRanchLaunchProjects.Add($($folders -join "-") + "-v1", $("TestProjects/CadlRanch/$($subPath.Replace([System.IO.Path]::DirectorySeparatorChar, '/'))") + "/v1") + $cadlRanchLaunchProjects.Add($($folders -join "-") + "-v2", $("TestProjects/CadlRanch/$($subPath.Replace([System.IO.Path]::DirectorySeparatorChar, '/'))") + "/v2") + continue + } + $cadlRanchLaunchProjects.Add(($folders -join "-"), ("TestProjects/CadlRanch/$($subPath.Replace([System.IO.Path]::DirectorySeparatorChar, '/'))")) if ($LaunchOnly) { continue @@ -114,11 +122,6 @@ foreach ($directory in $directories) { exit $LASTEXITCODE } - # srv-driven contains two separate specs, for two separate clients. We need to generate both. - if ($folders.Contains("srv-driven")) { - Generate-Srv-Driven $directory.FullName $generationDir -generateStub $stubbed - } - # TODO need to build but depends on https://github.com/Azure/autorest.csharp/issues/4463 } diff --git a/packages/http-client-csharp/eng/scripts/Generation.psm1 b/packages/http-client-csharp/eng/scripts/Generation.psm1 index a2c6af5274..cd10f88dc3 100644 --- a/packages/http-client-csharp/eng/scripts/Generation.psm1 +++ b/packages/http-client-csharp/eng/scripts/Generation.psm1 @@ -91,14 +91,27 @@ function Generate-Srv-Driven { [bool]$createOutputDirIfNotExist = $true ) - $specFilePath = $(Join-Path $specFilePath "old.tsp") - $outputDir = $(Join-Path $outputDir "v1") - if ($createOutputDirIfNotExist -and -not (Test-Path $outputDir)) { - New-Item -ItemType Directory -Path $outputDir | Out-Null + $v1Dir = $(Join-Path $outputDir "v1") + if ($createOutputDirIfNotExist -and -not (Test-Path $v1Dir)) { + New-Item -ItemType Directory -Path $v1Dir | Out-Null } - Write-Host "Generating http\resiliency\srv-driven\v1" -ForegroundColor Cyan - Invoke (Get-TspCommand $specFilePath $outputDir -generateStub $generateStub -namespaceOverride "Resiliency.ServiceDriven.V1") + $v2Dir = $(Join-Path $outputDir "v2") + if ($createOutputDirIfNotExist -and -not (Test-Path $v2Dir)) { + New-Item -ItemType Directory -Path $v2Dir | Out-Null + } + + ## get the last two directories of the output directory and add V1/V2 to disambiguate the namespaces + $namespaceRoot = $(($outputDir.Split([System.IO.Path]::DirectorySeparatorChar)[-2..-1] | ` + ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1) }) -replace '-(\p{L})', { $_.Groups[1].Value.ToUpper() } -replace '\W', '' -join ".") + $v1NamespaceOverride = $namespaceRoot + ".V1" + $v2NamespaceOverride = $namespaceRoot + ".V2" + + $v1SpecFilePath = $(Join-Path $specFilePath "old.tsp") + $v2SpecFilePath = $(Join-Path $specFilePath "main.tsp") + + Invoke (Get-TspCommand $v1SpecFilePath $v1Dir -generateStub $generateStub -namespaceOverride $v1NamespaceOverride) + Invoke (Get-TspCommand $v2SpecFilePath $v2Dir -generateStub $generateStub -namespaceOverride $v2NamespaceOverride) # exit if the generation failed if ($LASTEXITCODE -ne 0) { diff --git a/packages/http-client-csharp/eng/scripts/Get-CadlRanch-Coverage.ps1 b/packages/http-client-csharp/eng/scripts/Get-CadlRanch-Coverage.ps1 index da844ced98..ff668da5aa 100644 --- a/packages/http-client-csharp/eng/scripts/Get-CadlRanch-Coverage.ps1 +++ b/packages/http-client-csharp/eng/scripts/Get-CadlRanch-Coverage.ps1 @@ -46,17 +46,20 @@ foreach ($directory in $directories) { continue } + if ($subPath.Contains("srv-driven")) { + if ($subPath.Contains("v1")) { + # this will generate v1 and v2 so we only need to call it once for one of the versions + Generate-Srv-Driven ($(Join-Path $specsDirectory $subPath) | Split-Path) $($outputDir | Split-Path) -createOutputDirIfNotExist $false + } + continue + } + $command = Get-TspCommand $specFile $outputDir Invoke $command # exit if the generation failed if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - # srv-driven contains two separate specs, for two separate clients. We need to generate both. - if ($subPath.Contains('srv-driven')) { - Generate-Srv-Driven $(Join-Path $specsDirectory $subPath) $outputDir -createOutputDirIfNotExist $false - } } # test all diff --git a/packages/http-client-csharp/eng/scripts/Test-CadlRanch.ps1 b/packages/http-client-csharp/eng/scripts/Test-CadlRanch.ps1 index 65266e5ac8..698c0537df 100644 --- a/packages/http-client-csharp/eng/scripts/Test-CadlRanch.ps1 +++ b/packages/http-client-csharp/eng/scripts/Test-CadlRanch.ps1 @@ -32,10 +32,6 @@ foreach ($directory in $directories) { if (-not (Compare-Paths $subPath $filter)) { continue } - - if ($subPath.Contains($(Join-Path 'srv-driven' 'v1'))) { - continue - } $testPath = "$cadlRanchRoot.Tests" $testFilter = "TestProjects.CadlRanch.Tests" @@ -67,21 +63,21 @@ foreach ($directory in $directories) { Generate-Versioning ($(Join-Path $specsDirectory $subPath) | Split-Path) $($outputDir | Split-Path) -createOutputDirIfNotExist $false } } + elseif ($subPath.Contains("srv-driven")) { + if ($subPath.Contains("v1")) { + Generate-Srv-Driven ($(Join-Path $specsDirectory $subPath) | Split-Path) $($outputDir | Split-Path) -createOutputDirIfNotExist $false + } + } else { $command = Get-TspCommand $specFile $outputDir Invoke $command } - + # exit if the generation failed if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - # srv-driven contains two separate specs, for two separate clients. We need to generate both. - if ($subPath.Contains("srv-driven")) { - Generate-Srv-Driven $(Join-Path $specsDirectory $subPath) $outputDir -createOutputDirIfNotExist $false - } - Write-Host "Testing $subPath" -ForegroundColor Cyan $command = "dotnet test $cadlRanchCsproj --filter `"FullyQualifiedName~$testFilter`"" Invoke $command diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Properties/launchSettings.json b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Properties/launchSettings.json index a8e2843798..f2d5ebb72d 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Properties/launchSettings.json +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Properties/launchSettings.json @@ -120,8 +120,13 @@ "commandName": "Executable", "executablePath": "$(SolutionDir)/../dist/generator/Microsoft.Generator.CSharp.exe" }, - "http-resiliency-srv-driven": { - "commandLineArgs": "$(SolutionDir)/TestProjects/CadlRanch/http/resiliency/srv-driven -p StubLibraryPlugin", + "http-resiliency-srv-driven-v1": { + "commandLineArgs": "$(SolutionDir)/TestProjects/CadlRanch/http/resiliency/srv-driven/v1 -p StubLibraryPlugin", + "commandName": "Executable", + "executablePath": "$(SolutionDir)/../dist/generator/Microsoft.Generator.CSharp.exe" + }, + "http-resiliency-srv-driven-v2": { + "commandLineArgs": "$(SolutionDir)/TestProjects/CadlRanch/http/resiliency/srv-driven/v2 -p StubLibraryPlugin", "commandName": "Executable", "executablePath": "$(SolutionDir)/../dist/generator/Microsoft.Generator.CSharp.exe" }, diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/SrvDrivenTests.V1.cs b/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/V1/SrvDrivenV1Tests.cs similarity index 94% rename from packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/SrvDrivenTests.V1.cs rename to packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/V1/SrvDrivenV1Tests.cs index 02f91bb8ba..dd321081b3 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/SrvDrivenTests.V1.cs +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/V1/SrvDrivenV1Tests.cs @@ -2,16 +2,19 @@ // Licensed under the MIT License. using NUnit.Framework; -using Resiliency.ServiceDriven.V1; +using Resiliency.SrvDriven.V1; using System.Threading.Tasks; -namespace TestProjects.CadlRanch.Tests.Http.Resiliency.SrvDriven +namespace TestProjects.CadlRanch.Tests.Http.Resiliency.SrvDriven.V1 { /// /// Contains tests for the service-driven resiliency V1 client. /// - public partial class SrvDrivenTests : CadlRanchTestBase + public partial class SrvDrivenV2Tests : CadlRanchTestBase { + private const string ServiceDeploymentV1 = "v1"; + private const string ServiceDeploymentV2 = "v2"; + // This test validates the v1 client behavior when both the service deployment and api version are set to V1. [CadlRanchTest] public Task AddOptionalParamFromNone_V1Client_V1Service_WithApiVersionV1() => Test(async (host) => diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/SrvDrivenTests.cs b/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/V2/SrvDrivenV2Tests.cs similarity index 96% rename from packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/SrvDrivenTests.cs rename to packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/V2/SrvDrivenV2Tests.cs index ca6a493224..cc2dfb9985 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/SrvDrivenTests.cs +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Resiliency/SrvDriven/V2/SrvDrivenV2Tests.cs @@ -3,13 +3,12 @@ using NUnit.Framework; using System.Threading.Tasks; -using Resiliency.ServiceDriven; +using Resiliency.SrvDriven.V2; -namespace TestProjects.CadlRanch.Tests.Http.Resiliency.SrvDriven +namespace TestProjects.CadlRanch.Tests.Http.Resiliency.SrvDriven.V2 { - public partial class SrvDrivenTests : CadlRanchTestBase + public partial class SrvDrivenV2Tests : CadlRanchTestBase { - private const string ServiceDeploymentV1 = "v1"; private const string ServiceDeploymentV2 = "v2"; [CadlRanchTest] diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Versioning/Removed/V1/VersioningRemovedV1Tests.cs b/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Versioning/Removed/V1/VersioningRemovedV1Tests.cs index 3fdf09cc9e..d96e2a4292 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Versioning/Removed/V1/VersioningRemovedV1Tests.cs +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Versioning/Removed/V1/VersioningRemovedV1Tests.cs @@ -3,8 +3,10 @@ using System; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using Versioning.Removed.V1; +using Versioning.Removed.V1.Models; namespace TestProjects.CadlRanch.Tests.Http.Versioning.Removed.V1 { @@ -39,5 +41,15 @@ public void TestRemovedMembers() var enumType = typeof(RemovedClientOptions.ServiceVersion); Assert.AreEqual(new string[] { "V1" }, enumType.GetEnumNames()); } + + [CadlRanchTest] + public Task Versioning_Removed_V3Model() => Test(async (host) => + { + var model = new ModelV3("123", EnumV3.EnumMemberV1); + var response = await new RemovedClient(host).ModelV3Async(model); + Assert.AreEqual(200, response.GetRawResponse().Status); + Assert.AreEqual("123", response.Value.Id); + Assert.AreEqual(EnumV3.EnumMemberV1, response.Value.EnumProp); + }); } } diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Infrastructure/CadlRanchTestAttribute.cs b/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Infrastructure/CadlRanchTestAttribute.cs index 32688b3275..29897ff980 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Infrastructure/CadlRanchTestAttribute.cs +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Infrastructure/CadlRanchTestAttribute.cs @@ -24,6 +24,12 @@ internal partial class CadlRanchTestAttribute : TestAttribute, IApplyToTest { string clientCodeDirectory = GetGeneratedDirectory(test); + if (!Directory.Exists(clientCodeDirectory)) + { + // Not all cadl-ranch scenarios use kebab-case directories, so try again without kebab-case. + clientCodeDirectory = GetGeneratedDirectory(test, false); + } + var clientCsFile = GetClientCsFile(clientCodeDirectory); TestContext.Progress.WriteLine($"Checking if '{clientCsFile}' is a stubbed implementation."); @@ -69,21 +75,26 @@ private static void SkipTest(Test test) .FirstOrDefault(); } - private static string GetGeneratedDirectory(Test test) + private static string GetGeneratedDirectory(Test test, bool kebabCaseDirectories = true) { var namespaceParts = test.FullName.Split('.').Skip(3); namespaceParts = namespaceParts.Take(namespaceParts.Count() - 2); var clientCodeDirectory = Path.Combine(TestContext.CurrentContext.TestDirectory, "..", "..", "..", "..", "..", "TestProjects", "CadlRanch"); foreach (var part in namespaceParts) { - clientCodeDirectory = Path.Combine(clientCodeDirectory, FixName(part)); + clientCodeDirectory = Path.Combine(clientCodeDirectory, FixName(part, kebabCaseDirectories)); } return Path.Combine(clientCodeDirectory, "src", "Generated"); } - private static string FixName(string part) + private static string FixName(string part, bool kebabCaseDirectories) { - return ToKebabCase().Replace(part.StartsWith("_", StringComparison.Ordinal) ? part.Substring(1) : part, "-$1").ToLower(); + if (kebabCaseDirectories) + { + return ToKebabCase().Replace(part.StartsWith("_", StringComparison.Ordinal) ? part.Substring(1) : part, "-$1").ToLowerInvariant(); + } + // Use camelCase + return char.ToLowerInvariant(part[0]) + part[1..]; } } } diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/Configuration.json b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/Configuration.json deleted file mode 100644 index 03f7a6232d..0000000000 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/Configuration.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "output-folder": ".", - "namespace": "Resiliency.ServiceDriven", - "library-name": "Resiliency.ServiceDriven", - "use-model-reader-writer": true -} diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/Configuration.json b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/Configuration.json index 23dcd7aba7..43d151251e 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/Configuration.json +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/Configuration.json @@ -1,6 +1,6 @@ { "output-folder": ".", - "namespace": "Resiliency.ServiceDriven.V1", - "library-name": "Resiliency.ServiceDriven.V1", + "namespace": "Resiliency.SrvDriven.V1", + "library-name": "Resiliency.SrvDriven.V1", "use-model-reader-writer": true } diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/Resiliency.ServiceDriven.sln b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/Resiliency.SrvDriven.V1.sln similarity index 96% rename from packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/Resiliency.ServiceDriven.sln rename to packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/Resiliency.SrvDriven.V1.sln index 1022c33783..92889ce27d 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/Resiliency.ServiceDriven.sln +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/Resiliency.SrvDriven.V1.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29709.97 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Resiliency.ServiceDriven", "src\Resiliency.ServiceDriven.csproj", "{28FF4005-4467-4E36-92E7-DEA27DEB1519}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Resiliency.SrvDriven.V1", "src\Resiliency.SrvDriven.V1.csproj", "{28FF4005-4467-4E36-92E7-DEA27DEB1519}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Generated/ResiliencyServiceDrivenClient.cs b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Generated/ResiliencyServiceDrivenClient.cs index 857cc0ef51..8c25a81940 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Generated/ResiliencyServiceDrivenClient.cs +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Generated/ResiliencyServiceDrivenClient.cs @@ -8,7 +8,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Resiliency.ServiceDriven.V1 +namespace Resiliency.SrvDriven.V1 { public partial class ResiliencyServiceDrivenClient { diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Generated/ResiliencyServiceDrivenClientOptions.cs b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Generated/ResiliencyServiceDrivenClientOptions.cs index 9c387842e9..adf5be0b0f 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Generated/ResiliencyServiceDrivenClientOptions.cs +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Generated/ResiliencyServiceDrivenClientOptions.cs @@ -4,7 +4,7 @@ using System.ClientModel.Primitives; -namespace Resiliency.ServiceDriven.V1 +namespace Resiliency.SrvDriven.V1 { public partial class ResiliencyServiceDrivenClientOptions : ClientPipelineOptions { diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/src/Resiliency.ServiceDriven.csproj b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Resiliency.SrvDriven.V1.csproj similarity index 62% rename from packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/src/Resiliency.ServiceDriven.csproj rename to packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Resiliency.SrvDriven.V1.csproj index a8a84fc716..5f5ff2e5e4 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/src/Resiliency.ServiceDriven.csproj +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Resiliency.SrvDriven.V1.csproj @@ -1,9 +1,9 @@ - This is the Resiliency.ServiceDriven client library for developing .NET applications with rich experience. - SDK Code Generation Resiliency.ServiceDriven + This is the Resiliency.SrvDriven.V1 client library for developing .NET applications with rich experience. + SDK Code Generation Resiliency.SrvDriven.V1 1.0.0-beta.1 - Resiliency.ServiceDriven + Resiliency.SrvDriven.V1 netstandard2.0 latest true diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/Configuration.json b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/Configuration.json new file mode 100644 index 0000000000..b479794aef --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/Configuration.json @@ -0,0 +1,6 @@ +{ + "output-folder": ".", + "namespace": "Resiliency.SrvDriven.V2", + "library-name": "Resiliency.SrvDriven.V2", + "use-model-reader-writer": true +} diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/Resiliency.ServiceDriven.V1.sln b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/Resiliency.SrvDriven.V2.sln similarity index 96% rename from packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/Resiliency.ServiceDriven.V1.sln rename to packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/Resiliency.SrvDriven.V2.sln index 2507d0b1ce..47ec60440c 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/Resiliency.ServiceDriven.V1.sln +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/Resiliency.SrvDriven.V2.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29709.97 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Resiliency.ServiceDriven.V1", "src\Resiliency.ServiceDriven.V1.csproj", "{28FF4005-4467-4E36-92E7-DEA27DEB1519}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Resiliency.SrvDriven.V2", "src\Resiliency.SrvDriven.V2.csproj", "{28FF4005-4467-4E36-92E7-DEA27DEB1519}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/src/Generated/ResiliencyServiceDrivenClient.cs b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/src/Generated/ResiliencyServiceDrivenClient.cs similarity index 98% rename from packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/src/Generated/ResiliencyServiceDrivenClient.cs rename to packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/src/Generated/ResiliencyServiceDrivenClient.cs index ab38b9c6be..6cc3562d37 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/src/Generated/ResiliencyServiceDrivenClient.cs +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/src/Generated/ResiliencyServiceDrivenClient.cs @@ -8,7 +8,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Resiliency.ServiceDriven +namespace Resiliency.SrvDriven.V2 { public partial class ResiliencyServiceDrivenClient { diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/src/Generated/ResiliencyServiceDrivenClientOptions.cs b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/src/Generated/ResiliencyServiceDrivenClientOptions.cs similarity index 94% rename from packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/src/Generated/ResiliencyServiceDrivenClientOptions.cs rename to packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/src/Generated/ResiliencyServiceDrivenClientOptions.cs index 233945b82e..11ababcb63 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/src/Generated/ResiliencyServiceDrivenClientOptions.cs +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/src/Generated/ResiliencyServiceDrivenClientOptions.cs @@ -4,7 +4,7 @@ using System.ClientModel.Primitives; -namespace Resiliency.ServiceDriven +namespace Resiliency.SrvDriven.V2 { public partial class ResiliencyServiceDrivenClientOptions : ClientPipelineOptions { diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Resiliency.ServiceDriven.V1.csproj b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/src/Resiliency.SrvDriven.V2.csproj similarity index 61% rename from packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Resiliency.ServiceDriven.V1.csproj rename to packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/src/Resiliency.SrvDriven.V2.csproj index fa7663cb74..0cf65023a6 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v1/src/Resiliency.ServiceDriven.V1.csproj +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/src/Resiliency.SrvDriven.V2.csproj @@ -1,9 +1,9 @@ - This is the Resiliency.ServiceDriven.V1 client library for developing .NET applications with rich experience. - SDK Code Generation Resiliency.ServiceDriven.V1 + This is the Resiliency.SrvDriven.V2 client library for developing .NET applications with rich experience. + SDK Code Generation Resiliency.SrvDriven.V2 1.0.0-beta.1 - Resiliency.ServiceDriven.V1 + Resiliency.SrvDriven.V2 netstandard2.0 latest true diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/tspCodeModel.json b/packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/tspCodeModel.json similarity index 100% rename from packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/tspCodeModel.json rename to packages/http-client-csharp/generator/TestProjects/CadlRanch/http/resiliency/srv-driven/v2/tspCodeModel.json diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/EnumTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/EnumTemplate.java index d7fbac8ba4..f75dd15152 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/EnumTemplate.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/EnumTemplate.java @@ -67,6 +67,7 @@ protected void writeBrandedExpandableEnum(EnumType enumType, JavaFile javaFile, imports.add("java.util.ArrayList"); imports.add("java.util.Objects"); imports.add(ClassType.EXPANDABLE_ENUM.getFullName()); + imports.add("java.util.function.Function"); if (!settings.isStreamStyleSerialization()) { imports.add("com.fasterxml.jackson.annotation.JsonCreator"); } @@ -84,6 +85,8 @@ protected void writeBrandedExpandableEnum(EnumType enumType, JavaFile javaFile, javaFile.publicFinalClass(declaration, classBlock -> { classBlock.privateStaticFinalVariable( String.format("Map<%1$s, %2$s> VALUES = new ConcurrentHashMap<>()", pascalTypeName, enumName)); + classBlock.privateStaticFinalVariable( + String.format("Function<%1$s, %2$s> NEW_INSTANCE = %2$s::new", pascalTypeName, enumName)); for (ClientEnumValue enumValue : enumType.getValues()) { String value = enumValue.getValue(); @@ -115,9 +118,7 @@ protected void writeBrandedExpandableEnum(EnumType enumType, JavaFile javaFile, classBlock.publicStaticMethod(String.format("%1$s fromValue(%2$s value)", enumName, pascalTypeName), function -> { function.line("Objects.requireNonNull(value, \"'value' cannot be null.\");"); - function.line(enumName + " member = VALUES.get(value);"); - function.ifBlock("member != null", ifAction -> ifAction.line("return member;")); - function.methodReturn("VALUES.computeIfAbsent(value, key -> new " + enumName + "(key))"); + function.methodReturn("VALUES.computeIfAbsent(value, NEW_INSTANCE)"); }); // values @@ -150,7 +151,7 @@ protected void writeBrandedExpandableEnum(EnumType enumType, JavaFile javaFile, addGeneratedAnnotation(classBlock); classBlock.annotation("Override"); classBlock.method(JavaVisibility.Public, null, "boolean equals(Object obj)", - function -> function.methodReturn("Objects.equals(this.value, obj)")); + function -> function.methodReturn("this == obj")); // hashcode addGeneratedAnnotation(classBlock); diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/armresourceprovider/models/PriorityModel.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/armresourceprovider/models/PriorityModel.java index 80db9412da..33536bff25 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/armresourceprovider/models/PriorityModel.java +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/armresourceprovider/models/PriorityModel.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; /** * Defines values for PriorityModel. @@ -18,6 +19,8 @@ public final class PriorityModel implements ExpandableEnum { private static final Map VALUES = new ConcurrentHashMap<>(); + private static final Function NEW_INSTANCE = PriorityModel::new; + /** * Static value 0 for PriorityModel. */ @@ -43,11 +46,7 @@ private PriorityModel(Integer value) { @JsonCreator public static PriorityModel fromValue(Integer value) { Objects.requireNonNull(value, "'value' cannot be null."); - PriorityModel member = VALUES.get(value); - if (member != null) { - return member; - } - return VALUES.computeIfAbsent(value, key -> new PriorityModel(key)); + return VALUES.computeIfAbsent(value, NEW_INSTANCE); } /** @@ -76,7 +75,7 @@ public String toString() { @Override public boolean equals(Object obj) { - return Objects.equals(this.value, obj); + return this == obj; } @Override diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/enumservice/models/OlympicRecordModel.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/enumservice/models/OlympicRecordModel.java index dce6221287..c46f716d2b 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/enumservice/models/OlympicRecordModel.java +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/enumservice/models/OlympicRecordModel.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; /** * Defines values for OlympicRecordModel. @@ -18,6 +19,8 @@ public final class OlympicRecordModel implements ExpandableEnum { private static final Map VALUES = new ConcurrentHashMap<>(); + private static final Function NEW_INSTANCE = OlympicRecordModel::new; + /** * Static value 9.58 for OlympicRecordModel. */ @@ -45,11 +48,7 @@ private OlympicRecordModel(Double value) { @Generated public static OlympicRecordModel fromValue(Double value) { Objects.requireNonNull(value, "'value' cannot be null."); - OlympicRecordModel member = VALUES.get(value); - if (member != null) { - return member; - } - return VALUES.computeIfAbsent(value, key -> new OlympicRecordModel(key)); + return VALUES.computeIfAbsent(value, NEW_INSTANCE); } /** @@ -82,7 +81,7 @@ public String toString() { @Generated @Override public boolean equals(Object obj) { - return Objects.equals(this.value, obj); + return this == obj; } @Generated diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/enumservice/models/PriorityModel.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/enumservice/models/PriorityModel.java index 99432a3cf0..43b1221128 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/enumservice/models/PriorityModel.java +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/enumservice/models/PriorityModel.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; /** * Defines values for PriorityModel. @@ -18,6 +19,8 @@ public final class PriorityModel implements ExpandableEnum { private static final Map VALUES = new ConcurrentHashMap<>(); + private static final Function NEW_INSTANCE = PriorityModel::new; + /** * Static value 100 for PriorityModel. */ @@ -45,11 +48,7 @@ private PriorityModel(Integer value) { @Generated public static PriorityModel fromValue(Integer value) { Objects.requireNonNull(value, "'value' cannot be null."); - PriorityModel member = VALUES.get(value); - if (member != null) { - return member; - } - return VALUES.computeIfAbsent(value, key -> new PriorityModel(key)); + return VALUES.computeIfAbsent(value, NEW_INSTANCE); } /** @@ -82,7 +81,7 @@ public String toString() { @Generated @Override public boolean equals(Object obj) { - return Objects.equals(this.value, obj); + return this == obj; } @Generated diff --git a/packages/http-client-java/generator/http-client-generator-test/src/test/java/tsptest/enumservice/EnumTests.java b/packages/http-client-java/generator/http-client-generator-test/src/test/java/tsptest/enumservice/EnumTests.java index 05bf2aa4df..fd3ba7b708 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/test/java/tsptest/enumservice/EnumTests.java +++ b/packages/http-client-java/generator/http-client-generator-test/src/test/java/tsptest/enumservice/EnumTests.java @@ -23,6 +23,7 @@ import tsptest.enumservice.implementation.EnumServiceClientImpl; import tsptest.enumservice.models.ColorModel; import tsptest.enumservice.models.Priority; +import tsptest.enumservice.models.PriorityModel; public class EnumTests { @@ -129,6 +130,15 @@ public void testStringArrayAsMulti() throws Exception { Assertions.assertEquals("colorArrayOpt=Green&colorArrayOpt=Red", request.getUrl().getQuery()); } + @Test + public void testExpandableEnum() { + Assertions.assertEquals(PriorityModel.HIGH, PriorityModel.fromValue(100)); + Assertions.assertNotEquals(PriorityModel.HIGH, PriorityModel.LOW); + Assertions.assertNotEquals(PriorityModel.HIGH, PriorityModel.fromValue(200)); + + Assertions.assertEquals(100, PriorityModel.HIGH.getValue()); + } + private static void verifyQuery(String query, String key, String value) { Assertions.assertEquals( URLEncoder.encode(key, StandardCharsets.UTF_8) + "=" + URLEncoder.encode(value, StandardCharsets.UTF_8), diff --git a/packages/http-client-python/CHANGELOG.md b/packages/http-client-python/CHANGELOG.md index 8248785288..b143c2cff5 100644 --- a/packages/http-client-python/CHANGELOG.md +++ b/packages/http-client-python/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log - @typespec/http-client-python +## 0.4.1 + +### Bug Fixes + +- Ignore models only used as LRO envelope results because we don't do anything with them + +## 0.4.0 + +### Features + +- Refine exception handling logic and support exception with ranged status code (#5270) + +### Bug Fixes + +- Filter out credential that python does not support for now (#5282) + ## 0.3.12 ### Other Changes diff --git a/packages/http-client-python/emitter/src/code-model.ts b/packages/http-client-python/emitter/src/code-model.ts index 0199c7de21..3afb945f30 100644 --- a/packages/http-client-python/emitter/src/code-model.ts +++ b/packages/http-client-python/emitter/src/code-model.ts @@ -269,6 +269,9 @@ export function emitCodeModel( } // loop through models and enums since there may be some orphaned models needs to be generated for (const model of sdkPackage.models) { + if (isAzureCoreModel(model)) { + continue; + } // filter out spread models if ( model.name === "" || @@ -278,6 +281,16 @@ export function emitCodeModel( ) { continue; } + // filter out models only used for polling and or envelope result + if ( + ((model.usage & UsageFlags.LroInitial) > 0 || + (model.usage & UsageFlags.LroFinalEnvelope) > 0 || + (model.usage & UsageFlags.LroPolling) > 0) && + (model.usage & UsageFlags.Input) === 0 && + (model.usage & UsageFlags.Output) === 0 + ) { + continue; + } // filter out specific models not used in python, e.g., pageable models if (disableGenerationMap.has(model)) { continue; @@ -289,6 +302,9 @@ export function emitCodeModel( getType(sdkContext, model); } for (const sdkEnum of sdkPackage.enums) { + if (isAzureCoreModel(sdkEnum)) { + continue; + } // filter out api version enum since python do not generate it if (sdkEnum.usage === UsageFlags.ApiVersionEnum) { continue; diff --git a/packages/http-client-python/emitter/src/http.ts b/packages/http-client-python/emitter/src/http.ts index ad62dd4082..cd2ae43904 100644 --- a/packages/http-client-python/emitter/src/http.ts +++ b/packages/http-client-python/emitter/src/http.ts @@ -379,7 +379,7 @@ function emitHttpResponse( headers: response.headers.map((x) => emitHttpResponseHeader(context, x)), statusCodes: typeof statusCodes === "object" - ? [(statusCodes as HttpStatusCodeRange).start] + ? [[(statusCodes as HttpStatusCodeRange).start, (statusCodes as HttpStatusCodeRange).end]] : statusCodes === "*" ? ["default"] : [statusCodes], diff --git a/packages/http-client-python/generator/pygen/codegen/__init__.py b/packages/http-client-python/generator/pygen/codegen/__init__.py index 11d829263c..1ab9bd6237 100644 --- a/packages/http-client-python/generator/pygen/codegen/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/__init__.py @@ -241,6 +241,16 @@ def _validate_code_model_options(self) -> None: if not self.options_retriever.is_azure_flavor and self.options_retriever.tracing: raise ValueError("Can only have tracing turned on for Azure SDKs.") + @staticmethod + def sort_exceptions(yaml_data: Dict[str, Any]) -> None: + for client in yaml_data["clients"]: + for group in client["operationGroups"]: + for operation in group["operations"]: + if not operation.get("exceptions"): + continue + # sort exceptions by status code, first single status code, then range, then default + operation["exceptions"] = sorted(operation["exceptions"], key=lambda x: 3 if x["statusCodes"][0] == "default" else (1 if isinstance(x["statusCodes"][0], int) else 2)) + @staticmethod def remove_cloud_errors(yaml_data: Dict[str, Any]) -> None: for client in yaml_data["clients"]: @@ -315,6 +325,8 @@ def process(self) -> bool: self._validate_code_model_options() options = self._build_code_model_options() yaml_data = self.get_yaml() + + self.sort_exceptions(yaml_data) if self.options_retriever.azure_arm: self.remove_cloud_errors(yaml_data) diff --git a/packages/http-client-python/generator/pygen/codegen/models/model_type.py b/packages/http-client-python/generator/pygen/codegen/models/model_type.py index 80e21252b4..09422a44e3 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/model_type.py +++ b/packages/http-client-python/generator/pygen/codegen/models/model_type.py @@ -348,7 +348,7 @@ def serialization_type(self) -> str: @property def instance_check_template(self) -> str: - return "isinstance({}, _model_base.Model)" + return "isinstance({}, " + f"_models.{self.name})" def imports(self, **kwargs: Any) -> FileImport: file_import = super().imports(**kwargs) diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation.py b/packages/http-client-python/generator/pygen/codegen/models/operation.py index 06631ca907..0ce0c62aee 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation.py @@ -9,6 +9,7 @@ List, Any, Optional, + Tuple, Union, TYPE_CHECKING, Generic, @@ -201,17 +202,11 @@ def default_error_deserialization(self) -> Optional[str]: exception_schema = default_exceptions[0].type if isinstance(exception_schema, ModelType): return exception_schema.type_annotation(skip_quote=True) - # in this case, it's just an AnyType - return "'object'" + return None @property def non_default_errors(self) -> List[Response]: - return [e for e in self.exceptions if "default" not in e.status_codes] - - @property - def non_default_error_status_codes(self) -> List[Union[str, int]]: - """Actually returns all of the status codes from exceptions (besides default)""" - return list(chain.from_iterable([error.status_codes for error in self.non_default_errors])) + return [e for e in self.exceptions if "default" not in e.status_codes and e.type and isinstance(e.type, ModelType)] def _imports_shared(self, async_mode: bool, **kwargs: Any) -> FileImport: # pylint: disable=unused-argument file_import = FileImport(self.code_model) @@ -344,19 +339,7 @@ def imports( # pylint: disable=too-many-branches, disable=too-many-statements file_import.add_submodule_import("exceptions", error, ImportType.SDKCORE) if self.code_model.options["azure_arm"]: file_import.add_submodule_import("azure.mgmt.core.exceptions", "ARMErrorFormat", ImportType.SDKCORE) - if self.non_default_errors: - file_import.add_submodule_import( - "typing", - "Type", - ImportType.STDLIB, - ) file_import.add_mutable_mapping_import() - if self.non_default_error_status_codes: - file_import.add_submodule_import( - "typing", - "cast", - ImportType.STDLIB, - ) if self.has_kwargs_to_pop_with_default( self.parameters.kwargs_to_pop, ParameterLocation.HEADER # type: ignore @@ -436,7 +419,7 @@ def imports( # pylint: disable=too-many-branches, disable=too-many-statements elif any(r.type for r in self.responses): file_import.add_submodule_import(f"{relative_path}_model_base", "_deserialize", ImportType.LOCAL) if self.default_error_deserialization or self.non_default_errors: - file_import.add_submodule_import(f"{relative_path}_model_base", "_deserialize", ImportType.LOCAL) + file_import.add_submodule_import(f"{relative_path}_model_base", "_failsafe_deserialize", ImportType.LOCAL) return file_import def get_response_from_status(self, status_code: Optional[Union[str, int]]) -> ResponseType: diff --git a/packages/http-client-python/generator/pygen/codegen/models/response.py b/packages/http-client-python/generator/pygen/codegen/models/response.py index 19a7d62e94..c36a98a371 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/response.py +++ b/packages/http-client-python/generator/pygen/codegen/models/response.py @@ -54,7 +54,7 @@ def __init__( type: Optional[BaseType] = None, ) -> None: super().__init__(yaml_data=yaml_data, code_model=code_model) - self.status_codes: List[Union[int, str]] = yaml_data["statusCodes"] + self.status_codes: List[Union[int, str, List[int]]] = yaml_data["statusCodes"] self.headers = headers or [] self.type = type self.nullable = yaml_data.get("nullable") diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index de7d6c2086..34924397e1 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -61,14 +61,6 @@ def _all_same(data: List[List[str]]) -> bool: return len(data) > 1 and all(sorted(data[0]) == sorted(data[i]) for i in range(1, len(data))) -def _need_type_ignore(builder: OperationType) -> bool: - for e in builder.non_default_errors: - for status_code in e.status_codes: - if status_code in (401, 404, 409, 304): - return True - return False - - def _xml_config(send_xml: bool, content_types: List[str]) -> str: if not (send_xml and "xml" in str(content_types)): return "" @@ -999,20 +991,80 @@ def handle_error_response(self, builder: OperationType) -> List[str]: elif isinstance(builder.stream_value, str): # _stream is not sure, so we need to judge it retval.append(" if _stream:") retval.extend([f" {l}" for l in response_read]) - type_ignore = " # type: ignore" if _need_type_ignore(builder) else "" retval.append( - f" map_error(status_code=response.status_code, response=response, error_map=error_map){type_ignore}" + f" map_error(status_code=response.status_code, response=response, error_map=error_map)" ) error_model = "" + if builder.non_default_errors and self.code_model.options["models_mode"]: + error_model = ", model=error" + condition = "if" + retval.append(" error = None") + for e in builder.non_default_errors: + # single status code + if isinstance(e.status_codes[0], int): + for status_code in e.status_codes: + retval.append(f" {condition} response.status_code == {status_code}:") + if self.code_model.options["models_mode"] == "dpg": + retval.append(f" error = _failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, response.json())") + else: + retval.append( + f" error = self._deserialize.failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, " + "pipeline_response)" + ) + # add build-in error type + # TODO: we should decide whether need to this wrapper for customized error type + if status_code == 401: + retval.append( + " raise ClientAuthenticationError(response=response{}{})".format( + error_model, + (", error_format=ARMErrorFormat" if self.code_model.options["azure_arm"] else ""), + ) + ) + elif status_code == 404: + retval.append( + " raise ResourceNotFoundError(response=response{}{})".format( + error_model, + (", error_format=ARMErrorFormat" if self.code_model.options["azure_arm"] else ""), + ) + ) + elif status_code == 409: + retval.append( + " raise ResourceExistsError(response=response{}{})".format( + error_model, + (", error_format=ARMErrorFormat" if self.code_model.options["azure_arm"] else ""), + ) + ) + elif status_code == 304: + retval.append( + " raise ResourceNotModifiedError(response=response{}{})".format( + error_model, + (", error_format=ARMErrorFormat" if self.code_model.options["azure_arm"] else ""), + ) + ) + # ranged status code only exist in typespec and will not have multiple status codes + else: + retval.append(f" {condition} {e.status_codes[0][0]} <= response.status_code <= {e.status_codes[0][1]}:") + if self.code_model.options["models_mode"] == "dpg": + retval.append(f" error = _failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, response.json())") + else: + retval.append( + f" error = self._deserialize.failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, " + "pipeline_response)" + ) + condition = "elif" + # default error handling if builder.default_error_deserialization and self.code_model.options["models_mode"]: + error_model = ", model=error" + indent = " " if builder.non_default_errors else " " + if builder.non_default_errors: + retval.append(" else:") if self.code_model.options["models_mode"] == "dpg": - retval.append(f" error = _deserialize({builder.default_error_deserialization}, response.json())") + retval.append(f"{indent}error = _failsafe_deserialize({builder.default_error_deserialization}, response.json())") else: retval.append( - f" error = self._deserialize.failsafe_deserialize({builder.default_error_deserialization}, " + f"{indent}error = self._deserialize.failsafe_deserialize({builder.default_error_deserialization}, " "pipeline_response)" ) - error_model = ", model=error" retval.append( " raise HttpResponseError(response=response{}{})".format( error_model, @@ -1085,60 +1137,28 @@ def handle_response(self, builder: OperationType) -> List[str]: retval.append("return 200 <= response.status_code <= 299") return retval + def _need_specific_error_map(self, code: int, builder: OperationType) -> bool: + for non_default_error in builder.non_default_errors: + # single status code + if code in non_default_error.status_codes: + return False + # ranged status code + if isinstance(non_default_error.status_codes[0], list) and non_default_error.status_codes[0][0] <= code <= non_default_error.status_codes[0][1]: + return False + return True + def error_map(self, builder: OperationType) -> List[str]: retval = ["error_map: MutableMapping = {"] - if builder.non_default_errors: - if not 401 in builder.non_default_error_status_codes: + if builder.non_default_errors and self.code_model.options["models_mode"]: + # TODO: we should decide whether to add the build-in error map when there is a customized default error type + if self._need_specific_error_map(401, builder): retval.append(" 401: ClientAuthenticationError,") - if not 404 in builder.non_default_error_status_codes: + if self._need_specific_error_map(404, builder): retval.append(" 404: ResourceNotFoundError,") - if not 409 in builder.non_default_error_status_codes: + if self._need_specific_error_map(409, builder): retval.append(" 409: ResourceExistsError,") - if not 304 in builder.non_default_error_status_codes: + if self._need_specific_error_map(304, builder): retval.append(" 304: ResourceNotModifiedError,") - for e in builder.non_default_errors: - error_model_str = "" - if isinstance(e.type, ModelType): - if self.code_model.options["models_mode"] == "msrest": - error_model_str = ( - f", model=self._deserialize(" f"_models.{e.type.serialization_type}, response)" - ) - elif self.code_model.options["models_mode"] == "dpg": - error_model_str = f", model=_deserialize(_models.{e.type.name}, response.json())" - error_format_str = ", error_format=ARMErrorFormat" if self.code_model.options["azure_arm"] else "" - for status_code in e.status_codes: - if status_code == 401: - retval.append( - " 401: cast(Type[HttpResponseError], " - "lambda response: ClientAuthenticationError(response=response" - f"{error_model_str}{error_format_str}))," - ) - elif status_code == 404: - retval.append( - " 404: cast(Type[HttpResponseError], " - "lambda response: ResourceNotFoundError(response=response" - f"{error_model_str}{error_format_str}))," - ) - elif status_code == 409: - retval.append( - " 409: cast(Type[HttpResponseError], " - "lambda response: ResourceExistsError(response=response" - f"{error_model_str}{error_format_str}))," - ) - elif status_code == 304: - retval.append( - " 304: cast(Type[HttpResponseError], " - "lambda response: ResourceNotModifiedError(response=response" - f"{error_model_str}{error_format_str}))," - ) - elif not error_model_str and not error_format_str: - retval.append(f" {status_code}: HttpResponseError,") - else: - retval.append( - f" {status_code}: cast(Type[HttpResponseError], " - "lambda response: HttpResponseError(response=response" - f"{error_model_str}{error_format_str}))," - ) else: retval.append( " 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, " diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 index 2bea913ef3..fd2cbdedc1 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 @@ -892,6 +892,23 @@ def _deserialize( return _deserialize_with_callable(deserializer, value) +def _failsafe_deserialize( + deserializer: typing.Any, + value: typing.Any, + module: typing.Optional[str] = None, + rf: typing.Optional["_RestField"] = None, + format: typing.Optional[str] = None, +) -> typing.Any: + try: + return _deserialize(deserializer, value, module, rf, format) + except DeserializationError: + _LOGGER.warning( + "Ran into a deserialization error. Ignoring since this is failsafe deserialization", + exc_info=True + ) + return None + + class _RestField: def __init__( self, diff --git a/packages/http-client-python/package-lock.json b/packages/http-client-python/package-lock.json index 500223ac08..4acecf6d8a 100644 --- a/packages/http-client-python/package-lock.json +++ b/packages/http-client-python/package-lock.json @@ -1,12 +1,12 @@ { "name": "@typespec/http-client-python", - "version": "0.3.9", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@typespec/http-client-python", - "version": "0.3.9", + "version": "0.4.1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -21,7 +21,7 @@ "@azure-tools/typespec-azure-core": "~0.48.0", "@azure-tools/typespec-azure-resource-manager": "~0.48.0", "@azure-tools/typespec-azure-rulesets": "~0.48.0", - "@azure-tools/typespec-client-generator-core": "~0.48.0", + "@azure-tools/typespec-client-generator-core": "~0.48.5", "@types/js-yaml": "~4.0.5", "@types/node": "~22.5.4", "@types/semver": "7.5.8", @@ -45,7 +45,7 @@ "@azure-tools/typespec-azure-core": ">=0.48.0 <1.0.0", "@azure-tools/typespec-azure-resource-manager": ">=0.48.0 <1.0.0", "@azure-tools/typespec-azure-rulesets": ">=0.48.0 <3.0.0", - "@azure-tools/typespec-client-generator-core": ">=0.48.0 <1.0.0", + "@azure-tools/typespec-client-generator-core": ">=0.48.5 <1.0.0", "@typespec/compiler": ">=0.62.0 <1.0.0", "@typespec/http": ">=0.62.0 <1.0.0", "@typespec/openapi": ">=0.62.0 <1.0.0", @@ -237,9 +237,9 @@ } }, "node_modules/@azure-tools/typespec-client-generator-core": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-client-generator-core/-/typespec-client-generator-core-0.48.0.tgz", - "integrity": "sha512-+fmKjapz0kP7ONPZap8dgcIKIdQw+YBSrf89csbIyhPTcLnVAk/BKljo8FoNypKXwqKHenslLm0njBKPllkopg==", + "version": "0.48.5", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-client-generator-core/-/typespec-client-generator-core-0.48.5.tgz", + "integrity": "sha512-oAGyH99f3FMzTVE82A/hHupMlpDhxBUTL63wCUab9DM6Rqk+liBGobGl/EPdiOxpvcvhm1drEhkFCkqJt6JenA==", "dev": true, "dependencies": { "change-case": "~5.4.4", diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index 34627e8a8d..02c12bbc48 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/http-client-python", - "version": "0.3.12", + "version": "0.4.1", "author": "Microsoft Corporation", "description": "TypeSpec emitter for Python SDKs", "homepage": "https://typespec.io", @@ -60,7 +60,7 @@ "@azure-tools/typespec-azure-resource-manager": ">=0.48.0 <1.0.0", "@azure-tools/typespec-autorest": ">=0.48.0 <1.0.0", "@azure-tools/typespec-azure-rulesets": ">=0.48.0 <3.0.0", - "@azure-tools/typespec-client-generator-core": ">=0.48.0 <1.0.0" + "@azure-tools/typespec-client-generator-core": ">=0.48.5 <1.0.0" }, "dependencies": { "js-yaml": "~4.1.0", @@ -77,7 +77,7 @@ "@azure-tools/typespec-azure-core": "~0.48.0", "@azure-tools/typespec-azure-rulesets": "~0.48.0", "@azure-tools/typespec-azure-resource-manager": "~0.48.0", - "@azure-tools/typespec-client-generator-core": "~0.48.0", + "@azure-tools/typespec-client-generator-core": "~0.48.5", "@azure-tools/cadl-ranch-specs": "~0.39.1", "@azure-tools/cadl-ranch-expect": "~0.15.6", "@types/js-yaml": "~4.0.5", diff --git a/packages/http-server-csharp/src/attributes.ts b/packages/http-server-csharp/src/attributes.ts index de792e7402..cc4531d31f 100644 --- a/packages/http-server-csharp/src/attributes.ts +++ b/packages/http-server-csharp/src/attributes.ts @@ -419,13 +419,45 @@ export function getNumericConstraintAttribute( export function getSafeIntAttribute(type: Scalar): Attribute | undefined { if (type.name.toLowerCase() !== "safeint") return undefined; - return new Attribute( + const attr: Attribute = new Attribute( new AttributeType({ - name: "SafeInt", + name: `NumericConstraint`, namespace: HelperNamespace, }), [], ); + + attr.parameters.push( + new Parameter({ + name: "MinValue", + value: new NumericValue(-9007199254740991), + optional: true, + type: new CSharpType({ + name: "long", + namespace: "System", + isBuiltIn: true, + isValueType: true, + isNullable: false, + }), + }), + ); + + attr.parameters.push( + new Parameter({ + name: "MaxValue", + value: new NumericValue(9007199254740991), + optional: true, + type: new CSharpType({ + name: "long", + namespace: "System", + isBuiltIn: true, + isValueType: true, + isNullable: false, + }), + }), + ); + + return attr; } function getEnumAttribute(type: Enum, cSharpName?: string): Attribute { diff --git a/packages/http-server-csharp/src/interfaces.ts b/packages/http-server-csharp/src/interfaces.ts index de6f87a985..b7142c48a0 100644 --- a/packages/http-server-csharp/src/interfaces.ts +++ b/packages/http-server-csharp/src/interfaces.ts @@ -20,17 +20,20 @@ export class CSharpType implements CSharpTypeMetadata { namespace: string; isBuiltIn: boolean; isValueType: boolean; + isNullable: boolean; public constructor(input: { name: string; namespace: string; isBuiltIn?: boolean; isValueType?: boolean; + isNullable?: boolean; }) { this.name = input.name; this.namespace = input.namespace; this.isBuiltIn = input.isBuiltIn !== undefined ? input.isBuiltIn : input.namespace === "System"; this.isValueType = input.isValueType !== undefined ? input.isValueType : false; + this.isNullable = input.isNullable !== undefined ? input.isNullable : false; } isNamespaceInScope(scope?: Scope, visited?: Set>): boolean { diff --git a/packages/http-server-csharp/src/service.ts b/packages/http-server-csharp/src/service.ts index a64e9bf690..2eefa32289 100644 --- a/packages/http-server-csharp/src/service.ts +++ b/packages/http-server-csharp/src/service.ts @@ -346,7 +346,8 @@ export async function $onEmit(context: EmitContext) property, property.name, ); - const [typeName, typeDefault] = this.#findPropertyType(property); + + const [typeName, typeDefault, nullable] = this.#findPropertyType(property); const doc = getDoc(this.emitter.getProgram(), property); const attributes = getModelAttributes(this.emitter.getProgram(), property, propertyName); // eslint-disable-next-line @typescript-eslint/no-deprecated @@ -356,7 +357,9 @@ export async function $onEmit(context: EmitContext) : typeDefault; return this.emitter.result .rawCode(code`${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public ${this.#isInheritedProperty(property) ? "new " : ""}${typeName}${ - property.optional && isValueType(this.emitter.getProgram(), property.type) ? "?" : "" + isValueType(this.emitter.getProgram(), property.type) && (property.optional || nullable) + ? "?" + : "" } ${propertyName} { get; ${typeDefault ? "}" : "set; }"}${ defaultValue ? ` = ${defaultValue};\n` : "\n" } @@ -365,14 +368,27 @@ export async function $onEmit(context: EmitContext) #findPropertyType( property: ModelProperty, - ): [EmitterOutput, string | boolean | undefined] { + ): [EmitterOutput, string | boolean | undefined, boolean] { return this.#getTypeInfoForTsType(property.type); } + #getTypeInfoForUnion( + union: Union, + ): [EmitterOutput, string | boolean | undefined, boolean] { + const propResult = this.#getNonNullableTsType(union); + if (propResult === undefined) { + return [ + code`${emitter.emitTypeReference(union)}`, + undefined, + [...union.variants.values()].filter((v) => isNullType(v.type)).length > 0, + ]; + } + const [typeName, typeDefault, _] = this.#getTypeInfoForTsType(propResult.type); + return [typeName, typeDefault, propResult.nullable]; + } #getTypeInfoForTsType( - this: any, tsType: Type, - ): [EmitterOutput, string | boolean | undefined] { + ): [EmitterOutput, string | boolean | undefined, boolean] { function extractStringValue(type: Type, span: StringTemplateSpan): string { switch (type.kind) { case "String": @@ -403,54 +419,62 @@ export async function $onEmit(context: EmitContext) } switch (tsType.kind) { case "String": - return [code`string`, `"${tsType.value}"`]; + return [code`string`, `"${tsType.value}"`, false]; case "StringTemplate": const template = tsType; if (template.stringValue !== undefined) - return [code`string`, `"${template.stringValue}"`]; + return [code`string`, `"${template.stringValue}"`, false]; const spanResults: string[] = []; for (const span of template.spans) { spanResults.push(extractStringValue(span, span)); } - return [code`string`, `"${spanResults.join("")}"`]; + return [code`string`, `"${spanResults.join("")}"`, false]; case "Boolean": - return [code`bool`, `${tsType.value === true ? true : false}`]; + return [code`bool`, `${tsType.value === true ? true : false}`, false]; case "Number": const [type, value] = this.#findNumericType(tsType); - return [code`${type}`, `${value}`]; + return [code`${type}`, `${value}`, false]; case "Tuple": const defaults = []; const [csharpType, isObject] = this.#coalesceTypes(tsType.values); - if (isObject) return ["object[]", undefined]; + if (isObject) return ["object[]", undefined, false]; for (const value of tsType.values) { const [_, itemDefault] = this.#getTypeInfoForTsType(value); defaults.push(itemDefault); } - return [code`${csharpType.getTypeReference()}[]`, `[${defaults.join(", ")}]`]; + return [ + code`${csharpType.getTypeReference()}[]`, + `[${defaults.join(", ")}]`, + csharpType.isNullable, + ]; case "Object": - return [code`object`, undefined]; + return [code`object`, undefined, false]; case "Model": if (this.#isRecord(tsType)) { - return [code`JsonObject`, undefined]; + return [code`JsonObject`, undefined, false]; } - return [code`${emitter.emitTypeReference(tsType)}`, undefined]; + return [code`${emitter.emitTypeReference(tsType)}`, undefined, false]; + case "ModelProperty": + return this.#getTypeInfoForTsType(tsType.type); case "Enum": - return [code`${emitter.emitTypeReference(tsType)}`, undefined]; + return [code`${emitter.emitTypeReference(tsType)}`, undefined, false]; case "EnumMember": if (typeof tsType.value === "number") { const stringValue = tsType.value.toString(); if (stringValue.includes(".") || stringValue.includes("e")) - return ["double", stringValue]; - return ["int", stringValue]; + return ["double", stringValue, false]; + return ["int", stringValue, false]; } if (typeof tsType.value === "string") { - return ["string", tsType.value]; + return ["string", tsType.value, false]; } - return [code`object`, undefined]; + return [code`object`, undefined, false]; case "Union": - return [code`${emitter.emitTypeReference(tsType)}`, undefined]; + return this.#getTypeInfoForUnion(tsType); + case "UnionVariant": + return this.#getTypeInfoForTsType(tsType.type); default: - return [code`${emitter.emitTypeReference(tsType)}`, undefined]; + return [code`${emitter.emitTypeReference(tsType)}`, undefined, false]; } } @@ -753,13 +777,13 @@ export async function $onEmit(context: EmitContext) } let i = 1; for (const requiredParam of requiredParams) { - const [paramType, _] = this.#findPropertyType(requiredParam); + const [paramType, _, __] = this.#findPropertyType(requiredParam); signature.push( code`${paramType} ${ensureCSharpIdentifier(this.emitter.getProgram(), requiredParam, requiredParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`, ); } for (const optionalParam of optionalParams) { - const [paramType, _] = this.#findPropertyType(optionalParam); + const [paramType, _, __] = this.#findPropertyType(optionalParam); signature.push( code`${paramType}? ${ensureCSharpIdentifier(this.emitter.getProgram(), optionalParam, optionalParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`, ); @@ -896,7 +920,7 @@ export async function $onEmit(context: EmitContext) name, NameCasingType.Parameter, ); - let [emittedType, emittedDefault] = this.#findPropertyType(parameter); + let [emittedType, emittedDefault, _] = this.#findPropertyType(parameter); if (emittedType.toString().endsWith("[]")) emittedDefault = undefined; // eslint-disable-next-line @typescript-eslint/no-deprecated const defaultValue = parameter.default @@ -907,11 +931,18 @@ export async function $onEmit(context: EmitContext) code`${httpParam.type !== "path" ? this.#emitParameterAttribute(httpParam) : ""}${emittedType} ${emittedName}${defaultValue === undefined ? "" : ` = ${defaultValue}`}`, ); } + #getBodyParameters(operation: HttpOperation): ModelProperty[] | undefined { + const bodyParam = operation.parameters.body; + if (bodyParam === undefined) return undefined; + if (bodyParam.property !== undefined) return [bodyParam.property]; + if (bodyParam.type.kind !== "Model" || bodyParam.type.properties.size < 1) return undefined; + return [...bodyParam.type.properties.values()]; + } #emitOperationCallParameters(operation: HttpOperation): EmitterOutput { const signature = new StringBuilder(); - const bodyParam = operation.parameters.body; let i = 0; + const bodyParameters = this.#getBodyParameters(operation); //const pathParameters = operation.parameters.parameters.filter((p) => p.type === "path"); for (const parameter of operation.parameters.parameters) { i++; @@ -922,13 +953,27 @@ export async function $onEmit(context: EmitContext) ) { signature.push( code`${this.#emitOperationCallParameter(operation, parameter)}${ - i < operation.parameters.parameters.length || bodyParam !== undefined ? ", " : "" + i < operation.parameters.parameters.length || bodyParameters !== undefined ? ", " : "" }`, ); } } - if (bodyParam !== undefined) { - signature.push(code`body`); + if (bodyParameters !== undefined) { + if (bodyParameters.length === 1) { + signature.push(code`body`); + } else { + let j = 0; + for (const parameter of bodyParameters) { + j++; + const propertyName = ensureCSharpIdentifier( + this.emitter.getProgram(), + parameter, + parameter.name, + NameCasingType.Property, + ); + signature.push(code`body?.${propertyName}${j < bodyParameters.length ? ", " : ""}`); + } + } } return signature.reduce(); @@ -1148,6 +1193,14 @@ export async function $onEmit(context: EmitContext) return result; } + #getNonNullableTsType(union: Union): { type: Type; nullable: boolean } | undefined { + const types = [...union.variants.values()]; + const nulls = types.flatMap((v) => v.type).filter((t) => isNullType(t)); + const nonNulls = types.flatMap((v) => v.type).filter((t) => !isNullType(t)); + if (nonNulls.length === 1) return { type: nonNulls[0], nullable: nulls.length > 0 }; + return undefined; + } + #coalesceTypes(types: Type[]): [CSharpType, boolean] { const defaultValue: [CSharpType, boolean] = [ new CSharpType({ @@ -1158,8 +1211,9 @@ export async function $onEmit(context: EmitContext) true, ]; let current: CSharpType | undefined = undefined; + let nullable: boolean = false; for (const type of types) { - let candidate: CSharpType; + let candidate: CSharpType | undefined = undefined; switch (type.kind) { case "Boolean": candidate = new CSharpType({ name: "bool", namespace: "System", isValueType: true }); @@ -1186,14 +1240,24 @@ export async function $onEmit(context: EmitContext) case "Scalar": candidate = getCSharpTypeForScalar(this.emitter.getProgram(), type); break; + case "Intrinsic": + if (isNullType(type)) { + nullable = true; + candidate = current; + } else { + return defaultValue; + } + break; default: return defaultValue; } current = current ?? candidate; - if (current === undefined || !candidate.equals(current)) return defaultValue; + if (current === undefined || (candidate !== undefined && !candidate.equals(current))) + return defaultValue; } + if (current !== undefined && nullable) current.isNullable = true; return current === undefined ? defaultValue : [current, false]; } diff --git a/packages/http-server-csharp/src/utils.ts b/packages/http-server-csharp/src/utils.ts index c345973b7c..ff667b0ca0 100644 --- a/packages/http-server-csharp/src/utils.ts +++ b/packages/http-server-csharp/src/utils.ts @@ -41,10 +41,13 @@ import { } from "./interfaces.js"; import { reportDiagnostic } from "./lib.js"; +const _scalars: Map = new Map(); export function getCSharpTypeForScalar(program: Program, scalar: Scalar): CSharpType { + if (_scalars.has(scalar)) return _scalars.get(scalar)!; if (program.checker.isStdType(scalar)) { return getCSharpTypeForStdScalars(program, scalar); } + if (scalar.baseScalar) { return getCSharpTypeForScalar(program, scalar.baseScalar); } @@ -54,12 +57,16 @@ export function getCSharpTypeForScalar(program: Program, scalar: Scalar): CSharp format: { typeName: scalar.name }, target: scalar, }); - return new CSharpType({ + + const result = new CSharpType({ name: "Object", namespace: "System", isBuiltIn: true, isValueType: false, }); + + _scalars.set(scalar, result); + return result; } export const UnknownType: CSharpType = new CSharpType({ @@ -71,7 +78,7 @@ export const UnknownType: CSharpType = new CSharpType({ export function getCSharpType( program: Program, type: Type, - namespace: string, + namespace?: string, ): { type: CSharpType; value?: CSharpValue } | undefined { const known = getKnownType(program, type); if (known !== undefined) return { type: known }; @@ -118,7 +125,7 @@ export function getCSharpType( return { type: new CSharpType({ name: ensureCSharpIdentifier(program, type, type.name, NameCasingType.Class), - namespace: namespace, + namespace: namespace || "Models", isBuiltIn: false, isValueType: false, }), @@ -167,24 +174,26 @@ export function getCSharpType( export function coalesceTypes( program: Program, types: Type[], - namespace: string, + namespace?: string, ): { type: CSharpType; value?: CSharpValue } { const visited = new Map(); let candidateType: CSharpType | undefined = undefined; let candidateValue: CSharpValue | undefined = undefined; for (const type of types) { - if (!visited.has(type)) { - const resolvedType = getCSharpType(program, type, namespace); - if (resolvedType === undefined) return { type: UnknownType }; - if (resolvedType.type === UnknownType) return resolvedType; - if (candidateType === undefined) { - candidateType = resolvedType.type; - candidateValue = resolvedType.value; - } else { - if (candidateValue !== resolvedType.value) candidateValue = undefined; - if (candidateType !== resolvedType.type) return { type: UnknownType }; + if (!isNullType(type)) { + if (!visited.has(type)) { + const resolvedType = getCSharpType(program, type, namespace); + if (resolvedType === undefined) return { type: UnknownType }; + if (resolvedType.type === UnknownType) return resolvedType; + if (candidateType === undefined) { + candidateType = resolvedType.type; + candidateValue = resolvedType.value; + } else { + if (candidateValue !== resolvedType.value) candidateValue = undefined; + if (candidateType !== resolvedType.type) return { type: UnknownType }; + } + visited.set(type, resolvedType); } - visited.set(type, resolvedType); } } @@ -364,8 +373,11 @@ export function getCSharpTypeForStdScalars( program: Program, scalar: Scalar & { name: ExtendedIntrinsicScalarName }, ): CSharpType { + const cached: CSharpType | undefined = _scalars.get(scalar); + if (cached !== undefined) return cached; const builtIn: CSharpType | undefined = standardScalars.get(scalar.name); if (builtIn !== undefined) { + _scalars.set(scalar, builtIn); if (scalar.name === "numeric" || scalar.name === "integer" || scalar.name === "float") { reportDiagnostic(program, { code: "no-numeric", @@ -390,10 +402,18 @@ export function getCSharpTypeForStdScalars( } export function isValueType(program: Program, type: Type): boolean { - if (type.kind === "Boolean" || type.kind === "Number" || type.kind === "Enum") return true; - if (type.kind !== "Scalar") return false; - const scalarType = getCSharpTypeForScalar(program, type); - return scalarType.isValueType; + if ( + type.kind === "Boolean" || + type.kind === "Number" || + type.kind === "Enum" || + type.kind === "EnumMember" + ) + return true; + if (type.kind === "Scalar") return getCSharpTypeForScalar(program, type).isValueType; + if (type.kind !== "Union") return false; + return [...type.variants.values()] + .flatMap((v) => v.type) + .every((t) => isNullType(t) || isValueType(program, t)); } export function formatComment( diff --git a/packages/http-server-csharp/test/generation.test.ts b/packages/http-server-csharp/test/generation.test.ts index cc1f17441b..78328daf5b 100644 --- a/packages/http-server-csharp/test/generation.test.ts +++ b/packages/http-server-csharp/test/generation.test.ts @@ -1024,3 +1024,77 @@ it("generates valid code for anonymous models", async () => { ], ); }); + +it("handles nullable types correctly", async () => { + await compileAndValidateMultiple( + runner, + ` + /** A simple test model*/ + model Foo { + /** Nullable numeric property */ + intProp: int32 | null; + /** Nullable reference type */ + stringProp: string | null; + #suppress "@typespec/http-server-csharp/anonymous-model" "This is a test" + /** A complex property */ + modelProp: { + bar: string; + } | null; + #suppress "@typespec/http-server-csharp/anonymous-model" "This is a test" + anotherModelProp: { + baz: string; + }; + + yetAnother: Foo.modelProp | null; + + } + + @route("/foo") op foo(): void; + `, + [ + ["Model0.cs", ["public partial class Model0", "public string Bar { get; set; }"]], + ["Model1.cs", ["public partial class Model1", "public string Baz { get; set; }"]], + [ + "Foo.cs", + [ + "public partial class Foo", + "public int? IntProp { get; set; }", + "public string StringProp { get; set; }", + "public Model0 ModelProp { get; set; }", + "public Model1 AnotherModelProp { get; set; }", + "public Model0 YetAnother { get; set; }", + ], + ], + ["ContosoOperationsControllerBase.cs", [`public virtual async Task Foo()`]], + ["IContosoOperations.cs", [`Task FooAsync( );`]], + ], + ); +}); + +it("handles implicit request body models correctly", async () => { + await compileAndValidateMultiple( + runner, + ` + #suppress "@typespec/http-server-csharp/anonymous-model" "Test" + @route("/foo") @post op foo(intProp?: int32, arrayProp?: string[]): void; + `, + [ + [ + "Model0.cs", + [ + "public partial class Model0", + "public int? IntProp { get; set; }", + "public string[] ArrayProp { get; set; }", + ], + ], + [ + "ContosoOperationsControllerBase.cs", + [ + `public virtual async Task Foo(Model0 body)`, + ".FooAsync(body?.IntProp, body?.ArrayProp)", + ], + ], + ["IContosoOperations.cs", [`Task FooAsync( int? intProp, string[]? arrayProp);`]], + ], + ); +}); diff --git a/packages/http-specs/CHANGELOG.md b/packages/http-specs/CHANGELOG.md index 2bfe98d87a..a64b980f23 100644 --- a/packages/http-specs/CHANGELOG.md +++ b/packages/http-specs/CHANGELOG.md @@ -1,5 +1,9 @@ # @typespec/http-specs +## 0.1.0-alpha.3 + +- Create coverages container if not existing + ## 0.1.0-alpha.2 - Minor `api-key` in the `authentication` specs diff --git a/packages/http-specs/package.json b/packages/http-specs/package.json index 33c0d78243..18ad38dd70 100644 --- a/packages/http-specs/package.json +++ b/packages/http-specs/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/http-specs", - "version": "0.1.0-alpha.2", + "version": "0.1.0-alpha.3", "description": "Spec scenarios and mock apis", "main": "dist/index.js", "type": "module", @@ -12,6 +12,8 @@ "validate-scenarios": "tsp-spector validate-scenarios ./specs", "generate-scenarios-summary": "tsp-spector generate-scenarios-summary ./specs", "regen-docs": "pnpm generate-scenarios-summary", + "upload-manifest": "tsp-spector upload-manifest ./specs --setName @typespec/http-specs --containerName manifests-typespec --storageAccountName typespec", + "upload-coverage": "tsp-spector upload-coverage --generatorName @typespec/http-specs --generatorVersion 0.1.0-alpha.3 --containerName coverages --generatorMode standard --storageAccountName typespec", "validate-mock-apis": "tsp-spector validate-mock-apis ./specs", "check-scenario-coverage": "tsp-spector check-coverage ./specs", "validate-client-server": "concurrently \"tsp-spector server start ./specs\" \"npm run client\" && tsp-spector server stop", diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 9fbaa6e661..ff4a3ec4d5 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -4360,77 +4360,6 @@ Expected input body: } ``` -### Type_Model_Templated_float32Type - -- Endpoint: `put /type/model/templated/float32ValuesType` - -Expected input body: - -```json -{ - "kind": "Float32Values", - "values": [0.5], - "value": 0.5 -} -``` - -Expected response body: - -```json -{ - "kind": "Float32Values", - "values": [0.5], - "value": 0.5 -} -``` - -### Type_Model_Templated_int32Type - -- Endpoint: `put /type/model/templated/int32ValuesType` - -Expected input body: - -```json -{ - "kind": "Int32Values", - "values": [1234], - "value": 1234 -} -``` - -Expected response body: - -```json -{ - "kind": "Int32Values", - "values": [1234], - "value": 1234 -} -``` - -### Type_Model_Templated_numericType - -- Endpoint: `put /type/model/templated/numericType` - -Expected input body: - -```json -{ - "kind": "Int32Values", - "values": [1234], - "value": 1234 -} -``` - -Expected response body: - -```json -{ - "values": [1234], - "value": 1234 -} -``` - ### Type_Model_Usage_input - Endpoint: `get /type/model/usage/input` @@ -7436,6 +7365,49 @@ Expected request body: { "prop": "foo" } ``` +### Versioning_Removed_modelV3 + +- Endpoint: `post /versioning/removed/api-version:{version}/v3` + +path: "/versioning/removed/api-version[:]v1/v3" +Expected request body: + +```json +{ "id": "123", "enumProp": "enumMemberV1" } +``` + +Expected response body: + +```json +{ "id": "123", "enumProp": "enumMemberV1" } +``` + +path: "/versioning/removed/api-version[:]v2preview/v3" +Expected request body: + +```json +{ "id": "123" } +``` + +Expected response body: + +```json +{ "id": "123" } +``` + +path: "/versioning/removed/api-version[:]v2/v3" +Expected request body: + +```json +{ "id": "123", "enumProp": "enumMemberV1" } +``` + +Expected response body: + +```json +{ "id": "123", "enumProp": "enumMemberV1" } +``` + ### Versioning_Removed_v2 - Endpoint: `post /versioning/removed/api-version:{version}/v2` diff --git a/packages/http-specs/specs/type/model/templated/main.tsp b/packages/http-specs/specs/type/model/templated/main.tsp deleted file mode 100644 index 3d9bc1d92e..0000000000 --- a/packages/http-specs/specs/type/model/templated/main.tsp +++ /dev/null @@ -1,130 +0,0 @@ -import "@typespec/http"; -import "@typespec/spector"; - -using Http; -using Spector; - -/** - * Illustrates the model templated cases. There is a base templated type and an instantiated type extending from it. - */ -@scenarioService("/type/model/templated") -namespace Type.Model.Templated; - -@friendlyName("{name}Type", T) -model NumericType { - /** - * An array of numeric values. - */ - values: T[]; - - value: T; -} - -/** - * An instantiated type representing int32 values type. - */ -model Int32ValuesType extends NumericType { - /** - * The Kind of the Int32ValuesType. - */ - kind: "Int32Values"; -} - -/** - * An instantiated type representing float32 values type. - */ -model Float32ValuesType extends NumericType { - /** - * The Kind of the Float32ValuesType. - */ - kind: "Float32Values"; -} - -@scenario -@scenarioDoc(""" - Expected input body: - ```json - { - "kind": "Int32Values", - "values": - [ - 1234 - ], - "value": 1234 - } - ``` - - Expected response body: - ```json - { - "values": - [ - 1234 - ], - "value": 1234 - } - ``` - """) -@route("/numericType") -@put -op numericType(@body input: NumericType): NumericType; - -@scenario -@scenarioDoc(""" - Expected input body: - ```json - { - "kind": "Float32Values", - "values": - [ - 0.5 - ], - "value": 0.5 - } - ``` - - Expected response body: - ```json - { - "kind": "Float32Values", - "values": - [ - 0.5 - ], - "value": 0.5 - } - ``` - """) -@route("/float32ValuesType") -@put -op float32Type(@body input: Float32ValuesType): Float32ValuesType; - -@scenario -@scenarioDoc(""" - Expected input body: - ```json - { - "kind": "Int32Values", - "values": - [ - 1234 - ], - "value": 1234 - } - ``` - - Expected response body: - ```json - { - "kind": "Int32Values", - "values": - [ - 1234 - ], - "value": 1234 - } - ``` - """) -@route("/int32ValuesType") -@put -op int32Type(@body input: Int32ValuesType): Int32ValuesType; diff --git a/packages/http-specs/specs/type/model/templated/mockapi.ts b/packages/http-specs/specs/type/model/templated/mockapi.ts deleted file mode 100644 index cd69b9bfa8..0000000000 --- a/packages/http-specs/specs/type/model/templated/mockapi.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { json, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api"; - -export const Scenarios: Record = {}; - -Scenarios.Type_Model_Templated_numericType = passOnSuccess({ - uri: "/type/model/templated/numericType", - method: "put", - request: { - body: { - kind: "Int32Values", - values: [1234], - value: 1234, - }, - }, - response: { - status: 200, - body: json({ - kind: "Int32Values", - values: [1234], - value: 1234, - }), - }, - kind: "MockApiDefinition", -}); - -Scenarios.Type_Model_Templated_float32Type = passOnSuccess({ - uri: "/type/model/templated/float32ValuesType", - method: "put", - request: { - body: { - kind: "Float32Values", - values: [0.5], - value: 0.5, - }, - }, - response: { - status: 200, - body: json({ - kind: "Float32Values", - values: [0.5], - value: 0.5, - }), - }, - kind: "MockApiDefinition", -}); - -Scenarios.Type_Model_Templated_int32Type = passOnSuccess({ - uri: "/type/model/templated/int32ValuesType", - method: "put", - request: { - body: { - kind: "Int32Values", - values: [1234], - value: 1234, - }, - }, - response: { - status: 200, - body: json({ - kind: "Int32Values", - values: [1234], - value: 1234, - }), - }, - kind: "MockApiDefinition", -}); diff --git a/packages/http-specs/specs/versioning/removed/main.tsp b/packages/http-specs/specs/versioning/removed/main.tsp index 609df7c6d3..17bb7b0be2 100644 --- a/packages/http-specs/specs/versioning/removed/main.tsp +++ b/packages/http-specs/specs/versioning/removed/main.tsp @@ -21,7 +21,7 @@ using TypeSpec.Versioning; endpoint: url, /** - * Need to be set as 'v1' or 'v2' in client. + * Need to be set as 'v1', 'v2preview' or 'v2' in client. */ version: Versions, } @@ -37,6 +37,11 @@ enum Versions { */ v1: "v1", + /** + * The V2 Preview version. + */ + v2preview: "v2preview", + /** * The version v2. */ @@ -67,6 +72,14 @@ model ModelV2 { unionProp: UnionV2; } +model ModelV3 { + id: string; + + @removed(Versions.v2preview) + @added(Versions.v2) + enumProp: EnumV3; +} + enum EnumV2 { @removed(Versions.v2) enumMemberV1, @@ -74,6 +87,14 @@ enum EnumV2 { enumMemberV2, } +enum EnumV3 { + @removed(Versions.v2preview) + @added(Versions.v2) + enumMemberV1, + + enumMemberV2Preview, +} + @removed(Versions.v2) union UnionV1 { string, @@ -124,3 +145,44 @@ interface InterfaceV1 { @route("/v1") v1InInterface(@body body: ModelV1): ModelV1; } + +/** This operation will pass different paths and different request bodies based on different versions. */ +@scenario +@scenarioDoc(""" + path: "/versioning/removed/api-version[:]v1/v3" + Expected request body: + ```json + { "id": "123", "enumProp": "enumMemberV1" } + ``` + + Expected response body: + ```json + { "id": "123", "enumProp": "enumMemberV1" } + ``` + + path: "/versioning/removed/api-version[:]v2preview/v3" + Expected request body: + ```json + { "id": "123"} + ``` + + Expected response body: + ```json + { "id": "123"} + ``` + + path: "/versioning/removed/api-version[:]v2/v3" + Expected request body: + ```json + { "id": "123", "enumProp": "enumMemberV1" } + ``` + + Expected response body: + ```json + { "id": "123", "enumProp": "enumMemberV1" } + ``` + + """) +@post +@route("/v3") +op modelV3(@body body: ModelV3): ModelV3; diff --git a/packages/http-specs/specs/versioning/removed/mockapi.ts b/packages/http-specs/specs/versioning/removed/mockapi.ts index 7f4e2534cf..c197f161d0 100644 --- a/packages/http-specs/specs/versioning/removed/mockapi.ts +++ b/packages/http-specs/specs/versioning/removed/mockapi.ts @@ -18,3 +18,50 @@ Scenarios.Versioning_Removed_v2 = passOnSuccess({ }, kind: "MockApiDefinition", }); + +Scenarios.Versioning_Removed_modelV3 = passOnSuccess({ + uri: `/versioning/removed/api-version[:]v1/v3`, + method: "post", + request: { + body: { + id: "123", + enumProp: "enumMemberV1", + }, + }, + response: { + status: 200, + body: json({ id: "123", enumProp: "enumMemberV1" }), + }, + kind: "MockApiDefinition", +}); + +Scenarios.Versioning_Removed_modelV3_V2 = passOnSuccess({ + uri: `/versioning/removed/api-version[:]v2/v3`, + method: "post", + request: { + body: { + id: "123", + enumProp: "enumMemberV1", + }, + }, + response: { + status: 200, + body: json({ id: "123", enumProp: "enumMemberV1" }), + }, + kind: "MockApiDefinition", +}); + +Scenarios.Versioning_Removed_modelV3_V2preview = passOnSuccess({ + uri: `/versioning/removed/api-version[:]v2preview/v3`, + method: "post", + request: { + body: { + id: "123", + }, + }, + response: { + status: 200, + body: json({ id: "123" }), + }, + kind: "MockApiDefinition", +}); diff --git a/packages/spector/src/actions/upload-coverage-report.ts b/packages/spector/src/actions/upload-coverage-report.ts index 9f5fb38ed4..caf1b20191 100644 --- a/packages/spector/src/actions/upload-coverage-report.ts +++ b/packages/spector/src/actions/upload-coverage-report.ts @@ -30,6 +30,7 @@ export async function uploadCoverageReport({ credential: new AzureCliCredential(), containerName, }); + await client.createIfNotExists(); const generatorMetadata: GeneratorMetadata = { name: generatorName, version: generatorVersion, diff --git a/website/src/content/docs/docs/getting-started/getting-started.md b/website/src/content/docs/docs/getting-started/getting-started.md index 2a57a3236b..acf2caa3d4 100644 --- a/website/src/content/docs/docs/getting-started/getting-started.md +++ b/website/src/content/docs/docs/getting-started/getting-started.md @@ -3,4 +3,4 @@ id: getting-started title: Getting Started --- -- [Get started with TypeSpec for REST](./getting-started-rest/01-setup-basic-syntax.md) +- [Get started with TypeSpec for REST](./getting-started-rest/01-setup-basic-syntax.mdx)