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() {