Skip to content

Commit

Permalink
Merge pull request #533 from hey-api/fix/type-array-3-1
Browse files Browse the repository at this point in the history
fix(parser): handle type array
  • Loading branch information
mrlubos authored May 5, 2024
2 parents f57123b + ead22bf commit 7d3a897
Show file tree
Hide file tree
Showing 14 changed files with 99 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/poor-flies-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": patch
---

fix(parser): handle type array
28 changes: 18 additions & 10 deletions packages/openapi-ts/src/compiler/typedef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const createTypeUnionNode = (
types: (any | ts.TypeNode)[],
isNullable: boolean = false,
) => {
const nodes = types.map((t) => createTypeNode(t));
const nodes = types.map((type) => createTypeNode(type));
if (isNullable) {
nodes.push(ts.factory.createTypeReferenceNode('null'));
}
Expand Down Expand Up @@ -135,19 +135,27 @@ export const createTypeIntersectNode = (

/**
* Create type tuple node. Example `string, number, boolean`
* @param types - the types in the union
* @param isNullable - if the whole type can be null
* @param isNullable if the whole type can be null
* @param types the types in the union
* @returns ts.UnionTypeNode
*/
export const createTypeTupleNode = (
types: (any | ts.TypeNode)[],
isNullable: boolean = false,
) => {
const nodes = types.map((t) => createTypeNode(t));
export const createTypeTupleNode = ({
isNullable = false,
types,
}: {
isNullable?: boolean;
types: Array<any | ts.TypeNode>;
}) => {
const nodes = types.map((type) => createTypeNode(type));
const tupleNode = ts.factory.createTupleTypeNode(nodes);
if (isNullable) {
nodes.push(ts.factory.createTypeReferenceNode('null'));
const unionNode = ts.factory.createUnionTypeNode([
tupleNode,
ts.factory.createTypeReferenceNode('null'),
]);
return unionNode;
}
return ts.factory.createTupleTypeNode(nodes);
return tupleNode;
};

/**
Expand Down
7 changes: 6 additions & 1 deletion packages/openapi-ts/src/openApi/common/parser/getDefault.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Model } from '../../common/interfaces/client';
import type { OpenApiParameter } from '../../v2/interfaces/OpenApiParameter';
import type { OpenApiSchema } from '../../v3/interfaces/OpenApiSchema';
import { getDefinitionTypes } from '../../v3/parser/inferType';
import type { OperationParameter } from '../interfaces/client';

export const getDefault = (
Expand All @@ -11,7 +12,11 @@ export const getDefault = (
return definition.default;
}

const type = definition.type || typeof definition.default;
const definitionTypes = getDefinitionTypes(definition);

const type =
definitionTypes.find((type) => type !== 'null') ||
typeof definition.default;

switch (type) {
case 'int':
Expand Down
3 changes: 2 additions & 1 deletion packages/openapi-ts/src/openApi/common/parser/type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isDefinitionTypeNullable } from '../../v3/parser/inferType';
import type { Type } from '../interfaces/Type';
import { ensureValidTypeScriptJavaScriptIdentifier } from './sanitize';
import { stripNamespace } from './stripNamespace';
Expand Down Expand Up @@ -72,7 +73,7 @@ export const getType = (
.join(' | ');
result.type = joinedType;
result.base = joinedType;
result.isNullable = type.includes('null');
result.isNullable = isDefinitionTypeNullable({ type });
return result;
}

Expand Down
20 changes: 13 additions & 7 deletions packages/openapi-ts/src/openApi/v3/parser/getModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import {
getAdditionalPropertiesModel,
getModelProperties,
} from './getModelProperties';
import { inferType } from './inferType';
import {
getDefinitionTypes,
inferType,
isDefinitionNullable,
} from './inferType';

export const getModel = (
openApi: OpenApi,
Expand All @@ -22,7 +26,9 @@ export const getModel = (
name: string = '',
parentDefinition: OpenApiSchema | null = null,
): Model => {
const inferredType = inferType(definition);
const definitionTypes = getDefinitionTypes(definition);
const inferredType = inferType(definition, definitionTypes);

const model: Model = {
$refs: [],
base: 'unknown',
Expand All @@ -36,7 +42,7 @@ export const getModel = (
format: definition.format,
imports: [],
isDefinition,
isNullable: definition.nullable === true,
isNullable: isDefinitionNullable(definition),
isReadOnly: definition.readOnly === true,
isRequired: false,
link: null,
Expand Down Expand Up @@ -81,7 +87,7 @@ export const getModel = (
}
}

if (definition.type === 'array' && definition.items) {
if (definitionTypes.includes('array') && definition.items) {
if (definition.items.$ref) {
const arrayItems = getType(definition.items.$ref);
model.$refs = [...model.$refs, definition.items.$ref];
Expand All @@ -99,7 +105,7 @@ export const getModel = (
if (
foundComposition &&
foundComposition.definitions.some(
(definition) => definition.type !== 'array',
(definition) => !getDefinitionTypes(definition).includes('array'),
)
) {
return getModel(openApi, definition.items);
Expand Down Expand Up @@ -139,7 +145,7 @@ export const getModel = (
return { ...model, ...composition };
}

if (definition.type === 'object' || definition.properties) {
if (definitionTypes.includes('object') || definition.properties) {
if (definition.properties) {
model.base = 'unknown';
model.export = 'interface';
Expand Down Expand Up @@ -191,7 +197,7 @@ export const getModel = (
}

// If the schema has a type than it can be a basic or generic type.
if (definition.type) {
if (definitionTypes.length) {
const definitionType = getType(definition.type, definition.format);
model.base = definitionType.base;
model.export = 'generic';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
mapPropertyValue,
} from './discriminator';
import type { getModel } from './getModel';
import { isDefinitionNullable } from './inferType';

// Fix for circular dependency
export type GetModelFn = typeof getModel;
Expand Down Expand Up @@ -125,7 +126,7 @@ export const getModelProperties = (
enums: [],
export: 'reference',
imports: [],
isNullable: property.nullable === true,
isNullable: isDefinitionNullable(property),
link: null,
properties: [],
template: null,
Expand All @@ -141,7 +142,7 @@ export const getModelProperties = (
enums: [],
export: 'reference',
imports: model.imports,
isNullable: model.isNullable || property.nullable === true,
isNullable: model.isNullable || isDefinitionNullable(property),
link: null,
properties: [],
template: model.template,
Expand All @@ -158,7 +159,7 @@ export const getModelProperties = (
enums: model.enums,
export: model.export,
imports: model.imports,
isNullable: model.isNullable || property.nullable === true,
isNullable: model.isNullable || isDefinitionNullable(property),
link: model.link,
properties: model.properties,
template: model.template,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { OpenApi } from '../interfaces/OpenApi';
import type { OpenApiParameter } from '../interfaces/OpenApiParameter';
import type { OpenApiSchema } from '../interfaces/OpenApiSchema';
import { getModel } from './getModel';
import { isDefinitionNullable } from './inferType';

export const getOperationParameter = (
openApi: OpenApi,
Expand All @@ -24,7 +25,7 @@ export const getOperationParameter = (
imports: [],
in: parameter.in,
isDefinition: false,
isNullable: parameter.nullable === true,
isNullable: isDefinitionNullable(parameter),
isReadOnly: false,
isRequired: parameter.required === true,
link: null,
Expand Down
25 changes: 23 additions & 2 deletions packages/openapi-ts/src/openApi/v3/parser/inferType.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
import type { OpenApiSchema } from '../interfaces/OpenApiSchema';

export const inferType = (definition: OpenApiSchema) => {
if (definition.enum && definition.type !== 'boolean') {
export const inferType = (
definition: OpenApiSchema,
definitionTypes: string[],
) => {
if (definition.enum && !definitionTypes.includes('boolean')) {
return 'enum';
}
return undefined;
};

export const isDefinitionTypeNullable = (
definition: Pick<OpenApiSchema, 'type'>,
) => getDefinitionTypes(definition).includes('null');

export const isDefinitionNullable = (
definition: Pick<OpenApiSchema, 'nullable' | 'type'>,
) => definition.nullable === true || isDefinitionTypeNullable(definition);

export const getDefinitionTypes = ({ type }: Pick<OpenApiSchema, 'type'>) => {
if (Array.isArray(type)) {
return type;
}
if (type) {
return [type];
}
return [];
};
21 changes: 17 additions & 4 deletions packages/openapi-ts/src/utils/write/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,31 @@ import { unique } from '../unique';

const base = (model: Model) => {
const config = getConfig();

if (model.base === 'binary') {
return compiler.typedef.union(['Blob', 'File']);
}

if (config.types.dates && model.format === 'date-time') {
return compiler.typedef.basic('Date');
}

// transform root level model names
if (model.base === model.type && model.$refs.length) {
if (model.$refs.some((ref) => ref.endsWith(model.base))) {
return compiler.typedef.basic(transformTypeName(model.base));
}
}

return compiler.typedef.basic(model.base);
};

const typeReference = (model: Model) =>
compiler.typedef.union([base(model)], model.isNullable);
const typeReference = (model: Model) => {
// nullable is false when base is null to avoid duplicate null statements
const isNullable = model.base === 'null' ? false : model.isNullable;
const unionNode = compiler.typedef.union([base(model)], isNullable);
return unionNode;
};

const typeArray = (model: Model) => {
// Special case where we use tuple to define constant size array.
Expand All @@ -38,7 +46,10 @@ const typeArray = (model: Model) => {
model.maxItems <= 100
) {
const types = Array(model.maxItems).fill(toType(model.link));
const tuple = compiler.typedef.tuple(types, model.isNullable);
const tuple = compiler.typedef.tuple({
isNullable: model.isNullable,
types,
});
return tuple;
}

Expand All @@ -62,7 +73,9 @@ const typeDict = (model: Model) => {
const typeUnion = (model: Model) => {
const models = model.properties;
const types = models
.map((m) => compiler.utils.toString({ node: toType(m), unescape: true }))
.map((model) =>
compiler.utils.toString({ node: toType(model), unescape: true }),
)
.filter(unique);
return compiler.typedef.union(types, model.isNullable);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1390,8 +1390,7 @@ export const $CompositionWithOneOfAndProperties = {
} as const;

export const $NullableObject = {
type: 'object',
nullable: true,
type: ['object', 'null'],
description: 'An object that can be null',
properties: {
foo: {
Expand Down Expand Up @@ -1592,7 +1591,7 @@ export const $ModelWithAnyOfConstantSizeArray = {
} as const;

export const $ModelWithAnyOfConstantSizeArrayNullable = {
type: 'array',
type: ['array'],
items: {
oneOf: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1280,8 +1280,7 @@ export const $CompositionWithOneOfAndProperties = {
} as const;

export const $NullableObject = {
type: 'object',
nullable: true,
type: ['object', 'null'],
properties: {
foo: {
type: 'string'
Expand Down Expand Up @@ -1480,7 +1479,7 @@ export const $ModelWithAnyOfConstantSizeArray = {
} as const;

export const $ModelWithAnyOfConstantSizeArrayNullable = {
type: 'array',
type: ['array'],
items: {
oneOf: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1390,8 +1390,7 @@ export const $CompositionWithOneOfAndProperties = {
} as const;

export const $NullableObject = {
type: 'object',
nullable: true,
type: ['object', 'null'],
description: 'An object that can be null',
properties: {
foo: {
Expand Down Expand Up @@ -1592,7 +1591,7 @@ export const $ModelWithAnyOfConstantSizeArray = {
} as const;

export const $ModelWithAnyOfConstantSizeArrayNullable = {
type: 'array',
type: ['array'],
items: {
oneOf: [
{
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-ts/test/sample.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const main = async () => {
// export: false,
},
types: {
// include: '^DictionaryWithPropertiesAndAdditionalProperties',
// include: '^NestedAnyOfArraysNullable',
},
};

Expand Down
5 changes: 2 additions & 3 deletions packages/openapi-ts/test/spec/v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -2969,8 +2969,7 @@
}
},
"NullableObject": {
"type": "object",
"nullable": true,
"type": ["object", "null"],
"description": "An object that can be null",
"properties": {
"foo": {
Expand Down Expand Up @@ -3159,7 +3158,7 @@
"maxItems": 3
},
"ModelWithAnyOfConstantSizeArrayNullable": {
"type": "array",
"type": ["array"],
"items": {
"oneOf": [
{
Expand Down

0 comments on commit 7d3a897

Please sign in to comment.