Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/generators/payloads.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<Name>`) and a plain-data companion interface (`<Name>Interface`)
declared above it. The class constructor takes the interface
(`constructor(input: <Name>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
`<Name>Interface | <Name>` 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.

Expand Down
4 changes: 4 additions & 0 deletions examples/ecommerce-asyncapi-payload/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
`<Name>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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, any>
}
class Address {
private _street: string;
private _city: string;
Expand All @@ -8,14 +16,7 @@ class Address {
private _postalCode: string;
private _additionalProperties?: Record<string, any>;

constructor(input: {
street: string,
city: string,
state?: string,
country: string,
postalCode: string,
additionalProperties?: Record<string, any>,
}) {
constructor(input: AddressInterface) {
this._street = input.street;
this._city = input.city;
this._state = input.state;
Expand All @@ -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; }

Expand Down Expand Up @@ -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 {
Expand All @@ -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 };
export { Address, AddressInterface };
Original file line number Diff line number Diff line change
@@ -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<string, any>
}
class Attachment {
private _filename?: string;
private _contentType?: string;
private _data?: string;
private _additionalProperties?: Record<string, any>;

constructor(input: {
filename?: string,
contentType?: string,
data?: string,
additionalProperties?: Record<string, any>,
}) {
constructor(input: AttachmentInterface) {
this._filename = input.filename;
this._contentType = input.contentType;
this._data = input.data;
Expand Down Expand Up @@ -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 {
Expand All @@ -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 };
export { Attachment, AttachmentInterface };
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

/**
* Currency code
*/
enum Currency {
USD = "USD",
EUR = "EUR",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, any>
}
class EmailNotification {
private _type: 'email' = 'email';
private _recipientId: string;
Expand All @@ -9,13 +16,7 @@ class EmailNotification {
private _attachments?: Attachment[];
private _additionalProperties?: Record<string, any>;

constructor(input: {
recipientId: string,
subject: string,
body: string,
attachments?: Attachment[],
additionalProperties?: Record<string, any>,
}) {
constructor(input: EmailNotificationInterface) {
this._recipientId = input.recipientId;
this._subject = input.subject;
this._body = input.body;
Expand Down Expand Up @@ -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(',')}],`;
}
Expand Down Expand Up @@ -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));
}

Expand All @@ -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 {
Expand All @@ -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 };
export { EmailNotification, EmailNotificationInterface };
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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 };
Loading
Loading