From 9424477418ddef8080bd282ba5d443e3cd3e376c Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Wed, 19 Aug 2026 14:58:38 +0800 Subject: [PATCH] fix(ir): detect plain internally tagged oneOf variants Plain inline oneOf members with the discriminator alongside two or more content properties were not recognized by TaggedEnumPattern. They fell back to an untagged Union, so TypeScript camelCase mode emitted cast-through union converters and skipped member field conversion. Add detection for required single-value string enum tag fields on plain object oneOf members with more than two properties, for OAS 3.0, 3.1, and 3.2, and add a nested-request TypeScript camelCase golden test that exercises the generated union converters. --- src/ir/lower/v30.rs | 45 ++ src/ir/lower/v31.rs | 55 ++ src/ir/lower/v32.rs | 45 ++ src/ir/tagged_enum_pattern.rs | 85 ++- .../discriminated-union-plain-internal.yaml | 64 +++ .../README.md.golden | 250 +++++++++ .../apis/DefaultApi.ts.golden | 97 ++++ .../apis/index.ts.golden | 14 + .../index.ts.golden | 10 + .../models/PreviewRequest.ts.golden | 29 ++ .../models/ScheduleRule.ts.golden | 33 ++ .../models/ScheduleRuleCron.ts.golden | 34 ++ .../models/ScheduleRuleInterval.ts.golden | 34 ++ .../models/ScheduleRuleOnce.ts.golden | 30 ++ .../models/index.ts.golden | 17 + .../package.json.golden | 26 + .../runtime/runtime.ts.golden | 492 ++++++++++++++++++ .../tsconfig.esm.json.golden | 7 + .../tsconfig.json.golden | 31 ++ tests/golden_tests_typescript_fetch.rs | 19 + 20 files changed, 1416 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/valid/type-aliases/discriminated-union-plain-internal.yaml create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/README.md.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/apis/DefaultApi.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/apis/index.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/index.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/PreviewRequest.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRule.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleCron.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleInterval.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleOnce.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/index.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/package.json.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/runtime/runtime.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/tsconfig.esm.json.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/tsconfig.json.golden diff --git a/src/ir/lower/v30.rs b/src/ir/lower/v30.rs index 8598aa8bd..386cefd66 100644 --- a/src/ir/lower/v30.rs +++ b/src/ir/lower/v30.rs @@ -1692,4 +1692,49 @@ components: panic!("Expected Named alias"); } } + + #[test] + fn test_lower_tagged_union_with_plain_internal_variants() { + let ir = lower_yaml( + r#" +openapi: "3.0.0" +info: + title: Test + version: "1.0" +paths: {} +components: + schemas: + ScheduleRule: + oneOf: + - type: object + required: [type, at] + properties: + type: + type: string + enum: [once] + at: + type: string + - type: object + required: [type, every_minutes, anchor_at] + properties: + type: + type: string + enum: [interval] + every_minutes: + type: integer + format: int32 + anchor_at: + type: string +"#, + ); + let schedule_rule = &ir.schemas["ScheduleRule"]; + let IrSchemaKind::TaggedUnion(tu) = &schedule_rule.kind else { + panic!("Expected TaggedUnion, got {:?}", schedule_rule.kind); + }; + assert!(matches!(tu.tagging, TaggingStyle::Internal)); + assert_eq!(tu.discriminator_field, "type"); + assert_eq!(tu.variants.len(), 2); + assert_eq!(tu.variants[0].discriminator_value, "once"); + assert_eq!(tu.variants[1].discriminator_value, "interval"); + } } diff --git a/src/ir/lower/v31.rs b/src/ir/lower/v31.rs index 6b72abbf5..692f59a15 100644 --- a/src/ir/lower/v31.rs +++ b/src/ir/lower/v31.rs @@ -1824,4 +1824,59 @@ components: panic!("Expected Named alias"); } } + + #[test] + fn test_lower_tagged_union_with_plain_internal_variants() { + let ir = lower_yaml( + r#" +openapi: "3.1.0" +info: + title: Test + version: "1.0" +components: + schemas: + ScheduleRule: + oneOf: + - type: object + required: [type, at] + properties: + type: + type: string + enum: [once] + at: + type: string + - type: object + required: [type, every_minutes, anchor_at] + properties: + type: + type: string + enum: [interval] + every_minutes: + type: integer + format: int32 + anchor_at: + type: string + - type: object + required: [type, expression, timezone] + properties: + type: + type: string + enum: [cron] + expression: + type: string + timezone: + type: string +"#, + ); + let schedule_rule = &ir.schemas["ScheduleRule"]; + let IrSchemaKind::TaggedUnion(tu) = &schedule_rule.kind else { + panic!("Expected TaggedUnion, got {:?}", schedule_rule.kind); + }; + assert!(matches!(tu.tagging, TaggingStyle::Internal)); + assert_eq!(tu.discriminator_field, "type"); + assert_eq!(tu.variants.len(), 3); + assert_eq!(tu.variants[0].discriminator_value, "once"); + assert_eq!(tu.variants[1].discriminator_value, "interval"); + assert_eq!(tu.variants[2].discriminator_value, "cron"); + } } diff --git a/src/ir/lower/v32.rs b/src/ir/lower/v32.rs index 4fc9f18af..4291c6411 100644 --- a/src/ir/lower/v32.rs +++ b/src/ir/lower/v32.rs @@ -1886,4 +1886,49 @@ paths: assert!(!resp.item_content.is_empty()); assert!(resp.item_content.contains_key("text/event-stream")); } + + #[test] + fn test_lower_tagged_union_with_plain_internal_variants() { + let ir = lower_yaml( + r#" +openapi: "3.2.0" +info: + title: Test + version: "1.0" +paths: {} +components: + schemas: + ScheduleRule: + oneOf: + - type: object + required: [type, at] + properties: + type: + type: string + enum: [once] + at: + type: string + - type: object + required: [type, every_minutes, anchor_at] + properties: + type: + type: string + enum: [interval] + every_minutes: + type: integer + format: int32 + anchor_at: + type: string +"#, + ); + let schedule_rule = &ir.schemas["ScheduleRule"]; + let IrSchemaKind::TaggedUnion(tu) = &schedule_rule.kind else { + panic!("Expected TaggedUnion, got {:?}", schedule_rule.kind); + }; + assert!(matches!(tu.tagging, TaggingStyle::Internal)); + assert_eq!(tu.discriminator_field, "type"); + assert_eq!(tu.variants.len(), 2); + assert_eq!(tu.variants[0].discriminator_value, "once"); + assert_eq!(tu.variants[1].discriminator_value, "interval"); + } } diff --git a/src/ir/tagged_enum_pattern.rs b/src/ir/tagged_enum_pattern.rs index d5bcd72a2..498080dcc 100644 --- a/src/ir/tagged_enum_pattern.rs +++ b/src/ir/tagged_enum_pattern.rs @@ -84,7 +84,9 @@ impl TaggedEnumPattern { /// /// - Externally tagged: Single required property becomes the variant name /// - Adjacently tagged: Object with exactly 2 properties - one string enum (tag) and one object/ref (content) - /// - Internally tagged: allOf schema with a string enum property (tag field) + /// - Internally tagged: allOf schema with a string enum property (tag field), + /// or a plain object with a required single-value string enum tag field + /// alongside its content properties /// - Untagged: Schema reference to a component schema pub fn detect_from_schema(schema_ref: &ObjectOrReference) -> Option { match schema_ref { @@ -151,6 +153,33 @@ impl TaggedEnumPattern { }); } } + ObjectOrReference::Object(obj_schema) + if obj_schema.properties.len() > 2 && obj_schema.all_of.is_empty() => + { + let mut tag_field: Option = None; + let mut enum_value: Option = None; + for (prop_name, prop_schema) in &obj_schema.properties { + if !obj_schema.required.contains(prop_name) { + continue; + } + if let ObjectOrReference::Object(prop_obj) = prop_schema + && prop_obj.enum_values.len() == 1 + && let Some(serde_json::Value::String(enum_val)) = + prop_obj.enum_values.first() + { + tag_field = Some(prop_name.clone()); + enum_value = Some(enum_val.clone()); + break; + } + } + if let (Some(tag_field), Some(enum_val)) = (tag_field, enum_value) { + let variant_name = enum_val.to_pascal_case(); + return Some(TaggedEnumPattern::InternallyTagged { + variant_name, + tag_field, + }); + } + } ObjectOrReference::Object(obj_schema) if !obj_schema.all_of.is_empty() => { for item in &obj_schema.all_of { if let ObjectOrReference::Object(item_schema) = item { @@ -264,6 +293,33 @@ impl TaggedEnumPattern { }); } } + ObjectOrReference32::Object(obj_schema) + if obj_schema.properties.len() > 2 && obj_schema.all_of.is_empty() => + { + let mut tag_field: Option = None; + let mut enum_value: Option = None; + for (prop_name, prop_schema) in &obj_schema.properties { + if !obj_schema.required.contains(prop_name) { + continue; + } + if let ObjectOrReference32::Object(prop_obj) = prop_schema + && prop_obj.enum_values.len() == 1 + && let Some(serde_json::Value::String(enum_val)) = + prop_obj.enum_values.first() + { + tag_field = Some(prop_name.clone()); + enum_value = Some(enum_val.clone()); + break; + } + } + if let (Some(tag_field), Some(enum_val)) = (tag_field, enum_value) { + let variant_name = enum_val.to_pascal_case(); + return Some(TaggedEnumPattern::InternallyTagged { + variant_name, + tag_field, + }); + } + } ObjectOrReference32::Object(obj_schema) if !obj_schema.all_of.is_empty() => { for item in &obj_schema.all_of { if let ObjectOrReference32::Object(item_schema) = item { @@ -358,6 +414,33 @@ impl TaggedEnumPattern { }); } } + ObjectOrReference30::Object(obj_schema) + if obj_schema.properties.len() > 2 && obj_schema.all_of.is_empty() => + { + let mut tag_field: Option = None; + let mut enum_value: Option = None; + for (prop_name, prop_schema) in &obj_schema.properties { + if !obj_schema.required.contains(prop_name) { + continue; + } + if let ObjectOrReference30::Object(prop_obj) = prop_schema + && prop_obj.enum_values.len() == 1 + && let Some(serde_json::Value::String(enum_val)) = + prop_obj.enum_values.first() + { + tag_field = Some(prop_name.clone()); + enum_value = Some(enum_val.clone()); + break; + } + } + if let (Some(tag_field), Some(enum_val)) = (tag_field, enum_value) { + let variant_name = enum_val.to_pascal_case(); + return Some(TaggedEnumPattern::InternallyTagged { + variant_name, + tag_field, + }); + } + } ObjectOrReference30::Object(obj_schema) if !obj_schema.all_of.is_empty() => { for item in &obj_schema.all_of { if let ObjectOrReference30::Object(item_schema) = item { diff --git a/tests/fixtures/valid/type-aliases/discriminated-union-plain-internal.yaml b/tests/fixtures/valid/type-aliases/discriminated-union-plain-internal.yaml new file mode 100644 index 000000000..a3494f52c --- /dev/null +++ b/tests/fixtures/valid/type-aliases/discriminated-union-plain-internal.yaml @@ -0,0 +1,64 @@ +openapi: 3.1.0 +info: + title: Discriminated Union Plain Internal Tagging Test + description: | + Internally tagged oneOf whose inline variant objects carry the required + discriminator plus their content properties directly (no allOf wrapper). + version: 1.0.0 +paths: + /preview: + post: + summary: Preview a schedule rule + operationId: preview + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PreviewRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PreviewRequest' +components: + schemas: + ScheduleRule: + oneOf: + - type: object + required: [type, at] + properties: + type: + type: string + enum: [once] + at: + type: string + - type: object + required: [type, every_minutes, anchor_at] + properties: + type: + type: string + enum: [interval] + every_minutes: + type: integer + format: int32 + anchor_at: + type: string + - type: object + required: [type, expression, timezone] + properties: + type: + type: string + enum: [cron] + expression: + type: string + timezone: + type: string + PreviewRequest: + type: object + required: [rule] + properties: + rule: + $ref: '#/components/schemas/ScheduleRule' diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/README.md.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/README.md.golden new file mode 100644 index 000000000..7a9ba9626 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/README.md.golden @@ -0,0 +1,250 @@ +# discriminated-union-plain-internal-tagging-test + +Internally tagged oneOf whose inline variant objects carry the required +discriminator plus their content properties directly (no allOf wrapper). + + +**Version:** 1.0.0 + +## Overview + +This package provides a TypeScript/JavaScript client for the Discriminated Union Plain Internal Tagging Test API. It uses the native [Fetch API](https://fetch.spec.whatwg.org/) for HTTP requests and works in both Node.js and browser environments. + +## Features + +- ✨ **Type-safe** - Full TypeScript support with generated types +- 🚀 **Modern** - Uses native Fetch API, no external HTTP dependencies +- 🔧 **Configurable** - Flexible configuration options +- 🎯 **Middleware** - Support for request/response interceptors +- 📦 **Tree-shakeable** - Import only what you need +- 🌐 **Universal** - Works in Node.js and browsers + +## Installation + +### From npm (published package) + +```bash +npm install discriminated-union-plain-internal-tagging-test +``` + +### From local path (development) + +Add the package to your `package.json` using the `file:` protocol: + +```json +{ + "dependencies": { + "discriminated-union-plain-internal-tagging-test": "file:../../path/to/generated/package" + } +} +``` + +Then run: + +```bash +npm install +``` + +## Quick Start + +```typescript +import { Configuration, DefaultApi } from 'discriminated-union-plain-internal-tagging-test'; + +// Create a configuration +const config = new Configuration({ + basePath: 'https://api.example.com', + headers: { + 'Authorization': 'Bearer YOUR_TOKEN' + } +}); + +// Initialize the API client +const api = new DefaultApi(config); + +// Make API calls +try { + const result = await api.someMethod(); + console.log(result); +} catch (error) { + console.error('API Error:', error); +} +``` + +## Configuration + +The `Configuration` class accepts the following options: + +```typescript +interface ConfigurationParameters { + /** Base URL for API requests */ + basePath?: string; + + /** Custom fetch implementation */ + fetchApi?: typeof fetch; + + /** Request/response middleware */ + middleware?: Middleware[]; + + /** Custom query string serializer */ + queryParamsStringify?: (params: HTTPQuery) => string; + + /** Default headers for all requests */ + headers?: Record; + + /** Credentials mode for requests */ + credentials?: RequestCredentials; +} +``` + +### Example with custom configuration + +```typescript +const config = new Configuration({ + basePath: 'https://api.example.com', + headers: { + 'X-API-Key': 'your-api-key', + 'Content-Type': 'application/json' + }, + credentials: 'include' +}); +``` + +## Middleware + +Add custom middleware to intercept requests and responses: + +```typescript +import { Configuration, Middleware } from 'discriminated-union-plain-internal-tagging-test'; + +const loggingMiddleware: Middleware = { + pre: async (context) => { + console.log('Request:', context.url); + return context; + }, + post: async (context) => { + console.log('Response:', context.response.status); + return context.response; + }, + onError: async (context) => { + console.error('Error:', context.error); + return undefined; + } +}; + +const config = new Configuration({ + basePath: 'https://api.example.com', + middleware: [loggingMiddleware] +}); +``` + +## Error Handling + +The client throws typed errors for different failure scenarios: + +```typescript +import { ResponseError, FetchError, RequiredError } from 'discriminated-union-plain-internal-tagging-test'; + +try { + const result = await api.someMethod(); +} catch (error) { + if (error instanceof ResponseError) { + // HTTP error response (4xx, 5xx) + console.error('HTTP Error:', error.response.status); + } else if (error instanceof FetchError) { + // Network or fetch error + console.error('Network Error:', error.cause); + } else if (error instanceof RequiredError) { + // Missing required parameter + console.error('Missing field:', error.field); + } +} +``` + +## API Reference + +This package exports the following: + +- **Configuration** - Client configuration class +- **BaseAPI** - Base class for all API clients +- **API Classes** - Generated API client classes (e.g., `UserApi`, `PostApi`) +- **Models** - Generated TypeScript interfaces for request/response types +- **Errors** - `ResponseError`, `FetchError`, `RequiredError` +- **Types** - TypeScript type definitions + +## Development + +### Building + +To build the package: + +```bash +npm install +npm run build +``` + +This will compile TypeScript to JavaScript in the `dist/` directory. + +### Building for ESM + +To build ES modules: + +```bash +npm run build +``` + +## TypeScript Support + +This package includes TypeScript type definitions. No additional `@types` package is needed. + +### TypeScript Configuration + +This package works with standard TypeScript configurations. If you're using a bundler-based setup, you may want to configure: + +```json +{ + "compilerOptions": { + "moduleResolution": "bundler" + } +} +``` + +### Type Imports + +```typescript +import type { User, CreateUserRequest } from 'discriminated-union-plain-internal-tagging-test'; + +const user: User = { + id: 1, + name: 'John Doe', + email: 'john@example.com' +}; +``` + +## Browser Support + +This package uses the native Fetch API, which is supported in: + +- Chrome 42+ +- Firefox 39+ +- Safari 10.1+ +- Edge 14+ +- Node.js 18+ (native fetch) +- Node.js <18 (with `node-fetch` polyfill) + +For older browsers, you may need to include a fetch polyfill. + +## License + +This is an auto-generated API client. Please refer to your API documentation for license information. + +## Support + +For issues related to the API itself, please contact the API provider. + +For issues with this generated client, please check the OpenAPI specification used to generate it. + +--- + +**Generated by OpenAPI Generator** + +API Version: 1.0.0 diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/apis/DefaultApi.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/apis/DefaultApi.ts.golden new file mode 100644 index 000000000..54da2b515 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/apis/DefaultApi.ts.golden @@ -0,0 +1,97 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Discriminated Union Plain Internal Tagging Test — 1.0.0 + * Internally tagged oneOf whose inline variant objects carry the required + * discriminator plus their content properties directly (no allOf wrapper). + */ +import type { PreviewRequest, PreviewRequest$Wire } from '../models/PreviewRequest'; +import { previewRequestFromJSON, previewRequestToJSON } from '../models/PreviewRequest'; +import type { Configuration, HTTPQuery, HttpSuccessResponse, InitOverrideFunction } from '../runtime/runtime'; +import { BaseAPI, DefaultConfig, JSONApiResponse, RequiredError, ResponseError } from '../runtime/runtime'; + +export interface ApiPreviewRequest { + body: PreviewRequest; +} + +export type PreviewRawResponse = + | JSONApiResponse & { status: 200 } + | JSONApiResponse & HttpSuccessResponse; + + +export type PreviewErrorDetail = + { kind: 'unexpected'; status: number; raw: Response; value(): Promise }; + +export class PreviewError extends ResponseError { + readonly detail: PreviewErrorDetail; + + constructor(response: Response, detail: PreviewErrorDetail, msg = 'Response returned an error code') { + super(response, msg); + this.detail = detail; + } +} + + +export interface DefaultApiInterface { + previewRaw: (requestParameters: ApiPreviewRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; + preview: (requestParameters: ApiPreviewRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; +} + +export class DefaultApi extends BaseAPI implements DefaultApiInterface { + /** + * Initialize the API client + */ + constructor(configuration?: Configuration) { + super(configuration ?? DefaultConfig); + } + + async previewRaw(requestParameters: ApiPreviewRequest, initOverrides?: RequestInit + | InitOverrideFunction): Promise { + if (requestParameters.body === undefined || requestParameters.body === null) { + throw new RequiredError( + 'body', + 'Required parameter "body" was null or undefined when calling previewRaw().' + ); + } + // Build path with path parameters + const urlPath = `/preview`; + // Build query parameters + const queryParameters: HTTPQuery = {}; + // Build headers + const headerParameters: Record = { + 'Content-Type': 'application/json', + }; + + // Prepare request body + const requestBody = previewRequestToJSON(requestParameters.body); + // Make request + const response = await this.request({ + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: requestBody, + }, initOverrides); + + // Handle responses + if (!(response.status >= 200 && response.status < 300)) { + const errorRaw = response.clone(); + { + let errorValue: Promise | undefined; + throw new PreviewError(response, { kind: 'unexpected', status: response.status, raw: response, value: () => errorValue ??= errorRaw.clone().blob() }); + } + } + if (response.status === 200) { + return new JSONApiResponse(response, (json) => previewRequestFromJSON(json as PreviewRequest$Wire)) as JSONApiResponse & { status: 200 }; + } + else { + return new JSONApiResponse(response) as JSONApiResponse &HttpSuccessResponse; + } + } + + async preview(requestParameters: ApiPreviewRequest, initOverrides?: RequestInit + | InitOverrideFunction): Promise { + const response = await this.previewRaw(requestParameters, initOverrides); + return await response.value() as PreviewRequest; + } +} diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/apis/index.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/apis/index.ts.golden new file mode 100644 index 000000000..e2942c8f6 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/apis/index.ts.golden @@ -0,0 +1,14 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Discriminated Union Plain Internal Tagging Test — 1.0.0 + * Internally tagged oneOf whose inline variant objects carry the required + * discriminator plus their content properties directly (no allOf wrapper). + */ +export type { + ApiPreviewRequest, + PreviewRawResponse, + PreviewErrorDetail, + DefaultApiInterface, +} from './DefaultApi'; +export { DefaultApi, PreviewError } from './DefaultApi'; diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/index.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/index.ts.golden new file mode 100644 index 000000000..ed6bfc5d4 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/index.ts.golden @@ -0,0 +1,10 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Discriminated Union Plain Internal Tagging Test — 1.0.0 + * Internally tagged oneOf whose inline variant objects carry the required + * discriminator plus their content properties directly (no allOf wrapper). + */ +export * from './runtime/runtime'; +export * from './apis'; +export * from './models'; diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/PreviewRequest.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/PreviewRequest.ts.golden new file mode 100644 index 000000000..f01b3122f --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/PreviewRequest.ts.golden @@ -0,0 +1,29 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Discriminated Union Plain Internal Tagging Test — 1.0.0 + * Internally tagged oneOf whose inline variant objects carry the required + * discriminator plus their content properties directly (no allOf wrapper). + */ +import type { ScheduleRule, ScheduleRule$Wire } from './ScheduleRule'; +import { scheduleRuleFromJSON, scheduleRuleToJSON } from './ScheduleRule'; + +export interface PreviewRequest$Wire { + readonly rule: ScheduleRule$Wire; +} + +export interface PreviewRequest { + readonly rule: ScheduleRule; +} + +export function previewRequestFromJSON(json: PreviewRequest$Wire): PreviewRequest { + return { + rule: scheduleRuleFromJSON(json.rule), + }; +} + +export function previewRequestToJSON(value: PreviewRequest): PreviewRequest$Wire { + return { + rule: scheduleRuleToJSON(value.rule), + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRule.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRule.ts.golden new file mode 100644 index 000000000..776c62e72 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRule.ts.golden @@ -0,0 +1,33 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Discriminated Union Plain Internal Tagging Test — 1.0.0 + * Internally tagged oneOf whose inline variant objects carry the required + * discriminator plus their content properties directly (no allOf wrapper). + */ +import type { ScheduleRuleCron, ScheduleRuleCron$Wire } from './ScheduleRuleCron'; +import { scheduleRuleCronFromJSON, scheduleRuleCronToJSON } from './ScheduleRuleCron'; +import type { ScheduleRuleInterval, ScheduleRuleInterval$Wire } from './ScheduleRuleInterval'; +import { scheduleRuleIntervalFromJSON, scheduleRuleIntervalToJSON } from './ScheduleRuleInterval'; +import type { ScheduleRuleOnce, ScheduleRuleOnce$Wire } from './ScheduleRuleOnce'; +import { scheduleRuleOnceFromJSON, scheduleRuleOnceToJSON } from './ScheduleRuleOnce'; + +export type ScheduleRule$Wire = ({ type: 'once' } & ScheduleRuleOnce$Wire) | ({ type: 'interval' } & ScheduleRuleInterval$Wire) | ({ type: 'cron' } & ScheduleRuleCron$Wire); + +export type ScheduleRule = ({ type: 'once' } & ScheduleRuleOnce) | ({ type: 'interval' } & ScheduleRuleInterval) | ({ type: 'cron' } & ScheduleRuleCron); + +export function scheduleRuleFromJSON(json: ScheduleRule$Wire): ScheduleRule { + switch (json.type) { + case 'once': return { ...scheduleRuleOnceFromJSON(json), type: 'once' }; + case 'interval': return { ...scheduleRuleIntervalFromJSON(json), type: 'interval' }; + case 'cron': return { ...scheduleRuleCronFromJSON(json), type: 'cron' }; + } +} + +export function scheduleRuleToJSON(value: ScheduleRule): ScheduleRule$Wire { + switch (value.type) { + case 'once': return { ...scheduleRuleOnceToJSON(value), type: 'once' } as ScheduleRule$Wire; + case 'interval': return { ...scheduleRuleIntervalToJSON(value), type: 'interval' } as ScheduleRule$Wire; + case 'cron': return { ...scheduleRuleCronToJSON(value), type: 'cron' } as ScheduleRule$Wire; + } +} diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleCron.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleCron.ts.golden new file mode 100644 index 000000000..243c7a6c5 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleCron.ts.golden @@ -0,0 +1,34 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Discriminated Union Plain Internal Tagging Test — 1.0.0 + * Internally tagged oneOf whose inline variant objects carry the required + * discriminator plus their content properties directly (no allOf wrapper). + */ +export interface ScheduleRuleCron$Wire { + readonly expression: string; + readonly timezone: string; + readonly type: 'cron'; +} + +export interface ScheduleRuleCron { + readonly expression: string; + readonly timezone: string; + readonly type: 'cron'; +} + +export function scheduleRuleCronFromJSON(json: ScheduleRuleCron$Wire): ScheduleRuleCron { + return { + expression: json.expression, + timezone: json.timezone, + type: json.type, + }; +} + +export function scheduleRuleCronToJSON(value: ScheduleRuleCron): ScheduleRuleCron$Wire { + return { + expression: value.expression, + timezone: value.timezone, + type: value.type, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleInterval.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleInterval.ts.golden new file mode 100644 index 000000000..6c735811d --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleInterval.ts.golden @@ -0,0 +1,34 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Discriminated Union Plain Internal Tagging Test — 1.0.0 + * Internally tagged oneOf whose inline variant objects carry the required + * discriminator plus their content properties directly (no allOf wrapper). + */ +export interface ScheduleRuleInterval$Wire { + readonly anchor_at: string; + readonly every_minutes: number; + readonly type: 'interval'; +} + +export interface ScheduleRuleInterval { + readonly anchorAt: string; + readonly everyMinutes: number; + readonly type: 'interval'; +} + +export function scheduleRuleIntervalFromJSON(json: ScheduleRuleInterval$Wire): ScheduleRuleInterval { + return { + anchorAt: json.anchor_at, + everyMinutes: json.every_minutes, + type: json.type, + }; +} + +export function scheduleRuleIntervalToJSON(value: ScheduleRuleInterval): ScheduleRuleInterval$Wire { + return { + anchor_at: value.anchorAt, + every_minutes: value.everyMinutes, + type: value.type, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleOnce.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleOnce.ts.golden new file mode 100644 index 000000000..bd661f6e2 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/ScheduleRuleOnce.ts.golden @@ -0,0 +1,30 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Discriminated Union Plain Internal Tagging Test — 1.0.0 + * Internally tagged oneOf whose inline variant objects carry the required + * discriminator plus their content properties directly (no allOf wrapper). + */ +export interface ScheduleRuleOnce$Wire { + readonly at: string; + readonly type: 'once'; +} + +export interface ScheduleRuleOnce { + readonly at: string; + readonly type: 'once'; +} + +export function scheduleRuleOnceFromJSON(json: ScheduleRuleOnce$Wire): ScheduleRuleOnce { + return { + at: json.at, + type: json.type, + }; +} + +export function scheduleRuleOnceToJSON(value: ScheduleRuleOnce): ScheduleRuleOnce$Wire { + return { + at: value.at, + type: value.type, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/index.ts.golden new file mode 100644 index 000000000..af2322e1c --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/models/index.ts.golden @@ -0,0 +1,17 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Discriminated Union Plain Internal Tagging Test — 1.0.0 + * Internally tagged oneOf whose inline variant objects carry the required + * discriminator plus their content properties directly (no allOf wrapper). + */ +export type { PreviewRequest, PreviewRequest$Wire } from './PreviewRequest'; +export { previewRequestFromJSON, previewRequestToJSON } from './PreviewRequest'; +export type { ScheduleRule, ScheduleRule$Wire } from './ScheduleRule'; +export { scheduleRuleFromJSON, scheduleRuleToJSON } from './ScheduleRule'; +export type { ScheduleRuleCron, ScheduleRuleCron$Wire } from './ScheduleRuleCron'; +export { scheduleRuleCronFromJSON, scheduleRuleCronToJSON } from './ScheduleRuleCron'; +export type { ScheduleRuleInterval, ScheduleRuleInterval$Wire } from './ScheduleRuleInterval'; +export { scheduleRuleIntervalFromJSON, scheduleRuleIntervalToJSON } from './ScheduleRuleInterval'; +export type { ScheduleRuleOnce, ScheduleRuleOnce$Wire } from './ScheduleRuleOnce'; +export { scheduleRuleOnceFromJSON, scheduleRuleOnceToJSON } from './ScheduleRuleOnce'; diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/package.json.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/package.json.golden new file mode 100644 index 000000000..4cd616457 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/package.json.golden @@ -0,0 +1,26 @@ +{ + "description": "Internally tagged oneOf whose inline variant objects carry the required\ndiscriminator plus their content properties directly (no allOf wrapper).\n", + "exports": { + ".": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "keywords": [ + "openapi", + "api-client", + "typescript", + "generated" + ], + "main": "./dist/index.js", + "name": "discriminated-union-plain-internal-tagging-test", + "scripts": { + "build": "tsc" + }, + "type": "module", + "types": "./dist/index.d.ts", + "version": "1.0.0" +} \ No newline at end of file diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/runtime/runtime.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/runtime/runtime.ts.golden new file mode 100644 index 000000000..7582dcd92 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/runtime/runtime.ts.golden @@ -0,0 +1,492 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Discriminated Union Plain Internal Tagging Test — 1.0.0 + * Internally tagged oneOf whose inline variant objects carry the required + * discriminator plus their content properties directly (no allOf wrapper). + */ +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +export interface ConfigurationParameters { + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string | (() => string | Promise); // parameter for basic security + password?: string | (() => string | Promise); // parameter for basic security + apiKey?: string | Promise | ((name: string) => string | Promise); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request +} + +export class Configuration { + private configuration: ConfigurationParameters; + + constructor(configuration: ConfigurationParameters = {}) { + this.configuration = configuration; + } + + set config(configuration: ConfigurationParameters) { + this.configuration = configuration; + } + + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } + + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } + + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } + + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } + + get username(): (() => string | Promise) | undefined { + const username = this.configuration.username; + if (username) { + return typeof username === 'function' ? username : async () => username; + } + return undefined; + } + + get password(): (() => string | Promise) | undefined { + const password = this.configuration.password; + if (password) { + return typeof password === 'function' ? password : async () => password; + } + return undefined; + } + + get apiKey(): ((name: string) => string | Promise) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } + + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } + + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } + + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } +} + +export const DefaultConfig = new Configuration(); + +/** + * This is the base class for all generated API classes. + */ +export class BaseAPI { + + private static readonly jsonRegex = new RegExp('^(:?application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i'); + protected configuration: Configuration; + private middleware: Middleware[]; + + constructor(configuration: Configuration = DefaultConfig) { + this.configuration = configuration; + this.middleware = configuration.middleware; + } + + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } + + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } + + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + protected isJsonMime(mime: string | null | undefined): boolean { + if (!mime) { + return false; + } + return BaseAPI.jsonRegex.test(mime); + } + + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + return await this.fetchApi(url, init); + } + + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } + + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + + const token = await this.configuration.accessToken?.(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (!headers['Authorization']) { + const username = await this.configuration.username?.(); + const password = await this.configuration.password?.(); + if (username && password) { + headers['Authorization'] = `Basic ${btoa(`${username}:${password}`)}`; + } + } + + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; + + const initParams: HTTPRequestInit = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + signal: context.signal, + }; + + const overriddenInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + }; + + let body: BodyInit | null | undefined; + if (isFormData(overriddenInit.body) + || (overriddenInit.body instanceof URLSearchParams) + || isBlob(overriddenInit.body)) { + body = overriddenInit.body; + } else if (this.isJsonMime(headers['Content-Type'])) { + body = JSON.stringify(overriddenInit.body); + } else { + body = overriddenInit.body as BodyInit | null | undefined; + } + + const init: RequestInit = { + ...overriddenInit, + body + }; + + return { url, init }; + } + + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response: Response | undefined = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } + + /** + * Create a shallow clone of `this` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as new (configuration: Configuration) => T; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } +} + +function isBlob(value: unknown): value is Blob { + return typeof Blob !== 'undefined' && value instanceof Blob; +} + +function isFormData(value: unknown): value is FormData { + return typeof FormData !== "undefined" && value instanceof FormData; +} + +export class ResponseError extends Error { + override name = "ResponseError" as const; + public response: Response; + + constructor(response: Response, msg?: string) { + super(msg); + this.response = response; + } +} + +export class FetchError extends Error { + override name = "FetchError" as const; + public cause: Error; + + constructor(cause: Error, msg?: string) { + super(msg); + this.cause = cause; + } +} + +export class RequiredError extends Error { + override name = "RequiredError" as const; + public field: string; + + constructor(field: string, msg?: string) { + super(msg); + this.field = field; + } +} + +export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; + +export type Json = unknown; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; +export type HTTPHeaders = { [key: string]: string }; +export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; +export type HTTPBody = Json | Blob | FormData | URLSearchParams; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents +export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody; signal?: AbortSignal }; +export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; + +export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise + +export interface FetchParams { + url: string; + init: RequestInit; +} + +export interface RequestOpts { + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; + signal?: AbortSignal; +} + +export function querystring(params: HTTPQuery, prefix: string = ''): string { + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); +} + +function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { + const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(`&${encodeURIComponent(fullKey)}=`); + return `${encodeURIComponent(fullKey)}=${multiValue}`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; +} + +export function exists(json: Record, key: string): boolean { + const value = json[key]; + return value !== null && value !== undefined; +} + +export function mapValues(data: Record, fn: (item: T) => U): Record { + const result: Record = {}; + for (const key of Object.keys(data)) { + result[key] = fn(data[key]); + } + return result; +} + +export function canConsumeForm(consumes: Consume[]): boolean { + for (const consume of consumes) { + if ('multipart/form-data' === consume.contentType) { + return true; + } + } + return false; +} + +export interface Consume { + contentType: string; +} + +export interface RequestContext { + fetch: FetchAPI; + url: string; + init: RequestInit; +} + +export interface ResponseContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; +} + +export interface ErrorContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; +} + +export interface Middleware { + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; +} + +export interface ApiResponse { + raw: Response; + value(): Promise; +} + +export type HttpSuccessStatus = + | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 + | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 + | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 + | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 + | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 + | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 + | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 + | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 + | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 + | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299; + +export type HttpSuccessResponse = { status: HttpSuccessStatus }; + +export interface ResponseTransformer { + (json: unknown): T; +} + +class ApiResponseBase { + public readonly status: number; + public readonly ok: boolean; + public readonly statusText: string; + public readonly headers: Headers; + public readonly raw: Response; + + constructor(raw: Response) { + this.raw = raw; + this.status = raw.status; + this.ok = raw.ok; + this.statusText = raw.statusText; + this.headers = raw.headers; + } +} + +export class JSONApiResponse extends ApiResponseBase { + private transformer: ResponseTransformer; + + constructor(raw: Response, transformer: ResponseTransformer = (jsonValue: unknown) => jsonValue as T) { + super(raw); + this.transformer = transformer; + } + + async value(): Promise { + return this.transformer(await this.raw.json()); + } +} + +export class VoidApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return undefined; + } +} + +export class BlobApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return await this.raw.blob(); + }; +} + +export class TextApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return await this.raw.text(); + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/tsconfig.esm.json.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/tsconfig.esm.json.golden new file mode 100644 index 000000000..bb8350cb6 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/tsconfig.esm.json.golden @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "ES2020", + "outDir": "dist/esm" + }, + "extends": "./tsconfig.json" +} \ No newline at end of file diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/tsconfig.json.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/tsconfig.json.golden new file mode 100644 index 000000000..4b3a8cf7f --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-tagged-union-plain-internal/tsconfig.json.golden @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": [ + "ES2020", + "DOM" + ], + "module": "ES2020", + "moduleResolution": "bundler", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./", + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2020", + "typeRoots": [ + "node_modules/@types" + ] + }, + "exclude": [ + "dist", + "node_modules" + ], + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/tests/golden_tests_typescript_fetch.rs b/tests/golden_tests_typescript_fetch.rs index f07861fb4..984ed783d 100644 --- a/tests/golden_tests_typescript_fetch.rs +++ b/tests/golden_tests_typescript_fetch.rs @@ -315,6 +315,25 @@ property_naming = "camelCase" ); } +#[test] +#[traced_test] +fn test_property_naming_camel_case_tagged_union_plain_internal_golden() { + let config: toml::value::Table = toml::from_str( + r#" +property_naming = "camelCase" +"#, + ) + .unwrap(); + let generator = TypeScriptFetchCodeGenerator::new(config); + run_golden_test( + &generator, + golden_dir(), + "ts-property-naming-camel-case-tagged-union-plain-internal", + "valid/type-aliases/discriminated-union-plain-internal.yaml", + UPDATE_HINT, + ); +} + #[test] #[traced_test] fn test_property_naming_camel_case_intersection_golden() {