Skip to content
Merged
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
23 changes: 17 additions & 6 deletions src/processors/AsyncAPIInputProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,13 +445,15 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor {
* Determine the best name for a schema based on available metadata and context.
*
* Priority order:
* 0. User-provided x-modelgen-inferred-name extension (highest priority)
* 1. Component schema key (from components/schemas)
* 2. Schema title field
* 3. Source file name (from custom resolver metadata)
* 4. Message ID (for payloads)
* 5. Context-based inference (property name, array item, enum)
* 6. Inferred name parameter
* 7. Fallback to sanitized anonymous ID
* 2. Schema ID (if looks like component name)
* 3. Schema title field
* 4. Source file name (from custom resolver metadata)
* 5. Message ID (for payloads)
* 6. Context-based inference (property name, array item, enum)
* 7. Inferred name parameter
* 8. Fallback to sanitized anonymous ID
*
* @param schemaId The schema ID from AsyncAPI parser
* @param schemaJson The JSON representation of the schema
Expand All @@ -466,6 +468,15 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor {
context?: SchemaContext,
inferredName?: string
): string {
// Priority 0: User-provided x-modelgen-inferred-name extension (highest priority)
const existingInferredName = schemaJson?.[this.MODELINA_INFERRED_NAME];
if (existingInferredName && typeof existingInferredName === 'string') {
Logger.debug(
`Using user-provided x-modelgen-inferred-name: ${existingInferredName}`
);
return existingInferredName;
}

// Priority 1: Component schema key from context
if (context?.componentKey) {
Logger.debug(`Using component key from context: ${context.componentKey}`);
Expand Down
140 changes: 140 additions & 0 deletions test/processors/AsyncAPIInputProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1639,4 +1639,144 @@ describe('AsyncAPIInputProcessor', () => {
);
});
});

describe('x-modelgen-inferred-name extension', () => {
test('should respect user-provided x-modelgen-inferred-name for inline enum', async () => {
const doc = {
asyncapi: '2.0.0',
info: { title: 'Test', version: '1.0.0' },
channels: {
test: {
subscribe: {
message: {
payload: {
$ref: '#/components/schemas/TestPayload'
}
}
}
}
},
components: {
schemas: {
TestPayload: {
type: 'object',
properties: {
status: {
'x-modelgen-inferred-name': 'Status',
type: 'string',
enum: ['active', 'inactive']
}
}
}
}
}
};
const processor = new AsyncAPIInputProcessor();
const commonInputModel = await processor.process(doc);

// The enum should be named "Status", NOT "TestPayloadStatusEnum"
const schema = AsyncAPIInputProcessor.convertToInternalSchema(
commonInputModel.originalInput.schemas()[0] as any
) as any;
expect(schema.properties.status['x-modelgen-inferred-name']).toEqual(
'Status'
);
});

test('should respect user-provided x-modelgen-inferred-name for inline object', async () => {
const doc = {
asyncapi: '2.0.0',
info: { title: 'Test', version: '1.0.0' },
channels: {
test: {
subscribe: {
message: {
payload: {
$ref: '#/components/schemas/TestPayload'
}
}
}
}
},
components: {
schemas: {
TestPayload: {
type: 'object',
properties: {
nested: {
'x-modelgen-inferred-name': 'CustomNestedObject',
type: 'object',
properties: {
value: { type: 'string' }
}
}
}
}
}
}
};
const processor = new AsyncAPIInputProcessor();
const commonInputModel = await processor.process(doc);

// The nested object should be named "CustomNestedObject", NOT "TestPayloadNested"
const schema = AsyncAPIInputProcessor.convertToInternalSchema(
commonInputModel.originalInput.schemas()[0] as any
) as any;
expect(schema.properties.nested['x-modelgen-inferred-name']).toEqual(
'CustomNestedObject'
);
});

test('should still use context-based naming for schemas without the extension', async () => {
const doc = {
asyncapi: '2.0.0',
info: { title: 'Test', version: '1.0.0' },
channels: {
test: {
subscribe: {
message: {
payload: {
$ref: '#/components/schemas/TestPayload'
}
}
}
}
},
components: {
schemas: {
TestPayload: {
type: 'object',
properties: {
statusWithExtension: {
'x-modelgen-inferred-name': 'MyCustomStatus',
type: 'string',
enum: ['a', 'b']
},
statusWithoutExtension: {
type: 'string',
enum: ['x', 'y']
}
}
}
}
}
};
const processor = new AsyncAPIInputProcessor();
const commonInputModel = await processor.process(doc);

const schema = AsyncAPIInputProcessor.convertToInternalSchema(
commonInputModel.originalInput.schemas()[0] as any
) as any;

// Schema WITH extension should use the provided name
expect(
schema.properties.statusWithExtension['x-modelgen-inferred-name']
).toEqual('MyCustomStatus');

// Schema WITHOUT extension should use context-based inferred name
expect(
schema.properties.statusWithoutExtension['x-modelgen-inferred-name']
).toEqual('TestPayloadStatusWithoutExtensionEnum');
});
});
});