Skip to content

Commit eba7d04

Browse files
authored
Merge pull request #3 from fortanix/feature/expand-json-schema-coverage
Expand JSON Schema coverage: arbitrary identifiers, better array type support
2 parents 9c8741c + bcc4f6f commit eba7d04

File tree

2 files changed

+40
-24
lines changed

2 files changed

+40
-24
lines changed

Diff for: src/analysis/GraphAnalyzer.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,12 @@ const depsShallow = (schema: OpenApiSchema): Set<Ref> => {
4545
if ('$ref' in schema) { // Case: OpenApi.ReferenceObject
4646
return new Set([schema.$ref]);
4747
} else { // Case: OpenApi.SchemaObject
48-
if ('items' in schema) { // Case: OpenApi.ArraySchemaObject
49-
return depsShallow(schema.items);
48+
if (schema.type === 'array' || 'items' in schema) { // Case: OpenApi.ArraySchemaObject
49+
if ('items' in schema) {
50+
return depsShallow(schema.items);
51+
} else { // Array of unknown
52+
return new Set();
53+
}
5054
} else { // Case: OpenApi.NonArraySchemaObject
5155
if ('allOf' in schema && typeof schema.allOf !== 'undefined') {
5256
return new Set(schema.allOf.flatMap(subschema => [...depsShallow(subschema)]));
@@ -57,6 +61,7 @@ const depsShallow = (schema: OpenApiSchema): Set<Ref> => {
5761
}
5862

5963
switch (schema.type) {
64+
case undefined: // Any type
6065
case 'null':
6166
case 'string':
6267
case 'number':
@@ -86,8 +91,12 @@ const depsDeep = (schema: OpenApiSchema, resolve: Resolve, visited: Set<Ref>): D
8691
if (visited.has(schema.$ref)) { return { [schema.$ref]: 'recurse' }; }
8792
return { [schema.$ref]: depsDeep(resolve(schema.$ref), resolve, new Set([...visited, schema.$ref])) };
8893
} else { // Case: OpenApi.SchemaObject
89-
if ('items' in schema) { // Case: OpenApi.ArraySchemaObject
90-
return depsDeep(schema.items, resolve, visited);
94+
if (schema.type === 'array' || 'items' in schema) { // Case: OpenApi.ArraySchemaObject
95+
if ('items' in schema) {
96+
return depsDeep(schema.items, resolve, visited);
97+
} else { // Array of unknown
98+
return {};
99+
}
91100
} else { // Case: OpenApi.NonArraySchemaObject
92101
if ('allOf' in schema && typeof schema.allOf !== 'undefined') {
93102
return Object.assign({}, ...schema.allOf.flatMap(subschema => depsDeep(subschema, resolve, visited)));
@@ -98,6 +107,8 @@ const depsDeep = (schema: OpenApiSchema, resolve: Resolve, visited: Set<Ref>): D
98107
}
99108

100109
switch (schema.type) {
110+
case undefined: // Any type
111+
return {};
101112
case 'null':
102113
case 'string':
103114
case 'number':
@@ -249,8 +260,8 @@ const _isObjectSchema = (schema: OpenApiSchema, resolve: Resolve, visited: Set<R
249260
}
250261
return _isObjectSchema(resolve(schema.$ref), resolve, new Set([...visited, schema.$ref]));
251262
} else { // Case: OpenApi.SchemaObject
252-
if ('items' in schema) { // Case: OpenApi.ArraySchemaObject
253-
return _isObjectSchema(schema.items, resolve, visited);
263+
if (schema.type === 'array' || 'items' in schema) { // Case: OpenApi.ArraySchemaObject
264+
return false;
254265
} else { // Case: OpenApi.NonArraySchemaObject
255266
if ('allOf' in schema && typeof schema.allOf !== 'undefined') {
256267
return schema.allOf.flatMap(subschema => _isObjectSchema(subschema, resolve, visited)).every(Boolean);
@@ -261,6 +272,8 @@ const _isObjectSchema = (schema: OpenApiSchema, resolve: Resolve, visited: Set<R
261272
}
262273

263274
switch (schema.type) {
275+
case undefined: // Any type
276+
return false; // Possibly an object, but we cannot know
264277
case 'null':
265278
case 'string':
266279
case 'number':

Diff for: src/generation/effSchemGen/schemaGen.ts

+21-18
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,26 @@ import { type OpenAPIV3_1 as OpenApi } from 'openapi-types';
1010
import { OpenApiSchemaId, type OpenApiRef, type OpenApiSchema } from '../../util/openapi.ts';
1111

1212
import * as GenSpec from '../generationSpec.ts';
13-
import { isObjectSchema } from '../../analysis/GraphAnalyzer.ts';
13+
import { isObjectSchema, schemaIdFromRef } from '../../analysis/GraphAnalyzer.ts';
1414
import { type GenResult, GenResultUtil } from './genUtil.ts';
1515

1616

17+
const id = GenResultUtil.encodeIdentifier;
18+
1719
export type Context = {
1820
schemas: Record<string, OpenApiSchema>,
1921
hooks: GenSpec.GenerationHooks,
2022
isSchemaIdBefore: (schemaId: OpenApiSchemaId) => boolean,
2123
};
2224

25+
export const generateForUnknownSchema = (ctx: Context, schema: OpenApi.NonArraySchemaObject): GenResult => {
26+
return {
27+
code: `S.Unknown`,
28+
refs: [],
29+
comments: GenResultUtil.commentsFromSchemaObject(schema),
30+
};
31+
};
32+
2333
export const generateForNullSchema = (ctx: Context, schema: OpenApi.NonArraySchemaObject): GenResult => {
2434
return {
2535
code: `S.Null`,
@@ -32,11 +42,11 @@ export const generateForStringSchema = (ctx: Context, schema: OpenApi.NonArraySc
3242
let refs: GenResult['refs'] = [];
3343
const code = ((): string => {
3444
if (Array.isArray(schema.enum)) {
35-
if (!schema.enum.every(value => typeof value === 'string')) {
45+
if (!schema.enum.every(value => typeof value === 'string' || typeof value === 'number')) {
3646
throw new TypeError(`Unknown enum value, expected string array: ${JSON.stringify(schema.enum)}`);
3747
}
3848
return dedent`S.Literal(
39-
${schema.enum.map((value: string) => JSON.stringify(value) + ',').join('\n')}
49+
${schema.enum.map((value: string | number) => JSON.stringify(String(value)) + ',').join('\n')}
4050
)`;
4151
}
4252

@@ -293,17 +303,13 @@ export const generateForArraySchema = (ctx: Context, schema: OpenApi.ArraySchema
293303

294304
export const generateForReferenceObject = (ctx: Context, schema: OpenApi.ReferenceObject): GenResult => {
295305
// FIXME: make this logic customizable (allow a callback to resolve a `$ref` string to a `Ref` instance?)
296-
const matches = schema.$ref.match(/^#\/components\/schemas\/([a-zA-Z0-9_$]+)/);
297-
if (!matches) {
298-
throw new Error(`Reference format not supported: ${schema.$ref}`);
299-
}
300-
301-
const schemaId = matches[1];
302-
if (typeof schemaId === 'undefined') { throw new Error('Should not happen'); }
306+
const schemaId = schemaIdFromRef(schema.$ref);
303307

304308
// If the referenced schema ID is topologically after the current one, wrap it in `S.suspend` for lazy eval
305309
const shouldSuspend = !ctx.isSchemaIdBefore(schemaId);
306-
const code = shouldSuspend ? `S.suspend((): S.Schema<_${schemaId}, _${schemaId}Encoded> => ${schemaId})` : schemaId;
310+
const code = shouldSuspend
311+
? `S.suspend((): S.Schema<_${id(schemaId)}, _${id(schemaId)}Encoded> => ${id(schemaId)})`
312+
: id(schemaId);
307313

308314
return { code, refs: [`./${schemaId}.ts`], comments: GenResultUtil.initComments() };
309315
};
@@ -317,8 +323,8 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
317323
if ('$ref' in schema) { // Case: OpenApi.ReferenceObject
318324
return generateForReferenceObject(ctx, schema);
319325
} else { // Case: OpenApi.SchemaObject
320-
if ('items' in schema && schema.type === 'array') { // Case: OpenApi.ArraySchemaObject
321-
return generateForArraySchema(ctx, schema);
326+
if (schema.type === 'array' || 'items' in schema) { // Case: OpenApi.ArraySchemaObject
327+
return generateForArraySchema(ctx, { items: {}, ...schema } as OpenApi.ArraySchemaObject);
322328
} else if (isNonArraySchemaType(schema)) { // Case: OpenApi.NonArraySchemaObject
323329
if ('allOf' in schema && typeof schema.allOf !== 'undefined') {
324330
const schemasHead: undefined | OpenApiSchema = schema.allOf[0];
@@ -483,7 +489,7 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
483489
})
484490
.join('\n')
485491
}
486-
);
492+
)
487493
`;
488494
return {
489495
code,
@@ -496,17 +502,14 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
496502
type SchemaType = 'array' | OpenApi.NonArraySchemaObjectType;
497503
const type: undefined | OpenApi.NonArraySchemaObjectType | Array<SchemaType> = schema.type;
498504

499-
if (typeof type === 'undefined') {
500-
throw new TypeError(`Missing 'type' in schema`);
501-
}
502-
503505
const hookResult: null | GenResult = ctx.hooks.generateSchema?.(schema) ?? null;
504506

505507
let result: GenResult;
506508
if (hookResult !== null) {
507509
result = hookResult;
508510
} else {
509511
switch (type) {
512+
case undefined: result = generateForUnknownSchema(ctx, schema); break;
510513
case 'null': result = generateForNullSchema(ctx, schema); break;
511514
case 'string': result = generateForStringSchema(ctx, schema); break;
512515
case 'number': result = generateForNumberSchema(ctx, schema); break;

0 commit comments

Comments
 (0)