Skip to content

Commit

Permalink
fix: handle nullable enums in experimental parser
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlubos committed Nov 8, 2024
1 parent de21dfd commit 61cd848
Show file tree
Hide file tree
Showing 13 changed files with 156 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/cool-shrimps-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hey-api/openapi-ts': patch
---

fix: handle nullable enums in experimental parser
46 changes: 30 additions & 16 deletions packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,36 +407,50 @@ const parseEnum = ({

for (const [index, enumValue] of schema.enum.entries()) {
const typeOfEnumValue = typeof enumValue;
let enumType: SchemaType | 'null' | undefined;

Check warning on line 411 in packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts#L410-L411

Added lines #L410 - L411 were not covered by tests
if (
typeOfEnumValue === 'string' ||
typeOfEnumValue === 'number' ||
typeOfEnumValue === 'boolean'
) {
const enumSchema = parseOneType({
context,
schema: {
description: schema['x-enum-descriptions']?.[index],
title:
schema['x-enum-varnames']?.[index] ??
schema['x-enumNames']?.[index],
type: typeOfEnumValue,
},
});
enumSchema.const = enumValue;
schemaItems.push(enumSchema);
enumType = typeOfEnumValue;
} else if (enumValue === null) {
// nullable must be true
if (schema.nullable) {
enumType = 'null';
}

Check warning on line 422 in packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts#L417-L422

Added lines #L417 - L422 were not covered by tests
} else {
console.warn(
'🚨',
`unhandled "${typeOfEnumValue}" typeof value "${enumValue}" for enum`,
schema.enum,
);
}
}

if (schema.nullable) {
schemaItems.push({
type: 'null',
if (!enumType) {
continue;
}

const enumSchema = parseOneType({
context,
schema: {
description: schema['x-enum-descriptions']?.[index],
title:
schema['x-enum-varnames']?.[index] ?? schema['x-enumNames']?.[index],
// cast enum to string temporarily
type: enumType === 'null' ? 'string' : enumType,
},

Check warning on line 443 in packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts#L431-L443

Added lines #L431 - L443 were not covered by tests
});

enumSchema.const = enumValue;

// cast enum back
if (enumType === 'null') {
enumSchema.type = enumType;
}

schemaItems.push(enumSchema);

Check warning on line 453 in packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts#L445-L453

Added lines #L445 - L453 were not covered by tests
}

irSchema = addItemsToSchema({
Expand Down
40 changes: 27 additions & 13 deletions packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,34 +443,48 @@ const parseEnum = ({
irSchema.type = 'enum';

const schemaItems: Array<IRSchemaObject> = [];
const schemaTypes = getSchemaTypes({ schema });

for (const [index, enumValue] of schema.enum.entries()) {
const typeOfEnumValue = typeof enumValue;
let enumType: SchemaType | undefined;

if (
typeOfEnumValue === 'string' ||
typeOfEnumValue === 'number' ||
typeOfEnumValue === 'boolean'
) {
schemaItems.push(
parseOneType({
context,
schema: {
const: enumValue,
description: schema['x-enum-descriptions']?.[index],
title:
schema['x-enum-varnames']?.[index] ??
schema['x-enumNames']?.[index],
type: typeOfEnumValue,
},
}),
);
enumType = typeOfEnumValue;
} else if (enumValue === null) {
// type must contain null
if (schemaTypes.includes('null')) {
enumType = 'null';
}

Check warning on line 462 in packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts#L459-L462

Added lines #L459 - L462 were not covered by tests
} else {
console.warn(
'🚨',
`unhandled "${typeOfEnumValue}" typeof value "${enumValue}" for enum`,
schema.enum,
);
}

if (!enumType) {
continue;
}

Check warning on line 473 in packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts#L472-L473

Added lines #L472 - L473 were not covered by tests

schemaItems.push(
parseOneType({
context,
schema: {
const: enumValue,
description: schema['x-enum-descriptions']?.[index],
title:
schema['x-enum-varnames']?.[index] ??
schema['x-enumNames']?.[index],
type: enumType,
},
}),
);
}

irSchema = addItemsToSchema({
Expand Down
15 changes: 5 additions & 10 deletions packages/openapi-ts/src/openApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,31 +75,26 @@ export const parseExperimental = ({
spec: spec as Record<string, any>,
});

// TODO: parser - handle Swagger 2.0

const ctx = context as IRContext<OpenApiV3_0_X | OpenApiV3_1_X>;
switch (ctx.spec.openapi) {
// TODO: parser - handle Swagger 2.0
case '3.0.0':
case '3.0.1':
case '3.0.2':
case '3.0.3':
case '3.0.4':
parseV3_0_X(context as IRContext<OpenApiV3_0_X>);
break;
return context;
case '3.1.0':
case '3.1.1':
parseV3_1_X(context as IRContext<OpenApiV3_1_X>);
break;
return context;
default:
// TODO: parser - uncomment after removing legacy parser.
// For now, we fall back to legacy parser if spec version
// is not supported
// throw new Error('Unsupported OpenAPI specification');
break;
}

if (!Object.keys(context.ir).length) {
return;
return;

Check warning on line 98 in packages/openapi-ts/src/openApi/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/index.ts#L98

Added line #L98 was not covered by tests
}

return context;
};
7 changes: 7 additions & 0 deletions packages/openapi-ts/test/3.0.x.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ describe(`OpenAPI ${VERSION}`, () => {
}),
description: 'escapes enum values',
},
{
config: createConfig({
input: 'enum-null.json',
output: 'enum-null',
}),
description: 'handles null enums',
},
];

it.each(scenarios)('$description', async ({ config }) => {
Expand Down
7 changes: 7 additions & 0 deletions packages/openapi-ts/test/3.1.x.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ describe(`OpenAPI ${VERSION}`, () => {
}),
description: 'escapes enum values',
},
{
config: createConfig({
input: 'enum-null.json',
output: 'enum-null',
}),
description: 'handles null enums',
},
{
config: createConfig({
input: 'object-properties-all-of.json',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts

export type Foo = 'foo' | 'bar' | null;

export type Bar = 'foo' | 'bar';

export type Baz = 'foo' | 'bar';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts

export type Foo = 'foo' | 'bar' | null;

export type Bar = 'foo' | 'bar';

export type Baz = 'foo' | 'bar';
18 changes: 9 additions & 9 deletions packages/openapi-ts/test/sample.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ const main = async () => {
// debug: true,
experimentalParser: true,
input: {
include:
'^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$',
path: './test/spec/3.1.x/full.json',
// include:
// '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$',
path: './test/spec/3.1.x/enum-null.json',
// path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json',
},
// name: 'foo',
Expand All @@ -27,12 +27,12 @@ const main = async () => {
// name: '@hey-api/schemas',
// type: 'json',
// },
{
// asClass: true,
// include...
name: '@hey-api/services',
// serviceNameBuilder: '^Parameters',
},
// {
// // asClass: true,
// // include...
// name: '@hey-api/services',
// // serviceNameBuilder: '^Parameters',
// },
// {
// dates: true,
// name: '@hey-api/transformers',
Expand Down
25 changes: 25 additions & 0 deletions packages/openapi-ts/test/spec/3.0.x/enum-null.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"openapi": "3.0.2",
"info": {
"title": "OpenAPI 3.0.2 enum null example",
"version": "1"
},
"components": {
"schemas": {
"Foo": {
"enum": ["foo", "bar", null],
"nullable": true,
"type": "string"
},
"Bar": {
"enum": ["foo", "bar", null],
"type": "string"
},
"Baz": {
"enum": ["foo", "bar"],
"nullable": true,
"type": "string"
}
}
}
}
23 changes: 23 additions & 0 deletions packages/openapi-ts/test/spec/3.1.x/enum-null.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"openapi": "3.1.0",
"info": {
"title": "OpenAPI 3.1.0 enum null example",
"version": "1"
},
"components": {
"schemas": {
"Foo": {
"enum": ["foo", "bar", null],
"type": ["string", "null"]
},
"Bar": {
"enum": ["foo", "bar", null],
"type": "string"
},
"Baz": {
"enum": ["foo", "bar"],
"type": ["string", "null"]
}
}
}
}

0 comments on commit 61cd848

Please sign in to comment.