diff --git a/packages/openapi-generator/src/ir.ts b/packages/openapi-generator/src/ir.ts index 8ebbabdc..c51e0a2c 100644 --- a/packages/openapi-generator/src/ir.ts +++ b/packages/openapi-generator/src/ir.ts @@ -1,4 +1,5 @@ import { Block } from 'comment-parser'; +import { OpenAPIV3 } from 'openapi-types'; import type { PseudoBigInt } from 'typescript'; export type AnyValue = { @@ -65,4 +66,20 @@ export type HasComment = { comment?: Block; }; -export type Schema = BaseSchema & HasComment; +export type SchemaMetadata = Omit< + OpenAPIV3.SchemaObject, + | 'type' + | 'additionalProperties' + | 'properties' + | 'enum' + | 'anyOf' + | 'allOf' + | 'oneOf' + | 'not' + | 'nullable' + | 'discriminator' + | 'xml' + | 'externalDocs' +>; + +export type Schema = BaseSchema & HasComment & SchemaMetadata; diff --git a/packages/openapi-generator/src/knownImports.ts b/packages/openapi-generator/src/knownImports.ts index cd1bae50..fcb68074 100644 --- a/packages/openapi-generator/src/knownImports.ts +++ b/packages/openapi-generator/src/knownImports.ts @@ -137,18 +137,68 @@ export const KNOWN_IMPORTS: KnownImports = { UnknownRecord: () => E.right({ type: 'record', codomain: { type: 'any' } }), void: () => E.right({ type: 'undefined' }), }, + 'io-ts-numbers': { + NumberFromString: () => E.right({ type: 'string', format: 'number' }), + NaturalFromString: () => E.right({ type: 'string', format: 'number' }), + Negative: () => E.right({ type: 'number' }), + NegativeFromString: () => E.right({ type: 'string', format: 'number' }), + NegativeInt: () => E.right({ type: 'number' }), + NegativeIntFromString: () => E.right({ type: 'string', format: 'number' }), + NonNegative: () => E.right({ type: 'number' }), + NonNegativeFromString: () => E.right({ type: 'string', format: 'number' }), + NonNegativeInt: () => E.right({ type: 'number' }), + NonNegativeIntFromString: () => E.right({ type: 'string', format: 'number' }), + NonPositive: () => E.right({ type: 'number' }), + NonPositiveFromString: () => E.right({ type: 'string', format: 'number' }), + NonPositiveInt: () => E.right({ type: 'number' }), + NonPositiveIntFromString: () => E.right({ type: 'string', format: 'number' }), + NonZero: () => E.right({ type: 'number' }), + NonZeroFromString: () => E.right({ type: 'string', format: 'number' }), + NonZeroInt: () => E.right({ type: 'number' }), + NonZeroIntFromString: () => E.right({ type: 'string', format: 'number' }), + Positive: () => E.right({ type: 'number' }), + PositiveFromString: () => E.right({ type: 'string', format: 'number' }), + Zero: () => E.right({ type: 'number' }), + ZeroFromString: () => E.right({ type: 'string', format: 'number' }), + }, + 'io-ts-bigint': { + BigIntFromString: () => E.right({ type: 'string', format: 'number' }), + NegativeBigInt: () => E.right({ type: 'number' }), + NegativeBigIntFromString: () => E.right({ type: 'string', format: 'number' }), + NonEmptyString: () => E.right({ type: 'string' }), + NonNegativeBigInt: () => E.right({ type: 'number' }), + NonNegativeBigIntFromString: () => E.right({ type: 'string', format: 'number' }), + NonPositiveBigInt: () => E.right({ type: 'number' }), + NonPositiveBigIntFromString: () => E.right({ type: 'string', format: 'number' }), + NonZeroBigInt: () => E.right({ type: 'number' }), + NonZeroBigIntFromString: () => E.right({ type: 'string', format: 'number' }), + PositiveBigInt: () => E.right({ type: 'number' }), + PositiveBigIntFromString: () => E.right({ type: 'string', format: 'number' }), + ZeroBigInt: () => E.right({ type: 'number' }), + ZeroBigIntFromString: () => E.right({ type: 'string', format: 'number' }), + }, 'io-ts-types': { - BigIntFromString: () => E.right({ type: 'string' }), - BooleanFromNumber: () => E.right({ type: 'number' }), - BooleanFromString: () => E.right({ type: 'string' }), - DateFromISOString: () => E.right({ type: 'string' }), - DateFromNumber: () => E.right({ type: 'number' }), - DateFromUnixTime: () => E.right({ type: 'number' }), - IntFromString: () => E.right({ type: 'string' }), + NumberFromString: () => E.right({ type: 'string', format: 'number' }), + BigIntFromString: () => E.right({ type: 'string', format: 'number' }), + BooleanFromNumber: () => E.right({ type: 'number', enum: [0, 1] }), + BooleanFromString: () => E.right({ type: 'string', enum: ['true', 'false'] }), + DateFromISOString: () => E.right({ type: 'string', format: 'date-time' }), + DateFromNumber: () => + E.right({ + type: 'number', + format: 'number', + description: 'Number of milliseconds since the Unix epoch', + }), + DateFromUnixTime: () => + E.right({ + type: 'number', + format: 'number', + description: 'Number of seconds since the Unix epoch', + }), + IntFromString: () => E.right({ type: 'string', format: 'integer' }), JsonFromString: () => E.right({ type: 'string' }), nonEmptyArray: (_, innerSchema) => E.right({ type: 'array', items: innerSchema }), NonEmptyString: () => E.right({ type: 'string' }), - NumberFromString: () => E.right({ type: 'string' }), readonlyNonEmptyArray: (_, innerSchema) => E.right({ type: 'array', items: innerSchema }), UUID: () => E.right({ type: 'string' }), diff --git a/packages/openapi-generator/src/openapi.ts b/packages/openapi-generator/src/openapi.ts index 7eeb6a02..4fadbd57 100644 --- a/packages/openapi-generator/src/openapi.ts +++ b/packages/openapi-generator/src/openapi.ts @@ -177,28 +177,29 @@ function schemaToOpenAPI( const emptyBlock: Block = { description: '', tags: [], source: [], problems: [] }; const jsdoc = parseCommentBlock(schema.comment ?? emptyBlock); - const defaultValue = jsdoc?.tags?.default; - const example = jsdoc?.tags?.example; - const maxLength = jsdoc?.tags?.maxLength; - const minLength = jsdoc?.tags?.minLength; - const pattern = jsdoc?.tags?.pattern; - const minimum = jsdoc?.tags?.minimum; - const maximum = jsdoc?.tags?.maximum; - const minItems = jsdoc?.tags?.minItems; - const maxItems = jsdoc?.tags?.maxItems; - const minProperties = jsdoc?.tags?.minProperties; - const maxProperties = jsdoc?.tags?.maxProperties; - const exclusiveMinimum = jsdoc?.tags?.exclusiveMinimum; - const exclusiveMaximum = jsdoc?.tags?.exclusiveMaximum; - const multipleOf = jsdoc?.tags?.multipleOf; - const uniqueItems = jsdoc?.tags?.uniqueItems; - const readOnly = jsdoc?.tags?.readOnly; - const writeOnly = jsdoc?.tags?.writeOnly; - const format = jsdoc?.tags?.format; - const title = jsdoc?.tags?.title; + const defaultValue = jsdoc?.tags?.default ?? schema.default; + const example = jsdoc?.tags?.example ?? schema.example; + const maxLength = jsdoc?.tags?.maxLength ?? schema.maxLength; + const minLength = jsdoc?.tags?.minLength ?? schema.minLength; + const pattern = jsdoc?.tags?.pattern ?? schema.pattern; + const minimum = jsdoc?.tags?.minimum ?? schema.maximum; + const maximum = jsdoc?.tags?.maximum ?? schema.minimum; + const minItems = jsdoc?.tags?.minItems ?? schema.minItems; + const maxItems = jsdoc?.tags?.maxItems ?? schema.maxItems; + const minProperties = jsdoc?.tags?.minProperties ?? schema.minProperties; + const maxProperties = jsdoc?.tags?.maxProperties ?? schema.maxProperties; + const exclusiveMinimum = jsdoc?.tags?.exclusiveMinimum ?? schema.exclusiveMinimum; + const exclusiveMaximum = jsdoc?.tags?.exclusiveMaximum ?? schema.exclusiveMaximum; + const multipleOf = jsdoc?.tags?.multipleOf ?? schema.multipleOf; + const uniqueItems = jsdoc?.tags?.uniqueItems ?? schema.uniqueItems; + const readOnly = jsdoc?.tags?.readOnly ?? schema.readOnly; + const writeOnly = jsdoc?.tags?.writeOnly ?? schema.writeOnly; + const format = jsdoc?.tags?.format ?? schema.format ?? schema.format; + const title = jsdoc?.tags?.title ?? schema.title; - const deprecated = Object.keys(jsdoc?.tags || {}).includes('deprecated'); - const description = schema.comment?.description; + const deprecated = + Object.keys(jsdoc?.tags || {}).includes('deprecated') || !!schema.deprecated; + const description = schema.comment?.description ?? schema.description; const defaultOpenAPIObject = { ...(defaultValue ? { default: parseField(schema, defaultValue) } : {}), diff --git a/packages/openapi-generator/test/openapi.test.ts b/packages/openapi-generator/test/openapi.test.ts index 30e8f225..8aa62b2a 100644 --- a/packages/openapi-generator/test/openapi.test.ts +++ b/packages/openapi-generator/test/openapi.test.ts @@ -3018,4 +3018,148 @@ testCase('route with api error schema', ROUTE_WITH_SCHEMA_WITH_COMMENT, { } }, }, -}); \ No newline at end of file +}); + +const ROUTE_WITH_SCHEMA_WITH_DEFAULT_METADATA = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; +import { DateFromNumber } from 'io-ts-types'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + ipRestrict: t.boolean + }, + }), + response: { + 200: { + test: DateFromNumber + } + }, +}); +`; + +testCase('route with schema with default metadata', ROUTE_WITH_SCHEMA_WITH_DEFAULT_METADATA, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + parameters: [ + { + in: 'query', + name: 'ipRestrict', + required: true, + schema: { + type: 'boolean', + } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'number', + format: 'number', + description: 'Number of milliseconds since the Unix epoch', + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + +const ROUTE_WITH_OVERIDDEN_METADATA = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; +import { DateFromNumber } from 'io-ts-types'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + ipRestrict: t.boolean + }, + }), + response: { + 200: { + /** + * Testing overridden metadata + * @format string + */ + test: DateFromNumber + } + }, +}); +`; + +testCase('route with schema with default metadata', ROUTE_WITH_OVERIDDEN_METADATA, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + parameters: [ + { + in: 'query', + name: 'ipRestrict', + required: true, + schema: { + type: 'boolean', + } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'number', + format: 'string', + description: 'Testing overridden metadata', + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +});