Skip to content

Commit

Permalink
Expand JSON Schema coverage: arbitrary identifiers, better array type…
Browse files Browse the repository at this point in the history
… support.
  • Loading branch information
mkrause committed Jun 27, 2024
1 parent 9c8741c commit bcc4f6f
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 24 deletions.
25 changes: 19 additions & 6 deletions src/analysis/GraphAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ const depsShallow = (schema: OpenApiSchema): Set<Ref> => {
if ('$ref' in schema) { // Case: OpenApi.ReferenceObject
return new Set([schema.$ref]);
} else { // Case: OpenApi.SchemaObject
if ('items' in schema) { // Case: OpenApi.ArraySchemaObject
return depsShallow(schema.items);
if (schema.type === 'array' || 'items' in schema) { // Case: OpenApi.ArraySchemaObject
if ('items' in schema) {
return depsShallow(schema.items);
} else { // Array of unknown
return new Set();
}
} else { // Case: OpenApi.NonArraySchemaObject
if ('allOf' in schema && typeof schema.allOf !== 'undefined') {
return new Set(schema.allOf.flatMap(subschema => [...depsShallow(subschema)]));
Expand All @@ -57,6 +61,7 @@ const depsShallow = (schema: OpenApiSchema): Set<Ref> => {
}

switch (schema.type) {
case undefined: // Any type
case 'null':
case 'string':
case 'number':
Expand Down Expand Up @@ -86,8 +91,12 @@ const depsDeep = (schema: OpenApiSchema, resolve: Resolve, visited: Set<Ref>): D
if (visited.has(schema.$ref)) { return { [schema.$ref]: 'recurse' }; }
return { [schema.$ref]: depsDeep(resolve(schema.$ref), resolve, new Set([...visited, schema.$ref])) };
} else { // Case: OpenApi.SchemaObject
if ('items' in schema) { // Case: OpenApi.ArraySchemaObject
return depsDeep(schema.items, resolve, visited);
if (schema.type === 'array' || 'items' in schema) { // Case: OpenApi.ArraySchemaObject
if ('items' in schema) {
return depsDeep(schema.items, resolve, visited);
} else { // Array of unknown
return {};
}
} else { // Case: OpenApi.NonArraySchemaObject
if ('allOf' in schema && typeof schema.allOf !== 'undefined') {
return Object.assign({}, ...schema.allOf.flatMap(subschema => depsDeep(subschema, resolve, visited)));
Expand All @@ -98,6 +107,8 @@ const depsDeep = (schema: OpenApiSchema, resolve: Resolve, visited: Set<Ref>): D
}

switch (schema.type) {
case undefined: // Any type
return {};
case 'null':
case 'string':
case 'number':
Expand Down Expand Up @@ -249,8 +260,8 @@ const _isObjectSchema = (schema: OpenApiSchema, resolve: Resolve, visited: Set<R
}
return _isObjectSchema(resolve(schema.$ref), resolve, new Set([...visited, schema.$ref]));
} else { // Case: OpenApi.SchemaObject
if ('items' in schema) { // Case: OpenApi.ArraySchemaObject
return _isObjectSchema(schema.items, resolve, visited);
if (schema.type === 'array' || 'items' in schema) { // Case: OpenApi.ArraySchemaObject
return false;
} else { // Case: OpenApi.NonArraySchemaObject
if ('allOf' in schema && typeof schema.allOf !== 'undefined') {
return schema.allOf.flatMap(subschema => _isObjectSchema(subschema, resolve, visited)).every(Boolean);
Expand All @@ -261,6 +272,8 @@ const _isObjectSchema = (schema: OpenApiSchema, resolve: Resolve, visited: Set<R
}

switch (schema.type) {
case undefined: // Any type
return false; // Possibly an object, but we cannot know
case 'null':
case 'string':
case 'number':
Expand Down
39 changes: 21 additions & 18 deletions src/generation/effSchemGen/schemaGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,26 @@ import { type OpenAPIV3_1 as OpenApi } from 'openapi-types';
import { OpenApiSchemaId, type OpenApiRef, type OpenApiSchema } from '../../util/openapi.ts';

import * as GenSpec from '../generationSpec.ts';
import { isObjectSchema } from '../../analysis/GraphAnalyzer.ts';
import { isObjectSchema, schemaIdFromRef } from '../../analysis/GraphAnalyzer.ts';
import { type GenResult, GenResultUtil } from './genUtil.ts';


const id = GenResultUtil.encodeIdentifier;

export type Context = {
schemas: Record<string, OpenApiSchema>,
hooks: GenSpec.GenerationHooks,
isSchemaIdBefore: (schemaId: OpenApiSchemaId) => boolean,
};

export const generateForUnknownSchema = (ctx: Context, schema: OpenApi.NonArraySchemaObject): GenResult => {
return {
code: `S.Unknown`,
refs: [],
comments: GenResultUtil.commentsFromSchemaObject(schema),
};
};

export const generateForNullSchema = (ctx: Context, schema: OpenApi.NonArraySchemaObject): GenResult => {
return {
code: `S.Null`,
Expand All @@ -32,11 +42,11 @@ export const generateForStringSchema = (ctx: Context, schema: OpenApi.NonArraySc
let refs: GenResult['refs'] = [];
const code = ((): string => {
if (Array.isArray(schema.enum)) {
if (!schema.enum.every(value => typeof value === 'string')) {
if (!schema.enum.every(value => typeof value === 'string' || typeof value === 'number')) {
throw new TypeError(`Unknown enum value, expected string array: ${JSON.stringify(schema.enum)}`);
}
return dedent`S.Literal(
${schema.enum.map((value: string) => JSON.stringify(value) + ',').join('\n')}
${schema.enum.map((value: string | number) => JSON.stringify(String(value)) + ',').join('\n')}
)`;
}

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

export const generateForReferenceObject = (ctx: Context, schema: OpenApi.ReferenceObject): GenResult => {
// FIXME: make this logic customizable (allow a callback to resolve a `$ref` string to a `Ref` instance?)
const matches = schema.$ref.match(/^#\/components\/schemas\/([a-zA-Z0-9_$]+)/);
if (!matches) {
throw new Error(`Reference format not supported: ${schema.$ref}`);
}

const schemaId = matches[1];
if (typeof schemaId === 'undefined') { throw new Error('Should not happen'); }
const schemaId = schemaIdFromRef(schema.$ref);

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

return { code, refs: [`./${schemaId}.ts`], comments: GenResultUtil.initComments() };
};
Expand All @@ -317,8 +323,8 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
if ('$ref' in schema) { // Case: OpenApi.ReferenceObject
return generateForReferenceObject(ctx, schema);
} else { // Case: OpenApi.SchemaObject
if ('items' in schema && schema.type === 'array') { // Case: OpenApi.ArraySchemaObject
return generateForArraySchema(ctx, schema);
if (schema.type === 'array' || 'items' in schema) { // Case: OpenApi.ArraySchemaObject
return generateForArraySchema(ctx, { items: {}, ...schema } as OpenApi.ArraySchemaObject);
} else if (isNonArraySchemaType(schema)) { // Case: OpenApi.NonArraySchemaObject
if ('allOf' in schema && typeof schema.allOf !== 'undefined') {
const schemasHead: undefined | OpenApiSchema = schema.allOf[0];
Expand Down Expand Up @@ -483,7 +489,7 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
})
.join('\n')
}
);
)
`;
return {
code,
Expand All @@ -496,17 +502,14 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
type SchemaType = 'array' | OpenApi.NonArraySchemaObjectType;
const type: undefined | OpenApi.NonArraySchemaObjectType | Array<SchemaType> = schema.type;

if (typeof type === 'undefined') {
throw new TypeError(`Missing 'type' in schema`);
}

const hookResult: null | GenResult = ctx.hooks.generateSchema?.(schema) ?? null;

let result: GenResult;
if (hookResult !== null) {
result = hookResult;
} else {
switch (type) {
case undefined: result = generateForUnknownSchema(ctx, schema); break;
case 'null': result = generateForNullSchema(ctx, schema); break;
case 'string': result = generateForStringSchema(ctx, schema); break;
case 'number': result = generateForNumberSchema(ctx, schema); break;
Expand Down

0 comments on commit bcc4f6f

Please sign in to comment.