Skip to content

Commit 6730167

Browse files
authored
Merge pull request #14 from jsonjoy-com/copilot/fix-13
[WIP] Refactor type `toJsonSchema()` methods
2 parents 20cea29 + 9db570a commit 6730167

18 files changed

Lines changed: 353 additions & 146 deletions

src/json-schema/converter.ts

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
import type {AbstractType} from '../type/classes/AbstractType';
2+
import type {AnyType} from '../type/classes/AnyType';
3+
import type {ArrayType} from '../type/classes/ArrayType';
4+
import type {BinaryType} from '../type/classes/BinaryType';
5+
import type {BooleanType} from '../type/classes/BooleanType';
6+
import type {ConstType} from '../type/classes/ConstType';
7+
import type {MapType} from '../type/classes/MapType';
8+
import type {NumberType} from '../type/classes/NumberType';
9+
import type {ObjectType} from '../type/classes/ObjectType';
10+
import type {OrType} from '../type/classes/OrType';
11+
import type {RefType} from '../type/classes/RefType';
12+
import type {StringType} from '../type/classes/StringType';
13+
import type {TupleType} from '../type/classes/TupleType';
14+
import type {TypeExportContext} from '../system/TypeExportContext';
15+
import type * as schema from '../schema';
16+
import type {
17+
JsonSchemaNode,
18+
JsonSchemaGenericKeywords,
19+
JsonSchemaAny,
20+
JsonSchemaArray,
21+
JsonSchemaBinary,
22+
JsonSchemaBoolean,
23+
JsonSchemaString,
24+
JsonSchemaNumber,
25+
JsonSchemaObject,
26+
JsonSchemaRef,
27+
JsonSchemaOr,
28+
JsonSchemaNull,
29+
} from './types';
30+
31+
/**
32+
* Extracts the base JSON Schema properties that are common to all types.
33+
* This replaces the logic from AbstractType.toJsonSchema().
34+
*/
35+
function getBaseJsonSchema(type: AbstractType<any>, ctx?: TypeExportContext): JsonSchemaGenericKeywords {
36+
const typeSchema = type.getSchema();
37+
const jsonSchema: JsonSchemaGenericKeywords = {};
38+
39+
if (typeSchema.title) jsonSchema.title = typeSchema.title;
40+
if (typeSchema.description) jsonSchema.description = typeSchema.description;
41+
if (typeSchema.examples) {
42+
jsonSchema.examples = typeSchema.examples.map((example: schema.TExample) => example.value);
43+
}
44+
45+
return jsonSchema;
46+
}
47+
48+
/**
49+
* Main router function that converts a type to JSON Schema using a switch statement.
50+
* This replaces the individual toJsonSchema() methods on each type class.
51+
*/
52+
export function typeToJsonSchema(type: AbstractType<any>, ctx?: TypeExportContext): JsonSchemaNode {
53+
const typeName = type.getTypeName();
54+
55+
switch (typeName) {
56+
case 'any':
57+
return anyToJsonSchema(type as AnyType, ctx);
58+
case 'arr':
59+
return arrayToJsonSchema(type as ArrayType<any>, ctx);
60+
case 'bin':
61+
return binaryToJsonSchema(type as BinaryType<any>, ctx);
62+
case 'bool':
63+
return booleanToJsonSchema(type as BooleanType, ctx);
64+
case 'const':
65+
return constToJsonSchema(type as ConstType<any>, ctx);
66+
case 'map':
67+
return mapToJsonSchema(type as MapType<any>, ctx);
68+
case 'num':
69+
return numberToJsonSchema(type as NumberType, ctx);
70+
case 'obj':
71+
return objectToJsonSchema(type as ObjectType<any>, ctx);
72+
case 'or':
73+
return orToJsonSchema(type as OrType<any>, ctx);
74+
case 'ref':
75+
return refToJsonSchema(type as RefType<any>, ctx);
76+
case 'str':
77+
return stringToJsonSchema(type as StringType, ctx);
78+
case 'tup':
79+
return tupleToJsonSchema(type as TupleType<any>, ctx);
80+
default:
81+
// Fallback to base implementation for unknown types
82+
return getBaseJsonSchema(type, ctx);
83+
}
84+
}
85+
86+
// Individual converter functions for each type
87+
88+
function anyToJsonSchema(type: AnyType, ctx?: TypeExportContext): JsonSchemaAny {
89+
const baseSchema = getBaseJsonSchema(type, ctx);
90+
const result: JsonSchemaAny = {
91+
type: ['string', 'number', 'boolean', 'null', 'array', 'object'] as const,
92+
};
93+
94+
// Add base properties
95+
Object.assign(result, baseSchema);
96+
97+
return result;
98+
}
99+
100+
function arrayToJsonSchema(type: ArrayType<any>, ctx?: TypeExportContext): JsonSchemaArray {
101+
const schema = type.getSchema();
102+
const baseSchema = getBaseJsonSchema(type, ctx);
103+
const result: JsonSchemaArray = {
104+
type: 'array',
105+
items: typeToJsonSchema((type as any).type, ctx),
106+
};
107+
108+
// Add base properties
109+
Object.assign(result, baseSchema);
110+
111+
if (schema.min !== undefined) result.minItems = schema.min;
112+
if (schema.max !== undefined) result.maxItems = schema.max;
113+
114+
return result;
115+
}
116+
117+
function binaryToJsonSchema(type: BinaryType<any>, ctx?: TypeExportContext): JsonSchemaBinary {
118+
const baseSchema = getBaseJsonSchema(type, ctx);
119+
const result: JsonSchemaBinary = {
120+
type: 'binary' as any,
121+
};
122+
123+
// Add base properties
124+
Object.assign(result, baseSchema);
125+
126+
return result;
127+
}
128+
129+
function booleanToJsonSchema(type: BooleanType, ctx?: TypeExportContext): JsonSchemaBoolean {
130+
const baseSchema = getBaseJsonSchema(type, ctx);
131+
const result: JsonSchemaBoolean = {
132+
type: 'boolean',
133+
};
134+
135+
// Add base properties
136+
Object.assign(result, baseSchema);
137+
138+
return result;
139+
}
140+
141+
function constToJsonSchema(type: ConstType<any>, ctx?: TypeExportContext): JsonSchemaNode {
142+
const schema = type.getSchema();
143+
const baseSchema = getBaseJsonSchema(type, ctx);
144+
const value = schema.value;
145+
146+
if (typeof value === 'string') {
147+
const result: JsonSchemaString = {
148+
type: 'string',
149+
const: value,
150+
};
151+
Object.assign(result, baseSchema);
152+
return result;
153+
} else if (typeof value === 'number') {
154+
const result: JsonSchemaNumber = {
155+
type: 'number',
156+
const: value,
157+
};
158+
Object.assign(result, baseSchema);
159+
return result;
160+
} else if (typeof value === 'boolean') {
161+
const result: JsonSchemaBoolean = {
162+
type: 'boolean',
163+
const: value,
164+
};
165+
Object.assign(result, baseSchema);
166+
return result;
167+
} else if (value === null) {
168+
const result: any = {
169+
type: 'null',
170+
const: null,
171+
};
172+
Object.assign(result, baseSchema);
173+
return result;
174+
} else if (typeof value === 'undefined') {
175+
// For undefined values, we return a special schema
176+
const result: any = {
177+
type: 'undefined',
178+
const: undefined,
179+
};
180+
Object.assign(result, baseSchema);
181+
return result;
182+
} else if (Array.isArray(value)) {
183+
const result: JsonSchemaArray = {
184+
type: 'array',
185+
const: value,
186+
items: false,
187+
};
188+
Object.assign(result, baseSchema);
189+
return result;
190+
} else if (typeof value === 'object') {
191+
const result: JsonSchemaObject = {
192+
type: 'object',
193+
const: value,
194+
};
195+
Object.assign(result, baseSchema);
196+
return result;
197+
}
198+
199+
return baseSchema;
200+
}
201+
202+
function mapToJsonSchema(type: MapType<any>, ctx?: TypeExportContext): JsonSchemaObject {
203+
const baseSchema = getBaseJsonSchema(type, ctx);
204+
const result: JsonSchemaObject = {
205+
type: 'object',
206+
patternProperties: {
207+
'.*': typeToJsonSchema((type as any).type, ctx),
208+
},
209+
};
210+
211+
// Add base properties
212+
Object.assign(result, baseSchema);
213+
214+
return result;
215+
}
216+
217+
function numberToJsonSchema(type: NumberType, ctx?: TypeExportContext): JsonSchemaNumber {
218+
const schema = type.getSchema();
219+
const baseSchema = getBaseJsonSchema(type, ctx);
220+
const result: JsonSchemaNumber = {
221+
type: 'number',
222+
};
223+
224+
// Check if it's an integer format
225+
const ints = new Set(['i8', 'i16', 'i32', 'u8', 'u16', 'u32']);
226+
if (schema.format && ints.has(schema.format)) {
227+
result.type = 'integer';
228+
}
229+
230+
// Add base properties
231+
Object.assign(result, baseSchema);
232+
233+
if (schema.gt !== undefined) result.exclusiveMinimum = schema.gt;
234+
if (schema.gte !== undefined) result.minimum = schema.gte;
235+
if (schema.lt !== undefined) result.exclusiveMaximum = schema.lt;
236+
if (schema.lte !== undefined) result.maximum = schema.lte;
237+
238+
return result;
239+
}
240+
241+
function objectToJsonSchema(type: ObjectType<any>, ctx?: TypeExportContext): JsonSchemaObject {
242+
const schema = type.getSchema();
243+
const baseSchema = getBaseJsonSchema(type, ctx);
244+
const result: JsonSchemaObject = {
245+
type: 'object',
246+
properties: {},
247+
};
248+
249+
const required = [];
250+
const fields = (type as any).fields;
251+
for (const field of fields) {
252+
result.properties![field.key] = typeToJsonSchema(field.value, ctx);
253+
if (!field.constructor.name.includes('Optional')) {
254+
required.push(field.key);
255+
}
256+
}
257+
258+
if (required.length) result.required = required;
259+
if (schema.unknownFields === false) result.additionalProperties = false;
260+
261+
// Add base properties
262+
Object.assign(result, baseSchema);
263+
264+
return result;
265+
}
266+
267+
function orToJsonSchema(type: OrType<any>, ctx?: TypeExportContext): JsonSchemaOr {
268+
const baseSchema = getBaseJsonSchema(type, ctx);
269+
const types = (type as any).types;
270+
const result: JsonSchemaOr = {
271+
anyOf: types.map((t: any) => typeToJsonSchema(t, ctx)),
272+
};
273+
274+
// Add base properties
275+
Object.assign(result, baseSchema);
276+
277+
return result;
278+
}
279+
280+
function refToJsonSchema(type: RefType<any>, ctx?: TypeExportContext): JsonSchemaRef {
281+
const schema = type.getSchema();
282+
const baseSchema = getBaseJsonSchema(type, ctx);
283+
const ref = schema.ref;
284+
285+
if (ctx) ctx.mentionRef(ref);
286+
287+
const result: JsonSchemaRef = {
288+
$ref: `#/$defs/${ref}`,
289+
};
290+
291+
// Add base properties
292+
Object.assign(result, baseSchema);
293+
294+
return result;
295+
}
296+
297+
function stringToJsonSchema(type: StringType, ctx?: TypeExportContext): JsonSchemaString {
298+
const schema = type.getSchema();
299+
const baseSchema = getBaseJsonSchema(type, ctx);
300+
const result: JsonSchemaString = {
301+
type: 'string',
302+
};
303+
304+
if (schema.min !== undefined) result.minLength = schema.min;
305+
if (schema.max !== undefined) result.maxLength = schema.max;
306+
307+
// Add format to JSON Schema if specified
308+
if (schema.format) {
309+
if (schema.format === 'ascii') {
310+
// JSON Schema doesn't have an "ascii" format, but we can use a pattern
311+
// ASCII characters are from 0x00 to 0x7F (0-127)
312+
result.pattern = '^[\\x00-\\x7F]*$';
313+
}
314+
// UTF-8 is the default for JSON Schema strings, so we don't need to add anything special
315+
} else if (schema.ascii) {
316+
// Backward compatibility: if ascii=true, add pattern
317+
result.pattern = '^[\\x00-\\x7F]*$';
318+
}
319+
320+
// Add base properties
321+
Object.assign(result, baseSchema);
322+
323+
return result;
324+
}
325+
326+
function tupleToJsonSchema(type: TupleType<any>, ctx?: TypeExportContext): JsonSchemaArray {
327+
const baseSchema = getBaseJsonSchema(type, ctx);
328+
const types = (type as any).types;
329+
const result: JsonSchemaArray = {
330+
type: 'array',
331+
items: false,
332+
prefixItems: types.map((t: any) => typeToJsonSchema(t, ctx)),
333+
};
334+
335+
// Add base properties
336+
Object.assign(result, baseSchema);
337+
338+
return result;
339+
}

