From ab2c473d53781ea229728c1d60d7d596a9edc68b Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:42:15 -0400 Subject: [PATCH 1/3] fix(csharp,php): handle literal-typed global headers with env fallback (#17442) * fix(csharp,php): handle literal-typed global headers with env fallback Literal-typed global headers no longer throw when neither the parameter nor the env var is set, and the SDK version header is emitted globally qualified so it cannot be shadowed. Co-Authored-By: bot_apk * fix(csharp): parenthesize global:: version access inside interpolated strings Co-Authored-By: bot_apk --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: bot_apk --- .../base/src/context/GeneratorContext.ts | 17 +- .../codegen/src/ast/types/ClassReference.ts | 12 + ...fix-literal-global-header-env-fallback.yml | 14 + .../src/root-client/RootClientGenerator.ts | 62 +- ...fix-literal-global-header-env-fallback.yml | 7 + .../src/root-client/RootClientGenerator.ts | 11 +- .../csharp-global-header-literal-env.json | 148 ++ .../php-global-header-literal-env.json | 148 ++ .../csharp-global-header-literal-env.json | 421 ++++++ .../php-global-header-literal-env.json | 421 ++++++ seed/cli/seed.yml | 2 + .../src/SeedAccept/SeedAcceptClient.cs | 2 +- .../SeedAliasExtendsClient.cs | 2 +- .../alias/src/SeedAlias/SeedAliasClient.cs | 2 +- .../allof-inline/src/SeedApi/SeedApiClient.cs | 2 +- .../allof/src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedAnyAuth/SeedAnyAuthClient.cs | 2 +- .../src/SeedAnyAuth/SeedAnyAuthClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedApiWideBasePathClient.cs | 2 +- .../src/SeedAudiences/SeedAudiencesClient.cs | 2 +- ...SeedBasicAuthEnvironmentVariablesClient.cs | 2 +- .../SeedBasicAuthPwOmittedClient.cs | 2 +- .../src/SeedBasicAuth/SeedBasicAuthClient.cs | 2 +- .../src/SeedBasicAuth/SeedBasicAuthClient.cs | 2 +- .../src/SeedBasicAuth/SeedBasicAuthClient.cs | 2 +- ...eedBearerTokenEnvironmentVariableClient.cs | 5 +- ...eedBearerTokenEnvironmentVariableClient.cs | 5 +- .../SeedBytesDownloadClient.cs | 2 +- .../SeedBytesUpload/SeedBytesUploadClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedClientSideParamsClient.cs | 2 +- .../SeedContentTypesClient.cs | 2 +- .../SeedCrossPackageTypeNamesClient.cs | 2 +- .../SeedCsharpGlobalHeaderEnvClient.cs | 2 +- .../.editorconfig | 35 + .../.fern/metadata.json | 10 + .../.github/workflows/ci.yml | 51 + .../.gitignore | 484 +++++++ .../CONTRIBUTING.md | 119 ++ .../README.md | 210 +++ .../SeedCsharpGlobalHeaderLiteralEnv.slnx | 4 + .../Snippets/Example0.cs | 16 + .../Snippets/Snippets.csproj | 11 + .../reference.md | 41 + .../snippet.json | 17 + .../Core/HeadersBuilderTests.cs | 326 +++++ .../Core/Json/AdditionalPropertiesTests.cs | 365 +++++ .../Core/Json/DateOnlyJsonTests.cs | 100 ++ .../Core/Json/DateTimeJsonTests.cs | 134 ++ .../Core/Json/JsonAccessAttributeTests.cs | 160 +++ .../Core/QueryStringBuilderTests.cs | 672 +++++++++ .../Core/QueryStringConverterTests.cs | 158 +++ .../Core/RawClientTests/GzipResponseTests.cs | 104 ++ .../Core/RawClientTests/MultipartFormTests.cs | 1121 +++++++++++++++ .../RawClientTests/QueryParameterTests.cs | 108 ++ .../Core/RawClientTests/RetriesTests.cs | 540 +++++++ .../Core/WithRawResponseTests.cs | 269 ++++ ...rpGlobalHeaderLiteralEnv.Test.Custom.props | 6 + ...edCsharpGlobalHeaderLiteralEnv.Test.csproj | 39 + .../TestClient.cs | 6 + .../Unit/MockServer/BaseMockServerTest.cs | 39 + .../GetWithLiteralVersionHeaderTest.cs | 30 + .../Utils/AdditionalPropertiesComparer.cs | 219 +++ .../Utils/JsonAssert.cs | 33 + .../Utils/JsonElementComparer.cs | 243 ++++ .../Utils/NUnitExtensions.cs | 32 + .../Utils/OneOfComparer.cs | 100 ++ .../Utils/OptionalComparer.cs | 104 ++ .../Utils/ReadOnlyMemoryComparer.cs | 87 ++ .../Core/ApiResponse.cs | 13 + .../Core/BaseRequest.cs | 67 + .../Core/CollectionItemSerializer.cs | 91 ++ .../Core/Constants.cs | 7 + .../Core/DateOnlyConverter.cs | 747 ++++++++++ .../Core/DateTimeSerializer.cs | 40 + .../Core/DefaultHttpClientFactory.cs | 25 + .../Core/EmptyRequest.cs | 11 + .../Core/EncodingCache.cs | 11 + .../Core/Extensions.cs | 55 + .../Core/FormUrlEncoder.cs | 33 + .../Core/HeaderValue.cs | 52 + .../Core/Headers.cs | 28 + .../Core/HeadersBuilder.cs | 197 +++ .../Core/HttpContentExtensions.cs | 20 + .../Core/HttpMethodExtensions.cs | 8 + .../Core/IIsRetryableContent.cs | 6 + .../Core/IRequestOptions.cs | 83 ++ .../Core/JsonAccessAttribute.cs | 15 + .../Core/JsonConfiguration.cs | 275 ++++ .../Core/JsonRequest.cs | 36 + .../Core/MultipartFormRequest.cs | 294 ++++ .../Core/NullableAttribute.cs | 18 + .../Core/OneOfSerializer.cs | 190 +++ .../Core/Optional.cs | 474 +++++++ .../Core/OptionalAttribute.cs | 17 + .../Core/Public/AdditionalProperties.cs | 353 +++++ .../Core/Public/ClientOptions.cs | 92 ++ .../Core/Public/FileParameter.cs | 63 + .../Core/Public/RawResponse.cs | 24 + .../Core/Public/RequestOptions.cs | 94 ++ ...sharpGlobalHeaderLiteralEnvApiException.cs | 28 + ...edCsharpGlobalHeaderLiteralEnvException.cs | 9 + .../Core/Public/Version.cs | 7 + .../Core/Public/WithRawResponse.cs | 18 + .../Core/Public/WithRawResponseStream.cs | 66 + .../Core/Public/WithRawResponseTask.cs | 187 +++ .../Core/QueryStringBuilder.cs | 656 +++++++++ .../Core/QueryStringConverter.cs | 259 ++++ .../Core/RawClient.cs | 364 +++++ .../Core/RawResponse.cs | 24 + .../Core/ResponseHeaders.cs | 108 ++ .../Core/StreamRequest.cs | 29 + .../Core/StringEnum.cs | 6 + .../Core/StringEnumExtensions.cs | 6 + .../Core/ValueConvert.cs | 115 ++ ...ISeedCsharpGlobalHeaderLiteralEnvClient.cs | 6 + ...dCsharpGlobalHeaderLiteralEnv.Custom.props | 20 + .../SeedCsharpGlobalHeaderLiteralEnv.csproj | 68 + .../SeedCsharpGlobalHeaderLiteralEnvClient.cs | 60 + .../Service/IServiceClient.cs | 12 + .../Service/ServiceClient.cs | 112 ++ .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../package-id/src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedObject/SeedObjectClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/Contoso.Net/Contoso.cs | 2 +- .../src/Contoso.Net/Contoso.cs | 2 +- .../src/Contoso.Net/ContosoClient.cs | 2 +- .../src/Seed.CsharpNamespaceConflict/Seed.cs | 2 +- .../SeedCsharpOauthTokenOptionalClient.cs | 2 +- .../SeedCsharpPathParamOrderClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedCsharpReadonlyRequestClient.cs | 2 +- .../src/SeedCsharpSystemCollision/System.cs | 2 +- .../SeedCsharpUnionBasePropertiesClient.cs | 2 +- .../SeedCsharpXmlEntitiesClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedDollarStringExamplesClient.cs | 2 +- .../SeedEmptyClientsClient.cs | 2 +- .../SeedEndpointSecurityAuthClient.cs | 2 +- .../src/SeedEnum/SeedEnumClient.cs | 2 +- .../src/SeedEnum/SeedEnumClient.cs | 2 +- .../SeedErrorPropertyClient.cs | 2 +- .../errors/src/SeedErrors/SeedErrorsClient.cs | 2 +- .../src/SeedExamples/SeedExamplesClient.cs | 2 +- .../src/SeedExamples/SeedExamplesClient.cs | 2 +- .../SeedExhaustive/SeedExhaustiveClient.cs | 2 +- .../SeedExhaustive/SeedExhaustiveClient.cs | 2 +- .../SeedExhaustive/SeedExhaustiveClient.cs | 2 +- .../SeedExhaustive/SeedExhaustiveClient.cs | 2 +- .../SeedExhaustive/SeedExhaustiveClient.cs | 2 +- .../SeedExhaustive/SeedExhaustiveClient.cs | 2 +- .../SeedExhaustive/SeedExhaustiveClient.cs | 2 +- .../SeedExhaustive/SeedExhaustiveClient.cs | 2 +- .../src/SeedExtends/SeedExtendsClient.cs | 2 +- .../SeedExtraPropertiesClient.cs | 2 +- .../SeedFileDownloadClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedFileUpload/SeedFileUploadClient.cs | 2 +- .../folders/src/SeedApi/SeedApiClient.cs | 2 +- ...eedHeaderTokenEnvironmentVariableClient.cs | 5 +- .../SeedHeaderToken/SeedHeaderTokenClient.cs | 2 +- .../src/SeedHttpHead/SeedHttpHeadClient.cs | 2 +- .../SeedIdempotencyHeadersClient.cs | 2 +- .../SeedIdempotencyHeadersClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 4 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/BaseClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 4 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedInferredAuthExplicitClient.cs | 2 +- .../SeedInferredAuthImplicitApiKeyClient.cs | 2 +- .../SeedInferredAuthImplicitNoExpiryClient.cs | 2 +- .../SeedInferredAuthImplicitClient.cs | 2 +- .../SeedInferredAuthImplicitClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedLicense/SeedLicenseClient.cs | 2 +- .../src/SeedLicense/SeedLicenseClient.cs | 2 +- .../SeedLiteralUserAgentClient.cs | 2 +- .../src/SeedLiteral/SeedLiteralClient.cs | 2 +- .../src/SeedLiteral/SeedLiteralClient.cs | 2 +- .../SeedLiteralsUnionsClient.cs | 2 +- .../src/SeedMixedCase/SeedMixedCaseClient.cs | 2 +- .../SeedMixedFileDirectoryClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedMultiLineDocsClient.cs | 2 +- .../SeedMultiUrlEnvironmentNoDefaultClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedMultiUrlEnvironmentClient.cs | 2 +- .../SeedMultiUrlEnvironmentClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedNoEnvironmentClient.cs | 2 +- .../src/SeedNoRetries/SeedNoRetriesClient.cs | 2 +- .../null-type/src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedNullableOptionalClient.cs | 2 +- .../SeedNullableOptionalClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedNullable/SeedNullableClient.cs | 2 +- .../src/SeedNullable/SeedNullableClient.cs | 2 +- .../SeedOauthClientCredentialsClient.cs | 2 +- ...SeedOauthClientCredentialsDefaultClient.cs | 2 +- ...ntCredentialsEnvironmentVariablesClient.cs | 5 +- ...uthClientCredentialsMandatoryAuthClient.cs | 5 +- ...uthClientCredentialsMandatoryAuthClient.cs | 5 +- .../SeedOauthClientCredentialsClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- ...edOauthClientCredentialsReferenceClient.cs | 5 +- ...uthClientCredentialsWithVariablesClient.cs | 5 +- .../SeedOauthClientCredentialsClient.cs | 2 +- .../SeedOauthClientCredentialsClient.cs | 2 +- .../src/SeedOauthPkce/SeedOauthPkceClient.cs | 2 +- .../object/src/SeedObject/SeedObjectClient.cs | 2 +- .../SeedObjectsWithImportsClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedObjectsWithImportsClient.cs | 2 +- .../SeedObjectsWithImportsClient.cs | 2 +- .../SeedPackageYml/SeedPackageYmlClient.cs | 2 +- .../SeedPagination/SeedPaginationClient.cs | 2 +- .../SeedPaginationUriPathClient.cs | 2 +- .../SeedPagination/SeedPaginationClient.cs | 2 +- .../SeedPagination/SeedPaginationClient.cs | 2 +- .../SeedPagination/SeedPaginationClient.cs | 2 +- .../SeedPagination/SeedPaginationClient.cs | 2 +- .../SeedPathParametersClient.cs | 2 +- .../SeedPathParametersClient.cs | 2 +- .../src/SeedPlainText/SeedPlainTextClient.cs | 2 +- .../SeedPropertyAccessClient.cs | 2 +- .../SeedPublicObjectClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedQueryParametersClient.cs | 2 +- .../SeedRequestParametersClient.cs | 2 +- .../SeedRequestParametersClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedNurseryApi/SeedNurseryApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedResponsePropertyClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedServerSentEventsClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedServerSentEventsResumableClient.cs | 2 +- .../SeedServerSentEventsClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedSimpleApi/SeedSimpleApiClient.cs | 2 +- .../SeedSimpleApi/SeedSimpleApiClient.cs | 2 +- .../src/SeedSimpleApi/SeedSimpleApiClient.cs | 2 +- .../src/SeedSimpleApi/SeedSimpleApiClient.cs | 2 +- .../simple-fhir/src/SeedApi/SeedApiClient.cs | 2 +- .../SeedSingleUrlEnvironmentDefaultClient.cs | 2 +- ...SeedSingleUrlEnvironmentNoDefaultClient.cs | 2 +- .../src/SeedStreaming/SeedStreamingClient.cs | 2 +- .../src/SeedStreaming/SeedStreamingClient.cs | 2 +- .../src/SeedStreaming/SeedStreamingClient.cs | 2 +- .../trace/src/SeedTrace/SeedTraceClient.cs | 2 +- ...iminatedUnionWithResponsePropertyClient.cs | 5 +- .../SeedUndiscriminatedUnionsClient.cs | 2 +- .../SeedUndiscriminatedUnionsClient.cs | 2 +- .../SeedUnionQueryParametersClient.cs | 2 +- .../src/SeedUnions/SeedUnionsClient.cs | 2 +- .../src/SeedUnions/SeedUnionsClient.cs | 2 +- .../src/SeedUnions/SeedUnionsClient.cs | 2 +- .../SeedUnknownAsAnyClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../SeedValidation/SeedValidationClient.cs | 2 +- .../src/SeedVariables/SeedVariablesClient.cs | 2 +- .../src/SeedVersion/SeedVersionClient.cs | 2 +- .../src/SeedVersion/SeedVersionClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedWebhooks/SeedWebhooksClient.cs | 2 +- .../SeedWebsocketBearerAuthClient.cs | 2 +- .../SeedWebsocketAuthClient.cs | 2 +- .../SeedWebsocketMultiUrlClient.cs | 2 +- .../src/SeedWebsocket/SeedWebsocketClient.cs | 2 +- .../src/SeedWebsocket/SeedWebsocketClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../src/SeedApi/SeedApiClient.cs | 2 +- .../.fern/metadata.json | 9 + .../.github/workflows/ci.yml | 52 + .../php-global-header-literal-env/.gitignore | 5 + .../CONTRIBUTING.md | 120 ++ .../php-global-header-literal-env/README.md | 146 ++ .../composer.json | 46 + .../phpstan.neon | 6 + .../php-global-header-literal-env/phpunit.xml | 7 + .../reference.md | 41 + .../src/Core/Client/BaseApiRequest.php | 22 + .../src/Core/Client/HttpClientBuilder.php | 56 + .../src/Core/Client/HttpMethod.php | 13 + .../src/Core/Client/MockHttpClient.php | 75 + .../src/Core/Client/RawClient.php | 359 +++++ .../src/Core/Client/RetryDecoratingClient.php | 241 ++++ .../src/Core/Client/UrlEncodedApiRequest.php | 25 + .../src/Core/Json/JsonApiRequest.php | 28 + .../src/Core/Json/JsonDecoder.php | 161 +++ .../src/Core/Json/JsonDeserializer.php | 218 +++ .../src/Core/Json/JsonEncoder.php | 20 + .../src/Core/Json/JsonProperty.php | 13 + .../src/Core/Json/JsonSerializableType.php | 226 +++ .../src/Core/Json/JsonSerializer.php | 212 +++ .../src/Core/Json/Utils.php | 62 + .../Core/Multipart/MultipartApiRequest.php | 28 + .../src/Core/Multipart/MultipartFormData.php | 58 + .../Core/Multipart/MultipartFormDataPart.php | 62 + .../src/Core/Types/ArrayType.php | 16 + .../src/Core/Types/Constant.php | 12 + .../src/Core/Types/Date.php | 16 + .../src/Core/Types/Union.php | 62 + .../src/Exceptions/SeedApiException.php | 53 + .../src/Exceptions/SeedException.php | 12 + .../src/SeedClient.php | 85 ++ .../src/Service/ServiceClient.php | 102 ++ .../src/Utils/File.php | 129 ++ .../src/dynamic-snippets/example0/snippet.php | 14 + .../tests/Core/Client/RawClientTest.php | 1249 +++++++++++++++++ .../Core/Json/AdditionalPropertiesTest.php | 76 + .../tests/Core/Json/DateArrayTest.php | 54 + .../tests/Core/Json/EmptyArrayTest.php | 78 + .../tests/Core/Json/EmptyObjectTest.php | 409 ++++++ .../tests/Core/Json/EnumTest.php | 77 + .../tests/Core/Json/ExhaustiveTest.php | 197 +++ .../tests/Core/Json/InvalidTest.php | 42 + .../tests/Core/Json/NestedUnionArrayTest.php | 89 ++ .../tests/Core/Json/NullPropertyTest.php | 53 + .../tests/Core/Json/NullableArrayTest.php | 49 + .../tests/Core/Json/ScalarTest.php | 116 ++ .../tests/Core/Json/TraitTest.php | 60 + .../tests/Core/Json/UnionArrayTest.php | 57 + .../tests/Core/Json/UnionPropertyTest.php | 109 ++ .../definition/api.yml | 13 + .../definition/service.yml | 9 + .../generators.yml | 2 + .../definition/api.yml | 13 + .../definition/service.yml | 9 + .../generators.yml | 2 + 353 files changed, 18635 insertions(+), 220 deletions(-) create mode 100644 generators/csharp/sdk/changes/unreleased/fix-literal-global-header-env-fallback.yml create mode 100644 generators/php/sdk/changes/unreleased/fix-literal-global-header-env-fallback.yml create mode 100644 packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-literal-env.json create mode 100644 packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/php-global-header-literal-env.json create mode 100644 packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-literal-env.json create mode 100644 packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/php-global-header-literal-env.json create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/.editorconfig create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/.fern/metadata.json create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/.github/workflows/ci.yml create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/.gitignore create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/CONTRIBUTING.md create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/README.md create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/SeedCsharpGlobalHeaderLiteralEnv.slnx create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/Snippets/Example0.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/Snippets/Snippets.csproj create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/reference.md create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/snippet.json create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/HeadersBuilderTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/AdditionalPropertiesTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/DateOnlyJsonTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/DateTimeJsonTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/JsonAccessAttributeTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/QueryStringBuilderTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/QueryStringConverterTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/GzipResponseTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/MultipartFormTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/QueryParameterTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/RetriesTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/WithRawResponseTests.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.Custom.props create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.csproj create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/TestClient.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Unit/MockServer/BaseMockServerTest.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Unit/MockServer/Service/GetWithLiteralVersionHeaderTest.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/AdditionalPropertiesComparer.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/JsonAssert.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/JsonElementComparer.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/NUnitExtensions.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/OneOfComparer.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/OptionalComparer.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/ReadOnlyMemoryComparer.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ApiResponse.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/BaseRequest.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/CollectionItemSerializer.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Constants.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DateOnlyConverter.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DateTimeSerializer.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DefaultHttpClientFactory.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/EmptyRequest.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/EncodingCache.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Extensions.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/FormUrlEncoder.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HeaderValue.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Headers.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HeadersBuilder.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HttpContentExtensions.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HttpMethodExtensions.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/IIsRetryableContent.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/IRequestOptions.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonAccessAttribute.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonConfiguration.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonRequest.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/MultipartFormRequest.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/NullableAttribute.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/OneOfSerializer.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Optional.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/OptionalAttribute.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/AdditionalProperties.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/ClientOptions.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/FileParameter.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/RawResponse.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/RequestOptions.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/SeedCsharpGlobalHeaderLiteralEnvApiException.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/SeedCsharpGlobalHeaderLiteralEnvException.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/Version.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponse.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponseStream.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponseTask.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/QueryStringBuilder.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/QueryStringConverter.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/RawClient.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/RawResponse.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ResponseHeaders.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StreamRequest.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StringEnum.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StringEnumExtensions.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ValueConvert.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/ISeedCsharpGlobalHeaderLiteralEnvClient.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.Custom.props create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.csproj create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnvClient.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Service/IServiceClient.cs create mode 100644 seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Service/ServiceClient.cs create mode 100644 seed/php-sdk/php-global-header-literal-env/.fern/metadata.json create mode 100644 seed/php-sdk/php-global-header-literal-env/.github/workflows/ci.yml create mode 100644 seed/php-sdk/php-global-header-literal-env/.gitignore create mode 100644 seed/php-sdk/php-global-header-literal-env/CONTRIBUTING.md create mode 100644 seed/php-sdk/php-global-header-literal-env/README.md create mode 100644 seed/php-sdk/php-global-header-literal-env/composer.json create mode 100644 seed/php-sdk/php-global-header-literal-env/phpstan.neon create mode 100644 seed/php-sdk/php-global-header-literal-env/phpunit.xml create mode 100644 seed/php-sdk/php-global-header-literal-env/reference.md create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Client/BaseApiRequest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Client/HttpClientBuilder.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Client/HttpMethod.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Client/MockHttpClient.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Client/RawClient.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Client/RetryDecoratingClient.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Client/UrlEncodedApiRequest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonApiRequest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonDecoder.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonDeserializer.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonEncoder.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonProperty.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonSerializableType.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonSerializer.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Json/Utils.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartApiRequest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartFormData.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartFormDataPart.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Types/ArrayType.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Types/Constant.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Types/Date.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Core/Types/Union.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Exceptions/SeedApiException.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Exceptions/SeedException.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/SeedClient.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Service/ServiceClient.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/Utils/File.php create mode 100644 seed/php-sdk/php-global-header-literal-env/src/dynamic-snippets/example0/snippet.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Client/RawClientTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/AdditionalPropertiesTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/DateArrayTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EmptyArrayTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EmptyObjectTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EnumTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/ExhaustiveTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/InvalidTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NestedUnionArrayTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NullPropertyTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NullableArrayTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/ScalarTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/TraitTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/UnionArrayTest.php create mode 100644 seed/php-sdk/php-global-header-literal-env/tests/Core/Json/UnionPropertyTest.php create mode 100644 test-definitions/fern/apis/csharp-global-header-literal-env/definition/api.yml create mode 100644 test-definitions/fern/apis/csharp-global-header-literal-env/definition/service.yml create mode 100644 test-definitions/fern/apis/csharp-global-header-literal-env/generators.yml create mode 100644 test-definitions/fern/apis/php-global-header-literal-env/definition/api.yml create mode 100644 test-definitions/fern/apis/php-global-header-literal-env/definition/service.yml create mode 100644 test-definitions/fern/apis/php-global-header-literal-env/generators.yml diff --git a/generators/csharp/base/src/context/GeneratorContext.ts b/generators/csharp/base/src/context/GeneratorContext.ts index 8cc55034343f..64d639f47a3b 100644 --- a/generators/csharp/base/src/context/GeneratorContext.ts +++ b/generators/csharp/base/src/context/GeneratorContext.ts @@ -522,11 +522,24 @@ export abstract class GeneratorContext extends AbstractGeneratorContext { }); } - public getCurrentVersionValueAccess(): ast.CodeBlock { + /** + * @param inInterpolatedString wraps the access in parentheses, required inside an + * interpolated string hole where an unparenthesized `::` would otherwise start a + * format specifier + */ + public getCurrentVersionValueAccess({ inInterpolatedString = false } = {}): ast.CodeBlock { return this.csharp.codeblock((writer) => { - writer.writeNode(this.Types.Version); + if (inInterpolatedString) { + writer.write("("); + } + // qualify globally so the reference cannot be shadowed by a constructor + // parameter or local named `Version` + writer.writeNode(this.Types.Version.asGloballyQualified()); writer.write("."); writer.write(this.model.getPropertyNameFor(this.Types.Version.explicit("Current"))); + if (inInterpolatedString) { + writer.write(")"); + } }); } diff --git a/generators/csharp/codegen/src/ast/types/ClassReference.ts b/generators/csharp/codegen/src/ast/types/ClassReference.ts index f1e26ec406ac..e91207f1f868 100644 --- a/generators/csharp/codegen/src/ast/types/ClassReference.ts +++ b/generators/csharp/codegen/src/ast/types/ClassReference.ts @@ -480,6 +480,18 @@ export class ClassReference extends Node implements Type { }); } + /** + * returns this class reference as a fully qualified class reference with the `global::` + * alias, making it immune to shadowing by locals, parameters or members in scope + */ + public asGloballyQualified() { + return this.csharp.classReferenceInternal({ + ...this, + fullyQualified: true, + global: true + }); + } + /** returns a class instantiation node for this class reference */ public new(args?: Omit) { args = args ?? { arguments_: [] }; diff --git a/generators/csharp/sdk/changes/unreleased/fix-literal-global-header-env-fallback.yml b/generators/csharp/sdk/changes/unreleased/fix-literal-global-header-env-fallback.yml new file mode 100644 index 000000000000..416e2d49ae47 --- /dev/null +++ b/generators/csharp/sdk/changes/unreleased/fix-literal-global-header-env-fallback.yml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json + +- summary: | + Global headers typed as a `literal<"...">` with an `env` fallback no longer throw when + neither the constructor parameter nor the environment variable is set. The literal is + known at compile time, so it is now used as the header's default value. + type: fix + +- summary: | + The generated `X-Fern-SDK-Version` header value is now emitted as a globally qualified + reference, so it can no longer be shadowed by a constructor parameter or local that + PascalCases to `Version` (previously `error CS1061: 'string' does not contain a + definition for 'Current'`). + type: fix diff --git a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts index 1f9c1a76fa65..787cfa6f3424 100644 --- a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts @@ -83,6 +83,17 @@ interface HeaderInfo { prefix?: string; } +/** The compile-time value of a `literal<"...">`-typed global header, if this parameter is one. */ +function getLiteralHeaderValue(param: ConstructorParameter): Literal | undefined { + if (!param.isGlobalHeader) { + return undefined; + } + const { typeReference } = param; + return typeReference.type === "container" && typeReference.container.type === "literal" + ? typeReference.container.literal + : undefined; +} + export class RootClientGenerator extends FileGenerator { private rawClient: RawClient; private serviceId: ServiceId | undefined; @@ -474,7 +485,7 @@ export class RootClientGenerator extends FileGenerator` with an `env` fallback no longer throw when + neither the constructor parameter nor the environment variable is set. The literal is + known at compile time, so it is now used as the header's default value. + type: fix diff --git a/generators/php/sdk/src/root-client/RootClientGenerator.ts b/generators/php/sdk/src/root-client/RootClientGenerator.ts index 064feb221acc..b96362197ff1 100644 --- a/generators/php/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/php/sdk/src/root-client/RootClientGenerator.ts @@ -1464,6 +1464,10 @@ export class RootClientGenerator extends FileGenerator + RawResponse) +// and on endpoints with no response body (returns RawResponse only). +``` + +### Additional Headers + +If you would like to send additional headers as part of the request, use the `AdditionalHeaders` request option. + +```csharp +var response = await client.Service.GetWithLiteralVersionHeaderAsync( + ..., + new RequestOptions { + AdditionalHeaders = new Dictionary + { + { "X-Custom-Header", "custom-value" } + } + } +); +``` + +### Additional Query Parameters + +If you would like to send additional query parameters as part of the request, use the `AdditionalQueryParameters` request option. + +```csharp +var response = await client.Service.GetWithLiteralVersionHeaderAsync( + ..., + new RequestOptions { + AdditionalQueryParameters = new Dictionary + { + { "custom_param", "custom-value" } + } + } +); +``` + +### Additional Body Properties + +If you would like to send additional body properties as part of the request, use the `AdditionalBodyProperties` request option. +This is only applied to JSON requests. + +```csharp +var response = await client.Service.GetWithLiteralVersionHeaderAsync( + ..., + new RequestOptions { + AdditionalBodyProperties = new Dictionary + { + { "custom_field", "custom-value" } + } + } +); +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/SeedCsharpGlobalHeaderLiteralEnv.slnx b/seed/csharp-sdk/csharp-global-header-literal-env/SeedCsharpGlobalHeaderLiteralEnv.slnx new file mode 100644 index 000000000000..0d6598ef642e --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/SeedCsharpGlobalHeaderLiteralEnv.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/Snippets/Example0.cs b/seed/csharp-sdk/csharp-global-header-literal-env/Snippets/Example0.cs new file mode 100644 index 000000000000..a00f8b356887 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/Snippets/Example0.cs @@ -0,0 +1,16 @@ +using SeedCsharpGlobalHeaderLiteralEnv; + +public partial class Examples +{ + public async Task Example0() { + var client = new SeedCsharpGlobalHeaderLiteralEnvClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Service.GetWithLiteralVersionHeaderAsync(); + } + +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/Snippets/Snippets.csproj b/seed/csharp-sdk/csharp-global-header-literal-env/Snippets/Snippets.csproj new file mode 100644 index 000000000000..8d1c06d60829 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/Snippets/Snippets.csproj @@ -0,0 +1,11 @@ + + + net10.0 + Snippets + enable + enable + + + + + \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/reference.md b/seed/csharp-sdk/csharp-global-header-literal-env/reference.md new file mode 100644 index 000000000000..a6d3da788b66 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/reference.md @@ -0,0 +1,41 @@ +# Reference +## Service +
client.Service.GetWithLiteralVersionHeaderAsync() -> WithRawResponseTask<string> +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET request with a literal version header +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Service.GetWithLiteralVersionHeaderAsync(); +``` +
+
+
+
+ + +
+
+
+ diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/snippet.json b/seed/csharp-sdk/csharp-global-header-literal-env/snippet.json new file mode 100644 index 000000000000..f67064e52131 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/snippet.json @@ -0,0 +1,17 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": null, + "id": { + "path": "/version", + "method": "GET", + "identifier_override": "endpoint_service.getWithLiteralVersionHeader" + }, + "snippet": { + "type": "csharp", + "client": "using SeedCsharpGlobalHeaderLiteralEnv;\n\nvar client = new SeedCsharpGlobalHeaderLiteralEnvClient(\"TOKEN\", \"VERSION\");\nawait client.Service.GetWithLiteralVersionHeaderAsync();\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/HeadersBuilderTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/HeadersBuilderTests.cs new file mode 100644 index 000000000000..bf3c084c3be5 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/HeadersBuilderTests.cs @@ -0,0 +1,326 @@ +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core; + +[TestFixture] +public class HeadersBuilderTests +{ + [Test] + public async global::System.Threading.Tasks.Task Add_SimpleHeaders() + { + var headers = await new HeadersBuilder.Builder() + .Add("Content-Type", "application/json") + .Add("Authorization", "Bearer token123") + .Add("X-API-Key", "key456") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(3)); + Assert.That(headers["Content-Type"], Is.EqualTo("application/json")); + Assert.That(headers["Authorization"], Is.EqualTo("Bearer token123")); + Assert.That(headers["X-API-Key"], Is.EqualTo("key456")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_NullValuesIgnored() + { + var headers = await new HeadersBuilder.Builder() + .Add("Header1", "value1") + .Add("Header2", null) + .Add("Header3", "value3") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(2)); + Assert.That(headers.ContainsKey("Header1"), Is.True); + Assert.That(headers.ContainsKey("Header2"), Is.False); + Assert.That(headers.ContainsKey("Header3"), Is.True); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_OverwritesExistingHeader() + { + var headers = await new HeadersBuilder.Builder() + .Add("Content-Type", "application/json") + .Add("Content-Type", "application/xml") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(1)); + Assert.That(headers["Content-Type"], Is.EqualTo("application/xml")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_MergesExistingHeaders() + { + var existingHeaders = new Headers( + new Dictionary { { "Header1", "value1" }, { "Header2", "value2" } } + ); + + var result = await new HeadersBuilder.Builder() + .Add("Header3", "value3") + .Add(existingHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result["Header1"], Is.EqualTo("value1")); + Assert.That(result["Header2"], Is.EqualTo("value2")); + Assert.That(result["Header3"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_OverwritesExistingHeaders() + { + var existingHeaders = new Headers( + new Dictionary { { "Header1", "override" } } + ); + + var result = await new HeadersBuilder.Builder() + .Add("Header1", "original") + .Add("Header2", "keep") + .Add(existingHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result["Header1"], Is.EqualTo("override")); + Assert.That(result["Header2"], Is.EqualTo("keep")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_NullHeadersIgnored() + { + var result = await new HeadersBuilder.Builder() + .Add("Header1", "value1") + .Add((Headers?)null) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result["Header1"], Is.EqualTo("value1")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_KeyValuePairOverload_AddsHeaders() + { + var additionalHeaders = new List> + { + new("Header1", "value1"), + new("Header2", "value2"), + }; + + var headers = await new HeadersBuilder.Builder() + .Add("Header3", "value3") + .Add(additionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(3)); + Assert.That(headers["Header1"], Is.EqualTo("value1")); + Assert.That(headers["Header2"], Is.EqualTo("value2")); + Assert.That(headers["Header3"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_KeyValuePairOverload_IgnoresNullValues() + { + var additionalHeaders = new List> + { + new("Header1", "value1"), + new("Header2", null), // Should be ignored + }; + + var headers = await new HeadersBuilder.Builder() + .Add(additionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(1)); + Assert.That(headers.ContainsKey("Header2"), Is.False); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_DictionaryOverload_AddsHeaders() + { + var dict = new Dictionary + { + { "Header1", "value1" }, + { "Header2", "value2" }, + }; + + var headers = await new HeadersBuilder.Builder() + .Add("Header3", "value3") + .Add(dict) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(3)); + Assert.That(headers["Header1"], Is.EqualTo("value1")); + Assert.That(headers["Header2"], Is.EqualTo("value2")); + Assert.That(headers["Header3"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task EmptyBuilder_ReturnsEmptyHeaders() + { + var headers = await new HeadersBuilder.Builder().BuildAsync().ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(0)); + } + + [Test] + public async global::System.Threading.Tasks.Task OnlyNullValues_ReturnsEmptyHeaders() + { + var headers = await new HeadersBuilder.Builder() + .Add("Header1", null) + .Add("Header2", null) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(0)); + } + + [Test] + public async global::System.Threading.Tasks.Task ComplexMergingScenario() + { + // Simulates real SDK usage: endpoint headers + client headers + request options + var clientHeaders = new Headers( + new Dictionary + { + { "X-Client-Version", "1.0.0" }, + { "User-Agent", "MyClient/1.0" }, + } + ); + + var clientAdditionalHeaders = new List> + { + new("X-Custom-Header", "custom-value"), + }; + + var requestOptionsHeaders = new Headers( + new Dictionary + { + { "Authorization", "Bearer user-token" }, + { "User-Agent", "MyClient/2.0" }, // Override + } + ); + + var requestAdditionalHeaders = new List> + { + new("X-Request-ID", "req-123"), + new("X-Custom-Header", "overridden-value"), // Override + }; + + var headers = await new HeadersBuilder.Builder() + .Add("Content-Type", "application/json") // Endpoint header + .Add("X-Endpoint-ID", "endpoint-1") + .Add(clientHeaders) + .Add(clientAdditionalHeaders) + .Add(requestOptionsHeaders) + .Add(requestAdditionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + + // Verify precedence + Assert.That(headers["Content-Type"], Is.EqualTo("application/json")); + Assert.That(headers["X-Endpoint-ID"], Is.EqualTo("endpoint-1")); + Assert.That(headers["X-Client-Version"], Is.EqualTo("1.0.0")); + Assert.That(headers["User-Agent"], Is.EqualTo("MyClient/2.0")); // Overridden + Assert.That(headers["Authorization"], Is.EqualTo("Bearer user-token")); + Assert.That(headers["X-Request-ID"], Is.EqualTo("req-123")); + Assert.That(headers["X-Custom-Header"], Is.EqualTo("overridden-value")); // Overridden + } + + [Test] + public async global::System.Threading.Tasks.Task Builder_WithCapacity() + { + // Test that capacity constructor works without errors + var headers = await new HeadersBuilder.Builder(capacity: 10) + .Add("Header1", "value1") + .Add("Header2", "value2") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(2)); + Assert.That(headers["Header1"], Is.EqualTo("value1")); + Assert.That(headers["Header2"], Is.EqualTo("value2")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_ResolvesDynamicHeaderValues() + { + // Test that BuildAsync properly resolves HeaderValue instances + var existingHeaders = new Headers(); + existingHeaders["DynamicHeader"] = + (Func>)( + () => global::System.Threading.Tasks.Task.FromResult("dynamic-value") + ); + + var result = await new HeadersBuilder.Builder() + .Add("StaticHeader", "static-value") + .Add(existingHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result["StaticHeader"], Is.EqualTo("static-value")); + Assert.That(result["DynamicHeader"], Is.EqualTo("dynamic-value")); + } + + [Test] + public async global::System.Threading.Tasks.Task MultipleSyncAdds() + { + var headers1 = new Headers(new Dictionary { { "H1", "v1" } }); + var headers2 = new Headers(new Dictionary { { "H2", "v2" } }); + var headers3 = new Headers(new Dictionary { { "H3", "v3" } }); + + var result = await new HeadersBuilder.Builder() + .Add(headers1) + .Add(headers2) + .Add(headers3) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result["H1"], Is.EqualTo("v1")); + Assert.That(result["H2"], Is.EqualTo("v2")); + Assert.That(result["H3"], Is.EqualTo("v3")); + } + + [Test] + public async global::System.Threading.Tasks.Task PrecedenceOrder_LatestWins() + { + // Test that later operations override earlier ones + var headers1 = new Headers(new Dictionary { { "Key", "value1" } }); + var headers2 = new Headers(new Dictionary { { "Key", "value2" } }); + var additional = new List> { new("Key", "value3") }; + + var result = await new HeadersBuilder.Builder() + .Add("Key", "value0") + .Add(headers1) + .Add(headers2) + .Add(additional) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result["Key"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task CaseInsensitiveKeys() + { + // Test that header keys are case-insensitive + var headers = await new HeadersBuilder.Builder() + .Add("content-type", "application/json") + .Add("Content-Type", "application/xml") // Should overwrite + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(1)); + Assert.That(headers["content-type"], Is.EqualTo("application/xml")); + Assert.That(headers["Content-Type"], Is.EqualTo("application/xml")); + Assert.That(headers["CONTENT-TYPE"], Is.EqualTo("application/xml")); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/AdditionalPropertiesTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/AdditionalPropertiesTests.cs new file mode 100644 index 000000000000..e66563619502 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/AdditionalPropertiesTests.cs @@ -0,0 +1,365 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core.Json; + +[TestFixture] +public class AdditionalPropertiesTests +{ + [Test] + public void Record_OnDeserialized_ShouldPopulateAdditionalProperties() + { + // Arrange + const string json = """ + { + "id": "1", + "category": "fiction", + "title": "The Hobbit" + } + """; + + // Act + var record = JsonUtils.Deserialize(json); + + // Assert + Assert.That(record, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(record.Id, Is.EqualTo("1")); + Assert.That(record.AdditionalProperties["category"].GetString(), Is.EqualTo("fiction")); + Assert.That(record.AdditionalProperties["title"].GetString(), Is.EqualTo("The Hobbit")); + }); + } + + [Test] + public void RecordWithWriteableAdditionalProperties_OnSerialization_ShouldIncludeAdditionalProperties() + { + // Arrange + var record = new WriteableRecord + { + Id = "1", + AdditionalProperties = { ["category"] = "fiction", ["title"] = "The Hobbit" }, + }; + + // Act + var json = JsonUtils.Serialize(record); + var deserializedRecord = JsonUtils.Deserialize(json); + + // Assert + Assert.That(deserializedRecord, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(deserializedRecord.Id, Is.EqualTo("1")); + Assert.That( + deserializedRecord.AdditionalProperties["category"], + Is.InstanceOf() + ); + Assert.That( + ((JsonElement)deserializedRecord.AdditionalProperties["category"]!).GetString(), + Is.EqualTo("fiction") + ); + Assert.That( + deserializedRecord.AdditionalProperties["title"], + Is.InstanceOf() + ); + Assert.That( + ((JsonElement)deserializedRecord.AdditionalProperties["title"]!).GetString(), + Is.EqualTo("The Hobbit") + ); + }); + } + + [Test] + public void ReadOnlyAdditionalProperties_ShouldRetrieveValuesCorrectly() + { + // Arrange + var extensionData = new Dictionary + { + ["key1"] = JsonUtils.SerializeToElement("value1"), + ["key2"] = JsonUtils.SerializeToElement(123), + }; + var readOnlyProps = new ReadOnlyAdditionalProperties(); + readOnlyProps.CopyFromExtensionData(extensionData); + + // Act & Assert + Assert.That(readOnlyProps["key1"].GetString(), Is.EqualTo("value1")); + Assert.That(readOnlyProps["key2"].GetInt32(), Is.EqualTo(123)); + } + + [Test] + public void AdditionalProperties_ShouldBehaveAsDictionary() + { + // Arrange + var additionalProps = new AdditionalProperties { ["key1"] = "value1", ["key2"] = 123 }; + + // Act + additionalProps["key3"] = true; + + // Assert + Assert.Multiple(() => + { + Assert.That(additionalProps["key1"], Is.EqualTo("value1")); + Assert.That(additionalProps["key2"], Is.EqualTo(123)); + Assert.That((bool)additionalProps["key3"]!, Is.True); + Assert.That(additionalProps.Count, Is.EqualTo(3)); + }); + } + + [Test] + public void AdditionalProperties_ToJsonObject_ShouldSerializeCorrectly() + { + // Arrange + var additionalProps = new AdditionalProperties { ["key1"] = "value1", ["key2"] = 123 }; + + // Act + var jsonObject = additionalProps.ToJsonObject(); + + Assert.Multiple(() => + { + // Assert + Assert.That(jsonObject["key1"]!.GetValue(), Is.EqualTo("value1")); + Assert.That(jsonObject["key2"]!.GetValue(), Is.EqualTo(123)); + }); + } + + [Test] + public void AdditionalProperties_MixReadAndWrite_ShouldOverwriteDeserializedProperty() + { + // Arrange + const string json = """ + { + "id": "1", + "category": "fiction", + "title": "The Hobbit" + } + """; + var record = JsonUtils.Deserialize(json); + + // Act + record.AdditionalProperties["category"] = "non-fiction"; + + // Assert + Assert.Multiple(() => + { + Assert.That(record, Is.Not.Null); + Assert.That(record.Id, Is.EqualTo("1")); + Assert.That(record.AdditionalProperties["category"], Is.EqualTo("non-fiction")); + Assert.That(record.AdditionalProperties["title"], Is.InstanceOf()); + Assert.That( + ((JsonElement)record.AdditionalProperties["title"]!).GetString(), + Is.EqualTo("The Hobbit") + ); + }); + } + + [Test] + public void RecordWithReadonlyAdditionalPropertiesInts_OnDeserialized_ShouldPopulateAdditionalProperties() + { + // Arrange + const string json = """ + { + "extra1": 42, + "extra2": 99 + } + """; + + // Act + var record = JsonUtils.Deserialize(json); + + // Assert + Assert.That(record, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(record.AdditionalProperties["extra1"], Is.EqualTo(42)); + Assert.That(record.AdditionalProperties["extra2"], Is.EqualTo(99)); + }); + } + + [Test] + public void RecordWithAdditionalPropertiesInts_OnSerialization_ShouldIncludeAdditionalProperties() + { + // Arrange + var record = new WriteableRecordWithInts + { + AdditionalProperties = { ["extra1"] = 42, ["extra2"] = 99 }, + }; + + // Act + var json = JsonUtils.Serialize(record); + var deserializedRecord = JsonUtils.Deserialize(json); + + // Assert + Assert.That(deserializedRecord, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(deserializedRecord.AdditionalProperties["extra1"], Is.EqualTo(42)); + Assert.That(deserializedRecord.AdditionalProperties["extra2"], Is.EqualTo(99)); + }); + } + + [Test] + public void RecordWithReadonlyAdditionalPropertiesDictionaries_OnDeserialized_ShouldPopulateAdditionalProperties() + { + // Arrange + const string json = """ + { + "extra1": { "key1": true, "key2": false }, + "extra2": { "key3": true } + } + """; + + // Act + var record = JsonUtils.Deserialize(json); + + // Assert + Assert.That(record, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(record.AdditionalProperties["extra1"]["key1"], Is.True); + Assert.That(record.AdditionalProperties["extra1"]["key2"], Is.False); + Assert.That(record.AdditionalProperties["extra2"]["key3"], Is.True); + }); + } + + [Test] + public void RecordWithAdditionalPropertiesDictionaries_OnSerialization_ShouldIncludeAdditionalProperties() + { + // Arrange + var record = new WriteableRecordWithDictionaries + { + AdditionalProperties = + { + ["extra1"] = new Dictionary { { "key1", true }, { "key2", false } }, + ["extra2"] = new Dictionary { { "key3", true } }, + }, + }; + + // Act + var json = JsonUtils.Serialize(record); + var deserializedRecord = JsonUtils.Deserialize(json); + + // Assert + Assert.That(deserializedRecord, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(deserializedRecord.AdditionalProperties["extra1"]["key1"], Is.True); + Assert.That(deserializedRecord.AdditionalProperties["extra1"]["key2"], Is.False); + Assert.That(deserializedRecord.AdditionalProperties["extra2"]["key3"], Is.True); + }); + } + + private record Record : IJsonOnDeserialized + { + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + } + + private record WriteableRecord : IJsonOnDeserialized, IJsonOnSerializing + { + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public AdditionalProperties AdditionalProperties { get; set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + + void IJsonOnSerializing.OnSerializing() + { + AdditionalProperties.CopyToExtensionData(_extensionData); + } + } + + private record RecordWithInts : IJsonOnDeserialized + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + } + + private record WriteableRecordWithInts : IJsonOnDeserialized, IJsonOnSerializing + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public AdditionalProperties AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + + void IJsonOnSerializing.OnSerializing() + { + AdditionalProperties.CopyToExtensionData(_extensionData); + } + } + + private record RecordWithDictionaries : IJsonOnDeserialized + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties< + Dictionary + > AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + } + + private record WriteableRecordWithDictionaries : IJsonOnDeserialized, IJsonOnSerializing + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public AdditionalProperties> AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + + void IJsonOnSerializing.OnSerializing() + { + AdditionalProperties.CopyToExtensionData(_extensionData); + } + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/DateOnlyJsonTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/DateOnlyJsonTests.cs new file mode 100644 index 000000000000..312beb60de46 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/DateOnlyJsonTests.cs @@ -0,0 +1,100 @@ +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core.Json; + +[TestFixture] +public class DateOnlyJsonTests +{ + [Test] + public void SerializeDateOnly_ShouldMatchExpectedFormat() + { + (DateOnly dateOnly, string expected)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (new DateOnly(2023, 1, 1), "\"2023-01-01\""), + (new DateOnly(2023, 12, 31), "\"2023-12-31\""), + (new DateOnly(2023, 6, 15), "\"2023-06-15\""), + (new DateOnly(2023, 3, 10), "\"2023-03-10\""), + ]; + foreach (var (dateOnly, expected) in testCases) + { + var json = JsonUtils.Serialize(dateOnly); + Assert.That(json, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeDateOnly_ShouldMatchExpectedDateOnly() + { + (DateOnly expected, string json)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (new DateOnly(2023, 1, 1), "\"2023-01-01\""), + (new DateOnly(2023, 12, 31), "\"2023-12-31\""), + (new DateOnly(2023, 6, 15), "\"2023-06-15\""), + (new DateOnly(2023, 3, 10), "\"2023-03-10\""), + ]; + + foreach (var (expected, json) in testCases) + { + var dateOnly = JsonUtils.Deserialize(json); + Assert.That(dateOnly, Is.EqualTo(expected)); + } + } + + [Test] + public void SerializeNullableDateOnly_ShouldMatchExpectedFormat() + { + (DateOnly? dateOnly, string expected)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (null, "null"), + ]; + foreach (var (dateOnly, expected) in testCases) + { + var json = JsonUtils.Serialize(dateOnly); + Assert.That(json, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeNullableDateOnly_ShouldMatchExpectedDateOnly() + { + (DateOnly? expected, string json)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (null, "null"), + ]; + + foreach (var (expected, json) in testCases) + { + var dateOnly = JsonUtils.Deserialize(json); + Assert.That(dateOnly, Is.EqualTo(expected)); + } + } + + [Test] + public void ShouldSerializeDictionaryWithDateOnlyKey() + { + var key = new DateOnly(2023, 10, 5); + var dict = new Dictionary { { key, "value_a" } }; + var json = JsonUtils.Serialize(dict); + Assert.That(json, Does.Contain("2023-10-05")); + Assert.That(json, Does.Contain("value_a")); + } + + [Test] + public void ShouldDeserializeDictionaryWithDateOnlyKey() + { + var json = """ + { + "2023-10-05": "value_a" + } + """; + var dict = JsonUtils.Deserialize>(json); + Assert.That(dict, Is.Not.Null); + var key = new DateOnly(2023, 10, 5); + Assert.That(dict![key], Is.EqualTo("value_a")); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/DateTimeJsonTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/DateTimeJsonTests.cs new file mode 100644 index 000000000000..515e96e57220 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/DateTimeJsonTests.cs @@ -0,0 +1,134 @@ +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core.Json; + +[TestFixture] +public class DateTimeJsonTests +{ + [Test] + public void SerializeDateTime_ShouldMatchExpectedFormat() + { + (DateTime dateTime, string expected)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc), "\"2023-01-01T00:00:00.000Z\""), + ( + new DateTime(2023, 12, 31, 23, 59, 59, DateTimeKind.Utc), + "\"2023-12-31T23:59:59.000Z\"" + ), + (new DateTime(2023, 6, 15, 12, 0, 0, DateTimeKind.Utc), "\"2023-06-15T12:00:00.000Z\""), + ( + new DateTime(2023, 3, 10, 8, 45, 30, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.000Z\"" + ), + ( + new DateTime(2023, 3, 10, 8, 45, 30, 123, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.123Z\"" + ), + ]; + foreach (var (dateTime, expected) in testCases) + { + var json = JsonUtils.Serialize(dateTime); + Assert.That(json, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeDateTime_ShouldMatchExpectedDateTime() + { + (DateTime expected, string json)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc), "\"2023-01-01T00:00:00.000Z\""), + ( + new DateTime(2023, 12, 31, 23, 59, 59, DateTimeKind.Utc), + "\"2023-12-31T23:59:59.000Z\"" + ), + (new DateTime(2023, 6, 15, 12, 0, 0, DateTimeKind.Utc), "\"2023-06-15T12:00:00.000Z\""), + ( + new DateTime(2023, 3, 10, 8, 45, 30, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.000Z\"" + ), + (new DateTime(2023, 3, 10, 8, 45, 30, DateTimeKind.Utc), "\"2023-03-10T08:45:30Z\""), + ( + new DateTime(2023, 3, 10, 8, 45, 30, 123, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.123Z\"" + ), + ]; + + foreach (var (expected, json) in testCases) + { + var dateTime = JsonUtils.Deserialize(json); + Assert.That(dateTime, Is.EqualTo(expected)); + } + } + + [Test] + public void SerializeNullableDateTime_ShouldMatchExpectedFormat() + { + (DateTime? expected, string json)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (null, "null"), + ]; + + foreach (var (expected, json) in testCases) + { + var dateTime = JsonUtils.Deserialize(json); + Assert.That(dateTime, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeNullableDateTime_ShouldMatchExpectedDateTime() + { + (DateTime? expected, string json)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (null, "null"), + ]; + + foreach (var (expected, json) in testCases) + { + var dateTime = JsonUtils.Deserialize(json); + Assert.That(dateTime, Is.EqualTo(expected)); + } + } + + [Test] + public void ShouldSerializeDictionaryWithDateTimeKey() + { + var key = new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc); + var dict = new Dictionary { { key, "value_a" } }; + var json = JsonUtils.Serialize(dict); + Assert.That(json, Does.Contain("2023-10-05T14:30:00.000Z")); + Assert.That(json, Does.Contain("value_a")); + } + + [Test] + public void ShouldDeserializeDictionaryWithDateTimeKey() + { + var json = """ + { + "2023-10-05T14:30:00.000Z": "value_a" + } + """; + var dict = JsonUtils.Deserialize>(json); + Assert.That(dict, Is.Not.Null); + var key = new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc); + Assert.That(dict![key], Is.EqualTo("value_a")); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 000000000000..aee4a0806fb8 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,160 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + + [JsonPropertyName("read_only_nullable_list")] + [JsonAccess(JsonAccessType.ReadOnly)] + public IEnumerable? ReadOnlyNullableList { get; set; } + + [JsonPropertyName("read_only_list")] + [JsonAccess(JsonAccessType.ReadOnly)] + public IEnumerable ReadOnlyList { get; set; } = []; + + [JsonPropertyName("write_only_nullable_list")] + [JsonAccess(JsonAccessType.WriteOnly)] + public IEnumerable? WriteOnlyNullableList { get; set; } + + [JsonPropertyName("write_only_list")] + [JsonAccess(JsonAccessType.WriteOnly)] + public IEnumerable WriteOnlyList { get; set; } = []; + + [JsonPropertyName("normal_list")] + public IEnumerable NormalList { get; set; } = []; + + [JsonPropertyName("normal_nullable_list")] + public IEnumerable? NullableNormalList { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = """ + { + "read_only_prop": "read", + "write_only_prop": "write", + "normal_prop": "normal_prop", + "read_only_nullable_list": ["item1", "item2"], + "read_only_list": ["item3", "item4"], + "write_only_nullable_list": ["item5", "item6"], + "write_only_list": ["item7", "item8"], + "normal_list": ["normal1", "normal2"], + "normal_nullable_list": ["normal1", "normal2"] + } + """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + // String properties + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + + // List properties - read only + var nullableReadOnlyList = obj.ReadOnlyNullableList?.ToArray(); + Assert.That(nullableReadOnlyList, Is.Not.Null); + Assert.That(nullableReadOnlyList, Has.Length.EqualTo(2)); + Assert.That(nullableReadOnlyList![0], Is.EqualTo("item1")); + Assert.That(nullableReadOnlyList![1], Is.EqualTo("item2")); + + var readOnlyList = obj.ReadOnlyList.ToArray(); + Assert.That(readOnlyList, Is.Not.Null); + Assert.That(readOnlyList, Has.Length.EqualTo(2)); + Assert.That(readOnlyList[0], Is.EqualTo("item3")); + Assert.That(readOnlyList[1], Is.EqualTo("item4")); + + // List properties - write only + Assert.That(obj.WriteOnlyNullableList, Is.Null); + Assert.That(obj.WriteOnlyList, Is.Not.Null); + Assert.That(obj.WriteOnlyList, Is.Empty); + + // Normal list property + var normalList = obj.NormalList.ToArray(); + Assert.That(normalList, Is.Not.Null); + Assert.That(normalList, Has.Length.EqualTo(2)); + Assert.That(normalList[0], Is.EqualTo("normal1")); + Assert.That(normalList[1], Is.EqualTo("normal2")); + }); + + // Set up values for serialization + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + obj.WriteOnlyNullableList = new List { "write1", "write2" }; + obj.WriteOnlyList = new List { "write3", "write4" }; + obj.NormalList = new List { "new_normal" }; + obj.NullableNormalList = new List { "new_normal" }; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = """ + { + "write_only_prop": "write", + "normal_prop": "new_value", + "write_only_nullable_list": [ + "write1", + "write2" + ], + "write_only_list": [ + "write3", + "write4" + ], + "normal_list": [ + "new_normal" + ], + "normal_nullable_list": [ + "new_normal" + ] + } + """; + Assert.That(serializedJson, Is.EqualTo(expectedJson).IgnoreWhiteSpace); + } + + [Test] + public void JsonAccessAttribute_WithNullListsInJson_ShouldWorkAsExpected() + { + const string json = """ + { + "read_only_prop": "read", + "normal_prop": "normal_prop", + "read_only_nullable_list": null, + "read_only_list": [] + } + """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + // Read-only nullable list should be null when JSON contains null + var nullableReadOnlyList = obj.ReadOnlyNullableList?.ToArray(); + Assert.That(nullableReadOnlyList, Is.Null); + + // Read-only non-nullable list should never be null, but empty when JSON contains null + var readOnlyList = obj.ReadOnlyList.ToArray(); // This should be initialized to an empty list by default + Assert.That(readOnlyList, Is.Not.Null); + Assert.That(readOnlyList, Is.Empty); + }); + + // Serialize and verify read-only lists are not included + var serializedJson = JsonUtils.Serialize(obj); + Assert.That(serializedJson, Does.Not.Contain("read_only_prop")); + Assert.That(serializedJson, Does.Not.Contain("read_only_nullable_list")); + Assert.That(serializedJson, Does.Not.Contain("read_only_list")); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/QueryStringBuilderTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/QueryStringBuilderTests.cs new file mode 100644 index 000000000000..179c89a72e4a --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/QueryStringBuilderTests.cs @@ -0,0 +1,672 @@ +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core; + +[TestFixture] +public class QueryStringBuilderTests +{ + [Test] + public void Build_SimpleParameters() + { + var parameters = new List> + { + new("name", "John Doe"), + new("age", "30"), + new("city", "New York"), + }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo("?name=John%20Doe&age=30&city=New%20York")); + } + + [Test] + public void Build_EmptyList_ReturnsEmptyString() + { + var parameters = new List>(); + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Build_SpecialCharacters() + { + var parameters = new List> + { + new("email", "test@example.com"), + new("url", "https://example.com/path?query=value"), + new("special", "a+b=c&d"), + }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That( + result, + Is.EqualTo( + "?email=test@example.com&url=https://example.com/path?query=value&special=a%2Bb=c%26d" + ) + ); + } + + [Test] + public void Build_UnicodeCharacters() + { + var parameters = new List> { new("greeting", "Hello 世界") }; + + var result = QueryStringBuilder.Build(parameters); + + // Verify the Chinese characters are properly UTF-8 encoded + Assert.That(result, Does.StartWith("?greeting=Hello%20")); + Assert.That(result, Does.Contain("%E4%B8%96%E7%95%8C")); // 世界 + } + + [Test] + public void Build_SessionSettings_DeepObject() + { + // Simulate session settings with nested properties + var sessionSettings = new + { + custom_session_id = "my-custom-session-id", + system_prompt = "You are a helpful assistant", + variables = new Dictionary + { + { "userName", "John" }, + { "userAge", 30 }, + { "isPremium", true }, + }, + }; + + // Build query parameters list + var queryParams = new List> { new("api_key", "test_key_123") }; + + // Add session_settings with prefix using the new overload + queryParams.AddRange( + QueryStringConverter.ToDeepObject("session_settings", sessionSettings) + ); + + var result = QueryStringBuilder.Build(queryParams); + + // Verify the result contains properly formatted deep object notation + // Note: Square brackets are URL-encoded as %5B and %5D + Assert.That(result, Does.StartWith("?api_key=test_key_123")); + Assert.That( + result, + Does.Contain("session_settings%5Bcustom_session_id%5D=my-custom-session-id") + ); + Assert.That( + result, + Does.Contain("session_settings%5Bsystem_prompt%5D=You%20are%20a%20helpful%20assistant") + ); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserName%5D=John")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserAge%5D=30")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BisPremium%5D=true")); + + // Verify it's NOT JSON encoded (no braces or quotes in the original format) + Assert.That(result, Does.Not.Contain("%7B%22")); // Not {" sequence + } + + [Test] + public void Build_ChatApiLikeParameters() + { + // Simulate what ChatApi constructor does + var sessionSettings = new + { + system_prompt = "You are helpful", + variables = new Dictionary { { "name", "Alice" } }, + }; + + var queryParams = new List>(); + + // Simple parameters + var simpleParams = new Dictionary + { + { "access_token", "token123" }, + { "config_id", "config456" }, + { "api_key", "key789" }, + }; + queryParams.AddRange(QueryStringConverter.ToExplodedForm(simpleParams)); + + // Session settings as deep object with prefix + queryParams.AddRange( + QueryStringConverter.ToDeepObject("session_settings", sessionSettings) + ); + + var result = QueryStringBuilder.Build(queryParams); + + // Verify structure (square brackets are URL-encoded) + Assert.That(result, Does.StartWith("?")); + Assert.That(result, Does.Contain("access_token=token123")); + Assert.That(result, Does.Contain("config_id=config456")); + Assert.That(result, Does.Contain("api_key=key789")); + Assert.That( + result, + Does.Contain("session_settings%5Bsystem_prompt%5D=You%20are%20helpful") + ); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5Bname%5D=Alice")); + } + + [Test] + public void Build_ReservedCharacters_NotEncoded() + { + var parameters = new List> + { + new("path", "some-path"), + new("id", "123-456_789.test~value"), + }; + + var result = QueryStringBuilder.Build(parameters); + + // Safe query characters include RFC 3986 unreserved + sub-delimiters (except & = +) + : @ / + Assert.That(result, Is.EqualTo("?path=some-path&id=123-456_789.test~value")); + } + + [Test] + public void Builder_Add_SimpleParameters() + { + var result = new QueryStringBuilder.Builder() + .Add("name", "John Doe") + .Add("age", 30) + .Add("active", true) + .Build(); + + Assert.That(result, Does.Contain("name=John%20Doe")); + Assert.That(result, Does.Contain("age=30")); + Assert.That(result, Does.Contain("active=true")); + } + + [Test] + public void Builder_Add_NullValuesIgnored() + { + var result = new QueryStringBuilder.Builder() + .Add("name", "John") + .Add("middle", null) + .Add("age", 30) + .Build(); + + Assert.That(result, Does.Contain("name=John")); + Assert.That(result, Does.Contain("age=30")); + Assert.That(result, Does.Not.Contain("middle")); + } + + [Test] + public void Builder_AddDeepObject_WithPrefix() + { + var settings = new + { + custom_session_id = "id-123", + system_prompt = "You are helpful", + variables = new { name = "Alice", age = 25 }, + }; + + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddDeepObject("session_settings", settings) + .Build(); + + Assert.That(result, Does.Contain("api_key=key123")); + Assert.That(result, Does.Contain("session_settings%5Bcustom_session_id%5D=id-123")); + Assert.That( + result, + Does.Contain("session_settings%5Bsystem_prompt%5D=You%20are%20helpful") + ); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5Bname%5D=Alice")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5Bage%5D=25")); + } + + [Test] + public void Builder_AddDeepObject_NullIgnored() + { + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddDeepObject("settings", null) + .Build(); + + Assert.That(result, Is.EqualTo("?api_key=key123")); + Assert.That(result, Does.Not.Contain("settings")); + } + + [Test] + public void Builder_AddExploded_WithPrefix() + { + var filter = new { status = "active", type = "user" }; + + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddExploded("filter", filter) + .Build(); + + Assert.That(result, Does.Contain("api_key=key123")); + Assert.That(result, Does.Contain("filter%5Bstatus%5D=active")); + Assert.That(result, Does.Contain("filter%5Btype%5D=user")); + } + + [Test] + public void Builder_AddExploded_NullIgnored() + { + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddExploded("filter", null) + .Build(); + + Assert.That(result, Is.EqualTo("?api_key=key123")); + Assert.That(result, Does.Not.Contain("filter")); + } + + [Test] + public void Builder_WithCapacity() + { + // Test that capacity constructor works without errors + var result = new QueryStringBuilder.Builder(capacity: 10) + .Add("param1", "value1") + .Add("param2", "value2") + .Build(); + + Assert.That(result, Does.Contain("param1=value1")); + Assert.That(result, Does.Contain("param2=value2")); + } + + [Test] + public void Builder_ChatApiLikeUsage() + { + // Simulate real usage from ChatApi + var sessionSettings = new + { + custom_session_id = "session-123", + variables = new Dictionary + { + { "userName", "John" }, + { "userAge", 30 }, + }, + }; + + var result = new QueryStringBuilder.Builder(capacity: 16) + .Add("access_token", "token123") + .Add("allow_connection", true) + .Add("config_id", "config456") + .Add("api_key", "key789") + .AddDeepObject("session_settings", sessionSettings) + .Build(); + + Assert.That(result, Does.StartWith("?")); + Assert.That(result, Does.Contain("access_token=token123")); + Assert.That(result, Does.Contain("allow_connection=true")); + Assert.That(result, Does.Contain("config_id=config456")); + Assert.That(result, Does.Contain("api_key=key789")); + Assert.That(result, Does.Contain("session_settings%5Bcustom_session_id%5D=session-123")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserName%5D=John")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserAge%5D=30")); + } + + [Test] + public void Builder_EmptyBuilder_ReturnsEmptyString() + { + var result = new QueryStringBuilder.Builder().Build(); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Builder_OnlyNullValues_ReturnsEmptyString() + { + var result = new QueryStringBuilder.Builder() + .Add("param1", null) + .Add("param2", null) + .AddDeepObject("settings", null) + .Build(); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Builder_Set_OverridesSingleValue() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "original") + .Set("foo", "override") + .Build(); + + Assert.That(result, Is.EqualTo("?foo=override")); + } + + [Test] + public void Builder_Set_OverridesMultipleValues() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "value1") + .Add("foo", "value2") + .Set("foo", "override") + .Build(); + + Assert.That(result, Is.EqualTo("?foo=override")); + } + + [Test] + public void Builder_Set_WithArray_CreatesMultipleParameters() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "original") + .Set("foo", new[] { "value1", "value2" }) + .Build(); + + Assert.That(result, Is.EqualTo("?foo=value1&foo=value2")); + } + + [Test] + public void Builder_Set_WithNull_RemovesParameter() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "original") + .Add("bar", "keep") + .Set("foo", null) + .Build(); + + Assert.That(result, Is.EqualTo("?bar=keep")); + } + + [Test] + public void Builder_MergeAdditional_WithSingleValues() + { + var additional = new List> + { + new("foo", "bar"), + new("baz", "qux"), + }; + + var result = new QueryStringBuilder.Builder() + .Add("existing", "value") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("existing=value")); + Assert.That(result, Does.Contain("foo=bar")); + Assert.That(result, Does.Contain("baz=qux")); + } + + [Test] + public void Builder_MergeAdditional_WithDuplicateKeys_CreatesList() + { + var additional = new List> + { + new("foo", "bar1"), + new("foo", "bar2"), + new("baz", "qux"), + }; + + var result = new QueryStringBuilder.Builder() + .Add("existing", "value") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("existing=value")); + Assert.That(result, Does.Contain("foo=bar1")); + Assert.That(result, Does.Contain("foo=bar2")); + Assert.That(result, Does.Contain("baz=qux")); + } + + [Test] + public void Builder_MergeAdditional_OverridesExistingParameters() + { + var additional = new List> { new("foo", "override") }; + + var result = new QueryStringBuilder.Builder() + .Add("foo", "original1") + .Add("foo", "original2") + .Add("bar", "keep") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("bar=keep")); + Assert.That(result, Does.Contain("foo=override")); + Assert.That(result, Does.Not.Contain("original1")); + Assert.That(result, Does.Not.Contain("original2")); + } + + [Test] + public void Builder_MergeAdditional_WithDuplicates_OverridesExisting() + { + var additional = new List> + { + new("foo", "new1"), + new("foo", "new2"), + new("foo", "new3"), + }; + + var result = new QueryStringBuilder.Builder() + .Add("foo", "original1") + .Add("foo", "original2") + .Add("bar", "keep") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("bar=keep")); + Assert.That(result, Does.Contain("foo=new1")); + Assert.That(result, Does.Contain("foo=new2")); + Assert.That(result, Does.Contain("foo=new3")); + Assert.That(result, Does.Not.Contain("original1")); + Assert.That(result, Does.Not.Contain("original2")); + } + + [Test] + public void Builder_MergeAdditional_WithNull_NoOp() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "value") + .MergeAdditional(null) + .Build(); + + Assert.That(result, Is.EqualTo("?foo=value")); + } + + [Test] + public void Builder_MergeAdditional_WithEmptyList_NoOp() + { + var additional = new List>(); + + var result = new QueryStringBuilder.Builder() + .Add("foo", "value") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Is.EqualTo("?foo=value")); + } + + [Test] + public void Builder_MergeAdditional_RealWorldScenario() + { + // SDK generates foo=foo1&foo=foo2 + var builder = new QueryStringBuilder.Builder() + .Add("foo", "foo1") + .Add("foo", "foo2") + .Add("bar", "baz"); + + // User provides foo=override in AdditionalQueryParameters + var additional = new List> { new("foo", "override") }; + + var result = builder.MergeAdditional(additional).Build(); + + // Result should be foo=override&bar=baz (user overrides SDK) + Assert.That(result, Does.Contain("bar=baz")); + Assert.That(result, Does.Contain("foo=override")); + Assert.That(result, Does.Not.Contain("foo1")); + Assert.That(result, Does.Not.Contain("foo2")); + } + + [Test] + public void Builder_MergeAdditional_UserProvidesMultipleValues() + { + // SDK generates no foo parameter + var builder = new QueryStringBuilder.Builder().Add("bar", "baz"); + + // User provides foo=bar1&foo=bar2 in AdditionalQueryParameters + var additional = new List> + { + new("foo", "bar1"), + new("foo", "bar2"), + }; + + var result = builder.MergeAdditional(additional).Build(); + + // Result should be bar=baz&foo=bar1&foo=bar2 + Assert.That(result, Does.Contain("bar=baz")); + Assert.That(result, Does.Contain("foo=bar1")); + Assert.That(result, Does.Contain("foo=bar2")); + } + + [Test] + public void Builder_Add_WithCollection_CreatesMultipleParameters() + { + var tags = new[] { "tag1", "tag2", "tag3" }; + var result = new QueryStringBuilder.Builder().Add("tag", tags).Build(); + + Assert.That(result, Does.Contain("tag=tag1")); + Assert.That(result, Does.Contain("tag=tag2")); + Assert.That(result, Does.Contain("tag=tag3")); + } + + [Test] + public void Builder_Add_WithList_CreatesMultipleParameters() + { + var ids = new List { 1, 2, 3 }; + var result = new QueryStringBuilder.Builder().Add("id", ids).Build(); + + Assert.That(result, Does.Contain("id=1")); + Assert.That(result, Does.Contain("id=2")); + Assert.That(result, Does.Contain("id=3")); + } + + [Test] + public void Builder_Set_WithCollection_ReplacesAllPreviousValues() + { + var result = new QueryStringBuilder.Builder() + .Add("id", 1) + .Add("id", 2) + .Set("id", new[] { 10, 20, 30 }) + .Build(); + + Assert.That(result, Does.Contain("id=10")); + Assert.That(result, Does.Contain("id=20")); + Assert.That(result, Does.Contain("id=30")); + // Check that old values are not present (use word boundaries to avoid false positives with id=10) + Assert.That(result, Does.Not.Contain("id=1&")); + Assert.That(result, Does.Not.Contain("id=2&")); + Assert.That(result, Does.Not.Contain("id=1?")); + Assert.That(result, Does.Not.Contain("id=2?")); + Assert.That(result, Does.Not.EndWith("id=1")); + Assert.That(result, Does.Not.EndWith("id=2")); + } + + [Test] + public void EncodePathSegment_UnreservedChars_NotEncoded() + { + var result = QueryStringBuilder.EncodePathSegment("hello-world_test.value~123"); + Assert.That(result, Is.EqualTo("hello-world_test.value~123")); + } + + [Test] + public void EncodePathSegment_SubDelimiters_NotEncoded() + { + // All sub-delimiters are safe in path segments per RFC 3986 + var result = QueryStringBuilder.EncodePathSegment("a!b$c&d'e(f)g*h+i,j;k=l"); + Assert.That(result, Is.EqualTo("a!b$c&d'e(f)g*h+i,j;k=l")); + } + + [Test] + public void EncodePathSegment_ColonAndAt_NotEncoded() + { + var result = QueryStringBuilder.EncodePathSegment("user@host:8080"); + Assert.That(result, Is.EqualTo("user@host:8080")); + } + + [Test] + public void EncodePathSegment_SlashAndQuestion_Encoded() + { + // "/" and "?" are NOT part of pchar, so they must be encoded in path segments + var result = QueryStringBuilder.EncodePathSegment("path/with?query"); + Assert.That(result, Is.EqualTo("path%2Fwith%3Fquery")); + } + + [Test] + public void EncodePathSegment_Space_Encoded() + { + var result = QueryStringBuilder.EncodePathSegment("hello world"); + Assert.That(result, Is.EqualTo("hello%20world")); + } + + [Test] + public void EncodePathSegment_EmptyAndNull() + { + Assert.That(QueryStringBuilder.EncodePathSegment(""), Is.EqualTo("")); + Assert.That(QueryStringBuilder.EncodePathSegment(null!), Is.Null); + } + + [Test] + public void Build_QueryKeyVsValue_DifferentEncoding() + { + // "=" is safe in query values but NOT in query keys + var parameters = new List> + { + new("key=with=equals", "value=with=equals"), + }; + + var result = QueryStringBuilder.Build(parameters); + + // Key: "=" must be encoded + // Value: "=" is safe (part of query value safe chars) + Assert.That(result, Is.EqualTo("?key%3Dwith%3Dequals=value=with=equals")); + } + + [Test] + public void Build_QueryValue_QuestionMarkNotEncoded() + { + // "?" is safe in both query keys and query values per RFC 3986 + var parameters = new List> { new("q?key", "is this?") }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo("?q?key=is%20this?")); + } + + [Test] + public void Build_QueryKey_PlusEncoded() + { + // "+" must be encoded in both query keys and query values + var parameters = new List> { new("a+b", "c+d") }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo("?a%2Bb=c%2Bd")); + } + + [Test] + public void Build_Semicolon_Encoded() + { + // ";" is a legacy parameter separator, so it must be encoded in keys and values + var parameters = new List> + { + new("a;b", "jo@example.com; ceo@example.com"), + }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo("?a%3Bb=jo@example.com%3B%20ceo@example.com")); + } + + [Test] + public void Build_ODataFilter_DollarPreserved() + { + // "$" is safe in query keys (sub-delimiter), verifies OData-style parameters work + var parameters = new List> + { + new("$filter", "status eq 'active'"), + new("$top", "10"), + }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Does.Contain("$filter=status%20eq%20'active'")); + Assert.That(result, Does.Contain("$top=10")); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/QueryStringConverterTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/QueryStringConverterTests.cs new file mode 100644 index 000000000000..c1faebbea1f9 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/QueryStringConverterTests.cs @@ -0,0 +1,158 @@ +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core; + +[TestFixture] +public class QueryStringConverterTests +{ + [Test] + public void ToQueryStringCollection_Form() + { + var obj = new + { + Name = "John", + Age = 30, + Address = new + { + Street = "123 Main St", + City = "Anytown", + Coordinates = new[] { 39.781721f, -89.650148f }, + }, + Tags = new[] { "Developer", "Blogger" }, + }; + var result = QueryStringConverter.ToForm(obj); + var expected = new List> + { + new("Name", "John"), + new("Age", "30"), + new("Address[Street]", "123 Main St"), + new("Address[City]", "Anytown"), + new("Address[Coordinates]", "39.78172,-89.65015"), + new("Tags", "Developer,Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_ExplodedForm() + { + var obj = new + { + Name = "John", + Age = 30, + Address = new + { + Street = "123 Main St", + City = "Anytown", + Coordinates = new[] { 39.781721f, -89.650148f }, + }, + Tags = new[] { "Developer", "Blogger" }, + }; + var result = QueryStringConverter.ToExplodedForm(obj); + var expected = new List> + { + new("Name", "John"), + new("Age", "30"), + new("Address[Street]", "123 Main St"), + new("Address[City]", "Anytown"), + new("Address[Coordinates]", "39.78172"), + new("Address[Coordinates]", "-89.65015"), + new("Tags", "Developer"), + new("Tags", "Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_DeepObject() + { + var obj = new + { + Name = "John", + Age = 30, + Address = new + { + Street = "123 Main St", + City = "Anytown", + Coordinates = new[] { 39.781721f, -89.650148f }, + }, + Tags = new[] { "Developer", "Blogger" }, + }; + var result = QueryStringConverter.ToDeepObject(obj); + var expected = new List> + { + new("Name", "John"), + new("Age", "30"), + new("Address[Street]", "123 Main St"), + new("Address[City]", "Anytown"), + new("Address[Coordinates][0]", "39.78172"), + new("Address[Coordinates][1]", "-89.65015"), + new("Tags[0]", "Developer"), + new("Tags[1]", "Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_OnString_ThrowsException() + { + var exception = Assert.Throws(() => + QueryStringConverter.ToForm("invalid") + ); + Assert.That( + exception.Message, + Is.EqualTo( + "Only objects can be converted to query string collections. Given type is String." + ) + ); + } + + [Test] + public void ToQueryStringCollection_OnArray_ThrowsException() + { + var exception = Assert.Throws(() => + QueryStringConverter.ToForm(Array.Empty()) + ); + Assert.That( + exception.Message, + Is.EqualTo( + "Only objects can be converted to query string collections. Given type is Array." + ) + ); + } + + [Test] + public void ToQueryStringCollection_DeepObject_WithPrefix() + { + var obj = new + { + custom_session_id = "my-id", + system_prompt = "You are helpful", + variables = new { name = "Alice", age = 25 }, + }; + var result = QueryStringConverter.ToDeepObject("session_settings", obj); + var expected = new List> + { + new("session_settings[custom_session_id]", "my-id"), + new("session_settings[system_prompt]", "You are helpful"), + new("session_settings[variables][name]", "Alice"), + new("session_settings[variables][age]", "25"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_ExplodedForm_WithPrefix() + { + var obj = new { Name = "John", Tags = new[] { "Developer", "Blogger" } }; + var result = QueryStringConverter.ToExplodedForm("user", obj); + var expected = new List> + { + new("user[Name]", "John"), + new("user[Tags]", "Developer"), + new("user[Tags]", "Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/GzipResponseTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/GzipResponseTests.cs new file mode 100644 index 000000000000..cc1582e2f9e1 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/GzipResponseTests.cs @@ -0,0 +1,104 @@ +using global::System.IO.Compression; +using global::System.Net.Http; +using global::System.Text; +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; +using WireMock.Server; +using SystemTask = global::System.Threading.Tasks.Task; +using WireMockRequest = WireMock.RequestBuilders.Request; +using WireMockResponse = WireMock.ResponseBuilders.Response; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core.RawClientTests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class GzipResponseTests +{ + private WireMockServer _server; + private RawClient _rawClient; + private string _baseUrl; + + [SetUp] + public void SetUp() + { + _server = WireMockServer.Start(); + _baseUrl = _server.Url ?? ""; + _rawClient = new RawClient(new ClientOptions { MaxRetries = 0 }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldDecompressGzipResponse() + { + const string body = "{\"message\": \"gzipped response\"}"; + _server + .Given( + WireMockRequest + .Create() + .WithPath("/gzip") + .WithHeader("Accept-Encoding", "gzip*") + .UsingGet() + ) + .RespondWith( + WireMockResponse + .Create() + .WithStatusCode(200) + .WithHeader("Content-Encoding", "gzip") + .WithBody(Compress(body)) + ); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/gzip", + Headers = new Dictionary { ["Accept-Encoding"] = "gzip" }, + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.That(content, Is.EqualTo(body)); + Assert.That(response.Raw.Content.Headers.ContentEncoding, Is.Empty); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldReturnUncompressedResponseUnchanged() + { + const string body = "{\"message\": \"plain response\"}"; + _server + .Given(WireMockRequest.Create().WithPath("/plain").UsingGet()) + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody(body)); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/plain", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.That(content, Is.EqualTo(body)); + } + + private static byte[] Compress(string value) + { + using var output = new MemoryStream(); + using (var gzipStream = new GZipStream(output, CompressionMode.Compress)) + { + var bytes = Encoding.UTF8.GetBytes(value); + gzipStream.Write(bytes, 0, bytes.Length); + } + return output.ToArray(); + } + + [TearDown] + public void TearDown() + { + _server.Stop(); + _server.Dispose(); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/MultipartFormTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/MultipartFormTests.cs new file mode 100644 index 000000000000..297ff378ac2e --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/MultipartFormTests.cs @@ -0,0 +1,1121 @@ +using global::System.Net.Http; +using global::System.Text; +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; +using SystemTask = global::System.Threading.Tasks.Task; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core.RawClientTests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class MultipartFormTests +{ + private static SimpleObject _simpleObject = new(); + + private static string _simpleFormEncoded = + "meta=data&Date=2023-10-01&Time=12:00:00&Duration=01:00:00&Id=1a1bb98f-47c6-407b-9481-78476affe52a&IsActive=true&Count=42&Initial=A&Values=data,2023-10-01,12:00:00,01:00:00,1a1bb98f-47c6-407b-9481-78476affe52a,true,42,A"; + + private static string _simpleExplodedFormEncoded = + "meta=data&Date=2023-10-01&Time=12:00:00&Duration=01:00:00&Id=1a1bb98f-47c6-407b-9481-78476affe52a&IsActive=true&Count=42&Initial=A&Values=data&Values=2023-10-01&Values=12:00:00&Values=01:00:00&Values=1a1bb98f-47c6-407b-9481-78476affe52a&Values=true&Values=42&Values=A"; + + private static ComplexObject _complexObject = new(); + + private static string _complexJson = """ + { + "meta": "data", + "Nested": { + "foo": "value" + }, + "NestedDictionary": { + "key": { + "foo": "value" + } + }, + "ListOfObjects": [ + { + "foo": "value" + }, + { + "foo": "value2" + } + ], + "Date": "2023-10-01", + "Time": "12:00:00", + "Duration": "01:00:00", + "Id": "1a1bb98f-47c6-407b-9481-78476affe52a", + "IsActive": true, + "Count": 42, + "Initial": "A" + } + """; + + [Test] + public async SystemTask ShouldAddStringPart() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", partInput); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=string + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts("strings", [partInput, partInput]); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask GivenNull_ShouldNotAddStringPart() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", null); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts_WithNullsInList() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts("strings", [partInput, null, partInput]); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringPart_WithContentType() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", partInput, "text/xml"); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml + Content-Disposition: form-data; name=string + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringPart_WithContentTypeAndCharset() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", partInput, "text/xml; charset=utf-8"); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml; charset=utf-8 + Content-Disposition: form-data; name=string + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts_WithContentType() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts("strings", [partInput, partInput], "text/xml"); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/xml + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts_WithContentTypeAndCharset() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts( + "strings", + [partInput, partInput], + "text/xml; charset=utf-8" + ); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml; charset=utf-8 + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/xml; charset=utf-8 + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithFileName() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter { Stream = partInput, FileName = "test.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithoutFileName() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", partInput); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithContentType() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter + { + Stream = partInput, + FileName = "test.txt", + ContentType = "text/plain", + }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "ignored-fallback-content-type"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithContentTypeAndCharset() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter + { + Stream = partInput, + FileName = "test.txt", + ContentType = "text/plain; charset=utf-8", + }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "ignored-fallback-content-type"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain; charset=utf-8 + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithFallbackContentType() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter { Stream = partInput, FileName = "test.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "text/plain"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithFallbackContentTypeAndCharset() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter { Stream = partInput, FileName = "test.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "text/plain; charset=utf-8"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain; charset=utf-8 + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameters() + { + var (partInput1, partExpectedString1) = GetFileParameterTestData(); + var (partInput2, partExpectedString2) = GetFileParameterTestData(); + var file1 = new FileParameter { Stream = partInput1, FileName = "test1.txt" }; + var file2 = new FileParameter { Stream = partInput2, FileName = "test2.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterParts("file", [file1, file2]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test1.txt; filename*=utf-8''test1.txt + + {partExpectedString1} + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test2.txt; filename*=utf-8''test2.txt + + {partExpectedString2} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameters_WithNullsInList() + { + var (partInput1, partExpectedString1) = GetFileParameterTestData(); + var (partInput2, partExpectedString2) = GetFileParameterTestData(); + var file1 = new FileParameter { Stream = partInput1, FileName = "test1.txt" }; + var file2 = new FileParameter { Stream = partInput2, FileName = "test2.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterParts("file", [file1, null, file2]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test1.txt; filename*=utf-8''test1.txt + + {partExpectedString1} + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test2.txt; filename*=utf-8''test2.txt + + {partExpectedString2} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask GivenNull_ShouldNotAddFileParameter() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonPart_WithComplexObject() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonPart("object", _complexObject); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=object + + {_complexJson} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonPart_WithComplexObjectList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonParts("objects", [_complexObject, _complexObject]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=objects + + {_complexJson} + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=objects + + {_complexJson} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask GivenNull_ShouldNotAddJsonPart() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonPart("object", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonParts_WithNullsInList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonParts("objects", [_complexObject, null]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=objects + + {_complexJson} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonParts_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonParts("objects", [new { }], "application/json-patch+json"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $$""" + --{{boundary}} + Content-Type: application/json-patch+json + Content-Disposition: form-data; name=objects + + {} + --{{boundary}}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithSimpleObject() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedPart("object", _simpleObject); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=object + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithSimpleObjectList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts("objects", [_simpleObject, _simpleObject]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddFormEncodedParts_WithNull() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts("object", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddFormEncodedParts_WithNullsInList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts("objects", [_simpleObject, null]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedPart_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedPart_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithSimpleObject() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart("object", _simpleObject); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=object + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithSimpleObjectList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts("objects", [_simpleObject, _simpleObject]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddExplodedFormEncodedParts_WithNull() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart("object", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddExplodedFormEncodedParts_WithNullsInList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts("objects", [_simpleObject, null]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedPart_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedPart_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + private static string EscapeFormEncodedString(string input) + { + return string.Join( + "&", + input + .Split('&') + .Select(x => x.Split('=')) + .Select(x => $"{Uri.EscapeDataString(x[0])}={Uri.EscapeDataString(x[1])}") + ); + } + + private static string GetBoundary(MultipartFormDataContent content) + { + return content + .Headers.ContentType?.Parameters.Single(p => + p.Name.Equals("boundary", StringComparison.OrdinalIgnoreCase) + ) + .Value?.Trim('"') + ?? throw new global::System.Exception("Boundary not found"); + } + + private static SeedCsharpGlobalHeaderLiteralEnv.Core.MultipartFormRequest CreateMultipartFormRequest() + { + return new SeedCsharpGlobalHeaderLiteralEnv.Core.MultipartFormRequest + { + BaseUrl = "https://localhost", + Method = HttpMethod.Post, + Path = "", + }; + } + + private static (Stream partInput, string partExpectedString) GetFileParameterTestData() + { + const string partExpectedString = "file content"; + var partInput = new MemoryStream(Encoding.Default.GetBytes(partExpectedString)); + return (partInput, partExpectedString); + } + + private class SimpleObject + { + [JsonPropertyName("meta")] + public string Meta { get; set; } = "data"; + public DateOnly Date { get; set; } = DateOnly.Parse("2023-10-01"); + public TimeOnly Time { get; set; } = TimeOnly.Parse("12:00:00"); + public TimeSpan Duration { get; set; } = TimeSpan.FromHours(1); + public Guid Id { get; set; } = Guid.Parse("1a1bb98f-47c6-407b-9481-78476affe52a"); + public bool IsActive { get; set; } = true; + public int Count { get; set; } = 42; + public char Initial { get; set; } = 'A'; + public IEnumerable Values { get; set; } = + [ + "data", + DateOnly.Parse("2023-10-01"), + TimeOnly.Parse("12:00:00"), + TimeSpan.FromHours(1), + Guid.Parse("1a1bb98f-47c6-407b-9481-78476affe52a"), + true, + 42, + 'A', + ]; + } + + private class ComplexObject + { + [JsonPropertyName("meta")] + public string Meta { get; set; } = "data"; + + public object Nested { get; set; } = new { foo = "value" }; + + public Dictionary NestedDictionary { get; set; } = + new() { { "key", new { foo = "value" } } }; + + public IEnumerable ListOfObjects { get; set; } = + new List { new { foo = "value" }, new { foo = "value2" } }; + + public DateOnly Date { get; set; } = DateOnly.Parse("2023-10-01"); + public TimeOnly Time { get; set; } = TimeOnly.Parse("12:00:00"); + public TimeSpan Duration { get; set; } = TimeSpan.FromHours(1); + public Guid Id { get; set; } = Guid.Parse("1a1bb98f-47c6-407b-9481-78476affe52a"); + public bool IsActive { get; set; } = true; + public int Count { get; set; } = 42; + public char Initial { get; set; } = 'A'; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/QueryParameterTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/QueryParameterTests.cs new file mode 100644 index 000000000000..0110eaf36f56 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/QueryParameterTests.cs @@ -0,0 +1,108 @@ +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core.RawClientTests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class QueryParameterTests +{ + [Test] + public void QueryParameters_BasicParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "bar") + .Add("baz", "qux") + .Build(); + + Assert.That(queryString, Is.EqualTo("?foo=bar&baz=qux")); + } + + [Test] + public void QueryParameters_SpecialCharacterEscaping() + { + var queryString = new QueryStringBuilder.Builder() + .Add("email", "bob+test@example.com") + .Add("%Complete", "100") + .Add("space test", "hello world") + .Build(); + + Assert.That(queryString, Does.Contain("email=bob%2Btest@example.com")); + Assert.That(queryString, Does.Contain("%25Complete=100")); + Assert.That(queryString, Does.Contain("space%20test=hello%20world")); + } + + [Test] + public void QueryParameters_MergeAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("sdk", "param") + .MergeAdditional(new List> { new("user", "value") }) + .Build(); + + Assert.That(queryString, Does.Contain("sdk=param")); + Assert.That(queryString, Does.Contain("user=value")); + } + + [Test] + public void QueryParameters_AdditionalOverridesSdk() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "sdk_value") + .MergeAdditional(new List> { new("foo", "user_override") }) + .Build(); + + Assert.That(queryString, Does.Contain("foo=user_override")); + Assert.That(queryString, Does.Not.Contain("sdk_value")); + } + + [Test] + public void QueryParameters_AdditionalMultipleValues() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "sdk_value") + .MergeAdditional( + new List> { new("foo", "user1"), new("foo", "user2") } + ) + .Build(); + + Assert.That(queryString, Does.Contain("foo=user1")); + Assert.That(queryString, Does.Contain("foo=user2")); + Assert.That(queryString, Does.Not.Contain("sdk_value")); + } + + [Test] + public void QueryParameters_OnlyAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .MergeAdditional( + new List> { new("foo", "bar"), new("baz", "qux") } + ) + .Build(); + + Assert.That(queryString, Does.Contain("foo=bar")); + Assert.That(queryString, Does.Contain("baz=qux")); + } + + [Test] + public void QueryParameters_EmptyAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "bar") + .MergeAdditional(new List>()) + .Build(); + + Assert.That(queryString, Is.EqualTo("?foo=bar")); + } + + [Test] + public void QueryParameters_NullAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "bar") + .MergeAdditional(null) + .Build(); + + Assert.That(queryString, Is.EqualTo("?foo=bar")); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/RetriesTests.cs new file mode 100644 index 000000000000..92f32aed1f3d --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/RawClientTests/RetriesTests.cs @@ -0,0 +1,540 @@ +using global::System.Net.Http; +using global::System.Text.Json; +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; +using WireMock.Server; +using SystemTask = global::System.Threading.Tasks.Task; +using WireMockRequest = WireMock.RequestBuilders.Request; +using WireMockResponse = WireMock.ResponseBuilders.Response; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core.RawClientTests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class RetriesTests +{ + private const int MaxRetries = 3; + private WireMockServer _server; + private HttpClient _httpClient; + private RawClient _rawClient; + private string _baseUrl; + + [SetUp] + public void SetUp() + { + _server = WireMockServer.Start(); + _baseUrl = _server.Url ?? ""; + _httpClient = new HttpClient { BaseAddress = new Uri(_baseUrl) }; + _rawClient = new RawClient( + new ClientOptions { HttpClient = _httpClient, MaxRetries = MaxRetries } + ) + { + BaseRetryDelay = 0, + }; + } + + [Test] + [TestCase(408)] + [TestCase(429)] + [TestCase(500)] + [TestCase(504)] + public async SystemTask SendRequestAsync_ShouldRetry_OnRetryableStatusCodes(int statusCode) + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WhenStateIs("Server Error") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + using (Assert.EnterMultipleScope()) + { + Assert.That(content, Is.EqualTo("Success")); + + Assert.That(_server.LogEntries, Has.Count.EqualTo(MaxRetries)); + } + } + + [Test] + [TestCase(400)] + [TestCase(409)] + public async SystemTask SendRequestAsync_ShouldRetry_OnNonRetryableStatusCodes(int statusCode) + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode).WithBody("Failure")); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.JsonRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + Body = new { }, + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(statusCode)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Failure")); + + Assert.That(_server.LogEntries, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldNotRetry_WithStreamRequest() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(429).WithBody("Failure")); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.StreamRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + Body = new MemoryStream(), + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(429)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Failure")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldNotRetry_WithMultiPartFormRequest_WithStream() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(429).WithBody("Failure")); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.MultipartFormRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + }; + request.AddFileParameterPart("file", new MemoryStream()); + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(429)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Failure")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRetry_WithMultiPartFormRequest_WithoutStream() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(429)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WhenStateIs("Server Error") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(429)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.MultipartFormRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + }; + request.AddJsonPart("object", new { }); + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(MaxRetries)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRespectRetryAfterHeader_WithSecondsValue() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfter") + .WillSetStateTo("Success") + .RespondWith( + WireMockResponse.Create().WithStatusCode(429).WithHeader("Retry-After", "1") + ); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfter") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRespectRetryAfterHeader_WithHttpDateValue() + { + var retryAfterDate = DateTimeOffset.UtcNow.AddSeconds(1).ToString("R"); + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfterDate") + .WillSetStateTo("Success") + .RespondWith( + WireMockResponse + .Create() + .WithStatusCode(429) + .WithHeader("Retry-After", retryAfterDate) + ); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfterDate") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() + { + var resetTime = DateTimeOffset.UtcNow.AddSeconds(1).ToUnixTimeSeconds().ToString(); + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RateLimitReset") + .WillSetStateTo("Success") + .RespondWith( + WireMockResponse + .Create() + .WithStatusCode(429) + .WithHeader("X-RateLimit-Reset", resetTime) + ); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RateLimitReset") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryWithBody") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(500)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryWithBody") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.JsonRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + Body = new { key = "value" }, + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + using (Assert.EnterMultipleScope()) + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) + var retriedEntry = _server.LogEntries.ElementAt(1); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); + } + } + + [Test] + public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryMultipart") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(500)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryMultipart") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.MultipartFormRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + }; + request.AddJsonPart("object", new { key = "value" }); + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + using (Assert.EnterMultipleScope()) + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) + var retriedEntry = _server.LogEntries.ElementAt(1); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); + } + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRetry_WhenHandlerDisposesRequestContent() + { + // ContentDisposingHandler simulates HTTP/2's disposal of request.Content after send; + // WireMock's loopback HTTP/1.1 path does not exhibit that on its own. + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("DisposeContentRetry") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(500)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("DisposeContentRetry") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + using var disposingClient = new HttpClient( + new ContentDisposingHandler(new HttpClientHandler()) + ); + var rawClient = new RawClient( + new ClientOptions { HttpClient = disposingClient, MaxRetries = MaxRetries } + ) + { + BaseRetryDelay = 0, + }; + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.JsonRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + Body = new { key = "value" }, + }; + + var response = await rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + using (Assert.EnterMultipleScope()) + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + + var retriedEntry = _server.LogEntries.ElementAt(1); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); + } + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRetry_WhenHandlerDisposesRequestContent_AcrossMultipleRetries() + { + // Exercises 2nd and 3rd clones — the single-retry variant can pass if those break. + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("DisposeContentMultiRetry") + .WillSetStateTo("Second") + .RespondWith(WireMockResponse.Create().WithStatusCode(500)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("DisposeContentMultiRetry") + .WhenStateIs("Second") + .WillSetStateTo("Third") + .RespondWith(WireMockResponse.Create().WithStatusCode(500)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("DisposeContentMultiRetry") + .WhenStateIs("Third") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(500)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("DisposeContentMultiRetry") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + using var disposingClient = new HttpClient( + new ContentDisposingHandler(new HttpClientHandler()) + ); + var rawClient = new RawClient( + new ClientOptions { HttpClient = disposingClient, MaxRetries = MaxRetries } + ) + { + BaseRetryDelay = 0, + }; + + var request = new SeedCsharpGlobalHeaderLiteralEnv.Core.JsonRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + Body = new { key = "value" }, + }; + + var response = await rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + using (Assert.EnterMultipleScope()) + { + // Initial attempt + 3 retries == 4 requests reaching the server. + Assert.That(_server.LogEntries, Has.Count.EqualTo(MaxRetries + 1)); + + // Every retried request must have preserved the original body. + foreach (var entry in _server.LogEntries) + { + using var actualJson = JsonDocument.Parse(entry.RequestMessage.Body!); + Assert.That( + actualJson.RootElement.GetProperty("key").GetString(), + Is.EqualTo("value") + ); + } + } + } + + [TearDown] + public void TearDown() + { + _server.Dispose(); + _httpClient.Dispose(); + } + + private sealed class ContentDisposingHandler : DelegatingHandler + { + public ContentDisposingHandler(HttpMessageHandler inner) + : base(inner) { } + + protected override async global::System.Threading.Tasks.Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken + ) + { + var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + request.Content?.Dispose(); + return response; + } + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/WithRawResponseTests.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/WithRawResponseTests.cs new file mode 100644 index 000000000000..261754d1ce86 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Core/WithRawResponseTests.cs @@ -0,0 +1,269 @@ +using global::System.Net; +using global::System.Net.Http.Headers; +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Core; + +[TestFixture] +public class WithRawResponseTests +{ + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_DirectAwait_ReturnsData() + { + // Arrange + var expectedData = "test-data"; + var task = CreateWithRawResponseTask(expectedData, HttpStatusCode.OK); + + // Act + var result = await task; + + // Assert + Assert.That(result, Is.EqualTo(expectedData)); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_WithRawResponse_ReturnsDataAndMetadata() + { + // Arrange + var expectedData = "test-data"; + var expectedStatusCode = HttpStatusCode.Created; + var task = CreateWithRawResponseTask(expectedData, expectedStatusCode); + + // Act + var result = await task.WithRawResponse(); + + // Assert + Assert.That(result.Data, Is.EqualTo(expectedData)); + Assert.That(result.RawResponse.StatusCode, Is.EqualTo(expectedStatusCode)); + Assert.That(result.RawResponse.Url, Is.Not.Null); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValue_CaseInsensitive() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("X-Request-Id", "12345"); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act & Assert + Assert.That(headers.TryGetValue("X-Request-Id", out var value), Is.True); + Assert.That(value, Is.EqualTo("12345")); + + Assert.That(headers.TryGetValue("x-request-id", out value), Is.True); + Assert.That(value, Is.EqualTo("12345")); + + Assert.That(headers.TryGetValue("X-REQUEST-ID", out value), Is.True); + Assert.That(value, Is.EqualTo("12345")); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValues_ReturnsMultipleValues() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("Set-Cookie", new[] { "cookie1=value1", "cookie2=value2" }); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var success = headers.TryGetValues("Set-Cookie", out var values); + + // Assert + Assert.That(success, Is.True); + Assert.That(values, Is.Not.Null); + Assert.That(values!.Count(), Is.EqualTo(2)); + Assert.That(values, Does.Contain("cookie1=value1")); + Assert.That(values, Does.Contain("cookie2=value2")); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_ContentType_ReturnsValue() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Content = new StringContent( + "{}", + global::System.Text.Encoding.UTF8, + "application/json" + ); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var contentType = headers.ContentType; + + // Assert + Assert.That(contentType, Is.Not.Null); + Assert.That(contentType, Does.Contain("application/json")); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_ContentLength_ReturnsValue() + { + // Arrange + var content = "test content"; + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Content = new StringContent(content); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var contentLength = headers.ContentLength; + + // Assert + Assert.That(contentLength, Is.Not.Null); + Assert.That(contentLength, Is.GreaterThan(0)); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_Contains_ReturnsTrueForExistingHeader() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("X-Custom-Header", "value"); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act & Assert + Assert.That(headers.Contains("X-Custom-Header"), Is.True); + Assert.That(headers.Contains("x-custom-header"), Is.True); + Assert.That(headers.Contains("NonExistent"), Is.False); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_Enumeration_IncludesAllHeaders() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("X-Header-1", "value1"); + response.Headers.Add("X-Header-2", "value2"); + response.Content = new StringContent("test"); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var allHeaders = headers.ToList(); + + // Assert + Assert.That(allHeaders.Count, Is.GreaterThan(0)); + Assert.That(allHeaders.Any(h => h.Name == "X-Header-1"), Is.True); + Assert.That(allHeaders.Any(h => h.Name == "X-Header-2"), Is.True); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_ErrorStatusCode_StillReturnsMetadata() + { + // Arrange + var expectedData = "error-data"; + var task = CreateWithRawResponseTask(expectedData, HttpStatusCode.BadRequest); + + // Act + var result = await task.WithRawResponse(); + + // Assert + Assert.That(result.Data, Is.EqualTo(expectedData)); + Assert.That(result.RawResponse.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_Url_IsPreserved() + { + // Arrange + var expectedUrl = new Uri("https://api.example.com/users/123"); + var task = CreateWithRawResponseTask("data", HttpStatusCode.OK, expectedUrl); + + // Act + var result = await task.WithRawResponse(); + + // Assert + Assert.That(result.RawResponse.Url, Is.EqualTo(expectedUrl)); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValue_NonExistentHeader_ReturnsFalse() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var success = headers.TryGetValue("X-NonExistent", out var value); + + // Assert + Assert.That(success, Is.False); + Assert.That(value, Is.Null); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValues_NonExistentHeader_ReturnsFalse() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var success = headers.TryGetValues("X-NonExistent", out var values); + + // Assert + Assert.That(success, Is.False); + Assert.That(values, Is.Null); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_ImplicitConversion_ToTask() + { + // Arrange + var expectedData = "test-data"; + var task = CreateWithRawResponseTask(expectedData, HttpStatusCode.OK); + + // Act - implicitly convert to Task + global::System.Threading.Tasks.Task regularTask = task; + var result = await regularTask; + + // Assert + Assert.That(result, Is.EqualTo(expectedData)); + } + + [Test] + public void WithRawResponseTask_ImplicitConversion_AssignToTaskVariable() + { + // Arrange + var expectedData = "test-data"; + var wrappedTask = CreateWithRawResponseTask(expectedData, HttpStatusCode.OK); + + // Act - assign to Task variable + global::System.Threading.Tasks.Task regularTask = wrappedTask; + + // Assert + Assert.That(regularTask, Is.Not.Null); + Assert.That(regularTask, Is.InstanceOf>()); + } + + // Helper methods + + private static WithRawResponseTask CreateWithRawResponseTask( + T data, + HttpStatusCode statusCode, + Uri? url = null + ) + { + url ??= new Uri("https://api.example.com/test"); + using var httpResponse = CreateHttpResponse(statusCode); + httpResponse.RequestMessage = new HttpRequestMessage(HttpMethod.Get, url); + + var rawResponse = new RawResponse + { + StatusCode = statusCode, + Url = url, + Headers = ResponseHeaders.FromHttpResponseMessage(httpResponse), + }; + + var withRawResponse = new WithRawResponse { Data = data, RawResponse = rawResponse }; + + var task = global::System.Threading.Tasks.Task.FromResult(withRawResponse); + return new WithRawResponseTask(task); + } + + private static HttpResponseMessage CreateHttpResponse(HttpStatusCode statusCode) + { + return new HttpResponseMessage(statusCode) { Content = new StringContent("") }; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.Custom.props b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.Custom.props new file mode 100644 index 000000000000..aac9b5020d80 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.Custom.props @@ -0,0 +1,6 @@ + + diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.csproj b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.csproj new file mode 100644 index 000000000000..588c6413866d --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.csproj @@ -0,0 +1,39 @@ + + + net9.0 + 12 + enable + enable + false + true + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/TestClient.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/TestClient.cs new file mode 100644 index 000000000000..02e98ba9587b --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/TestClient.cs @@ -0,0 +1,6 @@ +using NUnit.Framework; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test; + +[TestFixture] +public class TestClient; diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Unit/MockServer/BaseMockServerTest.cs new file mode 100644 index 000000000000..7bc4f19c7278 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Unit/MockServer/BaseMockServerTest.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv; +using WireMock.Logging; +using WireMock.Server; +using WireMock.Settings; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Unit.MockServer; + +public class BaseMockServerTest +{ + protected WireMockServer Server { get; set; } = null!; + + protected SeedCsharpGlobalHeaderLiteralEnvClient Client { get; set; } = null!; + + protected RequestOptions RequestOptions { get; set; } = new(); + + [OneTimeSetUp] + public void GlobalSetup() + { + // Start the WireMock server + Server = WireMockServer.Start( + new WireMockServerSettings { Logger = new WireMockConsoleLogger() } + ); + + // Initialize the Client + Client = new SeedCsharpGlobalHeaderLiteralEnvClient( + "TOKEN", + "VERSION", + clientOptions: new ClientOptions { BaseUrl = Server.Urls[0], MaxRetries = 0 } + ); + } + + [OneTimeTearDown] + public void GlobalTeardown() + { + Server.Stop(); + Server.Dispose(); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Unit/MockServer/Service/GetWithLiteralVersionHeaderTest.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Unit/MockServer/Service/GetWithLiteralVersionHeaderTest.cs new file mode 100644 index 000000000000..2313f9cc0637 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Unit/MockServer/Service/GetWithLiteralVersionHeaderTest.cs @@ -0,0 +1,30 @@ +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Test.Unit.MockServer; +using SeedCsharpGlobalHeaderLiteralEnv.Test.Utils; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Unit.MockServer.Service; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class GetWithLiteralVersionHeaderTest : BaseMockServerTest +{ + [NUnit.Framework.Test] + public async Task MockServerTest() + { + const string mockResponse = """ + "string" + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/version").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Service.GetWithLiteralVersionHeaderAsync(); + JsonAssert.AreEqual(response, mockResponse); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/AdditionalPropertiesComparer.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/AdditionalPropertiesComparer.cs new file mode 100644 index 000000000000..96abaa8846e1 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/AdditionalPropertiesComparer.cs @@ -0,0 +1,219 @@ +using global::System.Text.Json; +using NUnit.Framework.Constraints; +using SeedCsharpGlobalHeaderLiteralEnv; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle AdditionalProperties values. +/// +public static class AdditionalPropertiesComparerExtensions +{ + /// + /// Modifies the EqualConstraint to handle AdditionalProperties instances by comparing their + /// serialized JSON representations. This handles the type mismatch between native C# types + /// and JsonElement values that occur when comparing manually constructed objects with + /// deserialized objects. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingAdditionalPropertiesComparer(this EqualConstraint constraint) + { + constraint.Using( + (x, y) => + { + if (x.Count != y.Count) + { + return false; + } + + foreach (var key in x.Keys) + { + if (!y.ContainsKey(key)) + { + return false; + } + + var xElement = JsonUtils.SerializeToElement(x[key]); + var yElement = JsonUtils.SerializeToElement(y[key]); + + if (!JsonElementsAreEqual(xElement, yElement)) + { + return false; + } + } + + return true; + } + ); + + return constraint; + } + + /// + /// Modifies the EqualConstraint to handle Dictionary<string, object?> values by comparing + /// their serialized JSON representations. This handles the type mismatch between native C# types + /// and JsonElement values that occur when comparing manually constructed objects with + /// deserialized objects. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingObjectDictionaryComparer(this EqualConstraint constraint) + { + constraint.Using>( + (x, y) => + { + if (x.Count != y.Count) + { + return false; + } + + foreach (var key in x.Keys) + { + if (!y.ContainsKey(key)) + { + return false; + } + + var xElement = JsonUtils.SerializeToElement(x[key]); + var yElement = JsonUtils.SerializeToElement(y[key]); + + if (!JsonElementsAreEqual(xElement, yElement)) + { + return false; + } + } + + return true; + } + ); + + return constraint; + } + + internal static bool JsonElementsAreEqualPublic(JsonElement x, JsonElement y) => + JsonElementsAreEqual(x, y); + + private static bool JsonElementsAreEqual(JsonElement x, JsonElement y) + { + if (x.ValueKind != y.ValueKind) + { + return false; + } + + return x.ValueKind switch + { + JsonValueKind.Object => CompareJsonObjects(x, y), + JsonValueKind.Array => CompareJsonArrays(x, y), + JsonValueKind.String => x.GetString() == y.GetString(), + JsonValueKind.Number => x.GetDecimal() == y.GetDecimal(), + JsonValueKind.True => true, + JsonValueKind.False => true, + JsonValueKind.Null => true, + _ => false, + }; + } + + private static bool CompareJsonObjects(JsonElement x, JsonElement y) + { + var xProps = new Dictionary(); + var yProps = new Dictionary(); + + foreach (var prop in x.EnumerateObject()) + xProps[prop.Name] = prop.Value; + + foreach (var prop in y.EnumerateObject()) + yProps[prop.Name] = prop.Value; + + if (xProps.Count != yProps.Count) + { + return false; + } + + foreach (var key in xProps.Keys) + { + if (!yProps.ContainsKey(key)) + { + return false; + } + + if (!JsonElementsAreEqual(xProps[key], yProps[key])) + { + return false; + } + } + + return true; + } + + private static bool CompareJsonArrays(JsonElement x, JsonElement y) + { + var xArray = x.EnumerateArray().ToList(); + var yArray = y.EnumerateArray().ToList(); + + if (xArray.Count != yArray.Count) + { + return false; + } + + for (var i = 0; i < xArray.Count; i++) + { + if (!JsonElementsAreEqual(xArray[i], yArray[i])) + { + return false; + } + } + + return true; + } + + /// + /// Modifies the EqualConstraint to handle cross-type comparisons involving JsonElement. + /// When UsingPropertiesComparer() walks object properties and encounters a property typed as + /// 'object', the expected side may be a Dictionary<object, object?> while the actual + /// (deserialized) side is a JsonElement. These typed predicates bridge that gap by serializing + /// the non-JsonElement side and comparing JSON representations. + /// + /// Uses typed Func<TExpected, TActual, bool> predicates instead of a non-generic + /// IComparer/IEqualityComparer so that NUnit's CanCompare type check ensures these only + /// fire when one side is a JsonElement, letting UsingPropertiesComparer() handle all + /// same-type comparisons normally. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingJsonSerializationComparer(this EqualConstraint constraint) + { + // Handle: expected is non-JsonElement, actual is JsonElement + constraint.Using( + (actualJsonElement, expectedObj) => + { + try + { + var expectedElement = JsonUtils.SerializeToElement(expectedObj); + return JsonElementsAreEqualPublic(expectedElement, actualJsonElement); + } + catch + { + return false; + } + } + ); + // Handle reverse: expected is JsonElement, actual is non-JsonElement + constraint.Using( + (actualObj, expectedJsonElement) => + { + try + { + var actualElement = JsonUtils.SerializeToElement(actualObj); + return JsonElementsAreEqualPublic(expectedJsonElement, actualElement); + } + catch + { + return false; + } + } + ); + return constraint; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/JsonAssert.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/JsonAssert.cs new file mode 100644 index 000000000000..9a3824db2c5a --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/JsonAssert.cs @@ -0,0 +1,33 @@ +using global::System.Text.Json; +using NUnit.Framework; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Test.Utils; + +internal static class JsonAssert +{ + /// + /// Asserts that the serialized JSON of an object equals the expected JSON string. + /// Uses JsonElement comparison for reliable deep equality of collections and union types. + /// + internal static void AreEqual(object actual, string expectedJson) + { + var actualElement = JsonUtils.SerializeToElement(actual); + var expectedElement = JsonUtils.Deserialize(expectedJson); + Assert.That(actualElement, Is.EqualTo(expectedElement).UsingJsonElementComparer()); + } + + /// + /// Asserts that the given JSON string survives a deserialization/serialization round-trip. + /// Deserializes to T, re-serializes to get the canonical form, then verifies a second + /// round-trip produces the same canonical form (idempotency). This accounts for serializer + /// options like WhenWritingNull that may normalize the output. + /// + internal static void Roundtrips(string json) + { + var deserialized = JsonUtils.Deserialize(json); + var serialized = JsonUtils.Serialize(deserialized!); + var deserialized2 = JsonUtils.Deserialize(serialized); + AreEqual(deserialized2!, serialized); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/JsonElementComparer.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/JsonElementComparer.cs new file mode 100644 index 000000000000..36fa0e9d4ed1 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/JsonElementComparer.cs @@ -0,0 +1,243 @@ +using global::System.Text.Json; +using NUnit.Framework.Constraints; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle JsonElement objects. +/// +public static class JsonElementComparerExtensions +{ + /// + /// Extension method for comparing JsonElement objects in NUnit tests. + /// Property order doesn't matter, but array order does matter. + /// Includes special handling for DateTime string formats. + /// + /// The Is.EqualTo() constraint instance. + /// A constraint that can compare JsonElements with detailed diffs. + public static EqualConstraint UsingJsonElementComparer(this EqualConstraint constraint) + { + return constraint.Using(new JsonElementComparer()); + } +} + +/// +/// Equality comparer for JsonElement with detailed reporting. +/// Property order doesn't matter, but array order does matter. +/// Now includes special handling for DateTime string formats with improved null handling. +/// +public class JsonElementComparer : IEqualityComparer +{ + private string _failurePath = string.Empty; + + /// + public bool Equals(JsonElement x, JsonElement y) + { + _failurePath = string.Empty; + return CompareJsonElements(x, y, string.Empty); + } + + /// + public int GetHashCode(JsonElement obj) + { + return JsonSerializer.Serialize(obj).GetHashCode(); + } + + private bool CompareJsonElements(JsonElement x, JsonElement y, string path) + { + // If value kinds don't match, they're not equivalent + if (x.ValueKind != y.ValueKind) + { + _failurePath = $"{path}: Expected {x.ValueKind} but got {y.ValueKind}"; + return false; + } + + switch (x.ValueKind) + { + case JsonValueKind.Object: + return CompareJsonObjects(x, y, path); + + case JsonValueKind.Array: + return CompareJsonArraysInOrder(x, y, path); + + case JsonValueKind.String: + string? xStr = x.GetString(); + string? yStr = y.GetString(); + + // Handle null strings + if (xStr is null && yStr is null) + return true; + + if (xStr is null || yStr is null) + { + _failurePath = + $"{path}: Expected {(xStr is null ? "null" : $"\"{xStr}\"")} but got {(yStr is null ? "null" : $"\"{yStr}\"")}"; + return false; + } + + // Check if they are identical strings + if (xStr == yStr) + return true; + + // Try to handle DateTime strings + if (IsLikelyDateTimeString(xStr) && IsLikelyDateTimeString(yStr)) + { + if (AreEquivalentDateTimeStrings(xStr, yStr)) + return true; + } + + _failurePath = $"{path}: Expected \"{xStr}\" but got \"{yStr}\""; + return false; + + case JsonValueKind.Number: + if (x.GetDecimal() != y.GetDecimal()) + { + if (x.GetDouble() != y.GetDouble()) + { + if (x.GetSingle() != y.GetSingle()) + { + _failurePath = + $"{path}: Expected {x.GetDecimal()} but got {y.GetDecimal()}"; + return false; + } + } + } + + return true; + + case JsonValueKind.True: + case JsonValueKind.False: + if (x.GetBoolean() != y.GetBoolean()) + { + _failurePath = $"{path}: Expected {x.GetBoolean()} but got {y.GetBoolean()}"; + return false; + } + + return true; + + case JsonValueKind.Null: + return true; + + default: + _failurePath = $"{path}: Unsupported JsonValueKind {x.ValueKind}"; + return false; + } + } + + private bool IsLikelyDateTimeString(string? str) + { + // Simple heuristic to identify likely ISO date time strings + return str is not null + && (str.Contains("T") && (str.EndsWith("Z") || str.Contains("+") || str.Contains("-"))); + } + + private bool AreEquivalentDateTimeStrings(string str1, string str2) + { + // Try to parse both as DateTime + if (DateTime.TryParse(str1, out DateTime dt1) && DateTime.TryParse(str2, out DateTime dt2)) + { + return dt1 == dt2; + } + + return false; + } + + private bool CompareJsonObjects(JsonElement x, JsonElement y, string path) + { + // Create dictionaries for both JSON objects + var xProps = new Dictionary(); + var yProps = new Dictionary(); + + foreach (var prop in x.EnumerateObject()) + xProps[prop.Name] = prop.Value; + + foreach (var prop in y.EnumerateObject()) + yProps[prop.Name] = prop.Value; + + // Check if all properties in x exist in y + foreach (var key in xProps.Keys) + { + if (!yProps.ContainsKey(key)) + { + _failurePath = $"{path}: Missing property '{key}'"; + return false; + } + } + + // Check if y has extra properties + foreach (var key in yProps.Keys) + { + if (!xProps.ContainsKey(key)) + { + _failurePath = $"{path}: Unexpected property '{key}'"; + return false; + } + } + + // Compare each property value + foreach (var key in xProps.Keys) + { + var propPath = string.IsNullOrEmpty(path) ? key : $"{path}.{key}"; + if (!CompareJsonElements(xProps[key], yProps[key], propPath)) + { + return false; + } + } + + return true; + } + + private bool CompareJsonArraysInOrder(JsonElement x, JsonElement y, string path) + { + var xArray = x.EnumerateArray(); + var yArray = y.EnumerateArray(); + + // Count x elements + var xCount = 0; + var xElements = new List(); + foreach (var item in xArray) + { + xElements.Add(item); + xCount++; + } + + // Count y elements + var yCount = 0; + var yElements = new List(); + foreach (var item in yArray) + { + yElements.Add(item); + yCount++; + } + + // Check if counts match + if (xCount != yCount) + { + _failurePath = $"{path}: Expected {xCount} items but found {yCount}"; + return false; + } + + // Compare elements in order + for (var i = 0; i < xCount; i++) + { + var itemPath = $"{path}[{i}]"; + if (!CompareJsonElements(xElements[i], yElements[i], itemPath)) + { + return false; + } + } + + return true; + } + + /// + public override string ToString() + { + if (!string.IsNullOrEmpty(_failurePath)) + { + return $"JSON comparison failed at {_failurePath}"; + } + + return "JsonElementEqualityComparer"; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/NUnitExtensions.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/NUnitExtensions.cs new file mode 100644 index 000000000000..816f4c010e6e --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/NUnitExtensions.cs @@ -0,0 +1,32 @@ +using NUnit.Framework.Constraints; + +namespace NUnit.Framework; + +/// +/// Extensions for NUnit constraints. +/// +public static class NUnitExtensions +{ + /// + /// Modifies the EqualConstraint to use our own set of default comparers. + /// + /// + /// + public static EqualConstraint UsingDefaults(this EqualConstraint constraint) => + constraint + .UsingPropertiesComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingOneOfComparer() + .UsingJsonElementComparer() + .UsingOptionalComparer() + .UsingObjectDictionaryComparer() + .UsingAdditionalPropertiesComparer() + .UsingJsonSerializationComparer(); +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/OneOfComparer.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/OneOfComparer.cs new file mode 100644 index 000000000000..dd091a148f2f --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/OneOfComparer.cs @@ -0,0 +1,100 @@ +using NUnit.Framework.Constraints; +using OneOf; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle OneOf values. +/// +public static class EqualConstraintExtensions +{ + /// + /// Modifies the EqualConstraint to handle OneOf instances by comparing their inner values. + /// This works alongside other comparison modifiers like UsingPropertiesComparer. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingOneOfComparer(this EqualConstraint constraint) + { + // Register a comparer factory for IOneOf types + constraint.Using( + (x, y) => + { + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (x.Value is null && y.Value is null) + { + return true; + } + + if (x.Value is null) + { + return false; + } + + // Undiscriminated unions of string enums are only distinguishable by their + // wire value: the concrete member type is not recoverable when deserializing, + // so two members with the same string value are considered equal. + if (x.Value is IStringEnum xStringEnum && y.Value is IStringEnum yStringEnum) + { + return xStringEnum.Value == yStringEnum.Value; + } + + var propertiesComparer = new NUnitEqualityComparer(); + var tolerance = Tolerance.Default; + propertiesComparer.CompareProperties = true; + // Add OneOf comparer to handle nested OneOf values (e.g., in Lists) + propertiesComparer.ExternalComparers.Add( + new OneOfEqualityAdapter(propertiesComparer) + ); + return propertiesComparer.AreEqual(x.Value, y.Value, ref tolerance); + } + ); + + return constraint; + } + + /// + /// EqualityAdapter for comparing IOneOf instances within NUnitEqualityComparer. + /// This enables recursive comparison of nested OneOf values. + /// + private class OneOfEqualityAdapter : EqualityAdapter + { + private readonly NUnitEqualityComparer _comparer; + + public OneOfEqualityAdapter(NUnitEqualityComparer comparer) + { + _comparer = comparer; + } + + public override bool CanCompare(object? x, object? y) + { + return x is IOneOf && y is IOneOf; + } + + public override bool AreEqual(object? x, object? y) + { + var oneOfX = (IOneOf?)x; + var oneOfY = (IOneOf?)y; + + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (oneOfX?.Value is null && oneOfY?.Value is null) + { + return true; + } + + if (oneOfX?.Value is null || oneOfY?.Value is null) + { + return false; + } + + if (oneOfX.Value is IStringEnum xStringEnum && oneOfY.Value is IStringEnum yStringEnum) + { + return xStringEnum.Value == yStringEnum.Value; + } + + var tolerance = Tolerance.Default; + return _comparer.AreEqual(oneOfX.Value, oneOfY.Value, ref tolerance); + } + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/OptionalComparer.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/OptionalComparer.cs new file mode 100644 index 000000000000..aae731a5dbda --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/OptionalComparer.cs @@ -0,0 +1,104 @@ +using NUnit.Framework.Constraints; +using OneOf; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle Optional values. +/// +public static class OptionalComparerExtensions +{ + /// + /// Modifies the EqualConstraint to handle Optional instances by comparing their IsDefined state and inner values. + /// This works alongside other comparison modifiers like UsingPropertiesComparer. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingOptionalComparer(this EqualConstraint constraint) + { + // Register a comparer factory for IOptional types + constraint.Using( + (x, y) => + { + // Both must have the same IsDefined state + if (x.IsDefined != y.IsDefined) + { + return false; + } + + // If both are undefined, they're equal + if (!x.IsDefined) + { + return true; + } + + // Both are defined, compare their boxed values + var xValue = x.GetBoxedValue(); + var yValue = y.GetBoxedValue(); + + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (xValue is null && yValue is null) + { + return true; + } + + if (xValue is null || yValue is null) + { + return false; + } + + // Use NUnit's property comparer for the inner values + var propertiesComparer = new NUnitEqualityComparer(); + var tolerance = Tolerance.Default; + propertiesComparer.CompareProperties = true; + // Add OneOf comparer to handle nested OneOf values (e.g., in Lists within Optional) + propertiesComparer.ExternalComparers.Add( + new OneOfEqualityAdapter(propertiesComparer) + ); + return propertiesComparer.AreEqual(xValue, yValue, ref tolerance); + } + ); + + return constraint; + } + + /// + /// EqualityAdapter for comparing IOneOf instances within NUnitEqualityComparer. + /// This enables recursive comparison of nested OneOf values within Optional types. + /// + private class OneOfEqualityAdapter : EqualityAdapter + { + private readonly NUnitEqualityComparer _comparer; + + public OneOfEqualityAdapter(NUnitEqualityComparer comparer) + { + _comparer = comparer; + } + + public override bool CanCompare(object? x, object? y) + { + return x is IOneOf && y is IOneOf; + } + + public override bool AreEqual(object? x, object? y) + { + var oneOfX = (IOneOf?)x; + var oneOfY = (IOneOf?)y; + + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (oneOfX?.Value is null && oneOfY?.Value is null) + { + return true; + } + + if (oneOfX?.Value is null || oneOfY?.Value is null) + { + return false; + } + + var tolerance = Tolerance.Default; + return _comparer.AreEqual(oneOfX.Value, oneOfY.Value, ref tolerance); + } + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/ReadOnlyMemoryComparer.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/ReadOnlyMemoryComparer.cs new file mode 100644 index 000000000000..fc0b595a5e54 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv.Test/Utils/ReadOnlyMemoryComparer.cs @@ -0,0 +1,87 @@ +using NUnit.Framework.Constraints; + +namespace NUnit.Framework; + +/// +/// Extensions for NUnit constraints. +/// +public static class ReadOnlyMemoryComparerExtensions +{ + /// + /// Extension method for comparing ReadOnlyMemory<T> in NUnit tests. + /// + /// The type of elements in the ReadOnlyMemory. + /// The Is.EqualTo() constraint instance. + /// A constraint that can compare ReadOnlyMemory<T>. + public static EqualConstraint UsingReadOnlyMemoryComparer(this EqualConstraint constraint) + where T : IComparable + { + return constraint.Using(new ReadOnlyMemoryComparer()); + } +} + +/// +/// Comparer for ReadOnlyMemory<T>. Compares sequences by value. +/// +/// +/// The type of elements in the ReadOnlyMemory. +/// +public class ReadOnlyMemoryComparer : IComparer> + where T : IComparable +{ + /// + public int Compare(ReadOnlyMemory x, ReadOnlyMemory y) + { + // Check if sequences are equal + var xSpan = x.Span; + var ySpan = y.Span; + + // Optimized case for IEquatable implementations + if (typeof(IEquatable).IsAssignableFrom(typeof(T))) + { + var areEqual = xSpan.SequenceEqual(ySpan); + if (areEqual) + { + return 0; // Sequences are equal + } + } + else + { + // Manual equality check for non-IEquatable types + if (xSpan.Length == ySpan.Length) + { + var areEqual = true; + for (var i = 0; i < xSpan.Length; i++) + { + if (!EqualityComparer.Default.Equals(xSpan[i], ySpan[i])) + { + areEqual = false; + break; + } + } + + if (areEqual) + { + return 0; // Sequences are equal + } + } + } + + // For non-equal sequences, we need to return a consistent ordering + // First compare lengths + if (x.Length != y.Length) + return x.Length.CompareTo(y.Length); + + // Same length but different content - compare first differing element + for (var i = 0; i < x.Length; i++) + { + if (!EqualityComparer.Default.Equals(xSpan[i], ySpan[i])) + { + return xSpan[i].CompareTo(ySpan[i]); + } + } + + // Should never reach here if not equal + return 0; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ApiResponse.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ApiResponse.cs new file mode 100644 index 000000000000..277a1380ec71 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ApiResponse.cs @@ -0,0 +1,13 @@ +using global::System.Net.Http; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// The response object returned from the API. +/// +internal record ApiResponse +{ + internal required int StatusCode { get; init; } + + internal required HttpResponseMessage Raw { get; init; } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/BaseRequest.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/BaseRequest.cs new file mode 100644 index 000000000000..fc1a1a631762 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/BaseRequest.cs @@ -0,0 +1,67 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; +using global::System.Text; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal abstract record BaseRequest +{ + internal string? BaseUrl { get; init; } + + internal required HttpMethod Method { get; init; } + + internal required string Path { get; init; } + + internal string? ContentType { get; init; } + + /// + /// The query string for this request (including the leading '?' if non-empty). + /// + internal string? QueryString { get; init; } + + internal Dictionary Headers { get; init; } = + new(StringComparer.OrdinalIgnoreCase); + + internal IRequestOptions? Options { get; init; } + + internal abstract HttpContent? CreateContent(); + + protected static ( + Encoding encoding, + string? charset, + string mediaType + ) ParseContentTypeOrDefault( + string? contentType, + Encoding encodingFallback, + string mediaTypeFallback + ) + { + var encoding = encodingFallback; + var mediaType = mediaTypeFallback; + string? charset = null; + if (string.IsNullOrEmpty(contentType)) + { + return (encoding, charset, mediaType); + } + + if (!MediaTypeHeaderValue.TryParse(contentType, out var mediaTypeHeaderValue)) + { + return (encoding, charset, mediaType); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.CharSet)) + { + charset = mediaTypeHeaderValue.CharSet; + encoding = Encoding.GetEncoding(mediaTypeHeaderValue.CharSet); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.MediaType)) + { + mediaType = mediaTypeHeaderValue.MediaType; + } + + return (encoding, charset, mediaType); + } + + protected static Encoding Utf8NoBom => EncodingCache.Utf8NoBom; +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/CollectionItemSerializer.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/CollectionItemSerializer.cs new file mode 100644 index 000000000000..eb1ff4526b82 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/CollectionItemSerializer.cs @@ -0,0 +1,91 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Json collection converter. +/// +/// Type of item to convert. +/// Converter to use for individual items. +internal class CollectionItemSerializer + : JsonConverter> + where TConverterType : JsonConverter, new() +{ + private static readonly TConverterType _converter = new TConverterType(); + + /// + /// Reads a json string and deserializes it into an object. + /// + /// Json reader. + /// Type to convert. + /// Serializer options. + /// Created object. + public override IEnumerable? Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + + var jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(_converter); + + var returnValue = new List(); + + while (reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + var item = (TDatatype)( + JsonSerializer.Deserialize(ref reader, typeof(TDatatype), jsonSerializerOptions) + ?? throw new global::System.Exception( + $"Failed to deserialize collection item of type {typeof(TDatatype)}" + ) + ); + returnValue.Add(item); + } + + reader.Read(); + } + + return returnValue; + } + + /// + /// Writes a json string. + /// + /// Json writer. + /// Value to write. + /// Serializer options. + public override void Write( + Utf8JsonWriter writer, + IEnumerable? value, + JsonSerializerOptions options + ) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + var jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(_converter); + + writer.WriteStartArray(); + + foreach (var data in value) + { + JsonSerializer.Serialize(writer, data, jsonSerializerOptions); + } + + writer.WriteEndArray(); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Constants.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Constants.cs new file mode 100644 index 000000000000..5242b3d8f2ef --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Constants.cs @@ -0,0 +1,7 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal static class Constants +{ + public const string DateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffK"; + public const string DateFormat = "yyyy-MM-dd"; +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DateOnlyConverter.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DateOnlyConverter.cs new file mode 100644 index 000000000000..f3623cc3a94a --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DateOnlyConverter.cs @@ -0,0 +1,747 @@ +// ReSharper disable All +#pragma warning disable + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using global::System.Diagnostics; +using global::System.Diagnostics.CodeAnalysis; +using global::System.Globalization; +using global::System.Runtime.CompilerServices; +using global::System.Runtime.InteropServices; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +// ReSharper disable SuggestVarOrType_SimpleTypes +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core +{ + /// + /// Custom converter for handling the data type with the System.Text.Json library. + /// + /// + /// This class backported from: + /// + /// System.Text.Json.Serialization.Converters.DateOnlyConverter + /// + public sealed class DateOnlyConverter : JsonConverter + { + private const int FormatLength = 10; // YYYY-MM-DD + + private const int MaxEscapedFormatLength = + FormatLength * JsonConstants.MaxExpansionFactorWhileEscaping; + + /// + public override DateOnly Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType != JsonTokenType.String) + { + ThrowHelper.ThrowInvalidOperationException_ExpectedString(reader.TokenType); + } + + return ReadCore(ref reader); + } + + /// + public override DateOnly ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + return ReadCore(ref reader); + } + + private static DateOnly ReadCore(ref Utf8JsonReader reader) + { + if ( + !JsonHelpers.IsInRangeInclusive( + reader.ValueLength(), + FormatLength, + MaxEscapedFormatLength + ) + ) + { + ThrowHelper.ThrowFormatException(DataType.DateOnly); + } + + scoped ReadOnlySpan source; + if (!reader.HasValueSequence && !reader.ValueIsEscaped) + { + source = reader.ValueSpan; + } + else + { + Span stackSpan = stackalloc byte[MaxEscapedFormatLength]; + int bytesWritten = reader.CopyString(stackSpan); + source = stackSpan.Slice(0, bytesWritten); + } + + if (!JsonHelpers.TryParseAsIso(source, out DateOnly value)) + { + ThrowHelper.ThrowFormatException(DataType.DateOnly); + } + + return value; + } + + /// + public override void Write( + Utf8JsonWriter writer, + DateOnly value, + JsonSerializerOptions options + ) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[FormatLength]; +#else + Span buffer = stackalloc char[FormatLength]; +#endif + // ReSharper disable once RedundantAssignment + bool formattedSuccessfully = value.TryFormat( + buffer, + out int charsWritten, + "O".AsSpan(), + CultureInfo.InvariantCulture + ); + Debug.Assert(formattedSuccessfully && charsWritten == FormatLength); + writer.WriteStringValue(buffer); + } + + /// + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + DateOnly value, + JsonSerializerOptions options + ) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[FormatLength]; +#else + Span buffer = stackalloc char[FormatLength]; +#endif + // ReSharper disable once RedundantAssignment + bool formattedSuccessfully = value.TryFormat( + buffer, + out int charsWritten, + "O".AsSpan(), + CultureInfo.InvariantCulture + ); + Debug.Assert(formattedSuccessfully && charsWritten == FormatLength); + writer.WritePropertyName(buffer); + } + } + + internal static class JsonConstants + { + // The maximum number of fraction digits the Json DateTime parser allows + public const int DateTimeParseNumFractionDigits = 16; + + // In the worst case, an ASCII character represented as a single utf-8 byte could expand 6x when escaped. + public const int MaxExpansionFactorWhileEscaping = 6; + + // The largest fraction expressible by TimeSpan and DateTime formats + public const int MaxDateTimeFraction = 9_999_999; + + // TimeSpan and DateTime formats allow exactly up to many digits for specifying the fraction after the seconds. + public const int DateTimeNumFractionDigits = 7; + + public const byte UtcOffsetToken = (byte)'Z'; + + public const byte TimePrefix = (byte)'T'; + + public const byte Period = (byte)'.'; + + public const byte Hyphen = (byte)'-'; + + public const byte Colon = (byte)':'; + + public const byte Plus = (byte)'+'; + } + + // ReSharper disable SuggestVarOrType_Elsewhere + // ReSharper disable SuggestVarOrType_SimpleTypes + // ReSharper disable SuggestVarOrType_BuiltInTypes + + internal static class JsonHelpers + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInRangeInclusive(int value, int lowerBound, int upperBound) => + (uint)(value - lowerBound) <= (uint)(upperBound - lowerBound); + + public static bool IsDigit(byte value) => (uint)(value - '0') <= '9' - '0'; + + [StructLayout(LayoutKind.Auto)] + private struct DateTimeParseData + { + public int Year; + public int Month; + public int Day; + public bool IsCalendarDateOnly; + public int Hour; + public int Minute; + public int Second; + public int Fraction; // This value should never be greater than 9_999_999. + public int OffsetHours; + public int OffsetMinutes; + + // ReSharper disable once NotAccessedField.Local + public byte OffsetToken; + } + + public static bool TryParseAsIso(ReadOnlySpan source, out DateOnly value) + { + if ( + TryParseDateTimeOffset(source, out DateTimeParseData parseData) + && parseData.IsCalendarDateOnly + && TryCreateDateTime(parseData, DateTimeKind.Unspecified, out DateTime dateTime) + ) + { + value = DateOnly.FromDateTime(dateTime); + return true; + } + + value = default; + return false; + } + + /// + /// ISO 8601 date time parser (ISO 8601-1:2019). + /// + /// The date/time to parse in UTF-8 format. + /// The parsed for the given . + /// + /// Supports extended calendar date (5.2.2.1) and complete (5.4.2.1) calendar date/time of day + /// representations with optional specification of seconds and fractional seconds. + /// + /// Times can be explicitly specified as UTC ("Z" - 5.3.3) or offsets from UTC ("+/-hh:mm" 5.3.4.2). + /// If unspecified they are considered to be local per spec. + /// + /// Examples: (TZD is either "Z" or hh:mm offset from UTC) + /// + /// YYYY-MM-DD (e.g. 1997-07-16) + /// YYYY-MM-DDThh:mm (e.g. 1997-07-16T19:20) + /// YYYY-MM-DDThh:mm:ss (e.g. 1997-07-16T19:20:30) + /// YYYY-MM-DDThh:mm:ss.s (e.g. 1997-07-16T19:20:30.45) + /// YYYY-MM-DDThh:mmTZD (e.g. 1997-07-16T19:20+01:00) + /// YYYY-MM-DDThh:mm:ssTZD (e.g. 1997-07-16T19:20:3001:00) + /// YYYY-MM-DDThh:mm:ss.sTZD (e.g. 1997-07-16T19:20:30.45Z) + /// + /// Generally speaking we always require the "extended" option when one exists (3.1.3.5). + /// The extended variants have separator characters between components ('-', ':', '.', etc.). + /// Spaces are not permitted. + /// + /// "true" if successfully parsed. + private static bool TryParseDateTimeOffset( + ReadOnlySpan source, + out DateTimeParseData parseData + ) + { + parseData = default; + + // too short datetime + Debug.Assert(source.Length >= 10); + + // Parse the calendar date + // ----------------------- + // ISO 8601-1:2019 5.2.2.1b "Calendar date complete extended format" + // [dateX] = [year]["-"][month]["-"][day] + // [year] = [YYYY] [0000 - 9999] (4.3.2) + // [month] = [MM] [01 - 12] (4.3.3) + // [day] = [DD] [01 - 28, 29, 30, 31] (4.3.4) + // + // Note: 5.2.2.2 "Representations with reduced precision" allows for + // just [year]["-"][month] (a) and just [year] (b), but we currently + // don't permit it. + + { + uint digit1 = source[0] - (uint)'0'; + uint digit2 = source[1] - (uint)'0'; + uint digit3 = source[2] - (uint)'0'; + uint digit4 = source[3] - (uint)'0'; + + if (digit1 > 9 || digit2 > 9 || digit3 > 9 || digit4 > 9) + { + return false; + } + + parseData.Year = (int)(digit1 * 1000 + digit2 * 100 + digit3 * 10 + digit4); + } + + if ( + source[4] != JsonConstants.Hyphen + || !TryGetNextTwoDigits(source.Slice(start: 5, length: 2), ref parseData.Month) + || source[7] != JsonConstants.Hyphen + || !TryGetNextTwoDigits(source.Slice(start: 8, length: 2), ref parseData.Day) + ) + { + return false; + } + + // We now have YYYY-MM-DD [dateX] + // ReSharper disable once ConvertIfStatementToSwitchStatement + if (source.Length == 10) + { + parseData.IsCalendarDateOnly = true; + return true; + } + + // Parse the time of day + // --------------------- + // + // ISO 8601-1:2019 5.3.1.2b "Local time of day complete extended format" + // [timeX] = ["T"][hour][":"][min][":"][sec] + // [hour] = [hh] [00 - 23] (4.3.8a) + // [minute] = [mm] [00 - 59] (4.3.9a) + // [sec] = [ss] [00 - 59, 60 with a leap second] (4.3.10a) + // + // ISO 8601-1:2019 5.3.3 "UTC of day" + // [timeX]["Z"] + // + // ISO 8601-1:2019 5.3.4.2 "Local time of day with the time shift between + // local timescale and UTC" (Extended format) + // + // [shiftX] = ["+"|"-"][hour][":"][min] + // + // Notes: + // + // "T" is optional per spec, but _only_ when times are used alone. In our + // case, we're reading out a complete date & time and as such require "T". + // (5.4.2.1b). + // + // For [timeX] We allow seconds to be omitted per 5.3.1.3a "Representations + // with reduced precision". 5.3.1.3b allows just specifying the hour, but + // we currently don't permit this. + // + // Decimal fractions are allowed for hours, minutes and seconds (5.3.14). + // We only allow fractions for seconds currently. Lower order components + // can't follow, i.e. you can have T23.3, but not T23.3:04. There must be + // one digit, but the max number of digits is implementation defined. We + // currently allow up to 16 digits of fractional seconds only. While we + // support 16 fractional digits we only parse the first seven, anything + // past that is considered a zero. This is to stay compatible with the + // DateTime implementation which is limited to this resolution. + + if (source.Length < 16) + { + // Source does not have enough characters for YYYY-MM-DDThh:mm + return false; + } + + // Parse THH:MM (e.g. "T10:32") + if ( + source[10] != JsonConstants.TimePrefix + || source[13] != JsonConstants.Colon + || !TryGetNextTwoDigits(source.Slice(start: 11, length: 2), ref parseData.Hour) + || !TryGetNextTwoDigits(source.Slice(start: 14, length: 2), ref parseData.Minute) + ) + { + return false; + } + + // We now have YYYY-MM-DDThh:mm + Debug.Assert(source.Length >= 16); + if (source.Length == 16) + { + return true; + } + + byte curByte = source[16]; + int sourceIndex = 17; + + // Either a TZD ['Z'|'+'|'-'] or a seconds separator [':'] is valid at this point + switch (curByte) + { + case JsonConstants.UtcOffsetToken: + parseData.OffsetToken = JsonConstants.UtcOffsetToken; + return sourceIndex == source.Length; + case JsonConstants.Plus: + case JsonConstants.Hyphen: + parseData.OffsetToken = curByte; + return ParseOffset(ref parseData, source.Slice(sourceIndex)); + case JsonConstants.Colon: + break; + default: + return false; + } + + // Try reading the seconds + if ( + source.Length < 19 + || !TryGetNextTwoDigits(source.Slice(start: 17, length: 2), ref parseData.Second) + ) + { + return false; + } + + // We now have YYYY-MM-DDThh:mm:ss + Debug.Assert(source.Length >= 19); + if (source.Length == 19) + { + return true; + } + + curByte = source[19]; + sourceIndex = 20; + + // Either a TZD ['Z'|'+'|'-'] or a seconds decimal fraction separator ['.'] is valid at this point + switch (curByte) + { + case JsonConstants.UtcOffsetToken: + parseData.OffsetToken = JsonConstants.UtcOffsetToken; + return sourceIndex == source.Length; + case JsonConstants.Plus: + case JsonConstants.Hyphen: + parseData.OffsetToken = curByte; + return ParseOffset(ref parseData, source.Slice(sourceIndex)); + case JsonConstants.Period: + break; + default: + return false; + } + + // Source does not have enough characters for second fractions (i.e. ".s") + // YYYY-MM-DDThh:mm:ss.s + if (source.Length < 21) + { + return false; + } + + // Parse fraction. This value should never be greater than 9_999_999 + int numDigitsRead = 0; + int fractionEnd = Math.Min( + sourceIndex + JsonConstants.DateTimeParseNumFractionDigits, + source.Length + ); + + while (sourceIndex < fractionEnd && IsDigit(curByte = source[sourceIndex])) + { + if (numDigitsRead < JsonConstants.DateTimeNumFractionDigits) + { + parseData.Fraction = parseData.Fraction * 10 + (int)(curByte - (uint)'0'); + numDigitsRead++; + } + + sourceIndex++; + } + + if (parseData.Fraction != 0) + { + while (numDigitsRead < JsonConstants.DateTimeNumFractionDigits) + { + parseData.Fraction *= 10; + numDigitsRead++; + } + } + + // We now have YYYY-MM-DDThh:mm:ss.s + Debug.Assert(sourceIndex <= source.Length); + if (sourceIndex == source.Length) + { + return true; + } + + curByte = source[sourceIndex++]; + + // TZD ['Z'|'+'|'-'] is valid at this point + switch (curByte) + { + case JsonConstants.UtcOffsetToken: + parseData.OffsetToken = JsonConstants.UtcOffsetToken; + return sourceIndex == source.Length; + case JsonConstants.Plus: + case JsonConstants.Hyphen: + parseData.OffsetToken = curByte; + return ParseOffset(ref parseData, source.Slice(sourceIndex)); + default: + return false; + } + + static bool ParseOffset(ref DateTimeParseData parseData, ReadOnlySpan offsetData) + { + // Parse the hours for the offset + if ( + offsetData.Length < 2 + || !TryGetNextTwoDigits(offsetData.Slice(0, 2), ref parseData.OffsetHours) + ) + { + return false; + } + + // We now have YYYY-MM-DDThh:mm:ss.s+|-hh + + if (offsetData.Length == 2) + { + // Just hours offset specified + return true; + } + + // Ensure we have enough for ":mm" + return offsetData.Length == 5 + && offsetData[2] == JsonConstants.Colon + && TryGetNextTwoDigits(offsetData.Slice(3), ref parseData.OffsetMinutes); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + // ReSharper disable once RedundantAssignment + private static bool TryGetNextTwoDigits(ReadOnlySpan source, ref int value) + { + Debug.Assert(source.Length == 2); + + uint digit1 = source[0] - (uint)'0'; + uint digit2 = source[1] - (uint)'0'; + + if (digit1 > 9 || digit2 > 9) + { + value = 0; + return false; + } + + value = (int)(digit1 * 10 + digit2); + return true; + } + + // The following methods are borrowed verbatim from src/Common/src/CoreLib/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.Helpers.cs + + /// + /// Overflow-safe DateTime factory. + /// + private static bool TryCreateDateTime( + DateTimeParseData parseData, + DateTimeKind kind, + out DateTime value + ) + { + if (parseData.Year == 0) + { + value = default; + return false; + } + + Debug.Assert(parseData.Year <= 9999); // All of our callers to date parse the year from fixed 4-digit fields so this value is trusted. + + if ((uint)parseData.Month - 1 >= 12) + { + value = default; + return false; + } + + uint dayMinusOne = (uint)parseData.Day - 1; + if ( + dayMinusOne >= 28 + && dayMinusOne >= DateTime.DaysInMonth(parseData.Year, parseData.Month) + ) + { + value = default; + return false; + } + + if ((uint)parseData.Hour > 23) + { + value = default; + return false; + } + + if ((uint)parseData.Minute > 59) + { + value = default; + return false; + } + + // This needs to allow leap seconds when appropriate. + // See https://github.com/dotnet/runtime/issues/30135. + if ((uint)parseData.Second > 59) + { + value = default; + return false; + } + + Debug.Assert(parseData.Fraction is >= 0 and <= JsonConstants.MaxDateTimeFraction); // All of our callers to date parse the fraction from fixed 7-digit fields so this value is trusted. + + ReadOnlySpan days = DateTime.IsLeapYear(parseData.Year) + ? DaysToMonth366 + : DaysToMonth365; + int yearMinusOne = parseData.Year - 1; + int totalDays = + yearMinusOne * 365 + + yearMinusOne / 4 + - yearMinusOne / 100 + + yearMinusOne / 400 + + days[parseData.Month - 1] + + parseData.Day + - 1; + long ticks = totalDays * TimeSpan.TicksPerDay; + int totalSeconds = parseData.Hour * 3600 + parseData.Minute * 60 + parseData.Second; + ticks += totalSeconds * TimeSpan.TicksPerSecond; + ticks += parseData.Fraction; + value = new DateTime(ticks: ticks, kind: kind); + return true; + } + + private static ReadOnlySpan DaysToMonth365 => + [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]; + private static ReadOnlySpan DaysToMonth366 => + [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]; + } + + internal static class ThrowHelper + { + private const string ExceptionSourceValueToRethrowAsJsonException = + "System.Text.Json.Rethrowable"; + + [DoesNotReturn] + public static void ThrowInvalidOperationException_ExpectedString(JsonTokenType tokenType) + { + throw GetInvalidOperationException("string", tokenType); + } + + public static void ThrowFormatException(DataType dataType) + { + throw new FormatException(SR.Format(SR.UnsupportedFormat, dataType)) + { + Source = ExceptionSourceValueToRethrowAsJsonException, + }; + } + + private static global::System.Exception GetInvalidOperationException( + string message, + JsonTokenType tokenType + ) + { + return GetInvalidOperationException(SR.Format(SR.InvalidCast, tokenType, message)); + } + + private static InvalidOperationException GetInvalidOperationException(string message) + { + return new InvalidOperationException(message) + { + Source = ExceptionSourceValueToRethrowAsJsonException, + }; + } + } + + internal static class Utf8JsonReaderExtensions + { + internal static int ValueLength(this Utf8JsonReader reader) => + reader.HasValueSequence + ? checked((int)reader.ValueSequence.Length) + : reader.ValueSpan.Length; + } + + internal enum DataType + { + TimeOnly, + DateOnly, + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal static class SR + { + private static readonly bool s_usingResourceKeys = + AppContext.TryGetSwitch( + "System.Resources.UseSystemResourceKeys", + out bool usingResourceKeys + ) && usingResourceKeys; + + public static string UnsupportedFormat => Strings.UnsupportedFormat; + + public static string InvalidCast => Strings.InvalidCast; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string Format(string resourceFormat, object? p1) => + s_usingResourceKeys + ? string.Join(", ", resourceFormat, p1) + : string.Format(resourceFormat, p1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string Format(string resourceFormat, object? p1, object? p2) => + s_usingResourceKeys + ? string.Join(", ", resourceFormat, p1, p2) + : string.Format(resourceFormat, p1, p2); + } + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute( + "System.Resources.Tools.StronglyTypedResourceBuilder", + "17.0.0.0" + )] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings + { + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( + "Microsoft.Performance", + "CA1811:AvoidUncalledPrivateCode" + )] + internal Strings() { } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute( + global::System.ComponentModel.EditorBrowsableState.Advanced + )] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { + global::System.Resources.ResourceManager temp = + new global::System.Resources.ResourceManager( + "System.Text.Json.Resources.Strings", + typeof(Strings).Assembly + ); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute( + global::System.ComponentModel.EditorBrowsableState.Advanced + )] + internal static global::System.Globalization.CultureInfo Culture + { + get { return resourceCulture; } + set { resourceCulture = value; } + } + + /// + /// Looks up a localized string similar to Cannot get the value of a token type '{0}' as a {1}.. + /// + internal static string InvalidCast + { + get { return ResourceManager.GetString("InvalidCast", resourceCulture); } + } + + /// + /// Looks up a localized string similar to The JSON value is not in a supported {0} format.. + /// + internal static string UnsupportedFormat + { + get { return ResourceManager.GetString("UnsupportedFormat", resourceCulture); } + } + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DateTimeSerializer.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DateTimeSerializer.cs new file mode 100644 index 000000000000..9648a8accf9e --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DateTimeSerializer.cs @@ -0,0 +1,40 @@ +using global::System.Globalization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal class DateTimeSerializer : JsonConverter +{ + public override DateTime Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + return DateTime.Parse(reader.GetString()!, null, DateTimeStyles.RoundtripKind); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(Constants.DateTimeFormat)); + } + + public override DateTime ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + return DateTime.Parse(reader.GetString()!, null, DateTimeStyles.RoundtripKind); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + DateTime value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.ToString(Constants.DateTimeFormat)); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DefaultHttpClientFactory.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DefaultHttpClientFactory.cs new file mode 100644 index 000000000000..8f7699a2da5b --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/DefaultHttpClientFactory.cs @@ -0,0 +1,25 @@ +using global::System.Net; +using global::System.Net.Http; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Creates the default used by the SDK, with automatic +/// response decompression enabled so that gzip/deflate encoded response bodies +/// are decompressed based on the response's Content-Encoding header. +/// +internal static class DefaultHttpClientFactory +{ + internal static HttpClient Create() + { + var handler = new HttpClientHandler + { +#if NET5_0_OR_GREATER + AutomaticDecompression = DecompressionMethods.All, +#else + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, +#endif + }; + return new HttpClient(handler); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/EmptyRequest.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/EmptyRequest.cs new file mode 100644 index 000000000000..9051a0054bd7 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/EmptyRequest.cs @@ -0,0 +1,11 @@ +using global::System.Net.Http; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// The request object to send without a request body. +/// +internal record EmptyRequest : BaseRequest +{ + internal override HttpContent? CreateContent() => null; +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/EncodingCache.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/EncodingCache.cs new file mode 100644 index 000000000000..4700fd9b1945 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/EncodingCache.cs @@ -0,0 +1,11 @@ +using global::System.Text; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal static class EncodingCache +{ + internal static readonly Encoding Utf8NoBom = new UTF8Encoding( + encoderShouldEmitUTF8Identifier: false, + throwOnInvalidBytes: true + ); +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Extensions.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Extensions.cs new file mode 100644 index 000000000000..7cd2007ffd45 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Extensions.cs @@ -0,0 +1,55 @@ +using global::System.Diagnostics.CodeAnalysis; +using global::System.Runtime.Serialization; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal static class Extensions +{ + public static string Stringify(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + if (field is not null) + { + var attribute = (EnumMemberAttribute?) + global::System.Attribute.GetCustomAttribute(field, typeof(EnumMemberAttribute)); + return attribute?.Value ?? value.ToString(); + } + return value.ToString(); + } + + /// + /// Asserts that a condition is true, throwing an exception with the specified message if it is false. + /// + /// The condition to assert. + /// The exception message if the assertion fails. + /// Thrown when the condition is false. + internal static void Assert(this object value, bool condition, string message) + { + if (!condition) + { + throw new global::System.Exception(message); + } + } + + /// + /// Asserts that a value is not null, throwing an exception with the specified message if it is null. + /// + /// The type of the value to assert. + /// The value to assert is not null. + /// The exception message if the assertion fails. + /// The non-null value. + /// Thrown when the value is null. + internal static TValue Assert( + this object _unused, + [NotNull] TValue? value, + string message + ) + where TValue : class + { + if (value is null) + { + throw new global::System.Exception(message); + } + return value; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/FormUrlEncoder.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/FormUrlEncoder.cs new file mode 100644 index 000000000000..e03938f2ae4d --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/FormUrlEncoder.cs @@ -0,0 +1,33 @@ +using global::System.Net.Http; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Encodes an object into a form URL-encoded content. +/// +public static class FormUrlEncoder +{ + /// + /// Encodes an object into a form URL-encoded content using Deep Object notation. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + internal static FormUrlEncodedContent EncodeAsDeepObject(object value) => + new(QueryStringConverter.ToDeepObject(value)); + + /// + /// Encodes an object into a form URL-encoded content using Exploded Form notation. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + internal static FormUrlEncodedContent EncodeAsExplodedForm(object value) => + new(QueryStringConverter.ToExplodedForm(value)); + + /// + /// Encodes an object into a form URL-encoded content using Form notation without exploding parameters. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + internal static FormUrlEncodedContent EncodeAsForm(object value) => + new(QueryStringConverter.ToForm(value)); +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HeaderValue.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HeaderValue.cs new file mode 100644 index 000000000000..4c5efa16b352 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HeaderValue.cs @@ -0,0 +1,52 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal sealed class HeaderValue +{ + private readonly Func> _resolver; + + public HeaderValue(string value) + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value); + } + + public HeaderValue(Func value) + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value()); + } + + public HeaderValue(Func> value) + { + _resolver = value; + } + + public HeaderValue(Func> value) + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value()); + } + + public static implicit operator HeaderValue(string value) => new(value); + + public static implicit operator HeaderValue(Func value) => new(value); + + public static implicit operator HeaderValue( + Func> value + ) => new(value); + + public static implicit operator HeaderValue( + Func> value + ) => new(value); + + public static HeaderValue FromString(string value) => new(value); + + public static HeaderValue FromFunc(Func value) => new(value); + + public static HeaderValue FromValueTaskFunc( + Func> value + ) => new(value); + + public static HeaderValue FromTaskFunc( + Func> value + ) => new(value); + + internal global::System.Threading.Tasks.ValueTask ResolveAsync() => _resolver(); +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Headers.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Headers.cs new file mode 100644 index 000000000000..84d63e9c0cfd --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Headers.cs @@ -0,0 +1,28 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Represents the headers sent with the request. +/// +internal sealed class Headers : Dictionary +{ + internal Headers() { } + + /// + /// Initializes a new instance of the Headers class with the specified value. + /// + /// + internal Headers(Dictionary value) + { + foreach (var kvp in value) + { + this[kvp.Key] = kvp.Value; + } + } + + /// + /// Initializes a new instance of the Headers class with the specified value. + /// + /// + internal Headers(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HeadersBuilder.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HeadersBuilder.cs new file mode 100644 index 000000000000..d02fc8a87f74 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HeadersBuilder.cs @@ -0,0 +1,197 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Fluent builder for constructing HTTP headers with support for merging from multiple sources. +/// Provides a clean API for building headers with proper precedence handling. +/// +internal static class HeadersBuilder +{ + /// + /// Fluent builder for constructing HTTP headers. + /// + public sealed class Builder + { + private readonly Dictionary _headers; + + /// + /// Initializes a new instance with default capacity. + /// Uses case-insensitive header name comparison. + /// + public Builder() + { + _headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Initializes a new instance with the specified initial capacity. + /// Uses case-insensitive header name comparison. + /// + public Builder(int capacity) + { + _headers = new Dictionary( + capacity, + StringComparer.OrdinalIgnoreCase + ); + } + + /// + /// Adds a header with the specified key and value. + /// If a header with the same key already exists, it will be overwritten. + /// Null values are ignored. + /// + /// The header name. + /// The header value. Null values are ignored. + /// This builder instance for method chaining. + public Builder Add(string key, string? value) + { + if (value is not null) + { + _headers[key] = (value); + } + return this; + } + + /// + /// Adds a header with the specified key and object value. + /// The value will be converted to string using ValueConvert for consistent serialization. + /// If a header with the same key already exists, it will be overwritten. + /// Null values are ignored. + /// + /// The header name. + /// The header value. Null values are ignored. + /// This builder instance for method chaining. + public Builder Add(string key, object? value) + { + if (value is null) + { + return this; + } + + // Use ValueConvert for consistent serialization across headers, query params, and path params + var stringValue = ValueConvert.ToString(value); + if (stringValue is not null) + { + _headers[key] = (stringValue); + } + return this; + } + + /// + /// Adds multiple headers from a Headers dictionary. + /// HeaderValue instances are stored and will be resolved when BuildAsync() is called. + /// Overwrites any existing headers with the same key. + /// Null entries are ignored. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder Add(Headers? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + _headers[header.Key] = header.Value; + } + + return this; + } + + /// + /// Adds multiple headers from a Headers dictionary, excluding the Authorization header. + /// This is useful for endpoints that don't require authentication, to avoid triggering + /// lazy auth token resolution. + /// HeaderValue instances are stored and will be resolved when BuildAsync() is called. + /// Overwrites any existing headers with the same key. + /// Null entries are ignored. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder AddWithoutAuth(Headers? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + if (header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + _headers[header.Key] = header.Value; + } + + return this; + } + + /// + /// Adds multiple headers from a key-value pair collection. + /// Overwrites any existing headers with the same key. + /// Null values are ignored. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder Add(IEnumerable>? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + if (header.Value is not null) + { + _headers[header.Key] = (header.Value); + } + } + + return this; + } + + /// + /// Adds multiple headers from a dictionary. + /// Overwrites any existing headers with the same key. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder Add(Dictionary? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + _headers[header.Key] = (header.Value); + } + + return this; + } + + /// + /// Asynchronously builds the final headers dictionary containing all merged headers. + /// Resolves all HeaderValue instances that may contain async operations. + /// Returns a case-insensitive dictionary. + /// + /// A task that represents the asynchronous operation, containing a case-insensitive dictionary of headers. + public async global::System.Threading.Tasks.Task> BuildAsync() + { + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in _headers) + { + var value = await kvp.Value.ResolveAsync().ConfigureAwait(false); + if (value is not null) + { + headers[kvp.Key] = value; + } + } + return headers; + } + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HttpContentExtensions.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HttpContentExtensions.cs new file mode 100644 index 000000000000..92018cf3655f --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HttpContentExtensions.cs @@ -0,0 +1,20 @@ +#if !NET5_0_OR_GREATER +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Polyfill extension providing a ReadAsStringAsync(CancellationToken) overload +/// for target frameworks older than .NET 5, where only the parameterless +/// ReadAsStringAsync() is available. +/// +internal static class HttpContentExtensions +{ + internal static Task ReadAsStringAsync( + this HttpContent httpContent, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return httpContent.ReadAsStringAsync(); + } +} +#endif diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HttpMethodExtensions.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HttpMethodExtensions.cs new file mode 100644 index 000000000000..1f7b9aab703d --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/HttpMethodExtensions.cs @@ -0,0 +1,8 @@ +using global::System.Net.Http; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal static class HttpMethodExtensions +{ + public static readonly HttpMethod Patch = new("PATCH"); +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/IIsRetryableContent.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/IIsRetryableContent.cs new file mode 100644 index 000000000000..56aaab0846f3 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/IIsRetryableContent.cs @@ -0,0 +1,6 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +public interface IIsRetryableContent +{ + public bool IsRetryable { get; } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/IRequestOptions.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/IRequestOptions.cs new file mode 100644 index 000000000000..67816e0cc64b --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/IRequestOptions.cs @@ -0,0 +1,83 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal interface IRequestOptions +{ + /// + /// The Base URL for the API. + /// + public string? BaseUrl { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The http client used to make requests. + /// + public HttpClient? HttpClient { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with the request. + /// Headers previously set with matching keys will be overwritten. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The max number of retries to attempt. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional query parameters sent with the request. + /// + public IEnumerable> AdditionalQueryParameters { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional body properties sent with the request. + /// This is only applied to JSON requests. + /// + public object? AdditionalBodyProperties { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonAccessAttribute.cs new file mode 100644 index 000000000000..a7474bfa0961 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonAccessAttribute.cs @@ -0,0 +1,15 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +[global::System.AttributeUsage( + global::System.AttributeTargets.Property | global::System.AttributeTargets.Field +)] +internal class JsonAccessAttribute(JsonAccessType accessType) : global::System.Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonConfiguration.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonConfiguration.cs new file mode 100644 index 000000000000..074bf1fd21e1 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonConfiguration.cs @@ -0,0 +1,275 @@ +using global::System.Reflection; +using global::System.Text.Encodings.Web; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal static partial class JsonOptions +{ + internal static readonly JsonSerializerOptions JsonSerializerOptions; + internal static readonly JsonSerializerOptions JsonSerializerOptionsRelaxedEscaping; + + static JsonOptions() + { + var options = new JsonSerializerOptions + { + Converters = + { + new DateTimeSerializer(), +#if USE_PORTABLE_DATE_ONLY + new DateOnlyConverter(), +#endif + new OneOfSerializer(), + new OptionalJsonConverterFactory(), + }, +#if DEBUG + WriteIndented = true, +#endif + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + NullableOptionalModifier, + JsonAccessAndIgnoreModifier, + HandleExtensionDataFields, + }, + }, + }; + ConfigureJsonSerializerOptions(options); + JsonSerializerOptions = options; + + var relaxedOptions = new JsonSerializerOptions(options) + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + JsonSerializerOptionsRelaxedEscaping = relaxedOptions; + } + + private static void NullableOptionalModifier(JsonTypeInfo typeInfo) + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var property in typeInfo.Properties) + { + var propertyInfo = property.AttributeProvider as global::System.Reflection.PropertyInfo; + + if (propertyInfo is null) + continue; + + // Check for ReadOnly JsonAccessAttribute - it overrides Optional/Nullable behavior + var jsonAccessAttribute = propertyInfo.GetCustomAttribute(); + if (jsonAccessAttribute?.AccessType == JsonAccessType.ReadOnly) + { + // ReadOnly means "never serialize", which completely overrides Optional/Nullable. + // Skip Optional/Nullable processing since JsonAccessAndIgnoreModifier + // will set ShouldSerialize = false anyway. + continue; + } + // Note: WriteOnly doesn't conflict with Optional/Nullable since it only + // affects deserialization (Set), not serialization (ShouldSerialize) + + var isOptionalType = + property.PropertyType.IsGenericType + && property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>); + + var hasOptionalAttribute = + propertyInfo.GetCustomAttribute() is not null; + var hasNullableAttribute = + propertyInfo.GetCustomAttribute() is not null; + + if (isOptionalType && hasOptionalAttribute) + { + var originalGetter = property.Get; + if (originalGetter is not null) + { + var capturedIsNullable = hasNullableAttribute; + + property.ShouldSerialize = (obj, value) => + { + var optionalValue = originalGetter(obj); + if (optionalValue is not IOptional optional) + return false; + + if (!optional.IsDefined) + return false; + + if (!capturedIsNullable) + { + var innerValue = optional.GetBoxedValue(); + if (innerValue is null) + return false; + } + + return true; + }; + } + } + else if (hasNullableAttribute) + { + // Force serialization of nullable properties even when null + property.ShouldSerialize = (obj, value) => true; + } + } + } + + private static void JsonAccessAndIgnoreModifier(JsonTypeInfo typeInfo) + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes(typeof(JsonAccessAttribute), true) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute is not null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + var jsonIgnoreAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes(typeof(JsonIgnoreAttribute), true) + .OfType() + .FirstOrDefault(); + + if (jsonIgnoreAttribute is not null) + { + propertyInfo.IsRequired = false; + } + } + } + + private static void HandleExtensionDataFields(JsonTypeInfo typeInfo) + { + if ( + typeInfo.Kind == JsonTypeInfoKind.Object + && typeInfo.Properties.All(prop => !prop.IsExtensionData) + ) + { + var extensionProp = typeInfo + .Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic) + .FirstOrDefault(prop => + prop.GetCustomAttribute() is not null + ); + + if (extensionProp is not null) + { + var jsonPropertyInfo = typeInfo.CreateJsonPropertyInfo( + extensionProp.FieldType, + extensionProp.Name + ); + jsonPropertyInfo.Get = extensionProp.GetValue; + jsonPropertyInfo.Set = extensionProp.SetValue; + jsonPropertyInfo.IsExtensionData = true; + typeInfo.Properties.Add(jsonPropertyInfo); + } + } + } + + static partial void ConfigureJsonSerializerOptions(JsonSerializerOptions defaultOptions); +} + +internal static class JsonUtils +{ + internal static string Serialize(T obj) => + JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptions); + + internal static string Serialize(object obj, global::System.Type type) => + JsonSerializer.Serialize(obj, type, JsonOptions.JsonSerializerOptions); + + internal static string SerializeRelaxedEscaping(T obj) => + JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptionsRelaxedEscaping); + + internal static string SerializeRelaxedEscaping(object obj, global::System.Type type) => + JsonSerializer.Serialize(obj, type, JsonOptions.JsonSerializerOptionsRelaxedEscaping); + + internal static JsonElement SerializeToElement(T obj) => + JsonSerializer.SerializeToElement(obj, JsonOptions.JsonSerializerOptions); + + internal static JsonElement SerializeToElement(object obj, global::System.Type type) => + JsonSerializer.SerializeToElement(obj, type, JsonOptions.JsonSerializerOptions); + + internal static JsonDocument SerializeToDocument(T obj) => + JsonSerializer.SerializeToDocument(obj, JsonOptions.JsonSerializerOptions); + + internal static JsonNode? SerializeToNode(T obj) => + JsonSerializer.SerializeToNode(obj, JsonOptions.JsonSerializerOptions); + + internal static byte[] SerializeToUtf8Bytes(T obj) => + JsonSerializer.SerializeToUtf8Bytes(obj, JsonOptions.JsonSerializerOptions); + + internal static string SerializeWithAdditionalProperties( + T obj, + object? additionalProperties = null + ) + { + if (additionalProperties is null) + { + return Serialize(obj); + } + var additionalPropertiesJsonNode = SerializeToNode(additionalProperties); + if (additionalPropertiesJsonNode is not JsonObject additionalPropertiesJsonObject) + { + throw new InvalidOperationException( + "The additional properties must serialize to a JSON object." + ); + } + var jsonNode = SerializeToNode(obj); + if (jsonNode is not JsonObject jsonObject) + { + throw new InvalidOperationException( + "The serialized object must be a JSON object to add properties." + ); + } + MergeJsonObjects(jsonObject, additionalPropertiesJsonObject); + return jsonObject.ToJsonString(JsonOptions.JsonSerializerOptions); + } + + private static void MergeJsonObjects(JsonObject baseObject, JsonObject overrideObject) + { + foreach (var property in overrideObject) + { + if (!baseObject.TryGetPropertyValue(property.Key, out JsonNode? existingValue)) + { + baseObject[property.Key] = property.Value is not null + ? JsonNode.Parse(property.Value.ToJsonString()) + : null; + continue; + } + if ( + existingValue is JsonObject nestedBaseObject + && property.Value is JsonObject nestedOverrideObject + ) + { + // If both values are objects, recursively merge them. + MergeJsonObjects(nestedBaseObject, nestedOverrideObject); + continue; + } + // Otherwise, the overrideObject takes precedence. + baseObject[property.Key] = property.Value is not null + ? JsonNode.Parse(property.Value.ToJsonString()) + : null; + } + } + + internal static T Deserialize(string json) => + JsonSerializer.Deserialize(json, JsonOptions.JsonSerializerOptions)!; +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonRequest.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonRequest.cs new file mode 100644 index 000000000000..ceb33aa74311 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/JsonRequest.cs @@ -0,0 +1,36 @@ +using global::System.Net.Http; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// The request object to be sent for JSON APIs. +/// +internal record JsonRequest : BaseRequest +{ + internal object? Body { get; init; } + + internal override HttpContent? CreateContent() + { + if (Body is null && Options?.AdditionalBodyProperties is null) + { + return null; + } + + var (encoding, charset, mediaType) = ParseContentTypeOrDefault( + ContentType, + Utf8NoBom, + "application/json" + ); + var content = new StringContent( + JsonUtils.SerializeWithAdditionalProperties(Body, Options?.AdditionalBodyProperties), + encoding, + mediaType + ); + if (string.IsNullOrEmpty(charset) && content.Headers.ContentType is not null) + { + content.Headers.ContentType.CharSet = ""; + } + + return content; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/MultipartFormRequest.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/MultipartFormRequest.cs new file mode 100644 index 000000000000..f98c0bd808c7 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/MultipartFormRequest.cs @@ -0,0 +1,294 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// The request object to be sent for multipart form data. +/// +internal record MultipartFormRequest : BaseRequest +{ + private readonly List> _partAdders = []; + + internal void AddJsonPart(string name, object? value) => AddJsonPart(name, value, null); + + internal void AddJsonPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var (encoding, charset, mediaType) = ParseContentTypeOrDefault( + contentType, + Utf8NoBom, + "application/json" + ); + var content = new StringContent(JsonUtils.Serialize(value), encoding, mediaType); + if (string.IsNullOrEmpty(charset) && content.Headers.ContentType is not null) + { + content.Headers.ContentType.CharSet = ""; + } + + form.Add(content, name); + }); + } + + internal void AddJsonParts(string name, IEnumerable? value) => + AddJsonParts(name, value, null); + + internal void AddJsonParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddJsonPart(name, item, contentType); + } + } + + internal void AddJsonParts(string name, IEnumerable? value) => + AddJsonParts(name, value, null); + + internal void AddJsonParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddJsonPart(name, item, contentType); + } + } + + internal void AddStringPart(string name, object? value) => AddStringPart(name, value, null); + + internal void AddStringPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + AddStringPart(name, ValueConvert.ToString(value), contentType); + } + + internal void AddStringPart(string name, string? value) => AddStringPart(name, value, null); + + internal void AddStringPart(string name, string? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var (encoding, charset, mediaType) = ParseContentTypeOrDefault( + contentType, + Utf8NoBom, + "text/plain" + ); + var content = new StringContent(value, encoding, mediaType); + if (string.IsNullOrEmpty(charset) && content.Headers.ContentType is not null) + { + content.Headers.ContentType.CharSet = ""; + } + + form.Add(content, name); + }); + } + + internal void AddStringParts(string name, IEnumerable? value) => + AddStringParts(name, value, null); + + internal void AddStringParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + AddStringPart(name, ValueConvert.ToString(value), contentType); + } + + internal void AddStringParts(string name, IEnumerable? value) => + AddStringParts(name, value, null); + + internal void AddStringParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddStringPart(name, item, contentType); + } + } + + internal void AddStreamPart(string name, Stream? stream, string? fileName) => + AddStreamPart(name, stream, fileName, null); + + internal void AddStreamPart(string name, Stream? stream, string? fileName, string? contentType) + { + if (stream is null) + { + return; + } + + _partAdders.Add(form => + { + var content = new StreamContent(stream) + { + Headers = + { + ContentType = MediaTypeHeaderValue.Parse( + contentType ?? "application/octet-stream" + ), + }, + }; + + if (fileName is not null) + { + form.Add(content, name, fileName); + } + else + { + form.Add(content, name); + } + }); + } + + internal void AddFileParameterPart(string name, Stream? stream) => + AddStreamPart(name, stream, null, null); + + internal void AddFileParameterPart(string name, FileParameter? file) => + AddFileParameterPart(name, file, null); + + internal void AddFileParameterPart( + string name, + FileParameter? file, + string? fallbackContentType + ) => + AddStreamPart(name, file?.Stream, file?.FileName, file?.ContentType ?? fallbackContentType); + + internal void AddFileParameterParts(string name, IEnumerable? files) => + AddFileParameterParts(name, files, null); + + internal void AddFileParameterParts( + string name, + IEnumerable? files, + string? fallbackContentType + ) + { + if (files is null) + { + return; + } + + foreach (var file in files) + { + AddFileParameterPart(name, file, fallbackContentType); + } + } + + internal void AddFormEncodedPart(string name, object? value) => + AddFormEncodedPart(name, value, null); + + internal void AddFormEncodedPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var content = FormUrlEncoder.EncodeAsForm(value); + if (!string.IsNullOrEmpty(contentType)) + { + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + + form.Add(content, name); + }); + } + + internal void AddFormEncodedParts(string name, IEnumerable? value) => + AddFormEncodedParts(name, value, null); + + internal void AddFormEncodedParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddFormEncodedPart(name, item, contentType); + } + } + + internal void AddExplodedFormEncodedPart(string name, object? value) => + AddExplodedFormEncodedPart(name, value, null); + + internal void AddExplodedFormEncodedPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var content = FormUrlEncoder.EncodeAsExplodedForm(value); + if (!string.IsNullOrEmpty(contentType)) + { + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + + form.Add(content, name); + }); + } + + internal void AddExplodedFormEncodedParts(string name, IEnumerable? value) => + AddExplodedFormEncodedParts(name, value, null); + + internal void AddExplodedFormEncodedParts( + string name, + IEnumerable? value, + string? contentType + ) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddExplodedFormEncodedPart(name, item, contentType); + } + } + + internal override HttpContent CreateContent() + { + var form = new MultipartFormDataContent(); + foreach (var adder in _partAdders) + { + adder(form); + } + + return form; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/NullableAttribute.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/NullableAttribute.cs new file mode 100644 index 000000000000..59ff381d876b --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/NullableAttribute.cs @@ -0,0 +1,18 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Marks a property as nullable in the OpenAPI specification. +/// When applied to Optional properties, this indicates that null values should be +/// written to JSON when the optional is defined with null. +/// +/// +/// For regular (required) properties: +/// - Without [Nullable]: null values are invalid (omit from JSON at runtime) +/// - With [Nullable]: null values are written to JSON +/// +/// For Optional properties (also marked with [Optional]): +/// - Without [Nullable]: Optional.Of(null) → omit from JSON (runtime edge case) +/// - With [Nullable]: Optional.Of(null) → write null to JSON +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] +public class NullableAttribute : global::System.Attribute { } diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/OneOfSerializer.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/OneOfSerializer.cs new file mode 100644 index 000000000000..3fc176f05ec9 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/OneOfSerializer.cs @@ -0,0 +1,190 @@ +using global::System.Reflection; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using OneOf; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal class OneOfSerializer : JsonConverter +{ + public override IOneOf? Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType is JsonTokenType.Null) + return default; + + var json = JsonElement.ParseValue(ref reader); + + IOneOf? firstMatch = null; + IOneOf? bestMatch = null; + + foreach (var (type, cast) in GetOneOfTypes(typeToConvert)) + { + try + { + var result = JsonSerializer.Deserialize(json, type, options); + var oneOf = (IOneOf)cast.Invoke(null, [result])!; + firstMatch ??= oneOf; + + if (!ContainsJsonElement(result)) + { + bestMatch = oneOf; + break; + } + } + catch (JsonException) { } + } + + return bestMatch + ?? firstMatch + ?? throw new JsonException( + $"Cannot deserialize into one of the supported types for {typeToConvert}" + ); + } + + /// + /// Checks if the deserialized object is or contains a raw JsonElement value, + /// indicating the deserializer used a catch-all strategy rather than + /// strongly-typed deserialization. + /// + private static bool ContainsJsonElement(object? result) + { + if (result == null || result is JsonElement) + return true; + + foreach ( + var prop in result.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) + ) + { + if (prop.GetCustomAttribute() != null) + continue; + if (prop.GetCustomAttribute() != null) + continue; + + try + { + if (prop.GetValue(result) is JsonElement) + return true; + } + catch + { + // Ignore inaccessible properties + } + } + + return false; + } + + public override void Write(Utf8JsonWriter writer, IOneOf value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + + public override IOneOf ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = reader.GetString(); + if (stringValue == null) + throw new JsonException("Cannot deserialize null property name into OneOf type"); + + // Try to deserialize the string value into one of the supported types + foreach (var (type, cast) in GetOneOfTypes(typeToConvert)) + { + try + { + // For primitive types, try direct conversion + if (type == typeof(string)) + { + return (IOneOf)cast.Invoke(null, [stringValue])!; + } + + // For other types, try to deserialize from JSON string + var result = JsonSerializer.Deserialize($"\"{stringValue}\"", type, options); + if (result != null) + { + return (IOneOf)cast.Invoke(null, [result])!; + } + } + catch { } + } + + // If no type-specific deserialization worked, default to string if available + var stringType = GetOneOfTypes(typeToConvert).FirstOrDefault(t => t.type == typeof(string)); + if (stringType != default) + { + return (IOneOf)stringType.cast.Invoke(null, [stringValue])!; + } + + throw new JsonException( + $"Cannot deserialize dictionary key '{stringValue}' into one of the supported types for {typeToConvert}" + ); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + IOneOf value, + JsonSerializerOptions options + ) + { + // Serialize the underlying value to a string suitable for use as a dictionary key + var stringValue = value.Value?.ToString() ?? "null"; + writer.WritePropertyName(stringValue); + } + + private static (global::System.Type type, MethodInfo cast)[] GetOneOfTypes( + global::System.Type typeToConvert + ) + { + var type = typeToConvert; + if (Nullable.GetUnderlyingType(type) is { } underlyingType) + { + type = underlyingType; + } + + var casts = type.GetRuntimeMethods() + .Where(m => m.IsSpecialName && m.Name == "op_Implicit") + .ToArray(); + while (type is not null) + { + if ( + type.IsGenericType + && (type.Name.StartsWith("OneOf`") || type.Name.StartsWith("OneOfBase`")) + ) + { + var genericArguments = type.GetGenericArguments(); + if (genericArguments.Length == 1) + { + return [(genericArguments[0], casts[0])]; + } + + // if object type is present, make sure it is last + var indexOfObjectType = Array.IndexOf(genericArguments, typeof(object)); + if (indexOfObjectType != -1 && genericArguments.Length - 1 != indexOfObjectType) + { + genericArguments = genericArguments + .OrderBy(t => t == typeof(object) ? 1 : 0) + .ToArray(); + } + + return genericArguments + .Select(t => (t, casts.First(c => c.GetParameters()[0].ParameterType == t))) + .ToArray(); + } + + type = type.BaseType; + } + + throw new InvalidOperationException($"{type} isn't OneOf or OneOfBase"); + } + + public override bool CanConvert(global::System.Type typeToConvert) + { + return typeof(IOneOf).IsAssignableFrom(typeToConvert); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Optional.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Optional.cs new file mode 100644 index 000000000000..5e787718c8ae --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Optional.cs @@ -0,0 +1,474 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Non-generic interface for Optional types to enable reflection-free checks. +/// +public interface IOptional +{ + /// + /// Returns true if the value is defined (set), even if the value is null. + /// + bool IsDefined { get; } + + /// + /// Gets the boxed value. Returns null if undefined or if the value is null. + /// + object? GetBoxedValue(); +} + +/// +/// Represents a field that can be "not set" (undefined) vs "explicitly set" (defined). +/// Use this for HTTP PATCH requests where you need to distinguish between: +/// +/// Undefined: Don't send this field (leave it unchanged on the server) +/// Defined with null: Send null (clear the field on the server) +/// Defined with value: Send the value (update the field on the server) +/// +/// +/// The type of the value. Use nullable types (T?) for fields that can be null. +/// +/// For nullable string fields, use Optional<string?>: +/// +/// public class UpdateUserRequest +/// { +/// public Optional<string?> Name { get; set; } = Optional<string?>.Undefined; +/// } +/// +/// var request = new UpdateUserRequest +/// { +/// Name = "John" // Will send: { "name": "John" } +/// }; +/// +/// var request2 = new UpdateUserRequest +/// { +/// Name = Optional<string?>.Of(null) // Will send: { "name": null } +/// }; +/// +/// var request3 = new UpdateUserRequest(); // Will send: {} (name not included) +/// +/// +public readonly struct Optional : IOptional, IEquatable> +{ + private readonly T _value; + private readonly bool _isDefined; + + private Optional(T value, bool isDefined) + { + _value = value; + _isDefined = isDefined; + } + + /// + /// Creates an undefined value - the field will not be included in the HTTP request. + /// Use this as the default value for optional fields. + /// + /// + /// + /// public Optional<string?> Email { get; set; } = Optional<string?>.Undefined; + /// + /// + public static Optional Undefined => new(default!, false); + + /// + /// Creates a defined value - the field will be included in the HTTP request. + /// The value can be null if T is a nullable type. + /// + /// The value to set. Can be null if T is nullable (e.g., string?, int?). + /// + /// + /// // Set to a value + /// request.Name = Optional<string?>.Of("John"); + /// + /// // Set to null (clears the field) + /// request.Email = Optional<string?>.Of(null); + /// + /// // Or use implicit conversion + /// request.Name = "John"; // Same as Of("John") + /// request.Email = null; // Same as Of(null) + /// + /// + public static Optional Of(T value) => new(value, true); + + /// + /// Returns true if the field is defined (set), even if the value is null. + /// Use this to determine if the field should be included in the HTTP request. + /// + /// + /// + /// if (request.Name.IsDefined) + /// { + /// requestBody["name"] = request.Name.Value; // Include in request (can be null) + /// } + /// + /// + public bool IsDefined => _isDefined; + + /// + /// Returns true if the field is undefined (not set). + /// Use this to check if the field should be excluded from the HTTP request. + /// + /// + /// + /// if (request.Email.IsUndefined) + /// { + /// // Don't include email in the request - leave it unchanged + /// } + /// + /// + public bool IsUndefined => !_isDefined; + + /// + /// Gets the value. The value may be null if T is a nullable type. + /// + /// Thrown if the value is undefined. + /// + /// Always check before accessing Value, or use instead. + /// + /// + /// + /// if (request.Name.IsDefined) + /// { + /// string? name = request.Name.Value; // Safe - can be null if Optional<string?> + /// } + /// + /// // Or check for null explicitly + /// if (request.Email.IsDefined && request.Email.Value is null) + /// { + /// // Email is explicitly set to null (clear it) + /// } + /// + /// + public T Value + { + get + { + if (!_isDefined) + throw new InvalidOperationException("Optional value is undefined"); + return _value; + } + } + + /// + /// Gets the value if defined, otherwise returns the specified default value. + /// Note: If the value is defined as null, this returns null (not the default). + /// + /// The value to return if undefined. + /// The actual value if defined (can be null), otherwise the default value. + /// + /// + /// string name = request.Name.GetValueOrDefault("Anonymous"); + /// // If Name is undefined: returns "Anonymous" + /// // If Name is Of(null): returns null + /// // If Name is Of("John"): returns "John" + /// + /// + public T GetValueOrDefault(T defaultValue = default!) + { + return _isDefined ? _value : defaultValue; + } + + /// + /// Tries to get the value. Returns true if the value is defined (even if null). + /// + /// + /// When this method returns, contains the value if defined, or default(T) if undefined. + /// The value may be null if T is nullable. + /// + /// True if the value is defined; otherwise, false. + /// + /// + /// if (request.Email.TryGetValue(out var email)) + /// { + /// requestBody["email"] = email; // email can be null + /// } + /// else + /// { + /// // Email is undefined - don't include in request + /// } + /// + /// + public bool TryGetValue(out T value) + { + if (_isDefined) + { + value = _value; + return true; + } + value = default!; + return false; + } + + /// + /// Implicitly converts a value to Optional<T>.Of(value). + /// This allows natural assignment: request.Name = "John" instead of request.Name = Optional<string?>.Of("John"). + /// + /// The value to convert (can be null if T is nullable). + public static implicit operator Optional(T value) => Of(value); + + /// + /// Returns a string representation of this Optional value. + /// + /// "Undefined" if not set, or "Defined(value)" if set. + public override string ToString() => _isDefined ? $"Defined({_value})" : "Undefined"; + + /// + /// Gets the boxed value. Returns null if undefined or if the value is null. + /// + public object? GetBoxedValue() + { + if (!_isDefined) + return null; + return _value; + } + + /// + public bool Equals(Optional other) => + _isDefined == other._isDefined && EqualityComparer.Default.Equals(_value, other._value); + + /// + public override bool Equals(object? obj) => obj is Optional other && Equals(other); + + /// + public override int GetHashCode() + { + if (!_isDefined) + return 0; + unchecked + { + int hash = 17; + hash = hash * 31 + 1; // _isDefined = true + hash = hash * 31 + (_value is null ? 0 : _value.GetHashCode()); + return hash; + } + } + + /// + /// Determines whether two Optional values are equal. + /// + /// The first Optional to compare. + /// The second Optional to compare. + /// True if the Optional values are equal; otherwise, false. + public static bool operator ==(Optional left, Optional right) => left.Equals(right); + + /// + /// Determines whether two Optional values are not equal. + /// + /// The first Optional to compare. + /// The second Optional to compare. + /// True if the Optional values are not equal; otherwise, false. + public static bool operator !=(Optional left, Optional right) => !left.Equals(right); +} + +/// +/// Extension methods for Optional to simplify common operations. +/// +public static class OptionalExtensions +{ + /// + /// Adds the value to a dictionary if the optional is defined (even if the value is null). + /// This is useful for building JSON request payloads where null values should be included. + /// + /// The type of the optional value. + /// The optional value to add. + /// The dictionary to add to. + /// The key to use in the dictionary. + /// + /// + /// var dict = new Dictionary<string, object?>(); + /// request.Name.AddTo(dict, "name"); // Adds only if Name.IsDefined + /// request.Email.AddTo(dict, "email"); // Adds only if Email.IsDefined + /// + /// + public static void AddTo( + this Optional optional, + Dictionary dictionary, + string key + ) + { + if (optional.IsDefined) + { + dictionary[key] = optional.Value; + } + } + + /// + /// Executes an action if the optional is defined. + /// + /// The type of the optional value. + /// The optional value. + /// The action to execute with the value. + /// + /// + /// request.Name.IfDefined(name => Console.WriteLine($"Name: {name}")); + /// + /// + public static void IfDefined(this Optional optional, Action action) + { + if (optional.IsDefined) + { + action(optional.Value); + } + } + + /// + /// Maps the value to a new type if the optional is defined, otherwise returns undefined. + /// + /// The type of the original value. + /// The type to map to. + /// The optional value to map. + /// The mapping function. + /// An optional containing the mapped value if defined, otherwise undefined. + /// + /// + /// Optional<string?> name = Optional<string?>.Of("John"); + /// Optional<int> length = name.Map(n => n?.Length ?? 0); // Optional.Of(4) + /// + /// + public static Optional Map( + this Optional optional, + Func mapper + ) + { + return optional.IsDefined + ? Optional.Of(mapper(optional.Value)) + : Optional.Undefined; + } + + /// + /// Adds a nullable value to a dictionary only if it is not null. + /// This is useful for regular nullable properties where null means "omit from request". + /// + /// The type of the value (must be a reference type or Nullable). + /// The nullable value to add. + /// The dictionary to add to. + /// The key to use in the dictionary. + /// + /// + /// var dict = new Dictionary<string, object?>(); + /// request.Description.AddIfNotNull(dict, "description"); // Only adds if not null + /// request.Score.AddIfNotNull(dict, "score"); // Only adds if not null + /// + /// + public static void AddIfNotNull( + this T? value, + Dictionary dictionary, + string key + ) + where T : class + { + if (value is not null) + { + dictionary[key] = value; + } + } + + /// + /// Adds a nullable value type to a dictionary only if it has a value. + /// This is useful for regular nullable properties where null means "omit from request". + /// + /// The underlying value type. + /// The nullable value to add. + /// The dictionary to add to. + /// The key to use in the dictionary. + /// + /// + /// var dict = new Dictionary<string, object?>(); + /// request.Age.AddIfNotNull(dict, "age"); // Only adds if HasValue + /// request.Score.AddIfNotNull(dict, "score"); // Only adds if HasValue + /// + /// + public static void AddIfNotNull( + this T? value, + Dictionary dictionary, + string key + ) + where T : struct + { + if (value.HasValue) + { + dictionary[key] = value.Value; + } + } +} + +/// +/// JSON converter factory for Optional that handles undefined vs null correctly. +/// Uses a TypeInfoResolver to conditionally include/exclude properties based on Optional.IsDefined. +/// +public class OptionalJsonConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(global::System.Type typeToConvert) + { + if (!typeToConvert.IsGenericType) + return false; + + return typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>); + } + + public override JsonConverter? CreateConverter( + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var valueType = typeToConvert.GetGenericArguments()[0]; + var converterType = typeof(OptionalJsonConverter<>).MakeGenericType(valueType); + return (JsonConverter?)global::System.Activator.CreateInstance(converterType); + } +} + +/// +/// JSON converter for Optional that unwraps the value during serialization. +/// The actual property skipping is handled by the OptionalTypeInfoResolver. +/// +public class OptionalJsonConverter : JsonConverter> +{ + public override Optional Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType == JsonTokenType.Null) + { + return Optional.Of(default!); + } + + var value = JsonSerializer.Deserialize(ref reader, options); + return Optional.Of(value!); + } + + public override void Write( + Utf8JsonWriter writer, + Optional value, + JsonSerializerOptions options + ) + { + // This will be called by the serializer + // We need to unwrap and serialize the inner value + // The TypeInfoResolver will handle skipping undefined values + + if (value.IsUndefined) + { + // This shouldn't be called for undefined values due to ShouldSerialize + // But if it is, write null and let the resolver filter it + writer.WriteNullValue(); + return; + } + + // Get the inner value + var innerValue = value.Value; + + // Write null directly if the value is null (don't use JsonSerializer.Serialize for null) + if (innerValue is null) + { + writer.WriteNullValue(); + return; + } + + // Serialize the unwrapped value + JsonSerializer.Serialize(writer, innerValue, options); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/OptionalAttribute.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/OptionalAttribute.cs new file mode 100644 index 000000000000..62992a73fd7f --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/OptionalAttribute.cs @@ -0,0 +1,17 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Marks a property as optional in the OpenAPI specification. +/// Optional properties use the Optional type and can be undefined (not present in JSON). +/// +/// +/// Properties marked with [Optional] should use the Optional type: +/// - Undefined: Optional.Undefined → omitted from JSON +/// - Defined: Optional.Of(value) → written to JSON +/// +/// Combine with [Nullable] to allow null values: +/// - [Optional, Nullable] Optional → can be undefined, null, or a value +/// - [Optional] Optional → can be undefined or a value (null is invalid) +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] +public class OptionalAttribute : global::System.Attribute { } diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/AdditionalProperties.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/AdditionalProperties.cs new file mode 100644 index 000000000000..7d6437359631 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/AdditionalProperties.cs @@ -0,0 +1,353 @@ +using global::System.Collections; +using global::System.Collections.ObjectModel; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv; + +public record ReadOnlyAdditionalProperties : ReadOnlyAdditionalProperties +{ + internal ReadOnlyAdditionalProperties() { } + + internal ReadOnlyAdditionalProperties(IDictionary properties) + : base(properties) { } +} + +public record ReadOnlyAdditionalProperties : IReadOnlyDictionary +{ + private readonly Dictionary _extensionData = new(); + private readonly Dictionary _convertedCache = new(); + + internal ReadOnlyAdditionalProperties() + { + _extensionData = new Dictionary(); + _convertedCache = new Dictionary(); + } + + internal ReadOnlyAdditionalProperties(IDictionary properties) + { + _extensionData = new Dictionary(properties.Count); + _convertedCache = new Dictionary(properties.Count); + foreach (var kvp in properties) + { + if (kvp.Value is JsonElement element) + { + _extensionData.Add(kvp.Key, element); + } + else + { + _extensionData[kvp.Key] = JsonUtils.SerializeToElement(kvp.Value); + } + + _convertedCache[kvp.Key] = kvp.Value; + } + } + + private static T ConvertToT(JsonElement value) + { + if (typeof(T) == typeof(JsonElement)) + { + return (T)(object)value; + } + + return value.Deserialize(JsonOptions.JsonSerializerOptions)!; + } + + internal void CopyFromExtensionData(IDictionary extensionData) + { + _extensionData.Clear(); + _convertedCache.Clear(); + foreach (var kvp in extensionData) + { + _extensionData[kvp.Key] = kvp.Value; + if (kvp.Value is T value) + { + _convertedCache[kvp.Key] = value; + } + } + } + + private T GetCached(string key) + { + if (_convertedCache.TryGetValue(key, out var cached)) + { + return cached; + } + + var value = ConvertToT(_extensionData[key]); + _convertedCache[key] = value; + return value; + } + + public IEnumerator> GetEnumerator() + { + return _extensionData + .Select(kvp => new KeyValuePair(kvp.Key, GetCached(kvp.Key))) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _extensionData.Count; + + public bool ContainsKey(string key) => _extensionData.ContainsKey(key); + + public bool TryGetValue(string key, out T value) + { + if (_convertedCache.TryGetValue(key, out value!)) + { + return true; + } + + if (_extensionData.TryGetValue(key, out var element)) + { + value = ConvertToT(element); + _convertedCache[key] = value; + return true; + } + + return false; + } + + public T this[string key] => GetCached(key); + + public IEnumerable Keys => _extensionData.Keys; + + public IEnumerable Values => Keys.Select(GetCached); +} + +public record AdditionalProperties : AdditionalProperties +{ + public AdditionalProperties() { } + + public AdditionalProperties(IDictionary properties) + : base(properties) { } +} + +public record AdditionalProperties : IDictionary +{ + private readonly Dictionary _extensionData; + private readonly Dictionary _convertedCache; + + public AdditionalProperties() + { + _extensionData = new Dictionary(); + _convertedCache = new Dictionary(); + } + + public AdditionalProperties(IDictionary properties) + { + _extensionData = new Dictionary(properties.Count); + _convertedCache = new Dictionary(properties.Count); + foreach (var kvp in properties) + { + _extensionData[kvp.Key] = kvp.Value; + _convertedCache[kvp.Key] = kvp.Value; + } + } + + private static T ConvertToT(object? extensionDataValue) + { + return extensionDataValue switch + { + T value => value, + JsonElement jsonElement => jsonElement.Deserialize( + JsonOptions.JsonSerializerOptions + )!, + JsonNode jsonNode => jsonNode.Deserialize(JsonOptions.JsonSerializerOptions)!, + _ => JsonUtils + .SerializeToElement(extensionDataValue) + .Deserialize(JsonOptions.JsonSerializerOptions)!, + }; + } + + internal void CopyFromExtensionData(IDictionary extensionData) + { + _extensionData.Clear(); + _convertedCache.Clear(); + foreach (var kvp in extensionData) + { + _extensionData[kvp.Key] = kvp.Value; + if (kvp.Value is T value) + { + _convertedCache[kvp.Key] = value; + } + } + } + + internal void CopyToExtensionData(IDictionary extensionData) + { + extensionData.Clear(); + foreach (var kvp in _extensionData) + { + extensionData[kvp.Key] = kvp.Value; + } + } + + public JsonObject ToJsonObject() => + ( + JsonUtils.SerializeToNode(_extensionData) + ?? throw new InvalidOperationException( + "Failed to serialize AdditionalProperties to JSON Node." + ) + ).AsObject(); + + public JsonNode ToJsonNode() => + JsonUtils.SerializeToNode(_extensionData) + ?? throw new InvalidOperationException( + "Failed to serialize AdditionalProperties to JSON Node." + ); + + public JsonElement ToJsonElement() => JsonUtils.SerializeToElement(_extensionData); + + public JsonDocument ToJsonDocument() => JsonUtils.SerializeToDocument(_extensionData); + + public IReadOnlyDictionary ToJsonElementDictionary() + { + return new ReadOnlyDictionary( + _extensionData.ToDictionary( + kvp => kvp.Key, + kvp => + { + if (kvp.Value is JsonElement jsonElement) + { + return jsonElement; + } + + return JsonUtils.SerializeToElement(kvp.Value); + } + ) + ); + } + + public ICollection Keys => _extensionData.Keys; + + public ICollection Values + { + get + { + var values = new T[_extensionData.Count]; + var i = 0; + foreach (var key in Keys) + { + values[i++] = GetCached(key); + } + + return values; + } + } + + private T GetCached(string key) + { + if (_convertedCache.TryGetValue(key, out var value)) + { + return value; + } + + value = ConvertToT(_extensionData[key]); + _convertedCache.Add(key, value); + return value; + } + + private void SetCached(string key, T value) + { + _extensionData[key] = value; + _convertedCache[key] = value; + } + + private void AddCached(string key, T value) + { + _extensionData.Add(key, value); + _convertedCache.Add(key, value); + } + + private bool RemoveCached(string key) + { + var isRemoved = _extensionData.Remove(key); + _convertedCache.Remove(key); + return isRemoved; + } + + public int Count => _extensionData.Count; + public bool IsReadOnly => false; + + public T this[string key] + { + get => GetCached(key); + set => SetCached(key, value); + } + + public void Add(string key, T value) => AddCached(key, value); + + public void Add(KeyValuePair item) => AddCached(item.Key, item.Value); + + public bool Remove(string key) => RemoveCached(key); + + public bool Remove(KeyValuePair item) => RemoveCached(item.Key); + + public bool ContainsKey(string key) => _extensionData.ContainsKey(key); + + public bool Contains(KeyValuePair item) + { + return _extensionData.ContainsKey(item.Key) + && EqualityComparer.Default.Equals(GetCached(item.Key), item.Value); + } + + public bool TryGetValue(string key, out T value) + { + if (_convertedCache.TryGetValue(key, out value!)) + { + return true; + } + + if (_extensionData.TryGetValue(key, out var extensionDataValue)) + { + value = ConvertToT(extensionDataValue); + _convertedCache[key] = value; + return true; + } + + return false; + } + + public void Clear() + { + _extensionData.Clear(); + _convertedCache.Clear(); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (array is null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + if (array.Length - arrayIndex < _extensionData.Count) + { + throw new ArgumentException( + "The array does not have enough space to copy the elements." + ); + } + + foreach (var kvp in _extensionData) + { + array[arrayIndex++] = new KeyValuePair(kvp.Key, GetCached(kvp.Key)); + } + } + + public IEnumerator> GetEnumerator() + { + return _extensionData + .Select(kvp => new KeyValuePair(kvp.Key, GetCached(kvp.Key))) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/ClientOptions.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/ClientOptions.cs new file mode 100644 index 000000000000..93670833fa80 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/ClientOptions.cs @@ -0,0 +1,92 @@ +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv; + +[Serializable] +public partial class ClientOptions +{ + /// + /// The http headers sent with the request. + /// + internal Headers Headers { get; init; } = new(); + + /// + /// The Base URL for the API. + /// + public string BaseUrl { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = ""; + + /// + /// The http client used to make requests. + /// + public HttpClient HttpClient { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = DefaultHttpClientFactory.Create(); + + /// + /// Additional headers to be sent with HTTP requests. + /// Headers with matching keys will be overwritten by headers set on the request. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = []; + + /// + /// The max number of retries to attempt. + /// + public int MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = 2; + + /// + /// The timeout for the request. + /// + public TimeSpan Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = TimeSpan.FromMilliseconds(30000); + + public string? Version { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = null; + + /// + /// Clones this and returns a new instance + /// + internal ClientOptions Clone() + { + return new ClientOptions + { + BaseUrl = BaseUrl, + HttpClient = HttpClient, + MaxRetries = MaxRetries, + Timeout = Timeout, + Headers = new Headers(new Dictionary(Headers)), + AdditionalHeaders = AdditionalHeaders, + }; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/FileParameter.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/FileParameter.cs new file mode 100644 index 000000000000..7fad4bb0fddc --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/FileParameter.cs @@ -0,0 +1,63 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv; + +/// +/// File parameter for uploading files. +/// +public record FileParameter : IDisposable +#if NET6_0_OR_GREATER + , IAsyncDisposable +#endif +{ + private bool _disposed; + + /// + /// The name of the file to be uploaded. + /// + public string? FileName { get; set; } + + /// + /// The content type of the file to be uploaded. + /// + public string? ContentType { get; set; } + + /// + /// The content of the file to be uploaded. + /// + public required Stream Stream { get; set; } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + if (disposing) + { + Stream.Dispose(); + } + + _disposed = true; + } + +#if NET6_0_OR_GREATER + /// + public async ValueTask DisposeAsync() + { + if (!_disposed) + { + await Stream.DisposeAsync().ConfigureAwait(false); + _disposed = true; + } + + GC.SuppressFinalize(this); + } +#endif + + public static implicit operator FileParameter(Stream stream) => new() { Stream = stream }; +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/RawResponse.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/RawResponse.cs new file mode 100644 index 000000000000..4c306e2202c2 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/RawResponse.cs @@ -0,0 +1,24 @@ +using global::System.Net; + +namespace SeedCsharpGlobalHeaderLiteralEnv; + +/// +/// Contains HTTP response metadata including status code, URL, and headers. +/// +public record RawResponse +{ + /// + /// The HTTP status code of the response. + /// + public required HttpStatusCode StatusCode { get; init; } + + /// + /// The request URL that generated this response. + /// + public required Uri Url { get; init; } + + /// + /// The HTTP response headers. + /// + public required Core.ResponseHeaders Headers { get; init; } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/RequestOptions.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/RequestOptions.cs new file mode 100644 index 000000000000..cb7a28753622 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/RequestOptions.cs @@ -0,0 +1,94 @@ +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv; + +[Serializable] +public partial class RequestOptions : IRequestOptions +{ + /// + /// The Base URL for the API. + /// + public string? BaseUrl { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The http client used to make requests. + /// + public HttpClient? HttpClient { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with the request. + /// Headers previously set with matching keys will be overwritten. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = []; + + /// + /// The max number of retries to attempt. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional query parameters sent with the request. + /// + public IEnumerable> AdditionalQueryParameters { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = Enumerable.Empty>(); + + /// + /// Additional body properties sent with the request. + /// This is only applied to JSON requests. + /// + public object? AdditionalBodyProperties { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + public string? Version { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/SeedCsharpGlobalHeaderLiteralEnvApiException.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/SeedCsharpGlobalHeaderLiteralEnvApiException.cs new file mode 100644 index 000000000000..81c4ebcb7410 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/SeedCsharpGlobalHeaderLiteralEnvApiException.cs @@ -0,0 +1,28 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +public class SeedCsharpGlobalHeaderLiteralEnvApiException( + string message, + int statusCode, + object body, + Exception? innerException = null, + SeedCsharpGlobalHeaderLiteralEnv.RawResponse? rawResponse = null +) : SeedCsharpGlobalHeaderLiteralEnvException(message, innerException) +{ + /// + /// The error code of the response that triggered the exception. + /// + public int StatusCode => statusCode; + + /// + /// The body of the response that triggered the exception. + /// + public object Body => body; + + /// + /// The raw HTTP response (status code, URL, headers) that triggered the exception, if available. + /// + public SeedCsharpGlobalHeaderLiteralEnv.RawResponse? RawResponse => rawResponse; +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/SeedCsharpGlobalHeaderLiteralEnvException.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/SeedCsharpGlobalHeaderLiteralEnvException.cs new file mode 100644 index 000000000000..0b7b9ce6ac17 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/SeedCsharpGlobalHeaderLiteralEnvException.cs @@ -0,0 +1,9 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv; + +/// +/// Base exception class for all exceptions thrown by the SDK. +/// +public class SeedCsharpGlobalHeaderLiteralEnvException( + string message, + Exception? innerException = null +) : Exception(message, innerException); diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/Version.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/Version.cs new file mode 100644 index 000000000000..a35ccb5ee1a1 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/Version.cs @@ -0,0 +1,7 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv; + +[Serializable] +internal class Version +{ + public const string Current = "0.0.1"; +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponse.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponse.cs new file mode 100644 index 000000000000..f52715f5f842 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponse.cs @@ -0,0 +1,18 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv; + +/// +/// Wraps a parsed response value with its raw HTTP response metadata. +/// +/// The type of the parsed response data. +public readonly struct WithRawResponse +{ + /// + /// The parsed response data. + /// + public required T Data { get; init; } + + /// + /// The raw HTTP response metadata. + /// + public required RawResponse RawResponse { get; init; } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponseStream.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponseStream.cs new file mode 100644 index 000000000000..67925170b300 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponseStream.cs @@ -0,0 +1,66 @@ +using global::System.Collections.Generic; +using global::System.Runtime.CompilerServices; +using global::System.Threading; +using global::System.Threading.Tasks; + +namespace SeedCsharpGlobalHeaderLiteralEnv; + +/// +/// A streaming wrapper that provides dual-mode access to a streaming endpoint: +/// - Direct await foreach iterates the parsed stream values (zero-allocation path for common case) +/// - .WithRawResponse() yields the underlying exposing both the stream and raw response metadata +/// +/// The element type of the parsed stream. +public readonly struct WithRawResponseStream : IAsyncEnumerable +{ + private readonly Task>> _task; + private readonly CancellationToken _originalCancellationToken; + + /// + /// Creates a new WithRawResponseStream wrapping the given task that opens the underlying HTTP response. + /// + /// The task opening the HTTP response and producing the parsed stream. + /// + /// The cancellation token supplied at the SDK call site. Linked with any token supplied via + /// .WithCancellation(...) on the enumerator so both cancel the inner reads. + /// + public WithRawResponseStream( + Task>> task, + CancellationToken cancellationToken = default + ) + { + _task = task; + _originalCancellationToken = cancellationToken; + } + + /// + /// Returns the underlying task that yields both the stream and raw response metadata once headers are received. + /// + public Task>> WithRawResponse() => _task; + + /// + /// Returns an enumerator that iterates the parsed stream values. Awaits the underlying HTTP response, then yields each parsed element from the body stream. + /// + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => + EnumerateAsync(_task, _originalCancellationToken, cancellationToken) + .GetAsyncEnumerator(cancellationToken); + + private static async IAsyncEnumerable EnumerateAsync( + Task>> task, + CancellationToken originalCancellationToken, + [EnumeratorCancellation] CancellationToken cancellationToken + ) + { + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( + originalCancellationToken, + cancellationToken + ); + var wrapped = await task.ConfigureAwait(false); + await foreach ( + var item in wrapped.Data.WithCancellation(linkedCts.Token).ConfigureAwait(false) + ) + { + yield return item; + } + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponseTask.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponseTask.cs new file mode 100644 index 000000000000..07b6e23c042e --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/Public/WithRawResponseTask.cs @@ -0,0 +1,187 @@ +using global::System.Runtime.CompilerServices; + +namespace SeedCsharpGlobalHeaderLiteralEnv; + +/// +/// A task-like type that wraps Task<WithRawResponse<T>> and provides dual-mode awaiting: +/// - Direct await yields just T (zero-allocation path for common case) +/// - .WithRawResponse() yields WithRawResponse<T> (when raw response metadata is needed) +/// +/// The type of the parsed response data. +public readonly struct WithRawResponseTask +{ + private readonly global::System.Threading.Tasks.Task> _task; + + /// + /// Creates a new WithRawResponseTask wrapping the given task. + /// + public WithRawResponseTask(global::System.Threading.Tasks.Task> task) + { + _task = task; + } + + /// + /// Returns the underlying task that yields both the data and raw response metadata. + /// + public global::System.Threading.Tasks.Task> WithRawResponse() => _task; + + /// + /// Gets the custom awaiter that unwraps to just T when awaited. + /// + public Awaiter GetAwaiter() => new(_task.GetAwaiter()); + + /// + /// Configures the awaiter to continue on the captured context or not. + /// + public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) => + new(_task.ConfigureAwait(continueOnCapturedContext)); + + /// + /// Implicitly converts WithRawResponseTask<T> to global::System.Threading.Tasks.Task<T> for backward compatibility. + /// The resulting task will yield just the data when awaited. + /// + public static implicit operator global::System.Threading.Tasks.Task( + WithRawResponseTask task + ) + { + return task._task.ContinueWith( + t => t.Result.Data, + TaskContinuationOptions.ExecuteSynchronously + ); + } + + /// + /// Custom awaiter that unwraps WithRawResponse<T> to just T. + /// + public readonly struct Awaiter : ICriticalNotifyCompletion + { + private readonly TaskAwaiter> _awaiter; + + internal Awaiter(TaskAwaiter> awaiter) + { + _awaiter = awaiter; + } + + /// + /// Gets whether the underlying task has completed. + /// + public bool IsCompleted => _awaiter.IsCompleted; + + /// + /// Gets the result, unwrapping to just the data. + /// + public T GetResult() => _awaiter.GetResult().Data; + + /// + /// Schedules the continuation action. + /// + public void OnCompleted(global::System.Action continuation) => + _awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action without capturing the execution context. + /// + public void UnsafeOnCompleted(global::System.Action continuation) => + _awaiter.UnsafeOnCompleted(continuation); + } + + /// + /// Awaitable type returned by ConfigureAwait that unwraps to just T. + /// + public readonly struct ConfiguredTaskAwaitable + { + private readonly ConfiguredTaskAwaitable> _configuredTask; + + internal ConfiguredTaskAwaitable(ConfiguredTaskAwaitable> configuredTask) + { + _configuredTask = configuredTask; + } + + /// + /// Gets the configured awaiter that unwraps to just T. + /// + public ConfiguredAwaiter GetAwaiter() => new(_configuredTask.GetAwaiter()); + + /// + /// Custom configured awaiter that unwraps WithRawResponse<T> to just T. + /// + public readonly struct ConfiguredAwaiter : ICriticalNotifyCompletion + { + private readonly ConfiguredTaskAwaitable< + WithRawResponse + >.ConfiguredTaskAwaiter _awaiter; + + internal ConfiguredAwaiter( + ConfiguredTaskAwaitable>.ConfiguredTaskAwaiter awaiter + ) + { + _awaiter = awaiter; + } + + /// + /// Gets whether the underlying task has completed. + /// + public bool IsCompleted => _awaiter.IsCompleted; + + /// + /// Gets the result, unwrapping to just the data. + /// + public T GetResult() => _awaiter.GetResult().Data; + + /// + /// Schedules the continuation action. + /// + public void OnCompleted(global::System.Action continuation) => + _awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action without capturing the execution context. + /// + public void UnsafeOnCompleted(global::System.Action continuation) => + _awaiter.UnsafeOnCompleted(continuation); + } + } +} + +/// +/// A task-like type that wraps Task<RawResponse> and provides dual-mode awaiting for endpoints with no response body: +/// - Direct await completes with no value (void semantics) +/// - .WithRawResponse() yields RawResponse (when raw response metadata is needed) +/// +public readonly struct WithRawResponseTask +{ + private readonly global::System.Threading.Tasks.Task _task; + + /// + /// Creates a new WithRawResponseTask wrapping the given task. + /// + public WithRawResponseTask(global::System.Threading.Tasks.Task task) + { + _task = task; + } + + /// + /// Returns the underlying task that yields raw response metadata. + /// + public global::System.Threading.Tasks.Task WithRawResponse() => _task; + + /// + /// Awaiter delegates to the non-generic Task, completing with no value. + /// + public TaskAwaiter GetAwaiter() => ((global::System.Threading.Tasks.Task)_task).GetAwaiter(); + + /// + /// Configures the awaiter to continue on the captured context or not. The configured awaitable completes with no value. + /// + public global::System.Runtime.CompilerServices.ConfiguredTaskAwaitable ConfigureAwait( + bool continueOnCapturedContext + ) => ((global::System.Threading.Tasks.Task)_task).ConfigureAwait(continueOnCapturedContext); + + /// + /// Implicitly converts WithRawResponseTask to global::System.Threading.Tasks.Task for backward compatibility. + /// + public static implicit operator global::System.Threading.Tasks.Task(WithRawResponseTask task) + { + return task._task; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/QueryStringBuilder.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/QueryStringBuilder.cs new file mode 100644 index 000000000000..fb8cd80a4628 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/QueryStringBuilder.cs @@ -0,0 +1,656 @@ +using global::System.Buffers; +using global::System.Runtime.CompilerServices; +#if !NET6_0_OR_GREATER +using global::System.Text; +#endif + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// High-performance query string builder with RFC 3986 compliant percent-encoding. +/// Uses span-based APIs on .NET 6+ and StringBuilder fallback for older targets. +/// +/// RFC 3986 defines the following relevant productions: +/// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +/// query = *( pchar / "/" / "?" ) +/// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +/// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" +/// +/// Three encoding contexts are distinguished: +/// Path segment (pchar): unreserved + sub-delims + ":" + "@" +/// Query key: query chars minus "&", "=", "+", ";", "#" +/// Query value: query chars minus "&", "+", ";", "#" +/// +/// ";" is percent-encoded in queries even though RFC 3986 permits it: it is a +/// legacy parameter separator that many servers and frameworks still split on, +/// so leaving it raw truncates the value. +/// +internal static class QueryStringBuilder +{ + // ────────────────────────────────────────────────────────────────────── + // RFC 3986 character sets + // + // Query key safe: unreserved + (sub-delims \ {& = + ;}) + : @ / ? + // Query value safe: unreserved + (sub-delims \ {& + ;}) + : @ / ? + // Path segment safe: unreserved + sub-delims + : @ + // ────────────────────────────────────────────────────────────────────── + +#if NET8_0_OR_GREATER + private static readonly SearchValues SafeQueryKeyChars = SearchValues.Create( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$'()*,:@/?" + ); + + private static readonly SearchValues SafeQueryValueChars = SearchValues.Create( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$'()*,=:@/?" + ); + + private static readonly SearchValues SafePathChars = SearchValues.Create( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$&'()*+,;=:@" + ); +#else + private const string SafeQueryKeyChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$'()*,:@/?"; + + private const string SafeQueryValueChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$'()*,=:@/?"; + + private const string SafePathChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$&'()*+,;=:@"; +#endif + +#if NET7_0_OR_GREATER + private static ReadOnlySpan UpperHexChars => "0123456789ABCDEF"u8; +#else + private static readonly byte[] UpperHexChars = + { + (byte)'0', + (byte)'1', + (byte)'2', + (byte)'3', + (byte)'4', + (byte)'5', + (byte)'6', + (byte)'7', + (byte)'8', + (byte)'9', + (byte)'A', + (byte)'B', + (byte)'C', + (byte)'D', + (byte)'E', + (byte)'F', + }; +#endif + + private enum EncodingContext + { + QueryKey, + QueryValue, + Path, + } + + /// + /// Percent-encodes a path segment value per RFC 3986 section 3.3 (pchar). + /// Allowed unencoded: unreserved / sub-delims / ":" / "@" + /// + public static string EncodePathSegment(string value) + { + if (string.IsNullOrEmpty(value)) + return value; + +#if NET6_0_OR_GREATER + if (!NeedsEncoding(value.AsSpan(), EncodingContext.Path)) + return value; + + var buffer = ArrayPool.Shared.Rent(value.Length * 3); + try + { + var written = EncodeSlow(value.AsSpan(), buffer.AsSpan(), EncodingContext.Path); + return new string(buffer.AsSpan(0, written)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } +#else + var sb = new StringBuilder(value.Length); + AppendEncoded(sb, value, EncodingContext.Path); + return sb.ToString(); +#endif + } + + /// + /// Builds a query string from the provided parameters. + /// +#if NET6_0_OR_GREATER + public static string Build(ReadOnlySpan> parameters) + { + if (parameters.IsEmpty) + return string.Empty; + + var estimatedLength = EstimateLength(parameters); + if (estimatedLength == 0) + return string.Empty; + + var bufferSize = Math.Min(estimatedLength * 3, 8192); + var buffer = ArrayPool.Shared.Rent(bufferSize); + + try + { + var written = BuildCore(parameters, buffer); + return new string(buffer.AsSpan(0, written)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private static int EstimateLength(ReadOnlySpan> parameters) + { + var estimatedLength = 0; + foreach (var kvp in parameters) + { + estimatedLength += kvp.Key.Length + kvp.Value.Length + 2; + } + return estimatedLength; + } +#endif + + /// + /// Builds a query string from the provided parameters. + /// + public static string Build(IEnumerable> parameters) + { +#if NET6_0_OR_GREATER + // Try to get span access for collections that support it + if (parameters is ICollection> collection) + { + if (collection.Count == 0) + return string.Empty; + + var array = ArrayPool>.Shared.Rent(collection.Count); + try + { + collection.CopyTo(array, 0); + return Build(array.AsSpan(0, collection.Count)); + } + finally + { + ArrayPool>.Shared.Return(array); + } + } + + // Fallback for non-collection enumerables + using var enumerator = parameters.GetEnumerator(); + if (!enumerator.MoveNext()) + return string.Empty; + + var buffer = ArrayPool.Shared.Rent(4096); + try + { + var position = 0; + var first = true; + + do + { + var kvp = enumerator.Current; + + // Ensure capacity (worst case: 3x for encoding + separators) + var required = (kvp.Key.Length + kvp.Value.Length + 2) * 3; + if (position + required > buffer.Length) + { + var newBuffer = ArrayPool.Shared.Rent(buffer.Length * 2); + buffer.AsSpan(0, position).CopyTo(newBuffer); + ArrayPool.Shared.Return(buffer); + buffer = newBuffer; + } + + buffer[position++] = first ? '?' : '&'; + first = false; + + position += EncodeWithCharSet( + kvp.Key.AsSpan(), + buffer.AsSpan(position), + EncodingContext.QueryKey + ); + buffer[position++] = '='; + position += EncodeWithCharSet( + kvp.Value.AsSpan(), + buffer.AsSpan(position), + EncodingContext.QueryValue + ); + } while (enumerator.MoveNext()); + + return first ? string.Empty : new string(buffer.AsSpan(0, position)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } +#else + // netstandard2.0 / net462 fallback using StringBuilder + var sb = new StringBuilder(); + var first = true; + + foreach (var kvp in parameters) + { + sb.Append(first ? '?' : '&'); + first = false; + + AppendEncoded(sb, kvp.Key, EncodingContext.QueryKey); + sb.Append('='); + AppendEncoded(sb, kvp.Value, EncodingContext.QueryValue); + } + + return sb.ToString(); +#endif + } + +#if NET6_0_OR_GREATER + private static int BuildCore( + ReadOnlySpan> parameters, + Span buffer + ) + { + var position = 0; + var first = true; + + foreach (var kvp in parameters) + { + buffer[position++] = first ? '?' : '&'; + first = false; + + position += EncodeWithCharSet( + kvp.Key.AsSpan(), + buffer.Slice(position), + EncodingContext.QueryKey + ); + buffer[position++] = '='; + position += EncodeWithCharSet( + kvp.Value.AsSpan(), + buffer.Slice(position), + EncodingContext.QueryValue + ); + } + + return position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int EncodeWithCharSet( + ReadOnlySpan input, + Span output, + EncodingContext context + ) + { + if (!NeedsEncoding(input, context)) + { + input.CopyTo(output); + return input.Length; + } + + return EncodeSlow(input, output, context); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool NeedsEncoding(ReadOnlySpan value, EncodingContext context) + { + return context switch + { + EncodingContext.QueryKey => value.ContainsAnyExcept(SafeQueryKeyChars), + EncodingContext.QueryValue => value.ContainsAnyExcept(SafeQueryValueChars), + EncodingContext.Path => value.ContainsAnyExcept(SafePathChars), + _ => true, + }; + } + + private static int EncodeSlow( + ReadOnlySpan input, + Span output, + EncodingContext context + ) + { + var position = 0; + + foreach (var c in input) + { + if (IsSafeChar(c, context)) + { + output[position++] = c; + } + else if (c == ' ') + { + output[position++] = '%'; + output[position++] = '2'; + output[position++] = '0'; + } + else if (char.IsAscii(c)) + { + position += EncodeAscii((byte)c, output.Slice(position)); + } + else + { + position += EncodeUtf8(c, output.Slice(position)); + } + } + + return position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int EncodeAscii(byte value, Span output) + { + output[0] = '%'; + output[1] = (char)UpperHexChars[value >> 4]; + output[2] = (char)UpperHexChars[value & 0xF]; + return 3; + } + + private static int EncodeUtf8(char c, Span output) + { + Span utf8Bytes = stackalloc byte[4]; + Span singleChar = stackalloc char[1] { c }; + var byteCount = global::System.Text.Encoding.UTF8.GetBytes(singleChar, utf8Bytes); + + var position = 0; + for (var i = 0; i < byteCount; i++) + { + output[position++] = '%'; + output[position++] = (char)UpperHexChars[utf8Bytes[i] >> 4]; + output[position++] = (char)UpperHexChars[utf8Bytes[i] & 0xF]; + } + + return position; + } +#else + // netstandard2.0 / net462 StringBuilder-based encoding + private static void AppendEncoded(StringBuilder sb, string value, EncodingContext context) + { + foreach (var c in value) + { + if (IsSafeChar(c, context)) + { + sb.Append(c); + } + else if (c == ' ') + { + sb.Append("%20"); + } + else if (c <= 127) + { + AppendPercentEncoded(sb, (byte)c); + } + else + { + var bytes = Encoding.UTF8.GetBytes(new[] { c }); + foreach (var b in bytes) + { + AppendPercentEncoded(sb, b); + } + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendPercentEncoded(StringBuilder sb, byte value) + { + sb.Append('%'); + sb.Append((char)UpperHexChars[value >> 4]); + sb.Append((char)UpperHexChars[value & 0xF]); + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSafeChar(char c, EncodingContext context) + { + return context switch + { + EncodingContext.QueryKey => IsSafeQueryKeyChar(c), + EncodingContext.QueryValue => IsSafeQueryValueChar(c), + EncodingContext.Path => IsSafePathChar(c), + _ => false, + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSafeQueryKeyChar(char c) + { +#if NET8_0_OR_GREATER + return SafeQueryKeyChars.Contains(c); +#else + // query = *( pchar / "/" / "?" ) minus "&", "=", "+", ";", "#" + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '-' + || c == '_' + || c == '.' + || c == '~' + || c == '!' + || c == '$' + || c == (char)39 // single quote + || c == '(' + || c == ')' + || c == '*' + || c == ',' + || c == ':' + || c == '@' + || c == '/' + || c == '?'; +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSafeQueryValueChar(char c) + { +#if NET8_0_OR_GREATER + return SafeQueryValueChars.Contains(c); +#else + // query = *( pchar / "/" / "?" ) minus "&", "+", ";", "#" + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '-' + || c == '_' + || c == '.' + || c == '~' + || c == '!' + || c == '$' + || c == (char)39 // single quote + || c == '(' + || c == ')' + || c == '*' + || c == ',' + || c == '=' + || c == ':' + || c == '@' + || c == '/' + || c == '?'; +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSafePathChar(char c) + { +#if NET8_0_OR_GREATER + return SafePathChars.Contains(c); +#else + // pchar = unreserved / sub-delims / ":" / "@" + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '-' + || c == '_' + || c == '.' + || c == '~' + || c == '!' + || c == '$' + || c == '&' + || c == (char)39 // single quote + || c == '(' + || c == ')' + || c == '*' + || c == '+' + || c == ',' + || c == ';' + || c == '=' + || c == ':' + || c == '@'; +#endif + } + + /// + /// Fluent builder for constructing query strings with support for simple parameters and deep object notation. + /// + public sealed class Builder + { + private readonly List> _params; + + /// + /// Initializes a new instance with default capacity. + /// + public Builder() + { + _params = new List>(); + } + + /// + /// Initializes a new instance with the specified initial capacity. + /// + public Builder(int capacity) + { + _params = new List>(capacity); + } + + /// + /// Adds a simple parameter. For collections, adds multiple key-value pairs (one per element). + /// + public Builder Add(string key, object? value) + { + if (value is null) + { + return this; + } + + // Handle string separately since it implements IEnumerable + if (value is string stringValue) + { + _params.Add(new KeyValuePair(key, stringValue)); + return this; + } + + // Handle collections (arrays, lists, etc.) - add each element as a separate key-value pair + if ( + value + is global::System.Collections.IEnumerable enumerable + and not global::System.Collections.IDictionary + ) + { + foreach (var item in enumerable) + { + if (item is not null) + { + _params.Add( + new KeyValuePair( + key, + ValueConvert.ToQueryStringValue(item) + ) + ); + } + } + return this; + } + + // Handle scalar values + _params.Add( + new KeyValuePair(key, ValueConvert.ToQueryStringValue(value)) + ); + return this; + } + + /// + /// Sets a parameter, removing any existing parameters with the same key before adding the new value. + /// For collections, removes all existing parameters with the key, then adds multiple key-value pairs (one per element). + /// This allows overriding parameters set earlier in the builder. + /// + public Builder Set(string key, object? value) + { + // Remove all existing parameters with this key + _params.RemoveAll(kv => kv.Key == key); + + // Add the new value(s) + return Add(key, value); + } + + /// + /// Merges additional query parameters with override semantics. + /// Groups parameters by key and calls Set() once per unique key. + /// This ensures that parameters with the same key are properly merged: + /// - If a key appears once, it's added as a single value + /// - If a key appears multiple times, all values are added as an array + /// - All parameters override any existing parameters with the same key + /// + public Builder MergeAdditional( + global::System.Collections.Generic.IEnumerable>? additionalParameters + ) + { + if (additionalParameters is null) + { + return this; + } + + // Group by key to handle multiple values for the same key correctly + var grouped = additionalParameters + .GroupBy(kv => kv.Key) + .Select(g => new global::System.Collections.Generic.KeyValuePair( + g.Key, + g.Count() == 1 ? (object)g.First().Value : g.Select(kv => kv.Value).ToArray() + )); + + foreach (var param in grouped) + { + Set(param.Key, param.Value); + } + + return this; + } + + /// + /// Adds a complex object using deep object notation with a prefix. + /// Deep object notation nests properties with brackets: prefix[key][nested]=value + /// + public Builder AddDeepObject(string prefix, object? value) + { + if (value is not null) + { + _params.AddRange(QueryStringConverter.ToDeepObject(prefix, value)); + } + return this; + } + + /// + /// Adds a complex object using exploded form notation with an optional prefix. + /// Exploded form flattens properties: prefix[key]=value (no deep nesting). + /// + public Builder AddExploded(string prefix, object? value) + { + if (value is not null) + { + _params.AddRange(QueryStringConverter.ToExplodedForm(prefix, value)); + } + return this; + } + + /// + /// Builds the final query string. + /// + public string Build() + { + return QueryStringBuilder.Build(_params); + } + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/QueryStringConverter.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/QueryStringConverter.cs new file mode 100644 index 000000000000..9d5d0dbbb038 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/QueryStringConverter.cs @@ -0,0 +1,259 @@ +using global::System.Text.Json; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Converts an object into a query string collection. +/// +internal static class QueryStringConverter +{ + /// + /// Converts an object into a query string collection using Deep Object notation with a prefix. + /// + /// The prefix to prepend to all keys (e.g., "session_settings"). Pass empty string for no prefix. + /// Object to form URL-encode. Can be an object, array of objects, or dictionary. + /// Throws when passing in a string or primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToDeepObject( + string prefix, + object value + ) + { + var queryCollection = new List>(); + var json = JsonUtils.SerializeToElement(value); + JsonToDeepObject(json, prefix, queryCollection); + return queryCollection; + } + + /// + /// Converts an object into a query string collection using Deep Object notation. + /// + /// Object to form URL-encode. Can be an object, array of objects, or dictionary. + /// Throws when passing in a string or primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToDeepObject(object value) + { + return ToDeepObject("", value); + } + + /// + /// Converts an object into a query string collection using Exploded Form notation with a prefix. + /// + /// The prefix to prepend to all keys. Pass empty string for no prefix. + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToExplodedForm( + string prefix, + object value + ) + { + var queryCollection = new List>(); + var json = JsonUtils.SerializeToElement(value); + AssertRootJson(json); + JsonToFormExploded(json, prefix, queryCollection); + return queryCollection; + } + + /// + /// Converts an object into a query string collection using Exploded Form notation. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToExplodedForm(object value) + { + return ToExplodedForm("", value); + } + + /// + /// Converts an object into a query string collection using Form notation without exploding parameters. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToForm(object value) + { + var queryCollection = new List>(); + var json = JsonUtils.SerializeToElement(value); + AssertRootJson(json); + JsonToForm(json, "", queryCollection); + return queryCollection; + } + + private static void AssertRootJson(JsonElement json) + { + switch (json.ValueKind) + { + case JsonValueKind.Object: + break; + case JsonValueKind.Array: + case JsonValueKind.Undefined: + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Null: + default: + throw new global::System.Exception( + $"Only objects can be converted to query string collections. Given type is {json.ValueKind}." + ); + } + } + + private static void JsonToForm( + JsonElement element, + string prefix, + List> parameters + ) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var newPrefix = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}[{property.Name}]"; + + JsonToForm(property.Value, newPrefix, parameters); + } + break; + case JsonValueKind.Array: + var arrayValues = element.EnumerateArray().Select(ValueToString).ToArray(); + parameters.Add( + new KeyValuePair(prefix, string.Join(",", arrayValues)) + ); + break; + case JsonValueKind.Null: + break; + case JsonValueKind.Undefined: + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + default: + parameters.Add(new KeyValuePair(prefix, ValueToString(element))); + break; + } + } + + private static void JsonToFormExploded( + JsonElement element, + string prefix, + List> parameters + ) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var newPrefix = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}[{property.Name}]"; + + JsonToFormExploded(property.Value, newPrefix, parameters); + } + + break; + case JsonValueKind.Array: + foreach (var item in element.EnumerateArray()) + { + if ( + item.ValueKind != JsonValueKind.Object + && item.ValueKind != JsonValueKind.Array + ) + { + parameters.Add( + new KeyValuePair(prefix, ValueToString(item)) + ); + } + else + { + JsonToFormExploded(item, prefix, parameters); + } + } + + break; + case JsonValueKind.Null: + break; + case JsonValueKind.Undefined: + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + default: + parameters.Add(new KeyValuePair(prefix, ValueToString(element))); + break; + } + } + + private static void JsonToDeepObject( + JsonElement element, + string prefix, + List> parameters + ) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var newPrefix = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}[{property.Name}]"; + + JsonToDeepObject(property.Value, newPrefix, parameters); + } + + break; + case JsonValueKind.Array: + var index = 0; + foreach (var item in element.EnumerateArray()) + { + var newPrefix = $"{prefix}[{index++}]"; + + if ( + item.ValueKind != JsonValueKind.Object + && item.ValueKind != JsonValueKind.Array + ) + { + parameters.Add( + new KeyValuePair(newPrefix, ValueToString(item)) + ); + } + else + { + JsonToDeepObject(item, newPrefix, parameters); + } + } + + break; + case JsonValueKind.Null: + case JsonValueKind.Undefined: + // Skip null and undefined values - don't add parameters for them + break; + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + default: + parameters.Add(new KeyValuePair(prefix, ValueToString(element))); + break; + } + } + + private static string ValueToString(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString() ?? "", + JsonValueKind.Number => element.GetRawText(), + JsonValueKind.True => "true", + JsonValueKind.False => "false", + JsonValueKind.Null => "", + _ => element.GetRawText(), + }; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/RawClient.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/RawClient.cs new file mode 100644 index 000000000000..7047439a9101 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/RawClient.cs @@ -0,0 +1,364 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; +using global::System.Text; +using SystemTask = global::System.Threading.Tasks.Task; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Utility class for making raw HTTP requests to the API. +/// +internal partial class RawClient(ClientOptions clientOptions) +{ + private const int MaxRetryDelayMs = 60000; + private const double JitterFactor = 0.2; +#if NET6_0_OR_GREATER + // Use Random.Shared for thread-safe random number generation on .NET 6+ +#else + private static readonly object JitterLock = new(); + private static readonly Random JitterRandom = new(); +#endif + internal int BaseRetryDelay { get; set; } = 1000; + + /// + /// The client options applied on every request. + /// + internal readonly ClientOptions Options = clientOptions; + + internal async global::System.Threading.Tasks.Task SendRequestAsync( + global::SeedCsharpGlobalHeaderLiteralEnv.Core.BaseRequest request, + CancellationToken cancellationToken = default + ) + { + // Apply the request timeout. + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var timeout = request.Options?.Timeout ?? Options.Timeout; + cts.CancelAfter(timeout); + + var httpRequest = await CreateHttpRequestAsync(request).ConfigureAwait(false); + // Send the request. + return await SendWithRetriesAsync(httpRequest, request.Options, cts.Token) + .ConfigureAwait(false); + } + + internal async global::System.Threading.Tasks.Task SendRequestAsync( + HttpRequestMessage request, + IRequestOptions? options, + CancellationToken cancellationToken = default + ) + { + // Apply the request timeout. + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var timeout = options?.Timeout ?? Options.Timeout; + cts.CancelAfter(timeout); + + // Send the request. + return await SendWithRetriesAsync(request, options, cts.Token).ConfigureAwait(false); + } + + private static async global::System.Threading.Tasks.Task CloneRequestAsync( + HttpRequestMessage request, + CancellationToken cancellationToken = default + ) + { + var clonedRequest = new HttpRequestMessage(request.Method, request.RequestUri); + clonedRequest.Version = request.Version; + + if (request.Content != null) + { + switch (request.Content) + { + case MultipartContent oldMultipartFormContent: + var originalBoundary = + oldMultipartFormContent + .Headers.ContentType?.Parameters.First(p => + p.Name.Equals("boundary", StringComparison.OrdinalIgnoreCase) + ) + .Value?.Trim('"') + ?? Guid.NewGuid().ToString(); + var newMultipartContent = oldMultipartFormContent switch + { + MultipartFormDataContent => new MultipartFormDataContent(originalBoundary), + _ => new MultipartContent(), + }; + foreach (var content in oldMultipartFormContent) + { + var ms = new MemoryStream(); +#if NET5_0_OR_GREATER + await content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false); +#else + await content.CopyToAsync(ms).ConfigureAwait(false); +#endif + ms.Position = 0; + var newPart = new StreamContent(ms); + foreach (var header in content.Headers) + { + newPart.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + newMultipartContent.Add(newPart); + } + + clonedRequest.Content = newMultipartContent; + break; + default: + var bodyStream = new MemoryStream(); +#if NET5_0_OR_GREATER + await request + .Content.CopyToAsync(bodyStream, cancellationToken) + .ConfigureAwait(false); +#else + await request.Content.CopyToAsync(bodyStream).ConfigureAwait(false); +#endif + bodyStream.Position = 0; + var clonedContent = new StreamContent(bodyStream); + foreach (var header in request.Content.Headers) + { + clonedContent.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + clonedRequest.Content = clonedContent; + break; + } + } + + foreach (var header in request.Headers) + { + clonedRequest.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return clonedRequest; + } + + /// + /// Sends the request with retries, unless the request content is not retryable, + /// such as stream requests and multipart form data with stream content. + /// + private async global::System.Threading.Tasks.Task SendWithRetriesAsync( + HttpRequestMessage request, + IRequestOptions? options, + CancellationToken cancellationToken + ) + { + var httpClient = options?.HttpClient ?? Options.HttpClient; + var maxRetries = Math.Max(0, options?.MaxRetries ?? Options.MaxRetries); + var isRetryableContent = IsRetryableContent(request); + + if (!isRetryableContent || maxRetries == 0) + { + var response = await httpClient + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + return new global::SeedCsharpGlobalHeaderLiteralEnv.Core.ApiResponse + { + StatusCode = (int)response.StatusCode, + Raw = response, + }; + } + + // Always send a clone, never the original: HttpClient (e.g. under HTTP/2) disposes + // request.Content after sending, which would break the next attempt's clone. + HttpResponseMessage? retryResponse = null; + for (var attempt = 0; attempt <= maxRetries; attempt++) + { + if (attempt > 0) + { + var delayMs = GetRetryDelayFromHeaders(retryResponse!, attempt - 1); + await SystemTask.Delay(delayMs, cancellationToken).ConfigureAwait(false); + } + + using var attemptRequest = await CloneRequestAsync(request, cancellationToken) + .ConfigureAwait(false); + retryResponse = await httpClient + .SendAsync( + attemptRequest, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken + ) + .ConfigureAwait(false); + + if (!ShouldRetry(retryResponse)) + { + break; + } + } + + return new global::SeedCsharpGlobalHeaderLiteralEnv.Core.ApiResponse + { + StatusCode = (int)retryResponse!.StatusCode, + Raw = retryResponse, + }; + } + + private static bool ShouldRetry(HttpResponseMessage response) + { + var statusCode = (int)response.StatusCode; + + return statusCode is 408 or 429 or (>= 500); + } + + private static int AddPositiveJitter(int delayMs) + { +#if NET6_0_OR_GREATER + var random = Random.Shared.NextDouble(); +#else + double random; + lock (JitterLock) + { + random = JitterRandom.NextDouble(); + } +#endif + var jitterMultiplier = 1 + random * JitterFactor; + return (int)(delayMs * jitterMultiplier); + } + + private static int AddSymmetricJitter(int delayMs) + { +#if NET6_0_OR_GREATER + var random = Random.Shared.NextDouble(); +#else + double random; + lock (JitterLock) + { + random = JitterRandom.NextDouble(); + } +#endif + var jitterMultiplier = 1 + (random - 0.5) * JitterFactor; + return (int)(delayMs * jitterMultiplier); + } + + private int GetRetryDelayFromHeaders(HttpResponseMessage response, int retryAttempt) + { + if (response.Headers.TryGetValues("Retry-After", out var retryAfterValues)) + { + var retryAfter = retryAfterValues.FirstOrDefault(); + if (!string.IsNullOrEmpty(retryAfter)) + { + if (int.TryParse(retryAfter, out var retryAfterSeconds) && retryAfterSeconds > 0) + { + return Math.Min(retryAfterSeconds * 1000, MaxRetryDelayMs); + } + + if (DateTimeOffset.TryParse(retryAfter, out var retryAfterDate)) + { + var delay = (int)(retryAfterDate - DateTimeOffset.UtcNow).TotalMilliseconds; + if (delay > 0) + { + return Math.Min(delay, MaxRetryDelayMs); + } + } + } + } + + if (response.Headers.TryGetValues("X-RateLimit-Reset", out var rateLimitResetValues)) + { + var rateLimitReset = rateLimitResetValues.FirstOrDefault(); + if ( + !string.IsNullOrEmpty(rateLimitReset) + && long.TryParse(rateLimitReset, out var resetTime) + ) + { + var resetDateTime = DateTimeOffset.FromUnixTimeSeconds(resetTime); + var delay = (int)(resetDateTime - DateTimeOffset.UtcNow).TotalMilliseconds; + if (delay > 0) + { + return AddPositiveJitter(Math.Min(delay, MaxRetryDelayMs)); + } + } + } + + var exponentialDelay = Math.Min(BaseRetryDelay * (1 << retryAttempt), MaxRetryDelayMs); + return AddSymmetricJitter(exponentialDelay); + } + + private static bool IsRetryableContent(HttpRequestMessage request) + { + return request.Content switch + { + IIsRetryableContent c => c.IsRetryable, + StreamContent => false, + MultipartContent content => !content.Any(c => c is StreamContent), + _ => true, + }; + } + + internal async global::System.Threading.Tasks.Task CreateHttpRequestAsync( + global::SeedCsharpGlobalHeaderLiteralEnv.Core.BaseRequest request + ) + { + var url = BuildUrl(request); + var httpRequest = new HttpRequestMessage(request.Method, url); + httpRequest.Content = request.CreateContent(); + SetHeaders(httpRequest, request.Headers); + + return httpRequest; + } + + private string BuildUrl(global::SeedCsharpGlobalHeaderLiteralEnv.Core.BaseRequest request) + { + var baseUrl = request.Options?.BaseUrl ?? request.BaseUrl ?? Options.BaseUrl; + + var trimmedBaseUrl = baseUrl.TrimEnd('/'); + var trimmedBasePath = request.Path.TrimStart('/'); + var url = $"{trimmedBaseUrl}/{trimmedBasePath}"; + + // Append query string if present + if (!string.IsNullOrEmpty(request.QueryString)) + { + return url + request.QueryString; + } + + return url; + } + + private void SetHeaders(HttpRequestMessage httpRequest, Dictionary? headers) + { + if (headers is null) + { + return; + } + + foreach (var kv in headers) + { + if (kv.Value is null) + { + continue; + } + + httpRequest.Headers.TryAddWithoutValidation(kv.Key, kv.Value); + } + } + + private static (Encoding encoding, string? charset, string mediaType) ParseContentTypeOrDefault( + string? contentType, + Encoding encodingFallback, + string mediaTypeFallback + ) + { + var encoding = encodingFallback; + var mediaType = mediaTypeFallback; + string? charset = null; + if (string.IsNullOrEmpty(contentType)) + { + return (encoding, charset, mediaType); + } + + if (!MediaTypeHeaderValue.TryParse(contentType, out var mediaTypeHeaderValue)) + { + return (encoding, charset, mediaType); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.CharSet)) + { + charset = mediaTypeHeaderValue.CharSet; + encoding = Encoding.GetEncoding(mediaTypeHeaderValue.CharSet); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.MediaType)) + { + mediaType = mediaTypeHeaderValue.MediaType; + } + + return (encoding, charset, mediaType); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/RawResponse.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/RawResponse.cs new file mode 100644 index 000000000000..541128efcb1e --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/RawResponse.cs @@ -0,0 +1,24 @@ +using global::System.Net; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Contains HTTP response metadata including status code, URL, and headers. +/// +public record RawResponse +{ + /// + /// The HTTP status code of the response. + /// + public required HttpStatusCode StatusCode { get; init; } + + /// + /// The request URL that generated this response. + /// + public required Uri Url { get; init; } + + /// + /// The HTTP response headers. + /// + public required Core.ResponseHeaders Headers { get; init; } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ResponseHeaders.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ResponseHeaders.cs new file mode 100644 index 000000000000..de330e93cec4 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ResponseHeaders.cs @@ -0,0 +1,108 @@ +using global::System.Collections; +using global::System.Net.Http.Headers; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Represents HTTP response headers with case-insensitive lookup. +/// +public readonly struct ResponseHeaders : IEnumerable +{ + private readonly HttpResponseHeaders? _headers; + private readonly HttpContentHeaders? _contentHeaders; + + private ResponseHeaders(HttpResponseHeaders headers, HttpContentHeaders? contentHeaders) + { + _headers = headers; + _contentHeaders = contentHeaders; + } + + /// + /// Gets the Content-Type header value, if present. + /// + public string? ContentType => _contentHeaders?.ContentType?.ToString(); + + /// + /// Gets the Content-Length header value, if present. + /// + public long? ContentLength => _contentHeaders?.ContentLength; + + /// + /// Creates a ResponseHeaders instance from an HttpResponseMessage. + /// + public static ResponseHeaders FromHttpResponseMessage(HttpResponseMessage response) + { + return new ResponseHeaders(response.Headers, response.Content?.Headers); + } + + /// + /// Tries to get a single header value. Returns the first value if multiple values exist. + /// + public bool TryGetValue(string name, out string? value) + { + if (TryGetValues(name, out var values) && values is not null) + { + value = values.FirstOrDefault(); + return true; + } + + value = null; + return false; + } + + /// + /// Tries to get all values for a header. + /// + public bool TryGetValues(string name, out IEnumerable? values) + { + if (_headers?.TryGetValues(name, out values) == true) + { + return true; + } + + if (_contentHeaders?.TryGetValues(name, out values) == true) + { + return true; + } + + values = null; + return false; + } + + /// + /// Checks if the headers contain a specific header name. + /// + public bool Contains(string name) + { + return _headers?.Contains(name) == true || _contentHeaders?.Contains(name) == true; + } + + /// + /// Gets an enumerator for all headers. + /// + public IEnumerator GetEnumerator() + { + if (_headers is not null) + { + foreach (var header in _headers) + { + yield return new HttpHeader(header.Key, string.Join(", ", header.Value)); + } + } + + if (_contentHeaders is not null) + { + foreach (var header in _contentHeaders) + { + yield return new HttpHeader(header.Key, string.Join(", ", header.Value)); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +/// +/// Represents a single HTTP header. +/// +public readonly record struct HttpHeader(string Name, string Value); diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StreamRequest.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StreamRequest.cs new file mode 100644 index 000000000000..ff161a26b19c --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StreamRequest.cs @@ -0,0 +1,29 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// The request object to be sent for streaming uploads. +/// +internal record StreamRequest : BaseRequest +{ + internal Stream? Body { get; init; } + + internal override HttpContent? CreateContent() + { + if (Body is null) + { + return null; + } + + var content = new StreamContent(Body) + { + Headers = + { + ContentType = MediaTypeHeaderValue.Parse(ContentType ?? "application/octet-stream"), + }, + }; + return content; + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StringEnum.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StringEnum.cs new file mode 100644 index 000000000000..9946e64dc2b2 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StringEnum.cs @@ -0,0 +1,6 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +public interface IStringEnum : IEquatable +{ + public string Value { get; } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StringEnumExtensions.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StringEnumExtensions.cs new file mode 100644 index 000000000000..bbe8ccbfd1f8 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/StringEnumExtensions.cs @@ -0,0 +1,6 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +internal static class StringEnumExtensions +{ + public static string Stringify(this IStringEnum stringEnum) => stringEnum.Value; +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ValueConvert.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ValueConvert.cs new file mode 100644 index 000000000000..26502a008991 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Core/ValueConvert.cs @@ -0,0 +1,115 @@ +using global::System.Globalization; + +namespace SeedCsharpGlobalHeaderLiteralEnv.Core; + +/// +/// Convert values to string for path and query parameters. +/// +public static class ValueConvert +{ + internal static string ToPathParameterString(T value) => ToString(value); + + internal static string ToPathParameterString(bool v) => ToString(v); + + internal static string ToPathParameterString(int v) => ToString(v); + + internal static string ToPathParameterString(long v) => ToString(v); + + internal static string ToPathParameterString(float v) => ToString(v); + + internal static string ToPathParameterString(double v) => ToString(v); + + internal static string ToPathParameterString(decimal v) => ToString(v); + + internal static string ToPathParameterString(short v) => ToString(v); + + internal static string ToPathParameterString(ushort v) => ToString(v); + + internal static string ToPathParameterString(uint v) => ToString(v); + + internal static string ToPathParameterString(ulong v) => ToString(v); + + internal static string ToPathParameterString(string v) => + QueryStringBuilder.EncodePathSegment(v); + + internal static string ToPathParameterString(char v) => ToString(v); + + internal static string ToPathParameterString(Guid v) => ToString(v); + + internal static string ToQueryStringValue(T value) => value is null ? "" : ToString(value); + + internal static string ToQueryStringValue(bool v) => ToString(v); + + internal static string ToQueryStringValue(int v) => ToString(v); + + internal static string ToQueryStringValue(long v) => ToString(v); + + internal static string ToQueryStringValue(float v) => ToString(v); + + internal static string ToQueryStringValue(double v) => ToString(v); + + internal static string ToQueryStringValue(decimal v) => ToString(v); + + internal static string ToQueryStringValue(short v) => ToString(v); + + internal static string ToQueryStringValue(ushort v) => ToString(v); + + internal static string ToQueryStringValue(uint v) => ToString(v); + + internal static string ToQueryStringValue(ulong v) => ToString(v); + + internal static string ToQueryStringValue(string v) => v is null ? "" : v; + + internal static string ToQueryStringValue(char v) => ToString(v); + + internal static string ToQueryStringValue(Guid v) => ToString(v); + + internal static string ToString(T value) + { + return value switch + { + null => "null", + string str => str, + true => "true", + false => "false", + int i => ToString(i), + long l => ToString(l), + float f => ToString(f), + double d => ToString(d), + decimal dec => ToString(dec), + short s => ToString(s), + ushort u => ToString(u), + uint u => ToString(u), + ulong u => ToString(u), + char c => ToString(c), + Guid guid => ToString(guid), + _ => JsonUtils.SerializeRelaxedEscaping(value, value.GetType()).Trim('"'), + }; + } + + internal static string ToString(bool v) => v ? "true" : "false"; + + internal static string ToString(int v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(long v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(float v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(double v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(decimal v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(short v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(ushort v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(uint v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(ulong v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(char v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(string v) => v; + + internal static string ToString(Guid v) => v.ToString("D"); +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/ISeedCsharpGlobalHeaderLiteralEnvClient.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/ISeedCsharpGlobalHeaderLiteralEnvClient.cs new file mode 100644 index 000000000000..a1fa0865808e --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/ISeedCsharpGlobalHeaderLiteralEnvClient.cs @@ -0,0 +1,6 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv; + +public partial interface ISeedCsharpGlobalHeaderLiteralEnvClient +{ + public IServiceClient Service { get; } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.Custom.props b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.Custom.props new file mode 100644 index 000000000000..17a84cada530 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.Custom.props @@ -0,0 +1,20 @@ + + + + diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.csproj b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.csproj new file mode 100644 index 000000000000..9cdd222e57e3 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.csproj @@ -0,0 +1,68 @@ + + + net462;net8.0;net9.0;netstandard2.0 + enable + 12 + enable + 0.0.1 + $(Version) + $(Version) + README.md + https://github.com/csharp-global-header-literal-env/fern + true + + + + false + + + $(DefineConstants);USE_PORTABLE_DATE_ONLY + true + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + <_Parameter1>SeedCsharpGlobalHeaderLiteralEnv.Test + + + + + diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnvClient.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnvClient.cs new file mode 100644 index 000000000000..eed293756061 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnvClient.cs @@ -0,0 +1,60 @@ +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv; + +public partial class SeedCsharpGlobalHeaderLiteralEnvClient + : ISeedCsharpGlobalHeaderLiteralEnvClient +{ + private readonly RawClient _client; + + public SeedCsharpGlobalHeaderLiteralEnvClient( + string? token = null, + string? Version = null, + ClientOptions? clientOptions = null + ) + { + token ??= GetFromEnvironmentOrThrow( + "SQUARE_TOKEN", + "Please pass in token or set the environment variable SQUARE_TOKEN." + ); + Version ??= Environment.GetEnvironmentVariable("VERSION") ?? "2026-07-15"; + clientOptions ??= new ClientOptions(); + var platformHeaders = new Headers( + new Dictionary() + { + { "X-Fern-Language", "C#" }, + { "X-Fern-SDK-Name", "SeedCsharpGlobalHeaderLiteralEnv" }, + { "X-Fern-SDK-Version", global::SeedCsharpGlobalHeaderLiteralEnv.Version.Current }, + { "User-Agent", "Ferncsharp-global-header-literal-env/0.0.1" }, + } + ); + foreach (var header in platformHeaders) + { + if (!clientOptions.Headers.ContainsKey(header.Key)) + { + clientOptions.Headers[header.Key] = header.Value; + } + } + var clientOptionsWithAuth = clientOptions.Clone(); + var authHeaders = new Headers( + new Dictionary() + { + { "Authorization", $"Bearer {token ?? ""}" }, + { "Square-Version", Version ?? "2026-07-15" }, + } + ); + foreach (var header in authHeaders) + { + clientOptionsWithAuth.Headers[header.Key] = header.Value; + } + _client = new RawClient(clientOptionsWithAuth); + Service = new ServiceClient(_client); + } + + public IServiceClient Service { get; } + + private static string GetFromEnvironmentOrThrow(string env, string message) + { + return Environment.GetEnvironmentVariable(env) ?? throw new Exception(message); + } +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Service/IServiceClient.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Service/IServiceClient.cs new file mode 100644 index 000000000000..914d0792b7c7 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Service/IServiceClient.cs @@ -0,0 +1,12 @@ +namespace SeedCsharpGlobalHeaderLiteralEnv; + +public partial interface IServiceClient +{ + /// + /// GET request with a literal version header + /// + WithRawResponseTask GetWithLiteralVersionHeaderAsync( + RequestOptions? options = null, + CancellationToken cancellationToken = default + ); +} diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Service/ServiceClient.cs b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Service/ServiceClient.cs new file mode 100644 index 000000000000..1c612096c6f3 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/src/SeedCsharpGlobalHeaderLiteralEnv/Service/ServiceClient.cs @@ -0,0 +1,112 @@ +using global::System.Text.Json; +using SeedCsharpGlobalHeaderLiteralEnv.Core; + +namespace SeedCsharpGlobalHeaderLiteralEnv; + +public partial class ServiceClient : IServiceClient +{ + private readonly RawClient _client; + + internal ServiceClient(RawClient client) + { + _client = client; + } + + private async Task> GetWithLiteralVersionHeaderAsyncCore( + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var _queryString = new SeedCsharpGlobalHeaderLiteralEnv.Core.QueryStringBuilder.Builder( + capacity: 0 + ) + .MergeAdditional(options?.AdditionalQueryParameters) + .Build(); + var _headers = await new SeedCsharpGlobalHeaderLiteralEnv.Core.HeadersBuilder.Builder() + .Add(_client.Options.Headers) + .Add(_client.Options.AdditionalHeaders) + .Add(options?.AdditionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + var response = await _client + .SendRequestAsync( + new JsonRequest + { + Method = HttpMethod.Get, + Path = "version", + QueryString = _queryString, + Headers = _headers, + Options = options, + }, + cancellationToken + ) + .ConfigureAwait(false); + if (response.StatusCode is >= 200 and < 400) + { + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + try + { + var responseData = JsonUtils.Deserialize(responseBody)!; + return new WithRawResponse() + { + Data = responseData, + RawResponse = new SeedCsharpGlobalHeaderLiteralEnv.RawResponse() + { + StatusCode = response.Raw.StatusCode, + Url = response.Raw.RequestMessage?.RequestUri ?? new Uri("about:blank"), + Headers = ResponseHeaders.FromHttpResponseMessage(response.Raw), + }, + }; + } + catch (JsonException e) + { + throw new SeedCsharpGlobalHeaderLiteralEnvApiException( + "Failed to deserialize response", + response.StatusCode, + responseBody, + e, + rawResponse: new SeedCsharpGlobalHeaderLiteralEnv.RawResponse() + { + StatusCode = response.Raw.StatusCode, + Url = response.Raw.RequestMessage?.RequestUri ?? new Uri("about:blank"), + Headers = ResponseHeaders.FromHttpResponseMessage(response.Raw), + } + ); + } + } + { + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + throw new SeedCsharpGlobalHeaderLiteralEnvApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody, + rawResponse: new SeedCsharpGlobalHeaderLiteralEnv.RawResponse() + { + StatusCode = response.Raw.StatusCode, + Url = response.Raw.RequestMessage?.RequestUri ?? new Uri("about:blank"), + Headers = ResponseHeaders.FromHttpResponseMessage(response.Raw), + } + ); + } + } + + /// + /// GET request with a literal version header + /// + /// + /// await client.Service.GetWithLiteralVersionHeaderAsync(); + /// + public WithRawResponseTask GetWithLiteralVersionHeaderAsync( + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return new WithRawResponseTask( + GetWithLiteralVersionHeaderAsyncCore(options, cancellationToken) + ); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApiClient.cs index 85a0d6a11eaa..57fe96640990 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApiClient.cs @@ -20,7 +20,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Ferncsharp-grpc-proto-exhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/SeedApiClient.cs index 0ac0815067d2..f8a10fe69722 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Ferncsharp-grpc-proto-exhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/SeedApiClient.cs index e69871809ae3..a5cca857d84c 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "Seed.Client" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Ferncsharp-grpc-proto-exhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/SeedApiClient.cs index 0ac0815067d2..f8a10fe69722 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Ferncsharp-grpc-proto-exhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApiClient.cs index c31c606689d7..576d5d2eff6f 100644 --- a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Ferncsharp-grpc-proto/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-inline-types/inline-types/src/SeedObject/SeedObjectClient.cs b/seed/csharp-sdk/csharp-inline-types/inline-types/src/SeedObject/SeedObjectClient.cs index 11329621cd8e..aaf0c544b1fb 100644 --- a/seed/csharp-sdk/csharp-inline-types/inline-types/src/SeedObject/SeedObjectClient.cs +++ b/seed/csharp-sdk/csharp-inline-types/inline-types/src/SeedObject/SeedObjectClient.cs @@ -15,7 +15,7 @@ public SeedObjectClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedObject" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedObject.Version.Current }, { "User-Agent", "Ferncsharp-inline-types/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-multi-env-url-templating/no-custom-config/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/csharp-multi-env-url-templating/no-custom-config/src/SeedApi/SeedApiClient.cs index bae3c2aeed0b..29e6289ccbaa 100644 --- a/seed/csharp-sdk/csharp-multi-env-url-templating/no-custom-config/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/csharp-multi-env-url-templating/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -51,7 +51,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Ferncsharp-multi-env-url-templating/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-namespace-collision/explicit-namespaces/src/Contoso.Net/Contoso.cs b/seed/csharp-sdk/csharp-namespace-collision/explicit-namespaces/src/Contoso.Net/Contoso.cs index abe2c164b698..99dfa3551d94 100644 --- a/seed/csharp-sdk/csharp-namespace-collision/explicit-namespaces/src/Contoso.Net/Contoso.cs +++ b/seed/csharp-sdk/csharp-namespace-collision/explicit-namespaces/src/Contoso.Net/Contoso.cs @@ -17,7 +17,7 @@ public Contoso(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "Contoso.Net" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::Contoso.Net.Version.Current }, { "User-Agent", "Ferncsharp-namespace-collision/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-namespace-collision/namespace-client-collision/src/Contoso.Net/Contoso.cs b/seed/csharp-sdk/csharp-namespace-collision/namespace-client-collision/src/Contoso.Net/Contoso.cs index abe2c164b698..99dfa3551d94 100644 --- a/seed/csharp-sdk/csharp-namespace-collision/namespace-client-collision/src/Contoso.Net/Contoso.cs +++ b/seed/csharp-sdk/csharp-namespace-collision/namespace-client-collision/src/Contoso.Net/Contoso.cs @@ -17,7 +17,7 @@ public Contoso(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "Contoso.Net" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::Contoso.Net.Version.Current }, { "User-Agent", "Ferncsharp-namespace-collision/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-namespace-collision/no-client-namespace-match/src/Contoso.Net/ContosoClient.cs b/seed/csharp-sdk/csharp-namespace-collision/no-client-namespace-match/src/Contoso.Net/ContosoClient.cs index 2f55f71fd9ea..d6aafd8acd86 100644 --- a/seed/csharp-sdk/csharp-namespace-collision/no-client-namespace-match/src/Contoso.Net/ContosoClient.cs +++ b/seed/csharp-sdk/csharp-namespace-collision/no-client-namespace-match/src/Contoso.Net/ContosoClient.cs @@ -17,7 +17,7 @@ public ContosoClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "Contoso.Net" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::Contoso.Net.Version.Current }, { "User-Agent", "Ferncsharp-namespace-collision/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-namespace-conflict/client-class-name-matches-namespace-root/src/Seed.CsharpNamespaceConflict/Seed.cs b/seed/csharp-sdk/csharp-namespace-conflict/client-class-name-matches-namespace-root/src/Seed.CsharpNamespaceConflict/Seed.cs index 7012fd217ad7..f91458b6716d 100644 --- a/seed/csharp-sdk/csharp-namespace-conflict/client-class-name-matches-namespace-root/src/Seed.CsharpNamespaceConflict/Seed.cs +++ b/seed/csharp-sdk/csharp-namespace-conflict/client-class-name-matches-namespace-root/src/Seed.CsharpNamespaceConflict/Seed.cs @@ -14,7 +14,7 @@ public Seed(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "Seed.CsharpNamespaceConflict" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::Seed.CsharpNamespaceConflict.Version.Current }, { "User-Agent", "Ferncsharp-namespace-conflict/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-oauth-token-optional/src/SeedCsharpOauthTokenOptional/SeedCsharpOauthTokenOptionalClient.cs b/seed/csharp-sdk/csharp-oauth-token-optional/src/SeedCsharpOauthTokenOptional/SeedCsharpOauthTokenOptionalClient.cs index b143da35b3a5..b08f23b0441d 100644 --- a/seed/csharp-sdk/csharp-oauth-token-optional/src/SeedCsharpOauthTokenOptional/SeedCsharpOauthTokenOptionalClient.cs +++ b/seed/csharp-sdk/csharp-oauth-token-optional/src/SeedCsharpOauthTokenOptional/SeedCsharpOauthTokenOptionalClient.cs @@ -18,7 +18,7 @@ public SeedCsharpOauthTokenOptionalClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedCsharpOauthTokenOptional" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedCsharpOauthTokenOptional.Version.Current }, { "User-Agent", "Ferncsharp-oauth-token-optional/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-path-param-order/no-custom-config/src/SeedCsharpPathParamOrder/SeedCsharpPathParamOrderClient.cs b/seed/csharp-sdk/csharp-path-param-order/no-custom-config/src/SeedCsharpPathParamOrder/SeedCsharpPathParamOrderClient.cs index 018de7a76216..05b5dee82806 100644 --- a/seed/csharp-sdk/csharp-path-param-order/no-custom-config/src/SeedCsharpPathParamOrder/SeedCsharpPathParamOrderClient.cs +++ b/seed/csharp-sdk/csharp-path-param-order/no-custom-config/src/SeedCsharpPathParamOrder/SeedCsharpPathParamOrderClient.cs @@ -15,7 +15,7 @@ public SeedCsharpPathParamOrderClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedCsharpPathParamOrder" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedCsharpPathParamOrder.Version.Current }, { "User-Agent", "Ferncsharp-path-param-order/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-property-name-collision/no-custom-config/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/csharp-property-name-collision/no-custom-config/src/SeedApi/SeedApiClient.cs index bef4298a724d..bf7b8972bde1 100644 --- a/seed/csharp-sdk/csharp-property-name-collision/no-custom-config/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/csharp-property-name-collision/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Ferncsharp-property-name-collision/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest/SeedCsharpReadonlyRequestClient.cs b/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest/SeedCsharpReadonlyRequestClient.cs index 9aa1e1f4d272..fc3a1800901c 100644 --- a/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest/SeedCsharpReadonlyRequestClient.cs +++ b/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest/SeedCsharpReadonlyRequestClient.cs @@ -15,7 +15,7 @@ public SeedCsharpReadonlyRequestClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedCsharpReadonlyRequest" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedCsharpReadonlyRequest.Version.Current }, { "User-Agent", "Ferncsharp-readonly-request/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision/System.cs b/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision/System.cs index 900dda6bf75d..7e976d16d9bf 100644 --- a/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision/System.cs +++ b/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision/System.cs @@ -15,7 +15,7 @@ public System(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedCsharpSystemCollision" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedCsharpSystemCollision.Version.Current }, { "User-Agent", "Ferncsharp-system-collision/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-union-base-properties/dedupe-union-base-properties/src/SeedCsharpUnionBaseProperties/SeedCsharpUnionBasePropertiesClient.cs b/seed/csharp-sdk/csharp-union-base-properties/dedupe-union-base-properties/src/SeedCsharpUnionBaseProperties/SeedCsharpUnionBasePropertiesClient.cs index 96eedc8c918d..76e9f05d5a57 100644 --- a/seed/csharp-sdk/csharp-union-base-properties/dedupe-union-base-properties/src/SeedCsharpUnionBaseProperties/SeedCsharpUnionBasePropertiesClient.cs +++ b/seed/csharp-sdk/csharp-union-base-properties/dedupe-union-base-properties/src/SeedCsharpUnionBaseProperties/SeedCsharpUnionBasePropertiesClient.cs @@ -15,7 +15,7 @@ public SeedCsharpUnionBasePropertiesClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedCsharpUnionBaseProperties" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedCsharpUnionBaseProperties.Version.Current }, { "User-Agent", "Ferncsharp-union-base-properties/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities/SeedCsharpXmlEntitiesClient.cs b/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities/SeedCsharpXmlEntitiesClient.cs index 388bdee9a7c0..dcdfb1038d52 100644 --- a/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities/SeedCsharpXmlEntitiesClient.cs +++ b/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities/SeedCsharpXmlEntitiesClient.cs @@ -15,7 +15,7 @@ public SeedCsharpXmlEntitiesClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedCsharpXmlEntities" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedCsharpXmlEntities.Version.Current }, { "User-Agent", "Ferncsharp-xml-entities/0.0.1" }, } ); diff --git a/seed/csharp-sdk/discriminated-union-with-nested-oneof/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/discriminated-union-with-nested-oneof/src/SeedApi/SeedApiClient.cs index 09030c4bb9dd..09b18473c675 100644 --- a/seed/csharp-sdk/discriminated-union-with-nested-oneof/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/discriminated-union-with-nested-oneof/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Ferndiscriminated-union-with-nested-oneof/0.0.1" }, } ); diff --git a/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples/SeedDollarStringExamplesClient.cs b/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples/SeedDollarStringExamplesClient.cs index 845484b6f71b..c22ac47c9be2 100644 --- a/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples/SeedDollarStringExamplesClient.cs +++ b/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples/SeedDollarStringExamplesClient.cs @@ -14,7 +14,7 @@ public SeedDollarStringExamplesClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedDollarStringExamples" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedDollarStringExamples.Version.Current }, { "User-Agent", "Ferndollar-string-examples/0.0.1" }, } ); diff --git a/seed/csharp-sdk/empty-clients/src/SeedEmptyClients/SeedEmptyClientsClient.cs b/seed/csharp-sdk/empty-clients/src/SeedEmptyClients/SeedEmptyClientsClient.cs index 724eb462263e..28130557c117 100644 --- a/seed/csharp-sdk/empty-clients/src/SeedEmptyClients/SeedEmptyClientsClient.cs +++ b/seed/csharp-sdk/empty-clients/src/SeedEmptyClients/SeedEmptyClientsClient.cs @@ -14,7 +14,7 @@ public SeedEmptyClientsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedEmptyClients" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedEmptyClients.Version.Current }, { "User-Agent", "Fernempty-clients/0.0.1" }, } ); diff --git a/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth/SeedEndpointSecurityAuthClient.cs b/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth/SeedEndpointSecurityAuthClient.cs index 929398273b37..33bb6db9c5fe 100644 --- a/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth/SeedEndpointSecurityAuthClient.cs +++ b/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth/SeedEndpointSecurityAuthClient.cs @@ -28,7 +28,7 @@ public SeedEndpointSecurityAuthClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedEndpointSecurityAuth" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedEndpointSecurityAuth.Version.Current }, { "User-Agent", "Fernendpoint-security-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/SeedEnumClient.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/SeedEnumClient.cs index beef1826d6fa..03dd51d4050c 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/SeedEnumClient.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/SeedEnumClient.cs @@ -14,7 +14,7 @@ public SeedEnumClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedEnum" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedEnum.Version.Current }, { "User-Agent", "Fernenum/0.0.1" }, } ); diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/SeedEnumClient.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/SeedEnumClient.cs index beef1826d6fa..03dd51d4050c 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/SeedEnumClient.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/SeedEnumClient.cs @@ -14,7 +14,7 @@ public SeedEnumClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedEnum" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedEnum.Version.Current }, { "User-Agent", "Fernenum/0.0.1" }, } ); diff --git a/seed/csharp-sdk/error-property/src/SeedErrorProperty/SeedErrorPropertyClient.cs b/seed/csharp-sdk/error-property/src/SeedErrorProperty/SeedErrorPropertyClient.cs index 48d8dcb1e25e..eee90b902de5 100644 --- a/seed/csharp-sdk/error-property/src/SeedErrorProperty/SeedErrorPropertyClient.cs +++ b/seed/csharp-sdk/error-property/src/SeedErrorProperty/SeedErrorPropertyClient.cs @@ -14,7 +14,7 @@ public SeedErrorPropertyClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedErrorProperty" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedErrorProperty.Version.Current }, { "User-Agent", "Fernerror-property/0.0.1" }, } ); diff --git a/seed/csharp-sdk/errors/src/SeedErrors/SeedErrorsClient.cs b/seed/csharp-sdk/errors/src/SeedErrors/SeedErrorsClient.cs index e789689845b4..22ffeee91e76 100644 --- a/seed/csharp-sdk/errors/src/SeedErrors/SeedErrorsClient.cs +++ b/seed/csharp-sdk/errors/src/SeedErrors/SeedErrorsClient.cs @@ -14,7 +14,7 @@ public SeedErrorsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedErrors" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedErrors.Version.Current }, { "User-Agent", "Fernerrors/0.0.1" }, } ); diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/SeedExamplesClient.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/SeedExamplesClient.cs index ed7b69925e07..0b08a46cec48 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/SeedExamplesClient.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/SeedExamplesClient.cs @@ -18,7 +18,7 @@ public SeedExamplesClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExamples" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExamples.Version.Current }, { "User-Agent", "Fernexamples/0.0.1" }, } ); diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/SeedExamplesClient.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/SeedExamplesClient.cs index ed7b69925e07..0b08a46cec48 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/SeedExamplesClient.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/SeedExamplesClient.cs @@ -18,7 +18,7 @@ public SeedExamplesClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExamples" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExamples.Version.Current }, { "User-Agent", "Fernexamples/0.0.1" }, } ); diff --git a/seed/csharp-sdk/exhaustive/auto-generate-idempotency-key/src/SeedExhaustive/SeedExhaustiveClient.cs b/seed/csharp-sdk/exhaustive/auto-generate-idempotency-key/src/SeedExhaustive/SeedExhaustiveClient.cs index eb2f5fe9e2bb..fd61469127ee 100644 --- a/seed/csharp-sdk/exhaustive/auto-generate-idempotency-key/src/SeedExhaustive/SeedExhaustiveClient.cs +++ b/seed/csharp-sdk/exhaustive/auto-generate-idempotency-key/src/SeedExhaustive/SeedExhaustiveClient.cs @@ -15,7 +15,7 @@ public SeedExhaustiveClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExhaustive" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExhaustive.Version.Current }, { "User-Agent", "Fernexhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/SeedExhaustiveClient.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/SeedExhaustiveClient.cs index 31c8a595ef07..07aa0c14feed 100644 --- a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/SeedExhaustiveClient.cs +++ b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/SeedExhaustiveClient.cs @@ -19,7 +19,7 @@ public SeedExhaustiveClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExhaustive" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExhaustive.Version.Current }, { "User-Agent", "Fernexhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/SeedExhaustiveClient.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/SeedExhaustiveClient.cs index 1d526199b4b0..c3e40caafafe 100644 --- a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/SeedExhaustiveClient.cs +++ b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/SeedExhaustiveClient.cs @@ -20,7 +20,7 @@ public SeedExhaustiveClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExhaustive" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExhaustive.Version.Current }, { "User-Agent", "Fernexhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/SeedExhaustiveClient.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/SeedExhaustiveClient.cs index eb2f5fe9e2bb..fd61469127ee 100644 --- a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/SeedExhaustiveClient.cs +++ b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/SeedExhaustiveClient.cs @@ -15,7 +15,7 @@ public SeedExhaustiveClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExhaustive" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExhaustive.Version.Current }, { "User-Agent", "Fernexhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/SeedExhaustiveClient.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/SeedExhaustiveClient.cs index eb2f5fe9e2bb..fd61469127ee 100644 --- a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/SeedExhaustiveClient.cs +++ b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/SeedExhaustiveClient.cs @@ -15,7 +15,7 @@ public SeedExhaustiveClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExhaustive" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExhaustive.Version.Current }, { "User-Agent", "Fernexhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/exhaustive/oidc-token/src/SeedExhaustive/SeedExhaustiveClient.cs b/seed/csharp-sdk/exhaustive/oidc-token/src/SeedExhaustive/SeedExhaustiveClient.cs index 56209a2a0974..687b768c1321 100644 --- a/seed/csharp-sdk/exhaustive/oidc-token/src/SeedExhaustive/SeedExhaustiveClient.cs +++ b/seed/csharp-sdk/exhaustive/oidc-token/src/SeedExhaustive/SeedExhaustiveClient.cs @@ -15,7 +15,7 @@ public SeedExhaustiveClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExhaustive" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExhaustive.Version.Current }, { "User-Agent", "Fern.Exhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/SeedExhaustiveClient.cs b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/SeedExhaustiveClient.cs index eb2f5fe9e2bb..fd61469127ee 100644 --- a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/SeedExhaustiveClient.cs +++ b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/SeedExhaustiveClient.cs @@ -15,7 +15,7 @@ public SeedExhaustiveClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExhaustive" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExhaustive.Version.Current }, { "User-Agent", "Fernexhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/SeedExhaustiveClient.cs b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/SeedExhaustiveClient.cs index eb2f5fe9e2bb..fd61469127ee 100644 --- a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/SeedExhaustiveClient.cs +++ b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/SeedExhaustiveClient.cs @@ -15,7 +15,7 @@ public SeedExhaustiveClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExhaustive" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExhaustive.Version.Current }, { "User-Agent", "Fernexhaustive/0.0.1" }, } ); diff --git a/seed/csharp-sdk/extends/src/SeedExtends/SeedExtendsClient.cs b/seed/csharp-sdk/extends/src/SeedExtends/SeedExtendsClient.cs index 42a34fcdd316..a7d5b520dc7c 100644 --- a/seed/csharp-sdk/extends/src/SeedExtends/SeedExtendsClient.cs +++ b/seed/csharp-sdk/extends/src/SeedExtends/SeedExtendsClient.cs @@ -14,7 +14,7 @@ public SeedExtendsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExtends" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExtends.Version.Current }, { "User-Agent", "Fernextends/0.0.1" }, } ); diff --git a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/SeedExtraPropertiesClient.cs b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/SeedExtraPropertiesClient.cs index d44d9bdac383..c809fdcdaf12 100644 --- a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/SeedExtraPropertiesClient.cs +++ b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/SeedExtraPropertiesClient.cs @@ -14,7 +14,7 @@ public SeedExtraPropertiesClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedExtraProperties" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedExtraProperties.Version.Current }, { "User-Agent", "Fernextra-properties/0.0.1" }, } ); diff --git a/seed/csharp-sdk/file-download/src/SeedFileDownload/SeedFileDownloadClient.cs b/seed/csharp-sdk/file-download/src/SeedFileDownload/SeedFileDownloadClient.cs index 4cf1f8a04475..9392c9cb10f5 100644 --- a/seed/csharp-sdk/file-download/src/SeedFileDownload/SeedFileDownloadClient.cs +++ b/seed/csharp-sdk/file-download/src/SeedFileDownload/SeedFileDownloadClient.cs @@ -14,7 +14,7 @@ public SeedFileDownloadClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedFileDownload" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedFileDownload.Version.Current }, { "User-Agent", "Fernfile-download/0.0.1" }, } ); diff --git a/seed/csharp-sdk/file-upload-openapi/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/file-upload-openapi/src/SeedApi/SeedApiClient.cs index fca660d6780d..53af79634797 100644 --- a/seed/csharp-sdk/file-upload-openapi/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/file-upload-openapi/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernfile-upload-openapi/0.0.1" }, } ); diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload/SeedFileUploadClient.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload/SeedFileUploadClient.cs index 0714b028b035..92efc5b50723 100644 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload/SeedFileUploadClient.cs +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload/SeedFileUploadClient.cs @@ -14,7 +14,7 @@ public SeedFileUploadClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedFileUpload" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedFileUpload.Version.Current }, { "User-Agent", "Fernfile-upload/0.0.1" }, } ); diff --git a/seed/csharp-sdk/folders/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/folders/src/SeedApi/SeedApiClient.cs index 8a2031f0a904..d8ceab47fd1e 100644 --- a/seed/csharp-sdk/folders/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/folders/src/SeedApi/SeedApiClient.cs @@ -16,7 +16,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernfolders/0.0.1" }, } ); diff --git a/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/SeedHeaderTokenEnvironmentVariableClient.cs b/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/SeedHeaderTokenEnvironmentVariableClient.cs index ea50c7a223eb..c25656c0119c 100644 --- a/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/SeedHeaderTokenEnvironmentVariableClient.cs +++ b/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/SeedHeaderTokenEnvironmentVariableClient.cs @@ -22,7 +22,10 @@ public SeedHeaderTokenEnvironmentVariableClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedHeaderTokenEnvironmentVariable" }, - { "X-Fern-SDK-Version", Version.Current }, + { + "X-Fern-SDK-Version", + global::SeedHeaderTokenEnvironmentVariable.Version.Current + }, { "User-Agent", "Fernheader-auth-environment-variable/0.0.1" }, } ); diff --git a/seed/csharp-sdk/header-auth/src/SeedHeaderToken/SeedHeaderTokenClient.cs b/seed/csharp-sdk/header-auth/src/SeedHeaderToken/SeedHeaderTokenClient.cs index b180ddb3215a..7d6dfc38e893 100644 --- a/seed/csharp-sdk/header-auth/src/SeedHeaderToken/SeedHeaderTokenClient.cs +++ b/seed/csharp-sdk/header-auth/src/SeedHeaderToken/SeedHeaderTokenClient.cs @@ -17,7 +17,7 @@ public SeedHeaderTokenClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedHeaderToken" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedHeaderToken.Version.Current }, { "User-Agent", "Fernheader-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/http-head/src/SeedHttpHead/SeedHttpHeadClient.cs b/seed/csharp-sdk/http-head/src/SeedHttpHead/SeedHttpHeadClient.cs index 6f94ee113c76..319641e37c29 100644 --- a/seed/csharp-sdk/http-head/src/SeedHttpHead/SeedHttpHeadClient.cs +++ b/seed/csharp-sdk/http-head/src/SeedHttpHead/SeedHttpHeadClient.cs @@ -14,7 +14,7 @@ public SeedHttpHeadClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedHttpHead" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedHttpHead.Version.Current }, { "User-Agent", "Fernhttp-head/0.0.1" }, } ); diff --git a/seed/csharp-sdk/idempotency-headers/auto-generate-idempotency-key/src/SeedIdempotencyHeaders/SeedIdempotencyHeadersClient.cs b/seed/csharp-sdk/idempotency-headers/auto-generate-idempotency-key/src/SeedIdempotencyHeaders/SeedIdempotencyHeadersClient.cs index 7b46c0e10376..4188c47a951c 100644 --- a/seed/csharp-sdk/idempotency-headers/auto-generate-idempotency-key/src/SeedIdempotencyHeaders/SeedIdempotencyHeadersClient.cs +++ b/seed/csharp-sdk/idempotency-headers/auto-generate-idempotency-key/src/SeedIdempotencyHeaders/SeedIdempotencyHeadersClient.cs @@ -14,7 +14,7 @@ public SeedIdempotencyHeadersClient(string? token = null, ClientOptions? clientO { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedIdempotencyHeaders" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedIdempotencyHeaders.Version.Current }, { "User-Agent", "Fernidempotency-headers/0.0.1" }, } ); diff --git a/seed/csharp-sdk/idempotency-headers/no-custom-config/src/SeedIdempotencyHeaders/SeedIdempotencyHeadersClient.cs b/seed/csharp-sdk/idempotency-headers/no-custom-config/src/SeedIdempotencyHeaders/SeedIdempotencyHeadersClient.cs index 7b46c0e10376..4188c47a951c 100644 --- a/seed/csharp-sdk/idempotency-headers/no-custom-config/src/SeedIdempotencyHeaders/SeedIdempotencyHeadersClient.cs +++ b/seed/csharp-sdk/idempotency-headers/no-custom-config/src/SeedIdempotencyHeaders/SeedIdempotencyHeadersClient.cs @@ -14,7 +14,7 @@ public SeedIdempotencyHeadersClient(string? token = null, ClientOptions? clientO { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedIdempotencyHeaders" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedIdempotencyHeaders.Version.Current }, { "User-Agent", "Fernidempotency-headers/0.0.1" }, } ); diff --git a/seed/csharp-sdk/imdb/allow-user-agent-app-info/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/imdb/allow-user-agent-app-info/src/SeedApi/SeedApiClient.cs index c2488bc5447b..c9b6c2d8a3e1 100644 --- a/seed/csharp-sdk/imdb/allow-user-agent-app-info/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/imdb/allow-user-agent-app-info/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", AppendAppInfoToUserAgent(BuildUserAgent(), clientOptions.AppInfo) }, } ); @@ -66,7 +66,7 @@ private static string BuildUserAgent() : ""; var runtimeVersion = global::System.Environment.Version.ToString(); var runtime = runtimeVersion.Length > 0 ? $" dotnet/{runtimeVersion}" : " dotnet"; - return $"Fernimdb/{Version.Current}{platform}{runtime}"; + return $"Fernimdb/{(global::SeedApi.Version.Current)}{platform}{runtime}"; } private static string AppendAppInfoToUserAgent(string userAgent, AppInfo? appInfo) diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/SeedApiClient.cs index 0f7549ea5914..a8d49349c756 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernimdb/0.0.1" }, } ); diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/BaseClient.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/BaseClient.cs index 6fbc80e73e51..dba0edef194b 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/BaseClient.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/BaseClient.cs @@ -14,7 +14,7 @@ public BaseClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernimdb/0.0.1" }, } ); diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/SeedApiClient.cs index 0f7549ea5914..a8d49349c756 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernimdb/0.0.1" }, } ); diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/SeedApiClient.cs index 0f7549ea5914..a8d49349c756 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernimdb/0.0.1" }, } ); diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/SeedApiClient.cs index 4a0953f8b2f9..dfd95e6314cd 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/SeedApiClient.cs @@ -19,7 +19,7 @@ public SeedApiClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernimdb/0.0.1" }, } ); diff --git a/seed/csharp-sdk/imdb/include-platform-headers/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/imdb/include-platform-headers/src/SeedApi/SeedApiClient.cs index 60b42f2d7691..c58ee2ff9aa5 100644 --- a/seed/csharp-sdk/imdb/include-platform-headers/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/imdb/include-platform-headers/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", BuildUserAgent() }, } ); @@ -66,6 +66,6 @@ private static string BuildUserAgent() : ""; var runtimeVersion = global::System.Environment.Version.ToString(); var runtime = runtimeVersion.Length > 0 ? $" dotnet/{runtimeVersion}" : " dotnet"; - return $"Fernimdb/{Version.Current}{platform}{runtime}"; + return $"Fernimdb/{(global::SeedApi.Version.Current)}{platform}{runtime}"; } } diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/SeedApiClient.cs index 0f7549ea5914..a8d49349c756 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernimdb/0.0.1" }, } ); diff --git a/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit/SeedInferredAuthExplicitClient.cs b/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit/SeedInferredAuthExplicitClient.cs index f205ea7e9f8a..9621a1d12dd0 100644 --- a/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit/SeedInferredAuthExplicitClient.cs +++ b/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit/SeedInferredAuthExplicitClient.cs @@ -22,7 +22,7 @@ public SeedInferredAuthExplicitClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedInferredAuthExplicit" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedInferredAuthExplicit.Version.Current }, { "User-Agent", "Ferninferred-auth-explicit/0.0.1" }, } ); diff --git a/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/SeedInferredAuthImplicitApiKeyClient.cs b/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/SeedInferredAuthImplicitApiKeyClient.cs index 2713c5dff22f..698c3ba0bddf 100644 --- a/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/SeedInferredAuthImplicitApiKeyClient.cs +++ b/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/SeedInferredAuthImplicitApiKeyClient.cs @@ -16,7 +16,7 @@ public SeedInferredAuthImplicitApiKeyClient(string apiKey, ClientOptions? client { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedInferredAuthImplicitApiKey" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedInferredAuthImplicitApiKey.Version.Current }, { "User-Agent", "Ferninferred-auth-implicit-api-key/0.0.1" }, } ); diff --git a/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/SeedInferredAuthImplicitNoExpiryClient.cs b/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/SeedInferredAuthImplicitNoExpiryClient.cs index ff031a6d2b3a..6d96ce30bcb5 100644 --- a/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/SeedInferredAuthImplicitNoExpiryClient.cs +++ b/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/SeedInferredAuthImplicitNoExpiryClient.cs @@ -23,7 +23,7 @@ public SeedInferredAuthImplicitNoExpiryClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedInferredAuthImplicitNoExpiry" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedInferredAuthImplicitNoExpiry.Version.Current }, { "User-Agent", "Ferninferred-auth-implicit-no-expiry/0.0.1" }, } ); diff --git a/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/SeedInferredAuthImplicitClient.cs b/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/SeedInferredAuthImplicitClient.cs index 7736ba2dfcdd..6aee74a81086 100644 --- a/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/SeedInferredAuthImplicitClient.cs +++ b/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/SeedInferredAuthImplicitClient.cs @@ -21,7 +21,7 @@ public SeedInferredAuthImplicitClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedInferredAuthImplicit" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedInferredAuthImplicit.Version.Current }, { "User-Agent", "Ferninferred-auth-implicit-reference/0.0.1" }, } ); diff --git a/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit/SeedInferredAuthImplicitClient.cs b/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit/SeedInferredAuthImplicitClient.cs index b3229b5906bf..171a1a510295 100644 --- a/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit/SeedInferredAuthImplicitClient.cs +++ b/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit/SeedInferredAuthImplicitClient.cs @@ -22,7 +22,7 @@ public SeedInferredAuthImplicitClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedInferredAuthImplicit" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedInferredAuthImplicit.Version.Current }, { "User-Agent", "Ferninferred-auth-implicit/0.0.1" }, } ); diff --git a/seed/csharp-sdk/inline-enum-type-name-override/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/inline-enum-type-name-override/src/SeedApi/SeedApiClient.cs index cc133f8bd781..0d610528004f 100644 --- a/seed/csharp-sdk/inline-enum-type-name-override/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/inline-enum-type-name-override/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Ferninline-enum-type-name-override/0.0.1" }, } ); diff --git a/seed/csharp-sdk/license/custom-license/src/SeedLicense/SeedLicenseClient.cs b/seed/csharp-sdk/license/custom-license/src/SeedLicense/SeedLicenseClient.cs index cb34865c6956..a13debb583fb 100644 --- a/seed/csharp-sdk/license/custom-license/src/SeedLicense/SeedLicenseClient.cs +++ b/seed/csharp-sdk/license/custom-license/src/SeedLicense/SeedLicenseClient.cs @@ -14,7 +14,7 @@ public SeedLicenseClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedLicense" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedLicense.Version.Current }, { "User-Agent", "Fernlicense/0.0.1" }, } ); diff --git a/seed/csharp-sdk/license/mit-license/src/SeedLicense/SeedLicenseClient.cs b/seed/csharp-sdk/license/mit-license/src/SeedLicense/SeedLicenseClient.cs index cb34865c6956..a13debb583fb 100644 --- a/seed/csharp-sdk/license/mit-license/src/SeedLicense/SeedLicenseClient.cs +++ b/seed/csharp-sdk/license/mit-license/src/SeedLicense/SeedLicenseClient.cs @@ -14,7 +14,7 @@ public SeedLicenseClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedLicense" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedLicense.Version.Current }, { "User-Agent", "Fernlicense/0.0.1" }, } ); diff --git a/seed/csharp-sdk/literal-user-agent/src/SeedLiteralUserAgent/SeedLiteralUserAgentClient.cs b/seed/csharp-sdk/literal-user-agent/src/SeedLiteralUserAgent/SeedLiteralUserAgentClient.cs index 2fddd40c9a90..8bae46b9ea3b 100644 --- a/seed/csharp-sdk/literal-user-agent/src/SeedLiteralUserAgent/SeedLiteralUserAgentClient.cs +++ b/seed/csharp-sdk/literal-user-agent/src/SeedLiteralUserAgent/SeedLiteralUserAgentClient.cs @@ -15,7 +15,7 @@ public SeedLiteralUserAgentClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedLiteralUserAgent" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedLiteralUserAgent.Version.Current }, } ); if (clientOptions.UserAgent != null) diff --git a/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral/SeedLiteralClient.cs b/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral/SeedLiteralClient.cs index 9da4899501a3..6ada5858ab92 100644 --- a/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral/SeedLiteralClient.cs +++ b/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral/SeedLiteralClient.cs @@ -14,7 +14,7 @@ public SeedLiteralClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedLiteral" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedLiteral.Version.Current }, { "User-Agent", "Fernliteral/0.0.1" }, } ); diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral/SeedLiteralClient.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral/SeedLiteralClient.cs index 9da4899501a3..6ada5858ab92 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral/SeedLiteralClient.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral/SeedLiteralClient.cs @@ -14,7 +14,7 @@ public SeedLiteralClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedLiteral" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedLiteral.Version.Current }, { "User-Agent", "Fernliteral/0.0.1" }, } ); diff --git a/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions/SeedLiteralsUnionsClient.cs b/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions/SeedLiteralsUnionsClient.cs index 0e41d1b40994..9e7bdc6c83fe 100644 --- a/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions/SeedLiteralsUnionsClient.cs +++ b/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions/SeedLiteralsUnionsClient.cs @@ -14,7 +14,7 @@ public SeedLiteralsUnionsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedLiteralsUnions" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedLiteralsUnions.Version.Current }, { "User-Agent", "Fernliterals-unions/0.0.1" }, } ); diff --git a/seed/csharp-sdk/mixed-case/src/SeedMixedCase/SeedMixedCaseClient.cs b/seed/csharp-sdk/mixed-case/src/SeedMixedCase/SeedMixedCaseClient.cs index a52ef3eae93f..c292d7314d4a 100644 --- a/seed/csharp-sdk/mixed-case/src/SeedMixedCase/SeedMixedCaseClient.cs +++ b/seed/csharp-sdk/mixed-case/src/SeedMixedCase/SeedMixedCaseClient.cs @@ -14,7 +14,7 @@ public SeedMixedCaseClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedMixedCase" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedMixedCase.Version.Current }, { "User-Agent", "Fernmixed-case/0.0.1" }, } ); diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectoryClient.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectoryClient.cs index 8a88bb5df85f..f1dd22243762 100644 --- a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectoryClient.cs +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/SeedMixedFileDirectoryClient.cs @@ -14,7 +14,7 @@ public SeedMixedFileDirectoryClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedMixedFileDirectory" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedMixedFileDirectory.Version.Current }, { "User-Agent", "Fernmixed-file-directory/0.0.1" }, } ); diff --git a/seed/csharp-sdk/multi-content-type-examples/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/multi-content-type-examples/src/SeedApi/SeedApiClient.cs index 6f1723531c95..5a78a7702b97 100644 --- a/seed/csharp-sdk/multi-content-type-examples/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/multi-content-type-examples/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernmulti-content-type-examples/0.0.1" }, } ); diff --git a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/SeedMultiLineDocsClient.cs b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/SeedMultiLineDocsClient.cs index f118d80f71b0..98b7b0104a6c 100644 --- a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/SeedMultiLineDocsClient.cs +++ b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/SeedMultiLineDocsClient.cs @@ -14,7 +14,7 @@ public SeedMultiLineDocsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedMultiLineDocs" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedMultiLineDocs.Version.Current }, { "User-Agent", "Fernmulti-line-docs/0.0.1" }, } ); diff --git a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/SeedMultiUrlEnvironmentNoDefaultClient.cs b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/SeedMultiUrlEnvironmentNoDefaultClient.cs index 9ede34ebfe44..a558205c8134 100644 --- a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/SeedMultiUrlEnvironmentNoDefaultClient.cs +++ b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/SeedMultiUrlEnvironmentNoDefaultClient.cs @@ -18,7 +18,7 @@ public SeedMultiUrlEnvironmentNoDefaultClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedMultiUrlEnvironmentNoDefault" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedMultiUrlEnvironmentNoDefault.Version.Current }, { "User-Agent", "Fernmulti-url-environment-no-default/0.0.1" }, } ); diff --git a/seed/csharp-sdk/multi-url-environment-reference/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/multi-url-environment-reference/src/SeedApi/SeedApiClient.cs index 689d7ba36f41..d854be6456e5 100644 --- a/seed/csharp-sdk/multi-url-environment-reference/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/multi-url-environment-reference/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernmulti-url-environment-reference/0.0.1" }, } ); diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs index 2cbeb041c5a8..bf31e288882c 100644 --- a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs @@ -14,7 +14,7 @@ public SeedMultiUrlEnvironmentClient(string? token = null, ClientOptions? client { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedMultiUrlEnvironment" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedMultiUrlEnvironment.Version.Current }, { "User-Agent", "Fernmulti-url-environment/0.0.1" }, } ); diff --git a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs index 2cbeb041c5a8..bf31e288882c 100644 --- a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs +++ b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs @@ -14,7 +14,7 @@ public SeedMultiUrlEnvironmentClient(string? token = null, ClientOptions? client { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedMultiUrlEnvironment" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedMultiUrlEnvironment.Version.Current }, { "User-Agent", "Fernmulti-url-environment/0.0.1" }, } ); diff --git a/seed/csharp-sdk/multiple-request-bodies/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/multiple-request-bodies/src/SeedApi/SeedApiClient.cs index 0318c40a9174..a362c47fa97d 100644 --- a/seed/csharp-sdk/multiple-request-bodies/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/multiple-request-bodies/src/SeedApi/SeedApiClient.cs @@ -16,7 +16,7 @@ public SeedApiClient(string? token = null, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernmultiple-request-bodies/0.0.1" }, } ); diff --git a/seed/csharp-sdk/no-content-response/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/no-content-response/src/SeedApi/SeedApiClient.cs index 726fe9dfa036..591c08f0565c 100644 --- a/seed/csharp-sdk/no-content-response/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/no-content-response/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernno-content-response/0.0.1" }, } ); diff --git a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/SeedNoEnvironmentClient.cs b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/SeedNoEnvironmentClient.cs index dba74ea07ea1..394153e488ad 100644 --- a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/SeedNoEnvironmentClient.cs +++ b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/SeedNoEnvironmentClient.cs @@ -14,7 +14,7 @@ public SeedNoEnvironmentClient(string? token = null, ClientOptions? clientOption { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedNoEnvironment" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedNoEnvironment.Version.Current }, { "User-Agent", "Fernno-environment/0.0.1" }, } ); diff --git a/seed/csharp-sdk/no-retries/src/SeedNoRetries/SeedNoRetriesClient.cs b/seed/csharp-sdk/no-retries/src/SeedNoRetries/SeedNoRetriesClient.cs index cf65cd623030..87a4e8ddd806 100644 --- a/seed/csharp-sdk/no-retries/src/SeedNoRetries/SeedNoRetriesClient.cs +++ b/seed/csharp-sdk/no-retries/src/SeedNoRetries/SeedNoRetriesClient.cs @@ -14,7 +14,7 @@ public SeedNoRetriesClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedNoRetries" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedNoRetries.Version.Current }, { "User-Agent", "Fernno-retries/0.0.1" }, } ); diff --git a/seed/csharp-sdk/null-type/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/null-type/src/SeedApi/SeedApiClient.cs index e9128795abcf..49432cde7453 100644 --- a/seed/csharp-sdk/null-type/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/null-type/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernnull-type/0.0.1" }, } ); diff --git a/seed/csharp-sdk/nullable-allof-extends/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/nullable-allof-extends/src/SeedApi/SeedApiClient.cs index 1747c0213c03..9b10b3e82272 100644 --- a/seed/csharp-sdk/nullable-allof-extends/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/nullable-allof-extends/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernnullable-allof-extends/0.0.1" }, } ); diff --git a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/SeedNullableOptionalClient.cs b/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/SeedNullableOptionalClient.cs index bfade2a61b3d..02ab5e9cc609 100644 --- a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/SeedNullableOptionalClient.cs +++ b/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/SeedNullableOptionalClient.cs @@ -14,7 +14,7 @@ public SeedNullableOptionalClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedNullableOptional" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedNullableOptional.Version.Current }, { "User-Agent", "Fernnullable-optional/0.0.1" }, } ); diff --git a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/SeedNullableOptionalClient.cs b/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/SeedNullableOptionalClient.cs index bfade2a61b3d..02ab5e9cc609 100644 --- a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/SeedNullableOptionalClient.cs +++ b/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/SeedNullableOptionalClient.cs @@ -14,7 +14,7 @@ public SeedNullableOptionalClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedNullableOptional" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedNullableOptional.Version.Current }, { "User-Agent", "Fernnullable-optional/0.0.1" }, } ); diff --git a/seed/csharp-sdk/nullable-request-body/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/nullable-request-body/src/SeedApi/SeedApiClient.cs index 66ab09f77a4c..181addcb16ce 100644 --- a/seed/csharp-sdk/nullable-request-body/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/nullable-request-body/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernnullable-request-body/0.0.1" }, } ); diff --git a/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable/SeedNullableClient.cs b/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable/SeedNullableClient.cs index 00a7bbcb76b4..4aef38bc1209 100644 --- a/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable/SeedNullableClient.cs +++ b/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable/SeedNullableClient.cs @@ -14,7 +14,7 @@ public SeedNullableClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedNullable" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedNullable.Version.Current }, { "User-Agent", "Fernnullable/0.0.1" }, } ); diff --git a/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable/SeedNullableClient.cs b/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable/SeedNullableClient.cs index 00a7bbcb76b4..4aef38bc1209 100644 --- a/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable/SeedNullableClient.cs +++ b/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable/SeedNullableClient.cs @@ -14,7 +14,7 @@ public SeedNullableClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedNullable" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedNullable.Version.Current }, { "User-Agent", "Fernnullable/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs index 51ae90f305c7..ac703eb565d8 100644 --- a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs @@ -22,7 +22,7 @@ public SeedOauthClientCredentialsClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthClientCredentials" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedOauthClientCredentials.Version.Current }, { "User-Agent", "Fernoauth-client-credentials-custom/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/SeedOauthClientCredentialsDefaultClient.cs b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/SeedOauthClientCredentialsDefaultClient.cs index c07be75f3354..a0718daaa4ad 100644 --- a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/SeedOauthClientCredentialsDefaultClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/SeedOauthClientCredentialsDefaultClient.cs @@ -21,7 +21,7 @@ public SeedOauthClientCredentialsDefaultClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthClientCredentialsDefault" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedOauthClientCredentialsDefault.Version.Current }, { "User-Agent", "Fernoauth-client-credentials-default/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/SeedOauthClientCredentialsEnvironmentVariablesClient.cs b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/SeedOauthClientCredentialsEnvironmentVariablesClient.cs index 744a49150b33..1a7ffaf4795d 100644 --- a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/SeedOauthClientCredentialsEnvironmentVariablesClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/SeedOauthClientCredentialsEnvironmentVariablesClient.cs @@ -29,7 +29,10 @@ public SeedOauthClientCredentialsEnvironmentVariablesClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthClientCredentialsEnvironmentVariables" }, - { "X-Fern-SDK-Version", Version.Current }, + { + "X-Fern-SDK-Version", + global::SeedOauthClientCredentialsEnvironmentVariables.Version.Current + }, { "User-Agent", "Fernoauth-client-credentials-environment-variables/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuthClient.cs b/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuthClient.cs index 1a0030756462..bb0fae221fdb 100644 --- a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuthClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuthClient.cs @@ -20,7 +20,10 @@ public SeedOauthClientCredentialsMandatoryAuthClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthClientCredentialsMandatoryAuth" }, - { "X-Fern-SDK-Version", Version.Current }, + { + "X-Fern-SDK-Version", + global::SeedOauthClientCredentialsMandatoryAuth.Version.Current + }, { "User-Agent", "Fernoauth-client-credentials-mandatory-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/unified-client-options/src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuthClient.cs b/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/unified-client-options/src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuthClient.cs index 9a031c50305a..9423a24ff6c6 100644 --- a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/unified-client-options/src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuthClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/unified-client-options/src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuthClient.cs @@ -15,7 +15,10 @@ public SeedOauthClientCredentialsMandatoryAuthClient(ClientOptions clientOptions { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthClientCredentialsMandatoryAuth" }, - { "X-Fern-SDK-Version", Version.Current }, + { + "X-Fern-SDK-Version", + global::SeedOauthClientCredentialsMandatoryAuth.Version.Current + }, { "User-Agent", "Fernoauth-client-credentials-mandatory-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs index 9f94479ac25b..37e37ac36c26 100644 --- a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs @@ -21,7 +21,7 @@ public SeedOauthClientCredentialsClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthClientCredentials" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedOauthClientCredentials.Version.Current }, { "User-Agent", "Fernoauth-client-credentials-nested-root/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials-openapi/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/oauth-client-credentials-openapi/src/SeedApi/SeedApiClient.cs index 6b89795c144c..b8cf1c5d2786 100644 --- a/seed/csharp-sdk/oauth-client-credentials-openapi/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials-openapi/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(string clientId, string clientSecret, ClientOptions? client { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernoauth-client-credentials-openapi/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/SeedOauthClientCredentialsReferenceClient.cs b/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/SeedOauthClientCredentialsReferenceClient.cs index 61f6b1cc7a00..2715a8614c2e 100644 --- a/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/SeedOauthClientCredentialsReferenceClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/SeedOauthClientCredentialsReferenceClient.cs @@ -19,7 +19,10 @@ public SeedOauthClientCredentialsReferenceClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthClientCredentialsReference" }, - { "X-Fern-SDK-Version", Version.Current }, + { + "X-Fern-SDK-Version", + global::SeedOauthClientCredentialsReference.Version.Current + }, { "User-Agent", "Fernoauth-client-credentials-reference/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/SeedOauthClientCredentialsWithVariablesClient.cs b/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/SeedOauthClientCredentialsWithVariablesClient.cs index 201e3a1ece3d..3d6b4e9b4251 100644 --- a/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/SeedOauthClientCredentialsWithVariablesClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/SeedOauthClientCredentialsWithVariablesClient.cs @@ -21,7 +21,10 @@ public SeedOauthClientCredentialsWithVariablesClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthClientCredentialsWithVariables" }, - { "X-Fern-SDK-Version", Version.Current }, + { + "X-Fern-SDK-Version", + global::SeedOauthClientCredentialsWithVariables.Version.Current + }, { "User-Agent", "Fernoauth-client-credentials-with-variables/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs index e2cdfa2135c0..de60fc7400e8 100644 --- a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs @@ -25,7 +25,7 @@ public SeedOauthClientCredentialsClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthClientCredentials" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedOauthClientCredentials.Version.Current }, { "User-Agent", "Fernoauth-client-credentials/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs index b71fdf879983..ab8b87017a4a 100644 --- a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs @@ -20,7 +20,7 @@ public SeedOauthClientCredentialsClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthClientCredentials" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedOauthClientCredentials.Version.Current }, { "User-Agent", "Fernoauth-client-credentials/0.0.1" }, } ); diff --git a/seed/csharp-sdk/oauth-pkce/src/SeedOauthPkce/SeedOauthPkceClient.cs b/seed/csharp-sdk/oauth-pkce/src/SeedOauthPkce/SeedOauthPkceClient.cs index 33e9b2eba76a..0779ef7a6fbe 100644 --- a/seed/csharp-sdk/oauth-pkce/src/SeedOauthPkce/SeedOauthPkceClient.cs +++ b/seed/csharp-sdk/oauth-pkce/src/SeedOauthPkce/SeedOauthPkceClient.cs @@ -14,7 +14,7 @@ public SeedOauthPkceClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedOauthPkce" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedOauthPkce.Version.Current }, { "User-Agent", "Fernoauth-pkce/0.0.1" }, } ); diff --git a/seed/csharp-sdk/object/src/SeedObject/SeedObjectClient.cs b/seed/csharp-sdk/object/src/SeedObject/SeedObjectClient.cs index 857940dba3b6..a0de6c9b21a3 100644 --- a/seed/csharp-sdk/object/src/SeedObject/SeedObjectClient.cs +++ b/seed/csharp-sdk/object/src/SeedObject/SeedObjectClient.cs @@ -14,7 +14,7 @@ public SeedObjectClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedObject" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedObject.Version.Current }, { "User-Agent", "Fernobject/0.0.1" }, } ); diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs index 8be0c15b7911..f636c5d04331 100644 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs @@ -14,7 +14,7 @@ public SeedObjectsWithImportsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedObjectsWithImports" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedObjectsWithImports.Version.Current }, { "User-Agent", "Fernobjects-with-imports/0.0.1" }, } ); diff --git a/seed/csharp-sdk/openapi-path-param-body-collision/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/openapi-path-param-body-collision/src/SeedApi/SeedApiClient.cs index 343736976fc0..6d829c0a42a8 100644 --- a/seed/csharp-sdk/openapi-path-param-body-collision/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/openapi-path-param-body-collision/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernopenapi-path-param-body-collision/0.0.1" }, } ); diff --git a/seed/csharp-sdk/openapi-request-body-ref/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/openapi-request-body-ref/src/SeedApi/SeedApiClient.cs index 2404535195f4..1a9bf0637941 100644 --- a/seed/csharp-sdk/openapi-request-body-ref/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/openapi-request-body-ref/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernopenapi-request-body-ref/0.0.1" }, } ); diff --git a/seed/csharp-sdk/openapi-subtitle/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/openapi-subtitle/src/SeedApi/SeedApiClient.cs index cf67ee52461c..42c4af807c39 100644 --- a/seed/csharp-sdk/openapi-subtitle/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/openapi-subtitle/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernopenapi-subtitle/0.0.1" }, } ); diff --git a/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs b/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs index 8e143978c072..440cd657e826 100644 --- a/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs +++ b/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs @@ -14,7 +14,7 @@ public SeedObjectsWithImportsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedObjectsWithImports" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedObjectsWithImports.Version.Current }, { "User-Agent", "Fernoptional/0.0.1" }, } ); diff --git a/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs b/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs index 8e143978c072..440cd657e826 100644 --- a/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs +++ b/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports/SeedObjectsWithImportsClient.cs @@ -14,7 +14,7 @@ public SeedObjectsWithImportsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedObjectsWithImports" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedObjectsWithImports.Version.Current }, { "User-Agent", "Fernoptional/0.0.1" }, } ); diff --git a/seed/csharp-sdk/package-yml/src/SeedPackageYml/SeedPackageYmlClient.cs b/seed/csharp-sdk/package-yml/src/SeedPackageYml/SeedPackageYmlClient.cs index 1bdb4eaec063..c8a3b6487afd 100644 --- a/seed/csharp-sdk/package-yml/src/SeedPackageYml/SeedPackageYmlClient.cs +++ b/seed/csharp-sdk/package-yml/src/SeedPackageYml/SeedPackageYmlClient.cs @@ -15,7 +15,7 @@ public SeedPackageYmlClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPackageYml" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPackageYml.Version.Current }, { "User-Agent", "Fernpackage-yml/0.0.1" }, } ); diff --git a/seed/csharp-sdk/pagination-custom/src/SeedPagination/SeedPaginationClient.cs b/seed/csharp-sdk/pagination-custom/src/SeedPagination/SeedPaginationClient.cs index db3ce6d728c8..2a3042603cbb 100644 --- a/seed/csharp-sdk/pagination-custom/src/SeedPagination/SeedPaginationClient.cs +++ b/seed/csharp-sdk/pagination-custom/src/SeedPagination/SeedPaginationClient.cs @@ -14,7 +14,7 @@ public SeedPaginationClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPagination" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPagination.Version.Current }, { "User-Agent", "Fernpagination-custom/0.0.1" }, } ); diff --git a/seed/csharp-sdk/pagination-uri-path/src/SeedPaginationUriPath/SeedPaginationUriPathClient.cs b/seed/csharp-sdk/pagination-uri-path/src/SeedPaginationUriPath/SeedPaginationUriPathClient.cs index 3482bbb767cc..9260089998e7 100644 --- a/seed/csharp-sdk/pagination-uri-path/src/SeedPaginationUriPath/SeedPaginationUriPathClient.cs +++ b/seed/csharp-sdk/pagination-uri-path/src/SeedPaginationUriPath/SeedPaginationUriPathClient.cs @@ -14,7 +14,7 @@ public SeedPaginationUriPathClient(string token, ClientOptions? clientOptions = { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPaginationUriPath" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPaginationUriPath.Version.Current }, { "User-Agent", "Fernpagination-uri-path/0.0.1" }, } ); diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/SeedPaginationClient.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/SeedPaginationClient.cs index 5c1cb57442d7..85e2b5b7557d 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/SeedPaginationClient.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/SeedPaginationClient.cs @@ -20,7 +20,7 @@ public SeedPaginationClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPagination" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPagination.Version.Current }, { "User-Agent", "Fernpagination/0.0.1" }, } ); diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/SeedPaginationClient.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/SeedPaginationClient.cs index 2c413425909c..e754b136716f 100644 --- a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/SeedPaginationClient.cs +++ b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/SeedPaginationClient.cs @@ -15,7 +15,7 @@ public SeedPaginationClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPagination" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPagination.Version.Current }, { "User-Agent", "Fernpagination/0.0.1" }, } ); diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/SeedPaginationClient.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/SeedPaginationClient.cs index 2c413425909c..e754b136716f 100644 --- a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/SeedPaginationClient.cs +++ b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/SeedPaginationClient.cs @@ -15,7 +15,7 @@ public SeedPaginationClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPagination" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPagination.Version.Current }, { "User-Agent", "Fernpagination/0.0.1" }, } ); diff --git a/seed/csharp-sdk/pagination/page-index-semantics/src/SeedPagination/SeedPaginationClient.cs b/seed/csharp-sdk/pagination/page-index-semantics/src/SeedPagination/SeedPaginationClient.cs index 2c413425909c..e754b136716f 100644 --- a/seed/csharp-sdk/pagination/page-index-semantics/src/SeedPagination/SeedPaginationClient.cs +++ b/seed/csharp-sdk/pagination/page-index-semantics/src/SeedPagination/SeedPaginationClient.cs @@ -15,7 +15,7 @@ public SeedPaginationClient(string token, ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPagination" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPagination.Version.Current }, { "User-Agent", "Fernpagination/0.0.1" }, } ); diff --git a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/SeedPathParametersClient.cs b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/SeedPathParametersClient.cs index 98777e533243..0cfd6a203a60 100644 --- a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/SeedPathParametersClient.cs +++ b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/SeedPathParametersClient.cs @@ -14,7 +14,7 @@ public SeedPathParametersClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPathParameters" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPathParameters.Version.Current }, { "User-Agent", "Fernpath-parameters/0.0.1" }, } ); diff --git a/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters/SeedPathParametersClient.cs b/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters/SeedPathParametersClient.cs index 98777e533243..0cfd6a203a60 100644 --- a/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters/SeedPathParametersClient.cs +++ b/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters/SeedPathParametersClient.cs @@ -14,7 +14,7 @@ public SeedPathParametersClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPathParameters" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPathParameters.Version.Current }, { "User-Agent", "Fernpath-parameters/0.0.1" }, } ); diff --git a/seed/csharp-sdk/plain-text/src/SeedPlainText/SeedPlainTextClient.cs b/seed/csharp-sdk/plain-text/src/SeedPlainText/SeedPlainTextClient.cs index df861d9cd10b..f7b4288f8bdd 100644 --- a/seed/csharp-sdk/plain-text/src/SeedPlainText/SeedPlainTextClient.cs +++ b/seed/csharp-sdk/plain-text/src/SeedPlainText/SeedPlainTextClient.cs @@ -14,7 +14,7 @@ public SeedPlainTextClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPlainText" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPlainText.Version.Current }, { "User-Agent", "Fernplain-text/0.0.1" }, } ); diff --git a/seed/csharp-sdk/property-access/src/SeedPropertyAccess/SeedPropertyAccessClient.cs b/seed/csharp-sdk/property-access/src/SeedPropertyAccess/SeedPropertyAccessClient.cs index d534393d9624..a4bce70b5de0 100644 --- a/seed/csharp-sdk/property-access/src/SeedPropertyAccess/SeedPropertyAccessClient.cs +++ b/seed/csharp-sdk/property-access/src/SeedPropertyAccess/SeedPropertyAccessClient.cs @@ -15,7 +15,7 @@ public SeedPropertyAccessClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPropertyAccess" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPropertyAccess.Version.Current }, { "User-Agent", "Fernproperty-access/0.0.1" }, } ); diff --git a/seed/csharp-sdk/public-object/src/SeedPublicObject/SeedPublicObjectClient.cs b/seed/csharp-sdk/public-object/src/SeedPublicObject/SeedPublicObjectClient.cs index 399b0020db64..f7e25038cd97 100644 --- a/seed/csharp-sdk/public-object/src/SeedPublicObject/SeedPublicObjectClient.cs +++ b/seed/csharp-sdk/public-object/src/SeedPublicObject/SeedPublicObjectClient.cs @@ -14,7 +14,7 @@ public SeedPublicObjectClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedPublicObject" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedPublicObject.Version.Current }, { "User-Agent", "Fernpublic-object/0.0.1" }, } ); diff --git a/seed/csharp-sdk/query-param-name-conflict/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/query-param-name-conflict/src/SeedApi/SeedApiClient.cs index d14b1bf78d1e..843deabf6a1b 100644 --- a/seed/csharp-sdk/query-param-name-conflict/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/query-param-name-conflict/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernquery-param-name-conflict/0.0.1" }, } ); diff --git a/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi/SeedApiClient.cs index b573fef54882..9b66a6c7ebec 100644 --- a/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernquery-parameters-openapi-as-objects/0.0.1" }, } ); diff --git a/seed/csharp-sdk/query-parameters-openapi/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/query-parameters-openapi/src/SeedApi/SeedApiClient.cs index 7ae8f1064fa1..2fb337b07b04 100644 --- a/seed/csharp-sdk/query-parameters-openapi/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/query-parameters-openapi/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernquery-parameters-openapi/0.0.1" }, } ); diff --git a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/SeedQueryParametersClient.cs b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/SeedQueryParametersClient.cs index f4dca6ab2d2c..4ff5cdc7d361 100644 --- a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/SeedQueryParametersClient.cs +++ b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/SeedQueryParametersClient.cs @@ -14,7 +14,7 @@ public SeedQueryParametersClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedQueryParameters" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedQueryParameters.Version.Current }, { "User-Agent", "Fernquery-parameters/0.0.1" }, } ); diff --git a/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters/SeedRequestParametersClient.cs b/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters/SeedRequestParametersClient.cs index 5476e1941560..e2e5a0088f38 100644 --- a/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters/SeedRequestParametersClient.cs +++ b/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters/SeedRequestParametersClient.cs @@ -14,7 +14,7 @@ public SeedRequestParametersClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedRequestParameters" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedRequestParameters.Version.Current }, { "User-Agent", "Fernrequest-parameters/0.0.1" }, } ); diff --git a/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters/SeedRequestParametersClient.cs b/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters/SeedRequestParametersClient.cs index 5476e1941560..e2e5a0088f38 100644 --- a/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters/SeedRequestParametersClient.cs +++ b/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters/SeedRequestParametersClient.cs @@ -14,7 +14,7 @@ public SeedRequestParametersClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedRequestParameters" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedRequestParameters.Version.Current }, { "User-Agent", "Fernrequest-parameters/0.0.1" }, } ); diff --git a/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi/SeedApiClient.cs index 1eac5be51be4..2c1796b5f827 100644 --- a/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernrequired-nullable/0.0.1" }, } ); diff --git a/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi/SeedApiClient.cs index 42ac5858dd26..64715cccd613 100644 --- a/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernrequired-nullable/0.0.1" }, } ); diff --git a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/SeedNurseryApiClient.cs b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/SeedNurseryApiClient.cs index 2f270380cd3a..5ce52e76c5f7 100644 --- a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/SeedNurseryApiClient.cs +++ b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/SeedNurseryApiClient.cs @@ -14,7 +14,7 @@ public SeedNurseryApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedNurseryApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedNurseryApi.Version.Current }, { "User-Agent", "Fernreserved-keywords/0.0.1" }, } ); diff --git a/seed/csharp-sdk/respect-optional-request-body/no-custom-config/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/respect-optional-request-body/no-custom-config/src/SeedApi/SeedApiClient.cs index a881e55752d6..0bd9d230397c 100644 --- a/seed/csharp-sdk/respect-optional-request-body/no-custom-config/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/respect-optional-request-body/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernrespect-optional-request-body/0.0.1" }, } ); diff --git a/seed/csharp-sdk/respect-optional-request-body/respect-optional-request-body/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/respect-optional-request-body/respect-optional-request-body/src/SeedApi/SeedApiClient.cs index 91167ee278db..bbd92661d22a 100644 --- a/seed/csharp-sdk/respect-optional-request-body/respect-optional-request-body/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/respect-optional-request-body/respect-optional-request-body/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernrespect-optional-request-body/0.0.1" }, } ); diff --git a/seed/csharp-sdk/response-property/src/SeedResponseProperty/SeedResponsePropertyClient.cs b/seed/csharp-sdk/response-property/src/SeedResponseProperty/SeedResponsePropertyClient.cs index 444c802929e8..cafa9d51b61b 100644 --- a/seed/csharp-sdk/response-property/src/SeedResponseProperty/SeedResponsePropertyClient.cs +++ b/seed/csharp-sdk/response-property/src/SeedResponseProperty/SeedResponsePropertyClient.cs @@ -14,7 +14,7 @@ public SeedResponsePropertyClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedResponseProperty" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedResponseProperty.Version.Current }, { "User-Agent", "Fernresponse-property/0.0.1" }, } ); diff --git a/seed/csharp-sdk/schemaless-request-body-examples/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/schemaless-request-body-examples/src/SeedApi/SeedApiClient.cs index 65428e65a6c9..57ced3f6406e 100644 --- a/seed/csharp-sdk/schemaless-request-body-examples/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/schemaless-request-body-examples/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernschemaless-request-body-examples/0.0.1" }, } ); diff --git a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/SeedServerSentEventsClient.cs b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/SeedServerSentEventsClient.cs index 2931957dc909..d1e502128ccb 100644 --- a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/SeedServerSentEventsClient.cs +++ b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/SeedServerSentEventsClient.cs @@ -14,7 +14,7 @@ public SeedServerSentEventsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedServerSentEvents" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedServerSentEvents.Version.Current }, { "User-Agent", "Fernserver-sent-event-examples/0.0.1" }, } ); diff --git a/seed/csharp-sdk/server-sent-events-openapi/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/server-sent-events-openapi/src/SeedApi/SeedApiClient.cs index 29197932265d..b7b790f08d8c 100644 --- a/seed/csharp-sdk/server-sent-events-openapi/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/server-sent-events-openapi/src/SeedApi/SeedApiClient.cs @@ -17,7 +17,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernserver-sent-events-openapi/0.0.1" }, } ); diff --git a/seed/csharp-sdk/server-sent-events-resumable/src/SeedServerSentEventsResumable/SeedServerSentEventsResumableClient.cs b/seed/csharp-sdk/server-sent-events-resumable/src/SeedServerSentEventsResumable/SeedServerSentEventsResumableClient.cs index ee3027c7a216..aa1c4414cfc0 100644 --- a/seed/csharp-sdk/server-sent-events-resumable/src/SeedServerSentEventsResumable/SeedServerSentEventsResumableClient.cs +++ b/seed/csharp-sdk/server-sent-events-resumable/src/SeedServerSentEventsResumable/SeedServerSentEventsResumableClient.cs @@ -14,7 +14,7 @@ public SeedServerSentEventsResumableClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedServerSentEventsResumable" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedServerSentEventsResumable.Version.Current }, { "User-Agent", "Fernserver-sent-events-resumable/0.0.1" }, } ); diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEventsClient.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEventsClient.cs index 300e8706efc5..ee1583c0c03b 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEventsClient.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/SeedServerSentEventsClient.cs @@ -14,7 +14,7 @@ public SeedServerSentEventsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedServerSentEvents" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedServerSentEvents.Version.Current }, { "User-Agent", "Fernserver-sent-events/0.0.1" }, } ); diff --git a/seed/csharp-sdk/server-url-templating-single-url/disable-server-url-variables/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/server-url-templating-single-url/disable-server-url-variables/src/SeedApi/SeedApiClient.cs index bdec30aaef57..e9708b06a0a1 100644 --- a/seed/csharp-sdk/server-url-templating-single-url/disable-server-url-variables/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/server-url-templating-single-url/disable-server-url-variables/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernserver-url-templating-single-url/0.0.1" }, } ); diff --git a/seed/csharp-sdk/server-url-templating-single-url/no-custom-config/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/server-url-templating-single-url/no-custom-config/src/SeedApi/SeedApiClient.cs index bd40fe157e63..82c8dceb0c60 100644 --- a/seed/csharp-sdk/server-url-templating-single-url/no-custom-config/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/server-url-templating-single-url/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -30,7 +30,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernserver-url-templating-single-url/0.0.1" }, } ); diff --git a/seed/csharp-sdk/server-url-templating/disable-server-url-variables/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/server-url-templating/disable-server-url-variables/src/SeedApi/SeedApiClient.cs index 5d1b7934e315..1b95160af415 100644 --- a/seed/csharp-sdk/server-url-templating/disable-server-url-variables/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/server-url-templating/disable-server-url-variables/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernserver-url-templating/0.0.1" }, } ); diff --git a/seed/csharp-sdk/server-url-templating/no-custom-config/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/server-url-templating/no-custom-config/src/SeedApi/SeedApiClient.cs index b20d99ea6243..20d23114a0b5 100644 --- a/seed/csharp-sdk/server-url-templating/no-custom-config/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/server-url-templating/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -36,7 +36,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernserver-url-templating/0.0.1" }, } ); diff --git a/seed/csharp-sdk/simple-api/custom-output-path-object/lib/SeedApi/SeedSimpleApi/SeedSimpleApiClient.cs b/seed/csharp-sdk/simple-api/custom-output-path-object/lib/SeedApi/SeedSimpleApi/SeedSimpleApiClient.cs index 15b170641a08..03201993603e 100644 --- a/seed/csharp-sdk/simple-api/custom-output-path-object/lib/SeedApi/SeedSimpleApi/SeedSimpleApiClient.cs +++ b/seed/csharp-sdk/simple-api/custom-output-path-object/lib/SeedApi/SeedSimpleApi/SeedSimpleApiClient.cs @@ -14,7 +14,7 @@ public SeedSimpleApiClient(string? token = null, ClientOptions? clientOptions = { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedSimpleApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedSimpleApi.Version.Current }, { "User-Agent", "Fernsimple-api/0.0.1" }, } ); diff --git a/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi/SeedSimpleApiClient.cs b/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi/SeedSimpleApiClient.cs index 15b170641a08..03201993603e 100644 --- a/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi/SeedSimpleApiClient.cs +++ b/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi/SeedSimpleApiClient.cs @@ -14,7 +14,7 @@ public SeedSimpleApiClient(string? token = null, ClientOptions? clientOptions = { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedSimpleApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedSimpleApi.Version.Current }, { "User-Agent", "Fernsimple-api/0.0.1" }, } ); diff --git a/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi/SeedSimpleApiClient.cs b/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi/SeedSimpleApiClient.cs index 15b170641a08..03201993603e 100644 --- a/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi/SeedSimpleApiClient.cs +++ b/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi/SeedSimpleApiClient.cs @@ -14,7 +14,7 @@ public SeedSimpleApiClient(string? token = null, ClientOptions? clientOptions = { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedSimpleApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedSimpleApi.Version.Current }, { "User-Agent", "Fernsimple-api/0.0.1" }, } ); diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApiClient.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApiClient.cs index 15b170641a08..03201993603e 100644 --- a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApiClient.cs +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApiClient.cs @@ -14,7 +14,7 @@ public SeedSimpleApiClient(string? token = null, ClientOptions? clientOptions = { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedSimpleApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedSimpleApi.Version.Current }, { "User-Agent", "Fernsimple-api/0.0.1" }, } ); diff --git a/seed/csharp-sdk/simple-fhir/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/simple-fhir/src/SeedApi/SeedApiClient.cs index 3bc3c072a245..df35897d8ff0 100644 --- a/seed/csharp-sdk/simple-fhir/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/simple-fhir/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernsimple-fhir/0.0.1" }, } ); diff --git a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/SeedSingleUrlEnvironmentDefaultClient.cs b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/SeedSingleUrlEnvironmentDefaultClient.cs index 2757ca5a4583..b0fd04e3af1a 100644 --- a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/SeedSingleUrlEnvironmentDefaultClient.cs +++ b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/SeedSingleUrlEnvironmentDefaultClient.cs @@ -17,7 +17,7 @@ public SeedSingleUrlEnvironmentDefaultClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedSingleUrlEnvironmentDefault" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedSingleUrlEnvironmentDefault.Version.Current }, { "User-Agent", "Fernsingle-url-environment-default/0.0.1" }, } ); diff --git a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/SeedSingleUrlEnvironmentNoDefaultClient.cs b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/SeedSingleUrlEnvironmentNoDefaultClient.cs index 0ef8f9577e67..b16f2e9696e2 100644 --- a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/SeedSingleUrlEnvironmentNoDefaultClient.cs +++ b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/SeedSingleUrlEnvironmentNoDefaultClient.cs @@ -18,7 +18,7 @@ public SeedSingleUrlEnvironmentNoDefaultClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedSingleUrlEnvironmentNoDefault" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedSingleUrlEnvironmentNoDefault.Version.Current }, { "User-Agent", "Fernsingle-url-environment-no-default/0.0.1" }, } ); diff --git a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/SeedStreamingClient.cs b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/SeedStreamingClient.cs index f15c10f0a96f..a73bfc1eb9f8 100644 --- a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/SeedStreamingClient.cs +++ b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/SeedStreamingClient.cs @@ -14,7 +14,7 @@ public SeedStreamingClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedStreaming" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedStreaming.Version.Current }, { "User-Agent", "Fernstreaming-parameter/0.0.1" }, } ); diff --git a/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming/SeedStreamingClient.cs b/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming/SeedStreamingClient.cs index e0b690ee0a95..bda140f52686 100644 --- a/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming/SeedStreamingClient.cs +++ b/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming/SeedStreamingClient.cs @@ -14,7 +14,7 @@ public SeedStreamingClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedStreaming" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedStreaming.Version.Current }, { "User-Agent", "Fernstreaming/0.0.1" }, } ); diff --git a/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming/SeedStreamingClient.cs b/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming/SeedStreamingClient.cs index e0b690ee0a95..bda140f52686 100644 --- a/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming/SeedStreamingClient.cs +++ b/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming/SeedStreamingClient.cs @@ -14,7 +14,7 @@ public SeedStreamingClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedStreaming" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedStreaming.Version.Current }, { "User-Agent", "Fernstreaming/0.0.1" }, } ); diff --git a/seed/csharp-sdk/trace/src/SeedTrace/SeedTraceClient.cs b/seed/csharp-sdk/trace/src/SeedTrace/SeedTraceClient.cs index fac0e7afc1d7..82eb32acfef6 100644 --- a/seed/csharp-sdk/trace/src/SeedTrace/SeedTraceClient.cs +++ b/seed/csharp-sdk/trace/src/SeedTrace/SeedTraceClient.cs @@ -19,7 +19,7 @@ public SeedTraceClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedTrace" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedTrace.Version.Current }, { "User-Agent", "Ferntrace/0.0.1" }, } ); diff --git a/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/SeedUndiscriminatedUnionWithResponsePropertyClient.cs b/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/SeedUndiscriminatedUnionWithResponsePropertyClient.cs index 5b7d61c7ef16..2a4b121f3e09 100644 --- a/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/SeedUndiscriminatedUnionWithResponsePropertyClient.cs +++ b/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/SeedUndiscriminatedUnionWithResponsePropertyClient.cs @@ -16,7 +16,10 @@ public SeedUndiscriminatedUnionWithResponsePropertyClient(ClientOptions? clientO { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedUndiscriminatedUnionWithResponseProperty" }, - { "X-Fern-SDK-Version", Version.Current }, + { + "X-Fern-SDK-Version", + global::SeedUndiscriminatedUnionWithResponseProperty.Version.Current + }, { "User-Agent", "Fernundiscriminated-union-with-response-property/0.0.1" }, } ); diff --git a/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/SeedUndiscriminatedUnionsClient.cs b/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/SeedUndiscriminatedUnionsClient.cs index c7572e8d4a5f..bb7701239bb1 100644 --- a/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/SeedUndiscriminatedUnionsClient.cs +++ b/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/SeedUndiscriminatedUnionsClient.cs @@ -14,7 +14,7 @@ public SeedUndiscriminatedUnionsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedUndiscriminatedUnions" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedUndiscriminatedUnions.Version.Current }, { "User-Agent", "Fernundiscriminated-unions/0.0.1" }, } ); diff --git a/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/SeedUndiscriminatedUnionsClient.cs b/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/SeedUndiscriminatedUnionsClient.cs index c7572e8d4a5f..bb7701239bb1 100644 --- a/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/SeedUndiscriminatedUnionsClient.cs +++ b/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/SeedUndiscriminatedUnionsClient.cs @@ -14,7 +14,7 @@ public SeedUndiscriminatedUnionsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedUndiscriminatedUnions" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedUndiscriminatedUnions.Version.Current }, { "User-Agent", "Fernundiscriminated-unions/0.0.1" }, } ); diff --git a/seed/csharp-sdk/union-query-parameters/src/SeedUnionQueryParameters/SeedUnionQueryParametersClient.cs b/seed/csharp-sdk/union-query-parameters/src/SeedUnionQueryParameters/SeedUnionQueryParametersClient.cs index 8b75ea6d3006..350476dea9b2 100644 --- a/seed/csharp-sdk/union-query-parameters/src/SeedUnionQueryParameters/SeedUnionQueryParametersClient.cs +++ b/seed/csharp-sdk/union-query-parameters/src/SeedUnionQueryParameters/SeedUnionQueryParametersClient.cs @@ -14,7 +14,7 @@ public SeedUnionQueryParametersClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedUnionQueryParameters" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedUnionQueryParameters.Version.Current }, { "User-Agent", "Fernunion-query-parameters/0.0.1" }, } ); diff --git a/seed/csharp-sdk/unions-with-local-date/src/SeedUnions/SeedUnionsClient.cs b/seed/csharp-sdk/unions-with-local-date/src/SeedUnions/SeedUnionsClient.cs index 80a64333691c..20f341dd2bb3 100644 --- a/seed/csharp-sdk/unions-with-local-date/src/SeedUnions/SeedUnionsClient.cs +++ b/seed/csharp-sdk/unions-with-local-date/src/SeedUnions/SeedUnionsClient.cs @@ -14,7 +14,7 @@ public SeedUnionsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedUnions" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedUnions.Version.Current }, { "User-Agent", "Fernunions-with-local-date/0.0.1" }, } ); diff --git a/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions/SeedUnionsClient.cs b/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions/SeedUnionsClient.cs index 4edf96cf11ea..4ccf34e98f39 100644 --- a/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions/SeedUnionsClient.cs +++ b/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions/SeedUnionsClient.cs @@ -14,7 +14,7 @@ public SeedUnionsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedUnions" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedUnions.Version.Current }, { "User-Agent", "Fernunions/0.0.1" }, } ); diff --git a/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions/SeedUnionsClient.cs b/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions/SeedUnionsClient.cs index 4edf96cf11ea..4ccf34e98f39 100644 --- a/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions/SeedUnionsClient.cs +++ b/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions/SeedUnionsClient.cs @@ -14,7 +14,7 @@ public SeedUnionsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedUnions" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedUnions.Version.Current }, { "User-Agent", "Fernunions/0.0.1" }, } ); diff --git a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/SeedUnknownAsAnyClient.cs b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/SeedUnknownAsAnyClient.cs index e5c1bb5d9814..5792a7b49fab 100644 --- a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/SeedUnknownAsAnyClient.cs +++ b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/SeedUnknownAsAnyClient.cs @@ -14,7 +14,7 @@ public SeedUnknownAsAnyClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedUnknownAsAny" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedUnknownAsAny.Version.Current }, { "User-Agent", "Fernunknown/0.0.1" }, } ); diff --git a/seed/csharp-sdk/url-form-encoded/no-custom-config/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/url-form-encoded/no-custom-config/src/SeedApi/SeedApiClient.cs index 1f89b85c50fc..791054a835d7 100644 --- a/seed/csharp-sdk/url-form-encoded/no-custom-config/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/url-form-encoded/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernurl-form-encoded/0.0.1" }, } ); diff --git a/seed/csharp-sdk/validation/src/SeedValidation/SeedValidationClient.cs b/seed/csharp-sdk/validation/src/SeedValidation/SeedValidationClient.cs index d48a4e7aaf1d..9140fbd772fd 100644 --- a/seed/csharp-sdk/validation/src/SeedValidation/SeedValidationClient.cs +++ b/seed/csharp-sdk/validation/src/SeedValidation/SeedValidationClient.cs @@ -15,7 +15,7 @@ public SeedValidationClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedValidation" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedValidation.Version.Current }, { "User-Agent", "Fernvalidation/0.0.1" }, } ); diff --git a/seed/csharp-sdk/variables/src/SeedVariables/SeedVariablesClient.cs b/seed/csharp-sdk/variables/src/SeedVariables/SeedVariablesClient.cs index f478728a183f..6ea31ad280de 100644 --- a/seed/csharp-sdk/variables/src/SeedVariables/SeedVariablesClient.cs +++ b/seed/csharp-sdk/variables/src/SeedVariables/SeedVariablesClient.cs @@ -14,7 +14,7 @@ public SeedVariablesClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedVariables" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedVariables.Version.Current }, { "User-Agent", "Fernvariables/0.0.1" }, } ); diff --git a/seed/csharp-sdk/version-no-default/src/SeedVersion/SeedVersionClient.cs b/seed/csharp-sdk/version-no-default/src/SeedVersion/SeedVersionClient.cs index 72dfad5aa0ce..0ed701414191 100644 --- a/seed/csharp-sdk/version-no-default/src/SeedVersion/SeedVersionClient.cs +++ b/seed/csharp-sdk/version-no-default/src/SeedVersion/SeedVersionClient.cs @@ -14,7 +14,7 @@ public SeedVersionClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedVersion" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedVersion.Version.Current }, { "User-Agent", "Fernversion-no-default/0.0.1" }, } ); diff --git a/seed/csharp-sdk/version/src/SeedVersion/SeedVersionClient.cs b/seed/csharp-sdk/version/src/SeedVersion/SeedVersionClient.cs index 66ca7c4560e5..5593e399622f 100644 --- a/seed/csharp-sdk/version/src/SeedVersion/SeedVersionClient.cs +++ b/seed/csharp-sdk/version/src/SeedVersion/SeedVersionClient.cs @@ -14,7 +14,7 @@ public SeedVersionClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedVersion" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedVersion.Version.Current }, { "User-Agent", "Fernversion/0.0.1" }, } ); diff --git a/seed/csharp-sdk/webhook-audience/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/webhook-audience/src/SeedApi/SeedApiClient.cs index fd2a2cc2538d..7d3226e56a97 100644 --- a/seed/csharp-sdk/webhook-audience/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/webhook-audience/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernwebhook-audience/0.0.1" }, } ); diff --git a/seed/csharp-sdk/webhooks/src/SeedWebhooks/SeedWebhooksClient.cs b/seed/csharp-sdk/webhooks/src/SeedWebhooks/SeedWebhooksClient.cs index 39e251669501..8c1ccb76b2b7 100644 --- a/seed/csharp-sdk/webhooks/src/SeedWebhooks/SeedWebhooksClient.cs +++ b/seed/csharp-sdk/webhooks/src/SeedWebhooks/SeedWebhooksClient.cs @@ -14,7 +14,7 @@ public SeedWebhooksClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedWebhooks" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedWebhooks.Version.Current }, { "User-Agent", "Fernwebhooks/0.0.1" }, } ); diff --git a/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth/SeedWebsocketBearerAuthClient.cs b/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth/SeedWebsocketBearerAuthClient.cs index 4d6103fcfc32..4ac6152277ad 100644 --- a/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth/SeedWebsocketBearerAuthClient.cs +++ b/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth/SeedWebsocketBearerAuthClient.cs @@ -18,7 +18,7 @@ public SeedWebsocketBearerAuthClient(string? apiKey = null, ClientOptions? clien { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedWebsocketBearerAuth" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedWebsocketBearerAuth.Version.Current }, { "User-Agent", "Fernwebsocket-bearer-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth/SeedWebsocketAuthClient.cs b/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth/SeedWebsocketAuthClient.cs index e75497edd726..4ce15c9ec455 100644 --- a/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth/SeedWebsocketAuthClient.cs +++ b/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth/SeedWebsocketAuthClient.cs @@ -20,7 +20,7 @@ public SeedWebsocketAuthClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedWebsocketAuth" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedWebsocketAuth.Version.Current }, { "User-Agent", "Fernwebsocket-inferred-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/websocket-multi-url/no-custom-config/src/SeedWebsocketMultiUrl/SeedWebsocketMultiUrlClient.cs b/seed/csharp-sdk/websocket-multi-url/no-custom-config/src/SeedWebsocketMultiUrl/SeedWebsocketMultiUrlClient.cs index a694a211855c..98776f041fda 100644 --- a/seed/csharp-sdk/websocket-multi-url/no-custom-config/src/SeedWebsocketMultiUrl/SeedWebsocketMultiUrlClient.cs +++ b/seed/csharp-sdk/websocket-multi-url/no-custom-config/src/SeedWebsocketMultiUrl/SeedWebsocketMultiUrlClient.cs @@ -14,7 +14,7 @@ public SeedWebsocketMultiUrlClient(string? token = null, ClientOptions? clientOp { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedWebsocketMultiUrl" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedWebsocketMultiUrl.Version.Current }, { "User-Agent", "Fernwebsocket-multi-url/0.0.1" }, } ); diff --git a/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket/SeedWebsocketClient.cs b/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket/SeedWebsocketClient.cs index 40a24718c1c5..4c94cb979ed5 100644 --- a/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket/SeedWebsocketClient.cs +++ b/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket/SeedWebsocketClient.cs @@ -14,7 +14,7 @@ public SeedWebsocketClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedWebsocket" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedWebsocket.Version.Current }, { "User-Agent", "Fernwebsocket/0.0.1" }, } ); diff --git a/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket/SeedWebsocketClient.cs b/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket/SeedWebsocketClient.cs index 6d4cb183c9c1..fbb7afed0bd5 100644 --- a/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket/SeedWebsocketClient.cs +++ b/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket/SeedWebsocketClient.cs @@ -15,7 +15,7 @@ public SeedWebsocketClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedWebsocket" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedWebsocket.Version.Current }, { "User-Agent", "Fernwebsocket/0.0.1" }, } ); diff --git a/seed/csharp-sdk/x-fern-default/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/x-fern-default/src/SeedApi/SeedApiClient.cs index e63e26dc0137..dcf81fa59022 100644 --- a/seed/csharp-sdk/x-fern-default/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/x-fern-default/src/SeedApi/SeedApiClient.cs @@ -15,7 +15,7 @@ public SeedApiClient(string? apiVersion = null, ClientOptions? clientOptions = n { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernx-fern-default/0.0.1" }, } ); diff --git a/seed/csharp-sdk/x-fern-global-parameters/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/x-fern-global-parameters/src/SeedApi/SeedApiClient.cs index 18d70d11345c..5493e0b57714 100644 --- a/seed/csharp-sdk/x-fern-global-parameters/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/x-fern-global-parameters/src/SeedApi/SeedApiClient.cs @@ -14,7 +14,7 @@ public SeedApiClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApi" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApi.Version.Current }, { "User-Agent", "Fernx-fern-global-parameters/0.0.1" }, } ); diff --git a/seed/php-sdk/php-global-header-literal-env/.fern/metadata.json b/seed/php-sdk/php-global-header-literal-env/.fern/metadata.json new file mode 100644 index 000000000000..9ad868459c3d --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/.fern/metadata.json @@ -0,0 +1,9 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-php-sdk", + "generatorVersion": "latest", + "originGitCommit": "DUMMY", + "invokedBy": "manual", + "requestedVersion": "0.0.1", + "sdkVersion": "0.0.1" +} \ No newline at end of file diff --git a/seed/php-sdk/php-global-header-literal-env/.github/workflows/ci.yml b/seed/php-sdk/php-global-header-literal-env/.github/workflows/ci.yml new file mode 100644 index 000000000000..ac6fa0af1afa --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: ci + +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + + - name: Install tools + run: | + composer install + + - name: Build + run: | + composer build + + - name: Analyze + run: | + composer analyze + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + + - name: Install tools + run: | + composer install + + - name: Run Tests + run: | + composer test diff --git a/seed/php-sdk/php-global-header-literal-env/.gitignore b/seed/php-sdk/php-global-header-literal-env/.gitignore new file mode 100644 index 000000000000..31a1aeb14f35 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/.gitignore @@ -0,0 +1,5 @@ +.idea +.php-cs-fixer.cache +.phpunit.result.cache +composer.lock +vendor/ \ No newline at end of file diff --git a/seed/php-sdk/php-global-header-literal-env/CONTRIBUTING.md b/seed/php-sdk/php-global-header-literal-env/CONTRIBUTING.md new file mode 100644 index 000000000000..0c772cdc8020 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/CONTRIBUTING.md @@ -0,0 +1,120 @@ +# Contributing + +Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. + +## Getting Started + +### Prerequisites + +- PHP 8.1+ +- Composer + +### Installation + +Install the project dependencies: + +```bash +composer install +``` + +### Testing + +Run the test suite: + +```bash +composer test +``` + +Or run PHPUnit directly: + +```bash +./vendor/bin/phpunit +``` + +### Linting & Formatting + +Fix code style issues: + +```bash +./vendor/bin/php-cs-fixer fix +``` + +### Static Analysis + +Run static analysis: + +```bash +./vendor/bin/phpstan analyse +``` + +## About Generated Code + +**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. + +### Generated Files + +The following directories contain generated code: +- `src/` - API client classes and types +- Most PHP files in the project + +### How to Customize + +If you need to customize the SDK, you have two options: + +#### Option 1: Use `.fernignore` + +For custom code that should persist across SDK regenerations: + +1. Create a `.fernignore` file in the project root +2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) +3. Add your custom code to those files + +Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. + +For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). + +#### Option 2: Contribute to the Generator + +If you want to change how code is generated for all users of this SDK: + +1. The PHP SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) +2. Generator code is located at `generators/php/` +3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) +4. Submit a pull request with your changes to the generator + +This approach is best for: +- Bug fixes in generated code +- New features that would benefit all users +- Improvements to code generation patterns + +## Making Changes + +### Workflow + +1. Create a new branch for your changes +2. Make your modifications +3. Run tests to ensure nothing breaks: `composer test` +4. Run formatting: `./vendor/bin/php-cs-fixer fix` +5. Run static analysis: `./vendor/bin/phpstan analyse` +6. Commit your changes with a clear commit message +7. Push your branch and create a pull request + +### Commit Messages + +Write clear, descriptive commit messages that explain what changed and why. + +### Code Style + +This project uses PHP CS Fixer for automated code formatting. Run `./vendor/bin/php-cs-fixer fix` before committing to ensure your code meets the project's style guidelines. + +## Questions or Issues? + +If you have questions or run into issues: + +1. Check the [Fern documentation](https://buildwithfern.com) +2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) +3. Open a new issue if your question hasn't been addressed + +## License + +By contributing to this project, you agree that your contributions will be licensed under the same license as the project. diff --git a/seed/php-sdk/php-global-header-literal-env/README.md b/seed/php-sdk/php-global-header-literal-env/README.md new file mode 100644 index 000000000000..e9f9ddbb2855 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/README.md @@ -0,0 +1,146 @@ +# Seed PHP Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FPHP) +[![php shield](https://img.shields.io/badge/php-packagist-pink)](https://packagist.org/packages/seed/seed) + +The Seed PHP library provides convenient access to the Seed APIs from PHP. + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Usage](#usage) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Custom Client](#custom-client) + - [Retries](#retries) + - [Timeouts](#timeouts) +- [Contributing](#contributing) + +## Requirements + +This SDK requires PHP ^8.1. + +## Installation + +```sh +composer require seed/seed +``` + +## Usage + +Instantiate and use the client with the following: + +```php +', +); +$client->service->getWithLiteralVersionHeader(); + +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), an exception will be thrown. + +```php +use Seed\Exceptions\SeedApiException; +use Seed\Exceptions\SeedException; + +try { + $response = $client->service->getWithLiteralVersionHeader(...); +} catch (SeedApiException $e) { + echo 'API Exception occurred: ' . $e->getMessage() . "\n"; + echo 'Status Code: ' . $e->getCode() . "\n"; + echo 'Response Body: ' . $e->getBody() . "\n"; + // Optionally, rethrow the exception or handle accordingly. +} +``` + +## Advanced + +### Custom Client + +This SDK is built to work with any HTTP client that implements the [PSR-18](https://www.php-fig.org/psr/psr-18/) `ClientInterface`. +By default, if no client is provided, the SDK will use `php-http/discovery` to find an installed HTTP client. +However, you can pass your own client that adheres to `ClientInterface`: + +```php +use Seed\SeedClient; + +// Pass any PSR-18 compatible HTTP client implementation. +// For example, using Guzzle: +$customClient = new \GuzzleHttp\Client([ + 'timeout' => 5.0, +]); + +$client = new SeedClient(options: [ + 'client' => $customClient +]); + +// Or using Symfony HttpClient: +// $customClient = (new \Symfony\Component\HttpClient\Psr18Client()) +// ->withOptions(['timeout' => 5.0]); +// +// $client = new SeedClient(options: [ +// 'client' => $customClient +// ]); +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retryable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) (Internal Server Error) + +The `retryStatusCodes` configuration controls which [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) status codes are retried: + +- `legacy` (default): Retries `408`, `429`, and all `>= 500` +- `recommended`: Retries `408`, `429`, `502`, `503`, `504` only (excludes `500 Internal Server Error` to avoid retrying non-idempotent failures) + +Use the `maxRetries` request option to configure this behavior. + +```php +$response = $client->service->getWithLiteralVersionHeader( + ..., + options: [ + 'maxRetries' => 0 // Override maxRetries at the request level + ] +); +``` + +### Timeouts + +The SDK defaults to a 30 second timeout. Use the `timeout` option to configure this behavior. + +```php +$response = $client->service->getWithLiteralVersionHeader( + ..., + options: [ + 'timeout' => 3.0 // Override timeout at the request level + ] +); +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/php-sdk/php-global-header-literal-env/composer.json b/seed/php-sdk/php-global-header-literal-env/composer.json new file mode 100644 index 000000000000..4b33a660f733 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/composer.json @@ -0,0 +1,46 @@ +{ + "name": "seed/seed", + "version": "0.0.1", + "description": "Seed PHP Library", + "keywords": [ + "seed", + "api", + "sdk" + ], + "license": [], + "require": { + "php": "^8.1", + "ext-json": "*", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "php-http/discovery": "^1.0", + "php-http/multipart-stream-builder": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.22", + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.12", + "guzzlehttp/guzzle": "^7.4" + }, + "autoload": { + "psr-4": { + "Seed\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Seed\\Tests\\": "tests/" + } + }, + "scripts": { + "build": [ + "@php -l src", + "@php -l tests" + ], + "test": "phpunit", + "analyze": "phpstan analyze src tests --memory-limit=1G" + } +} \ No newline at end of file diff --git a/seed/php-sdk/php-global-header-literal-env/phpstan.neon b/seed/php-sdk/php-global-header-literal-env/phpstan.neon new file mode 100644 index 000000000000..780706b8f8a2 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: max + reportUnmatchedIgnoredErrors: false + paths: + - src + - tests \ No newline at end of file diff --git a/seed/php-sdk/php-global-header-literal-env/phpunit.xml b/seed/php-sdk/php-global-header-literal-env/phpunit.xml new file mode 100644 index 000000000000..54630a51163c --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + \ No newline at end of file diff --git a/seed/php-sdk/php-global-header-literal-env/reference.md b/seed/php-sdk/php-global-header-literal-env/reference.md new file mode 100644 index 000000000000..2dd59908411c --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/reference.md @@ -0,0 +1,41 @@ +# Reference +## Service +
$client->service->getWithLiteralVersionHeader() -> ?string +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET request with a literal version header +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```php +$client->service->getWithLiteralVersionHeader(); +``` +
+
+
+
+ + +
+
+
+ diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Client/BaseApiRequest.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Client/BaseApiRequest.php new file mode 100644 index 000000000000..5e1283e2b6f6 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Client/BaseApiRequest.php @@ -0,0 +1,22 @@ + $headers Additional headers for the request (optional) + * @param array $query Query parameters for the request (optional) + */ + public function __construct( + public readonly string $baseUrl, + public readonly string $path, + public readonly HttpMethod $method, + public readonly array $headers = [], + public readonly array $query = [], + ) { + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Client/HttpClientBuilder.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Client/HttpClientBuilder.php new file mode 100644 index 000000000000..8ac806af0325 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Client/HttpClientBuilder.php @@ -0,0 +1,56 @@ + + */ + private array $responses = []; + + /** + * @var array + */ + private array $requests = []; + + /** + * @param ResponseInterface ...$responses + */ + public function append(ResponseInterface ...$responses): void + { + foreach ($responses as $response) { + $this->responses[] = $response; + } + } + + /** + * @param RequestInterface $request + * @return ResponseInterface + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + $this->requests[] = $request; + + if (empty($this->responses)) { + throw new RuntimeException('No more responses in the queue. Add responses using append().'); + } + + return array_shift($this->responses); + } + + /** + * @return ?RequestInterface + */ + public function getLastRequest(): ?RequestInterface + { + if (empty($this->requests)) { + return null; + } + return $this->requests[count($this->requests) - 1]; + } + + /** + * @return int + */ + public function getRequestCount(): int + { + return count($this->requests); + } + + /** + * Returns the number of remaining responses in the queue. + * + * @return int + */ + public function count(): int + { + return count($this->responses); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Client/RawClient.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Client/RawClient.php new file mode 100644 index 000000000000..6ed50f4156f3 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Client/RawClient.php @@ -0,0 +1,359 @@ + $headers + */ + private array $headers; + + /** + * @var ?(callable(): array) $getAuthHeaders + */ + private $getAuthHeaders; + + /** + * @param ?array{ + * baseUrl?: string, + * client?: ClientInterface, + * maxRetries?: int, + * timeout?: float, + * headers?: array, + * getAuthHeaders?: callable(): array, + * } $options + */ + public function __construct( + public readonly ?array $options = null, + ) { + $this->client = HttpClientBuilder::build( + $this->options['client'] ?? null, + $this->options['maxRetries'] ?? 2, + ); + $this->requestFactory = HttpClientBuilder::requestFactory(); + $this->streamFactory = HttpClientBuilder::streamFactory(); + $this->headers = $this->options['headers'] ?? []; + $this->getAuthHeaders = $this->options['getAuthHeaders'] ?? null; + } + + /** + * @param BaseApiRequest $request + * @param ?array{ + * maxRetries?: int, + * timeout?: float, + * headers?: array, + * queryParameters?: array, + * bodyProperties?: array, + * } $options + * @return ResponseInterface + * @throws ClientExceptionInterface + */ + public function sendRequest( + BaseApiRequest $request, + ?array $options = null, + ): ResponseInterface { + $opts = $options ?? []; + $httpRequest = $this->buildRequest($request, $opts); + + $timeout = $opts['timeout'] ?? $this->options['timeout'] ?? null; + $maxRetries = $opts['maxRetries'] ?? null; + + return $this->client->send($httpRequest, $timeout, $maxRetries); + } + + /** + * @param BaseApiRequest $request + * @param array{ + * headers?: array, + * queryParameters?: array, + * bodyProperties?: array, + * } $options + * @return RequestInterface + */ + private function buildRequest( + BaseApiRequest $request, + array $options + ): RequestInterface { + $url = $this->buildUrl($request, $options); + $headers = $this->encodeHeaders($request, $options); + + $httpRequest = $this->requestFactory->createRequest( + $request->method->name, + $url, + ); + + // Encode body and, for multipart, capture the Content-Type with boundary. + if ($request instanceof MultipartApiRequest && $request->body !== null) { + $builder = new MultipartStreamBuilder($this->streamFactory); + $request->body->addToBuilder($builder); + $httpRequest = $httpRequest->withBody($builder->build()); + $headers['Content-Type'] = "multipart/form-data; boundary={$builder->getBoundary()}"; + } else { + $body = $this->encodeRequestBody($request, $options); + if ($body !== null) { + $httpRequest = $httpRequest->withBody($body); + } + } + + foreach ($headers as $name => $value) { + $httpRequest = $httpRequest->withHeader($name, $value); + } + + return $httpRequest; + } + + /** + * @param BaseApiRequest $request + * @param array{ + * headers?: array, + * } $options + * @return array + */ + private function encodeHeaders( + BaseApiRequest $request, + array $options, + ): array { + $authHeaders = $this->getAuthHeaders !== null ? ($this->getAuthHeaders)() : []; + return match (get_class($request)) { + JsonApiRequest::class => array_merge( + [ + "Content-Type" => "application/json", + "Accept" => "*/*", + ], + $this->headers, + $authHeaders, + $request->headers, + $options['headers'] ?? [], + ), + UrlEncodedApiRequest::class => array_merge( + [ + "Content-Type" => "application/x-www-form-urlencoded", + "Accept" => "*/*", + ], + $this->headers, + $authHeaders, + $request->headers, + $options['headers'] ?? [], + ), + MultipartApiRequest::class => array_merge( + $this->headers, + $authHeaders, + $request->headers, + $options['headers'] ?? [], + ), + default => throw new InvalidArgumentException('Unsupported request type: ' . get_class($request)), + }; + } + + /** + * @param BaseApiRequest $request + * @param array{ + * bodyProperties?: array, + * } $options + * @return ?StreamInterface + */ + private function encodeRequestBody( + BaseApiRequest $request, + array $options, + ): ?StreamInterface { + if ($request instanceof JsonApiRequest) { + return $request->body === null ? null : $this->streamFactory->createStream( + JsonEncoder::encode( + $this->buildJsonBody( + $request->body, + $options, + ), + ) + ); + } + + if ($request instanceof UrlEncodedApiRequest) { + if ($request->body === null) { + return null; + } + $body = $this->buildJsonBody($request->body, $options); + if ($body instanceof JsonSerializable) { + $body = $body->jsonSerialize(); + } + if (is_object($body)) { + $body = (array)$body; + } + if (!is_array($body)) { + throw new InvalidArgumentException('URL-encoded request bodies must serialize to an array.'); + } + return $this->streamFactory->createStream(http_build_query($body)); + } + + if ($request instanceof MultipartApiRequest) { + return null; + } + + throw new InvalidArgumentException('Unsupported request type: ' . get_class($request)); + } + + /** + * @param mixed $body + * @param array{ + * bodyProperties?: array, + * } $options + * @return mixed + */ + private function buildJsonBody( + mixed $body, + array $options, + ): mixed { + $overrideProperties = $options['bodyProperties'] ?? []; + if (is_array($body) && (empty($body) || self::isSequential($body))) { + return array_merge($body, $overrideProperties); + } + + if ($body instanceof JsonSerializable) { + $result = $body->jsonSerialize(); + } else { + $result = $body; + } + if (is_array($result)) { + $result = array_merge($result, $overrideProperties); + if (empty($result)) { + // force to be serialized as {} instead of [] + return (object)($result); + } + } + + return $result; + } + + /** + * Percent-encodes a value for use inside a single URL path segment. Encoding happens here, + * where the value is still separate from the path template, so that a value containing "/" + * or ".." cannot change which endpoint the request resolves to. + */ + public static function encodePathParam(mixed $value): string + { + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + if (is_null($value)) { + return ''; + } + if (is_scalar($value)) { + return rawurlencode((string)$value); + } + if (is_object($value) && method_exists($value, '__toString')) { + return rawurlencode((string)$value); + } + return rawurlencode(JsonEncoder::encode($value)); + } + + /** + * @param BaseApiRequest $request + * @param array{ + * queryParameters?: array, + * } $options + * @return string + */ + private function buildUrl( + BaseApiRequest $request, + array $options, + ): string { + $baseUrl = $request->baseUrl; + $trimmedBaseUrl = rtrim($baseUrl, '/'); + $trimmedBasePath = ltrim($request->path, '/'); + $url = "{$trimmedBaseUrl}/{$trimmedBasePath}"; + $query = array_merge( + $request->query, + $options['queryParameters'] ?? [], + ); + if (!empty($query)) { + $url .= '?' . $this->encodeQuery($query); + } + return $url; + } + + /** + * @param array $query + * @return string + */ + private function encodeQuery(array $query): string + { + $parts = []; + foreach ($query as $key => $value) { + if (is_array($value)) { + foreach ($value as $item) { + $parts[] = urlencode($key) . '=' . $this->encodeQueryValue($item); + } + } else { + $parts[] = urlencode($key) . '=' . $this->encodeQueryValue($value); + } + } + return implode('&', $parts); + } + + private function encodeQueryValue(mixed $value): string + { + if (is_string($value)) { + return urlencode($value); + } + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + if (is_scalar($value)) { + return urlencode((string)$value); + } + if (is_null($value)) { + return 'null'; + } + // Unreachable, but included for a best effort. + return urlencode(JsonEncoder::encode($value)); + } + + /** + * Check if an array is sequential, not associative. + * @param mixed[] $arr + * @return bool + */ + private static function isSequential(array $arr): bool + { + if (empty($arr)) { + return false; + } + $length = count($arr); + $keys = array_keys($arr); + for ($i = 0; $i < $length; $i++) { + if ($keys[$i] !== $i) { + return false; + } + } + return true; + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Client/RetryDecoratingClient.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Client/RetryDecoratingClient.php new file mode 100644 index 000000000000..46d82cb552fb --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Client/RetryDecoratingClient.php @@ -0,0 +1,241 @@ +client = $client; + $this->maxRetries = $maxRetries; + $this->baseDelay = $baseDelay; + $this->sleepFunction = $sleepFunction ?? 'usleep'; + } + + /** + * @param RequestInterface $request + * @return ResponseInterface + * @throws ClientExceptionInterface + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->send($request); + } + + /** + * Sends a request with optional per-request timeout and retry overrides. + * + * When a Guzzle or Symfony PSR-18 client is detected, the timeout is + * forwarded via the client's native API. For other PSR-18 clients the + * timeout value is silently ignored. + * + * @param RequestInterface $request + * @param ?float $timeout Timeout in seconds, or null to use the client default. + * @param ?int $maxRetries Maximum retry attempts, or null to use the client default. + * @return ResponseInterface + * @throws ClientExceptionInterface + */ + public function send( + RequestInterface $request, + ?float $timeout = null, + ?int $maxRetries = null, + ): ResponseInterface { + $maxRetries = $maxRetries ?? $this->maxRetries; + $retryAttempt = 0; + $lastResponse = null; + + while (true) { + try { + $lastResponse = $this->doSend($request, $timeout); + if (!$this->shouldRetry($retryAttempt, $maxRetries, $lastResponse)) { + return $lastResponse; + } + } catch (ClientExceptionInterface $e) { + if ($retryAttempt >= $maxRetries) { + throw $e; + } + } + + $retryAttempt++; + $delay = $this->getRetryDelay($retryAttempt, $lastResponse); + ($this->sleepFunction)($delay * 1000); // Convert milliseconds to microseconds + + // Rewind the request body so retries don't send an empty body. + $request->getBody()->rewind(); + } + } + + /** + * Dispatches the request to the underlying client, forwarding the timeout + * option to Guzzle or Symfony when available. + * + * @param RequestInterface $request + * @param ?float $timeout + * @return ResponseInterface + * @throws ClientExceptionInterface + */ + private function doSend(RequestInterface $request, ?float $timeout): ResponseInterface + { + static $warned = false; + + if ($timeout === null) { + return $this->client->sendRequest($request); + } + + if (interface_exists('GuzzleHttp\ClientInterface') + && $this->client instanceof \GuzzleHttp\ClientInterface + ) { + return $this->client->send($request, ['timeout' => $timeout]); + } + if (class_exists('Symfony\Component\HttpClient\Psr18Client') + && $this->client instanceof \Symfony\Component\HttpClient\Psr18Client + ) { + /** @var ClientInterface $clientWithTimeout */ + $clientWithTimeout = $this->client->withOptions(['timeout' => $timeout]); + return $clientWithTimeout->sendRequest($request); + } + + if ($warned) { + return $this->client->sendRequest($request); + } + $warned = true; + trigger_error( + 'Timeout option is not supported for the current PSR-18 client (' + . get_class($this->client) + . '). Use Guzzle or Symfony HttpClient for timeout support.', + E_USER_WARNING, + ); + return $this->client->sendRequest($request); + } + + /** + * @param int $retryAttempt + * @param int $maxRetries + * @param ?ResponseInterface $response + * @return bool + */ + private function shouldRetry( + int $retryAttempt, + int $maxRetries, + ?ResponseInterface $response = null, + ): bool { + if ($retryAttempt >= $maxRetries) { + return false; + } + + if ($response !== null) { + return $response->getStatusCode() >= 500 || + in_array($response->getStatusCode(), self::RETRY_STATUS_CODES); + } + + return false; + } + + /** + * Calculate the retry delay based on response headers or exponential backoff. + * + * @param int $retryAttempt + * @param ?ResponseInterface $response + * @return int milliseconds + */ + private function getRetryDelay(int $retryAttempt, ?ResponseInterface $response): int + { + if ($response !== null) { + // Check Retry-After header + $retryAfter = $response->getHeaderLine('Retry-After'); + if ($retryAfter !== '') { + // Try parsing as integer (seconds) + if (is_numeric($retryAfter)) { + $retryAfterSeconds = (int)$retryAfter; + if ($retryAfterSeconds > 0) { + return min($retryAfterSeconds * 1000, self::MAX_RETRY_DELAY); + } + } + + // Try parsing as HTTP date + $retryAfterDate = strtotime($retryAfter); + if ($retryAfterDate !== false) { + $delay = ($retryAfterDate - time()) * 1000; + if ($delay > 0) { + return min(max($delay, 0), self::MAX_RETRY_DELAY); + } + } + } + + // Check X-RateLimit-Reset header + $rateLimitReset = $response->getHeaderLine('X-RateLimit-Reset'); + if ($rateLimitReset !== '' && is_numeric($rateLimitReset)) { + $resetTime = (int)$rateLimitReset; + $delay = ($resetTime * 1000) - (int)(microtime(true) * 1000); + if ($delay > 0) { + return $this->addPositiveJitter(min($delay, self::MAX_RETRY_DELAY)); + } + } + } + + // Fall back to exponential backoff with symmetric jitter + return $this->addSymmetricJitter( + min($this->exponentialDelay($retryAttempt), self::MAX_RETRY_DELAY) + ); + } + + /** + * Add positive jitter (0% to +20%) to the delay. + * + * @param int $delay + * @return int + */ + private function addPositiveJitter(int $delay): int + { + $jitterMultiplier = 1 + (mt_rand() / mt_getrandmax()) * self::JITTER_FACTOR; + return (int)($delay * $jitterMultiplier); + } + + /** + * Add symmetric jitter (-10% to +10%) to the delay. + * + * @param int $delay + * @return int + */ + private function addSymmetricJitter(int $delay): int + { + $jitterMultiplier = 1 + ((mt_rand() / mt_getrandmax()) - 0.5) * self::JITTER_FACTOR; + return (int)($delay * $jitterMultiplier); + } + + /** + * Default exponential backoff delay function. + * + * @return int milliseconds. + */ + private function exponentialDelay(int $retryAttempt): int + { + return 2 ** ($retryAttempt - 1) * $this->baseDelay; + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Client/UrlEncodedApiRequest.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Client/UrlEncodedApiRequest.php new file mode 100644 index 000000000000..dc476a41ca01 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Client/UrlEncodedApiRequest.php @@ -0,0 +1,25 @@ + $headers Additional headers for the request (optional) + * @param array $query Query parameters for the request (optional) + * @param mixed|null $body The form-urlencoded request body (optional) + */ + public function __construct( + string $baseUrl, + string $path, + HttpMethod $method, + array $headers = [], + array $query = [], + public readonly mixed $body = null + ) { + parent::__construct($baseUrl, $path, $method, $headers, $query); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonApiRequest.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonApiRequest.php new file mode 100644 index 000000000000..8fdf493606e6 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonApiRequest.php @@ -0,0 +1,28 @@ + $headers Additional headers for the request (optional) + * @param array $query Query parameters for the request (optional) + * @param mixed|null $body The JSON request body (optional) + */ + public function __construct( + string $baseUrl, + string $path, + HttpMethod $method, + array $headers = [], + array $query = [], + public readonly mixed $body = null + ) { + parent::__construct($baseUrl, $path, $method, $headers, $query); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonDecoder.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonDecoder.php new file mode 100644 index 000000000000..2da34087c644 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonDecoder.php @@ -0,0 +1,161 @@ + $type The type definition for deserialization. + * @return mixed[]|array The deserialized array. + * @throws JsonException If the decoded value is not an array. + */ + public static function decodeArray(string $json, array $type): array + { + $decoded = self::decode($json); + if (!is_array($decoded)) { + throw new JsonException("Unexpected non-array json value: $json"); + } + return JsonDeserializer::deserializeArray($decoded, $type); + } + + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } + /** + * Decodes a JSON string and returns a mixed. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded mixed. + * @throws JsonException If the decoded value is not an mixed. + */ + public static function decodeMixed(string $json): mixed + { + return self::decode($json); + } + + /** + * Decodes a JSON string into a PHP value. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded value. + * @throws JsonException If an error occurs during JSON decoding. + */ + public static function decode(string $json): mixed + { + return json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonDeserializer.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonDeserializer.php new file mode 100644 index 000000000000..1a250c614e45 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonDeserializer.php @@ -0,0 +1,218 @@ + $data The array to be deserialized. + * @param array $type The type definition from the annotation. + * @return array The deserialized array. + * @throws JsonException If deserialization fails. + */ + public static function deserializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::deserializeMap($data, $type) + : self::deserializeList($data, $type); + } + + /** + * Deserializes a value based on its type definition. + * + * @param mixed $data The data to deserialize. + * @param mixed $type The type definition. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::deserializeUnion($data, $type); + } + + if (is_array($type)) { + return self::deserializeArray((array)$data, $type); + } + + if (gettype($type) !== "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::deserializeSingleValue($data, $type); + } + + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (\Throwable) { + // Catching Throwable instead of Exception to handle TypeError + // that occurs when assigning null to non-nullable typed properties + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: $type" + ); + } + + /** + * Deserializes a single value based on its expected type. + * + * @param mixed $data The data to deserialize. + * @param string $type The expected type. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if ($type === 'date' && is_string($data)) { + return self::deserializeDate($data); + } + + if ($type === 'datetime' && is_string($data)) { + return self::deserializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && is_array($data)) { + /** @var array $data */ + return self::deserializeObject($data, $type); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP, and because + // floats make come through from json_decoded as integers + if ($type === 'float' && (is_numeric($data))) { + return (float) $data; + } + + // Handle bools as a special case since gettype($data) returns "boolean" for bool values in PHP. + if ($type === 'bool' && is_bool($data)) { + return $data; + } + + if (gettype($data) === $type) { + return $data; + } + + throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); + } + + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement JsonSerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, JsonSerializableType::class)) { + throw new JsonException("$type is not a subclass of JsonSerializableType."); + } + return $type::jsonDeserialize($data); + } + + /** + * Deserializes a map (associative array) with defined key and value types. + * + * @param array $data The associative array to deserialize. + * @param array $type The type definition for the map. + * @return array The deserialized map. + * @throws JsonException If deserialization fails. + */ + private static function deserializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + if ($keyType === null) { + throw new JsonException("Unexpected no key in ArrayType."); + } + $keyType = (string) $keyType; + $valueType = $type[$keyType]; + /** @var array $result */ + $result = []; + + foreach ($data as $key => $item) { + $key = (string) Utils::castKey($key, $keyType); + $result[$key] = self::deserializeValue($item, $valueType); + } + + return $result; + } + + /** + * Deserializes a list (indexed array) with a defined value type. + * + * @param array $data The list to deserialize. + * @param array $type The type definition for the list. + * @return array The deserialized list. + * @throws JsonException If deserialization fails. + */ + private static function deserializeList(array $data, array $type): array + { + $valueType = $type[0]; + /** @var array */ + return array_map(fn ($item) => self::deserializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonEncoder.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonEncoder.php new file mode 100644 index 000000000000..0dbf3fcc9948 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonEncoder.php @@ -0,0 +1,20 @@ + Extra properties from JSON that don't map to class properties */ + private array $__additionalProperties = []; + + /** @var array Properties that have been explicitly set via setter methods */ + private array $__explicitlySetProperties = []; + + /** + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. + */ + public function toJson(): string + { + $serializedObject = $this->jsonSerialize(); + $encoded = JsonEncoder::encode(empty($serializedObject) ? new \stdClass() : $serializedObject); + if (!$encoded) { + throw new Exception("Could not encode type"); + } + return $encoded; + } + + /** + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. + */ + public function jsonSerialize(): array + { + $result = []; + $reflectionClass = new \ReflectionClass($this); + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property); + if ($jsonKey === null) { + continue; + } + $value = $property->getValue($this); + + // Handle DateTime properties + $dateTypeAttr = $property->getAttributes(Date::class)[0] ?? null; + if ($dateTypeAttr && $value instanceof DateTime) { + $dateType = $dateTypeAttr->newInstance()->type; + $value = ($dateType === Date::TYPE_DATE) + ? JsonSerializer::serializeDate($value) + : JsonSerializer::serializeDateTime($value); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + + // Handle arrays with type annotations + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if ($arrayTypeAttr && is_array($value)) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonSerializer::serializeArray($value, $arrayType); + } + + // Handle object (skip stdClass since it's already serialized, e.g. from union processing) + if (is_object($value) && !($value instanceof \stdClass)) { + $value = JsonSerializer::serializeObject($value); + } + + // Include the value if it's not null, OR if it was explicitly set (even to null) + if ($value !== null || array_key_exists($property->getName(), $this->__explicitlySetProperties)) { + $result[$jsonKey] = $value; + } + } + return $result; + } + + /** + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. + */ + public static function fromJson(string $json): static + { + $decodedJson = JsonDecoder::decode($json); + if (!is_array($decodedJson)) { + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + } + /** @var array $decodedJson */ + // static:: (not self::) so subclasses' typed jsonDeserialize overrides are dispatched via late static binding. + return static::jsonDeserialize($decodedJson); + } + + /** + * Deserializes an array into an instance of the calling class. + * + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. + */ + public static function jsonDeserialize(array $data): static + { + $reflectionClass = new \ReflectionClass(static::class); + $constructor = $reflectionClass->getConstructor(); + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + + $args = []; + $properties = []; + $additionalProperties = []; + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property) ?? $property->getName(); + $properties[$jsonKey] = $property; + } + + foreach ($data as $jsonKey => $value) { + if (!isset($properties[$jsonKey])) { + // This JSON key doesn't map to any class property - add it to additionalProperties + $additionalProperties[$jsonKey] = $value; + continue; + } + + $property = $properties[$jsonKey]; + + // Handle Date annotation + $dateTypeAttr = $property->getAttributes(Date::class)[0] ?? null; + if ($dateTypeAttr) { + $dateType = $dateTypeAttr->newInstance()->type; + if (!is_string($value)) { + throw new JsonException("Unexpected non-string type for date."); + } + $value = ($dateType === Date::TYPE_DATE) + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); + } + + // Handle Array annotation + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if (is_array($value) && $arrayTypeAttr) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonDeserializer::deserializeArray($value, $arrayType); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + /** @var array $arrayValue */ + $arrayValue = $value; + $value = JsonDeserializer::deserializeObject($arrayValue, $type->getName()); + } + + $args[$property->getName()] = $value; + } + + // Fill in any missing properties with defaults + foreach ($properties as $property) { + if (!isset($args[$property->getName()])) { + $args[$property->getName()] = $property->hasDefaultValue() ? $property->getDefaultValue() : null; + } + } + + // @phpstan-ignore-next-line + $result = new static($args); + $result->__additionalProperties = $additionalProperties; + return $result; + } + + /** + * Get properties from JSON that weren't mapped to class fields + * @return array + */ + public function getAdditionalProperties(): array + { + return $this->__additionalProperties; + } + + /** + * Mark a property as explicitly set. + * This ensures the property will be included in JSON serialization even if null. + * + * @param string $propertyName The name of the property to mark as explicitly set. + */ + protected function _setField(string $propertyName): void + { + $this->__explicitlySetProperties[$propertyName] = true; + } + + /** + * Retrieves the JSON key associated with a property. + * + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. + */ + private static function getJsonKey(ReflectionProperty $property): ?string + { + $jsonPropertyAttr = $property->getAttributes(JsonProperty::class)[0] ?? null; + return $jsonPropertyAttr?->newInstance()?->name; + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonSerializer.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonSerializer.php new file mode 100644 index 000000000000..849e45cee118 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/JsonSerializer.php @@ -0,0 +1,212 @@ +format(Constant::DateFormat); + } + + /** + * Serializes a DateTime object into a string using the date-time format. + * Normalizes UTC times to use 'Z' suffix instead of '+00:00'. + * + * @param DateTime $date The DateTime object to serialize. + * @return string The serialized date-time string. + */ + public static function serializeDateTime(DateTime $date): string + { + $formatted = $date->format(Constant::DateTimeFormat); + if (str_ends_with($formatted, '+00:00')) { + return substr($formatted, 0, -6) . 'Z'; + } + return $formatted; + } + + /** + * Serializes an array based on type annotations (either a list or map). + * + * @param array $data The array to be serialized. + * @param array $type The type definition from the annotation. + * @return array|\stdClass The serialized array, or stdClass if empty map. + * @throws JsonException If serialization fails. + */ + public static function serializeArray(array $data, array $type): array|\stdClass + { + return Utils::isMapType($type) + ? self::serializeMap($data, $type) + : self::serializeList($data, $type); + } + + /** + * Serializes a value based on its type definition. + * + * @param mixed $data The value to serialize. + * @param mixed $type The type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::serializeUnion($data, $type); + } + + if (is_array($type)) { + return self::serializeArray((array)$data, $type); + } + + if (gettype($type) !== "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::serializeSingleValue($data, $type); + } + + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: $unionType" + ); + } + + /** + * Serializes a single value based on its type. + * + * @param mixed $data The value to serialize. + * @param string $type The expected type. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if (($type === 'date' || $type === 'datetime') && $data instanceof DateTime) { + return $type === 'date' ? self::serializeDate($data) : self::serializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && $data instanceof $type) { + return self::serializeObject($data); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP. + if ($type === 'float' && is_float($data)) { + return $data; + } + + // Handle bools as a special case since gettype($data) returns "boolean" for bool values in PHP. + if ($type === 'bool' && is_bool($data)) { + return $data; + } + + if (gettype($data) === $type) { + return $data; + } + + throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); + } + + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + $result = $data->jsonSerialize(); + if (is_array($result) && empty($result)) { + return new \stdClass(); + } + return $result; + } + + /** + * Serializes a map (associative array) with defined key and value types. + * + * @param array $data The associative array to serialize. + * @param array $type The type definition for the map. + * @return array|\stdClass The serialized map, or stdClass if empty. + * @throws JsonException If serialization fails. + */ + private static function serializeMap(array $data, array $type): array|\stdClass + { + if (empty($data)) { + return new \stdClass(); + } + $keyType = array_key_first($type); + if ($keyType === null) { + throw new JsonException("Unexpected no key in ArrayType."); + } + $keyType = (string) $keyType; + $valueType = $type[$keyType]; + /** @var array $result */ + $result = []; + + foreach ($data as $key => $item) { + $key = (string) Utils::castKey($key, $keyType); + $result[$key] = self::serializeValue($item, $valueType); + } + + return $result; + } + + /** + * Serializes a list (indexed array) where only the value type is defined. + * + * @param array $data The list to serialize. + * @param array $type The type definition for the list. + * @return array The serialized list. + * @throws JsonException If serialization fails. + */ + private static function serializeList(array $data, array $type): array + { + $valueType = $type[0]; + /** @var array */ + return array_map(fn ($item) => self::serializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Json/Utils.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/Utils.php new file mode 100644 index 000000000000..4099b8253005 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Json/Utils.php @@ -0,0 +1,62 @@ + $type The type definition from the annotation. + * @return bool True if the type is a map, false if it's a list. + */ + public static function isMapType(array $type): bool + { + return count($type) === 1 && !array_is_list($type); + } + + /** + * Casts the key to the appropriate type based on the key type. + * + * @param mixed $key The key to be cast. + * @param string $keyType The type to cast the key to ('string', 'integer', 'float'). + * @return int|string The casted key. + * @throws JsonException + */ + public static function castKey(mixed $key, string $keyType): int|string + { + if (!is_scalar($key)) { + throw new JsonException("Key must be a scalar type."); + } + return match ($keyType) { + 'integer' => (int)$key, + // PHP arrays don't support float keys; truncate to int + 'float' => (int)$key, + 'string' => (string)$key, + default => is_int($key) ? $key : (string)$key, + }; + } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartApiRequest.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartApiRequest.php new file mode 100644 index 000000000000..7760366456c8 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartApiRequest.php @@ -0,0 +1,28 @@ + $headers Additional headers for the request (optional) + * @param array $query Query parameters for the request (optional) + * @param ?MultipartFormData $body The multipart form data for the request (optional) + */ + public function __construct( + string $baseUrl, + string $path, + HttpMethod $method, + array $headers = [], + array $query = [], + public readonly ?MultipartFormData $body = null + ) { + parent::__construct($baseUrl, $path, $method, $headers, $query); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartFormData.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartFormData.php new file mode 100644 index 000000000000..911a28b6ad64 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartFormData.php @@ -0,0 +1,58 @@ + + */ + private array $parts = []; + + /** + * Adds a new part to the multipart form data. + * + * @param string $name + * @param string|int|bool|float|StreamInterface $value + * @param ?string $contentType + */ + public function add( + string $name, + string|int|bool|float|StreamInterface $value, + ?string $contentType = null, + ): void { + $headers = $contentType !== null ? ['Content-Type' => $contentType] : null; + $this->addPart( + new MultipartFormDataPart( + name: $name, + value: $value, + headers: $headers, + ) + ); + } + + /** + * Adds a new part to the multipart form data. + * + * @param MultipartFormDataPart $part + */ + public function addPart(MultipartFormDataPart $part): void + { + $this->parts[] = $part; + } + + /** + * Adds all parts to a MultipartStreamBuilder. + * + * @param MultipartStreamBuilder $builder + */ + public function addToBuilder(MultipartStreamBuilder $builder): void + { + foreach ($this->parts as $part) { + $part->addToBuilder($builder); + } + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartFormDataPart.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartFormDataPart.php new file mode 100644 index 000000000000..4db35e58ae37 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Multipart/MultipartFormDataPart.php @@ -0,0 +1,62 @@ + + */ + private ?array $headers; + + /** + * @param string $name + * @param string|bool|float|int|StreamInterface $value + * @param ?string $filename + * @param ?array $headers + */ + public function __construct( + string $name, + string|bool|float|int|StreamInterface $value, + ?string $filename = null, + ?array $headers = null + ) { + $this->name = $name; + $this->contents = $value instanceof StreamInterface ? $value : (string)$value; + $this->filename = $filename; + $this->headers = $headers; + } + + /** + * Adds this part to a MultipartStreamBuilder. + * + * @param MultipartStreamBuilder $builder + */ + public function addToBuilder(MultipartStreamBuilder $builder): void + { + $options = array_filter([ + 'filename' => $this->filename, + 'headers' => $this->headers, + ], fn ($value) => $value !== null); + + $builder->addResource($this->name, $this->contents, $options); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Types/ArrayType.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Types/ArrayType.php new file mode 100644 index 000000000000..a26d29008ec3 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Types/ArrayType.php @@ -0,0 +1,16 @@ + 'valueType'] for maps, or ['valueType'] for lists + */ + public function __construct(public array $type) + { + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Core/Types/Constant.php b/seed/php-sdk/php-global-header-literal-env/src/Core/Types/Constant.php new file mode 100644 index 000000000000..5ac4518cc6d6 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Core/Types/Constant.php @@ -0,0 +1,12 @@ +> The types allowed for this property, which can be strings, arrays, or nested Union types. + */ + public array $types; + + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) + { + $this->types = $types; + } + + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ + public function __toString(): string + { + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Exceptions/SeedApiException.php b/seed/php-sdk/php-global-header-literal-env/src/Exceptions/SeedApiException.php new file mode 100644 index 000000000000..6d0bba7c39b3 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Exceptions/SeedApiException.php @@ -0,0 +1,53 @@ +body = $body; + parent::__construct($message, $statusCode, $previous); + } + + /** + * Returns the body of the response that triggered the exception. + * + * @return mixed + */ + public function getBody(): mixed + { + return $this->body; + } + + /** + * @return string + */ + public function __toString(): string + { + if (empty($this->body)) { + return $this->message . '; Status Code: ' . $this->getCode() . "\n"; + } + return $this->message . '; Status Code: ' . $this->getCode() . '; Body: ' . print_r($this->body, true) . "\n"; + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Exceptions/SeedException.php b/seed/php-sdk/php-global-header-literal-env/src/Exceptions/SeedException.php new file mode 100644 index 000000000000..457035276737 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Exceptions/SeedException.php @@ -0,0 +1,12 @@ +, + * } $options @phpstan-ignore-next-line Property is used in endpoint methods via HttpEndpointGenerator + */ + private array $options; + + /** + * @var RawClient $client + */ + private RawClient $client; + + /** + * @param ?string $token The token to use for authentication. + * @param ?'2026-07-15' $version + * @param ?array{ + * baseUrl?: string, + * client?: ClientInterface, + * maxRetries?: int, + * timeout?: float, + * headers?: array, + * } $options + */ + public function __construct( + ?string $token = null, + ?string $version = null, + ?array $options = null, + ) { + $token ??= $this->getFromEnvOrThrow('SQUARE_TOKEN', 'Please pass in token or set the environment variable SQUARE_TOKEN.'); + $envValue = getenv('VERSION'); + $version ??= ($envValue !== false ? $envValue : '2026-07-15'); + $defaultHeaders = [ + 'Authorization' => "Bearer $token", + 'Square-Version' => $version, + 'X-Fern-Language' => 'PHP', + 'X-Fern-SDK-Name' => 'Seed', + 'X-Fern-SDK-Version' => '0.0.1', + 'User-Agent' => 'seed/seed/0.0.1', + ]; + + $this->options = $options ?? []; + + $this->options['headers'] = array_merge( + $defaultHeaders, + $this->options['headers'] ?? [], + ); + + $this->client = new RawClient( + options: $this->options, + ); + + $this->service = new ServiceClient($this->client, $this->options); + } + + /** + * @param string $env + * @param string $message + * @return string + */ + private function getFromEnvOrThrow(string $env, string $message): string + { + $value = getenv($env); + return $value ? (string) $value : throw new Exception($message); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Service/ServiceClient.php b/seed/php-sdk/php-global-header-literal-env/src/Service/ServiceClient.php new file mode 100644 index 000000000000..2ed7e88c5467 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Service/ServiceClient.php @@ -0,0 +1,102 @@ +, + * } $options @phpstan-ignore-next-line Property is used in endpoint methods via HttpEndpointGenerator + */ + private array $options; + + /** + * @var RawClient $client + */ + private RawClient $client; + + /** + * @param RawClient $client + * @param ?array{ + * baseUrl?: string, + * client?: ClientInterface, + * maxRetries?: int, + * timeout?: float, + * headers?: array, + * } $options + */ + public function __construct( + RawClient $client, + ?array $options = null, + ) { + $this->client = $client; + $this->options = $options ?? []; + } + + /** + * GET request with a literal version header + * + * Example: + * ```php + * $client->service->getWithLiteralVersionHeader(); + * ``` + * + * @param ?array{ + * baseUrl?: string, + * maxRetries?: int, + * timeout?: float, + * headers?: array, + * queryParameters?: array, + * bodyProperties?: array, + * } $options + * @return ?string + * @throws SeedException + * @throws SeedApiException + */ + public function getWithLiteralVersionHeader(?array $options = null): ?string + { + $options = array_merge($this->options, $options ?? []); + try { + $response = $this->client->sendRequest( + new JsonApiRequest( + baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', + path: "version", + method: HttpMethod::GET, + ), + $options, + ); + $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + $json = $response->getBody()->getContents(); + if (empty($json)) { + return null; + } + return JsonDecoder::decodeString($json); + } + } catch (JsonException $e) { + throw new SeedException(message: "Failed to deserialize response: {$e->getMessage()}", previous: $e); + } catch (ClientExceptionInterface $e) { + throw new SeedException(message: $e->getMessage(), previous: $e); + } + throw new SeedApiException( + message: 'API request failed', + statusCode: $statusCode, + body: $response->getBody()->getContents(), + ); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/Utils/File.php b/seed/php-sdk/php-global-header-literal-env/src/Utils/File.php new file mode 100644 index 000000000000..ee2af27b8909 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/Utils/File.php @@ -0,0 +1,129 @@ +filename = $filename; + $this->contentType = $contentType; + $this->stream = $stream; + } + + /** + * Creates a File instance from a filepath. + * + * @param string $filepath + * @param ?string $filename + * @param ?string $contentType + * @return File + * @throws Exception + */ + public static function createFromFilepath( + string $filepath, + ?string $filename = null, + ?string $contentType = null, + ): File { + $resource = @fopen($filepath, 'r'); + if (!$resource) { + throw new Exception("Unable to open file $filepath"); + } + $stream = Psr17FactoryDiscovery::findStreamFactory()->createStreamFromResource($resource); + if (!$stream->isReadable()) { + throw new Exception("File $filepath is not readable"); + } + return new self( + stream: $stream, + filename: $filename ?? basename($filepath), + contentType: $contentType, + ); + } + + /** + * Creates a File instance from a string. + * + * @param string $content + * @param ?string $filename + * @param ?string $contentType + * @return File + */ + public static function createFromString( + string $content, + ?string $filename, + ?string $contentType = null, + ): File { + return new self( + stream: Psr17FactoryDiscovery::findStreamFactory()->createStream($content), + filename: $filename, + contentType: $contentType, + ); + } + + /** + * Maps this File into a multipart form data part. + * + * @param string $name The name of the multipart form data part. + * @param ?string $contentType Overrides the Content-Type associated with the file, if any. + * @return MultipartFormDataPart + */ + public function toMultipartFormDataPart(string $name, ?string $contentType = null): MultipartFormDataPart + { + $contentType ??= $this->contentType; + $headers = $contentType !== null + ? ['Content-Type' => $contentType] + : null; + + return new MultipartFormDataPart( + name: $name, + value: $this->stream, + filename: $this->filename, + headers: $headers, + ); + } + + /** + * Closes the file stream. + */ + public function close(): void + { + $this->stream->close(); + } + + /** + * Destructor to ensure stream is closed. + */ + public function __destruct() + { + try { + $this->close(); + } catch (\Throwable) { + // Swallow errors during garbage collection to avoid fatal errors. + } + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/src/dynamic-snippets/example0/snippet.php b/seed/php-sdk/php-global-header-literal-env/src/dynamic-snippets/example0/snippet.php new file mode 100644 index 000000000000..4da8d88bccd7 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/src/dynamic-snippets/example0/snippet.php @@ -0,0 +1,14 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->service->getWithLiteralVersionHeader(); diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Client/RawClientTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Client/RawClientTest.php new file mode 100644 index 000000000000..4916892c66e4 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Client/RawClientTest.php @@ -0,0 +1,1249 @@ +name = $values['name']; + } + + /** + * @return string + */ + public function getName(): ?string + { + return $this->name; + } +} + +class RawClientTest extends TestCase +{ + private string $baseUrl = 'https://api.example.com'; + private MockHttpClient $mockClient; + private RawClient $rawClient; + + protected function setUp(): void + { + $this->mockClient = new MockHttpClient(); + $this->rawClient = new RawClient(['client' => $this->mockClient, 'maxRetries' => 0]); + } + + /** + * @throws ClientExceptionInterface + */ + public function testHeaders(): void + { + $this->mockClient->append(self::createResponse(200)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET, + ['X-Custom-Header' => 'TestValue'] + ); + + $this->rawClient->sendRequest($request); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals('TestValue', $lastRequest->getHeaderLine('X-Custom-Header')); + } + + /** + * @throws ClientExceptionInterface + */ + public function testQueryParameters(): void + { + $this->mockClient->append(self::createResponse(200)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET, + [], + ['param1' => 'value1', 'param2' => ['a', 'b'], 'param3' => 'true'] + ); + + $this->rawClient->sendRequest($request); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + $this->assertEquals( + 'https://api.example.com/test?param1=value1¶m2=a¶m2=b¶m3=true', + (string)$lastRequest->getUri() + ); + } + + public function testEncodePathParam(): void + { + $this->assertEquals('..%2Fconnections', RawClient::encodePathParam('../connections')); + $this->assertEquals('user%20id%3F', RawClient::encodePathParam('user id?')); + $this->assertEquals('user_1', RawClient::encodePathParam('user_1')); + $this->assertEquals('42', RawClient::encodePathParam(42)); + $this->assertEquals('true', RawClient::encodePathParam(true)); + $this->assertEquals('false', RawClient::encodePathParam(false)); + $this->assertEquals('', RawClient::encodePathParam(null)); + } + + /** + * @throws ClientExceptionInterface + */ + public function testEncodedPathParamDoesNotTraverse(): void + { + $this->mockClient->append(self::createResponse(200)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/users/' . RawClient::encodePathParam('../connections'), + HttpMethod::GET + ); + + $this->rawClient->sendRequest($request); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + $this->assertEquals( + 'https://api.example.com/users/..%2Fconnections', + (string)$lastRequest->getUri() + ); + } + + /** + * @throws ClientExceptionInterface + */ + public function testJsonBody(): void + { + $this->mockClient->append(self::createResponse(200)); + + $body = ['key' => 'value']; + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + [], + [], + $body + ); + + $this->rawClient->sendRequest($request); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals(JsonEncoder::encode($body), (string)$lastRequest->getBody()); + } + + public function testAdditionalHeaders(): void + { + $this->mockClient->append(self::createResponse(200)); + + $body = new JsonRequest([ + 'name' => 'john.doe' + ]); + $headers = [ + 'X-API-Version' => '1.0.0', + ]; + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + $headers, + [], + $body + ); + + $this->rawClient->sendRequest( + $request, + options: [ + 'headers' => [ + 'X-Tenancy' => 'test' + ] + ] + ); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals('1.0.0', $lastRequest->getHeaderLine('X-API-Version')); + $this->assertEquals('test', $lastRequest->getHeaderLine('X-Tenancy')); + } + + public function testOverrideAdditionalHeaders(): void + { + $this->mockClient->append(self::createResponse(200)); + + $body = new JsonRequest([ + 'name' => 'john.doe' + ]); + $headers = [ + 'X-API-Version' => '1.0.0', + ]; + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + $headers, + [], + $body + ); + + $this->rawClient->sendRequest( + $request, + options: [ + 'headers' => [ + 'X-API-Version' => '2.0.0' + ] + ] + ); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals('2.0.0', $lastRequest->getHeaderLine('X-API-Version')); + } + + public function testAdditionalBodyProperties(): void + { + $this->mockClient->append(self::createResponse(200)); + + $body = new JsonRequest([ + 'name' => 'john.doe' + ]); + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + [], + [], + $body + ); + + $this->rawClient->sendRequest( + $request, + options: [ + 'bodyProperties' => [ + 'age' => 42 + ] + ] + ); + + $expectedJson = [ + 'name' => 'john.doe', + 'age' => 42 + ]; + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals(JsonEncoder::encode($expectedJson), (string)$lastRequest->getBody()); + } + + public function testOverrideAdditionalBodyProperties(): void + { + $this->mockClient->append(self::createResponse(200)); + + $body = [ + 'name' => 'john.doe' + ]; + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + [], + [], + $body + ); + + $this->rawClient->sendRequest( + $request, + options: [ + 'bodyProperties' => [ + 'name' => 'jane.doe' + ] + ] + ); + + $expectedJson = [ + 'name' => 'jane.doe', + ]; + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals(JsonEncoder::encode($expectedJson), (string)$lastRequest->getBody()); + } + + public function testAdditionalQueryParameters(): void + { + $this->mockClient->append(self::createResponse(200)); + + $query = ['key' => 'value']; + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + [], + $query, + [] + ); + + $this->rawClient->sendRequest( + $request, + options: [ + 'queryParameters' => [ + 'extra' => 42 + ] + ] + ); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals('key=value&extra=42', $lastRequest->getUri()->getQuery()); + } + + public function testOverrideQueryParameters(): void + { + $this->mockClient->append(self::createResponse(200)); + + $query = ['key' => 'invalid']; + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + [], + $query, + [] + ); + + $this->rawClient->sendRequest( + $request, + options: [ + 'queryParameters' => [ + 'key' => 'value' + ] + ] + ); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals('key=value', $lastRequest->getUri()->getQuery()); + } + + public function testDefaultRetries(): void + { + $this->mockClient->append(self::createResponse(500)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET + ); + + $response = $this->rawClient->sendRequest($request); + $this->assertEquals(500, $response->getStatusCode()); + $this->assertEquals(0, $this->mockClient->count()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testExplicitRetriesSuccess(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append(self::createResponse(500), self::createResponse(500), self::createResponse(200)); + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: 2, + sleepFunction: function (int $_microseconds): void { + }, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $response = $retryClient->sendRequest($request); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(0, $mockClient->count()); + } + + public function testExplicitRetriesFailure(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append(self::createResponse(500), self::createResponse(500), self::createResponse(500)); + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: 2, + sleepFunction: function (int $_microseconds): void { + }, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $response = $retryClient->sendRequest($request); + + $this->assertEquals(500, $response->getStatusCode()); + $this->assertEquals(0, $mockClient->count()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testShouldRetryOnStatusCodes(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append( + self::createResponse(408), + self::createResponse(429), + self::createResponse(500), + self::createResponse(501), + self::createResponse(502), + self::createResponse(503), + self::createResponse(504), + self::createResponse(505), + self::createResponse(599), + self::createResponse(200), + ); + $countOfErrorRequests = $mockClient->count() - 1; + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: $countOfErrorRequests, + sleepFunction: function (int $_microseconds): void { + }, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $response = $retryClient->sendRequest($request); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(0, $mockClient->count()); + } + + public function testShouldFailOn400Response(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append(self::createResponse(400), self::createResponse(200)); + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: 2, + sleepFunction: function (int $_microseconds): void { + }, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $response = $retryClient->sendRequest($request); + + $this->assertEquals(400, $response->getStatusCode()); + $this->assertEquals(1, $mockClient->count()); + } + + public function testRetryAfterSecondsHeaderControlsDelay(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append( + self::createResponse(503, ['Retry-After' => '10']), + self::createResponse(200), + ); + + $capturedDelays = []; + $sleepFunction = function (int $microseconds) use (&$capturedDelays): void { + $capturedDelays[] = (int) ($microseconds / 1000); // Convert microseconds to milliseconds + }; + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: 2, + baseDelay: 1000, + sleepFunction: $sleepFunction, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $retryClient->sendRequest($request); + + $this->assertCount(1, $capturedDelays); + $this->assertGreaterThanOrEqual(10000, $capturedDelays[0]); + $this->assertLessThanOrEqual(12000, $capturedDelays[0]); + } + + public function testRetryAfterHttpDateHeaderIsHandled(): void + { + $retryAfterDate = gmdate('D, d M Y H:i:s \G\M\T', time() + 5); + + $mockClient = new MockHttpClient(); + $mockClient->append( + self::createResponse(503, ['Retry-After' => $retryAfterDate]), + self::createResponse(200), + ); + + $capturedDelays = []; + $sleepFunction = function (int $microseconds) use (&$capturedDelays): void { + $capturedDelays[] = (int) ($microseconds / 1000); + }; + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: 2, + baseDelay: 1000, + sleepFunction: $sleepFunction, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $retryClient->sendRequest($request); + + $this->assertCount(1, $capturedDelays); + $this->assertGreaterThan(0, $capturedDelays[0]); + $this->assertLessThanOrEqual(60000, $capturedDelays[0]); + } + + public function testRateLimitResetHeaderControlsDelay(): void + { + $resetTime = (int) floor(microtime(true)) + 5; + + $mockClient = new MockHttpClient(); + $mockClient->append( + self::createResponse(429, ['X-RateLimit-Reset' => (string) $resetTime]), + self::createResponse(200), + ); + + $capturedDelays = []; + $sleepFunction = function (int $microseconds) use (&$capturedDelays): void { + $capturedDelays[] = (int) ($microseconds / 1000); + }; + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: 2, + baseDelay: 1000, + sleepFunction: $sleepFunction, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $retryClient->sendRequest($request); + + $this->assertCount(1, $capturedDelays); + $this->assertGreaterThan(0, $capturedDelays[0]); + $this->assertLessThanOrEqual(60000, $capturedDelays[0]); + } + + public function testRateLimitResetHeaderRespectsMaxDelayAndPositiveJitter(): void + { + $resetTime = (int) floor(microtime(true)) + 1000; + + $mockClient = new MockHttpClient(); + $mockClient->append( + self::createResponse(429, ['X-RateLimit-Reset' => (string) $resetTime]), + self::createResponse(200), + ); + + $capturedDelays = []; + $sleepFunction = function (int $microseconds) use (&$capturedDelays): void { + $capturedDelays[] = (int) ($microseconds / 1000); + }; + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: 1, + baseDelay: 1000, + sleepFunction: $sleepFunction, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $retryClient->sendRequest($request); + + $this->assertCount(1, $capturedDelays); + $this->assertGreaterThanOrEqual(60000, $capturedDelays[0]); + $this->assertLessThanOrEqual(72000, $capturedDelays[0]); + } + + public function testExponentialBackoffWithSymmetricJitterWhenNoHeaders(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append( + self::createResponse(503), + self::createResponse(200), + ); + + $capturedDelays = []; + $sleepFunction = function (int $microseconds) use (&$capturedDelays): void { + $capturedDelays[] = (int) ($microseconds / 1000); + }; + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: 1, + baseDelay: 1000, + sleepFunction: $sleepFunction, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $retryClient->sendRequest($request); + + $this->assertCount(1, $capturedDelays); + $this->assertGreaterThanOrEqual(900, $capturedDelays[0]); + $this->assertLessThanOrEqual(1100, $capturedDelays[0]); + } + + public function testRetryAfterHeaderTakesPrecedenceOverRateLimitReset(): void + { + $resetTime = (int) floor(microtime(true)) + 30; + + $mockClient = new MockHttpClient(); + $mockClient->append( + self::createResponse(503, [ + 'Retry-After' => '5', + 'X-RateLimit-Reset' => (string) $resetTime, + ]), + self::createResponse(200), + ); + + $capturedDelays = []; + $sleepFunction = function (int $microseconds) use (&$capturedDelays): void { + $capturedDelays[] = (int) ($microseconds / 1000); + }; + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: 2, + baseDelay: 1000, + sleepFunction: $sleepFunction, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $retryClient->sendRequest($request); + + $this->assertCount(1, $capturedDelays); + $this->assertGreaterThanOrEqual(5000, $capturedDelays[0]); + $this->assertLessThanOrEqual(6000, $capturedDelays[0]); + } + + public function testMaxDelayCapIsApplied(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append( + self::createResponse(503, ['Retry-After' => '120']), + self::createResponse(200), + ); + + $capturedDelays = []; + $sleepFunction = function (int $microseconds) use (&$capturedDelays): void { + $capturedDelays[] = (int) ($microseconds / 1000); + }; + + $retryClient = new RetryDecoratingClient( + $mockClient, + maxRetries: 2, + baseDelay: 1000, + sleepFunction: $sleepFunction, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $retryClient->sendRequest($request); + + $this->assertCount(1, $capturedDelays); + $this->assertGreaterThanOrEqual(60000, $capturedDelays[0]); + $this->assertLessThanOrEqual(72000, $capturedDelays[0]); + } + + public function testMultipartContentTypeIncludesBoundary(): void + { + $this->mockClient->append(self::createResponse(200)); + + $formData = new MultipartFormData(); + $formData->add('field', 'value'); + + $request = new MultipartApiRequest( + $this->baseUrl, + '/upload', + HttpMethod::POST, + [], + [], + $formData, + ); + + $this->rawClient->sendRequest($request); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + + $contentType = $lastRequest->getHeaderLine('Content-Type'); + $this->assertStringStartsWith('multipart/form-data; boundary=', $contentType); + + $boundary = substr($contentType, strlen('multipart/form-data; boundary=')); + $body = (string) $lastRequest->getBody(); + $this->assertStringContainsString("--{$boundary}\r\n", $body); + $this->assertStringContainsString("Content-Disposition: form-data; name=\"field\"\r\n", $body); + $this->assertStringContainsString("value", $body); + $this->assertStringContainsString("--{$boundary}--\r\n", $body); + } + + public function testMultipartWithFilename(): void + { + $this->mockClient->append(self::createResponse(200)); + + $formData = new MultipartFormData(); + $formData->addPart(new MultipartFormDataPart( + name: 'document', + value: 'file-contents', + filename: 'report.pdf', + headers: ['Content-Type' => 'application/pdf'], + )); + + $request = new MultipartApiRequest( + $this->baseUrl, + '/upload', + HttpMethod::POST, + [], + [], + $formData, + ); + + $this->rawClient->sendRequest($request); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + + $body = (string) $lastRequest->getBody(); + $this->assertStringContainsString( + 'Content-Disposition: form-data; name="document"; filename="report.pdf"', + $body, + ); + $this->assertStringContainsString('Content-Type: application/pdf', $body); + $this->assertStringContainsString('file-contents', $body); + } + + public function testMultipartWithMultipleParts(): void + { + $this->mockClient->append(self::createResponse(200)); + + $formData = new MultipartFormData(); + $formData->add('name', 'John'); + $formData->add('age', 30); + $formData->addPart(new MultipartFormDataPart( + name: 'avatar', + value: 'image-data', + filename: 'avatar.png', + )); + + $request = new MultipartApiRequest( + $this->baseUrl, + '/profile', + HttpMethod::POST, + [], + [], + $formData, + ); + + $this->rawClient->sendRequest($request); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + + $body = (string) $lastRequest->getBody(); + $this->assertStringContainsString('name="name"', $body); + $this->assertStringContainsString('John', $body); + $this->assertStringContainsString('name="age"', $body); + $this->assertStringContainsString('30', $body); + $this->assertStringContainsString('name="avatar"; filename="avatar.png"', $body); + $this->assertStringContainsString('image-data', $body); + } + + public function testMultipartDoesNotIncludeJsonContentType(): void + { + $this->mockClient->append(self::createResponse(200)); + + $formData = new MultipartFormData(); + $formData->add('field', 'value'); + + $request = new MultipartApiRequest( + $this->baseUrl, + '/upload', + HttpMethod::POST, + [], + [], + $formData, + ); + + $this->rawClient->sendRequest($request); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + + $contentType = $lastRequest->getHeaderLine('Content-Type'); + $this->assertStringStartsWith('multipart/form-data; boundary=', $contentType); + $this->assertStringNotContainsString('application/json', $contentType); + } + + public function testMultipartNullBodySendsNoBody(): void + { + $this->mockClient->append(self::createResponse(200)); + + $request = new MultipartApiRequest( + $this->baseUrl, + '/upload', + HttpMethod::POST, + ); + + $this->rawClient->sendRequest($request); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + + $this->assertEquals('', (string) $lastRequest->getBody()); + $this->assertStringNotContainsString('multipart/form-data', $lastRequest->getHeaderLine('Content-Type')); + } + + public function testJsonNullBodySendsNoBody(): void + { + $this->mockClient->append(self::createResponse(200)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + ); + + $this->rawClient->sendRequest($request); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + + $this->assertEquals('', (string) $lastRequest->getBody()); + } + + public function testEmptyJsonBodySerializesAsObject(): void + { + $this->mockClient->append(self::createResponse(200)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + [], + [], + ['key' => 'value'], + ); + + $this->rawClient->sendRequest( + $request, + options: [ + 'bodyProperties' => [ + 'key' => 'value', + ], + ], + ); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + + // When bodyProperties override all keys, the merged result should still + // serialize as a JSON object {}, not an array []. + $decoded = json_decode((string) $lastRequest->getBody(), true); + $this->assertIsArray($decoded); + $this->assertEquals('value', $decoded['key']); + } + + public function testAuthHeadersAreIncluded(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append(self::createResponse(200)); + + $rawClient = new RawClient([ + 'client' => $mockClient, + 'maxRetries' => 0, + 'getAuthHeaders' => fn () => ['Authorization' => 'Bearer test-token'], + ]); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET, + ); + + $rawClient->sendRequest($request); + + $lastRequest = $mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + + $this->assertEquals('Bearer test-token', $lastRequest->getHeaderLine('Authorization')); + } + + public function testAuthHeadersAreIncludedInMultipart(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append(self::createResponse(200)); + + $rawClient = new RawClient([ + 'client' => $mockClient, + 'maxRetries' => 0, + 'getAuthHeaders' => fn () => ['Authorization' => 'Bearer test-token'], + ]); + + $formData = new MultipartFormData(); + $formData->add('field', 'value'); + + $request = new MultipartApiRequest( + $this->baseUrl, + '/upload', + HttpMethod::POST, + [], + [], + $formData, + ); + + $rawClient->sendRequest($request); + + $lastRequest = $mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + + $this->assertEquals('Bearer test-token', $lastRequest->getHeaderLine('Authorization')); + $this->assertStringStartsWith('multipart/form-data; boundary=', $lastRequest->getHeaderLine('Content-Type')); + } + + /** + * Creates a PSR-7 response using discovery, without depending on any specific implementation. + * + * @param int $statusCode + * @param array $headers + * @param string $body + * @return ResponseInterface + */ + private static function createResponse( + int $statusCode = 200, + array $headers = [], + string $body = '', + ): ResponseInterface { + $response = \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse($statusCode); + foreach ($headers as $name => $value) { + $response = $response->withHeader($name, $value); + } + if ($body !== '') { + $response = $response->withBody( + \Http\Discovery\Psr17FactoryDiscovery::findStreamFactory() + ->createStream($body), + ); + } + return $response; + } + + + public function testTimeoutOptionIsAccepted(): void + { + $this->mockClient->append(self::createResponse(200)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET, + ); + + // MockHttpClient is not Guzzle/Symfony, so a warning is triggered once. + set_error_handler(static function (int $errno, string $errstr): bool { + return $errno === E_USER_WARNING + && str_contains($errstr, 'Timeout option is not supported'); + }); + + try { + $response = $this->rawClient->sendRequest( + $request, + options: [ + 'timeout' => 3.0 + ] + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $lastRequest = $this->mockClient->getLastRequest(); + $this->assertInstanceOf(RequestInterface::class, $lastRequest); + } finally { + restore_error_handler(); + } + } + + public function testClientLevelTimeoutIsAccepted(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append(self::createResponse(200)); + + $rawClient = new RawClient([ + 'client' => $mockClient, + 'maxRetries' => 0, + 'timeout' => 5.0, + ]); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET, + ); + + set_error_handler(static function (int $errno, string $errstr): bool { + return $errno === E_USER_WARNING + && str_contains($errstr, 'Timeout option is not supported'); + }); + + try { + $response = $rawClient->sendRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + } finally { + restore_error_handler(); + } + } + + public function testPerRequestTimeoutOverridesClientTimeout(): void + { + $mockClient = new MockHttpClient(); + $mockClient->append(self::createResponse(200)); + + $rawClient = new RawClient([ + 'client' => $mockClient, + 'maxRetries' => 0, + 'timeout' => 5.0, + ]); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET, + ); + + set_error_handler(static function (int $errno, string $errstr): bool { + return $errno === E_USER_WARNING + && str_contains($errstr, 'Timeout option is not supported'); + }); + + try { + $response = $rawClient->sendRequest( + $request, + options: [ + 'timeout' => 1.0 + ] + ); + + $this->assertEquals(200, $response->getStatusCode()); + } finally { + restore_error_handler(); + } + } + + public function testDiscoveryFindsHttpClient(): void + { + // HttpClientBuilder::build() with no client arg uses Psr18ClientDiscovery. + $client = HttpClientBuilder::build(); + $this->assertInstanceOf(\Psr\Http\Client\ClientInterface::class, $client); + } + + public function testDiscoveryFindsFactories(): void + { + $requestFactory = HttpClientBuilder::requestFactory(); + $this->assertInstanceOf(\Psr\Http\Message\RequestFactoryInterface::class, $requestFactory); + + $streamFactory = HttpClientBuilder::streamFactory(); + $this->assertInstanceOf(\Psr\Http\Message\StreamFactoryInterface::class, $streamFactory); + + // Verify they produce usable objects + $request = $requestFactory->createRequest('GET', 'https://example.com'); + $this->assertEquals('GET', $request->getMethod()); + + $stream = $streamFactory->createStream('hello'); + $this->assertEquals('hello', (string) $stream); + } + + public function testInterfaceExistsDetectsGuzzle(): void + { + $this->assertTrue( + interface_exists('GuzzleHttp\ClientInterface'), + 'interface_exists should detect GuzzleHttp\ClientInterface when Guzzle is installed', + ); + } + + public function testTimeoutForwardsToGuzzleSend(): void + { + $expectedResponse = self::createResponse(200); + + $guzzleClient = new class ($expectedResponse) implements \Psr\Http\Client\ClientInterface, \GuzzleHttp\ClientInterface { + private ResponseInterface $response; + /** @var array */ + public array $lastOptions = []; + + public function __construct(ResponseInterface $response) + { + $this->response = $response; + } + + /** @param array $options */ + public function send(\Psr\Http\Message\RequestInterface $request, array $options = []): ResponseInterface + { + $this->lastOptions = $options; + return $this->response; + } + + /** @param array $options */ + public function sendAsync(\Psr\Http\Message\RequestInterface $request, array $options = []): \GuzzleHttp\Promise\PromiseInterface + { + throw new \RuntimeException('Not implemented'); + } + + /** @param array $options */ + public function request(string $method, $uri, array $options = []): ResponseInterface + { + throw new \RuntimeException('Not implemented'); + } + + /** @param array $options */ + public function requestAsync(string $method, $uri, array $options = []): \GuzzleHttp\Promise\PromiseInterface + { + throw new \RuntimeException('Not implemented'); + } + + public function getConfig(?string $option = null) + { + return null; + } + + public function sendRequest(\Psr\Http\Message\RequestInterface $request): ResponseInterface + { + return $this->response; + } + }; + + $retryClient = new RetryDecoratingClient( + $guzzleClient, + maxRetries: 0, + sleepFunction: function (int $_microseconds): void { + }, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $response = $retryClient->send($request, timeout: 5.0); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertArrayHasKey('timeout', $guzzleClient->lastOptions); + $this->assertEquals(5.0, $guzzleClient->lastOptions['timeout']); + } + + public function testNoTimeoutDoesNotCallGuzzleSend(): void + { + $expectedResponse = self::createResponse(200); + + $guzzleClient = new class ($expectedResponse) implements \Psr\Http\Client\ClientInterface, \GuzzleHttp\ClientInterface { + private ResponseInterface $response; + public bool $sendCalled = false; + + public function __construct(ResponseInterface $response) + { + $this->response = $response; + } + + /** @param array $options */ + public function send(\Psr\Http\Message\RequestInterface $request, array $options = []): ResponseInterface + { + $this->sendCalled = true; + return $this->response; + } + + /** @param array $options */ + public function sendAsync(\Psr\Http\Message\RequestInterface $request, array $options = []): \GuzzleHttp\Promise\PromiseInterface + { + throw new \RuntimeException('Not implemented'); + } + + /** @param array $options */ + public function request(string $method, $uri, array $options = []): ResponseInterface + { + throw new \RuntimeException('Not implemented'); + } + + /** @param array $options */ + public function requestAsync(string $method, $uri, array $options = []): \GuzzleHttp\Promise\PromiseInterface + { + throw new \RuntimeException('Not implemented'); + } + + public function getConfig(?string $option = null) + { + return null; + } + + public function sendRequest(\Psr\Http\Message\RequestInterface $request): ResponseInterface + { + return $this->response; + } + }; + + $retryClient = new RetryDecoratingClient( + $guzzleClient, + maxRetries: 0, + sleepFunction: function (int $_microseconds): void { + }, + ); + + $requestFactory = \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(); + $request = $requestFactory->createRequest('GET', $this->baseUrl . '/test'); + + $response = $retryClient->send($request, timeout: null); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertFalse($guzzleClient->sendCalled, 'Guzzle send() should not be called when timeout is null'); + } + +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/AdditionalPropertiesTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/AdditionalPropertiesTest.php new file mode 100644 index 000000000000..2c32002340e7 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/AdditionalPropertiesTest.php @@ -0,0 +1,76 @@ +name; + } + + /** + * @return string|null + */ + public function getEmail(): ?string + { + return $this->email; + } + + /** + * @param array{ + * name: string, + * email?: string|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->name = $values['name']; + $this->email = $values['email'] ?? null; + } +} + +class AdditionalPropertiesTest extends TestCase +{ + public function testExtraProperties(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'name' => 'john.doe', + 'email' => 'john.doe@example.com', + 'age' => 42 + ], + ); + + $person = Person::fromJson($expectedJson); + $this->assertEquals('john.doe', $person->getName()); + $this->assertEquals('john.doe@example.com', $person->getEmail()); + $this->assertEquals( + [ + 'age' => 42 + ], + $person->getAdditionalProperties(), + ); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/DateArrayTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/DateArrayTest.php new file mode 100644 index 000000000000..e7794d652432 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/DateArrayTest.php @@ -0,0 +1,54 @@ +dates = $values['dates']; + } +} + +class DateArrayTest extends TestCase +{ + public function testDateTimeInArrays(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'dates' => ['2023-01-01', '2023-02-01', '2023-03-01'] + ], + ); + + $object = DateArray::fromJson($expectedJson); + $this->assertInstanceOf(DateTime::class, $object->dates[0], 'dates[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dates[0]->format('Y-m-d'), 'dates[0] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[1], 'dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-02-01', $object->dates[1]->format('Y-m-d'), 'dates[1] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[2], 'dates[2] should be a DateTime instance.'); + $this->assertEquals('2023-03-01', $object->dates[2]->format('Y-m-d'), 'dates[2] should have the correct date.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for dates array.'); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EmptyArrayTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EmptyArrayTest.php new file mode 100644 index 000000000000..58099e4e3dcc --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EmptyArrayTest.php @@ -0,0 +1,78 @@ + $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; + } +} + +class EmptyArrayTest extends TestCase +{ + public function testEmptyArray(): void + { + $inputJson = JsonEncoder::encode( + [ + 'empty_string_array' => [], + 'empty_map_array' => [], + 'empty_dates_array' => [] + ], + ); + + $object = EmptyArray::fromJson($inputJson); + $this->assertEmpty($object->emptyStringArray, 'empty_string_array should be empty.'); + $this->assertEmpty($object->emptyMapArray, 'empty_map_array should be empty.'); + $this->assertEmpty($object->emptyDatesArray, 'empty_dates_array should be empty.'); + + $actualJson = $object->toJson(); + $expectedJson = JsonEncoder::encode( + [ + 'empty_string_array' => [], + 'empty_map_array' => new \stdClass(), + 'empty_dates_array' => [] + ], + ); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match expected JSON for EmptyArraysType.'); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EmptyObjectTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EmptyObjectTest.php new file mode 100644 index 000000000000..d97af8833be8 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EmptyObjectTest.php @@ -0,0 +1,409 @@ +optionalField = $values['optionalField'] ?? null; + } +} + +class EmptyObjectWithNestedObject extends JsonSerializableType +{ + /** + * @var string $name + */ + #[JsonProperty('name')] + public string $name; + + /** + * @var EmptyObject $nested + */ + #[JsonProperty('nested')] + public EmptyObject $nested; + + /** + * @param array{ + * name: string, + * nested: EmptyObject, + * } $values + */ + public function __construct( + array $values, + ) { + $this->name = $values['name']; + $this->nested = $values['nested']; + } +} + +class ObjectWithEmptyMap extends JsonSerializableType +{ + /** + * @var array $metadata + */ + #[JsonProperty('metadata'), ArrayType(['string' => 'string'])] + public array $metadata; + + /** + * @param array{ + * metadata: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->metadata = $values['metadata']; + } +} + +class ObjectWithListAndMap extends JsonSerializableType +{ + /** + * @var string[] $list + */ + #[ArrayType(['string'])] + #[JsonProperty('list')] + public array $list; + + /** + * @var array $map + */ + #[ArrayType(['string' => 'string'])] + #[JsonProperty('map')] + public array $map; + + /** + * @param array{ + * list: string[], + * map: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->list = $values['list']; + $this->map = $values['map']; + } +} + +class EmptyUnionMember extends JsonSerializableType +{ + /** + * @var string|null $tag + */ + #[JsonProperty('tag')] + public ?string $tag; + + /** + * @param array{ + * tag?: string|null, + * } $values + */ + public function __construct( + array $values = [], + ) { + $this->tag = $values['tag'] ?? null; + } +} + +class ObjectWithUnionEmpty extends JsonSerializableType +{ + /** + * @var string|EmptyUnionMember|null $value + */ + #[Union('string', EmptyUnionMember::class, 'null')] + #[JsonProperty('value')] + public mixed $value; + + /** + * @param array{ + * value: string|EmptyUnionMember|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->value = $values['value']; + } +} + +class ObjectWithListOfMaps extends JsonSerializableType +{ + /** + * @var array> $items + */ + #[ArrayType([['string' => 'string']])] + #[JsonProperty('items')] + public array $items; + + /** + * @param array{ + * items: array>, + * } $values + */ + public function __construct( + array $values, + ) { + $this->items = $values['items']; + } +} + +class ObjectWithMapOfObjects extends JsonSerializableType +{ + /** + * @var array $entries + */ + #[ArrayType(['string' => EmptyUnionMember::class])] + #[JsonProperty('entries')] + public array $entries; + + /** + * @param array{ + * entries: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->entries = $values['entries']; + } +} + +class ObjectWithAdditionalOnly extends JsonSerializableType +{ + /** + * @var string|null $name + */ + #[JsonProperty('name')] + public ?string $name; + + /** + * @param array{ + * name?: string|null, + * } $values + */ + public function __construct( + array $values = [], + ) { + $this->name = $values['name'] ?? null; + } +} + +class EmptyObjectTest extends TestCase +{ + public function testEmptyObjectSerializesToObject(): void + { + $object = new EmptyObject([]); + $json = $object->toJson(); + $this->assertEquals('{}', $json, 'Empty object should serialize to {} not [].'); + } + + public function testEmptyObjectWithFieldSetSerializesCorrectly(): void + { + $object = new EmptyObject(['optionalField' => 'value']); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString('{"optional_field": "value"}', $json); + } + + public function testNestedEmptyObjectSerializesToObject(): void + { + $parent = new EmptyObjectWithNestedObject([ + 'name' => 'test', + 'nested' => new EmptyObject([]), + ]); + $json = $parent->toJson(); + $expected = '{"name": "test", "nested": {}}'; + $this->assertJsonStringEqualsJsonString($expected, $json, 'Nested empty object should serialize to {} not [].'); + } + + public function testDeserializeEmptyObject(): void + { + $json = '{}'; + $object = EmptyObject::fromJson($json); + $this->assertNull($object->optionalField); + $this->assertEquals('{}', $object->toJson(), 'Deserialized empty object should re-serialize to {}.'); + } + + public function testEmptyMapSerializesToObject(): void + { + $object = new ObjectWithEmptyMap(['metadata' => []]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString('{"metadata":{}}', $json, 'Empty map should serialize to {} not [].'); + } + + public function testNonEmptyMapSerializesCorrectly(): void + { + $object = new ObjectWithEmptyMap(['metadata' => ['key' => 'value']]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString('{"metadata":{"key":"value"}}', $json); + } + + public function testEmptyListAndEmptyMapSideBySide(): void + { + $object = new ObjectWithListAndMap(['list' => [], 'map' => []]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString( + '{"list":[],"map":{}}', + $json, + 'Empty list should serialize as [] and empty map as {} on the same object.' + ); + } + + public function testNonEmptyListAndEmptyMap(): void + { + $object = new ObjectWithListAndMap(['list' => ['a', 'b'], 'map' => []]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString( + '{"list":["a","b"],"map":{}}', + $json + ); + } + + public function testEmptyListAndNonEmptyMap(): void + { + $object = new ObjectWithListAndMap(['list' => [], 'map' => ['key' => 'val']]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString( + '{"list":[],"map":{"key":"val"}}', + $json + ); + } + + public function testUnionWithEmptyObject(): void + { + $object = new ObjectWithUnionEmpty(['value' => new EmptyUnionMember([])]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString( + '{"value":{}}', + $json, + 'Union containing an empty object should serialize to {} via the stdClass guard path.' + ); + } + + public function testUnionWithNonEmptyObject(): void + { + $object = new ObjectWithUnionEmpty(['value' => new EmptyUnionMember(['tag' => 'hello'])]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString( + '{"value":{"tag":"hello"}}', + $json + ); + } + + public function testUnionWithStringFallback(): void + { + $object = new ObjectWithUnionEmpty(['value' => 'plain string']); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString( + '{"value":"plain string"}', + $json + ); + } + + public function testListOfEmptyMaps(): void + { + $object = new ObjectWithListOfMaps(['items' => [[], []]]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString( + '{"items":[{},{}]}', + $json, + 'A list of empty maps should produce [{},{}] — each empty map is {} but the list stays [].' + ); + } + + public function testListOfMixedMaps(): void + { + $object = new ObjectWithListOfMaps(['items' => [['a' => 'b'], [], ['c' => 'd']]]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString( + '{"items":[{"a":"b"},{},{"c":"d"}]}', + $json + ); + } + + public function testMapOfEmptyObjects(): void + { + $object = new ObjectWithMapOfObjects([ + 'entries' => [ + 'first' => new EmptyUnionMember([]), + 'second' => new EmptyUnionMember([]), + ] + ]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString( + '{"entries":{"first":{},"second":{}}}', + $json, + 'A map whose values are empty objects should produce {"a":{},"b":{}}.' + ); + } + + public function testMapOfMixedObjects(): void + { + $object = new ObjectWithMapOfObjects([ + 'entries' => [ + 'empty' => new EmptyUnionMember([]), + 'filled' => new EmptyUnionMember(['tag' => 'present']), + ] + ]); + $json = $object->toJson(); + $this->assertJsonStringEqualsJsonString( + '{"entries":{"empty":{},"filled":{"tag":"present"}}}', + $json + ); + } + + public function testEmptyObjectWithAdditionalPropertiesOnly(): void + { + $json = '{"extra_key":"extra_value"}'; + $object = ObjectWithAdditionalOnly::fromJson($json); + $this->assertNull($object->name); + $this->assertEquals(['extra_key' => 'extra_value'], $object->getAdditionalProperties()); + + $reserialized = $object->toJson(); + $this->assertEquals('{}', $reserialized, 'Object with only additional properties (no declared fields set) should serialize to {}.'); + } + + public function testEmptyObjectNoAdditionalProperties(): void + { + $object = new ObjectWithAdditionalOnly([]); + $json = $object->toJson(); + $this->assertEquals('{}', $json, 'Object with all null properties and no additional properties should serialize to {}.'); + } + + public function testRoundTripListAndMap(): void + { + $inputJson = '{"list":[],"map":{}}'; + $object = ObjectWithListAndMap::fromJson($inputJson); + $this->assertEmpty($object->list); + $this->assertEmpty($object->map); + $this->assertJsonStringEqualsJsonString($inputJson, $object->toJson()); + } + + public function testRoundTripUnionWithEmptyObject(): void + { + $inputJson = '{"value":{}}'; + $object = ObjectWithUnionEmpty::fromJson($inputJson); + $this->assertInstanceOf(EmptyUnionMember::class, $object->value); + $this->assertJsonStringEqualsJsonString($inputJson, $object->toJson()); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EnumTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EnumTest.php new file mode 100644 index 000000000000..72dc6f2cfa00 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/EnumTest.php @@ -0,0 +1,77 @@ +value; + } +} + +class ShapeType extends JsonSerializableType +{ + /** + * @var Shape $shape + */ + #[JsonProperty('shape')] + public Shape $shape; + + /** + * @var Shape[] $shapes + */ + #[ArrayType([Shape::class])] + #[JsonProperty('shapes')] + public array $shapes; + + /** + * @param Shape $shape + * @param Shape[] $shapes + */ + public function __construct( + Shape $shape, + array $shapes, + ) { + $this->shape = $shape; + $this->shapes = $shapes; + } +} + +class EnumTest extends TestCase +{ + public function testEnumSerialization(): void + { + $object = new ShapeType( + Shape::Circle, + [Shape::Square, Shape::Circle, Shape::Triangle] + ); + + $expectedJson = JsonEncoder::encode([ + 'shape' => 'CIRCLE', + 'shapes' => ['SQUARE', 'CIRCLE', 'TRIANGLE'] + ]); + + $actualJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString( + $expectedJson, + $actualJson, + 'Serialized JSON does not match expected JSON for shape and shapes properties.' + ); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/ExhaustiveTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/ExhaustiveTest.php new file mode 100644 index 000000000000..4c288378b48b --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/ExhaustiveTest.php @@ -0,0 +1,197 @@ +nestedProperty = $values['nestedProperty']; + } +} + +class Type extends JsonSerializableType +{ + /** + * @var Nested nestedType + */ + #[JsonProperty('nested_type')] + public Nested $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[Date(Date::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[Date(Date::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(Nested::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: Nested, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; + } +} + +class ExhaustiveTest extends TestCase +{ + /** + * Test serialization and deserialization of all types in Type. + */ + public function testExhaustive(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'nested_type' => ['nested_property' => '1995-07-20'], + 'simple_property' => 'Test String', + // Omit 'nullable_property' to test null serialization + 'date_property' => '2023-01-01', + 'datetime_property' => '2023-01-01T12:34:56Z', + 'string_array' => ['one', 'two', 'three'], + 'map_property' => ['key1' => 1, 'key2' => 2], + 'object_array' => [ + 1 => ['nested_property' => '2021-07-20'], + 2 => null, // Testing nullable objects in array + ], + 'nested_array' => [ + 1 => [1 => 'value1', 2 => null], // Testing nullable strings in nested array + 2 => [3 => 'value3', 4 => 'value4'] + ], + 'dates_array' => ['2023-01-01', null, '2023-03-01'] // Testing nullable dates in array> + ], + ); + + $object = Type::fromJson($expectedJson); + + // Check that nullable property is null and not included in JSON + $this->assertNull($object->nullableProperty, 'Nullable property should be null.'); + + // Check date properties + $this->assertInstanceOf(DateTime::class, $object->dateProperty, 'date_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dateProperty->format('Y-m-d'), 'date_property should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->datetimeProperty, 'datetime_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:34:56', $object->datetimeProperty->format('Y-m-d H:i:s'), 'datetime_property should have the correct datetime.'); + + // Check scalar arrays + $this->assertEquals(['one', 'two', 'three'], $object->stringArray, 'string_array should match the original data.'); + $this->assertEquals(['key1' => 1, 'key2' => 2], $object->mapProperty, 'map_property should match the original data.'); + + // Check object array with nullable elements + $this->assertInstanceOf(Nested::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); + $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); + + // Check nested array with nullable strings + $this->assertEquals('value1', $object->nestedArray[1][1], 'nested_array[1][1] should match the original data.'); + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + $this->assertEquals('value3', $object->nestedArray[2][3], 'nested_array[2][3] should match the original data.'); + $this->assertEquals('value4', $object->nestedArray[2][4], 'nested_array[2][4] should match the original data.'); + + // Check dates array with nullable DateTime objects + $this->assertInstanceOf(DateTime::class, $object->datesArray[0], 'dates_array[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->datesArray[0]->format('Y-m-d'), 'dates_array[0] should have the correct date.'); + $this->assertNull($object->datesArray[1], 'dates_array[1] should be null.'); + $this->assertInstanceOf(DateTime::class, $object->datesArray[2], 'dates_array[2] should be a DateTime instance.'); + $this->assertEquals('2023-03-01', $object->datesArray[2]->format('Y-m-d'), 'dates_array[2] should have the correct date.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'The serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/InvalidTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/InvalidTest.php new file mode 100644 index 000000000000..9d845ea113b8 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/InvalidTest.php @@ -0,0 +1,42 @@ +integerProperty = $values['integerProperty']; + } +} + +class InvalidTest extends TestCase +{ + public function testInvalidJsonThrowsException(): void + { + $this->expectException(\TypeError::class); + $json = JsonEncoder::encode( + [ + 'integer_property' => 'not_an_integer' + ], + ); + Invalid::fromJson($json); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NestedUnionArrayTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NestedUnionArrayTest.php new file mode 100644 index 000000000000..8fbbeb939f02 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NestedUnionArrayTest.php @@ -0,0 +1,89 @@ +nestedProperty = $values['nestedProperty']; + } +} + +class NestedUnionArray extends JsonSerializableType +{ + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(UnionObject::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values + */ + public function __construct( + array $values, + ) { + $this->nestedArray = $values['nestedArray']; + } +} + +class NestedUnionArrayTest extends TestCase +{ + public function testNestedUnionArray(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'nested_array' => [ + 1 => [ + 1 => ['nested_property' => 'Nested One'], + 2 => null, + 4 => '2023-01-02' + ], + 2 => [ + 5 => ['nested_property' => 'Nested Two'], + 7 => '2023-02-02' + ] + ] + ], + ); + + $object = NestedUnionArray::fromJson($expectedJson); + $this->assertInstanceOf(UnionObject::class, $object->nestedArray[1][1], 'nested_array[1][1] should be an instance of Object.'); + $this->assertEquals('Nested One', $object->nestedArray[1][1]->nestedProperty, 'nested_array[1][1]->nestedProperty should match the original data.'); + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + $this->assertInstanceOf(DateTime::class, $object->nestedArray[1][4], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-01-02T00:00:00+00:00', $object->nestedArray[1][4]->format(Constant::DateTimeFormat), 'nested_array[1][4] should have the correct datetime.'); + $this->assertInstanceOf(UnionObject::class, $object->nestedArray[2][5], 'nested_array[2][5] should be an instance of Object.'); + $this->assertEquals('Nested Two', $object->nestedArray[2][5]->nestedProperty, 'nested_array[2][5]->nestedProperty should match the original data.'); + $this->assertInstanceOf(DateTime::class, $object->nestedArray[2][7], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-02-02', $object->nestedArray[2][7]->format('Y-m-d'), 'nested_array[1][4] should have the correct date.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for nested_array.'); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NullPropertyTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NullPropertyTest.php new file mode 100644 index 000000000000..ce20a2442825 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NullPropertyTest.php @@ -0,0 +1,53 @@ +nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; + } +} + +class NullPropertyTest extends TestCase +{ + public function testNullPropertiesAreOmitted(): void + { + $object = new NullProperty( + [ + "nonNullProperty" => "Test String", + "nullProperty" => null + ] + ); + + $serialized = $object->jsonSerialize(); + $this->assertArrayHasKey('non_null_property', $serialized, 'non_null_property should be present in the serialized JSON.'); + $this->assertArrayNotHasKey('null_property', $serialized, 'null_property should be omitted from the serialized JSON.'); + $this->assertEquals('Test String', $serialized['non_null_property'], 'non_null_property should have the correct value.'); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NullableArrayTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NullableArrayTest.php new file mode 100644 index 000000000000..d1749c434a4c --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/NullableArrayTest.php @@ -0,0 +1,49 @@ + $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->nullableStringArray = $values['nullableStringArray']; + } +} + +class NullableArrayTest extends TestCase +{ + public function testNullableArray(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'nullable_string_array' => ['one', null, 'three'] + ], + ); + + $object = NullableArray::fromJson($expectedJson); + $this->assertEquals(['one', null, 'three'], $object->nullableStringArray, 'nullable_string_array should match the original data.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for nullable_string_array.'); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/ScalarTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/ScalarTest.php new file mode 100644 index 000000000000..ad4db0251bb5 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/ScalarTest.php @@ -0,0 +1,116 @@ + $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var array $floatArray + */ + #[ArrayType(['float'])] + #[JsonProperty('float_array')] + public array $floatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * otherFloatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * floatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->otherFloatProperty = $values['otherFloatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->floatArray = $values['floatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; + } +} + +class ScalarTest extends TestCase +{ + public function testAllScalarTypesIncludingFloat(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'integer_property' => 42, + 'float_property' => 3.14159, + 'other_float_property' => 3, + 'boolean_property' => true, + 'string_property' => 'Hello, World!', + 'int_float_array' => [1, 2.5, 3, 4.75], + 'float_array' => [1, 2, 3, 4] // Ensure we handle "integer-looking" floats + ], + ); + + $object = Scalar::fromJson($expectedJson); + $this->assertEquals(42, $object->integerProperty, 'integer_property should be 42.'); + $this->assertEquals(3.14159, $object->floatProperty, 'float_property should be 3.14159.'); + $this->assertTrue($object->booleanProperty, 'boolean_property should be true.'); + $this->assertEquals('Hello, World!', $object->stringProperty, 'string_property should be "Hello, World!".'); + $this->assertNull($object->nullableBooleanProperty, 'nullable_boolean_property should be null.'); + $this->assertEquals([1, 2.5, 3, 4.75], $object->intFloatArray, 'int_float_array should match the original data.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for ScalarTypesTest.'); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/TraitTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/TraitTest.php new file mode 100644 index 000000000000..e18f06d4191b --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/TraitTest.php @@ -0,0 +1,60 @@ +integerProperty = $values['integerProperty']; + $this->stringProperty = $values['stringProperty']; + } +} + +class TraitTest extends TestCase +{ + public function testTraitPropertyAndString(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'integer_property' => 42, + 'string_property' => 'Hello, World!', + ], + ); + + $object = TypeWithTrait::fromJson($expectedJson); + $this->assertEquals(42, $object->integerProperty, 'integer_property should be 42.'); + $this->assertEquals('Hello, World!', $object->stringProperty, 'string_property should be "Hello, World!".'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for ScalarTypesTestWithTrait.'); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/UnionArrayTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/UnionArrayTest.php new file mode 100644 index 000000000000..de20cf9fde1b --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/UnionArrayTest.php @@ -0,0 +1,57 @@ + $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->mixedDates = $values['mixedDates']; + } +} + +class UnionArrayTest extends TestCase +{ + public function testUnionArray(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'mixed_dates' => [ + 1 => '2023-01-01T12:00:00Z', + 2 => null, + 3 => 'Some String' + ] + ], + ); + + $object = UnionArray::fromJson($expectedJson); + $this->assertInstanceOf(DateTime::class, $object->mixedDates[1], 'mixed_dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:00:00', $object->mixedDates[1]->format('Y-m-d H:i:s'), 'mixed_dates[1] should have the correct datetime.'); + $this->assertNull($object->mixedDates[2], 'mixed_dates[2] should be null.'); + $this->assertEquals('Some String', $object->mixedDates[3], 'mixed_dates[3] should be "Some String".'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for mixed_dates.'); + } +} diff --git a/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/UnionPropertyTest.php b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/UnionPropertyTest.php new file mode 100644 index 000000000000..ca014658f4d0 --- /dev/null +++ b/seed/php-sdk/php-global-header-literal-env/tests/Core/Json/UnionPropertyTest.php @@ -0,0 +1,109 @@ + 'integer'], UnionProperty::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionProperty + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'complexUnion' => [1 => 100, 2 => 200] + ], + ); + + $object = UnionProperty::fromJson($expectedJson); + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'complexUnion' => new UnionProperty( + [ + 'complexUnion' => 'Nested String' + ] + ) + ], + ); + + $object = UnionProperty::fromJson($expectedJson); + $this->assertInstanceOf(UnionProperty::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $expectedJson = '{}'; + + $object = UnionProperty::fromJson($expectedJson); + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'complexUnion' => 42 + ], + ); + + $object = UnionProperty::fromJson($expectedJson); + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $expectedJson = JsonEncoder::encode( + [ + 'complexUnion' => 'Some String' + ], + ); + + $object = UnionProperty::fromJson($expectedJson); + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/test-definitions/fern/apis/csharp-global-header-literal-env/definition/api.yml b/test-definitions/fern/apis/csharp-global-header-literal-env/definition/api.yml new file mode 100644 index 000000000000..0d46052042b1 --- /dev/null +++ b/test-definitions/fern/apis/csharp-global-header-literal-env/definition/api.yml @@ -0,0 +1,13 @@ +name: csharp-global-header-literal-env +headers: + Square-Version: + name: version + type: literal<"2026-07-15"> + env: VERSION +auth: Bearer +auth-schemes: + Bearer: + scheme: bearer + token: + name: token + env: SQUARE_TOKEN diff --git a/test-definitions/fern/apis/csharp-global-header-literal-env/definition/service.yml b/test-definitions/fern/apis/csharp-global-header-literal-env/definition/service.yml new file mode 100644 index 000000000000..db3087e25229 --- /dev/null +++ b/test-definitions/fern/apis/csharp-global-header-literal-env/definition/service.yml @@ -0,0 +1,9 @@ +service: + auth: true + base-path: "" + endpoints: + getWithLiteralVersionHeader: + docs: GET request with a literal version header + path: /version + method: GET + response: string diff --git a/test-definitions/fern/apis/csharp-global-header-literal-env/generators.yml b/test-definitions/fern/apis/csharp-global-header-literal-env/generators.yml new file mode 100644 index 000000000000..211e43123d30 --- /dev/null +++ b/test-definitions/fern/apis/csharp-global-header-literal-env/generators.yml @@ -0,0 +1,2 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +groups: {} diff --git a/test-definitions/fern/apis/php-global-header-literal-env/definition/api.yml b/test-definitions/fern/apis/php-global-header-literal-env/definition/api.yml new file mode 100644 index 000000000000..1de25349cf4c --- /dev/null +++ b/test-definitions/fern/apis/php-global-header-literal-env/definition/api.yml @@ -0,0 +1,13 @@ +name: php-global-header-literal-env +headers: + Square-Version: + name: version + type: literal<"2026-07-15"> + env: VERSION +auth: Bearer +auth-schemes: + Bearer: + scheme: bearer + token: + name: token + env: SQUARE_TOKEN diff --git a/test-definitions/fern/apis/php-global-header-literal-env/definition/service.yml b/test-definitions/fern/apis/php-global-header-literal-env/definition/service.yml new file mode 100644 index 000000000000..db3087e25229 --- /dev/null +++ b/test-definitions/fern/apis/php-global-header-literal-env/definition/service.yml @@ -0,0 +1,9 @@ +service: + auth: true + base-path: "" + endpoints: + getWithLiteralVersionHeader: + docs: GET request with a literal version header + path: /version + method: GET + response: string diff --git a/test-definitions/fern/apis/php-global-header-literal-env/generators.yml b/test-definitions/fern/apis/php-global-header-literal-env/generators.yml new file mode 100644 index 000000000000..211e43123d30 --- /dev/null +++ b/test-definitions/fern/apis/php-global-header-literal-env/generators.yml @@ -0,0 +1,2 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +groups: {} From a0c62b06cfaa0daf854e9dac4a7d633000fadcc0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Aug 2026 18:47:32 +0000 Subject: [PATCH 2/3] chore(csharp): release 2.82.1 --- .../fix-literal-global-header-env-fallback.yml | 0 generators/csharp/sdk/versions.yml | 15 +++++++++++++++ 2 files changed, 15 insertions(+) rename generators/csharp/sdk/changes/{unreleased => 2.82.1}/fix-literal-global-header-env-fallback.yml (100%) diff --git a/generators/csharp/sdk/changes/unreleased/fix-literal-global-header-env-fallback.yml b/generators/csharp/sdk/changes/2.82.1/fix-literal-global-header-env-fallback.yml similarity index 100% rename from generators/csharp/sdk/changes/unreleased/fix-literal-global-header-env-fallback.yml rename to generators/csharp/sdk/changes/2.82.1/fix-literal-global-header-env-fallback.yml diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index 805162f35e46..e3c8aa67c069 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -1,4 +1,19 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.82.1 + changelogEntry: + - summary: | + Global headers typed as a `literal<"...">` with an `env` fallback no longer throw when + neither the constructor parameter nor the environment variable is set. The literal is + known at compile time, so it is now used as the header's default value. + type: fix + - summary: | + The generated `X-Fern-SDK-Version` header value is now emitted as a globally qualified + reference, so it can no longer be shadowed by a constructor parameter or local that + PascalCases to `Version` (previously `error CS1061: 'string' does not contain a + definition for 'Current'`). + type: fix + createdAt: "2026-08-18" + irVersion: 67 - version: 2.82.0 changelogEntry: - summary: | From 016b244b8f07e83202729139a86389f6c7bac8ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Aug 2026 18:47:45 +0000 Subject: [PATCH 3/3] chore(php): release 2.20.2 --- .../fix-literal-global-header-env-fallback.yml | 0 generators/php/sdk/versions.yml | 9 +++++++++ 2 files changed, 9 insertions(+) rename generators/php/sdk/changes/{unreleased => 2.20.2}/fix-literal-global-header-env-fallback.yml (100%) diff --git a/generators/php/sdk/changes/unreleased/fix-literal-global-header-env-fallback.yml b/generators/php/sdk/changes/2.20.2/fix-literal-global-header-env-fallback.yml similarity index 100% rename from generators/php/sdk/changes/unreleased/fix-literal-global-header-env-fallback.yml rename to generators/php/sdk/changes/2.20.2/fix-literal-global-header-env-fallback.yml diff --git a/generators/php/sdk/versions.yml b/generators/php/sdk/versions.yml index 299fcd6f3b2d..6f1b8906a965 100644 --- a/generators/php/sdk/versions.yml +++ b/generators/php/sdk/versions.yml @@ -1,4 +1,13 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.20.2 + changelogEntry: + - summary: | + Global headers typed as a `literal<"...">` with an `env` fallback no longer throw when + neither the constructor parameter nor the environment variable is set. The literal is + known at compile time, so it is now used as the header's default value. + type: fix + createdAt: "2026-08-18" + irVersion: 67 - version: 2.20.1 changelogEntry: - summary: |