diff --git a/docs/generators/payloads.md b/docs/generators/payloads.md index 233b5cda..9ab2375b 100644 --- a/docs/generators/payloads.md +++ b/docs/generators/payloads.md @@ -24,6 +24,38 @@ This is supported through the following inputs: `asyncapi`, `openapi` It supports the following languages; [`typescript`](#typescript) +## Companion Interface + +Every generated **object** payload file exports **two** symbols: the payload +class (``) and a plain-data companion interface (`Interface`) +declared above it. The class constructor takes the interface +(`constructor(input: Interface)`), so the two always stay in sync. + +```typescript +export { UserSignedUp, UserSignedUpInterface }; +``` + +This lets you pass a **plain object** wherever a channel expects a payload — +you do not have to construct the class yourself: + +```typescript +// Both of these are accepted by every generated publish/request helper: +await publishToUserSignedup({ message: { displayName: 'Jane', email: 'jane@example.com' }, nc }); +await publishToUserSignedup({ message: new UserSignedUp({ displayName: 'Jane', email: 'jane@example.com' }), nc }); +``` + +Channel consumers type their message argument as the union +`Interface | ` and normalize it to a class instance internally (via +an `instanceof` guard) before calling `.marshal()`. The plain-object form is +purely an ergonomic convenience; the generated code always marshals a class +instance. + +This applies to **object** payloads only. Non-object payloads +(unions, primitives, arrays, and enums) keep their `type`/`enum` shape and +free-function marshalling — they have no companion interface and are exported as +a single symbol. See the [protocols documentation](../protocols) for how each +channel accepts payloads. + ## Languages Each language has a set of constraints which means that some typed model types are either supported or not, or it might just be the code generation library that does not yet support it. diff --git a/examples/ecommerce-asyncapi-payload/README.md b/examples/ecommerce-asyncapi-payload/README.md index 4573ed8b..1c9dbcd3 100644 --- a/examples/ecommerce-asyncapi-payload/README.md +++ b/examples/ecommerce-asyncapi-payload/README.md @@ -14,6 +14,10 @@ A comprehensive example showing how to generate TypeScript payload models from A - JSON Schema validation - Type-safe TypeScript classes - Serialization/deserialization +- **Companion interfaces** — every object payload exports a plain-data + `Interface` alongside its class, so you can pass a plain object anywhere + a payload is accepted (no `new` required). See `demonstrateCompanionInterface` + in `src/index.ts`. **Usage:** ```bash diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/Address.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/Address.ts index 7ee8a7e9..ba9d6f3b 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/Address.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/Address.ts @@ -1,5 +1,13 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; +import {default as addFormats} from 'ajv-formats'; +interface AddressInterface { + street: string + city: string + state?: string + country: string + postalCode: string + additionalProperties?: Record +} class Address { private _street: string; private _city: string; @@ -8,14 +16,7 @@ class Address { private _postalCode: string; private _additionalProperties?: Record; - constructor(input: { - street: string, - city: string, - state?: string, - country: string, - postalCode: string, - additionalProperties?: Record, - }) { + constructor(input: AddressInterface) { this._street = input.street; this._city = input.city; this._state = input.state; @@ -33,6 +34,9 @@ class Address { get state(): string | undefined { return this._state; } set state(state: string | undefined) { this._state = state; } + /** + * ISO 3166-1 alpha-2 country code + */ get country(): string { return this._country; } set country(country: string) { this._country = country; } @@ -100,6 +104,9 @@ class Address { public static theCodeGenSchema = {"type":"object","required":["street","city","country","postalCode"],"properties":{"street":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2 country code"},"postalCode":{"type":"string"}}}; public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. const parsedData = typeof data === 'string' ? JSON.parse(data) : data; const validate = ajvValidatorFunction ?? this.createValidator(context) return { @@ -110,9 +117,10 @@ class Address { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } } -export { Address }; \ No newline at end of file +export { Address, AddressInterface }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/Attachment.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/Attachment.ts index 9f70a552..fb3b973c 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/Attachment.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/Attachment.ts @@ -1,17 +1,18 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; +import {default as addFormats} from 'ajv-formats'; +interface AttachmentInterface { + filename?: string + contentType?: string + data?: string + additionalProperties?: Record +} class Attachment { private _filename?: string; private _contentType?: string; private _data?: string; private _additionalProperties?: Record; - constructor(input: { - filename?: string, - contentType?: string, - data?: string, - additionalProperties?: Record, - }) { + constructor(input: AttachmentInterface) { this._filename = input.filename; this._contentType = input.contentType; this._data = input.data; @@ -76,6 +77,9 @@ class Attachment { public static theCodeGenSchema = {"type":"object","properties":{"filename":{"type":"string"},"contentType":{"type":"string"},"data":{"type":"string","contentEncoding":"base64"}}}; public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. const parsedData = typeof data === 'string' ? JSON.parse(data) : data; const validate = ajvValidatorFunction ?? this.createValidator(context) return { @@ -86,9 +90,10 @@ class Attachment { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } } -export { Attachment }; \ No newline at end of file +export { Attachment, AttachmentInterface }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/Currency.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/Currency.ts index c8f709d0..cda851fc 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/Currency.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/Currency.ts @@ -1,4 +1,7 @@ +/** + * Currency code + */ enum Currency { USD = "USD", EUR = "EUR", diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/EmailNotification.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/EmailNotification.ts index e3945297..fafb3f54 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/EmailNotification.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/EmailNotification.ts @@ -1,6 +1,13 @@ import {Attachment} from './Attachment'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; +import {default as addFormats} from 'ajv-formats'; +interface EmailNotificationInterface { + recipientId: string + subject: string + body: string + attachments?: Attachment[] + additionalProperties?: Record +} class EmailNotification { private _type: 'email' = 'email'; private _recipientId: string; @@ -9,13 +16,7 @@ class EmailNotification { private _attachments?: Attachment[]; private _additionalProperties?: Record; - constructor(input: { - recipientId: string, - subject: string, - body: string, - attachments?: Attachment[], - additionalProperties?: Record, - }) { + constructor(input: EmailNotificationInterface) { this._recipientId = input.recipientId; this._subject = input.subject; this._body = input.body; @@ -57,7 +58,7 @@ class EmailNotification { if(this.attachments !== undefined) { let attachmentsJsonValues: any[] = []; for (const unionItem of this.attachments) { - attachmentsJsonValues.push(`${unionItem.marshal()}`); + attachmentsJsonValues.push(`${unionItem && typeof unionItem === 'object' && 'marshal' in unionItem && typeof unionItem.marshal === 'function' ? unionItem.marshal() : JSON.stringify(unionItem)}`); } json += `"attachments": [${attachmentsJsonValues.join(',')}],`; } @@ -87,7 +88,7 @@ class EmailNotification { } if (obj["attachments"] !== undefined) { instance.attachments = obj["attachments"] == null - ? null + ? undefined : obj["attachments"].map((item: any) => Attachment.unmarshal(item)); } @@ -101,6 +102,9 @@ class EmailNotification { public static theCodeGenSchema = {"type":"object","required":["type","recipientId","subject","body"],"properties":{"type":{"const":"email"},"recipientId":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string"},"contentType":{"type":"string"},"data":{"type":"string","contentEncoding":"base64"}}}}}}; public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. const parsedData = typeof data === 'string' ? JSON.parse(data) : data; const validate = ajvValidatorFunction ?? this.createValidator(context) return { @@ -111,9 +115,10 @@ class EmailNotification { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } } -export { EmailNotification }; \ No newline at end of file +export { EmailNotification, EmailNotificationInterface }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/NotificationSent.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/NotificationSent.ts index b3a6c7f8..fc6dd656 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/NotificationSent.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/NotificationSent.ts @@ -2,25 +2,9 @@ import {EmailNotification} from './EmailNotification'; import {SmsNotification} from './SmsNotification'; import {PushNotification} from './PushNotification'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; +import {default as addFormats} from 'ajv-formats'; type NotificationSent = EmailNotification | SmsNotification | PushNotification; -export const theCodeGenSchema = {"type":"object","$schema":"http://json-schema.org/draft-07/schema","oneOf":[{"type":"object","required":["type","recipientId","subject","body"],"properties":{"type":{"const":"email"},"recipientId":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string"},"contentType":{"type":"string"},"data":{"type":"string","contentEncoding":"base64"}}}}}},{"type":"object","required":["type","recipientId","message"],"properties":{"type":{"const":"sms"},"recipientId":{"type":"string"},"message":{"type":"string","maxLength":160}}},{"type":"object","required":["type","recipientId","title","body"],"properties":{"type":{"const":"push"},"recipientId":{"type":"string"},"title":{"type":"string"},"body":{"type":"string"},"badge":{"type":"integer","minimum":0}}}],"$id":"NotificationSent"}; -export function validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { - const {data, ajvValidatorFunction} = context ?? {}; - const parsedData = typeof data === 'string' ? JSON.parse(data) : data; - const validate = ajvValidatorFunction ?? createValidator(context) - return { - valid: validate(parsedData), - errors: validate.errors ?? undefined, - }; -} -export function createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { - const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; - addFormats(ajvInstance); - const validate = ajvInstance.compile(theCodeGenSchema); - return validate; -} export function unmarshal(json: any): NotificationSent { if(typeof json === 'object') { if(json.type === 'email') { @@ -48,4 +32,26 @@ return payload.marshal(); return JSON.stringify(payload); } +export const theCodeGenSchema = {"$schema":"http://json-schema.org/draft-07/schema","oneOf":[{"type":"object","required":["type","recipientId","subject","body"],"properties":{"type":{"const":"email"},"recipientId":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string"},"contentType":{"type":"string"},"data":{"type":"string","contentEncoding":"base64"}}}}}},{"type":"object","required":["type","recipientId","message"],"properties":{"type":{"const":"sms"},"recipientId":{"type":"string"},"message":{"type":"string","maxLength":160}}},{"type":"object","required":["type","recipientId","title","body"],"properties":{"type":{"const":"push"},"recipientId":{"type":"string"},"title":{"type":"string"},"body":{"type":"string"},"badge":{"type":"integer","minimum":0}}}],"$id":"NotificationSent"}; +export function validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; +} +export function createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + + const validate = ajvInstance.compile(theCodeGenSchema); + return validate; +} + + export { NotificationSent }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/OrderCreated.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/OrderCreated.ts index 9616f7a5..44306254 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/OrderCreated.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/OrderCreated.ts @@ -2,7 +2,17 @@ import {OrderItem} from './OrderItem'; import {Currency} from './Currency'; import {Address} from './Address'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; +import {default as addFormats} from 'ajv-formats'; +interface OrderCreatedInterface { + orderId: string + customerId: string + items: OrderItem[] + totalAmount: number + currency: Currency + shippingAddress?: Address + metadata?: Record + additionalProperties?: Record +} class OrderCreated { private _orderId: string; private _customerId: string; @@ -13,16 +23,7 @@ class OrderCreated { private _metadata?: Record; private _additionalProperties?: Record; - constructor(input: { - orderId: string, - customerId: string, - items: OrderItem[], - totalAmount: number, - currency: Currency, - shippingAddress?: Address, - metadata?: Record, - additionalProperties?: Record, - }) { + constructor(input: OrderCreatedInterface) { this._orderId = input.orderId; this._customerId = input.customerId; this._items = input.items; @@ -33,31 +34,46 @@ class OrderCreated { this._additionalProperties = input.additionalProperties; } + /** + * Unique order identifier + */ get orderId(): string { return this._orderId; } set orderId(orderId: string) { this._orderId = orderId; } + /** + * Customer who placed the order + */ get customerId(): string { return this._customerId; } set customerId(customerId: string) { this._customerId = customerId; } get items(): OrderItem[] { return this._items; } set items(items: OrderItem[]) { this._items = items; } + /** + * Total order amount in cents + */ get totalAmount(): number { return this._totalAmount; } set totalAmount(totalAmount: number) { this._totalAmount = totalAmount; } + /** + * Currency code + */ get currency(): Currency { return this._currency; } set currency(currency: Currency) { this._currency = currency; } get shippingAddress(): Address | undefined { return this._shippingAddress; } set shippingAddress(shippingAddress: Address | undefined) { this._shippingAddress = shippingAddress; } + /** + * Additional metadata + */ get metadata(): Record | undefined { return this._metadata; } set metadata(metadata: Record | undefined) { this._metadata = metadata; } get additionalProperties(): Record | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } - public marshal(): string { + public marshal() : string { let json = '{' if(this.orderId !== undefined) { json += `"orderId": ${typeof this.orderId === 'number' || typeof this.orderId === 'boolean' ? this.orderId : JSON.stringify(this.orderId)},`; @@ -68,7 +84,7 @@ class OrderCreated { if(this.items !== undefined) { let itemsJsonValues: any[] = []; for (const unionItem of this.items) { - itemsJsonValues.push(`${unionItem.marshal()}`); + itemsJsonValues.push(`${unionItem && typeof unionItem === 'object' && 'marshal' in unionItem && typeof unionItem.marshal === 'function' ? unionItem.marshal() : JSON.stringify(unionItem)}`); } json += `"items": [${itemsJsonValues.join(',')}],`; } @@ -79,7 +95,7 @@ class OrderCreated { json += `"currency": ${typeof this.currency === 'number' || typeof this.currency === 'boolean' ? this.currency : JSON.stringify(this.currency)},`; } if(this.shippingAddress !== undefined) { - json += `"shippingAddress": ${this.shippingAddress.marshal()},`; + json += `"shippingAddress": ${this.shippingAddress && typeof this.shippingAddress === 'object' && 'marshal' in this.shippingAddress && typeof this.shippingAddress.marshal === 'function' ? this.shippingAddress.marshal() : JSON.stringify(this.shippingAddress)},`; } if(this.metadata !== undefined) { json += `"metadata": ${typeof this.metadata === 'number' || typeof this.metadata === 'boolean' ? this.metadata : JSON.stringify(this.metadata)},`; @@ -133,6 +149,9 @@ class OrderCreated { public static theCodeGenSchema = {"type":"object","required":["orderId","customerId","items","totalAmount","currency"],"properties":{"orderId":{"type":"string","format":"uuid","description":"Unique order identifier"},"customerId":{"type":"string","format":"uuid","description":"Customer who placed the order"},"items":{"type":"array","items":{"type":"object","required":["productId","quantity","unitPrice"],"properties":{"productId":{"type":"string","description":"Product identifier"},"quantity":{"type":"integer","minimum":1,"description":"Number of items ordered"},"unitPrice":{"type":"number","minimum":0,"description":"Price per unit in cents"},"metadata":{"type":"object","additionalProperties":true,"description":"Additional metadata"}}}},"totalAmount":{"type":"number","minimum":0,"description":"Total order amount in cents"},"currency":{"type":"string","enum":["USD","EUR","GBP"],"description":"Currency code"},"shippingAddress":{"type":"object","required":["street","city","country","postalCode"],"properties":{"street":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2 country code"},"postalCode":{"type":"string"}}},"metadata":{"type":"object","additionalProperties":true,"description":"Additional metadata"}},"$id":"OrderCreated"}; public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. const parsedData = typeof data === 'string' ? JSON.parse(data) : data; const validate = ajvValidatorFunction ?? this.createValidator(context) return { @@ -143,9 +162,10 @@ class OrderCreated { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } } -export { OrderCreated }; \ No newline at end of file +export { OrderCreated, OrderCreatedInterface }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/OrderEventsPayload.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/OrderEventsPayload.ts index aae2e325..8eef81a2 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/OrderEventsPayload.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/OrderEventsPayload.ts @@ -1,27 +1,11 @@ import {OrderCreated} from './OrderCreated'; import {OrderStatusChanged} from './OrderStatusChanged'; -import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; import {Currency} from './Currency'; import {OrderStatus} from './OrderStatus'; +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; type OrderEventsPayload = OrderCreated | OrderStatusChanged; -export const theCodeGenSchema = {"type":"object","$schema":"http://json-schema.org/draft-07/schema","oneOf":[{"type":"object","required":["orderId","customerId","items","totalAmount","currency"],"properties":{"orderId":{"type":"string","format":"uuid","description":"Unique order identifier"},"customerId":{"type":"string","format":"uuid","description":"Customer who placed the order"},"items":{"type":"array","items":{"type":"object","required":["productId","quantity","unitPrice"],"properties":{"productId":{"type":"string","description":"Product identifier"},"quantity":{"type":"integer","minimum":1,"description":"Number of items ordered"},"unitPrice":{"type":"number","minimum":0,"description":"Price per unit in cents"},"metadata":{"type":"object","additionalProperties":true,"description":"Additional metadata"}}}},"totalAmount":{"type":"number","minimum":0,"description":"Total order amount in cents"},"currency":{"type":"string","enum":["USD","EUR","GBP"],"description":"Currency code"},"shippingAddress":{"type":"object","required":["street","city","country","postalCode"],"properties":{"street":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2 country code"},"postalCode":{"type":"string"}}},"metadata":{"type":"object","additionalProperties":true,"description":"Additional metadata"}},"$id":"OrderCreated"},{"type":"object","required":["orderId","previousStatus","newStatus","timestamp"],"properties":{"orderId":{"type":"string","format":"uuid"},"previousStatus":{"type":"string","enum":["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]},"newStatus":{"type":"string","enum":["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]},"timestamp":{"type":"string","format":"date-time"},"reason":{"type":"string","description":"Reason for status change"}},"$id":"OrderStatusChanged"}],"$id":"OrderEventsPayload"}; -export function validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { - const {data, ajvValidatorFunction} = context ?? {}; - const parsedData = typeof data === 'string' ? JSON.parse(data) : data; - const validate = ajvValidatorFunction ?? createValidator(context) - return { - valid: validate(parsedData), - errors: validate.errors ?? undefined, - }; -} -export function createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { - const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; - addFormats(ajvInstance); - const validate = ajvInstance.compile(theCodeGenSchema); - return validate; -} export function unmarshal(json: any): OrderEventsPayload { if(typeof json === 'object') { if(json.currency === Currency.USD) { @@ -43,4 +27,26 @@ return payload.marshal(); return JSON.stringify(payload); } +export const theCodeGenSchema = {"$schema":"http://json-schema.org/draft-07/schema","oneOf":[{"type":"object","required":["orderId","customerId","items","totalAmount","currency"],"properties":{"orderId":{"type":"string","format":"uuid","description":"Unique order identifier"},"customerId":{"type":"string","format":"uuid","description":"Customer who placed the order"},"items":{"type":"array","items":{"type":"object","required":["productId","quantity","unitPrice"],"properties":{"productId":{"type":"string","description":"Product identifier"},"quantity":{"type":"integer","minimum":1,"description":"Number of items ordered"},"unitPrice":{"type":"number","minimum":0,"description":"Price per unit in cents"},"metadata":{"type":"object","additionalProperties":true,"description":"Additional metadata"}}}},"totalAmount":{"type":"number","minimum":0,"description":"Total order amount in cents"},"currency":{"type":"string","enum":["USD","EUR","GBP"],"description":"Currency code"},"shippingAddress":{"type":"object","required":["street","city","country","postalCode"],"properties":{"street":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2 country code"},"postalCode":{"type":"string"}}},"metadata":{"type":"object","additionalProperties":true,"description":"Additional metadata"}},"$id":"OrderCreated"},{"type":"object","required":["orderId","previousStatus","newStatus","timestamp"],"properties":{"orderId":{"type":"string","format":"uuid"},"previousStatus":{"type":"string","enum":["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]},"newStatus":{"type":"string","enum":["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]},"timestamp":{"type":"string","format":"date-time"},"reason":{"type":"string","description":"Reason for status change"}},"$id":"OrderStatusChanged"}],"$id":"OrderEventsPayload"}; +export function validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { + const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. + const parsedData = typeof data === 'string' ? JSON.parse(data) : data; + const validate = ajvValidatorFunction ?? createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; +} +export function createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + + const validate = ajvInstance.compile(theCodeGenSchema); + return validate; +} + + export { OrderEventsPayload }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/OrderItem.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/OrderItem.ts index fbe93e22..8f9d67f4 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/OrderItem.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/OrderItem.ts @@ -1,5 +1,12 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; +import {default as addFormats} from 'ajv-formats'; +interface OrderItemInterface { + productId: string + quantity: number + unitPrice: number + metadata?: Record + additionalProperties?: Record +} class OrderItem { private _productId: string; private _quantity: number; @@ -7,13 +14,7 @@ class OrderItem { private _metadata?: Record; private _additionalProperties?: Record; - constructor(input: { - productId: string, - quantity: number, - unitPrice: number, - metadata?: Record, - additionalProperties?: Record, - }) { + constructor(input: OrderItemInterface) { this._productId = input.productId; this._quantity = input.quantity; this._unitPrice = input.unitPrice; @@ -21,15 +22,27 @@ class OrderItem { this._additionalProperties = input.additionalProperties; } + /** + * Product identifier + */ get productId(): string { return this._productId; } set productId(productId: string) { this._productId = productId; } + /** + * Number of items ordered + */ get quantity(): number { return this._quantity; } set quantity(quantity: number) { this._quantity = quantity; } + /** + * Price per unit in cents + */ get unitPrice(): number { return this._unitPrice; } set unitPrice(unitPrice: number) { this._unitPrice = unitPrice; } + /** + * Additional metadata + */ get metadata(): Record | undefined { return this._metadata; } set metadata(metadata: Record | undefined) { this._metadata = metadata; } @@ -88,6 +101,9 @@ class OrderItem { public static theCodeGenSchema = {"type":"object","required":["productId","quantity","unitPrice"],"properties":{"productId":{"type":"string","description":"Product identifier"},"quantity":{"type":"integer","minimum":1,"description":"Number of items ordered"},"unitPrice":{"type":"number","minimum":0,"description":"Price per unit in cents"},"metadata":{"type":"object","additionalProperties":true,"description":"Additional metadata"}}}; public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. const parsedData = typeof data === 'string' ? JSON.parse(data) : data; const validate = ajvValidatorFunction ?? this.createValidator(context) return { @@ -98,9 +114,10 @@ class OrderItem { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } } -export { OrderItem }; \ No newline at end of file +export { OrderItem, OrderItemInterface }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/OrderStatusChanged.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/OrderStatusChanged.ts index 54e2456e..830e92da 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/OrderStatusChanged.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/OrderStatusChanged.ts @@ -1,22 +1,23 @@ import {OrderStatus} from './OrderStatus'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; +import {default as addFormats} from 'ajv-formats'; +interface OrderStatusChangedInterface { + orderId: string + previousStatus: OrderStatus + newStatus: OrderStatus + timestamp: Date + reason?: string + additionalProperties?: Record +} class OrderStatusChanged { private _orderId: string; private _previousStatus: OrderStatus; private _newStatus: OrderStatus; - private _timestamp: string; + private _timestamp: Date; private _reason?: string; private _additionalProperties?: Record; - constructor(input: { - orderId: string, - previousStatus: OrderStatus, - newStatus: OrderStatus, - timestamp: string, - reason?: string, - additionalProperties?: Record, - }) { + constructor(input: OrderStatusChangedInterface) { this._orderId = input.orderId; this._previousStatus = input.previousStatus; this._newStatus = input.newStatus; @@ -34,9 +35,12 @@ class OrderStatusChanged { get newStatus(): OrderStatus { return this._newStatus; } set newStatus(newStatus: OrderStatus) { this._newStatus = newStatus; } - get timestamp(): string { return this._timestamp; } - set timestamp(timestamp: string) { this._timestamp = timestamp; } + get timestamp(): Date { return this._timestamp; } + set timestamp(timestamp: Date) { this._timestamp = timestamp; } + /** + * Reason for status change + */ get reason(): string | undefined { return this._reason; } set reason(reason: string | undefined) { this._reason = reason; } @@ -85,7 +89,7 @@ class OrderStatusChanged { instance.newStatus = obj["newStatus"]; } if (obj["timestamp"] !== undefined) { - instance.timestamp = obj["timestamp"]; + instance.timestamp = obj["timestamp"] == null ? null : new Date(obj["timestamp"]); } if (obj["reason"] !== undefined) { instance.reason = obj["reason"]; @@ -101,6 +105,9 @@ class OrderStatusChanged { public static theCodeGenSchema = {"type":"object","required":["orderId","previousStatus","newStatus","timestamp"],"properties":{"orderId":{"type":"string","format":"uuid"},"previousStatus":{"type":"string","enum":["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]},"newStatus":{"type":"string","enum":["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]},"timestamp":{"type":"string","format":"date-time"},"reason":{"type":"string","description":"Reason for status change"}},"$id":"OrderStatusChanged"}; public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. const parsedData = typeof data === 'string' ? JSON.parse(data) : data; const validate = ajvValidatorFunction ?? this.createValidator(context) return { @@ -111,9 +118,10 @@ class OrderStatusChanged { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } } -export { OrderStatusChanged }; \ No newline at end of file +export { OrderStatusChanged, OrderStatusChangedInterface }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/PaymentProcessed.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/PaymentProcessed.ts index c00ee520..dce9cb4e 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/PaymentProcessed.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/PaymentProcessed.ts @@ -1,7 +1,16 @@ import {Currency} from './Currency'; import {PaymentStatus} from './PaymentStatus'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; +import {default as addFormats} from 'ajv-formats'; +interface PaymentProcessedInterface { + paymentId: string + orderId: string + amount: number + currency: Currency + status: PaymentStatus + processorResponse?: Record + additionalProperties?: Record +} class PaymentProcessed { private _paymentId: string; private _orderId: string; @@ -11,15 +20,7 @@ class PaymentProcessed { private _processorResponse?: Record; private _additionalProperties?: Record; - constructor(input: { - paymentId: string, - orderId: string, - amount: number, - currency: Currency, - status: PaymentStatus, - processorResponse?: Record, - additionalProperties?: Record, - }) { + constructor(input: PaymentProcessedInterface) { this._paymentId = input.paymentId; this._orderId = input.orderId; this._amount = input.amount; @@ -38,12 +39,21 @@ class PaymentProcessed { get amount(): number { return this._amount; } set amount(amount: number) { this._amount = amount; } + /** + * Currency code + */ get currency(): Currency { return this._currency; } set currency(currency: Currency) { this._currency = currency; } + /** + * Payment processing status + */ get status(): PaymentStatus { return this._status; } set status(status: PaymentStatus) { this._status = status; } + /** + * Additional metadata + */ get processorResponse(): Record | undefined { return this._processorResponse; } set processorResponse(processorResponse: Record | undefined) { this._processorResponse = processorResponse; } @@ -114,6 +124,9 @@ class PaymentProcessed { public static theCodeGenSchema = {"type":"object","$schema":"http://json-schema.org/draft-07/schema","required":["paymentId","orderId","amount","currency","status"],"properties":{"paymentId":{"type":"string","format":"uuid"},"orderId":{"type":"string","format":"uuid"},"amount":{"type":"number","minimum":0},"currency":{"type":"string","enum":["USD","EUR","GBP"],"description":"Currency code"},"status":{"type":"string","enum":["success","failed","pending"],"description":"Payment processing status"},"processorResponse":{"type":"object","additionalProperties":true,"description":"Additional metadata"}},"$id":"PaymentProcessed"}; public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. const parsedData = typeof data === 'string' ? JSON.parse(data) : data; const validate = ajvValidatorFunction ?? this.createValidator(context) return { @@ -124,9 +137,10 @@ class PaymentProcessed { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } } -export { PaymentProcessed }; \ No newline at end of file +export { PaymentProcessed, PaymentProcessedInterface }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/PaymentStatus.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/PaymentStatus.ts index d7855116..49801410 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/PaymentStatus.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/PaymentStatus.ts @@ -1,4 +1,7 @@ +/** + * Payment processing status + */ enum PaymentStatus { SUCCESS = "success", FAILED = "failed", diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/PushNotification.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/PushNotification.ts index c2670ecb..4f63c501 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/PushNotification.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/PushNotification.ts @@ -1,5 +1,12 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; +import {default as addFormats} from 'ajv-formats'; +interface PushNotificationInterface { + recipientId: string + title: string + body: string + badge?: number + additionalProperties?: Record +} class PushNotification { private _type: 'push' = 'push'; private _recipientId: string; @@ -8,13 +15,7 @@ class PushNotification { private _badge?: number; private _additionalProperties?: Record; - constructor(input: { - recipientId: string, - title: string, - body: string, - badge?: number, - additionalProperties?: Record, - }) { + constructor(input: PushNotificationInterface) { this._recipientId = input.recipientId; this._title = input.title; this._body = input.body; @@ -94,6 +95,9 @@ class PushNotification { public static theCodeGenSchema = {"type":"object","required":["type","recipientId","title","body"],"properties":{"type":{"const":"push"},"recipientId":{"type":"string"},"title":{"type":"string"},"body":{"type":"string"},"badge":{"type":"integer","minimum":0}}}; public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. const parsedData = typeof data === 'string' ? JSON.parse(data) : data; const validate = ajvValidatorFunction ?? this.createValidator(context) return { @@ -104,9 +108,10 @@ class PushNotification { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } } -export { PushNotification }; \ No newline at end of file +export { PushNotification, PushNotificationInterface }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/generated/models/SmsNotification.ts b/examples/ecommerce-asyncapi-payload/src/generated/models/SmsNotification.ts index 1e2578f4..8bd2605c 100644 --- a/examples/ecommerce-asyncapi-payload/src/generated/models/SmsNotification.ts +++ b/examples/ecommerce-asyncapi-payload/src/generated/models/SmsNotification.ts @@ -1,16 +1,17 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import addFormats from 'ajv-formats'; +import {default as addFormats} from 'ajv-formats'; +interface SmsNotificationInterface { + recipientId: string + message: string + additionalProperties?: Record +} class SmsNotification { private _type: 'sms' = 'sms'; private _recipientId: string; private _message: string; private _additionalProperties?: Record; - constructor(input: { - recipientId: string, - message: string, - additionalProperties?: Record, - }) { + constructor(input: SmsNotificationInterface) { this._recipientId = input.recipientId; this._message = input.message; this._additionalProperties = input.additionalProperties; @@ -70,6 +71,9 @@ class SmsNotification { public static theCodeGenSchema = {"type":"object","required":["type","recipientId","message"],"properties":{"type":{"const":"sms"},"recipientId":{"type":"string"},"message":{"type":"string","maxLength":160}}}; public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { const {data, ajvValidatorFunction} = context ?? {}; + // Intentionally parse JSON strings to support validation of marshalled output. + // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. + // Note: String 'true' will be coerced to boolean true due to JSON.parse. const parsedData = typeof data === 'string' ? JSON.parse(data) : data; const validate = ajvValidatorFunction ?? this.createValidator(context) return { @@ -80,9 +84,10 @@ class SmsNotification { public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; addFormats(ajvInstance); + const validate = ajvInstance.compile(this.theCodeGenSchema); return validate; } } -export { SmsNotification }; \ No newline at end of file +export { SmsNotification, SmsNotificationInterface }; \ No newline at end of file diff --git a/examples/ecommerce-asyncapi-payload/src/index.ts b/examples/ecommerce-asyncapi-payload/src/index.ts index b26e9a9d..85e52b84 100644 --- a/examples/ecommerce-asyncapi-payload/src/index.ts +++ b/examples/ecommerce-asyncapi-payload/src/index.ts @@ -8,7 +8,7 @@ */ // Import generated payload models -import { OrderCreated } from './generated/models/OrderCreated'; +import { OrderCreated, OrderCreatedInterface } from './generated/models/OrderCreated'; import { PaymentProcessed } from './generated/models/PaymentProcessed'; import { OrderStatusChanged } from './generated/models/OrderStatusChanged'; import { validate as validateNotification } from './generated/models/NotificationSent'; @@ -166,7 +166,7 @@ async function demonstrateOrderStatusUpdate(order: OrderCreated) { orderId: order.orderId, previousStatus: OrderStatus.PENDING, newStatus: OrderStatus.PROCESSING, - timestamp: new Date().toISOString() + timestamp: new Date() }; const statusUpdate = new OrderStatusChanged(statusUpdateData); @@ -274,6 +274,55 @@ async function demonstrateSerialization() { } } +/** + * A publish-style helper that accepts EITHER a plain object satisfying the + * companion interface OR an `OrderCreated` class instance — exactly what every + * generated channel publish/request helper does. It normalizes to a class + * instance with an `instanceof` guard before marshalling. + */ +function publishOrder(order: OrderCreatedInterface | OrderCreated): string { + const instance = order instanceof OrderCreated ? order : new OrderCreated(order); + return instance.marshal(); +} + +async function demonstrateCompanionInterface() { + logSection('7. Companion Interface (plain-object ergonomics)'); + + try { + // Nested object payloads still need to be instances so they can marshal, + // but the TOP-LEVEL order can be a plain object — no `new OrderCreated(...)`. + const items = [new OrderItem({ productId: 'SKU-1', quantity: 1, unitPrice: 500 })]; + + // Pass a PLAIN OBJECT typed as the companion interface: + const fromPlainObject = publishOrder({ + orderId: 'iface-order-1', + customerId: 'iface-customer-1', + items, + totalAmount: 500, + currency: Currency.USD + }); + logSuccess('Published from a plain object (no class construction needed)'); + + // ...and a class instance produces identical marshalled output: + const fromClassInstance = publishOrder(new OrderCreated({ + orderId: 'iface-order-1', + customerId: 'iface-customer-1', + items, + totalAmount: 500, + currency: Currency.USD + })); + + if (fromPlainObject === fromClassInstance) { + logSuccess('Plain object ≡ class instance marshalling ✨'); + } else { + logError('Plain object and class instance produced different output'); + } + } catch (error) { + logError(`Companion interface demo failed: ${error instanceof Error ? error.message : String(error)}`); + throw error; + } +} + async function demonstrateMessagingIntegration() { logSection('6. Messaging Integration Examples'); @@ -369,6 +418,7 @@ async function main() { const notification = await demonstrateNotificationSending(order); await demonstrateSerialization(); + await demonstrateCompanionInterface(); await demonstrateMessagingIntegration(); logSection('Demo Complete'); @@ -378,6 +428,7 @@ async function main() { log('✨ Type-safe TypeScript classes with getters/setters', colors.green); log('✨ JSON Schema validation with AJV', colors.green); log('✨ Marshal/unmarshal for serialization', colors.green); + log('✨ Companion interfaces for plain-object ergonomics', colors.green); log('✨ Complex nested objects and arrays', colors.green); log('✨ Union types for polymorphic messages', colors.green); log('✨ Enum support for constrained values', colors.green); diff --git a/src/codegen/generators/typescript/channels/protocols/amqp/publishExchange.ts b/src/codegen/generators/typescript/channels/protocols/amqp/publishExchange.ts index ca629ae4..f357d4b8 100644 --- a/src/codegen/generators/typescript/channels/protocols/amqp/publishExchange.ts +++ b/src/codegen/generators/typescript/channels/protocols/amqp/publishExchange.ts @@ -6,6 +6,8 @@ import {RenderRegularParameters} from '../../types'; import { parameterInstanceExpression, parameterUnionType, + payloadInstanceExpression, + payloadUnionType, renderChannelJSDoc } from '../../utils'; @@ -26,9 +28,17 @@ export function renderPublishExchange({ const addressToUse = channelParameters ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; + // Object payloads gain a companion interface: widen the user-facing input to + // `Interface | Class` and normalize to a class instance before `.marshal()`. + const widenPayload = !messageModule && messageType !== 'null'; let messageMarshalling = 'message.marshal()'; + let messageInputType = messageType; if (messageModule) { messageMarshalling = `${messageModule}.marshal(message)`; + messageInputType = `${messageModule}.${messageType}`; + } else if (widenPayload) { + messageMarshalling = `${payloadInstanceExpression({messageType, source: 'message'})}.marshal()`; + messageInputType = payloadUnionType({messageType}); } messageType = messageModule ? `${messageModule}.${messageType}` : messageType; @@ -56,7 +66,7 @@ channel.publish(exchange, routingKey, Buffer.from(dataToSend), publishOptions);` const functionParameters = [ { parameter: `message`, - parameterType: `message: ${messageType}`, + parameterType: `message: ${messageInputType}`, jsDoc: ' * @param message to publish' }, ...(channelParameters @@ -121,6 +131,7 @@ function ${functionName}({ return { messageType, + messageUnionType: messageInputType, code, functionName, dependencies: [`import * as Amqp from 'amqplib';`], diff --git a/src/codegen/generators/typescript/channels/protocols/amqp/publishQueue.ts b/src/codegen/generators/typescript/channels/protocols/amqp/publishQueue.ts index ba8dfdca..ca3dc70f 100644 --- a/src/codegen/generators/typescript/channels/protocols/amqp/publishQueue.ts +++ b/src/codegen/generators/typescript/channels/protocols/amqp/publishQueue.ts @@ -6,6 +6,8 @@ import {RenderRegularParameters} from '../../types'; import { parameterInstanceExpression, parameterUnionType, + payloadInstanceExpression, + payloadUnionType, renderChannelJSDoc } from '../../utils'; @@ -23,9 +25,17 @@ export function renderPublishQueue({ const addressToUse = channelParameters ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; + // Object payloads gain a companion interface: widen the user-facing input to + // `Interface | Class` and normalize to a class instance before `.marshal()`. + const widenPayload = !messageModule && messageType !== 'null'; let messageMarshalling = 'message.marshal()'; + let messageInputType = messageType; if (messageModule) { messageMarshalling = `${messageModule}.marshal(message)`; + messageInputType = `${messageModule}.${messageType}`; + } else if (widenPayload) { + messageMarshalling = `${payloadInstanceExpression({messageType, source: 'message'})}.marshal()`; + messageInputType = payloadUnionType({messageType}); } messageType = messageModule ? `${messageModule}.${messageType}` : messageType; @@ -53,7 +63,7 @@ channel.sendToQueue(queue, Buffer.from(dataToSend), publishOptions);`; const functionParameters = [ { parameter: `message`, - parameterType: `message: ${messageType}`, + parameterType: `message: ${messageInputType}`, jsDoc: ' * @param message to publish' }, ...(channelParameters @@ -114,6 +124,7 @@ function ${functionName}({ return { messageType, + messageUnionType: messageInputType, code, functionName, dependencies: [`import * as Amqp from 'amqplib';`], diff --git a/src/codegen/generators/typescript/channels/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts index b0e8ba49..8a5a02ed 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/client.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -7,7 +7,9 @@ import {pascalCase} from '../../../utils'; import {ChannelFunctionTypes, RenderHttpParameters} from '../../types'; import { parameterUnionType, + payloadUnionType, renderParameterNormalization, + renderPayloadNormalization, renderChannelJSDoc } from '../../utils'; @@ -35,6 +37,17 @@ export function renderHttpFetchClient({ const messageType = requestMessageModule ? `${requestMessageModule}.${requestMessageType}` : requestMessageType; + // Object request payloads gain a companion interface: the context `payload` + // field widens to `Interface | Class` and is normalized to a class instance + // before `.marshal()`. Non-object payloads keep their module-qualified type. + const widenPayload = + requestMessageModule === undefined && + requestMessageType !== undefined && + requestMessageType !== 'null'; + const payloadInputType = + widenPayload && requestMessageType + ? payloadUnionType({messageType: requestMessageType}) + : messageType; const replyType = replyMessageModule ? `${replyMessageModule}.${replyMessageType}` : replyMessageType; @@ -49,7 +62,7 @@ export function renderHttpFetchClient({ // Generate the context interface (extends HttpClientContext) const contextInterface = generateContextInterface( contextInterfaceName, - messageType, + payloadInputType, channelParameters?.type, channelHeaders?.type, method @@ -70,6 +83,8 @@ export function renderHttpFetchClient({ replyMessageModule, replyMessageType, messageType, + requestMessageType, + requestMessageModule, requestTopic, hasParameters, parameterModelName: channelParameters?.name, @@ -89,6 +104,7 @@ ${functionCode}`; return { messageType, + messageUnionType: payloadInputType, replyType, code, functionName, @@ -103,16 +119,18 @@ ${functionCode}`; */ function generateContextInterface( interfaceName: string, - messageType: string | undefined, + payloadType: string | undefined, parametersType: string | undefined, headersType: string | undefined, method: string ): string { const fields: string[] = []; - // Add payload field for methods that have a body - if (messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) { - fields.push(` payload: ${messageType};`); + // Add payload field for methods that have a body. For object payloads + // `payloadType` is the ergonomic `Interface | Class` union; the function body + // normalizes it to a class instance before `.marshal()`. + if (payloadType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) { + fields.push(` payload: ${payloadType};`); } // Add parameters field if the operation has path parameters. The field @@ -168,6 +186,8 @@ function generateFunctionImplementation(params: { replyMessageModule: string | undefined; replyMessageType: string; messageType: string | undefined; + requestMessageType: string | undefined; + requestMessageModule: string | undefined; requestTopic: string; hasParameters: boolean; parameterModelName: string | undefined; @@ -187,6 +207,8 @@ function generateFunctionImplementation(params: { replyMessageModule, replyMessageType, messageType, + requestMessageType, + requestMessageModule, requestTopic, hasParameters, parameterModelName, @@ -227,10 +249,18 @@ function generateFunctionImplementation(params: { hasSerializeHeaders }); - // Generate body preparation - const bodyPrep = hasBody - ? `const body = context.payload?.marshal();` - : `const body = undefined;`; + // Generate body preparation. For object payloads the `Interface | Class` + // input is normalized to a class instance before `.marshal()`; non-object + // payloads pass through unchanged (mirroring the previous behavior). + const bodyPrep = + hasBody && requestMessageType + ? `${renderPayloadNormalization({ + messageType: requestMessageType, + messageModule: requestMessageModule, + source: 'context.payload', + target: 'payload' + })}\n const body = payload?.marshal();` + : `const body = undefined;`; // Generate response parsing. // Use unmarshalByStatusCode if the payload is a union type with status code support. diff --git a/src/codegen/generators/typescript/channels/protocols/kafka/publish.ts b/src/codegen/generators/typescript/channels/protocols/kafka/publish.ts index 5c70f9c8..e925b7aa 100644 --- a/src/codegen/generators/typescript/channels/protocols/kafka/publish.ts +++ b/src/codegen/generators/typescript/channels/protocols/kafka/publish.ts @@ -6,6 +6,8 @@ import {RenderRegularParameters} from '../../types'; import { parameterInstanceExpression, parameterUnionType, + payloadInstanceExpression, + payloadUnionType, renderChannelJSDoc } from '../../utils'; @@ -23,9 +25,17 @@ export function renderPublish({ const addressToUse = channelParameters ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; + // Object payloads gain a companion interface: widen the user-facing input to + // `Interface | Class` and normalize to a class instance before `.marshal()`. + const widenPayload = !messageModule && messageType !== 'null'; let messageMarshalling = 'message.marshal()'; + let messageInputType = messageType; if (messageModule) { messageMarshalling = `${messageModule}.marshal(message)`; + messageInputType = `${messageModule}.${messageType}`; + } else if (widenPayload) { + messageMarshalling = `${payloadInstanceExpression({messageType, source: 'message'})}.marshal()`; + messageInputType = payloadUnionType({messageType}); } messageType = messageModule ? `${messageModule}.${messageType}` : messageType; const publishOperation = @@ -36,7 +46,7 @@ export function renderPublish({ const functionParameters = [ { parameter: `message`, - parameterType: `message: ${messageType}`, + parameterType: `message: ${messageInputType}`, jsDoc: ' * @param message to publish' }, ...(channelParameters @@ -121,6 +131,7 @@ function ${functionName}({ return { messageType, + messageUnionType: messageInputType, code, functionName, dependencies: [`import * as Kafka from 'kafkajs';`], diff --git a/src/codegen/generators/typescript/channels/protocols/mqtt/publish.ts b/src/codegen/generators/typescript/channels/protocols/mqtt/publish.ts index 7c204c8f..d1109b6d 100644 --- a/src/codegen/generators/typescript/channels/protocols/mqtt/publish.ts +++ b/src/codegen/generators/typescript/channels/protocols/mqtt/publish.ts @@ -6,6 +6,8 @@ import {RenderRegularParameters} from '../../types'; import { parameterInstanceExpression, parameterUnionType, + payloadInstanceExpression, + payloadUnionType, renderChannelJSDoc } from '../../utils'; @@ -23,9 +25,17 @@ export function renderPublish({ const addressToUse = channelParameters ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; + // Object payloads gain a companion interface: widen the user-facing input to + // `Interface | Class` and normalize to a class instance before `.marshal()`. + const widenPayload = !messageModule && messageType !== 'null'; let messageMarshalling = 'message.marshal()'; + let messageInputType = messageType; if (messageModule) { messageMarshalling = `${messageModule}.marshal(message)`; + messageInputType = `${messageModule}.${messageType}`; + } else if (widenPayload) { + messageMarshalling = `${payloadInstanceExpression({messageType, source: 'message'})}.marshal()`; + messageInputType = payloadUnionType({messageType}); } messageType = messageModule ? `${messageModule}.${messageType}` : messageType; @@ -56,7 +66,7 @@ export function renderPublish({ const functionParameters = [ { parameter: `message`, - parameterType: `message: ${messageType}`, + parameterType: `message: ${messageInputType}`, jsDoc: ' * @param message to publish' }, ...(channelParameters @@ -112,6 +122,7 @@ function ${functionName}({ return { messageType, + messageUnionType: messageInputType, code, functionName, dependencies: [`import * as Mqtt from 'mqtt';`], diff --git a/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts b/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts index b9287be6..2901b46a 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts @@ -7,6 +7,8 @@ import {generateHeaderSetupCode, generateHeaderParameter} from './utils'; import { parameterInstanceExpression, parameterUnionType, + payloadInstanceExpression, + payloadUnionType, renderChannelJSDoc } from '../../utils'; @@ -27,9 +29,19 @@ export function renderCorePublish({ source: 'parameters' })}.getChannelWithParameters('${topic}')` : `'${topic}'`; + // Object payloads gain a companion interface: widen the user-facing input to + // `Interface | Class` and normalize to a class instance before `.marshal()`. + // Non-object payloads (with a module) and empty (`null`) payloads keep the + // existing behavior. + const widenPayload = !messageModule && messageType !== 'null'; let messageMarshalling = 'message.marshal()'; + let messageInputType = messageType; if (messageModule) { messageMarshalling = `${messageModule}.marshal(message)`; + messageInputType = `${messageModule}.${messageType}`; + } else if (widenPayload) { + messageMarshalling = `${payloadInstanceExpression({messageType, source: 'message'})}.marshal()`; + messageInputType = payloadUnionType({messageType}); } messageType = messageModule ? `${messageModule}.${messageType}` : messageType; const headerSetup = generateHeaderSetupCode(channelHeaders); @@ -47,7 +59,7 @@ nc.publish(${addressToUse}, dataToSend, options);`; const functionParameters = [ { parameter: `message`, - parameterType: `message: ${messageType}`, + parameterType: `message: ${messageInputType}`, jsDoc: ' * @param message to publish' }, ...(channelParameters @@ -105,6 +117,7 @@ function ${functionName}({ return { messageType, + messageUnionType: messageInputType, code, functionName, dependencies: [`import * as Nats from 'nats';`], diff --git a/src/codegen/generators/typescript/channels/protocols/nats/coreRequest.ts b/src/codegen/generators/typescript/channels/protocols/nats/coreRequest.ts index dccc7905..aa0ed526 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/coreRequest.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/coreRequest.ts @@ -7,6 +7,8 @@ import { getValidationFunctions, parameterInstanceExpression, parameterUnionType, + payloadInstanceExpression, + payloadUnionType, renderChannelJSDoc } from '../../utils'; @@ -31,6 +33,16 @@ export function renderCoreRequest({ const requestType = requestMessageModule ? `${requestMessageModule}.${requestMessageType}` : requestMessageType; + // Object request payloads gain a companion interface: widen the user-facing + // input to `Interface | Class` and normalize before `.marshal()`. + const widenRequestPayload = + !requestMessageModule && requestMessageType !== 'null'; + const requestInputType = widenRequestPayload + ? payloadUnionType({messageType: requestMessageType}) + : requestType; + const requestMarshalling = widenRequestPayload + ? `${payloadInstanceExpression({messageType: requestMessageType, source: 'requestMessage'})}.marshal()` + : 'requestMessage.marshal()'; const replyType = replyMessageModule ? `${replyMessageModule}.${replyMessageType}` : replyMessageType; @@ -46,7 +58,7 @@ export function renderCoreRequest({ const functionParameters = [ { parameter: `requestMessage`, - parameterType: `requestMessage: ${requestType}`, + parameterType: `requestMessage: ${requestInputType}`, jsDoc: ` * @param requestMessage the message to send` }, ...(channelParameters @@ -100,7 +112,7 @@ function ${functionName}({ return new Promise(async (resolve, reject) => { try { ${potentialValidatorCreation} - let dataToSend: any = requestMessage.marshal(); + let dataToSend: any = ${requestMarshalling}; dataToSend = codec.encode(dataToSend); const msg = await nc.request(${addressToUse}, dataToSend, options); @@ -115,6 +127,7 @@ function ${functionName}({ return { messageType: requestType, + messageUnionType: requestInputType, replyType, code, functionName, diff --git a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts index 6ca91131..3b4de563 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts @@ -7,6 +7,8 @@ import {generateHeaderSetupCode, generateHeaderParameter} from './utils'; import { parameterInstanceExpression, parameterUnionType, + payloadInstanceExpression, + payloadUnionType, renderChannelJSDoc } from '../../utils'; export function renderJetstreamPublish({ @@ -23,9 +25,17 @@ export function renderJetstreamPublish({ const addressToUse = channelParameters ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; + // Object payloads gain a companion interface: widen the user-facing input to + // `Interface | Class` and normalize to a class instance before `.marshal()`. + const widenPayload = !messageModule && messageType !== 'null'; let messageMarshalling = 'message.marshal()'; + let messageInputType = messageType; if (messageModule) { messageMarshalling = `${messageModule}.marshal(message)`; + messageInputType = `${messageModule}.${messageType}`; + } else if (widenPayload) { + messageMarshalling = `${payloadInstanceExpression({messageType, source: 'message'})}.marshal()`; + messageInputType = payloadUnionType({messageType}); } messageType = messageModule ? `${messageModule}.${messageType}` : messageType; @@ -44,7 +54,7 @@ await js.publish(${addressToUse}, dataToSend, options);`; const functionParameters = [ { parameter: `message`, - parameterType: `message: ${messageType}`, + parameterType: `message: ${messageInputType}`, jsDoc: ' * @param message to publish over jetstream' }, ...(channelParameters @@ -102,6 +112,7 @@ function ${functionName}({ return { messageType, + messageUnionType: messageInputType, code, functionName, dependencies: [`import * as Nats from 'nats';`], diff --git a/src/codegen/generators/typescript/channels/protocols/websocket/publish.ts b/src/codegen/generators/typescript/channels/protocols/websocket/publish.ts index f2fa01ae..33a0c209 100644 --- a/src/codegen/generators/typescript/channels/protocols/websocket/publish.ts +++ b/src/codegen/generators/typescript/channels/protocols/websocket/publish.ts @@ -3,7 +3,11 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {renderChannelJSDoc} from '../../utils'; +import { + payloadInstanceExpression, + payloadUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderWebSocketPublish({ topic, @@ -14,16 +18,24 @@ export function renderWebSocketPublish({ description, deprecated }: RenderRegularParameters): SingleFunctionRenderType { + // Object payloads gain a companion interface: widen the user-facing input to + // `Interface | Class` and normalize to a class instance before `.marshal()`. + const widenPayload = !messageModule && messageType !== 'null'; let messageMarshalling = 'message.marshal()'; + let messageInputType = messageType; if (messageModule) { messageMarshalling = `${messageModule}.marshal(message)`; + messageInputType = `${messageModule}.${messageType}`; + } else if (widenPayload) { + messageMarshalling = `${payloadInstanceExpression({messageType, source: 'message'})}.marshal()`; + messageInputType = payloadUnionType({messageType}); } messageType = messageModule ? `${messageModule}.${messageType}` : messageType; const functionParameters = [ { parameter: `message`, - parameterType: `message: ${messageType}`, + parameterType: `message: ${messageInputType}`, jsDoc: ' * @param message to publish' }, { @@ -68,6 +80,7 @@ function ${functionName}({ return { messageType, + messageUnionType: messageInputType, code, functionName, dependencies: [`import * as WebSocket from 'ws';`], diff --git a/src/codegen/generators/typescript/channels/types.ts b/src/codegen/generators/typescript/channels/types.ts index 3ecee502..67342b26 100644 --- a/src/codegen/generators/typescript/channels/types.ts +++ b/src/codegen/generators/typescript/channels/types.ts @@ -207,6 +207,13 @@ export type TypeScriptChannelRenderedFunctionType = { functionType: ChannelFunctionTypes; functionName: string; messageType: string; + /** + * User-facing input type for the message at publish/request sites (the + * `Interface | Class` union for object payloads, otherwise `messageType`). + * Read by the client generator so its wrapper publish methods accept the same + * ergonomic plain object the channel functions accept. + */ + messageUnionType?: string; replyType?: string; parameterType?: string; /** diff --git a/src/codegen/generators/typescript/channels/utils.ts b/src/codegen/generators/typescript/channels/utils.ts index 2f30af16..ccd2b541 100644 --- a/src/codegen/generators/typescript/channels/utils.ts +++ b/src/codegen/generators/typescript/channels/utils.ts @@ -40,10 +40,13 @@ export function addPayloadsToDependencies( `./${ensureRelativePath(payloadImportPath)}`, importExtension ); - if ( - payload.messageModel.model instanceof ConstrainedObjectModel || - payload.messageModel.model instanceof ConstrainedEnumModel - ) { + if (payload.messageModel.model instanceof ConstrainedObjectModel) { + // Object payloads gain a companion interface — import both symbols so + // consumers can accept the ergonomic plain-object `Interface | Class`. + dependencies.push( + `import {${payload.messageModel.modelName}, ${payload.messageModel.modelName}Interface} from '${importPath}';` + ); + } else if (payload.messageModel.model instanceof ConstrainedEnumModel) { dependencies.push( `import {${payload.messageModel.modelName}} from '${importPath}';` ); @@ -141,6 +144,71 @@ export function renderParameterNormalization({ return `const ${target} = ${source} instanceof ${modelName} ? ${source} : new ${modelName}(${source});`; } +/** + * Widen an object payload's user-facing input type to the `Interface | Class` + * union (ergonomic plain object literal, or the rich class). A non-object + * payload has a `messageModule` and no companion interface, so its + * module-qualified type is returned unchanged. + */ +export function payloadUnionType({ + messageType, + messageModule +}: { + messageType: string; + messageModule?: string; +}): string { + // Object payloads share the exact `Interface | Class` format with parameters; + // non-object payloads keep their module-qualified type. + return messageModule + ? `${messageModule}.${messageType}` + : parameterUnionType(messageType); +} + +/** + * Emit an inline `instanceof`-guarded expression that resolves a user-provided + * object-payload `Interface | Class` value to a concrete class instance + * (e.g. before `.marshal()`). Non-object payloads (with a `messageModule`) are + * marshalled through their module's free function and never wrapped with + * `new`, so the source is returned unchanged. + */ +export function payloadInstanceExpression({ + messageType, + messageModule, + source +}: { + messageType: string; + messageModule?: string; + source: string; +}): string { + if (messageModule) { + return source; + } + return parameterInstanceExpression({modelName: messageType, source}); +} + +/** + * Emit an `instanceof`-guarded normalization statement that turns a + * user-provided object-payload `Interface | Class` value into a concrete class + * instance before it is used. Non-object payloads are passed through unchanged + * (a plain `const = ;`). + */ +export function renderPayloadNormalization({ + messageType, + messageModule, + source, + target +}: { + messageType: string; + messageModule?: string; + source: string; + target: string; +}): string { + if (messageModule) { + return `const ${target} = ${source};`; + } + return renderParameterNormalization({modelName: messageType, source, target}); +} + export function addParametersToExports( parameters: Record, dependencies: string[] @@ -479,7 +547,12 @@ type RenderForExternal = Pick< | 'tags' | 'pathSegments' | 'method' -> & {messageType?: string; replyType?: string; parameterType?: string}; +> & { + messageType?: string; + messageUnionType?: string; + replyType?: string; + parameterType?: string; +}; /** * Push a protocol's renders into the shared output maps: the raw function code, @@ -517,6 +590,7 @@ export function addRendersToExternal({ functionType: value.functionType, functionName: value.functionName, messageType: value.messageType ?? '', + messageUnionType: value.messageUnionType ?? value.messageType ?? '', replyType: value.replyType, parameterType: value.parameterType ?? parameter?.type, tags: value.tags, diff --git a/src/codegen/generators/typescript/client/protocols/nats.ts b/src/codegen/generators/typescript/client/protocols/nats.ts index b9e9d7ad..e3ef3545 100644 --- a/src/codegen/generators/typescript/client/protocols/nats.ts +++ b/src/codegen/generators/typescript/client/protocols/nats.ts @@ -93,7 +93,10 @@ export async function generateNatsClient( channelName: func.functionName, channelParameterType: func.parameterType, description: '', - messageType: func.messageType + messageType: func.messageType, + // Publish wrappers accept the widened `Interface | Class` union so callers + // can pass a plain object; subscribe wrappers keep the bare class type. + messageUnionType: func.messageUnionType ?? func.messageType }; switch (func.functionType) { case ChannelFunctionTypes.NATS_SUBSCRIBE: diff --git a/src/codegen/generators/typescript/client/protocols/nats/corePublish.ts b/src/codegen/generators/typescript/client/protocols/nats/corePublish.ts index 5420bffa..6226e698 100644 --- a/src/codegen/generators/typescript/client/protocols/nats/corePublish.ts +++ b/src/codegen/generators/typescript/client/protocols/nats/corePublish.ts @@ -1,18 +1,18 @@ export function renderCorePublish({ description, - messageType, + messageUnionType, channelParameterType, channelName }: { description: string; - messageType: string; + messageUnionType: string; channelParameterType?: string; channelName: string; }) { const functionParameters = [ { parameter: `message`, - parameterType: `message: ${messageType}`, + parameterType: `message: ${messageUnionType}`, jsDoc: ' * @param message to publish' }, ...(channelParameterType diff --git a/src/codegen/generators/typescript/client/protocols/nats/jetstreamPublish.ts b/src/codegen/generators/typescript/client/protocols/nats/jetstreamPublish.ts index 11748d92..03afd746 100644 --- a/src/codegen/generators/typescript/client/protocols/nats/jetstreamPublish.ts +++ b/src/codegen/generators/typescript/client/protocols/nats/jetstreamPublish.ts @@ -1,18 +1,18 @@ export function renderJetStreamPublish({ description, - messageType, + messageUnionType, channelParameterType, channelName }: { description: string; - messageType: string; + messageUnionType: string; channelParameterType?: string; channelName: string; }) { const functionParameters = [ { parameter: `message`, - parameterType: `message: ${messageType}`, + parameterType: `message: ${messageUnionType}`, jsDoc: ' * @param message to publish' }, ...(channelParameterType diff --git a/src/codegen/generators/typescript/parameters.ts b/src/codegen/generators/typescript/parameters.ts index 6cb9dd89..59b4f004 100644 --- a/src/codegen/generators/typescript/parameters.ts +++ b/src/codegen/generators/typescript/parameters.ts @@ -19,6 +19,7 @@ import { } from '../../inputs/openapi/generators/parameters'; import {createMissingInputDocumentError} from '../../errors'; import {generateModels} from '../../output'; +import {withCompanionInterfaceExport} from './utils'; export const zodTypescriptParametersGenerator = z.object({ id: z @@ -80,36 +81,6 @@ export interface TypescriptParametersContext extends GenericCodegenContext { export type TypeScriptParameterRenderType = ParameterRenderType; -/** - * Rewrite a generated parameter model's trailing `export { };` to also - * export the companion `Interface` emitted by the class preset. Modelina - * appends the export outside the preset chain from the single class model, so - * the interface (raw text injected by the `self` hook) would otherwise not be - * exported. Only rewrites when the companion interface is actually present, so - * models without the interface treatment are left untouched. - */ -function withParameterInterfaceExport(model: OutputModel): OutputModel { - const interfaceName = `${model.modelName}Interface`; - const originalExport = `export { ${model.modelName} };`; - if ( - !model.result.includes(`interface ${interfaceName}`) || - !model.result.includes(originalExport) - ) { - return model; - } - const rewritten = model.result.replace( - originalExport, - `export { ${model.modelName}, ${interfaceName} };` - ); - return OutputModel.toOutputModel({ - result: rewritten, - model: model.model, - modelName: model.modelName, - inputModel: model.inputModel, - dependencies: model.dependencies - }); -} - // Main generator function that orchestrates input processing and generation export async function generateTypescriptParameters( context: TypescriptParametersContext @@ -163,7 +134,7 @@ export async function generateTypescriptParameters( }); const mainModel = result.models.length > 0 - ? withParameterInterfaceExport(result.models[0]) + ? withCompanionInterfaceExport(result.models[0]) : undefined; channelModels[channelId] = mainModel; for (const file of result.files) { diff --git a/src/codegen/generators/typescript/payloads.ts b/src/codegen/generators/typescript/payloads.ts index 8e362419..43bafbe0 100644 --- a/src/codegen/generators/typescript/payloads.ts +++ b/src/codegen/generators/typescript/payloads.ts @@ -16,7 +16,11 @@ import { } from '../../inputs/asyncapi/generators/payloads'; import {processOpenAPIPayloads} from '../../inputs/openapi/generators/payloads'; import {z} from 'zod'; -import {defaultCodegenTypescriptModelinaOptions} from './utils'; +import { + defaultCodegenTypescriptModelinaOptions, + payloadClassPreset, + withCompanionInterfaceExport +} from './utils'; import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; import {TS_COMMON_PRESET, TS_DESCRIPTION_PRESET} from '@asyncapi/modelina'; import { @@ -153,6 +157,26 @@ export async function generateTypescriptPayloadsCore( }; } +/** + * Rewrite each generated model's `export { };` to also export its + * companion `Interface` (object payloads only) and mirror the rewrite + * onto each model's own file. Non-object payloads have no companion interface, + * so {@link withCompanionInterfaceExport} returns them unchanged. + */ +function applyCompanionInterfaceExports(result: { + models: OutputModel[]; + files: GeneratedFile[]; +}): {models: OutputModel[]; files: GeneratedFile[]} { + const models = result.models.map(withCompanionInterfaceExport); + const files = result.files.map((file) => { + const model = models.find((candidate) => + file.path.endsWith(`/${candidate.modelName}.ts`) + ); + return model ? {path: file.path, content: model.result} : file; + }); + return {models, files}; +} + // Core generator function that works with processed schema data // eslint-disable-next-line sonarjs/cognitive-complexity export async function generateTypescriptPayloadsCoreFromSchemas({ @@ -174,6 +198,10 @@ export async function generateTypescriptPayloadsCoreFromSchemas({ marshalling: true } }, + // Must run after TS_COMMON_PRESET so its `ctor` override wins; only + // adds `class.self`/`class.ctor`, leaving `additionalContent` (owned by + // the validation preset) intact. + payloadClassPreset(), createValidationPreset( { includeValidation: generator.includeValidation @@ -216,11 +244,13 @@ export async function generateTypescriptPayloadsCoreFromSchemas({ processedSchemaData.channelPayloads )) { if (schemaData) { - const result = await generateModels({ - generator: modelinaGenerator, - input: schemaData.schema, - outputPath: generator.outputPath - }); + const result = applyCompanionInterfaceExports( + await generateModels({ + generator: modelinaGenerator, + input: schemaData.schema, + outputPath: generator.outputPath + }) + ); const models = result.models; files.push(...result.files); @@ -253,11 +283,13 @@ export async function generateTypescriptPayloadsCoreFromSchemas({ processedSchemaData.operationPayloads )) { if (schemaData) { - const result = await generateModels({ - generator: modelinaGenerator, - input: schemaData.schema, - outputPath: generator.outputPath - }); + const result = applyCompanionInterfaceExports( + await generateModels({ + generator: modelinaGenerator, + input: schemaData.schema, + outputPath: generator.outputPath + }) + ); const models = result.models; files.push(...result.files); @@ -287,11 +319,13 @@ export async function generateTypescriptPayloadsCoreFromSchemas({ // Generate models for other payloads for (const schemaData of processedSchemaData.otherPayloads) { - const result = await generateModels({ - generator: modelinaGenerator, - input: schemaData.schema, - outputPath: generator.outputPath - }); + const result = applyCompanionInterfaceExports( + await generateModels({ + generator: modelinaGenerator, + input: schemaData.schema, + outputPath: generator.outputPath + }) + ); files.push(...result.files); for (const model of result.models) { diff --git a/src/codegen/generators/typescript/utils.ts b/src/codegen/generators/typescript/utils.ts index 6623dc4f..c2596cba 100644 --- a/src/codegen/generators/typescript/utils.ts +++ b/src/codegen/generators/typescript/utils.ts @@ -3,6 +3,7 @@ import { ConstrainedObjectModel, FormatHelpers, NO_RESERVED_KEYWORDS, + OutputModel, typeScriptDefaultModelNameConstraints, typeScriptDefaultPropertyKeyConstraints, TypeScriptOptions, @@ -92,6 +93,73 @@ ${generateAdditionalMethods(model)}` }; } +/** + * Class preset for payload models. Prepends a plain-data companion + * `interface Interface` above the class (`self`) and rewrites the + * constructor to accept `input: Interface` (`ctor`), reusing the same + * body helper as {@link parameterClassPreset}. + * + * Unlike {@link parameterClassPreset}, this preset *augments* the existing + * payload preset chain rather than replacing it: it deliberately omits + * `additionalContent` (the validation preset owns that) and must be inserted + * **after** `TS_COMMON_PRESET` so its `ctor` override wins. Only object/class + * payloads gain the interface; non-object payloads (unions/primitives/enums) + * have no class hook and are untouched. + */ +export function payloadClassPreset(): TypeScriptPreset { + return { + class: { + self: ({content, model}) => + `interface ${model.name}Interface { +${buildParametersInterfaceBody(model)} +} +${content}`, + ctor: ({renderer, model}) => { + const assignments = Object.values(model.properties) + .filter((property) => !property.property.options.const) + .map( + (property) => + `this._${property.propertyName} = input.${property.propertyName};` + ); + return `constructor(input: ${model.name}Interface) { +${renderer.indent(renderer.renderBlock(assignments))} +}`; + } + } + }; +} + +/** + * Rewrite a generated model's trailing `export { };` to also export the + * companion `Interface` emitted by a class preset. Modelina appends the + * export outside the preset chain, so the interface (raw text injected by the + * `self` hook) would otherwise not be exported. Only rewrites when the + * companion interface is actually present, so models without the interface + * treatment (non-object payloads) are left untouched. Shared by the parameter + * and payload generators so the rewrite logic can never diverge. + */ +export function withCompanionInterfaceExport(model: OutputModel): OutputModel { + const interfaceName = `${model.modelName}Interface`; + const originalExport = `export { ${model.modelName} };`; + if ( + !model.result.includes(`interface ${interfaceName}`) || + !model.result.includes(originalExport) + ) { + return model; + } + const rewritten = model.result.replace( + originalExport, + `export { ${model.modelName}, ${interfaceName} };` + ); + return OutputModel.toOutputModel({ + result: rewritten, + model: model.model, + modelName: model.modelName, + inputModel: model.inputModel, + dependencies: model.dependencies + }); +} + /** * Convert RFC 6570 URI with parameters to NATS topic. */ diff --git a/src/codegen/types.ts b/src/codegen/types.ts index 78cf38ac..e2b75791 100644 --- a/src/codegen/types.ts +++ b/src/codegen/types.ts @@ -199,6 +199,13 @@ export interface SingleFunctionRenderType { dependencies: string[]; functionType: ChannelFunctionTypes; messageType: string; + /** + * The user-facing input type for the message at publish/request sites. For + * object payloads this is the `Interface | Class` union; otherwise it equals + * `messageType`. Consumed by the client generator so its wrapper publish + * methods accept the same ergonomic plain object the channel functions do. + */ + messageUnionType?: string; replyType?: string; /** * Grouping metadata used by the channels generator's `organization` option. @@ -215,6 +222,12 @@ export interface HttpRenderType { dependencies: string[]; functionType: ChannelFunctionTypes; messageType?: string; + /** + * The user-facing input type for the payload at the request site. For object + * payloads this is the `Interface | Class` union; otherwise it equals + * `messageType`. Consumed by the client generator. + */ + messageUnionType?: string; replyType: string; /** * Grouping metadata used by the channels generator's `organization` option. diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 84508252..99e26dd5 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -8,7 +8,7 @@ export {http_client}; `; exports[`channels typescript OpenAPI input should generate HTTP client protocol code for OpenAPI spec: openapi-http_client-protocol-code 1`] = ` -"import {Pet} from './../../../../../payloads/Pet'; +"import {Pet, PetInterface} from './../../../../../payloads/Pet'; import * as FindPetsByStatusAndCategoryResponseModule from './../../../../../payloads/FindPetsByStatusAndCategoryResponse'; import {FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface} from './../../../../../parameters/FindPetsByStatusAndCategoryParameters'; import {FindPetsByStatusAndCategoryHeaders, serializeFindPetsByStatusAndCategoryHeadersHeaders} from './../../../../../headers/FindPetsByStatusAndCategoryHeaders'; @@ -622,7 +622,7 @@ async function handleTokenRefresh( // ============================================================================ export interface AddPetContext extends HttpClientContext { - payload: Pet; + payload: PetInterface | Pet; } /** @@ -653,7 +653,8 @@ async function addPet(context: AddPetContext): Promise> url = authResult.url; // Prepare body - const body = context.payload?.marshal(); + const payload = context.payload instanceof Pet ? context.payload : new Pet(context.payload); + const body = payload?.marshal(); // Determine request function const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; @@ -732,7 +733,7 @@ async function addPet(context: AddPetContext): Promise> } export interface UpdatePetContext extends HttpClientContext { - payload: Pet; + payload: PetInterface | Pet; } /** @@ -763,7 +764,8 @@ async function updatePet(context: UpdatePetContext): Promise +} class SimpleObject2 { private _displayName?: string; private _email?: string; private _additionalProperties?: Record; - constructor(input: { - displayName?: string, - email?: string, - additionalProperties?: Record, - }) { + constructor(input: SimpleObject2Interface) { this._displayName = input.displayName; this._email = input.email; this._additionalProperties = input.additionalProperties; @@ -96,7 +97,7 @@ class SimpleObject2 { return instance; } } -export { SimpleObject2 };" +export { SimpleObject2, SimpleObject2Interface };" `; exports[`payloads typescript should work with basic AsyncAPI inputs 1`] = ` @@ -150,16 +151,17 @@ export { UnionPayload };" exports[`payloads typescript should work with basic AsyncAPI inputs 2`] = ` "import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface SimpleObject2Interface { + displayName?: string + email?: string + additionalProperties?: Record +} class SimpleObject2 { private _displayName?: string; private _email?: string; private _additionalProperties?: Record; - constructor(input: { - displayName?: string, - email?: string, - additionalProperties?: Record, - }) { + constructor(input: SimpleObject2Interface) { this._displayName = input.displayName; this._email = input.email; this._additionalProperties = input.additionalProperties; @@ -239,23 +241,24 @@ class SimpleObject2 { } } -export { SimpleObject2 };" +export { SimpleObject2, SimpleObject2Interface };" `; exports[`payloads typescript should work with no channels 1`] = ` "import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface SimpleObjectInterface { + displayName?: string + email?: string + additionalProperties?: Record +} class SimpleObject { private _type?: 'SimpleObject' = 'SimpleObject'; private _displayName?: string; private _email?: string; private _additionalProperties?: Record; - constructor(input: { - displayName?: string, - email?: string, - additionalProperties?: Record, - }) { + constructor(input: SimpleObjectInterface) { this._displayName = input.displayName; this._email = input.email; this._additionalProperties = input.additionalProperties; @@ -340,5 +343,5 @@ class SimpleObject { } } -export { SimpleObject };" +export { SimpleObject, SimpleObjectInterface };" `; diff --git a/test/codegen/generators/typescript/channels.spec.ts b/test/codegen/generators/typescript/channels.spec.ts index ec497e9d..e1f2f0da 100644 --- a/test/codegen/generators/typescript/channels.spec.ts +++ b/test/codegen/generators/typescript/channels.spec.ts @@ -514,6 +514,134 @@ describe('channels', () => { }); }); + describe('payload companion interface widening', () => { + // An object payload (ConstrainedObjectModel) gains a companion interface, + // so its user-facing input sites must widen to `Interface | Class` and + // normalize to a class instance before `.marshal()`. + const objectPayloadModel = new OutputModel( + '', + new ConstrainedObjectModel('UserSignedUpPayload', undefined, {}, 'object', {}), + 'UserSignedUpPayload', + {models: {}, originalInput: undefined}, + [] + ); + + const generateBrokerProtocol = async (protocol: string) => { + const parsedAsyncAPIDocument = await loadAsyncapiDocument( + path.resolve(__dirname, '../../../configs/asyncapi-channels.yaml') + ); + const objectPayload = { + messageModel: objectPayloadModel, + messageType: 'UserSignedUpPayload' + }; + const payloadsDependency: TypeScriptPayloadRenderType = { + channelModels: { + userSignedup: objectPayload, + noParameter: objectPayload + }, + operationModels: {}, + otherModels: [], + generator: {outputPath: './payloads'} as any, + files: [] + }; + const parametersDependency: TypeScriptParameterRenderType = { + channelModels: { + userSignedup: createParameterModelWithProperties({ + myParameter: new ConstrainedStringModel('myParameter', undefined, {}, 'string') + }), + noParameter: undefined + }, + generator: {outputPath: './parameters'} as any, + files: [] + }; + const generatedChannels = await generateTypeScriptChannels({ + generator: { + ...defaultTypeScriptChannelsGenerator, + outputPath: path.resolve(__dirname, './output'), + id: 'test', + asyncapiGenerateForOperations: false, + protocols: [protocol as any] + }, + inputType: 'asyncapi', + asyncapiDocument: parsedAsyncAPIDocument, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() + } + }); + return generatedChannels.protocolFiles[protocol]; + }; + + it.each(['nats', 'kafka', 'mqtt', 'amqp', 'websocket'])( + 'should widen the %s publish input site to Interface | Class and normalize before marshal', + async (protocol) => { + const code = await generateBrokerProtocol(protocol); + expect(code).toContain( + 'UserSignedUpPayloadInterface | UserSignedUpPayload' + ); + // Normalization guard before marshalling the payload. + expect(code).toContain('instanceof UserSignedUpPayload'); + // Dual import of the payload class and its companion interface. + expect(code).toContain( + 'import {UserSignedUpPayload, UserSignedUpPayloadInterface}' + ); + } + ); + + it('should widen the HTTP client payload input site (POST body) and import the companion interface', async () => { + // openapi-3.json's addPet is a POST with a `Pet` object request body — + // the only HTTP shape that carries a payload to widen. + const parsedOpenAPIDocument = await loadOpenapiDocument( + path.resolve(__dirname, '../../../runtime/openapi-3.json') + ); + const petPayloadModel = new OutputModel( + '', + new ConstrainedObjectModel('Pet', undefined, {}, 'object', {}), + 'Pet', + {models: {}, originalInput: undefined}, + [] + ); + const petPayload = {messageModel: petPayloadModel, messageType: 'Pet'}; + const payloadsDependency: TypeScriptPayloadRenderType = { + channelModels: {}, + operationModels: { + addPet: petPayload, + addPet_Response: petPayload + }, + otherModels: [], + generator: {outputPath: './payloads'} as any, + files: [] + }; + const generatedChannels = await generateTypeScriptChannels({ + generator: { + ...defaultTypeScriptChannelsGenerator, + outputPath: path.resolve(__dirname, './output'), + id: 'test', + asyncapiGenerateForOperations: true, + protocols: ['http_client'] + }, + inputType: 'openapi', + openapiDocument: parsedOpenAPIDocument, + dependencyOutputs: { + 'parameters-typescript': { + channelModels: {}, + generator: {outputPath: './parameters'} as any, + files: [] + }, + 'payloads-typescript': payloadsDependency, + 'headers-typescript': createHeadersDependency() + } + }); + const code = generatedChannels.protocolFiles['http_client']; + // Context `payload` field widened to `Interface | Class`. + expect(code).toContain('payload: PetInterface | Pet;'); + // Body normalized to a class instance before marshal. + expect(code).toContain('instanceof Pet'); + expect(code).toContain('import {Pet, PetInterface}'); + }); + }); + describe('OpenAPI input', () => { it('should generate HTTP client protocol code for OpenAPI spec', async () => { const parsedOpenAPIDocument = await loadOpenapiDocument( diff --git a/test/codegen/generators/typescript/channels/utils.spec.ts b/test/codegen/generators/typescript/channels/utils.spec.ts index a642b0ca..6cb2fc03 100644 --- a/test/codegen/generators/typescript/channels/utils.spec.ts +++ b/test/codegen/generators/typescript/channels/utils.spec.ts @@ -1,15 +1,159 @@ import { + ConstrainedAnyModel, + ConstrainedEnumModel, ConstrainedObjectModel, OutputModel } from '@asyncapi/modelina'; import { addParametersToDependencies, + addPayloadsToDependencies, parameterInstanceExpression, parameterUnionType, - renderParameterNormalization + payloadInstanceExpression, + payloadUnionType, + renderParameterNormalization, + renderPayloadNormalization } from '../../../../../src/codegen/generators/typescript/channels/utils'; +import {ChannelPayload} from '../../../../../src/codegen/types'; + +const objectPayload = (name: string): ChannelPayload => ({ + messageModel: new OutputModel( + '', + new ConstrainedObjectModel(name, undefined, {}, 'object', {}), + name, + {models: {}, originalInput: undefined} as any, + [] + ), + messageType: name +}); + +const enumPayload = (name: string): ChannelPayload => ({ + messageModel: new OutputModel( + '', + new ConstrainedEnumModel(name, undefined, {}, 'enum', []), + name, + {models: {}, originalInput: undefined} as any, + [] + ), + messageType: name +}); + +const primitivePayload = (name: string): ChannelPayload => ({ + messageModel: new OutputModel( + '', + new ConstrainedAnyModel(name, undefined, {}, 'any'), + name, + {models: {}, originalInput: undefined} as any, + [] + ), + messageType: name +}); describe('channels utils', () => { + describe('addPayloadsToDependencies (companion interface)', () => { + it('should import both the payload class and its companion interface for object payloads', () => { + const dependencies: string[] = []; + addPayloadsToDependencies( + [objectPayload('UserSignedUp')], + {outputPath: './payloads'}, + {outputPath: './channels'}, + dependencies + ); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]).toMatch( + /^import \{UserSignedUp, UserSignedUpInterface\} from '.+';$/ + ); + }); + + it('should NOT import a companion interface for enum payloads', () => { + const dependencies: string[] = []; + addPayloadsToDependencies( + [enumPayload('StatusEnum')], + {outputPath: './payloads'}, + {outputPath: './channels'}, + dependencies + ); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]).toMatch(/^import \{StatusEnum\} from '.+';$/); + expect(dependencies[0]).not.toContain('Interface'); + }); + + it('should import non-object payloads as a namespace module (unchanged)', () => { + const dependencies: string[] = []; + addPayloadsToDependencies( + [primitivePayload('PrimitivePayload')], + {outputPath: './payloads'}, + {outputPath: './channels'}, + dependencies + ); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]).toContain('import * as PrimitivePayloadModule'); + expect(dependencies[0]).not.toContain('Interface'); + }); + }); + + describe('payloadUnionType', () => { + it('should widen an object payload to an interface | class union', () => { + expect(payloadUnionType({messageType: 'UserSignedUp'})).toEqual( + 'UserSignedUpInterface | UserSignedUp' + ); + }); + + it('should leave a non-object (module-qualified) payload type unchanged', () => { + expect( + payloadUnionType({ + messageType: 'StringMessage', + messageModule: 'StringMessageModule' + }) + ).toEqual('StringMessageModule.StringMessage'); + }); + }); + + describe('payloadInstanceExpression', () => { + it('should resolve an object payload source to a class instance inline', () => { + expect( + payloadInstanceExpression({messageType: 'UserSignedUp', source: 'message'}) + ).toEqual( + '(message instanceof UserSignedUp ? message : new UserSignedUp(message))' + ); + }); + + it('should return a non-object payload source unchanged (no new)', () => { + expect( + payloadInstanceExpression({ + messageType: 'StringMessage', + messageModule: 'StringMessageModule', + source: 'message' + }) + ).toEqual('message'); + }); + }); + + describe('renderPayloadNormalization', () => { + it('should normalize an object payload source to a class instance via instanceof guard', () => { + expect( + renderPayloadNormalization({ + messageType: 'UserSignedUp', + source: 'context.payload', + target: 'payload' + }) + ).toEqual( + 'const payload = context.payload instanceof UserSignedUp ? context.payload : new UserSignedUp(context.payload);' + ); + }); + + it('should pass a non-object payload source through unchanged', () => { + expect( + renderPayloadNormalization({ + messageType: 'StringMessage', + messageModule: 'StringMessageModule', + source: 'context.payload', + target: 'payload' + }) + ).toEqual('const payload = context.payload;'); + }); + }); + describe('addParametersToDependencies', () => { it('should import both the parameter class and its companion interface', () => { const parameterModel = new OutputModel( diff --git a/test/codegen/generators/typescript/payload.spec.ts b/test/codegen/generators/typescript/payload.spec.ts index 12fa5e6b..53bdeb01 100644 --- a/test/codegen/generators/typescript/payload.spec.ts +++ b/test/codegen/generators/typescript/payload.spec.ts @@ -360,6 +360,67 @@ describe('payloads', () => { }, ]); }); + describe('companion interface', () => { + const generate = async () => { + const parsedAsyncAPIDocument = await loadAsyncapiDocument(path.resolve(__dirname, '../../../configs/payload.yaml')); + return generateTypescriptPayload({ + generator: { + ...defaultTypeScriptPayloadGenerator, + outputPath: path.resolve(__dirname, './output') + }, + inputType: 'asyncapi', + asyncapiDocument: parsedAsyncAPIDocument, + dependencyOutputs: { } + }); + }; + + it('emits an exported companion interface and retypes the constructor for object payloads', async () => { + const renderedContent = await generate(); + const result = renderedContent.channelModels['simple'].messageModel.result; + + expect(result).toContain('interface SimpleObject2Interface {'); + expect(result).toContain('constructor(input: SimpleObject2Interface)'); + expect(result).toContain('export { SimpleObject2, SimpleObject2Interface };'); + }); + + it('includes every accepted ctor property (including additionalProperties) in the companion interface', async () => { + const renderedContent = await generate(); + const result = renderedContent.channelModels['simple'].messageModel.result; + + const interfaceBody = /interface SimpleObject2Interface \{([\s\S]*?)\n\}/.exec(result)?.[1] ?? ''; + // The interface must be exactly the input the inline ctor type accepts + // today — no property dropped or retyped, additionalProperties included. + expect(interfaceBody).toContain('displayName?: string'); + expect(interfaceBody).toContain('email?: string'); + expect(interfaceBody).toContain('additionalProperties?: Record'); + }); + + it('omits const/discriminator properties from the companion interface', async () => { + const renderedContent = await generate(); + const simpleObject = renderedContent.otherModels.find( + (model) => model.messageModel.modelName === 'SimpleObject' + ); + expect(simpleObject).toBeDefined(); + const result = simpleObject!.messageModel.result; + + expect(result).toContain('interface SimpleObjectInterface {'); + const interfaceBody = /interface SimpleObjectInterface \{([\s\S]*?)\n\}/.exec(result)?.[1] ?? ''; + expect(interfaceBody).toContain('displayName?: string'); + expect(interfaceBody).toContain('email?: string'); + // `type` is a const discriminator — it must not appear as a property. + expect(/^\s*type\??:/m.test(interfaceBody)).toBe(false); + }); + + it('does not emit a companion interface for non-object (union) payloads', async () => { + const renderedContent = await generate(); + const result = renderedContent.channelModels['union'].messageModel.result; + + expect(result).not.toContain('Interface {'); + expect(result).not.toContain('UnionPayloadInterface'); + expect(result).toContain('export { UnionPayload };'); + }); + }); + describe('safeStringify', () => { it('should stringify basic primitive values', () => { expect(safeStringify('hello')).toBe('"hello"'); diff --git a/test/runtime/typescript/src/asyncapi-path-organization/channels/nats.ts b/test/runtime/typescript/src/asyncapi-path-organization/channels/nats.ts index 38401be4..2185b31a 100644 --- a/test/runtime/typescript/src/asyncapi-path-organization/channels/nats.ts +++ b/test/runtime/typescript/src/asyncapi-path-organization/channels/nats.ts @@ -1,9 +1,9 @@ -import {UserSignedUp} from './payload/UserSignedUp'; +import {UserSignedUp, UserSignedUpInterface} from './payload/UserSignedUp'; import * as StringMessageModule from './payload/StringMessage'; import * as ArrayMessageModule from './payload/ArrayMessage'; import * as UnionMessageModule from './payload/UnionMessage'; -import {LegacyNotification} from './payload/LegacyNotification'; -import {UnionPayloadOneOfOption2} from './payload/UnionPayloadOneOfOption2'; +import {LegacyNotification, LegacyNotificationInterface} from './payload/LegacyNotification'; +import {UnionPayloadOneOfOption2, UnionPayloadOneOfOption2Interface} from './payload/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './payload/LegacyNotificationPayloadLevelEnum'; import {UserSignedupParameters, UserSignedupParametersInterface} from './parameter/UserSignedupParameters'; import {UserSignedUpHeaders} from './headers/UserSignedUpHeaders'; @@ -27,7 +27,7 @@ function publishToSendUserSignedup({ codec = Nats.JSONCodec(), options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, nc: Nats.NatsConnection, @@ -36,7 +36,7 @@ function publishToSendUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); // Set up headers if provided if (headers) { const natsHeaders = Nats.headers(); @@ -76,7 +76,7 @@ function jetStreamPublishToSendUserSignedup({ codec = Nats.JSONCodec(), options = {} }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, js: Nats.JetStreamClient, @@ -85,7 +85,7 @@ function jetStreamPublishToSendUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); // Set up headers if provided if (headers) { const natsHeaders = Nats.headers(); @@ -357,7 +357,7 @@ function publishToNoParameter({ codec = Nats.JSONCodec(), options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, headers?: UserSignedUpHeaders, nc: Nats.NatsConnection, codec?: Nats.Codec, @@ -365,7 +365,7 @@ function publishToNoParameter({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); // Set up headers if provided if (headers) { const natsHeaders = Nats.headers(); @@ -625,7 +625,7 @@ function jetStreamPublishToNoParameter({ codec = Nats.JSONCodec(), options = {} }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, headers?: UserSignedUpHeaders, js: Nats.JetStreamClient, codec?: Nats.Codec, @@ -633,7 +633,7 @@ function jetStreamPublishToNoParameter({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); // Set up headers if provided if (headers) { const natsHeaders = Nats.headers(); @@ -1358,14 +1358,14 @@ function publishToSendLegacyNotification({ codec = Nats.JSONCodec(), options }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.PublishOptions }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof LegacyNotification ? message : new LegacyNotification(message)).marshal(); dataToSend = codec.encode(dataToSend); nc.publish('legacy.notification', dataToSend, options); @@ -1392,14 +1392,14 @@ function jetStreamPublishToSendLegacyNotification({ codec = Nats.JSONCodec(), options = {} }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, js: Nats.JetStreamClient, codec?: Nats.Codec, options?: Partial }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof LegacyNotification ? message : new LegacyNotification(message)).marshal(); dataToSend = codec.encode(dataToSend); await js.publish('legacy.notification', dataToSend, options); diff --git a/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/LegacyNotification.ts b/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/LegacyNotification.ts index 7a50dcf7..7d17d489 100644 --- a/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/LegacyNotification.ts +++ b/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/LegacyNotification.ts @@ -1,6 +1,11 @@ import {LegacyNotificationPayloadLevelEnum} from './LegacyNotificationPayloadLevelEnum'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface LegacyNotificationInterface { + message?: string + level?: LegacyNotificationPayloadLevelEnum + additionalProperties?: Record +} /** * Legacy notification payload - use NewNotificationPayload instead */ @@ -9,11 +14,7 @@ class LegacyNotification { private _level?: LegacyNotificationPayloadLevelEnum; private _additionalProperties?: Record; - constructor(input: { - message?: string, - level?: LegacyNotificationPayloadLevelEnum, - additionalProperties?: Record, - }) { + constructor(input: LegacyNotificationInterface) { this._message = input.message; this._level = input.level; this._additionalProperties = input.additionalProperties; @@ -93,4 +94,4 @@ class LegacyNotification { } } -export { LegacyNotification }; \ No newline at end of file +export { LegacyNotification, LegacyNotificationInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/UnionPayloadOneOfOption2.ts b/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/UnionPayloadOneOfOption2.ts index d166bc77..62c50aa0 100644 --- a/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/UnionPayloadOneOfOption2.ts +++ b/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/UnionPayloadOneOfOption2.ts @@ -1,13 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface UnionPayloadOneOfOption2Interface { + name?: string + additionalProperties?: Record +} class UnionPayloadOneOfOption2 { private _name?: string; private _additionalProperties?: Record; - constructor(input: { - name?: string, - additionalProperties?: Record, - }) { + constructor(input: UnionPayloadOneOfOption2Interface) { this._name = input.name; this._additionalProperties = input.additionalProperties; } @@ -71,4 +72,4 @@ class UnionPayloadOneOfOption2 { } } -export { UnionPayloadOneOfOption2 }; \ No newline at end of file +export { UnionPayloadOneOfOption2, UnionPayloadOneOfOption2Interface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/UserSignedUp.ts b/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/UserSignedUp.ts index 13f2c67a..881b054e 100644 --- a/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/UserSignedUp.ts +++ b/test/runtime/typescript/src/asyncapi-path-organization/channels/payload/UserSignedUp.ts @@ -1,5 +1,10 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface UserSignedUpInterface { + displayName?: string + email?: string + additionalProperties?: Record +} /** * Payload for user signup events containing user registration details */ @@ -8,11 +13,7 @@ class UserSignedUp { private _email?: string; private _additionalProperties?: Record; - constructor(input: { - displayName?: string, - email?: string, - additionalProperties?: Record, - }) { + constructor(input: UserSignedUpInterface) { this._displayName = input.displayName; this._email = input.email; this._additionalProperties = input.additionalProperties; @@ -92,4 +93,4 @@ class UserSignedUp { } } -export { UserSignedUp }; \ No newline at end of file +export { UserSignedUp, UserSignedUpInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/asyncapi-tag-organization/channels/nats.ts b/test/runtime/typescript/src/asyncapi-tag-organization/channels/nats.ts index dde54388..a78626f9 100644 --- a/test/runtime/typescript/src/asyncapi-tag-organization/channels/nats.ts +++ b/test/runtime/typescript/src/asyncapi-tag-organization/channels/nats.ts @@ -1,6 +1,6 @@ -import {UserSignedUp} from './payload/UserSignedUp'; -import {AdminAlert} from './payload/AdminAlert'; -import {SystemPing} from './payload/SystemPing'; +import {UserSignedUp, UserSignedUpInterface} from './payload/UserSignedUp'; +import {AdminAlert, AdminAlertInterface} from './payload/AdminAlert'; +import {SystemPing, SystemPingInterface} from './payload/SystemPing'; import {UserSignedupParameters, UserSignedupParametersInterface} from './parameter/UserSignedupParameters'; import * as Nats from 'nats'; @@ -20,7 +20,7 @@ function publishToSendUserSignedup({ codec = Nats.JSONCodec(), options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParametersInterface | UserSignedupParameters, nc: Nats.NatsConnection, codec?: Nats.Codec, @@ -28,7 +28,7 @@ function publishToSendUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); dataToSend = codec.encode(dataToSend); nc.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{id}'), dataToSend, options); @@ -55,7 +55,7 @@ function jetStreamPublishToSendUserSignedup({ codec = Nats.JSONCodec(), options = {} }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParametersInterface | UserSignedupParameters, js: Nats.JetStreamClient, codec?: Nats.Codec, @@ -63,7 +63,7 @@ function jetStreamPublishToSendUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); dataToSend = codec.encode(dataToSend); await js.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{id}'), dataToSend, options); @@ -265,14 +265,14 @@ function publishToSendAdminAlert({ codec = Nats.JSONCodec(), options }: { - message: AdminAlert, + message: AdminAlertInterface | AdminAlert, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.PublishOptions }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof AdminAlert ? message : new AdminAlert(message)).marshal(); dataToSend = codec.encode(dataToSend); nc.publish('admin.alert', dataToSend, options); @@ -297,14 +297,14 @@ function jetStreamPublishToSendAdminAlert({ codec = Nats.JSONCodec(), options = {} }: { - message: AdminAlert, + message: AdminAlertInterface | AdminAlert, js: Nats.JetStreamClient, codec?: Nats.Codec, options?: Partial }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof AdminAlert ? message : new AdminAlert(message)).marshal(); dataToSend = codec.encode(dataToSend); await js.publish('admin.alert', dataToSend, options); @@ -329,14 +329,14 @@ function publishToSendSystemPing({ codec = Nats.JSONCodec(), options }: { - message: SystemPing, + message: SystemPingInterface | SystemPing, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.PublishOptions }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof SystemPing ? message : new SystemPing(message)).marshal(); dataToSend = codec.encode(dataToSend); nc.publish('system.ping', dataToSend, options); @@ -361,14 +361,14 @@ function jetStreamPublishToSendSystemPing({ codec = Nats.JSONCodec(), options = {} }: { - message: SystemPing, + message: SystemPingInterface | SystemPing, js: Nats.JetStreamClient, codec?: Nats.Codec, options?: Partial }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof SystemPing ? message : new SystemPing(message)).marshal(); dataToSend = codec.encode(dataToSend); await js.publish('system.ping', dataToSend, options); diff --git a/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/AdminAlert.ts b/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/AdminAlert.ts index bf909d79..5578d658 100644 --- a/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/AdminAlert.ts +++ b/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/AdminAlert.ts @@ -1,11 +1,12 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AdminAlertInterface { + message?: string +} class AdminAlert { private _message?: string; - constructor(input: { - message?: string, - }) { + constructor(input: AdminAlertInterface) { this._message = input.message; } @@ -55,4 +56,4 @@ class AdminAlert { } } -export { AdminAlert }; \ No newline at end of file +export { AdminAlert, AdminAlertInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/SystemPing.ts b/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/SystemPing.ts index 308e232f..6d7eb2f9 100644 --- a/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/SystemPing.ts +++ b/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/SystemPing.ts @@ -1,11 +1,12 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface SystemPingInterface { + timestamp?: Date +} class SystemPing { private _timestamp?: Date; - constructor(input: { - timestamp?: Date, - }) { + constructor(input: SystemPingInterface) { this._timestamp = input.timestamp; } @@ -55,4 +56,4 @@ class SystemPing { } } -export { SystemPing }; \ No newline at end of file +export { SystemPing, SystemPingInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/UserSignedUp.ts b/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/UserSignedUp.ts index e698c1e0..d4e34d6d 100644 --- a/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/UserSignedUp.ts +++ b/test/runtime/typescript/src/asyncapi-tag-organization/channels/payload/UserSignedUp.ts @@ -1,13 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface UserSignedUpInterface { + displayName?: string + email?: string +} class UserSignedUp { private _displayName?: string; private _email?: string; - constructor(input: { - displayName?: string, - email?: string, - }) { + constructor(input: UserSignedUpInterface) { this._displayName = input.displayName; this._email = input.email; } @@ -67,4 +68,4 @@ class UserSignedUp { } } -export { UserSignedUp }; \ No newline at end of file +export { UserSignedUp, UserSignedUpInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/channels/amqp.ts b/test/runtime/typescript/src/channels/amqp.ts index c8abf775..7b986192 100644 --- a/test/runtime/typescript/src/channels/amqp.ts +++ b/test/runtime/typescript/src/channels/amqp.ts @@ -1,9 +1,9 @@ -import {UserSignedUp} from './../payloads/UserSignedUp'; +import {UserSignedUp, UserSignedUpInterface} from './../payloads/UserSignedUp'; import * as StringMessageModule from './../payloads/StringMessage'; import * as ArrayMessageModule from './../payloads/ArrayMessage'; import * as UnionMessageModule from './../payloads/UnionMessage'; -import {LegacyNotification} from './../payloads/LegacyNotification'; -import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; +import {LegacyNotification, LegacyNotificationInterface} from './../payloads/LegacyNotification'; +import {UnionPayloadOneOfOption2, UnionPayloadOneOfOption2Interface} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; @@ -25,7 +25,7 @@ function publishToSendUserSignedupExchange({ amqp, options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, amqp: Amqp.Connection, @@ -37,7 +37,7 @@ function publishToSendUserSignedupExchange({ return reject('No exchange value found, please provide one') } try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); const channel = await amqp.createChannel(); const routingKey = (parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); // Set up message properties (headers) if provided @@ -76,7 +76,7 @@ function publishToSendUserSignedupQueue({ amqp, options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, amqp: Amqp.Connection, @@ -84,7 +84,7 @@ function publishToSendUserSignedupQueue({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); const channel = await amqp.createChannel(); const queue = (parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); // Set up message properties (headers) if provided @@ -180,7 +180,7 @@ function publishToNoParameterExchange({ amqp, options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, headers?: UserSignedUpHeaders, amqp: Amqp.Connection, options?: {exchange: string | undefined} & Amqp.Options.Publish @@ -191,7 +191,7 @@ function publishToNoParameterExchange({ return reject('No exchange value found, please provide one') } try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); const channel = await amqp.createChannel(); const routingKey = 'noparameters'; // Set up message properties (headers) if provided @@ -228,14 +228,14 @@ function publishToNoParameterQueue({ amqp, options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, headers?: UserSignedUpHeaders, amqp: Amqp.Connection, options?: Amqp.Options.Publish }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); const channel = await amqp.createChannel(); const queue = 'noparameters'; // Set up message properties (headers) if provided @@ -658,7 +658,7 @@ function publishToSendLegacyNotificationExchange({ amqp, options }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, amqp: Amqp.Connection, options?: {exchange: string | undefined} & Amqp.Options.Publish }): Promise { @@ -668,7 +668,7 @@ function publishToSendLegacyNotificationExchange({ return reject('No exchange value found, please provide one') } try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof LegacyNotification ? message : new LegacyNotification(message)).marshal(); const channel = await amqp.createChannel(); const routingKey = 'legacy/notification'; let publishOptions = { ...options }; @@ -694,13 +694,13 @@ function publishToSendLegacyNotificationQueue({ amqp, options }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, amqp: Amqp.Connection, options?: Amqp.Options.Publish }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof LegacyNotification ? message : new LegacyNotification(message)).marshal(); const channel = await amqp.createChannel(); const queue = 'legacy/notification'; let publishOptions = { ...options }; diff --git a/test/runtime/typescript/src/channels/event_source.ts b/test/runtime/typescript/src/channels/event_source.ts index 69d41912..a1665193 100644 --- a/test/runtime/typescript/src/channels/event_source.ts +++ b/test/runtime/typescript/src/channels/event_source.ts @@ -1,9 +1,9 @@ -import {UserSignedUp} from './../payloads/UserSignedUp'; +import {UserSignedUp, UserSignedUpInterface} from './../payloads/UserSignedUp'; import * as StringMessageModule from './../payloads/StringMessage'; import * as ArrayMessageModule from './../payloads/ArrayMessage'; import * as UnionMessageModule from './../payloads/UnionMessage'; -import {LegacyNotification} from './../payloads/LegacyNotification'; -import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; +import {LegacyNotification, LegacyNotificationInterface} from './../payloads/LegacyNotification'; +import {UnionPayloadOneOfOption2, UnionPayloadOneOfOption2Interface} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; diff --git a/test/runtime/typescript/src/channels/kafka.ts b/test/runtime/typescript/src/channels/kafka.ts index ef5d6450..82fe588f 100644 --- a/test/runtime/typescript/src/channels/kafka.ts +++ b/test/runtime/typescript/src/channels/kafka.ts @@ -1,9 +1,9 @@ -import {UserSignedUp} from './../payloads/UserSignedUp'; +import {UserSignedUp, UserSignedUpInterface} from './../payloads/UserSignedUp'; import * as StringMessageModule from './../payloads/StringMessage'; import * as ArrayMessageModule from './../payloads/ArrayMessage'; import * as UnionMessageModule from './../payloads/UnionMessage'; -import {LegacyNotification} from './../payloads/LegacyNotification'; -import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; +import {LegacyNotification, LegacyNotificationInterface} from './../payloads/LegacyNotification'; +import {UnionPayloadOneOfOption2, UnionPayloadOneOfOption2Interface} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; @@ -23,14 +23,14 @@ function produceToSendUserSignedup({ headers, kafka }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, kafka: Kafka.Kafka }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); const producer = kafka.producer(); await producer.connect(); // Set up headers if provided @@ -151,13 +151,13 @@ function produceToNoParameter({ headers, kafka }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, headers?: UserSignedUpHeaders, kafka: Kafka.Kafka }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); const producer = kafka.producer(); await producer.connect(); // Set up headers if provided @@ -559,12 +559,12 @@ function produceToSendLegacyNotification({ message, kafka }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, kafka: Kafka.Kafka }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof LegacyNotification ? message : new LegacyNotification(message)).marshal(); const producer = kafka.producer(); await producer.connect(); diff --git a/test/runtime/typescript/src/channels/mqtt.ts b/test/runtime/typescript/src/channels/mqtt.ts index 94053e23..335ff5cb 100644 --- a/test/runtime/typescript/src/channels/mqtt.ts +++ b/test/runtime/typescript/src/channels/mqtt.ts @@ -1,9 +1,9 @@ -import {UserSignedUp} from './../payloads/UserSignedUp'; +import {UserSignedUp, UserSignedUpInterface} from './../payloads/UserSignedUp'; import * as StringMessageModule from './../payloads/StringMessage'; import * as ArrayMessageModule from './../payloads/ArrayMessage'; import * as UnionMessageModule from './../payloads/UnionMessage'; -import {LegacyNotification} from './../payloads/LegacyNotification'; -import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; +import {LegacyNotification, LegacyNotificationInterface} from './../payloads/LegacyNotification'; +import {UnionPayloadOneOfOption2, UnionPayloadOneOfOption2Interface} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; @@ -23,14 +23,14 @@ function publishToSendUserSignedup({ headers, mqtt }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, mqtt: Mqtt.MqttClient }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); // Set up user properties (headers) if provided let publishOptions: Mqtt.IClientPublishOptions = {}; if (headers) { @@ -147,13 +147,13 @@ function publishToNoParameter({ headers, mqtt }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, headers?: UserSignedUpHeaders, mqtt: Mqtt.MqttClient }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); // Set up user properties (headers) if provided let publishOptions: Mqtt.IClientPublishOptions = {}; if (headers) { @@ -545,12 +545,12 @@ function publishToSendLegacyNotification({ message, mqtt }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, mqtt: Mqtt.MqttClient }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof LegacyNotification ? message : new LegacyNotification(message)).marshal(); let publishOptions: Mqtt.IClientPublishOptions = {}; mqtt.publish('legacy/notification', dataToSend, publishOptions); resolve(); diff --git a/test/runtime/typescript/src/channels/nats.ts b/test/runtime/typescript/src/channels/nats.ts index 7341aac4..dfb2b7ae 100644 --- a/test/runtime/typescript/src/channels/nats.ts +++ b/test/runtime/typescript/src/channels/nats.ts @@ -1,9 +1,9 @@ -import {UserSignedUp} from './../payloads/UserSignedUp'; +import {UserSignedUp, UserSignedUpInterface} from './../payloads/UserSignedUp'; import * as StringMessageModule from './../payloads/StringMessage'; import * as ArrayMessageModule from './../payloads/ArrayMessage'; import * as UnionMessageModule from './../payloads/UnionMessage'; -import {LegacyNotification} from './../payloads/LegacyNotification'; -import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; +import {LegacyNotification, LegacyNotificationInterface} from './../payloads/LegacyNotification'; +import {UnionPayloadOneOfOption2, UnionPayloadOneOfOption2Interface} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; @@ -27,7 +27,7 @@ function publishToSendUserSignedup({ codec = Nats.JSONCodec(), options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, nc: Nats.NatsConnection, @@ -36,7 +36,7 @@ function publishToSendUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); // Set up headers if provided if (headers) { const natsHeaders = Nats.headers(); @@ -76,7 +76,7 @@ function jetStreamPublishToSendUserSignedup({ codec = Nats.JSONCodec(), options = {} }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, js: Nats.JetStreamClient, @@ -85,7 +85,7 @@ function jetStreamPublishToSendUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); // Set up headers if provided if (headers) { const natsHeaders = Nats.headers(); @@ -357,7 +357,7 @@ function publishToNoParameter({ codec = Nats.JSONCodec(), options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, headers?: UserSignedUpHeaders, nc: Nats.NatsConnection, codec?: Nats.Codec, @@ -365,7 +365,7 @@ function publishToNoParameter({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); // Set up headers if provided if (headers) { const natsHeaders = Nats.headers(); @@ -625,7 +625,7 @@ function jetStreamPublishToNoParameter({ codec = Nats.JSONCodec(), options = {} }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, headers?: UserSignedUpHeaders, js: Nats.JetStreamClient, codec?: Nats.Codec, @@ -633,7 +633,7 @@ function jetStreamPublishToNoParameter({ }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(); // Set up headers if provided if (headers) { const natsHeaders = Nats.headers(); @@ -1358,14 +1358,14 @@ function publishToSendLegacyNotification({ codec = Nats.JSONCodec(), options }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.PublishOptions }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof LegacyNotification ? message : new LegacyNotification(message)).marshal(); dataToSend = codec.encode(dataToSend); nc.publish('legacy.notification', dataToSend, options); @@ -1392,14 +1392,14 @@ function jetStreamPublishToSendLegacyNotification({ codec = Nats.JSONCodec(), options = {} }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, js: Nats.JetStreamClient, codec?: Nats.Codec, options?: Partial }): Promise { return new Promise(async (resolve, reject) => { try { - let dataToSend: any = message.marshal(); + let dataToSend: any = (message instanceof LegacyNotification ? message : new LegacyNotification(message)).marshal(); dataToSend = codec.encode(dataToSend); await js.publish('legacy.notification', dataToSend, options); diff --git a/test/runtime/typescript/src/channels/websocket.ts b/test/runtime/typescript/src/channels/websocket.ts index c9bfef02..85654f28 100644 --- a/test/runtime/typescript/src/channels/websocket.ts +++ b/test/runtime/typescript/src/channels/websocket.ts @@ -1,9 +1,9 @@ -import {UserSignedUp} from './../payloads/UserSignedUp'; +import {UserSignedUp, UserSignedUpInterface} from './../payloads/UserSignedUp'; import * as StringMessageModule from './../payloads/StringMessage'; import * as ArrayMessageModule from './../payloads/ArrayMessage'; import * as UnionMessageModule from './../payloads/UnionMessage'; -import {LegacyNotification} from './../payloads/LegacyNotification'; -import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; +import {LegacyNotification, LegacyNotificationInterface} from './../payloads/LegacyNotification'; +import {UnionPayloadOneOfOption2, UnionPayloadOneOfOption2Interface} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; @@ -20,7 +20,7 @@ function publishToSendUserSignedup({ message, ws }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, ws: WebSocket.WebSocket }): Promise { return new Promise((resolve, reject) => { @@ -31,7 +31,7 @@ function publishToSendUserSignedup({ } // Send message directly - ws.send(message.marshal(), (err) => { + ws.send((message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(), (err) => { if (err) { reject(new Error(`Failed to send message: ${err.message}`)); } @@ -208,7 +208,7 @@ function publishToNoParameter({ message, ws }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, ws: WebSocket.WebSocket }): Promise { return new Promise((resolve, reject) => { @@ -219,7 +219,7 @@ function publishToNoParameter({ } // Send message directly - ws.send(message.marshal(), (err) => { + ws.send((message instanceof UserSignedUp ? message : new UserSignedUp(message)).marshal(), (err) => { if (err) { reject(new Error(`Failed to send message: ${err.message}`)); } @@ -910,7 +910,7 @@ function publishToSendLegacyNotification({ message, ws }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, ws: WebSocket.WebSocket }): Promise { return new Promise((resolve, reject) => { @@ -921,7 +921,7 @@ function publishToSendLegacyNotification({ } // Send message directly - ws.send(message.marshal(), (err) => { + ws.send((message instanceof LegacyNotification ? message : new LegacyNotification(message)).marshal(), (err) => { if (err) { reject(new Error(`Failed to send message: ${err.message}`)); } diff --git a/test/runtime/typescript/src/client/NatsClient.ts b/test/runtime/typescript/src/client/NatsClient.ts index 3388dc9a..149eb794 100644 --- a/test/runtime/typescript/src/client/NatsClient.ts +++ b/test/runtime/typescript/src/client/NatsClient.ts @@ -1,9 +1,9 @@ -import {UserSignedUp} from './../payloads/UserSignedUp'; +import {UserSignedUp, UserSignedUpInterface} from './../payloads/UserSignedUp'; import * as StringMessageModule from './../payloads/StringMessage'; import * as ArrayMessageModule from './../payloads/ArrayMessage'; import * as UnionMessageModule from './../payloads/UnionMessage'; -import {LegacyNotification} from './../payloads/LegacyNotification'; -import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; +import {LegacyNotification, LegacyNotificationInterface} from './../payloads/LegacyNotification'; +import {UnionPayloadOneOfOption2, UnionPayloadOneOfOption2Interface} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; export {UserSignedUp}; export {StringMessageModule}; @@ -124,7 +124,7 @@ export class NatsClient { parameters, options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParameters, options?: Nats.PublishOptions }): Promise { @@ -153,7 +153,7 @@ export class NatsClient { parameters, options = {} }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, parameters: UserSignedupParameters, options?: Partial }): Promise { @@ -296,7 +296,7 @@ export class NatsClient { message, options }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, options?: Nats.PublishOptions }): Promise { if (!this.isClosed() && this.nc !== undefined && this.codec !== undefined) { @@ -424,7 +424,7 @@ export class NatsClient { message, options = {} }: { - message: UserSignedUp, + message: UserSignedUpInterface | UserSignedUp, options?: Partial }): Promise { if (!this.isClosed() && this.nc !== undefined && this.codec !== undefined && this.js !== undefined) { @@ -912,7 +912,7 @@ export class NatsClient { message, options }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, options?: Nats.PublishOptions }): Promise { if (!this.isClosed() && this.nc !== undefined && this.codec !== undefined) { @@ -937,7 +937,7 @@ export class NatsClient { message, options = {} }: { - message: LegacyNotification, + message: LegacyNotificationInterface | LegacyNotification, options?: Partial }): Promise { if (!this.isClosed() && this.nc !== undefined && this.codec !== undefined && this.js !== undefined) { diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts b/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts index d1a9164b..98c1ed43 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts @@ -1,12 +1,12 @@ -import {APet} from './payload/APet'; +import {APet, APetInterface} from './payload/APet'; import * as FindPetsByStatusAndCategoryResponse_200Module from './payload/FindPetsByStatusAndCategoryResponse_200'; -import {PetCategory} from './payload/PetCategory'; -import {PetTag} from './payload/PetTag'; +import {PetCategory, PetCategoryInterface} from './payload/PetCategory'; +import {PetTag, PetTagInterface} from './payload/PetTag'; import {Status} from './payload/Status'; import {ItemStatus} from './payload/ItemStatus'; -import {PetOrder} from './payload/PetOrder'; -import {AUser} from './payload/AUser'; -import {AnUploadedResponse} from './payload/AnUploadedResponse'; +import {PetOrder, PetOrderInterface} from './payload/PetOrder'; +import {AUser, AUserInterface} from './payload/AUser'; +import {AnUploadedResponse, AnUploadedResponseInterface} from './payload/AnUploadedResponse'; import {FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface} from './parameter/FindPetsByStatusAndCategoryParameters'; import {FindPetsByStatusAndCategoryHeaders, serializeFindPetsByStatusAndCategoryHeadersHeaders} from './headers/FindPetsByStatusAndCategoryHeaders'; @@ -619,7 +619,7 @@ async function handleTokenRefresh( // ============================================================================ export interface AddPetContext extends HttpClientContext { - payload: APet; + payload: APetInterface | APet; } /** @@ -650,7 +650,8 @@ async function addPet(context: AddPetContext): Promise> url = authResult.url; // Prepare body - const body = context.payload?.marshal(); + const payload = context.payload instanceof APet ? context.payload : new APet(context.payload); + const body = payload?.marshal(); // Determine request function const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; @@ -729,7 +730,7 @@ async function addPet(context: AddPetContext): Promise> } export interface UpdatePetContext extends HttpClientContext { - payload: APet; + payload: APetInterface | APet; } /** @@ -760,7 +761,8 @@ async function updatePet(context: UpdatePetContext): Promise +} /** * A pet for sale in the pet store */ @@ -15,15 +24,7 @@ class APet { private _status?: Status; private _additionalProperties?: Record; - constructor(input: { - id?: number, - category?: PetCategory, - name: string, - photoUrls: string[], - tags?: PetTag[], - status?: Status, - additionalProperties?: Record, - }) { + constructor(input: APetInterface) { this._id = input.id; this._category = input.category; this._name = input.name; @@ -153,4 +154,4 @@ class APet { } } -export { APet }; \ No newline at end of file +export { APet, APetInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/payload/AUser.ts b/test/runtime/typescript/src/openapi-path-organization/channels/payload/AUser.ts index 939e139a..f4ca1914 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/payload/AUser.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/payload/AUser.ts @@ -1,5 +1,16 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AUserInterface { + id?: number + username?: string + firstName?: string + lastName?: string + email?: string + password?: string + phone?: string + userStatus?: number + additionalProperties?: Record +} /** * A User who is purchasing from the pet store */ @@ -14,17 +25,7 @@ class AUser { private _userStatus?: number; private _additionalProperties?: Record; - constructor(input: { - id?: number, - username?: string, - firstName?: string, - lastName?: string, - email?: string, - password?: string, - phone?: string, - userStatus?: number, - additionalProperties?: Record, - }) { + constructor(input: AUserInterface) { this._id = input.id; this._username = input.username; this._firstName = input.firstName; @@ -161,4 +162,4 @@ class AUser { } } -export { AUser }; \ No newline at end of file +export { AUser, AUserInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/payload/AnUploadedResponse.ts b/test/runtime/typescript/src/openapi-path-organization/channels/payload/AnUploadedResponse.ts index eaf68676..188e7ef4 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/payload/AnUploadedResponse.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/payload/AnUploadedResponse.ts @@ -1,5 +1,11 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AnUploadedResponseInterface { + code?: number + type?: string + message?: string + additionalProperties?: Record +} /** * Describes the result of uploading an image resource */ @@ -9,12 +15,7 @@ class AnUploadedResponse { private _message?: string; private _additionalProperties?: Record; - constructor(input: { - code?: number, - type?: string, - message?: string, - additionalProperties?: Record, - }) { + constructor(input: AnUploadedResponseInterface) { this._code = input.code; this._type = input.type; this._message = input.message; @@ -98,4 +99,4 @@ class AnUploadedResponse { } } -export { AnUploadedResponse }; \ No newline at end of file +export { AnUploadedResponse, AnUploadedResponseInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetCategory.ts b/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetCategory.ts index 56f53566..0560f003 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetCategory.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetCategory.ts @@ -1,5 +1,10 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PetCategoryInterface { + id?: number + name?: string + additionalProperties?: Record +} /** * A category for a pet */ @@ -8,11 +13,7 @@ class PetCategory { private _name?: string; private _additionalProperties?: Record; - constructor(input: { - id?: number, - name?: string, - additionalProperties?: Record, - }) { + constructor(input: PetCategoryInterface) { this._id = input.id; this._name = input.name; this._additionalProperties = input.additionalProperties; @@ -86,4 +87,4 @@ class PetCategory { } } -export { PetCategory }; \ No newline at end of file +export { PetCategory, PetCategoryInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetOrder.ts b/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetOrder.ts index ec225b6b..5d80c839 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetOrder.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetOrder.ts @@ -1,6 +1,15 @@ import {Status} from './Status'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PetOrderInterface { + id?: number + petId?: number + quantity?: number + shipDate?: Date + status?: Status + complete?: boolean + additionalProperties?: Record +} /** * An order for a pets from the pet store */ @@ -13,15 +22,7 @@ class PetOrder { private _complete?: boolean; private _additionalProperties?: Record; - constructor(input: { - id?: number, - petId?: number, - quantity?: number, - shipDate?: Date, - status?: Status, - complete?: boolean, - additionalProperties?: Record, - }) { + constructor(input: PetOrderInterface) { this._id = input.id; this._petId = input.petId; this._quantity = input.quantity; @@ -138,4 +139,4 @@ class PetOrder { } } -export { PetOrder }; \ No newline at end of file +export { PetOrder, PetOrderInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetTag.ts b/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetTag.ts index 6a7dab71..21157394 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetTag.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/payload/PetTag.ts @@ -1,5 +1,10 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PetTagInterface { + id?: number + name?: string + additionalProperties?: Record +} /** * A tag for a pet */ @@ -8,11 +13,7 @@ class PetTag { private _name?: string; private _additionalProperties?: Record; - constructor(input: { - id?: number, - name?: string, - additionalProperties?: Record, - }) { + constructor(input: PetTagInterface) { this._id = input.id; this._name = input.name; this._additionalProperties = input.additionalProperties; @@ -86,4 +87,4 @@ class PetTag { } } -export { PetTag }; \ No newline at end of file +export { PetTag, PetTagInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts index d1a9164b..98c1ed43 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts @@ -1,12 +1,12 @@ -import {APet} from './payload/APet'; +import {APet, APetInterface} from './payload/APet'; import * as FindPetsByStatusAndCategoryResponse_200Module from './payload/FindPetsByStatusAndCategoryResponse_200'; -import {PetCategory} from './payload/PetCategory'; -import {PetTag} from './payload/PetTag'; +import {PetCategory, PetCategoryInterface} from './payload/PetCategory'; +import {PetTag, PetTagInterface} from './payload/PetTag'; import {Status} from './payload/Status'; import {ItemStatus} from './payload/ItemStatus'; -import {PetOrder} from './payload/PetOrder'; -import {AUser} from './payload/AUser'; -import {AnUploadedResponse} from './payload/AnUploadedResponse'; +import {PetOrder, PetOrderInterface} from './payload/PetOrder'; +import {AUser, AUserInterface} from './payload/AUser'; +import {AnUploadedResponse, AnUploadedResponseInterface} from './payload/AnUploadedResponse'; import {FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface} from './parameter/FindPetsByStatusAndCategoryParameters'; import {FindPetsByStatusAndCategoryHeaders, serializeFindPetsByStatusAndCategoryHeadersHeaders} from './headers/FindPetsByStatusAndCategoryHeaders'; @@ -619,7 +619,7 @@ async function handleTokenRefresh( // ============================================================================ export interface AddPetContext extends HttpClientContext { - payload: APet; + payload: APetInterface | APet; } /** @@ -650,7 +650,8 @@ async function addPet(context: AddPetContext): Promise> url = authResult.url; // Prepare body - const body = context.payload?.marshal(); + const payload = context.payload instanceof APet ? context.payload : new APet(context.payload); + const body = payload?.marshal(); // Determine request function const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; @@ -729,7 +730,7 @@ async function addPet(context: AddPetContext): Promise> } export interface UpdatePetContext extends HttpClientContext { - payload: APet; + payload: APetInterface | APet; } /** @@ -760,7 +761,8 @@ async function updatePet(context: UpdatePetContext): Promise +} /** * A pet for sale in the pet store */ @@ -15,15 +24,7 @@ class APet { private _status?: Status; private _additionalProperties?: Record; - constructor(input: { - id?: number, - category?: PetCategory, - name: string, - photoUrls: string[], - tags?: PetTag[], - status?: Status, - additionalProperties?: Record, - }) { + constructor(input: APetInterface) { this._id = input.id; this._category = input.category; this._name = input.name; @@ -153,4 +154,4 @@ class APet { } } -export { APet }; \ No newline at end of file +export { APet, APetInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/payload/AUser.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/payload/AUser.ts index 939e139a..f4ca1914 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/payload/AUser.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/payload/AUser.ts @@ -1,5 +1,16 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AUserInterface { + id?: number + username?: string + firstName?: string + lastName?: string + email?: string + password?: string + phone?: string + userStatus?: number + additionalProperties?: Record +} /** * A User who is purchasing from the pet store */ @@ -14,17 +25,7 @@ class AUser { private _userStatus?: number; private _additionalProperties?: Record; - constructor(input: { - id?: number, - username?: string, - firstName?: string, - lastName?: string, - email?: string, - password?: string, - phone?: string, - userStatus?: number, - additionalProperties?: Record, - }) { + constructor(input: AUserInterface) { this._id = input.id; this._username = input.username; this._firstName = input.firstName; @@ -161,4 +162,4 @@ class AUser { } } -export { AUser }; \ No newline at end of file +export { AUser, AUserInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/payload/AnUploadedResponse.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/payload/AnUploadedResponse.ts index eaf68676..188e7ef4 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/payload/AnUploadedResponse.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/payload/AnUploadedResponse.ts @@ -1,5 +1,11 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AnUploadedResponseInterface { + code?: number + type?: string + message?: string + additionalProperties?: Record +} /** * Describes the result of uploading an image resource */ @@ -9,12 +15,7 @@ class AnUploadedResponse { private _message?: string; private _additionalProperties?: Record; - constructor(input: { - code?: number, - type?: string, - message?: string, - additionalProperties?: Record, - }) { + constructor(input: AnUploadedResponseInterface) { this._code = input.code; this._type = input.type; this._message = input.message; @@ -98,4 +99,4 @@ class AnUploadedResponse { } } -export { AnUploadedResponse }; \ No newline at end of file +export { AnUploadedResponse, AnUploadedResponseInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetCategory.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetCategory.ts index 56f53566..0560f003 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetCategory.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetCategory.ts @@ -1,5 +1,10 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PetCategoryInterface { + id?: number + name?: string + additionalProperties?: Record +} /** * A category for a pet */ @@ -8,11 +13,7 @@ class PetCategory { private _name?: string; private _additionalProperties?: Record; - constructor(input: { - id?: number, - name?: string, - additionalProperties?: Record, - }) { + constructor(input: PetCategoryInterface) { this._id = input.id; this._name = input.name; this._additionalProperties = input.additionalProperties; @@ -86,4 +87,4 @@ class PetCategory { } } -export { PetCategory }; \ No newline at end of file +export { PetCategory, PetCategoryInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetOrder.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetOrder.ts index ec225b6b..5d80c839 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetOrder.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetOrder.ts @@ -1,6 +1,15 @@ import {Status} from './Status'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PetOrderInterface { + id?: number + petId?: number + quantity?: number + shipDate?: Date + status?: Status + complete?: boolean + additionalProperties?: Record +} /** * An order for a pets from the pet store */ @@ -13,15 +22,7 @@ class PetOrder { private _complete?: boolean; private _additionalProperties?: Record; - constructor(input: { - id?: number, - petId?: number, - quantity?: number, - shipDate?: Date, - status?: Status, - complete?: boolean, - additionalProperties?: Record, - }) { + constructor(input: PetOrderInterface) { this._id = input.id; this._petId = input.petId; this._quantity = input.quantity; @@ -138,4 +139,4 @@ class PetOrder { } } -export { PetOrder }; \ No newline at end of file +export { PetOrder, PetOrderInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetTag.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetTag.ts index 6a7dab71..21157394 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetTag.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/payload/PetTag.ts @@ -1,5 +1,10 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PetTagInterface { + id?: number + name?: string + additionalProperties?: Record +} /** * A tag for a pet */ @@ -8,11 +13,7 @@ class PetTag { private _name?: string; private _additionalProperties?: Record; - constructor(input: { - id?: number, - name?: string, - additionalProperties?: Record, - }) { + constructor(input: PetTagInterface) { this._id = input.id; this._name = input.name; this._additionalProperties = input.additionalProperties; @@ -86,4 +87,4 @@ class PetTag { } } -export { PetTag }; \ No newline at end of file +export { PetTag, PetTagInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/channels/http_client.ts b/test/runtime/typescript/src/openapi/channels/http_client.ts index dad13807..b356f6c5 100644 --- a/test/runtime/typescript/src/openapi/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi/channels/http_client.ts @@ -1,12 +1,12 @@ -import {APet} from './../payloads/APet'; +import {APet, APetInterface} from './../payloads/APet'; import * as FindPetsByStatusAndCategoryResponse_200Module from './../payloads/FindPetsByStatusAndCategoryResponse_200'; -import {PetCategory} from './../payloads/PetCategory'; -import {PetTag} from './../payloads/PetTag'; +import {PetCategory, PetCategoryInterface} from './../payloads/PetCategory'; +import {PetTag, PetTagInterface} from './../payloads/PetTag'; import {Status} from './../payloads/Status'; import {ItemStatus} from './../payloads/ItemStatus'; -import {PetOrder} from './../payloads/PetOrder'; -import {AUser} from './../payloads/AUser'; -import {AnUploadedResponse} from './../payloads/AnUploadedResponse'; +import {PetOrder, PetOrderInterface} from './../payloads/PetOrder'; +import {AUser, AUserInterface} from './../payloads/AUser'; +import {AnUploadedResponse, AnUploadedResponseInterface} from './../payloads/AnUploadedResponse'; import {FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface} from './../parameters/FindPetsByStatusAndCategoryParameters'; import {FindPetsByStatusAndCategoryHeaders, serializeFindPetsByStatusAndCategoryHeadersHeaders} from './../headers/FindPetsByStatusAndCategoryHeaders'; @@ -619,7 +619,7 @@ async function handleTokenRefresh( // ============================================================================ export interface AddPetContext extends HttpClientContext { - payload: APet; + payload: APetInterface | APet; } /** @@ -650,7 +650,8 @@ async function addPet(context: AddPetContext): Promise> url = authResult.url; // Prepare body - const body = context.payload?.marshal(); + const payload = context.payload instanceof APet ? context.payload : new APet(context.payload); + const body = payload?.marshal(); // Determine request function const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; @@ -729,7 +730,7 @@ async function addPet(context: AddPetContext): Promise> } export interface UpdatePetContext extends HttpClientContext { - payload: APet; + payload: APetInterface | APet; } /** @@ -760,7 +761,8 @@ async function updatePet(context: UpdatePetContext): Promise +} /** * A pet for sale in the pet store */ @@ -15,15 +24,7 @@ class APet { private _status?: Status; private _additionalProperties?: Record; - constructor(input: { - id?: number, - category?: PetCategory, - name: string, - photoUrls: string[], - tags?: PetTag[], - status?: Status, - additionalProperties?: Record, - }) { + constructor(input: APetInterface) { this._id = input.id; this._category = input.category; this._name = input.name; @@ -153,4 +154,4 @@ class APet { } } -export { APet }; \ No newline at end of file +export { APet, APetInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/AUser.ts b/test/runtime/typescript/src/openapi/payloads/AUser.ts index 939e139a..f4ca1914 100644 --- a/test/runtime/typescript/src/openapi/payloads/AUser.ts +++ b/test/runtime/typescript/src/openapi/payloads/AUser.ts @@ -1,5 +1,16 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AUserInterface { + id?: number + username?: string + firstName?: string + lastName?: string + email?: string + password?: string + phone?: string + userStatus?: number + additionalProperties?: Record +} /** * A User who is purchasing from the pet store */ @@ -14,17 +25,7 @@ class AUser { private _userStatus?: number; private _additionalProperties?: Record; - constructor(input: { - id?: number, - username?: string, - firstName?: string, - lastName?: string, - email?: string, - password?: string, - phone?: string, - userStatus?: number, - additionalProperties?: Record, - }) { + constructor(input: AUserInterface) { this._id = input.id; this._username = input.username; this._firstName = input.firstName; @@ -161,4 +162,4 @@ class AUser { } } -export { AUser }; \ No newline at end of file +export { AUser, AUserInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/AnUploadedResponse.ts b/test/runtime/typescript/src/openapi/payloads/AnUploadedResponse.ts index eaf68676..188e7ef4 100644 --- a/test/runtime/typescript/src/openapi/payloads/AnUploadedResponse.ts +++ b/test/runtime/typescript/src/openapi/payloads/AnUploadedResponse.ts @@ -1,5 +1,11 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AnUploadedResponseInterface { + code?: number + type?: string + message?: string + additionalProperties?: Record +} /** * Describes the result of uploading an image resource */ @@ -9,12 +15,7 @@ class AnUploadedResponse { private _message?: string; private _additionalProperties?: Record; - constructor(input: { - code?: number, - type?: string, - message?: string, - additionalProperties?: Record, - }) { + constructor(input: AnUploadedResponseInterface) { this._code = input.code; this._type = input.type; this._message = input.message; @@ -98,4 +99,4 @@ class AnUploadedResponse { } } -export { AnUploadedResponse }; \ No newline at end of file +export { AnUploadedResponse, AnUploadedResponseInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/PetCategory.ts b/test/runtime/typescript/src/openapi/payloads/PetCategory.ts index 56f53566..0560f003 100644 --- a/test/runtime/typescript/src/openapi/payloads/PetCategory.ts +++ b/test/runtime/typescript/src/openapi/payloads/PetCategory.ts @@ -1,5 +1,10 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PetCategoryInterface { + id?: number + name?: string + additionalProperties?: Record +} /** * A category for a pet */ @@ -8,11 +13,7 @@ class PetCategory { private _name?: string; private _additionalProperties?: Record; - constructor(input: { - id?: number, - name?: string, - additionalProperties?: Record, - }) { + constructor(input: PetCategoryInterface) { this._id = input.id; this._name = input.name; this._additionalProperties = input.additionalProperties; @@ -86,4 +87,4 @@ class PetCategory { } } -export { PetCategory }; \ No newline at end of file +export { PetCategory, PetCategoryInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/PetOrder.ts b/test/runtime/typescript/src/openapi/payloads/PetOrder.ts index ec225b6b..5d80c839 100644 --- a/test/runtime/typescript/src/openapi/payloads/PetOrder.ts +++ b/test/runtime/typescript/src/openapi/payloads/PetOrder.ts @@ -1,6 +1,15 @@ import {Status} from './Status'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PetOrderInterface { + id?: number + petId?: number + quantity?: number + shipDate?: Date + status?: Status + complete?: boolean + additionalProperties?: Record +} /** * An order for a pets from the pet store */ @@ -13,15 +22,7 @@ class PetOrder { private _complete?: boolean; private _additionalProperties?: Record; - constructor(input: { - id?: number, - petId?: number, - quantity?: number, - shipDate?: Date, - status?: Status, - complete?: boolean, - additionalProperties?: Record, - }) { + constructor(input: PetOrderInterface) { this._id = input.id; this._petId = input.petId; this._quantity = input.quantity; @@ -138,4 +139,4 @@ class PetOrder { } } -export { PetOrder }; \ No newline at end of file +export { PetOrder, PetOrderInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/payloads/PetTag.ts b/test/runtime/typescript/src/openapi/payloads/PetTag.ts index 6a7dab71..21157394 100644 --- a/test/runtime/typescript/src/openapi/payloads/PetTag.ts +++ b/test/runtime/typescript/src/openapi/payloads/PetTag.ts @@ -1,5 +1,10 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PetTagInterface { + id?: number + name?: string + additionalProperties?: Record +} /** * A tag for a pet */ @@ -8,11 +13,7 @@ class PetTag { private _name?: string; private _additionalProperties?: Record; - constructor(input: { - id?: number, - name?: string, - additionalProperties?: Record, - }) { + constructor(input: PetTagInterface) { this._id = input.id; this._name = input.name; this._additionalProperties = input.additionalProperties; @@ -86,4 +87,4 @@ class PetTag { } } -export { PetTag }; \ No newline at end of file +export { PetTag, PetTagInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/AllOfThreeTypes.ts b/test/runtime/typescript/src/payload-types/payloads/AllOfThreeTypes.ts index cc17bea3..30c67a44 100644 --- a/test/runtime/typescript/src/payload-types/payloads/AllOfThreeTypes.ts +++ b/test/runtime/typescript/src/payload-types/payloads/AllOfThreeTypes.ts @@ -1,5 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AllOfThreeTypesInterface { + id: string + name?: string + createdAt?: Date + updatedAt?: Date + createdBy?: string + updatedBy?: string + additionalProperties?: Record +} /** * AllOf combining three schemas */ @@ -12,15 +21,7 @@ class AllOfThreeTypes { private _updatedBy?: string; private _additionalProperties?: Record; - constructor(input: { - id: string, - name?: string, - createdAt?: Date, - updatedAt?: Date, - createdBy?: string, - updatedBy?: string, - additionalProperties?: Record, - }) { + constructor(input: AllOfThreeTypesInterface) { this._id = input.id; this._name = input.name; this._createdAt = input.createdAt; @@ -134,4 +135,4 @@ class AllOfThreeTypes { } } -export { AllOfThreeTypes }; \ No newline at end of file +export { AllOfThreeTypes, AllOfThreeTypesInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/AllOfTwoTypes.ts b/test/runtime/typescript/src/payload-types/payloads/AllOfTwoTypes.ts index 26620127..1e3db06b 100644 --- a/test/runtime/typescript/src/payload-types/payloads/AllOfTwoTypes.ts +++ b/test/runtime/typescript/src/payload-types/payloads/AllOfTwoTypes.ts @@ -1,5 +1,12 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AllOfTwoTypesInterface { + id: string + name?: string + createdAt?: Date + updatedAt?: Date + additionalProperties?: Record +} /** * AllOf combining base entity and timestamp */ @@ -10,13 +17,7 @@ class AllOfTwoTypes { private _updatedAt?: Date; private _additionalProperties?: Record; - constructor(input: { - id: string, - name?: string, - createdAt?: Date, - updatedAt?: Date, - additionalProperties?: Record, - }) { + constructor(input: AllOfTwoTypesInterface) { this._id = input.id; this._name = input.name; this._createdAt = input.createdAt; @@ -110,4 +111,4 @@ class AllOfTwoTypes { } } -export { AllOfTwoTypes }; \ No newline at end of file +export { AllOfTwoTypes, AllOfTwoTypesInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/AnyOfTwoTypesAnyOfOption0.ts b/test/runtime/typescript/src/payload-types/payloads/AnyOfTwoTypesAnyOfOption0.ts index 83b22f9d..de05365e 100644 --- a/test/runtime/typescript/src/payload-types/payloads/AnyOfTwoTypesAnyOfOption0.ts +++ b/test/runtime/typescript/src/payload-types/payloads/AnyOfTwoTypesAnyOfOption0.ts @@ -1,13 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AnyOfTwoTypesAnyOfOption0Interface { + name?: string + additionalProperties?: Record +} class AnyOfTwoTypesAnyOfOption0 { private _name?: string; private _additionalProperties?: Record; - constructor(input: { - name?: string, - additionalProperties?: Record, - }) { + constructor(input: AnyOfTwoTypesAnyOfOption0Interface) { this._name = input.name; this._additionalProperties = input.additionalProperties; } @@ -71,4 +72,4 @@ class AnyOfTwoTypesAnyOfOption0 { } } -export { AnyOfTwoTypesAnyOfOption0 }; \ No newline at end of file +export { AnyOfTwoTypesAnyOfOption0, AnyOfTwoTypesAnyOfOption0Interface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/AnyOfTwoTypesAnyOfOption1.ts b/test/runtime/typescript/src/payload-types/payloads/AnyOfTwoTypesAnyOfOption1.ts index 190614fb..b03daea4 100644 --- a/test/runtime/typescript/src/payload-types/payloads/AnyOfTwoTypesAnyOfOption1.ts +++ b/test/runtime/typescript/src/payload-types/payloads/AnyOfTwoTypesAnyOfOption1.ts @@ -1,13 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface AnyOfTwoTypesAnyOfOption1Interface { + id?: number + additionalProperties?: Record +} class AnyOfTwoTypesAnyOfOption1 { private _id?: number; private _additionalProperties?: Record; - constructor(input: { - id?: number, - additionalProperties?: Record, - }) { + constructor(input: AnyOfTwoTypesAnyOfOption1Interface) { this._id = input.id; this._additionalProperties = input.additionalProperties; } @@ -71,4 +72,4 @@ class AnyOfTwoTypesAnyOfOption1 { } } -export { AnyOfTwoTypesAnyOfOption1 }; \ No newline at end of file +export { AnyOfTwoTypesAnyOfOption1, AnyOfTwoTypesAnyOfOption1Interface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/CatPayload.ts b/test/runtime/typescript/src/payload-types/payloads/CatPayload.ts index 12d702d5..9bccc3d6 100644 --- a/test/runtime/typescript/src/payload-types/payloads/CatPayload.ts +++ b/test/runtime/typescript/src/payload-types/payloads/CatPayload.ts @@ -1,16 +1,17 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface CatPayloadInterface { + breed?: string + meowPitch?: string + additionalProperties?: Record +} class CatPayload { private _petType: 'cat' = 'cat'; private _breed?: string; private _meowPitch?: string; private _additionalProperties?: Record; - constructor(input: { - breed?: string, - meowPitch?: string, - additionalProperties?: Record, - }) { + constructor(input: CatPayloadInterface) { this._breed = input.breed; this._meowPitch = input.meowPitch; this._additionalProperties = input.additionalProperties; @@ -89,4 +90,4 @@ class CatPayload { } } -export { CatPayload }; \ No newline at end of file +export { CatPayload, CatPayloadInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObject.ts b/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObject.ts index 7d459355..9df24f90 100644 --- a/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObject.ts +++ b/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObject.ts @@ -1,6 +1,10 @@ import {DeeplyNestedObjectLevel1} from './DeeplyNestedObjectLevel1'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface DeeplyNestedObjectInterface { + level1?: DeeplyNestedObjectLevel1 + additionalProperties?: Record +} /** * Object with 3 levels of nesting */ @@ -8,10 +12,7 @@ class DeeplyNestedObject { private _level1?: DeeplyNestedObjectLevel1; private _additionalProperties?: Record; - constructor(input: { - level1?: DeeplyNestedObjectLevel1, - additionalProperties?: Record, - }) { + constructor(input: DeeplyNestedObjectInterface) { this._level1 = input.level1; this._additionalProperties = input.additionalProperties; } @@ -75,4 +76,4 @@ class DeeplyNestedObject { } } -export { DeeplyNestedObject }; \ No newline at end of file +export { DeeplyNestedObject, DeeplyNestedObjectInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1.ts b/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1.ts index a5708201..25195b8d 100644 --- a/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1.ts +++ b/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1.ts @@ -1,14 +1,15 @@ import {DeeplyNestedObjectLevel1Level2} from './DeeplyNestedObjectLevel1Level2'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface DeeplyNestedObjectLevel1Interface { + level2?: DeeplyNestedObjectLevel1Level2 + additionalProperties?: Record +} class DeeplyNestedObjectLevel1 { private _level2?: DeeplyNestedObjectLevel1Level2; private _additionalProperties?: Record; - constructor(input: { - level2?: DeeplyNestedObjectLevel1Level2, - additionalProperties?: Record, - }) { + constructor(input: DeeplyNestedObjectLevel1Interface) { this._level2 = input.level2; this._additionalProperties = input.additionalProperties; } @@ -72,4 +73,4 @@ class DeeplyNestedObjectLevel1 { } } -export { DeeplyNestedObjectLevel1 }; \ No newline at end of file +export { DeeplyNestedObjectLevel1, DeeplyNestedObjectLevel1Interface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1Level2.ts b/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1Level2.ts index 9c0ac0a2..41f32a6d 100644 --- a/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1Level2.ts +++ b/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1Level2.ts @@ -1,14 +1,15 @@ import {DeeplyNestedObjectLevel1Level2Level3} from './DeeplyNestedObjectLevel1Level2Level3'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface DeeplyNestedObjectLevel1Level2Interface { + level3?: DeeplyNestedObjectLevel1Level2Level3 + additionalProperties?: Record +} class DeeplyNestedObjectLevel1Level2 { private _level3?: DeeplyNestedObjectLevel1Level2Level3; private _additionalProperties?: Record; - constructor(input: { - level3?: DeeplyNestedObjectLevel1Level2Level3, - additionalProperties?: Record, - }) { + constructor(input: DeeplyNestedObjectLevel1Level2Interface) { this._level3 = input.level3; this._additionalProperties = input.additionalProperties; } @@ -72,4 +73,4 @@ class DeeplyNestedObjectLevel1Level2 { } } -export { DeeplyNestedObjectLevel1Level2 }; \ No newline at end of file +export { DeeplyNestedObjectLevel1Level2, DeeplyNestedObjectLevel1Level2Interface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1Level2Level3.ts b/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1Level2Level3.ts index 83d145fd..9c4bc0a6 100644 --- a/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1Level2Level3.ts +++ b/test/runtime/typescript/src/payload-types/payloads/DeeplyNestedObjectLevel1Level2Level3.ts @@ -1,13 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface DeeplyNestedObjectLevel1Level2Level3Interface { + value?: string + additionalProperties?: Record +} class DeeplyNestedObjectLevel1Level2Level3 { private _value?: string; private _additionalProperties?: Record; - constructor(input: { - value?: string, - additionalProperties?: Record, - }) { + constructor(input: DeeplyNestedObjectLevel1Level2Level3Interface) { this._value = input.value; this._additionalProperties = input.additionalProperties; } @@ -71,4 +72,4 @@ class DeeplyNestedObjectLevel1Level2Level3 { } } -export { DeeplyNestedObjectLevel1Level2Level3 }; \ No newline at end of file +export { DeeplyNestedObjectLevel1Level2Level3, DeeplyNestedObjectLevel1Level2Level3Interface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/DogPayload.ts b/test/runtime/typescript/src/payload-types/payloads/DogPayload.ts index 7fbf20f4..692786b0 100644 --- a/test/runtime/typescript/src/payload-types/payloads/DogPayload.ts +++ b/test/runtime/typescript/src/payload-types/payloads/DogPayload.ts @@ -1,16 +1,17 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface DogPayloadInterface { + breed?: string + barkVolume?: number + additionalProperties?: Record +} class DogPayload { private _petType: 'dog' = 'dog'; private _breed?: string; private _barkVolume?: number; private _additionalProperties?: Record; - constructor(input: { - breed?: string, - barkVolume?: number, - additionalProperties?: Record, - }) { + constructor(input: DogPayloadInterface) { this._breed = input.breed; this._barkVolume = input.barkVolume; this._additionalProperties = input.additionalProperties; @@ -89,4 +90,4 @@ class DogPayload { } } -export { DogPayload }; \ No newline at end of file +export { DogPayload, DogPayloadInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/NestedObject.ts b/test/runtime/typescript/src/payload-types/payloads/NestedObject.ts index 32789ae3..ba094a23 100644 --- a/test/runtime/typescript/src/payload-types/payloads/NestedObject.ts +++ b/test/runtime/typescript/src/payload-types/payloads/NestedObject.ts @@ -1,6 +1,10 @@ import {NestedObjectOuter} from './NestedObjectOuter'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface NestedObjectInterface { + outer?: NestedObjectOuter + additionalProperties?: Record +} /** * Object with one level of nesting */ @@ -8,10 +12,7 @@ class NestedObject { private _outer?: NestedObjectOuter; private _additionalProperties?: Record; - constructor(input: { - outer?: NestedObjectOuter, - additionalProperties?: Record, - }) { + constructor(input: NestedObjectInterface) { this._outer = input.outer; this._additionalProperties = input.additionalProperties; } @@ -75,4 +76,4 @@ class NestedObject { } } -export { NestedObject }; \ No newline at end of file +export { NestedObject, NestedObjectInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/NestedObjectOuter.ts b/test/runtime/typescript/src/payload-types/payloads/NestedObjectOuter.ts index 165e1ca2..6c4ef10d 100644 --- a/test/runtime/typescript/src/payload-types/payloads/NestedObjectOuter.ts +++ b/test/runtime/typescript/src/payload-types/payloads/NestedObjectOuter.ts @@ -1,13 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface NestedObjectOuterInterface { + inner?: string + additionalProperties?: Record +} class NestedObjectOuter { private _inner?: string; private _additionalProperties?: Record; - constructor(input: { - inner?: string, - additionalProperties?: Record, - }) { + constructor(input: NestedObjectOuterInterface) { this._inner = input.inner; this._additionalProperties = input.additionalProperties; } @@ -71,4 +72,4 @@ class NestedObjectOuter { } } -export { NestedObjectOuter }; \ No newline at end of file +export { NestedObjectOuter, NestedObjectOuterInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsFalse.ts b/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsFalse.ts index 1c68e5d9..0e594e01 100644 --- a/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsFalse.ts +++ b/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsFalse.ts @@ -1,14 +1,15 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface ObjectAdditionalPropsFalseInterface { + known?: string +} /** * Object that disallows additional properties */ class ObjectAdditionalPropsFalse { private _known?: string; - constructor(input: { - known?: string, - }) { + constructor(input: ObjectAdditionalPropsFalseInterface) { this._known = input.known; } @@ -58,4 +59,4 @@ class ObjectAdditionalPropsFalse { } } -export { ObjectAdditionalPropsFalse }; \ No newline at end of file +export { ObjectAdditionalPropsFalse, ObjectAdditionalPropsFalseInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsSchema.ts b/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsSchema.ts index 83b22586..600f736e 100644 --- a/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsSchema.ts +++ b/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsSchema.ts @@ -1,5 +1,9 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface ObjectAdditionalPropsSchemaInterface { + known?: string + additionalProperties?: Record +} /** * Object with typed additional properties (integers) */ @@ -7,10 +11,7 @@ class ObjectAdditionalPropsSchema { private _known?: string; private _additionalProperties?: Record; - constructor(input: { - known?: string, - additionalProperties?: Record, - }) { + constructor(input: ObjectAdditionalPropsSchemaInterface) { this._known = input.known; this._additionalProperties = input.additionalProperties; } @@ -74,4 +75,4 @@ class ObjectAdditionalPropsSchema { } } -export { ObjectAdditionalPropsSchema }; \ No newline at end of file +export { ObjectAdditionalPropsSchema, ObjectAdditionalPropsSchemaInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsTrue.ts b/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsTrue.ts index 8dd61be1..aec4f2f3 100644 --- a/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsTrue.ts +++ b/test/runtime/typescript/src/payload-types/payloads/ObjectAdditionalPropsTrue.ts @@ -1,5 +1,9 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface ObjectAdditionalPropsTrueInterface { + known?: string + additionalProperties?: Record +} /** * Object that allows additional properties */ @@ -7,10 +11,7 @@ class ObjectAdditionalPropsTrue { private _known?: string; private _additionalProperties?: Record; - constructor(input: { - known?: string, - additionalProperties?: Record, - }) { + constructor(input: ObjectAdditionalPropsTrueInterface) { this._known = input.known; this._additionalProperties = input.additionalProperties; } @@ -74,4 +75,4 @@ class ObjectAdditionalPropsTrue { } } -export { ObjectAdditionalPropsTrue }; \ No newline at end of file +export { ObjectAdditionalPropsTrue, ObjectAdditionalPropsTrueInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/ObjectArrayItem.ts b/test/runtime/typescript/src/payload-types/payloads/ObjectArrayItem.ts index bfe2a35b..e907edb8 100644 --- a/test/runtime/typescript/src/payload-types/payloads/ObjectArrayItem.ts +++ b/test/runtime/typescript/src/payload-types/payloads/ObjectArrayItem.ts @@ -1,15 +1,16 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface ObjectArrayItemInterface { + id?: number + name?: string + additionalProperties?: Record +} class ObjectArrayItem { private _id?: number; private _name?: string; private _additionalProperties?: Record; - constructor(input: { - id?: number, - name?: string, - additionalProperties?: Record, - }) { + constructor(input: ObjectArrayItemInterface) { this._id = input.id; this._name = input.name; this._additionalProperties = input.additionalProperties; @@ -83,4 +84,4 @@ class ObjectArrayItem { } } -export { ObjectArrayItem }; \ No newline at end of file +export { ObjectArrayItem, ObjectArrayItemInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/ObjectWithDefaults.ts b/test/runtime/typescript/src/payload-types/payloads/ObjectWithDefaults.ts index 71757323..0e1d3960 100644 --- a/test/runtime/typescript/src/payload-types/payloads/ObjectWithDefaults.ts +++ b/test/runtime/typescript/src/payload-types/payloads/ObjectWithDefaults.ts @@ -1,5 +1,11 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface ObjectWithDefaultsInterface { + status?: string + count?: number + enabled?: boolean + additionalProperties?: Record +} /** * Object with default values */ @@ -9,12 +15,7 @@ class ObjectWithDefaults { private _enabled?: boolean; private _additionalProperties?: Record; - constructor(input: { - status?: string, - count?: number, - enabled?: boolean, - additionalProperties?: Record, - }) { + constructor(input: ObjectWithDefaultsInterface) { this._status = input.status; this._count = input.count; this._enabled = input.enabled; @@ -98,4 +99,4 @@ class ObjectWithDefaults { } } -export { ObjectWithDefaults }; \ No newline at end of file +export { ObjectWithDefaults, ObjectWithDefaultsInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/ObjectWithFormats.ts b/test/runtime/typescript/src/payload-types/payloads/ObjectWithFormats.ts index 718406c1..b93bafd9 100644 --- a/test/runtime/typescript/src/payload-types/payloads/ObjectWithFormats.ts +++ b/test/runtime/typescript/src/payload-types/payloads/ObjectWithFormats.ts @@ -1,5 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface ObjectWithFormatsInterface { + email?: string + website?: string + userId?: string + birthDate?: Date + lastLogin?: Date + serverIp?: string + additionalProperties?: Record +} /** * Object with multiple format-validated properties */ @@ -12,15 +21,7 @@ class ObjectWithFormats { private _serverIp?: string; private _additionalProperties?: Record; - constructor(input: { - email?: string, - website?: string, - userId?: string, - birthDate?: Date, - lastLogin?: Date, - serverIp?: string, - additionalProperties?: Record, - }) { + constructor(input: ObjectWithFormatsInterface) { this._email = input.email; this._website = input.website; this._userId = input.userId; @@ -134,4 +135,4 @@ class ObjectWithFormats { } } -export { ObjectWithFormats }; \ No newline at end of file +export { ObjectWithFormats, ObjectWithFormatsInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/ObjectWithOptional.ts b/test/runtime/typescript/src/payload-types/payloads/ObjectWithOptional.ts index 70cdf901..4710bbb7 100644 --- a/test/runtime/typescript/src/payload-types/payloads/ObjectWithOptional.ts +++ b/test/runtime/typescript/src/payload-types/payloads/ObjectWithOptional.ts @@ -1,5 +1,11 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface ObjectWithOptionalInterface { + field1?: string + field2?: string + field3?: string + additionalProperties?: Record +} /** * Object with all optional properties */ @@ -9,12 +15,7 @@ class ObjectWithOptional { private _field3?: string; private _additionalProperties?: Record; - constructor(input: { - field1?: string, - field2?: string, - field3?: string, - additionalProperties?: Record, - }) { + constructor(input: ObjectWithOptionalInterface) { this._field1 = input.field1; this._field2 = input.field2; this._field3 = input.field3; @@ -98,4 +99,4 @@ class ObjectWithOptional { } } -export { ObjectWithOptional }; \ No newline at end of file +export { ObjectWithOptional, ObjectWithOptionalInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/ObjectWithRequired.ts b/test/runtime/typescript/src/payload-types/payloads/ObjectWithRequired.ts index 8699bacf..482a7791 100644 --- a/test/runtime/typescript/src/payload-types/payloads/ObjectWithRequired.ts +++ b/test/runtime/typescript/src/payload-types/payloads/ObjectWithRequired.ts @@ -1,5 +1,11 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface ObjectWithRequiredInterface { + id: string + name: string + optionalField?: string + additionalProperties?: Record +} /** * Object with required and optional properties */ @@ -9,12 +15,7 @@ class ObjectWithRequired { private _optionalField?: string; private _additionalProperties?: Record; - constructor(input: { - id: string, - name: string, - optionalField?: string, - additionalProperties?: Record, - }) { + constructor(input: ObjectWithRequiredInterface) { this._id = input.id; this._name = input.name; this._optionalField = input.optionalField; @@ -98,4 +99,4 @@ class ObjectWithRequired { } } -export { ObjectWithRequired }; \ No newline at end of file +export { ObjectWithRequired, ObjectWithRequiredInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/OneOfThreeTypesOneOfOption2.ts b/test/runtime/typescript/src/payload-types/payloads/OneOfThreeTypesOneOfOption2.ts index bc301c24..a335c359 100644 --- a/test/runtime/typescript/src/payload-types/payloads/OneOfThreeTypesOneOfOption2.ts +++ b/test/runtime/typescript/src/payload-types/payloads/OneOfThreeTypesOneOfOption2.ts @@ -1,13 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface OneOfThreeTypesOneOfOption2Interface { + value?: string + additionalProperties?: Record +} class OneOfThreeTypesOneOfOption2 { private _value?: string; private _additionalProperties?: Record; - constructor(input: { - value?: string, - additionalProperties?: Record, - }) { + constructor(input: OneOfThreeTypesOneOfOption2Interface) { this._value = input.value; this._additionalProperties = input.additionalProperties; } @@ -71,4 +72,4 @@ class OneOfThreeTypesOneOfOption2 { } } -export { OneOfThreeTypesOneOfOption2 }; \ No newline at end of file +export { OneOfThreeTypesOneOfOption2, OneOfThreeTypesOneOfOption2Interface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payload-types/payloads/SimpleObject.ts b/test/runtime/typescript/src/payload-types/payloads/SimpleObject.ts index e21b310a..2d9cf200 100644 --- a/test/runtime/typescript/src/payload-types/payloads/SimpleObject.ts +++ b/test/runtime/typescript/src/payload-types/payloads/SimpleObject.ts @@ -1,5 +1,10 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface SimpleObjectInterface { + name?: string + age?: number + additionalProperties?: Record +} /** * Simple object with two properties */ @@ -8,11 +13,7 @@ class SimpleObject { private _age?: number; private _additionalProperties?: Record; - constructor(input: { - name?: string, - age?: number, - additionalProperties?: Record, - }) { + constructor(input: SimpleObjectInterface) { this._name = input.name; this._age = input.age; this._additionalProperties = input.additionalProperties; @@ -86,4 +87,4 @@ class SimpleObject { } } -export { SimpleObject }; \ No newline at end of file +export { SimpleObject, SimpleObjectInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payloads/LegacyNotification.ts b/test/runtime/typescript/src/payloads/LegacyNotification.ts index 7a50dcf7..7d17d489 100644 --- a/test/runtime/typescript/src/payloads/LegacyNotification.ts +++ b/test/runtime/typescript/src/payloads/LegacyNotification.ts @@ -1,6 +1,11 @@ import {LegacyNotificationPayloadLevelEnum} from './LegacyNotificationPayloadLevelEnum'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface LegacyNotificationInterface { + message?: string + level?: LegacyNotificationPayloadLevelEnum + additionalProperties?: Record +} /** * Legacy notification payload - use NewNotificationPayload instead */ @@ -9,11 +14,7 @@ class LegacyNotification { private _level?: LegacyNotificationPayloadLevelEnum; private _additionalProperties?: Record; - constructor(input: { - message?: string, - level?: LegacyNotificationPayloadLevelEnum, - additionalProperties?: Record, - }) { + constructor(input: LegacyNotificationInterface) { this._message = input.message; this._level = input.level; this._additionalProperties = input.additionalProperties; @@ -93,4 +94,4 @@ class LegacyNotification { } } -export { LegacyNotification }; \ No newline at end of file +export { LegacyNotification, LegacyNotificationInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payloads/UnionPayloadOneOfOption2.ts b/test/runtime/typescript/src/payloads/UnionPayloadOneOfOption2.ts index d166bc77..62c50aa0 100644 --- a/test/runtime/typescript/src/payloads/UnionPayloadOneOfOption2.ts +++ b/test/runtime/typescript/src/payloads/UnionPayloadOneOfOption2.ts @@ -1,13 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface UnionPayloadOneOfOption2Interface { + name?: string + additionalProperties?: Record +} class UnionPayloadOneOfOption2 { private _name?: string; private _additionalProperties?: Record; - constructor(input: { - name?: string, - additionalProperties?: Record, - }) { + constructor(input: UnionPayloadOneOfOption2Interface) { this._name = input.name; this._additionalProperties = input.additionalProperties; } @@ -71,4 +72,4 @@ class UnionPayloadOneOfOption2 { } } -export { UnionPayloadOneOfOption2 }; \ No newline at end of file +export { UnionPayloadOneOfOption2, UnionPayloadOneOfOption2Interface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/payloads/UserSignedUp.ts b/test/runtime/typescript/src/payloads/UserSignedUp.ts index 13f2c67a..881b054e 100644 --- a/test/runtime/typescript/src/payloads/UserSignedUp.ts +++ b/test/runtime/typescript/src/payloads/UserSignedUp.ts @@ -1,5 +1,10 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface UserSignedUpInterface { + displayName?: string + email?: string + additionalProperties?: Record +} /** * Payload for user signup events containing user registration details */ @@ -8,11 +13,7 @@ class UserSignedUp { private _email?: string; private _additionalProperties?: Record; - constructor(input: { - displayName?: string, - email?: string, - additionalProperties?: Record, - }) { + constructor(input: UserSignedUpInterface) { this._displayName = input.displayName; this._email = input.email; this._additionalProperties = input.additionalProperties; @@ -92,4 +93,4 @@ class UserSignedUp { } } -export { UserSignedUp }; \ No newline at end of file +export { UserSignedUp, UserSignedUpInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/channels/http_client.ts b/test/runtime/typescript/src/request-reply/channels/http_client.ts index 3380d116..17b02755 100644 --- a/test/runtime/typescript/src/request-reply/channels/http_client.ts +++ b/test/runtime/typescript/src/request-reply/channels/http_client.ts @@ -1,13 +1,13 @@ -import {Pong} from './../payloads/Pong'; -import {Ping} from './../payloads/Ping'; +import {Pong, PongInterface} from './../payloads/Pong'; +import {Ping, PingInterface} from './../payloads/Ping'; import * as MultiStatusResponseReplyPayloadModule from './../payloads/MultiStatusResponseReplyPayload'; import * as GetUserItemReplyPayloadModule from './../payloads/GetUserItemReplyPayload'; import * as UpdateUserItemReplyPayloadModule from './../payloads/UpdateUserItemReplyPayload'; -import {ItemRequest} from './../payloads/ItemRequest'; +import {ItemRequest, ItemRequestInterface} from './../payloads/ItemRequest'; import * as PingPayloadModule from './../payloads/PingPayload'; import * as UserItemsPayloadModule from './../payloads/UserItemsPayload'; -import {NotFound} from './../payloads/NotFound'; -import {ItemResponse} from './../payloads/ItemResponse'; +import {NotFound, NotFoundInterface} from './../payloads/NotFound'; +import {ItemResponse, ItemResponseInterface} from './../payloads/ItemResponse'; import {UserItemsParameters, UserItemsParametersInterface} from './../parameters/UserItemsParameters'; import {ItemRequestHeaders} from './../headers/ItemRequestHeaders'; @@ -646,7 +646,7 @@ async function handleTokenRefresh( // ============================================================================ export interface PostPingPostRequestContext extends HttpClientContext { - payload: Ping; + payload: PingInterface | Ping; } /** @@ -677,7 +677,8 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise url = authResult.url; // Prepare body - const body = context.payload?.marshal(); + const payload = context.payload instanceof Ping ? context.payload : new Ping(context.payload); + const body = payload?.marshal(); // Determine request function const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; @@ -864,7 +865,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis } export interface PutPingPutRequestContext extends HttpClientContext { - payload: Ping; + payload: PingInterface | Ping; } /** @@ -895,7 +896,8 @@ async function putPingPutRequest(context: PutPingPutRequestContext): Promise, options?: Nats.RequestOptions, @@ -37,7 +37,7 @@ function requestToRegularRequest({ return new Promise(async (resolve, reject) => { try { const validator = Pong.createValidator(); - let dataToSend: any = requestMessage.marshal(); + let dataToSend: any = (requestMessage instanceof Ping ? requestMessage : new Ping(requestMessage)).marshal(); dataToSend = codec.encode(dataToSend); const msg = await nc.request('ping', dataToSend, options); diff --git a/test/runtime/typescript/src/request-reply/client/NatsClient.ts b/test/runtime/typescript/src/request-reply/client/NatsClient.ts index 97f9ea3c..dd11eeaf 100644 --- a/test/runtime/typescript/src/request-reply/client/NatsClient.ts +++ b/test/runtime/typescript/src/request-reply/client/NatsClient.ts @@ -1,13 +1,13 @@ -import {Pong} from './../payloads/Pong'; -import {Ping} from './../payloads/Ping'; +import {Pong, PongInterface} from './../payloads/Pong'; +import {Ping, PingInterface} from './../payloads/Ping'; import * as MultiStatusResponseReplyPayloadModule from './../payloads/MultiStatusResponseReplyPayload'; import * as GetUserItemReplyPayloadModule from './../payloads/GetUserItemReplyPayload'; import * as UpdateUserItemReplyPayloadModule from './../payloads/UpdateUserItemReplyPayload'; -import {ItemRequest} from './../payloads/ItemRequest'; +import {ItemRequest, ItemRequestInterface} from './../payloads/ItemRequest'; import * as PingPayloadModule from './../payloads/PingPayload'; import * as UserItemsPayloadModule from './../payloads/UserItemsPayload'; -import {NotFound} from './../payloads/NotFound'; -import {ItemResponse} from './../payloads/ItemResponse'; +import {NotFound, NotFoundInterface} from './../payloads/NotFound'; +import {ItemResponse, ItemResponseInterface} from './../payloads/ItemResponse'; export {Pong}; export {Ping}; export {MultiStatusResponseReplyPayloadModule}; diff --git a/test/runtime/typescript/src/request-reply/payloads/ItemRequest.ts b/test/runtime/typescript/src/request-reply/payloads/ItemRequest.ts index c09a8456..277d7c95 100644 --- a/test/runtime/typescript/src/request-reply/payloads/ItemRequest.ts +++ b/test/runtime/typescript/src/request-reply/payloads/ItemRequest.ts @@ -1,17 +1,18 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface ItemRequestInterface { + name: string + description?: string + quantity?: number + additionalProperties?: Record +} class ItemRequest { private _name: string; private _description?: string; private _quantity?: number; private _additionalProperties?: Record; - constructor(input: { - name: string, - description?: string, - quantity?: number, - additionalProperties?: Record, - }) { + constructor(input: ItemRequestInterface) { this._name = input.name; this._description = input.description; this._quantity = input.quantity; @@ -104,4 +105,4 @@ class ItemRequest { } } -export { ItemRequest }; \ No newline at end of file +export { ItemRequest, ItemRequestInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/payloads/ItemResponse.ts b/test/runtime/typescript/src/request-reply/payloads/ItemResponse.ts index b4bbd56f..b5f046c5 100644 --- a/test/runtime/typescript/src/request-reply/payloads/ItemResponse.ts +++ b/test/runtime/typescript/src/request-reply/payloads/ItemResponse.ts @@ -1,5 +1,13 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface ItemResponseInterface { + id?: string + userId?: string + name?: string + description?: string + quantity?: number + additionalProperties?: Record +} class ItemResponse { private _id?: string; private _userId?: string; @@ -8,14 +16,7 @@ class ItemResponse { private _quantity?: number; private _additionalProperties?: Record; - constructor(input: { - id?: string, - userId?: string, - name?: string, - description?: string, - quantity?: number, - additionalProperties?: Record, - }) { + constructor(input: ItemResponseInterface) { this._id = input.id; this._userId = input.userId; this._name = input.name; @@ -134,4 +135,4 @@ class ItemResponse { } } -export { ItemResponse }; \ No newline at end of file +export { ItemResponse, ItemResponseInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/payloads/NotFound.ts b/test/runtime/typescript/src/request-reply/payloads/NotFound.ts index c41f7837..9b115bba 100644 --- a/test/runtime/typescript/src/request-reply/payloads/NotFound.ts +++ b/test/runtime/typescript/src/request-reply/payloads/NotFound.ts @@ -1,15 +1,16 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface NotFoundInterface { + error?: string + code?: string + additionalProperties?: Record +} class NotFound { private _error?: string; private _code?: string; private _additionalProperties?: Record; - constructor(input: { - error?: string, - code?: string, - additionalProperties?: Record, - }) { + constructor(input: NotFoundInterface) { this._error = input.error; this._code = input.code; this._additionalProperties = input.additionalProperties; @@ -89,4 +90,4 @@ class NotFound { } } -export { NotFound }; \ No newline at end of file +export { NotFound, NotFoundInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/payloads/Ping.ts b/test/runtime/typescript/src/request-reply/payloads/Ping.ts index ff12aea3..85a9b7d7 100644 --- a/test/runtime/typescript/src/request-reply/payloads/Ping.ts +++ b/test/runtime/typescript/src/request-reply/payloads/Ping.ts @@ -1,13 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PingInterface { + ping?: string + additionalProperties?: Record +} class Ping { private _ping?: string; private _additionalProperties?: Record; - constructor(input: { - ping?: string, - additionalProperties?: Record, - }) { + constructor(input: PingInterface) { this._ping = input.ping; this._additionalProperties = input.additionalProperties; } @@ -74,4 +75,4 @@ class Ping { } } -export { Ping }; \ No newline at end of file +export { Ping, PingInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/payloads/Pong.ts b/test/runtime/typescript/src/request-reply/payloads/Pong.ts index 039f3c31..b0c28215 100644 --- a/test/runtime/typescript/src/request-reply/payloads/Pong.ts +++ b/test/runtime/typescript/src/request-reply/payloads/Pong.ts @@ -1,13 +1,14 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PongInterface { + pong?: string + additionalProperties?: Record +} class Pong { private _pong?: string; private _additionalProperties?: Record; - constructor(input: { - pong?: string, - additionalProperties?: Record, - }) { + constructor(input: PongInterface) { this._pong = input.pong; this._additionalProperties = input.additionalProperties; } @@ -74,4 +75,4 @@ class Pong { } } -export { Pong }; \ No newline at end of file +export { Pong, PongInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/test/payloads.spec.ts b/test/runtime/typescript/test/payloads.spec.ts index 7f253ea4..b1180bd4 100644 --- a/test/runtime/typescript/test/payloads.spec.ts +++ b/test/runtime/typescript/test/payloads.spec.ts @@ -1,4 +1,4 @@ -import { UserSignedUp } from '../src/payloads/UserSignedUp'; +import { UserSignedUp, UserSignedUpInterface } from '../src/payloads/UserSignedUp'; import * as StringMessage from '../src/payloads/StringMessage'; import * as ArrayMessage from '../src/payloads/ArrayMessage'; import * as UnionMessage from '../src/payloads/UnionMessage'; @@ -23,6 +23,34 @@ describe('payloads', () => { }); }); + describe('companion interface parity', () => { + // A plain object literal typed as the companion interface must be an + // exact substitute for constructing the class inline — the interface is + // the ctor input type, so both construction paths marshal identically. + // (Deliberately excludes `additionalProperties`: marshal() calls + // `.entries()` on it, an upstream Modelina Map-vs-Record quirk the plan + // explicitly leaves untouched.) + const asInterface: UserSignedUpInterface = { + displayName: 'displayNameTest', + email: 'emailTest' + }; + + test('plain object typed as the interface marshals identically to a class instance', () => { + const fromInterface = new UserSignedUp(asInterface); + const fromInline = new UserSignedUp({ + displayName: 'displayNameTest', + email: 'emailTest' + }); + expect(fromInterface.marshal()).toEqual(fromInline.marshal()); + }); + + test('interface-constructed payload round-trips through unmarshal', () => { + const serialized = new UserSignedUp(asInterface).marshal(); + const roundTripped = UserSignedUp.unmarshal(serialized); + expect(roundTripped.marshal()).toEqual(serialized); + }); + }); + describe('validate function', () => { test('should validate correct payload', () => { const result = UserSignedUp.validate({