Skip to content

Commit 87de81f

Browse files
Merge pull request #857 from BitGo/DX-637-support-keys-in-record-codecs
feat: support `domain` for `t.record` codec
2 parents 4373ca8 + d041033 commit 87de81f

File tree

6 files changed

+166
-14
lines changed

6 files changed

+166
-14
lines changed

packages/openapi-generator/src/ir.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type Object = {
3838

3939
export type RecordObject = {
4040
type: 'record';
41+
domain?: Schema;
4142
codomain: Schema;
4243
};
4344

packages/openapi-generator/src/knownImports.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@ export const KNOWN_IMPORTS: KnownImports = {
9494
required: Object.keys(props),
9595
});
9696
},
97-
record: (_, _domain, codomain) => {
97+
record: (_, domain, codomain) => {
9898
if (!codomain) {
9999
return E.left('Codomain of record must be specified');
100100
} else {
101-
return E.right({ type: 'record', codomain });
101+
return E.right({ type: 'record', domain, codomain });
102102
}
103103
},
104104
union: (_, schema) => {

packages/openapi-generator/src/openapi.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,24 @@ function schemaToOpenAPI(
148148
}
149149
case 'record':
150150
const additionalProperties = schemaToOpenAPI(schema.codomain);
151-
if (additionalProperties === undefined) {
152-
return undefined;
151+
if (additionalProperties === undefined) return undefined;
152+
153+
if (schema.domain !== undefined) {
154+
const keys = schemaToOpenAPI(schema.domain) as OpenAPIV3.SchemaObject;
155+
if (keys.type === 'string' && keys.enum !== undefined) {
156+
const properties = keys.enum.reduce((acc, key) => {
157+
return { ...acc, [key]: additionalProperties };
158+
}, {});
159+
160+
return {
161+
type: 'object',
162+
properties,
163+
...defaultOpenAPIObject,
164+
required: keys.enum,
165+
};
166+
}
153167
}
168+
154169
return {
155170
type: 'object',
156171
additionalProperties,

packages/openapi-generator/src/optimize.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,12 @@ export function optimize(schema: Schema): Schema {
226226
}
227227
return { type: 'array', items: optimized };
228228
} else if (schema.type === 'record') {
229-
if (schema.comment) {
230-
return {
231-
type: 'record',
232-
codomain: optimize(schema.codomain),
233-
comment: schema.comment,
234-
};
235-
}
236-
return { type: 'record', codomain: optimize(schema.codomain) };
229+
return {
230+
type: 'record',
231+
...(schema.domain ? { domain: optimize(schema.domain) } : {}),
232+
codomain: optimize(schema.codomain),
233+
...(schema.comment ? { comment: schema.comment } : {}),
234+
};
237235
} else if (schema.type === 'tuple') {
238236
const schemas = schema.schemas.map(optimize);
239237
return { type: 'tuple', schemas };

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export const FOO = t.record(t.string, t.number);
285285
`;
286286

287287
testCase('record type is parsed', RECORD, {
288-
FOO: { type: 'record', codomain: { type: 'number', primitive: true } },
288+
FOO: { type: 'record', domain: {type: 'string', primitive: true}, codomain: { type: 'number', primitive: true } },
289289
});
290290

291291
const ENUM = `

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

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3938,4 +3938,142 @@ testCase("route with nested array examples", ROUTE_WITH_NESTED_ARRAY_EXAMPLES, {
39383938
}
39393939
}
39403940
}
3941-
});
3941+
});
3942+
3943+
const ROUTE_WITH_RECORD_TYPES = `
3944+
import * as t from 'io-ts';
3945+
import * as h from '@api-ts/io-ts-http';
3946+
3947+
const ValidKeys = t.keyof({ name: "name", age: "age", address: "address" });
3948+
const PersonObject = t.type({ bigName: t.string, bigAge: t.number });
3949+
3950+
export const route = h.httpRoute({
3951+
path: '/foo',
3952+
method: 'GET',
3953+
request: h.httpRequest({
3954+
query: {
3955+
name: t.string,
3956+
},
3957+
}),
3958+
response: {
3959+
200: {
3960+
person: t.record(ValidKeys, t.string),
3961+
anotherPerson: t.record(ValidKeys, PersonObject),
3962+
bigPerson: t.record(t.string, t.string),
3963+
anotherBigPerson: t.record(t.string, PersonObject),
3964+
}
3965+
},
3966+
});
3967+
`;
3968+
3969+
testCase("route with record types", ROUTE_WITH_RECORD_TYPES, {
3970+
openapi: '3.0.3',
3971+
info: {
3972+
title: 'Test',
3973+
version: '1.0.0'
3974+
},
3975+
paths: {
3976+
'/foo': {
3977+
get: {
3978+
parameters: [
3979+
{
3980+
name: 'name',
3981+
in: 'query',
3982+
required: true,
3983+
schema: {
3984+
type: 'string'
3985+
}
3986+
}
3987+
],
3988+
responses: {
3989+
'200': {
3990+
description: 'OK',
3991+
content: {
3992+
'application/json': {
3993+
schema: {
3994+
type: 'object',
3995+
properties: {
3996+
// becomes t.type()
3997+
person: {
3998+
type: 'object',
3999+
properties: {
4000+
name: { type: 'string' },
4001+
age: { type: 'string' },
4002+
address: { type: 'string' }
4003+
},
4004+
required: [ 'name', 'age', 'address' ]
4005+
},
4006+
// becomes t.type()
4007+
anotherPerson: {
4008+
type: 'object',
4009+
properties: {
4010+
name: {
4011+
type: 'object',
4012+
properties: {
4013+
bigName: { type: 'string' },
4014+
bigAge: { type: 'number' }
4015+
},
4016+
required: [ 'bigName', 'bigAge' ]
4017+
},
4018+
age: {
4019+
type: 'object',
4020+
properties: {
4021+
bigName: { type: 'string' },
4022+
bigAge: { type: 'number' }
4023+
},
4024+
required: [ 'bigName', 'bigAge' ]
4025+
},
4026+
address: {
4027+
type: 'object',
4028+
properties: {
4029+
bigName: { type: 'string' },
4030+
bigAge: { type: 'number' }
4031+
},
4032+
required: [ 'bigName', 'bigAge' ]
4033+
}
4034+
},
4035+
required: [ 'name', 'age', 'address' ]
4036+
},
4037+
bigPerson: {
4038+
// stays as t.record()
4039+
type: 'object',
4040+
additionalProperties: { type: 'string' }
4041+
},
4042+
anotherBigPerson: {
4043+
// stays as t.record()
4044+
type: 'object',
4045+
additionalProperties: {
4046+
type: 'object',
4047+
properties: {
4048+
bigName: { type: 'string' },
4049+
bigAge: { type: 'number' }
4050+
},
4051+
required: [ 'bigName', 'bigAge' ]
4052+
}
4053+
}
4054+
},
4055+
required: [ 'person', 'anotherPerson', 'bigPerson', 'anotherBigPerson' ]
4056+
}
4057+
}
4058+
}
4059+
}
4060+
}
4061+
}
4062+
}
4063+
},
4064+
components: {
4065+
schemas: {
4066+
ValidKeys: {
4067+
title: 'ValidKeys',
4068+
type: 'string',
4069+
enum: [ 'name', 'age', 'address' ]
4070+
},
4071+
PersonObject: {
4072+
title: 'PersonObject',
4073+
type: 'object',
4074+
properties: { bigName: { type: 'string' }, bigAge: { type: 'number' } },
4075+
required: [ 'bigName', 'bigAge' ]
4076+
}
4077+
}
4078+
}
4079+
});

0 commit comments

Comments
 (0)