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
62 changes: 62 additions & 0 deletions docs/inputs/asyncapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,68 @@ no `filter`, output is unchanged. See the
[filtering section of the configurations guide](../configurations.md#filtering-channels-operations--paths)
for full semantics.

## Recursive and self-referencing schemas

A component schema may reference itself, directly or through another component.
Both forms generate models that reference each other the same way the document
does.

```yaml
components:
schemas:
Node:
type: object
required: [label]
properties:
label:
type: string
children:
type: array
items:
$ref: '#/components/schemas/Node'
```

Used as a message payload, `Node` produces one model whose `children` are more
of the same model:

```typescript
interface NodeMessageInterface {
label: string
children?: NodeMessage[]
additionalProperties?: Record<string, any>
}
```

Mutual recursion works the same way — `GraphNode.edge → GraphEdge` and
`GraphEdge.target → GraphNode` produce two models that reference each other:

```typescript
interface GraphNodeMessageInterface {
name: string
edge?: GraphEdge
additionalProperties?: Record<string, any>
}

interface GraphEdgeInterface {
weight: number
target?: GraphNodeMessage
additionalProperties?: Record<string, any>
}
```

Nested values round-trip to their own type, so a tree survives `marshal()` and
`unmarshal()` with its structure intact:

```typescript
const tree = new NodeMessage({
label: 'root',
children: [new NodeMessage({label: 'leaf'})]
});

const roundTripped = NodeMessage.unmarshal(tree.marshal());
roundTripped.children?.[0].label; // 'leaf'
```

## Basic AsyncAPI Document Structure

Here's a complete basic AsyncAPI document example to get you started:
Expand Down
58 changes: 42 additions & 16 deletions src/codegen/inputs/asyncapi/generators/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import {pascalCase} from '../../../generators/typescript/utils';
import {findNameFromChannel} from '../../../utils';
import {Logger} from '../../../../LoggingInterface';
import {AsyncAPIInputProcessor} from '@asyncapi/modelina';
import {
inlinedRootTarget,
inlinedUnionTargets,
resolveAsyncapiComponentRefs
} from '../refs';

/**
* Convert a single message's headers into an internal JSON Schema. Mirrors the
Expand Down Expand Up @@ -72,10 +77,15 @@ function collectReplyOnlyMessageIds(
* channel-scoped `oneOf` union when two or more do. Header-less messages that
* sit alongside header-bearing ones are warned about.
*/
function buildChannelHeaderEntry(
channel: ChannelInterface,
replyOnlyMessageIds: Set<string>
): {schema: any; schemaId: string} | undefined {
function buildChannelHeaderEntry({
channel,
replyOnlyMessageIds,
asyncapiDocument
}: {
channel: ChannelInterface;
replyOnlyMessageIds: Set<string>;
asyncapiDocument: AsyncAPIDocumentInterface;
}): {schema: any; schemaId: string} | undefined {
// Exclude reply-only messages — the channel headers model the request side.
const messages = channel
.messages()
Expand Down Expand Up @@ -103,24 +113,39 @@ function buildChannelHeaderEntry(
if (headerBearingMessages.length === 1) {
// Exactly one header-bearing message → unchanged single-message output.
const message = headerBearingMessages[0];
const schema = convertMessageHeaders(message);
return {
schema: convertMessageHeaders(message),
// The headers schema itself is the fragment root, so a pointer back to it
// resolves as `#`.
schema: resolveAsyncapiComponentRefs({
fragment: schema,
asyncapiDocument,
inlinedTargets: inlinedRootTarget(schema)
}),
schemaId: pascalCase(`${message.id()}_headers`)
};
}

// 2+ header-bearing messages → a oneOf union, mirroring the payloads union
// builder (channel-scoped union id).
const unionId = pascalCase(`${findNameFromChannel(channel)}_Headers`);
const unionSchema = {
type: 'object',
$id: unionId,
$schema: 'http://json-schema.org/draft-07/schema',
oneOf: headerBearingMessages.map((message) =>
convertMessageHeaders(message)
)
};
return {
schema: {
type: 'object',
$id: unionId,
$schema: 'http://json-schema.org/draft-07/schema',
oneOf: headerBearingMessages.map((message) =>
convertMessageHeaders(message)
)
},
// Applied once to the assembled root, not per member: every member is
// inlined at its own `oneOf` slot, and `#/definitions/...` resolves from
// the fragment root.
schema: resolveAsyncapiComponentRefs({
fragment: unionSchema,
asyncapiDocument,
inlinedTargets: inlinedUnionTargets(unionSchema.oneOf)
}),
schemaId: unionId
};
}
Expand All @@ -141,10 +166,11 @@ export function processAsyncAPIHeaders(
const replyOnlyMessageIds = collectReplyOnlyMessageIds(asyncapiDocument);

for (const channel of asyncapiDocument.allChannels().all()) {
channelHeaders[channel.id()] = buildChannelHeaderEntry(
channelHeaders[channel.id()] = buildChannelHeaderEntry({
channel,
replyOnlyMessageIds
);
replyOnlyMessageIds,
asyncapiDocument
});
}

return {channelHeaders};
Expand Down
25 changes: 23 additions & 2 deletions src/codegen/inputs/asyncapi/generators/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
pascalCase
} from '../../../generators/typescript/utils';
import {findNameFromChannel} from '../../../utils';
import {resolveAsyncapiComponentRefs} from '../refs';
import {
ConstrainedEnumModel,
ConstrainedObjectModel,
Expand Down Expand Up @@ -41,13 +42,33 @@ export async function processAsyncAPIParameters(
'x-channel-address': channel.address()
};

// Component name -> the parameter property the component is inlined at.
// Only AsyncAPI v2 Parameter Objects carry a `schema`, so this only ever
// has entries for v2 documents.
const inlinedTargets = new Map<string, string>();
for (const parameter of parameters) {
schemaObj.properties[parameter.id()] = parameter.schema()?.json();
// `any` because the parser's schema type does not model the
// `x-parser-schema-id` extension the rewrite keys on.
const parameterSchema: any = parameter.schema()?.json();
schemaObj.properties[parameter.id()] = parameterSchema;
schemaObj.required.push(parameter.id());
const parserSchemaId = parameterSchema?.['x-parser-schema-id'];
if (typeof parserSchemaId === 'string') {
inlinedTargets.set(parserSchemaId, `#/properties/${parameter.id()}`);
}
}

channelParameters[channel.id()] = {
schema: schemaObj,
// Applied once to the assembled root so `#/definitions/...` resolves.
// Parameter schemas are raw parser JSON rather than
// `convertToInternalSchema` output; a hoisted component is converted
// for consistency with the other extraction sites, which only adds
// Modelina's inferred-name extension on top of the same shape.
schema: resolveAsyncapiComponentRefs({
fragment: schemaObj,
asyncapiDocument,
inlinedTargets
}),
schemaId
};
}
Expand Down
21 changes: 21 additions & 0 deletions src/codegen/inputs/asyncapi/generators/payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import {
onlyUnique
} from '../../../utils';
import {Logger} from '../../../../LoggingInterface';
import {
inlinedRootTarget,
inlinedUnionTargets,
resolveAsyncapiComponentRefs
} from '../refs';

// Interface for processed payload schema data
export interface ProcessedPayloadSchemaData {
Expand Down Expand Up @@ -80,6 +85,15 @@ export async function processAsyncAPIPayloads(
});
}
}
// Every union member is inlined in this fragment, so a surviving pointer
// to one must aim at its `oneOf` slot rather than at a hoisted copy.
// Applied once to the assembled root because `#/definitions/...`
// resolves from the fragment root.
schemaObj = resolveAsyncapiComponentRefs({
fragment: schemaObj,
asyncapiDocument,
inlinedTargets: inlinedUnionTargets(schemaObj.oneOf)
});
} else {
const message = payloadBearingMessages[0];
const schema = AsyncAPIInputProcessor.convertToInternalSchema(
Expand All @@ -98,6 +112,13 @@ export async function processAsyncAPIPayloads(
...(schema as any),
$id: id
};
// The payload schema itself is the fragment root, so a pointer back to
// it resolves as `#`.
schemaObj = resolveAsyncapiComponentRefs({
fragment: schemaObj,
asyncapiDocument,
inlinedTargets: inlinedRootTarget(schemaObj)
});
}
}

Expand Down
Loading
Loading