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/2.82.1/fix-literal-global-header-env-fallback.yml b/generators/csharp/sdk/changes/2.82.1/fix-literal-global-header-env-fallback.yml new file mode 100644 index 000000000000..416e2d49ae47 --- /dev/null +++ b/generators/csharp/sdk/changes/2.82.1/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 + - 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: | diff --git a/generators/php/sdk/changes/2.20.2/fix-literal-global-header-env-fallback.yml b/generators/php/sdk/changes/2.20.2/fix-literal-global-header-env-fallback.yml new file mode 100644 index 000000000000..15d1e41d36e7 --- /dev/null +++ b/generators/php/sdk/changes/2.20.2/fix-literal-global-header-env-fallback.yml @@ -0,0 +1,7 @@ +# 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 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` 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: | diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-literal-env.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-literal-env.json new file mode 100644 index 000000000000..6fb724c8f661 --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/csharp-global-header-literal-env.json @@ -0,0 +1,148 @@ +{ + "version": "1.0.0", + "types": {}, + "headers": [ + { + "name": { + "wireValue": "Square-Version", + "name": { + "originalName": "version", + "camelCase": { + "unsafeName": "version", + "safeName": "version" + }, + "snakeCase": { + "unsafeName": "version", + "safeName": "version" + }, + "screamingSnakeCase": { + "unsafeName": "VERSION", + "safeName": "VERSION" + }, + "pascalCase": { + "unsafeName": "Version", + "safeName": "Version" + } + } + }, + "typeReference": { + "type": "literal", + "value": { + "type": "string", + "value": "2026-07-15" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "endpoints": { + "endpoint_service.getWithLiteralVersionHeader": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithLiteralVersionHeader", + "camelCase": { + "unsafeName": "getWithLiteralVersionHeader", + "safeName": "getWithLiteralVersionHeader" + }, + "snakeCase": { + "unsafeName": "get_with_literal_version_header", + "safeName": "get_with_literal_version_header" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_LITERAL_VERSION_HEADER", + "safeName": "GET_WITH_LITERAL_VERSION_HEADER" + }, + "pascalCase": { + "unsafeName": "GetWithLiteralVersionHeader", + "safeName": "GetWithLiteralVersionHeader" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "service", + "camelCase": { + "unsafeName": "service", + "safeName": "service" + }, + "snakeCase": { + "unsafeName": "service", + "safeName": "service" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE", + "safeName": "SERVICE" + }, + "pascalCase": { + "unsafeName": "Service", + "safeName": "Service" + } + } + ], + "packagePath": [], + "file": { + "originalName": "service", + "camelCase": { + "unsafeName": "service", + "safeName": "service" + }, + "snakeCase": { + "unsafeName": "service", + "safeName": "service" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE", + "safeName": "SERVICE" + }, + "pascalCase": { + "unsafeName": "Service", + "safeName": "Service" + } + } + } + }, + "location": { + "method": "GET", + "path": "/version" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": null, + "variables": null, + "globalParameters": null, + "generatorConfig": null +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/php-global-header-literal-env.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/php-global-header-literal-env.json new file mode 100644 index 000000000000..6fb724c8f661 --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/php-global-header-literal-env.json @@ -0,0 +1,148 @@ +{ + "version": "1.0.0", + "types": {}, + "headers": [ + { + "name": { + "wireValue": "Square-Version", + "name": { + "originalName": "version", + "camelCase": { + "unsafeName": "version", + "safeName": "version" + }, + "snakeCase": { + "unsafeName": "version", + "safeName": "version" + }, + "screamingSnakeCase": { + "unsafeName": "VERSION", + "safeName": "VERSION" + }, + "pascalCase": { + "unsafeName": "Version", + "safeName": "Version" + } + } + }, + "typeReference": { + "type": "literal", + "value": { + "type": "string", + "value": "2026-07-15" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "endpoints": { + "endpoint_service.getWithLiteralVersionHeader": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithLiteralVersionHeader", + "camelCase": { + "unsafeName": "getWithLiteralVersionHeader", + "safeName": "getWithLiteralVersionHeader" + }, + "snakeCase": { + "unsafeName": "get_with_literal_version_header", + "safeName": "get_with_literal_version_header" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_LITERAL_VERSION_HEADER", + "safeName": "GET_WITH_LITERAL_VERSION_HEADER" + }, + "pascalCase": { + "unsafeName": "GetWithLiteralVersionHeader", + "safeName": "GetWithLiteralVersionHeader" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "service", + "camelCase": { + "unsafeName": "service", + "safeName": "service" + }, + "snakeCase": { + "unsafeName": "service", + "safeName": "service" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE", + "safeName": "SERVICE" + }, + "pascalCase": { + "unsafeName": "Service", + "safeName": "Service" + } + } + ], + "packagePath": [], + "file": { + "originalName": "service", + "camelCase": { + "unsafeName": "service", + "safeName": "service" + }, + "snakeCase": { + "unsafeName": "service", + "safeName": "service" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE", + "safeName": "SERVICE" + }, + "pascalCase": { + "unsafeName": "Service", + "safeName": "Service" + } + } + } + }, + "location": { + "method": "GET", + "path": "/version" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": null, + "variables": null, + "globalParameters": null, + "generatorConfig": null +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-literal-env.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-literal-env.json new file mode 100644 index 000000000000..d904c5fab6b0 --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/csharp-global-header-literal-env.json @@ -0,0 +1,421 @@ +{ + "selfHosted": false, + "fdrApiDefinitionId": null, + "apiVersion": null, + "specVersion": null, + "apiName": "csharp-global-header-literal-env", + "apiDisplayName": null, + "apiDocs": null, + "auth": { + "requirement": "ALL", + "schemes": [ + { + "_type": "bearer", + "token": "token", + "tokenEnvVar": "SQUARE_TOKEN", + "tokenPlaceholder": null, + "key": "Bearer", + "docs": null + } + ], + "docs": null + }, + "headers": [ + { + "name": { + "wireValue": "Square-Version", + "name": "version" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "literal", + "literal": { + "type": "string", + "string": "2026-07-15" + } + } + }, + "env": "VERSION", + "clientDefault": null, + "defaultValue": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "idempotencyHeaders": [], + "types": {}, + "errors": {}, + "services": { + "service_service": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + "service" + ], + "packagePath": [], + "file": "service" + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_service.getWithLiteralVersionHeader", + "name": "getWithLiteralVersionHeader", + "displayName": null, + "subtitle": null, + "auth": true, + "security": [ + { + "Bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/version", + "parts": [] + }, + "fullPath": { + "head": "version", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d9186361", + "url": "/version", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "globalParameters": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "GET request with a literal version header" + } + ], + "audiences": null + } + }, + "constants": { + "errorInstanceIdKey": "errorInstanceId" + }, + "environments": null, + "errorDiscriminationStrategy": { + "type": "statusCode" + }, + "basePath": null, + "pathParameters": [], + "variables": [], + "globalParameters": null, + "serviceTypeReferenceInfo": { + "typesReferencedOnlyByService": {}, + "sharedTypes": [] + }, + "webhookGroups": {}, + "websocketChannels": {}, + "readmeConfig": null, + "sourceConfig": null, + "publishConfig": null, + "dynamic": { + "version": "1.0.0", + "types": {}, + "headers": [ + { + "name": { + "wireValue": "Square-Version", + "name": { + "originalName": "version", + "camelCase": { + "unsafeName": "version", + "safeName": "version" + }, + "snakeCase": { + "unsafeName": "version", + "safeName": "version" + }, + "screamingSnakeCase": { + "unsafeName": "VERSION", + "safeName": "VERSION" + }, + "pascalCase": { + "unsafeName": "Version", + "safeName": "Version" + } + } + }, + "typeReference": { + "type": "literal", + "value": { + "type": "string", + "value": "2026-07-15" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "endpoints": { + "endpoint_service.getWithLiteralVersionHeader": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithLiteralVersionHeader", + "camelCase": { + "unsafeName": "getWithLiteralVersionHeader", + "safeName": "getWithLiteralVersionHeader" + }, + "snakeCase": { + "unsafeName": "get_with_literal_version_header", + "safeName": "get_with_literal_version_header" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_LITERAL_VERSION_HEADER", + "safeName": "GET_WITH_LITERAL_VERSION_HEADER" + }, + "pascalCase": { + "unsafeName": "GetWithLiteralVersionHeader", + "safeName": "GetWithLiteralVersionHeader" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "service", + "camelCase": { + "unsafeName": "service", + "safeName": "service" + }, + "snakeCase": { + "unsafeName": "service", + "safeName": "service" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE", + "safeName": "SERVICE" + }, + "pascalCase": { + "unsafeName": "Service", + "safeName": "Service" + } + } + ], + "packagePath": [], + "file": { + "originalName": "service", + "camelCase": { + "unsafeName": "service", + "safeName": "service" + }, + "snakeCase": { + "unsafeName": "service", + "safeName": "service" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE", + "safeName": "SERVICE" + }, + "pascalCase": { + "unsafeName": "Service", + "safeName": "Service" + } + } + } + }, + "location": { + "method": "GET", + "path": "/version" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": null, + "variables": null, + "globalParameters": null, + "generatorConfig": null + }, + "audiences": null, + "generationMetadata": null, + "apiPlayground": true, + "casingsConfig": { + "generationLanguage": null, + "keywords": null, + "smartCasing": true, + "smartCasingDigitWordBoundary": null + }, + "subpackages": { + "subpackage_service": { + "name": "service", + "displayName": null, + "fernFilepath": { + "allParts": [ + "service" + ], + "packagePath": [], + "file": "service" + }, + "service": "service_service", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + } + }, + "rootPackage": { + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "websocket": null, + "service": null, + "types": [], + "errors": [], + "subpackages": [ + "subpackage_service" + ], + "webhooks": null, + "navigationConfig": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + }, + "sdkConfig": { + "isAuthMandatory": true, + "hasStreamingEndpoints": false, + "hasPaginatedEndpoints": false, + "hasFileDownloadEndpoints": false, + "idempotencyKeyGeneration": null, + "platformHeaders": { + "language": "X-Fern-Language", + "sdkName": "X-Fern-SDK-Name", + "sdkVersion": "X-Fern-SDK-Version", + "userAgent": null + } + } +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/php-global-header-literal-env.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/php-global-header-literal-env.json new file mode 100644 index 000000000000..8eb38baa0493 --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/php-global-header-literal-env.json @@ -0,0 +1,421 @@ +{ + "selfHosted": false, + "fdrApiDefinitionId": null, + "apiVersion": null, + "specVersion": null, + "apiName": "php-global-header-literal-env", + "apiDisplayName": null, + "apiDocs": null, + "auth": { + "requirement": "ALL", + "schemes": [ + { + "_type": "bearer", + "token": "token", + "tokenEnvVar": "SQUARE_TOKEN", + "tokenPlaceholder": null, + "key": "Bearer", + "docs": null + } + ], + "docs": null + }, + "headers": [ + { + "name": { + "wireValue": "Square-Version", + "name": "version" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "literal", + "literal": { + "type": "string", + "string": "2026-07-15" + } + } + }, + "env": "VERSION", + "clientDefault": null, + "defaultValue": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "idempotencyHeaders": [], + "types": {}, + "errors": {}, + "services": { + "service_service": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + "service" + ], + "packagePath": [], + "file": "service" + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_service.getWithLiteralVersionHeader", + "name": "getWithLiteralVersionHeader", + "displayName": null, + "subtitle": null, + "auth": true, + "security": [ + { + "Bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/version", + "parts": [] + }, + "fullPath": { + "head": "version", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d9186361", + "url": "/version", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "globalParameters": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "GET request with a literal version header" + } + ], + "audiences": null + } + }, + "constants": { + "errorInstanceIdKey": "errorInstanceId" + }, + "environments": null, + "errorDiscriminationStrategy": { + "type": "statusCode" + }, + "basePath": null, + "pathParameters": [], + "variables": [], + "globalParameters": null, + "serviceTypeReferenceInfo": { + "typesReferencedOnlyByService": {}, + "sharedTypes": [] + }, + "webhookGroups": {}, + "websocketChannels": {}, + "readmeConfig": null, + "sourceConfig": null, + "publishConfig": null, + "dynamic": { + "version": "1.0.0", + "types": {}, + "headers": [ + { + "name": { + "wireValue": "Square-Version", + "name": { + "originalName": "version", + "camelCase": { + "unsafeName": "version", + "safeName": "version" + }, + "snakeCase": { + "unsafeName": "version", + "safeName": "version" + }, + "screamingSnakeCase": { + "unsafeName": "VERSION", + "safeName": "VERSION" + }, + "pascalCase": { + "unsafeName": "Version", + "safeName": "Version" + } + } + }, + "typeReference": { + "type": "literal", + "value": { + "type": "string", + "value": "2026-07-15" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "endpoints": { + "endpoint_service.getWithLiteralVersionHeader": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithLiteralVersionHeader", + "camelCase": { + "unsafeName": "getWithLiteralVersionHeader", + "safeName": "getWithLiteralVersionHeader" + }, + "snakeCase": { + "unsafeName": "get_with_literal_version_header", + "safeName": "get_with_literal_version_header" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_LITERAL_VERSION_HEADER", + "safeName": "GET_WITH_LITERAL_VERSION_HEADER" + }, + "pascalCase": { + "unsafeName": "GetWithLiteralVersionHeader", + "safeName": "GetWithLiteralVersionHeader" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "service", + "camelCase": { + "unsafeName": "service", + "safeName": "service" + }, + "snakeCase": { + "unsafeName": "service", + "safeName": "service" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE", + "safeName": "SERVICE" + }, + "pascalCase": { + "unsafeName": "Service", + "safeName": "Service" + } + } + ], + "packagePath": [], + "file": { + "originalName": "service", + "camelCase": { + "unsafeName": "service", + "safeName": "service" + }, + "snakeCase": { + "unsafeName": "service", + "safeName": "service" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE", + "safeName": "SERVICE" + }, + "pascalCase": { + "unsafeName": "Service", + "safeName": "Service" + } + } + } + }, + "location": { + "method": "GET", + "path": "/version" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null, + "bodyRequired": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": null, + "variables": null, + "globalParameters": null, + "generatorConfig": null + }, + "audiences": null, + "generationMetadata": null, + "apiPlayground": true, + "casingsConfig": { + "generationLanguage": null, + "keywords": null, + "smartCasing": true, + "smartCasingDigitWordBoundary": null + }, + "subpackages": { + "subpackage_service": { + "name": "service", + "displayName": null, + "fernFilepath": { + "allParts": [ + "service" + ], + "packagePath": [], + "file": "service" + }, + "service": "service_service", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + } + }, + "rootPackage": { + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "websocket": null, + "service": null, + "types": [], + "errors": [], + "subpackages": [ + "subpackage_service" + ], + "webhooks": null, + "navigationConfig": null, + "hasEndpointsInTree": true, + "hasWebSocketInTree": false, + "docs": null + }, + "sdkConfig": { + "isAuthMandatory": true, + "hasStreamingEndpoints": false, + "hasPaginatedEndpoints": false, + "hasFileDownloadEndpoints": false, + "idempotencyKeyGeneration": null, + "platformHeaders": { + "language": "X-Fern-Language", + "sdkName": "X-Fern-SDK-Name", + "sdkVersion": "X-Fern-SDK-Version", + "userAgent": null + } + } +} \ No newline at end of file diff --git a/seed/cli/seed.yml b/seed/cli/seed.yml index 1e35fed8fbf7..f099d4cec805 100644 --- a/seed/cli/seed.yml +++ b/seed/cli/seed.yml @@ -315,6 +315,7 @@ allowedFailures: - content-type - cross-package-type-names - csharp-global-header-env + - csharp-global-header-literal-env - dollar-string-examples - empty-clients - endpoint-security-auth @@ -404,6 +405,7 @@ allowedFailures: - pagination-uri-path - path-parameters - php-global-header-env + - php-global-header-literal-env - plain-text - property-access - public-object diff --git a/seed/csharp-sdk/accept-header/src/SeedAccept/SeedAcceptClient.cs b/seed/csharp-sdk/accept-header/src/SeedAccept/SeedAcceptClient.cs index bf1ccef5daef..3c144241fabe 100644 --- a/seed/csharp-sdk/accept-header/src/SeedAccept/SeedAcceptClient.cs +++ b/seed/csharp-sdk/accept-header/src/SeedAccept/SeedAcceptClient.cs @@ -14,7 +14,7 @@ public SeedAcceptClient(string? token = null, ClientOptions? clientOptions = nul { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedAccept" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedAccept.Version.Current }, { "User-Agent", "Fernaccept-header/0.0.1" }, } ); diff --git a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/SeedAliasExtendsClient.cs b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/SeedAliasExtendsClient.cs index c72fbd32a3b6..65ca0efa889d 100644 --- a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/SeedAliasExtendsClient.cs +++ b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/SeedAliasExtendsClient.cs @@ -14,7 +14,7 @@ public SeedAliasExtendsClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedAliasExtends" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedAliasExtends.Version.Current }, { "User-Agent", "Fernalias-extends/0.0.1" }, } ); diff --git a/seed/csharp-sdk/alias/src/SeedAlias/SeedAliasClient.cs b/seed/csharp-sdk/alias/src/SeedAlias/SeedAliasClient.cs index c16e355d0c47..b4e34a729fca 100644 --- a/seed/csharp-sdk/alias/src/SeedAlias/SeedAliasClient.cs +++ b/seed/csharp-sdk/alias/src/SeedAlias/SeedAliasClient.cs @@ -14,7 +14,7 @@ public SeedAliasClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedAlias" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedAlias.Version.Current }, { "User-Agent", "Fernalias/0.0.1" }, } ); diff --git a/seed/csharp-sdk/allof-inline/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/allof-inline/src/SeedApi/SeedApiClient.cs index b746549adeab..329674854714 100644 --- a/seed/csharp-sdk/allof-inline/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/allof-inline/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", "Fernallof-inline/0.0.1" }, } ); diff --git a/seed/csharp-sdk/allof/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/allof/src/SeedApi/SeedApiClient.cs index 630ad71f93de..e4f67e1a921b 100644 --- a/seed/csharp-sdk/allof/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/allof/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", "Fernallof/0.0.1" }, } ); diff --git a/seed/csharp-sdk/any-auth/no-custom-config/src/SeedAnyAuth/SeedAnyAuthClient.cs b/seed/csharp-sdk/any-auth/no-custom-config/src/SeedAnyAuth/SeedAnyAuthClient.cs index 2d05505962c1..df8683660c12 100644 --- a/seed/csharp-sdk/any-auth/no-custom-config/src/SeedAnyAuth/SeedAnyAuthClient.cs +++ b/seed/csharp-sdk/any-auth/no-custom-config/src/SeedAnyAuth/SeedAnyAuthClient.cs @@ -28,7 +28,7 @@ public SeedAnyAuthClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedAnyAuth" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedAnyAuth.Version.Current }, { "User-Agent", "Fernany-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/any-auth/prefer-explicit-auth/src/SeedAnyAuth/SeedAnyAuthClient.cs b/seed/csharp-sdk/any-auth/prefer-explicit-auth/src/SeedAnyAuth/SeedAnyAuthClient.cs index 5337943202e9..d032972adb3d 100644 --- a/seed/csharp-sdk/any-auth/prefer-explicit-auth/src/SeedAnyAuth/SeedAnyAuthClient.cs +++ b/seed/csharp-sdk/any-auth/prefer-explicit-auth/src/SeedAnyAuth/SeedAnyAuthClient.cs @@ -30,7 +30,7 @@ public SeedAnyAuthClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedAnyAuth" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedAnyAuth.Version.Current }, { "User-Agent", "Fernany-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/api-wide-base-path-with-default/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/api-wide-base-path-with-default/src/SeedApi/SeedApiClient.cs index e72311b0a3ee..97387e774367 100644 --- a/seed/csharp-sdk/api-wide-base-path-with-default/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/api-wide-base-path-with-default/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", "Fernapi-wide-base-path-with-default/0.0.1" }, } ); diff --git a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/SeedApiWideBasePathClient.cs b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/SeedApiWideBasePathClient.cs index d7c19693db47..6bd5c116c289 100644 --- a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/SeedApiWideBasePathClient.cs +++ b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/SeedApiWideBasePathClient.cs @@ -14,7 +14,7 @@ public SeedApiWideBasePathClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedApiWideBasePath" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedApiWideBasePath.Version.Current }, { "User-Agent", "Fernapi-wide-base-path/0.0.1" }, } ); diff --git a/seed/csharp-sdk/audiences/src/SeedAudiences/SeedAudiencesClient.cs b/seed/csharp-sdk/audiences/src/SeedAudiences/SeedAudiencesClient.cs index 8c435401eadf..d8724427390d 100644 --- a/seed/csharp-sdk/audiences/src/SeedAudiences/SeedAudiencesClient.cs +++ b/seed/csharp-sdk/audiences/src/SeedAudiences/SeedAudiencesClient.cs @@ -16,7 +16,7 @@ public SeedAudiencesClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedAudiences" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedAudiences.Version.Current }, { "User-Agent", "Fernaudiences/0.0.1" }, } ); diff --git a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/SeedBasicAuthEnvironmentVariablesClient.cs b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/SeedBasicAuthEnvironmentVariablesClient.cs index b2f7c31cdfeb..e1951ad7f5ca 100644 --- a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/SeedBasicAuthEnvironmentVariablesClient.cs +++ b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/SeedBasicAuthEnvironmentVariablesClient.cs @@ -27,7 +27,7 @@ public SeedBasicAuthEnvironmentVariablesClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedBasicAuthEnvironmentVariables" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedBasicAuthEnvironmentVariables.Version.Current }, { "User-Agent", "Fernbasic-auth-environment-variables/0.0.1" }, } ); diff --git a/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthPwOmitted/SeedBasicAuthPwOmittedClient.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthPwOmitted/SeedBasicAuthPwOmittedClient.cs index 9ceaa24c808a..ca11074824d8 100644 --- a/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthPwOmitted/SeedBasicAuthPwOmittedClient.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthPwOmitted/SeedBasicAuthPwOmittedClient.cs @@ -17,7 +17,7 @@ public SeedBasicAuthPwOmittedClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedBasicAuthPwOmitted" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedBasicAuthPwOmitted.Version.Current }, { "User-Agent", "Fernbasic-auth-pw-omitted/0.0.1" }, } ); diff --git a/seed/csharp-sdk/basic-auth/no-custom-config/src/SeedBasicAuth/SeedBasicAuthClient.cs b/seed/csharp-sdk/basic-auth/no-custom-config/src/SeedBasicAuth/SeedBasicAuthClient.cs index 5d5be2020042..d21874ec5ab7 100644 --- a/seed/csharp-sdk/basic-auth/no-custom-config/src/SeedBasicAuth/SeedBasicAuthClient.cs +++ b/seed/csharp-sdk/basic-auth/no-custom-config/src/SeedBasicAuth/SeedBasicAuthClient.cs @@ -18,7 +18,7 @@ public SeedBasicAuthClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedBasicAuth" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedBasicAuth.Version.Current }, { "User-Agent", "Fernbasic-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/basic-auth/unified-client-options/src/SeedBasicAuth/SeedBasicAuthClient.cs b/seed/csharp-sdk/basic-auth/unified-client-options/src/SeedBasicAuth/SeedBasicAuthClient.cs index 119db847c220..08e773a93eff 100644 --- a/seed/csharp-sdk/basic-auth/unified-client-options/src/SeedBasicAuth/SeedBasicAuthClient.cs +++ b/seed/csharp-sdk/basic-auth/unified-client-options/src/SeedBasicAuth/SeedBasicAuthClient.cs @@ -13,7 +13,7 @@ public SeedBasicAuthClient(ClientOptions clientOptions) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedBasicAuth" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedBasicAuth.Version.Current }, { "User-Agent", "Fernbasic-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/basic-auth/wire-tests/src/SeedBasicAuth/SeedBasicAuthClient.cs b/seed/csharp-sdk/basic-auth/wire-tests/src/SeedBasicAuth/SeedBasicAuthClient.cs index 5d5be2020042..d21874ec5ab7 100644 --- a/seed/csharp-sdk/basic-auth/wire-tests/src/SeedBasicAuth/SeedBasicAuthClient.cs +++ b/seed/csharp-sdk/basic-auth/wire-tests/src/SeedBasicAuth/SeedBasicAuthClient.cs @@ -18,7 +18,7 @@ public SeedBasicAuthClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedBasicAuth" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedBasicAuth.Version.Current }, { "User-Agent", "Fernbasic-auth/0.0.1" }, } ); diff --git a/seed/csharp-sdk/bearer-token-environment-variable/no-custom-config/src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariableClient.cs b/seed/csharp-sdk/bearer-token-environment-variable/no-custom-config/src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariableClient.cs index 4c161008b1f2..3c9450fea007 100644 --- a/seed/csharp-sdk/bearer-token-environment-variable/no-custom-config/src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariableClient.cs +++ b/seed/csharp-sdk/bearer-token-environment-variable/no-custom-config/src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariableClient.cs @@ -22,7 +22,10 @@ public SeedBearerTokenEnvironmentVariableClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedBearerTokenEnvironmentVariable" }, - { "X-Fern-SDK-Version", Version.Current }, + { + "X-Fern-SDK-Version", + global::SeedBearerTokenEnvironmentVariable.Version.Current + }, { "User-Agent", "Fernbearer-token-environment-variable/0.0.1" }, } ); diff --git a/seed/csharp-sdk/bearer-token-environment-variable/unified-client-options/src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariableClient.cs b/seed/csharp-sdk/bearer-token-environment-variable/unified-client-options/src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariableClient.cs index ec2b3aeb765b..aa7f390d398b 100644 --- a/seed/csharp-sdk/bearer-token-environment-variable/unified-client-options/src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariableClient.cs +++ b/seed/csharp-sdk/bearer-token-environment-variable/unified-client-options/src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariableClient.cs @@ -18,7 +18,10 @@ public SeedBearerTokenEnvironmentVariableClient(ClientOptions clientOptions) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedBearerTokenEnvironmentVariable" }, - { "X-Fern-SDK-Version", Version.Current }, + { + "X-Fern-SDK-Version", + global::SeedBearerTokenEnvironmentVariable.Version.Current + }, { "User-Agent", "Fernbearer-token-environment-variable/0.0.1" }, } ); diff --git a/seed/csharp-sdk/bytes-download/src/SeedBytesDownload/SeedBytesDownloadClient.cs b/seed/csharp-sdk/bytes-download/src/SeedBytesDownload/SeedBytesDownloadClient.cs index 25c64fc171b8..8eb903ff1024 100644 --- a/seed/csharp-sdk/bytes-download/src/SeedBytesDownload/SeedBytesDownloadClient.cs +++ b/seed/csharp-sdk/bytes-download/src/SeedBytesDownload/SeedBytesDownloadClient.cs @@ -14,7 +14,7 @@ public SeedBytesDownloadClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedBytesDownload" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedBytesDownload.Version.Current }, { "User-Agent", "Fernbytes-download/0.0.1" }, } ); diff --git a/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload/SeedBytesUploadClient.cs b/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload/SeedBytesUploadClient.cs index 98a21068a6b4..73228e6f5a1f 100644 --- a/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload/SeedBytesUploadClient.cs +++ b/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload/SeedBytesUploadClient.cs @@ -14,7 +14,7 @@ public SeedBytesUploadClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedBytesUpload" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedBytesUpload.Version.Current }, { "User-Agent", "Fernbytes-upload/0.0.1" }, } ); diff --git a/seed/csharp-sdk/circular-references-advanced/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/circular-references-advanced/src/SeedApi/SeedApiClient.cs index c5c37d15882a..e10919df2c81 100644 --- a/seed/csharp-sdk/circular-references-advanced/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/circular-references-advanced/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", "Ferncircular-references-advanced/0.0.1" }, } ); diff --git a/seed/csharp-sdk/circular-references-extends/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/circular-references-extends/src/SeedApi/SeedApiClient.cs index fa3c10565c30..81324fb48ee1 100644 --- a/seed/csharp-sdk/circular-references-extends/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/circular-references-extends/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", "Ferncircular-references-extends/0.0.1" }, } ); diff --git a/seed/csharp-sdk/circular-references/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/circular-references/src/SeedApi/SeedApiClient.cs index 3c403f4c0811..67cef76480c9 100644 --- a/seed/csharp-sdk/circular-references/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/circular-references/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", "Ferncircular-references/0.0.1" }, } ); diff --git a/seed/csharp-sdk/client-side-params/src/SeedClientSideParams/SeedClientSideParamsClient.cs b/seed/csharp-sdk/client-side-params/src/SeedClientSideParams/SeedClientSideParamsClient.cs index 838bf5d3a13f..b3cc09be45d6 100644 --- a/seed/csharp-sdk/client-side-params/src/SeedClientSideParams/SeedClientSideParamsClient.cs +++ b/seed/csharp-sdk/client-side-params/src/SeedClientSideParams/SeedClientSideParamsClient.cs @@ -14,7 +14,7 @@ public SeedClientSideParamsClient(string token, ClientOptions? clientOptions = n { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedClientSideParams" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedClientSideParams.Version.Current }, { "User-Agent", "Fernclient-side-params/0.0.1" }, } ); diff --git a/seed/csharp-sdk/content-type/src/SeedContentTypes/SeedContentTypesClient.cs b/seed/csharp-sdk/content-type/src/SeedContentTypes/SeedContentTypesClient.cs index 8504363aef58..8832c3d9c602 100644 --- a/seed/csharp-sdk/content-type/src/SeedContentTypes/SeedContentTypesClient.cs +++ b/seed/csharp-sdk/content-type/src/SeedContentTypes/SeedContentTypesClient.cs @@ -14,7 +14,7 @@ public SeedContentTypesClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedContentTypes" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedContentTypes.Version.Current }, { "User-Agent", "Ferncontent-type/0.0.1" }, } ); diff --git a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/SeedCrossPackageTypeNamesClient.cs b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/SeedCrossPackageTypeNamesClient.cs index b4ecee3b7535..482df8784e6d 100644 --- a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/SeedCrossPackageTypeNamesClient.cs +++ b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/SeedCrossPackageTypeNamesClient.cs @@ -16,7 +16,7 @@ public SeedCrossPackageTypeNamesClient(ClientOptions? clientOptions = null) { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedCrossPackageTypeNames" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedCrossPackageTypeNames.Version.Current }, { "User-Agent", "Ferncross-package-type-names/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-global-header-env/src/SeedCsharpGlobalHeaderEnv/SeedCsharpGlobalHeaderEnvClient.cs b/seed/csharp-sdk/csharp-global-header-env/src/SeedCsharpGlobalHeaderEnv/SeedCsharpGlobalHeaderEnvClient.cs index 7a463d514fa2..86be53c457d3 100644 --- a/seed/csharp-sdk/csharp-global-header-env/src/SeedCsharpGlobalHeaderEnv/SeedCsharpGlobalHeaderEnvClient.cs +++ b/seed/csharp-sdk/csharp-global-header-env/src/SeedCsharpGlobalHeaderEnv/SeedCsharpGlobalHeaderEnvClient.cs @@ -24,7 +24,7 @@ public SeedCsharpGlobalHeaderEnvClient( { { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "SeedCsharpGlobalHeaderEnv" }, - { "X-Fern-SDK-Version", Version.Current }, + { "X-Fern-SDK-Version", global::SeedCsharpGlobalHeaderEnv.Version.Current }, { "User-Agent", "Ferncsharp-global-header-env/0.0.1" }, } ); diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/.editorconfig b/seed/csharp-sdk/csharp-global-header-literal-env/.editorconfig new file mode 100644 index 000000000000..1e7a0adbac80 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/.editorconfig @@ -0,0 +1,35 @@ +root = true + +[*.cs] +resharper_arrange_object_creation_when_type_evident_highlighting = hint +resharper_auto_property_can_be_made_get_only_global_highlighting = hint +resharper_check_namespace_highlighting = hint +resharper_class_never_instantiated_global_highlighting = hint +resharper_class_never_instantiated_local_highlighting = hint +resharper_collection_never_updated_global_highlighting = hint +resharper_convert_type_check_pattern_to_null_check_highlighting = hint +resharper_inconsistent_naming_highlighting = hint +resharper_member_can_be_private_global_highlighting = hint +resharper_member_hides_static_from_outer_class_highlighting = hint +resharper_not_accessed_field_local_highlighting = hint +resharper_nullable_warning_suppression_is_used_highlighting = suggestion +resharper_partial_type_with_single_part_highlighting = hint +resharper_prefer_concrete_value_over_default_highlighting = none +resharper_private_field_can_be_converted_to_local_variable_highlighting = hint +resharper_property_can_be_made_init_only_global_highlighting = hint +resharper_property_can_be_made_init_only_local_highlighting = hint +resharper_redundant_name_qualifier_highlighting = none +resharper_redundant_using_directive_highlighting = hint +resharper_replace_slice_with_range_indexer_highlighting = none +resharper_unused_auto_property_accessor_global_highlighting = hint +resharper_unused_auto_property_accessor_local_highlighting = hint +resharper_unused_member_global_highlighting = hint +resharper_unused_type_global_highlighting = hint +resharper_use_string_interpolation_highlighting = hint +dotnet_diagnostic.CS1591.severity = suggestion + +[src/**/Types/*.cs] +resharper_check_namespace_highlighting = none + +[src/**/Core/Public/*.cs] +resharper_check_namespace_highlighting = none \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/.fern/metadata.json b/seed/csharp-sdk/csharp-global-header-literal-env/.fern/metadata.json new file mode 100644 index 000000000000..b5663320f931 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/.fern/metadata.json @@ -0,0 +1,10 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-csharp-sdk", + "generatorVersion": "latest", + "generatorConfig": {}, + "originGitCommit": "DUMMY", + "invokedBy": "manual", + "requestedVersion": "0.0.1", + "sdkVersion": "0.0.1" +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/.github/workflows/ci.yml b/seed/csharp-sdk/csharp-global-header-literal-env/.github/workflows/ci.yml new file mode 100644 index 000000000000..fda431b457aa --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: ci + +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + DOTNET_NOLOGO: true + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.x + + - name: Install tools + run: dotnet tool restore + + - name: Restore dependencies + run: dotnet restore src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.csproj + + - name: Build + run: dotnet build src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.csproj --no-restore -c Release + + - name: Restore test dependencies + run: dotnet restore src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.csproj + + - name: Build tests + run: dotnet build src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.csproj --no-restore -c Release + + - name: Test + run: dotnet test src/SeedCsharpGlobalHeaderLiteralEnv.Test/SeedCsharpGlobalHeaderLiteralEnv.Test.csproj --no-restore --no-build -c Release + + - name: Pack + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + run: dotnet pack src/SeedCsharpGlobalHeaderLiteralEnv/SeedCsharpGlobalHeaderLiteralEnv.csproj --no-build --no-restore -c Release + + - name: Publish to NuGet.org + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_TOKEN }} + run: dotnet nuget push src/SeedCsharpGlobalHeaderLiteralEnv/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source "nuget.org" diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/.gitignore b/seed/csharp-sdk/csharp-global-header-literal-env/.gitignore new file mode 100644 index 000000000000..11014f2b33d7 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +## This is based on `dotnet new gitignore` and customized by Fern + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +# [Rr]elease/ (Ignored by Fern) +# [Rr]eleases/ (Ignored by Fern) +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +# [Ll]og/ (Ignored by Fern) +# [Ll]ogs/ (Ignored by Fern) + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/seed/csharp-sdk/csharp-global-header-literal-env/CONTRIBUTING.md b/seed/csharp-sdk/csharp-global-header-literal-env/CONTRIBUTING.md new file mode 100644 index 000000000000..1de047696582 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/CONTRIBUTING.md @@ -0,0 +1,119 @@ +# Contributing + +Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. + +## Getting Started + +### Prerequisites + +- .NET SDK (version compatible with the target frameworks: net462, net8.0, netstandard2.0) + +### Installation + +Install the project dependencies: + +```bash +dotnet restore +``` + +### Building + +Build the project: + +```bash +dotnet build +``` + +### Testing + +Run the test suite: + +```bash +dotnet test +``` + +### Formatting + +Check code style: + +```bash +dotnet format --verify-no-changes +``` + +Fix code style issues: + +```bash +dotnet format +``` + +## 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 C# 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 C# SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) +2. Generator code is located at `generators/csharp/` +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: `dotnet test` +4. Run formatting: `dotnet format` +5. Build the project: `dotnet build` +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 automated code formatting. Run `dotnet format` 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/csharp-sdk/csharp-global-header-literal-env/README.md b/seed/csharp-sdk/csharp-global-header-literal-env/README.md new file mode 100644 index 000000000000..40dc75ff7eb0 --- /dev/null +++ b/seed/csharp-sdk/csharp-global-header-literal-env/README.md @@ -0,0 +1,210 @@ +# Seed C# 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%2FC%23) +[![nuget shield](https://img.shields.io/nuget/v/Ferncsharp-global-header-literal-env)](https://nuget.org/packages/Ferncsharp-global-header-literal-env) + +The Seed C# library provides convenient access to the Seed APIs from C#. + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Raw Response](#raw-response) + - [Additional Headers](#additional-headers) + - [Additional Query Parameters](#additional-query-parameters) + - [Additional Body Properties](#additional-body-properties) +- [Contributing](#contributing) + +## Requirements + +This SDK requires: + +## Installation + +```sh +dotnet add package Ferncsharp-global-header-literal-env +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```csharp +using SeedCsharpGlobalHeaderLiteralEnv; + +var client = new SeedCsharpGlobalHeaderLiteralEnvClient("TOKEN", "VERSION"); +await client.Service.GetWithLiteralVersionHeaderAsync(); +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```csharp +using SeedCsharpGlobalHeaderLiteralEnv; + +try { + var response = await client.Service.GetWithLiteralVersionHeaderAsync(...); +} catch (SeedCsharpGlobalHeaderLiteralEnvApiException e) { + System.Console.WriteLine(e.Body); + System.Console.WriteLine(e.StatusCode); + + // Access the raw HTTP response (status code, URL, headers) off the exception + var rawResponse = e.RawResponse; + if (rawResponse != null) + { + System.Console.WriteLine(rawResponse.Url); + if (rawResponse.Headers.TryGetValue("X-Request-Id", out var requestId)) + { + System.Console.WriteLine($"Request ID: {requestId}"); + } + } +} +``` + +## Advanced + +### 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). + +Which status codes are retried depends on the `retryStatusCodes` generator configuration: + +**`legacy`** (current default): retries on +- [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) (All server errors, including 500) + +**`recommended`**: retries on +- [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) +- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway) +- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable) +- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout) + +Use the `MaxRetries` request option to configure this behavior. + +```csharp +var response = await client.Service.GetWithLiteralVersionHeaderAsync( + ..., + new RequestOptions { + 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. + +```csharp +var response = await client.Service.GetWithLiteralVersionHeaderAsync( + ..., + new RequestOptions { + Timeout: TimeSpan.FromSeconds(3) // Override timeout to 3s + } +); +``` + +### Raw Response + +Access raw HTTP response data (status code, headers, URL) alongside parsed response data using the `.WithRawResponse()` method. + +```csharp +using SeedCsharpGlobalHeaderLiteralEnv; + +// Access raw response data (status code, headers, etc.) alongside the parsed response +var result = await client.Service.GetWithLiteralVersionHeaderAsync(...).WithRawResponse(); + +// Access the parsed data +var data = result.Data; + +// Access raw response metadata +var statusCode = result.RawResponse.StatusCode; +var headers = result.RawResponse.Headers; +var url = result.RawResponse.Url; + +// Access specific headers (case-insensitive) +if (headers.TryGetValue("X-Request-Id", out var requestId)) +{ + System.Console.WriteLine($"Request ID: {requestId}"); +} + +// For the default behavior, simply await without .WithRawResponse() +var data = await client.Service.GetWithLiteralVersionHeaderAsync(...); + +// .WithRawResponse() also works on streaming endpoints (returns IAsyncEnumerable + 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: {}