diff --git a/packages/oas/src/operation/index.ts b/packages/oas/src/operation/index.ts index 7d2433a4..1c043d3f 100644 --- a/packages/oas/src/operation/index.ts +++ b/packages/oas/src/operation/index.ts @@ -63,6 +63,13 @@ export class Operation { */ contentType: string; + /** + * Selected content type for this operation. + * + * @example 'application/json' + */ + selectedContentType: string; + /** * An object with groups of all example definitions (body/header/query/path/response/etc.) */ @@ -98,6 +105,7 @@ export class Operation { this.method = method; this.contentType = undefined; + this.selectedContentType = undefined; this.requestBodyExamples = undefined; this.responseExamples = undefined; this.callbackExamples = undefined; @@ -129,6 +137,10 @@ export class Operation { } getContentType(): string { + if (this.selectedContentType) { + return this.selectedContentType; + } + if (this.contentType) { return this.contentType; } @@ -175,6 +187,22 @@ export class Operation { return matchesMimeType.xml(this.getContentType()); } + /** + * Sets a selected content type for this operation. + * + * @param selectedContentType Media type to select. + */ + setContentType(selectedContentType: string): void { + this.selectedContentType = selectedContentType; + } + + /** + * Returns all available media types for either the request body. + */ + getMediaTypes(): string[] { + return this.getRequestBodyMediaTypes(); + } + /** * Checks if the current operation is a webhook or not. * @@ -490,11 +518,6 @@ export class Operation { }); } - /** - * Get a single response for this status code, formatted as JSON schema. - * - * @param statusCode Status code to pull a JSON Schema response for. - */ getResponseAsJSONSchema( statusCode: number | string, opts: { @@ -510,11 +533,18 @@ export class Operation { * name, just make sure to return your transformed schema. */ transformer?: (schema: SchemaObject) => SchemaObject; + + /** + * Preferred content type to use when choosing a response schema. If specified, this content + * type will be used in preference to others if multiple response representations are available. + */ + preferContentType?: string; } = {}, ): SchemaObject { return getResponseAsJSONSchema(this, this.api, statusCode, { includeDiscriminatorMappingRefs: true, transformer: (s: SchemaObject) => s, + preferContentType: this.selectedContentType, ...opts, }); } @@ -611,12 +641,22 @@ export class Operation { return false; } - if (mediaType) { - if (!(mediaType in requestBody.content)) { + const mediaTypeToUse = mediaType || this.selectedContentType; + + if (mediaTypeToUse) { + if (!(mediaTypeToUse in requestBody.content)) { return false; } - return requestBody.content[mediaType]; + if (mediaType) { + return requestBody.content[mediaType]; + } + + return [ + mediaTypeToUse, + requestBody.content[mediaTypeToUse], + ...(requestBody.description ? [requestBody.description] : []), + ]; } // Since no media type was supplied we need to find either the first JSON-like media type that diff --git a/packages/oas/src/operation/lib/get-response-as-json-schema.ts b/packages/oas/src/operation/lib/get-response-as-json-schema.ts index 56ef666d..c63cb3d5 100644 --- a/packages/oas/src/operation/lib/get-response-as-json-schema.ts +++ b/packages/oas/src/operation/lib/get-response-as-json-schema.ts @@ -100,6 +100,7 @@ export function getResponseAsJSONSchema( statusCode: number | string, opts?: { includeDiscriminatorMappingRefs?: boolean; + preferContentType?: string; /** * With a transformer you can transform any data within a given schema, like say if you want * to rewrite a potentially unsafe `title` that might be eventually used as a JS variable @@ -140,6 +141,14 @@ export function getResponseAsJSONSchema( return null; } + if (opts?.preferContentType && content[opts.preferContentType]) { + return toJSONSchema(cloneObject(content[opts.preferContentType].schema), { + addEnumsToDescriptions: true, + refLogger, + transformer: opts.transformer, + }); + } + for (let i = 0; i < contentTypes.length; i++) { if (isJSON(contentTypes[i])) { return toJSONSchema(cloneObject(content[contentTypes[i]].schema), { diff --git a/packages/oas/test/operation/lib/get-response-as-json-schema-content-type.test.ts b/packages/oas/test/operation/lib/get-response-as-json-schema-content-type.test.ts new file mode 100644 index 00000000..0b444d30 --- /dev/null +++ b/packages/oas/test/operation/lib/get-response-as-json-schema-content-type.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; +import { createOasForOperation } from '../../__fixtures__/create-oas.js'; + +describe('#getResponseAsJSONSchema() content type override', () => { + it('should respect the preferContentType option if provided', () => { + const oas = createOasForOperation({ + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { json: { type: 'string' } }, + }, + }, + 'application/xml': { + schema: { + type: 'object', + properties: { xml: { type: 'string' } }, + }, + }, + }, + }, + }, + }); + const operation = oas.operation('/', 'get'); + + // Default behavior (prefers JSON) + const defaultSchema = operation.getResponseAsJSONSchema('200'); + expect(defaultSchema[0].schema.properties.json).toBeDefined(); + + // With override + const xmlSchema = operation.getResponseAsJSONSchema('200', { preferContentType: 'application/xml' }); + expect(xmlSchema[0].schema.properties.xml).toBeDefined(); + }); + + it('should fall back to default logic if provided preferContentType does not exist', () => { + const oas = createOasForOperation({ + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { json: { type: 'string' } }, + }, + }, + }, + }, + }, + }); + const operation = oas.operation('/', 'get'); + + const schema = operation.getResponseAsJSONSchema('200', { preferContentType: 'application/xml' }); + expect(schema[0].schema.properties.json).toBeDefined(); + }); +}); diff --git a/packages/oas/test/operation/selected-content-type.test.ts b/packages/oas/test/operation/selected-content-type.test.ts new file mode 100644 index 00000000..7fc347fb --- /dev/null +++ b/packages/oas/test/operation/selected-content-type.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from 'vitest'; +import { createOasForOperation } from '../__fixtures__/create-oas.js'; + +describe('Operation #selectedContentType', () => { + it('should allow setting and getting a selected content type', () => { + const oas = createOasForOperation({ + requestBody: { + content: { + 'application/json': { schema: { type: 'object' } }, + 'application/xml': { schema: { type: 'object' } }, + }, + }, + }); + const operation = oas.operation('/', 'get'); + + expect(operation.getContentType()).toBe('application/json'); + + operation.setContentType('application/xml'); + expect(operation.getContentType()).toBe('application/xml'); + }); + + it('should respect selected content type in getRequestBody()', () => { + const oas = createOasForOperation({ + requestBody: { + description: 'Test Description', + content: { + 'application/json': { schema: { type: 'object', properties: { json: { type: 'boolean' } } } }, + 'application/xml': { schema: { type: 'object', properties: { xml: { type: 'boolean' } } } }, + }, + }, + }); + const operation = oas.operation('/', 'get'); + + // Default (should be JSON) + const [defaultType] = operation.getRequestBody() as [string, any, string]; + expect(defaultType).toBe('application/json'); + + // Select XML + operation.setContentType('application/xml'); + const [selectedType, selectedSchema, description] = operation.getRequestBody() as [string, any, string]; + expect(selectedType).toBe('application/xml'); + expect(selectedSchema.schema.properties.xml).toBeDefined(); + expect(description).toBe('Test Description'); + }); + + it('should return false if selected content type does not exist in getRequestBody()', () => { + const oas = createOasForOperation({ + requestBody: { + content: { + 'application/json': { schema: { type: 'object' } }, + }, + }, + }); + const operation = oas.operation('/', 'get'); + + operation.setContentType('application/xml'); + expect(operation.getRequestBody()).toBe(false); + }); + + it('should return available media types via getMediaTypes()', () => { + const oas = createOasForOperation({ + requestBody: { + content: { + 'application/json': { schema: { type: 'object' } }, + 'application/xml': { schema: { type: 'object' } }, + }, + }, + responses: { + 200: { + description: 'Success response', + content: { + 'text/plain': { schema: { type: 'string' } }, + }, + }, + }, + }); + const operation = oas.operation('/', 'get'); + + expect(operation.getMediaTypes()).toStrictEqual(['application/json', 'application/xml', 'text/plain']); + }); + + describe('no content type requirements', () => { + it('should return an empty array if no media types are defined anywhere', () => { + const oas = createOasForOperation({ + responses: { + 204: { description: 'No Content' }, + }, + }); + const operation = oas.operation('/', 'get'); + + expect(operation.getMediaTypes()).toStrictEqual([]); + }); + + it('should not include Accept header if no response content exists', () => { + const oas = createOasForOperation({ + requestBody: { + content: { 'application/json': {} }, + }, + responses: { + 204: { description: 'No Content' }, + }, + }); + const operation = oas.operation('/', 'get'); + + const headers = operation.getHeaders(); + expect(headers.request).not.toContain('Accept'); + expect(headers.request).toContain('Content-Type'); // Added because request body exists + }); + }); +});