Skip to content

Commit

Permalink
Add support for OpenAPI v3.1 'type' array syntax.
Browse files Browse the repository at this point in the history
  • Loading branch information
mkrause committed Jul 4, 2024
1 parent f76e261 commit f493e9d
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 26 deletions.
23 changes: 18 additions & 5 deletions src/analysis/GraphAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@ const depsShallow = (schema: OpenApiSchema): Set<Ref> => {
return new Set(schema.anyOf.flatMap(subschema => [...depsShallow(subschema)]));
}

if (typeof schema.type === 'undefined') {
return new Set(); // Any type
} else if (Array.isArray(schema.type)) {
const schemaAnyOf = { anyOf: schema.type.map(type => ({ ...schema, type })) } as OpenApiSchema;
return depsShallow(schemaAnyOf);
}
switch (schema.type) {
case undefined: // Any type
case 'null':
case 'string':
case 'number':
Expand Down Expand Up @@ -106,9 +111,13 @@ const depsDeep = (schema: OpenApiSchema, resolve: Resolve, visited: Set<Ref>): D
return Object.assign({}, ...schema.anyOf.flatMap(subschema => depsDeep(subschema, resolve, visited)));
}

if (typeof schema.type === 'undefined') {
return {}; // Any type
} else if (Array.isArray(schema.type)) {
const schemaAnyOf = { anyOf: schema.type.map(type => ({ ...schema, type })) } as OpenApiSchema;
return depsDeep(schemaAnyOf, resolve, visited);
}
switch (schema.type) {
case undefined: // Any type
return {};
case 'null':
case 'string':
case 'number':
Expand Down Expand Up @@ -271,9 +280,13 @@ const _isObjectSchema = (schema: OpenApiSchema, resolve: Resolve, visited: Set<R
return false; // Union
}

if (typeof schema.type === 'undefined') {
return false; // Any type
} else if (Array.isArray(schema.type)) {
const schemaAnyOf = { anyOf: schema.type.map(type => ({ ...schema, type })) } as OpenApiSchema;
return _isObjectSchema(schemaAnyOf, resolve, visited);
}
switch (schema.type) {
case undefined: // Any type
return false; // Possibly an object, but we cannot know
case 'null':
case 'string':
case 'number':
Expand Down
32 changes: 19 additions & 13 deletions src/generation/effSchemGen/schemaGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
} else { // Case: OpenApi.SchemaObject
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
} else { // Case: OpenApi.NonArraySchemaObject | OpenApi.MixedSchemaObject
if ('allOf' in schema && typeof schema.allOf !== 'undefined') {
const schemasHead: undefined | OpenApiSchema = schema.allOf[0];
if (schemasHead && schema.allOf.length === 1) { // If only one schema, simply generate that schema
Expand Down Expand Up @@ -485,7 +485,7 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
return dedent`
${commentsGenerated.commentBlock}
${code}, ${commentsGenerated.commentInline}
`;
`.trim();
})
.join('\n')
}
Expand All @@ -508,23 +508,29 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
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;
case 'integer': result = generateForNumberSchema(ctx, schema); break;
case 'boolean': result = generateForBooleanSchema(ctx, schema); break;
case 'object': result = generateForObjectSchema(ctx, schema); break;
default: throw new TypeError(`Unsupported type "${type}"`);
if (typeof type === 'undefined') {
result = generateForUnknownSchema(ctx, schema as OpenApi.NonArraySchemaObject); // Any type
} else if (Array.isArray(type)) {
// `type` as an array is equivalent to `anyOf` with `type` set to the individual type string
const schemaAnyOf = { anyOf: type.map(type => ({ ...schema, type })) } as OpenApiSchema;
result = generateForSchema(ctx, schemaAnyOf);
} else {
const schemaNonMixed = schema as OpenApi.NonArraySchemaObject;
switch (type) {
case 'null': result = generateForNullSchema(ctx, schemaNonMixed); break;
case 'string': result = generateForStringSchema(ctx, schemaNonMixed); break;
case 'number': result = generateForNumberSchema(ctx, schemaNonMixed); break;
case 'integer': result = generateForNumberSchema(ctx, schemaNonMixed); break;
case 'boolean': result = generateForBooleanSchema(ctx, schemaNonMixed); break;
case 'object': result = generateForObjectSchema(ctx, schemaNonMixed); break;
default: throw new TypeError(`Unsupported type "${type}"`);
}
}
}
return {
...result,
code: `${result.code}`,
};
} else { // Case: OpenApi.MixedSchemaObject
throw new Error(`Currently unsupported: MixedSchemaObject`);
}
}
};
3 changes: 1 addition & 2 deletions src/openapiToEffect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,8 @@ export const run = async (argsRaw: Array<string>): Promise<void> => {
});
};

const [_argExec, argScript, ...args] = process.argv; // First two arguments should be the executable + script

// Detect if this module is being run directly from the command line
const [_argExec, argScript, ...args] = process.argv; // First two arguments should be the executable + script
if (argScript && await fs.realpath(argScript) === fileURLToPath(import.meta.url)) {
try {
await run(args);
Expand Down
7 changes: 5 additions & 2 deletions tests/fixtures/fixture0_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": ["null", "string"] },
"status": { "type": ["null", "string"], "enum": ["ACTIVE", "DEPRIORITIZED"] },
"subcategories": {
"type": "object",
"additionalProperties": {
Expand All @@ -18,7 +20,7 @@
"default": {}
}
},
"required": ["name"]
"required": ["name", "description"]
},
"User": {
"type": "object",
Expand All @@ -34,7 +36,8 @@
},
"last_logged_in": {
"title": "When the user last logged in.",
"type": "string", "format": "date-time"
"type": "string",
"format": "date-time"
},
"role": {
"title": "The user's role within the system.",
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/fixture0_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ export default {
schemaId: 'Category',
typeDeclarationEncoded: `{
readonly name: string,
readonly description: null | string,
readonly status?: undefined | null | 'ACTIVE' | 'DEPRIORITIZED',
readonly subcategories?: undefined | { readonly [key: string]: _CategoryEncoded }
}`,
typeDeclaration: `{
readonly name: string,
readonly description: null | string,
readonly status?: undefined | null | 'ACTIVE' | 'DEPRIORITIZED',
readonly subcategories: { readonly [key: string]: _Category }
}`,
},
Expand Down
14 changes: 11 additions & 3 deletions tests/integration/fixture0.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ test('fixture0', { timeout: 30_000/*ms*/ }, async (t) => {
const before = async () => {
const cwd = path.dirname(fileURLToPath(import.meta.url));
console.log('Preparing fixture0...');
const { stdout, stderr } = await exec(`./generate_fixture.sh fixture0`, { cwd });

try {
const { stdout, stderr } = await exec(`./generate_fixture.sh fixture0`, { cwd });
} catch (error: unknown) {
if (error instanceof Error && 'stderr' in error) {
console.error(error.stderr);
}
throw error;
}
};
await before();

Expand All @@ -36,14 +44,14 @@ test('fixture0', { timeout: 30_000/*ms*/ }, async (t) => {
last_logged_in: '2024-05-25T19:20:39.482Z',
role: 'USER',
interests: [
{ name: 'Music' },
{ name: 'Music', description: null },
],
};
assert.deepStrictEqual(S.decodeUnknownSync(fixture.User)(user1), {
...user1,
last_logged_in: new Date('2024-05-25T19:20:39.482Z'), // Transformed to Date
interests: [
{ name: 'Music', subcategories: {} }, // Added default value
{ name: 'Music', description: null, subcategories: {} }, // Added default value
],
});
});
Expand Down
10 changes: 9 additions & 1 deletion tests/integration/fixture1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ test('fixture1', { timeout: 30_000/*ms*/ }, async (t) => {
const before = async () => {
const cwd = path.dirname(fileURLToPath(import.meta.url));
console.log('Preparing fixture1...');
const { stdout, stderr } = await exec(`./generate_fixture.sh fixture1`, { cwd });

try {
const { stdout, stderr } = await exec(`./generate_fixture.sh fixture1`, { cwd });
} catch (error: unknown) {
if (error instanceof Error && 'stderr' in error) {
console.error(error.stderr);
}
throw error;
}
};
await before();

Expand Down

0 comments on commit f493e9d

Please sign in to comment.