Skip to content

Commit

Permalink
Merge pull request #701 from hey-api/feat/prefix-items
Browse files Browse the repository at this point in the history
Feat/prefix items
  • Loading branch information
mrlubos authored Jun 21, 2024
2 parents 7415cc1 + 1081bbf commit 3ad6f72
Show file tree
Hide file tree
Showing 29 changed files with 408 additions and 66 deletions.
5 changes: 5 additions & 0 deletions .changeset/lazy-cheetahs-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hey-api/openapi-ts': patch
---

feat: add initial implementation of prefixItems
72 changes: 30 additions & 42 deletions packages/openapi-ts/src/compiler/typedef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
tsNodeToString,
} from './utils';

const nullNode = ts.factory.createTypeReferenceNode('null');

export const createTypeNode = (
base: any | ts.TypeNode,
args?: (any | ts.TypeNode)[],
Expand Down Expand Up @@ -62,6 +64,23 @@ export type Property = {
comment?: Comments;
};

/**
* Returns a union of provided node with null if marked as nullable,
* otherwise returns the provided node unmodified.
*/
const maybeNullable = ({
isNullable,
node,
}: {
node: ts.TypeNode;
isNullable: boolean;
}) => {
if (!isNullable) {
return node;
}
return ts.factory.createUnionTypeNode([node, nullNode]);
};

/**
* Create a interface type node. Example `{ readonly x: string, y?: number }`
* @param properties - the properties of the interface.
Expand Down Expand Up @@ -90,13 +109,7 @@ export const createTypeInterfaceNode = (
return signature;
}),
);
if (!isNullable) {
return node;
}
return ts.factory.createUnionTypeNode([
node,
ts.factory.createTypeReferenceNode('null'),
]);
return maybeNullable({ isNullable, node });
};

/**
Expand All @@ -110,10 +123,10 @@ export const createTypeUnionNode = (
isNullable: boolean = false,
) => {
const nodes = types.map((type) => createTypeNode(type));
if (isNullable) {
nodes.push(ts.factory.createTypeReferenceNode('null'));
if (!isNullable) {
return ts.factory.createUnionTypeNode(nodes);
}
return ts.factory.createUnionTypeNode(nodes);
return ts.factory.createUnionTypeNode([...nodes, nullNode]);
};

/**
Expand All @@ -126,15 +139,9 @@ export const createTypeIntersectNode = (
types: (any | ts.TypeNode)[],
isNullable: boolean = false,
) => {
const nodes = types.map((t) => createTypeNode(t));
const intersect = ts.factory.createIntersectionTypeNode(nodes);
if (isNullable) {
return ts.factory.createUnionTypeNode([
intersect,
ts.factory.createTypeReferenceNode('null'),
]);
}
return intersect;
const nodes = types.map((type) => createTypeNode(type));
const node = ts.factory.createIntersectionTypeNode(nodes);
return maybeNullable({ isNullable, node });
};

/**
Expand All @@ -151,15 +158,8 @@ export const createTypeTupleNode = ({
types: Array<any | ts.TypeNode>;
}) => {
const nodes = types.map((type) => createTypeNode(type));
const tupleNode = ts.factory.createTupleTypeNode(nodes);
if (isNullable) {
const unionNode = ts.factory.createUnionTypeNode([
tupleNode,
ts.factory.createTypeReferenceNode('null'),
]);
return unionNode;
}
return tupleNode;
const node = ts.factory.createTupleTypeNode(nodes);
return maybeNullable({ isNullable, node });
};

/**
Expand All @@ -186,13 +186,7 @@ export const createTypeRecordNode = (
type: valueNode,
},
]);
if (!isNullable) {
return node;
}
return ts.factory.createUnionTypeNode([
node,
ts.factory.createTypeReferenceNode('null'),
]);
return maybeNullable({ isNullable, node });
};

/**
Expand All @@ -208,11 +202,5 @@ export const createTypeArrayNode = (
const node = ts.factory.createTypeReferenceNode('Array', [
createTypeUnionNode(types),
]);
if (!isNullable) {
return node;
}
return ts.factory.createUnionTypeNode([
node,
ts.factory.createTypeReferenceNode('null'),
]);
return maybeNullable({ isNullable, node });
};
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export interface Model extends Schema {
| OpenApiParameter['in']
| OperationResponse['in']
| '';
link: Model | null;
link: Model | Model[] | null;
meta?: ModelMeta;
/**
* @deprecated use `meta.name` instead
Expand Down
15 changes: 14 additions & 1 deletion packages/openapi-ts/src/openApi/common/parser/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,20 @@ const areEqual = (a: Model, b: Model): boolean => {
const equal =
a.type === b.type && a.base === b.base && a.template === b.template;
if (equal && a.link && b.link) {
return areEqual(a.link, b.link);
if (!Array.isArray(a.link) && !Array.isArray(b.link)) {
return areEqual(a.link, b.link);
}

if (
Array.isArray(a.link) &&
Array.isArray(b.link) &&
a.link.length === b.link.length
) {
const bLinks = b.link;
return a.link.every((model, index) => areEqual(model, bLinks[index]!));
}

return false;
}
return equal;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface OpenApiSchema extends OpenApiReference, WithEnumExtension {
nullable?: boolean;
oneOf?: OpenApiSchema[];
pattern?: string;
prefixItems?: OpenApiSchema[];
properties?: Dictionary<OpenApiSchema>;
readOnly?: boolean;
required?: string[];
Expand Down
39 changes: 38 additions & 1 deletion packages/openapi-ts/src/openApi/v3/parser/getModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,44 @@ export const getModel = ({
}
}

if (definitionTypes.includes('array') && definition.items) {
if (
definitionTypes.includes('array') &&
(definition.items || definition.prefixItems)
) {
if (definition.prefixItems) {
const arrayItems = definition.prefixItems.map((item) =>
getModel({
definition: item,
openApi,
parentDefinition: definition,
types,
}),
);

model.export = 'array';
model.$refs = [
...model.$refs,
...arrayItems.reduce(
(acc, m) => [...acc, ...m.$refs],
[] as Model['$refs'],
),
];
model.imports = [
...model.imports,
...arrayItems.reduce(
(acc, m) => [...acc, ...m.imports],
[] as Model['imports'],
),
];
model.link = arrayItems;
model.default = getDefault(definition, model);
return model;
}

if (!definition.items) {
return model;
}

if (definition.items.$ref) {
const arrayItems = getType({ type: definition.items.$ref });
model.$refs = [...model.$refs, definition.items.$ref];
Expand Down
48 changes: 30 additions & 18 deletions packages/openapi-ts/src/utils/write/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,33 @@ const typeReference = (model: Model) => {
};

const typeArray = (model: Model) => {
// Special case where we use tuple to define constant size array.
if (
model.export === 'array' &&
model.link &&
model.maxItems &&
model.minItems &&
model.maxItems === model.minItems &&
model.maxItems <= 100
) {
const types = Array(model.maxItems).fill(toType(model.link));
const tuple = compiler.typedef.tuple({
isNullable: model.isNullable,
types,
});
return tuple;
}

if (model.link) {
// We treat an array of `model.link` as constant size array definition.
if (Array.isArray(model.link)) {
const types = model.link.map((m) => toType(m));
const tuple = compiler.typedef.tuple({
isNullable: model.isNullable,
types,
});
return tuple;
}

// Special case where we use tuple to define constant size array.
if (
model.export === 'array' &&
model.maxItems &&
model.minItems &&
model.maxItems === model.minItems &&
model.maxItems <= 100
) {
const types = Array(model.maxItems).fill(toType(model.link));
const tuple = compiler.typedef.tuple({
isNullable: model.isNullable,
types,
});
return tuple;
}

return compiler.typedef.array([toType(model.link)], model.isNullable);
}

Expand All @@ -62,7 +71,8 @@ const typeEnum = (model: Model) => {
};

const typeDict = (model: Model) => {
const type = model.link ? toType(model.link) : base(model);
const type =
model.link && !Array.isArray(model.link) ? toType(model.link) : base(model);
return compiler.typedef.record(['string'], [type], model.isNullable);
};

Expand Down Expand Up @@ -137,6 +147,8 @@ export const toType = (model: Model): TypeNode => {
return typeEnum(model);
case 'interface':
return typeInterface(model);
case 'const':
case 'generic':
case 'reference':
default:
return typeReference(model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,28 @@ export const $ModelWithAnyOfConstantSizeArray = {
maxItems: 3
} as const;

export const $ModelWithPrefixItemsConstantSizeArray = {
type: 'array',
prefixItems: [
{
'$ref': '#/components/schemas/ModelWithInteger'
},
{
oneOf: [
{
type: 'number'
},
{
type: 'string'
}
]
},
{
type: 'string'
}
]
} as const;

export const $ModelWithAnyOfConstantSizeArrayNullable = {
type: ['array'],
items: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,12 @@ export type ModelWithAnyOfConstantSizeArray = [
number | string
];

export type ModelWithPrefixItemsConstantSizeArray = [
ModelWithInteger,
number | string,
string
];

export type ModelWithAnyOfConstantSizeArrayNullable = [
number | null | string,
number | null | string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,28 @@ export const $ModelWithAnyOfConstantSizeArray = {
maxItems: 3
} as const;

export const $ModelWithPrefixItemsConstantSizeArray = {
type: 'array',
prefixItems: [
{
'$ref': '#/components/schemas/ModelWithInteger'
},
{
oneOf: [
{
type: 'number'
},
{
type: 'string'
}
]
},
{
type: 'string'
}
]
} as const;

export const $ModelWithAnyOfConstantSizeArrayNullable = {
type: ['array'],
items: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,12 @@ export type ModelWithAnyOfConstantSizeArray = [
number | string
];

export type ModelWithPrefixItemsConstantSizeArray = [
ModelWithInteger,
number | string,
string
];

export type ModelWithAnyOfConstantSizeArrayNullable = [
number | null | string,
number | null | string,
Expand Down
Loading

0 comments on commit 3ad6f72

Please sign in to comment.