Skip to content

Commit ef9eeea

Browse files
committed
feat: add support for known import metadata
DX-504
1 parent a6802a3 commit ef9eeea

File tree

4 files changed

+243
-31
lines changed

4 files changed

+243
-31
lines changed

packages/openapi-generator/src/ir.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Block } from 'comment-parser';
2+
import { OpenAPIV3 } from 'openapi-types';
23
import type { PseudoBigInt } from 'typescript';
34

45
export type AnyValue = {
@@ -65,4 +66,20 @@ export type HasComment = {
6566
comment?: Block;
6667
};
6768

68-
export type Schema = BaseSchema & HasComment;
69+
export type SchemaMetadata = Omit<
70+
OpenAPIV3.SchemaObject,
71+
| 'type'
72+
| 'additionalProperties'
73+
| 'properties'
74+
| 'enum'
75+
| 'anyOf'
76+
| 'allOf'
77+
| 'oneOf'
78+
| 'not'
79+
| 'nullable'
80+
| 'discriminator'
81+
| 'xml'
82+
| 'externalDocs'
83+
>;
84+
85+
export type Schema = BaseSchema & HasComment & SchemaMetadata;

packages/openapi-generator/src/knownImports.ts

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,18 +137,68 @@ export const KNOWN_IMPORTS: KnownImports = {
137137
UnknownRecord: () => E.right({ type: 'record', codomain: { type: 'any' } }),
138138
void: () => E.right({ type: 'undefined' }),
139139
},
140+
'io-ts-numbers': {
141+
NumberFromString: () => E.right({ type: 'string', format: 'number' }),
142+
NaturalFromString: () => E.right({ type: 'string', format: 'number' }),
143+
Negative: () => E.right({ type: 'number' }),
144+
NegativeFromString: () => E.right({ type: 'string', format: 'number' }),
145+
NegativeInt: () => E.right({ type: 'number' }),
146+
NegativeIntFromString: () => E.right({ type: 'string', format: 'number' }),
147+
NonNegative: () => E.right({ type: 'number' }),
148+
NonNegativeFromString: () => E.right({ type: 'string', format: 'number' }),
149+
NonNegativeInt: () => E.right({ type: 'number' }),
150+
NonNegativeIntFromString: () => E.right({ type: 'string', format: 'number' }),
151+
NonPositive: () => E.right({ type: 'number' }),
152+
NonPositiveFromString: () => E.right({ type: 'string', format: 'number' }),
153+
NonPositiveInt: () => E.right({ type: 'number' }),
154+
NonPositiveIntFromString: () => E.right({ type: 'string', format: 'number' }),
155+
NonZero: () => E.right({ type: 'number' }),
156+
NonZeroFromString: () => E.right({ type: 'string', format: 'number' }),
157+
NonZeroInt: () => E.right({ type: 'number' }),
158+
NonZeroIntFromString: () => E.right({ type: 'string', format: 'number' }),
159+
Positive: () => E.right({ type: 'number' }),
160+
PositiveFromString: () => E.right({ type: 'string', format: 'number' }),
161+
Zero: () => E.right({ type: 'number' }),
162+
ZeroFromString: () => E.right({ type: 'string', format: 'number' }),
163+
},
164+
'io-ts-bigint': {
165+
BigIntFromString: () => E.right({ type: 'string', format: 'number' }),
166+
NegativeBigInt: () => E.right({ type: 'number' }),
167+
NegativeBigIntFromString: () => E.right({ type: 'string', format: 'number' }),
168+
NonEmptyString: () => E.right({ type: 'string' }),
169+
NonNegativeBigInt: () => E.right({ type: 'number' }),
170+
NonNegativeBigIntFromString: () => E.right({ type: 'string', format: 'number' }),
171+
NonPositiveBigInt: () => E.right({ type: 'number' }),
172+
NonPositiveBigIntFromString: () => E.right({ type: 'string', format: 'number' }),
173+
NonZeroBigInt: () => E.right({ type: 'number' }),
174+
NonZeroBigIntFromString: () => E.right({ type: 'string', format: 'number' }),
175+
PositiveBigInt: () => E.right({ type: 'number' }),
176+
PositiveBigIntFromString: () => E.right({ type: 'string', format: 'number' }),
177+
ZeroBigInt: () => E.right({ type: 'number' }),
178+
ZeroBigIntFromString: () => E.right({ type: 'string', format: 'number' }),
179+
},
140180
'io-ts-types': {
141-
BigIntFromString: () => E.right({ type: 'string' }),
142-
BooleanFromNumber: () => E.right({ type: 'number' }),
143-
BooleanFromString: () => E.right({ type: 'string' }),
144-
DateFromISOString: () => E.right({ type: 'string' }),
145-
DateFromNumber: () => E.right({ type: 'number' }),
146-
DateFromUnixTime: () => E.right({ type: 'number' }),
147-
IntFromString: () => E.right({ type: 'string' }),
181+
NumberFromString: () => E.right({ type: 'string', format: 'number' }),
182+
BigIntFromString: () => E.right({ type: 'string', format: 'number' }),
183+
BooleanFromNumber: () => E.right({ type: 'number', enum: [0, 1] }),
184+
BooleanFromString: () => E.right({ type: 'string', enum: ['true', 'false'] }),
185+
DateFromISOString: () => E.right({ type: 'string', format: 'date-time' }),
186+
DateFromNumber: () =>
187+
E.right({
188+
type: 'number',
189+
format: 'number',
190+
description: 'Number of milliseconds since the Unix epoch',
191+
}),
192+
DateFromUnixTime: () =>
193+
E.right({
194+
type: 'number',
195+
format: 'number',
196+
description: 'Number of seconds since the Unix epoch',
197+
}),
198+
IntFromString: () => E.right({ type: 'string', format: 'integer' }),
148199
JsonFromString: () => E.right({ type: 'string' }),
149200
nonEmptyArray: (_, innerSchema) => E.right({ type: 'array', items: innerSchema }),
150201
NonEmptyString: () => E.right({ type: 'string' }),
151-
NumberFromString: () => E.right({ type: 'string' }),
152202
readonlyNonEmptyArray: (_, innerSchema) =>
153203
E.right({ type: 'array', items: innerSchema }),
154204
UUID: () => E.right({ type: 'string' }),

packages/openapi-generator/src/openapi.ts

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -177,28 +177,29 @@ function schemaToOpenAPI(
177177
const emptyBlock: Block = { description: '', tags: [], source: [], problems: [] };
178178
const jsdoc = parseCommentBlock(schema.comment ?? emptyBlock);
179179

180-
const defaultValue = jsdoc?.tags?.default;
181-
const example = jsdoc?.tags?.example;
182-
const maxLength = jsdoc?.tags?.maxLength;
183-
const minLength = jsdoc?.tags?.minLength;
184-
const pattern = jsdoc?.tags?.pattern;
185-
const minimum = jsdoc?.tags?.minimum;
186-
const maximum = jsdoc?.tags?.maximum;
187-
const minItems = jsdoc?.tags?.minItems;
188-
const maxItems = jsdoc?.tags?.maxItems;
189-
const minProperties = jsdoc?.tags?.minProperties;
190-
const maxProperties = jsdoc?.tags?.maxProperties;
191-
const exclusiveMinimum = jsdoc?.tags?.exclusiveMinimum;
192-
const exclusiveMaximum = jsdoc?.tags?.exclusiveMaximum;
193-
const multipleOf = jsdoc?.tags?.multipleOf;
194-
const uniqueItems = jsdoc?.tags?.uniqueItems;
195-
const readOnly = jsdoc?.tags?.readOnly;
196-
const writeOnly = jsdoc?.tags?.writeOnly;
197-
const format = jsdoc?.tags?.format;
198-
const title = jsdoc?.tags?.title;
180+
const defaultValue = jsdoc?.tags?.default ?? schema.default;
181+
const example = jsdoc?.tags?.example ?? schema.example;
182+
const maxLength = jsdoc?.tags?.maxLength ?? schema.maxLength;
183+
const minLength = jsdoc?.tags?.minLength ?? schema.minLength;
184+
const pattern = jsdoc?.tags?.pattern ?? schema.pattern;
185+
const minimum = jsdoc?.tags?.minimum ?? schema.maximum;
186+
const maximum = jsdoc?.tags?.maximum ?? schema.minimum;
187+
const minItems = jsdoc?.tags?.minItems ?? schema.minItems;
188+
const maxItems = jsdoc?.tags?.maxItems ?? schema.maxItems;
189+
const minProperties = jsdoc?.tags?.minProperties ?? schema.minProperties;
190+
const maxProperties = jsdoc?.tags?.maxProperties ?? schema.maxProperties;
191+
const exclusiveMinimum = jsdoc?.tags?.exclusiveMinimum ?? schema.exclusiveMinimum;
192+
const exclusiveMaximum = jsdoc?.tags?.exclusiveMaximum ?? schema.exclusiveMaximum;
193+
const multipleOf = jsdoc?.tags?.multipleOf ?? schema.multipleOf;
194+
const uniqueItems = jsdoc?.tags?.uniqueItems ?? schema.uniqueItems;
195+
const readOnly = jsdoc?.tags?.readOnly ?? schema.readOnly;
196+
const writeOnly = jsdoc?.tags?.writeOnly ?? schema.writeOnly;
197+
const format = jsdoc?.tags?.format ?? schema.format ?? schema.format;
198+
const title = jsdoc?.tags?.title ?? schema.title;
199199

200-
const deprecated = Object.keys(jsdoc?.tags || {}).includes('deprecated');
201-
const description = schema.comment?.description;
200+
const deprecated =
201+
Object.keys(jsdoc?.tags || {}).includes('deprecated') || !!schema.deprecated;
202+
const description = schema.comment?.description ?? schema.description;
202203

203204
const defaultOpenAPIObject = {
204205
...(defaultValue ? { default: parseField(schema, defaultValue) } : {}),

packages/openapi-generator/test/openapi.test.ts

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3018,4 +3018,148 @@ testCase('route with api error schema', ROUTE_WITH_SCHEMA_WITH_COMMENT, {
30183018
}
30193019
},
30203020
},
3021-
});
3021+
});
3022+
3023+
const ROUTE_WITH_SCHEMA_WITH_DEFAULT_METADATA = `
3024+
import * as t from 'io-ts';
3025+
import * as h from '@api-ts/io-ts-http';
3026+
import { DateFromNumber } from 'io-ts-types';
3027+
3028+
export const route = h.httpRoute({
3029+
path: '/foo',
3030+
method: 'GET',
3031+
request: h.httpRequest({
3032+
query: {
3033+
ipRestrict: t.boolean
3034+
},
3035+
}),
3036+
response: {
3037+
200: {
3038+
test: DateFromNumber
3039+
}
3040+
},
3041+
});
3042+
`
3043+
3044+
testCase('route with schema with default metadata', ROUTE_WITH_SCHEMA_WITH_DEFAULT_METADATA, {
3045+
openapi: '3.0.3',
3046+
info: {
3047+
title: 'Test',
3048+
version: '1.0.0'
3049+
},
3050+
paths: {
3051+
'/foo': {
3052+
get: {
3053+
parameters: [
3054+
{
3055+
in: 'query',
3056+
name: 'ipRestrict',
3057+
required: true,
3058+
schema: {
3059+
type: 'boolean',
3060+
}
3061+
}
3062+
],
3063+
responses: {
3064+
'200': {
3065+
description: 'OK',
3066+
content: {
3067+
'application/json': {
3068+
schema: {
3069+
type: 'object',
3070+
properties: {
3071+
test: {
3072+
type: 'number',
3073+
format: 'number',
3074+
description: 'Number of milliseconds since the Unix epoch',
3075+
}
3076+
},
3077+
required: [
3078+
'test'
3079+
]
3080+
}
3081+
}
3082+
}
3083+
}
3084+
}
3085+
}
3086+
}
3087+
},
3088+
components: {
3089+
schemas: {}
3090+
}
3091+
});
3092+
3093+
const ROUTE_WITH_OVERIDDEN_METADATA = `
3094+
import * as t from 'io-ts';
3095+
import * as h from '@api-ts/io-ts-http';
3096+
import { DateFromNumber } from 'io-ts-types';
3097+
3098+
export const route = h.httpRoute({
3099+
path: '/foo',
3100+
method: 'GET',
3101+
request: h.httpRequest({
3102+
query: {
3103+
ipRestrict: t.boolean
3104+
},
3105+
}),
3106+
response: {
3107+
200: {
3108+
/**
3109+
* Testing overridden metadata
3110+
* @format string
3111+
*/
3112+
test: DateFromNumber
3113+
}
3114+
},
3115+
});
3116+
`
3117+
3118+
testCase('route with schema with default metadata', ROUTE_WITH_OVERIDDEN_METADATA, {
3119+
openapi: '3.0.3',
3120+
info: {
3121+
title: 'Test',
3122+
version: '1.0.0'
3123+
},
3124+
paths: {
3125+
'/foo': {
3126+
get: {
3127+
parameters: [
3128+
{
3129+
in: 'query',
3130+
name: 'ipRestrict',
3131+
required: true,
3132+
schema: {
3133+
type: 'boolean',
3134+
}
3135+
}
3136+
],
3137+
responses: {
3138+
'200': {
3139+
description: 'OK',
3140+
content: {
3141+
'application/json': {
3142+
schema: {
3143+
type: 'object',
3144+
properties: {
3145+
test: {
3146+
type: 'number',
3147+
format: 'string',
3148+
description: 'Testing overridden metadata',
3149+
}
3150+
},
3151+
required: [
3152+
'test'
3153+
]
3154+
}
3155+
}
3156+
}
3157+
}
3158+
}
3159+
}
3160+
}
3161+
},
3162+
components: {
3163+
schemas: {}
3164+
}
3165+
});

0 commit comments

Comments
 (0)