src/json-schema/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './types';
2+
export * from './converter';

src/system/TypeAlias.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {printTree} from 'tree-dump/lib/printTree';
22
import {ObjectType} from '../type/classes';
33
import {toText} from '../typescript/toText';
44
import type {JsonSchemaGenericKeywords, JsonSchemaValueNode} from '../json-schema';
5+
import {typeToJsonSchema} from '../json-schema';
56
import {TypeExportContext} from './TypeExportContext';
67
import type {TypeSystem} from '.';
78
import type {Type} from '../type';
@@ -61,11 +62,11 @@ export class TypeAlias<K extends string, T extends Type> implements Printable {
6162
};
6263
const ctx = new TypeExportContext();
6364
ctx.visitRef(this.id);
64-
node.$defs![this.id] = this.type.toJsonSchema(ctx) as JsonSchemaValueNode;
65+
node.$defs![this.id] = typeToJsonSchema(this.type, ctx) as JsonSchemaValueNode;
6566
let ref: string | undefined;
6667
while ((ref = ctx.nextMentionedRef())) {
6768
ctx.visitRef(ref);
68-
node.$defs![ref] = this.system.resolve(ref).type.toJsonSchema(ctx) as JsonSchemaValueNode;
69+
node.$defs![ref] = typeToJsonSchema(this.system.resolve(ref).type, ctx) as JsonSchemaValueNode;
6970
}
7071
return node;
7172
}

src/type/__tests__/getJsonSchema.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ test('can print a type', () => {
172172
},
173173
{
174174
"const": null,
175-
"type": "object",
175+
"type": "null",
176176
},
177177
],
178178
},

src/type/classes/AbstractType.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,9 @@ export abstract class AbstractType<S extends schema.Schema> implements BaseType<
8181
}
8282

8383
public toJsonSchema(ctx?: TypeExportContext): jsonSchema.JsonSchemaNode {
84-
const schema = this.getSchema();
85-
const jsonSchema = <jsonSchema.JsonSchemaGenericKeywords>{};
86-
if (schema.title) jsonSchema.title = schema.title;
87-
if (schema.description) jsonSchema.description = schema.description;
88-
if (schema.examples) jsonSchema.examples = schema.examples.map((example: schema.TExample) => example.value);
89-
return jsonSchema;
84+
// Use dynamic import to avoid circular dependency
85+
const converter = require('../../json-schema/converter');
86+
return converter.typeToJsonSchema(this, ctx);
9087
}
9188

9289
public options(options: schema.Optional<S>): this {

0 commit comments

Comments
 (0)