Skip to content

Commit

Permalink
feat: add OpenAPI 2.0.x experimental parser
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlubos committed Jan 6, 2025
1 parent 8d563b5 commit 118f571
Show file tree
Hide file tree
Showing 99 changed files with 6,499 additions and 525 deletions.
5 changes: 5 additions & 0 deletions .changeset/nasty-comics-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hey-api/openapi-ts': patch
---

fix: preserve leading indicators in enum keys
2 changes: 1 addition & 1 deletion packages/openapi-ts/src/compiler/typedef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export const createTypeArrayNode = (
) => {
const node = createTypeReferenceNode({
typeArguments: [
// @ts-ignore
// @ts-expect-error
Array.isArray(types) ? createTypeUnionNode({ types }) : types,
],
typeName: 'Array',
Expand Down
41 changes: 34 additions & 7 deletions packages/openapi-ts/src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,12 +644,12 @@ export const createEnumDeclaration = <
}): ts.EnumDeclaration => {
const members: Array<ts.EnumMember> = Array.isArray(obj)
? obj.map((value) => {
const enumMember = ts.factory.createEnumMember(
escapeName(value.key),
toExpression({
const enumMember = createEnumMember({
initializer: toExpression({

Check warning on line 648 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L647-L648

Added lines #L647 - L648 were not covered by tests
value: value.value,
}),
);
name: value.key,
});

Check warning on line 652 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L651-L652

Added lines #L651 - L652 were not covered by tests

addLeadingComments({
comments: value.comments,
Expand All @@ -658,9 +658,15 @@ export const createEnumDeclaration = <

return enumMember;
})
: Object.entries(obj).map(([key, value]) => {
const initializer = toExpression({ unescape: true, value });
const enumMember = ts.factory.createEnumMember(key, initializer);
: // TODO: parser - deprecate object syntax
Object.entries(obj).map(([key, value]) => {
const enumMember = ts.factory.createEnumMember(
key,
toExpression({
unescape: true,
value,
}),
);

Check warning on line 669 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L661-L669

Added lines #L661 - L669 were not covered by tests

addLeadingComments({
comments: enumMemberComments[key],
Expand All @@ -684,6 +690,27 @@ export const createEnumDeclaration = <
return node;
};

const createEnumMember = ({
initializer,
name,
}: {
initializer?: ts.Expression;
name: string | ts.PropertyName;
}) => {
let key = name;
if (typeof key === 'string') {
if (key.startsWith("'") && key.endsWith("'")) {
key = createStringLiteral({
isSingleQuote: false,
text: key,
});
} else {
key = escapeName(key);
}
}
return ts.factory.createEnumMember(key, initializer);
};

Check warning on line 712 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L694-L712

Added lines #L694 - L712 were not covered by tests

/**
* Create namespace declaration. Example `export namespace MyNamespace { ... }`
* @param name - the name of the namespace.
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { sync } from 'cross-spawn';
import { generateLegacyOutput, generateOutput } from './generate/output';
import { ensureDirSync } from './generate/utils';
import type { IR } from './ir/types';
import { parseExperimental, parseLegacy } from './openApi';
import { parseLegacy, parseOpenApiSpec } from './openApi';
import type { ClientPlugins, UserPlugins } from './plugins';
import { defaultPluginConfigs } from './plugins';
import type {
Expand Down Expand Up @@ -619,7 +619,7 @@ export async function createClient(
!isLegacyClient(config) &&
!legacyNameFromConfig(config)
) {
context = parseExperimental({ config, spec: data });
context = parseOpenApiSpec({ config, spec: data });
}

// fallback to legacy parser
Expand Down
2 changes: 2 additions & 0 deletions packages/openapi-ts/src/openApi/2.0.x/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { parseV2_0_X } from './parser';
export type { OpenApiV2_0_X } from './types/spec';
239 changes: 239 additions & 0 deletions packages/openapi-ts/src/openApi/2.0.x/parser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import type { IR } from '../../../ir/types';
import { canProcessRef } from '../../shared/utils/filter';
import { mergeParametersObjects } from '../../shared/utils/parameter';
import type {
OpenApiV2_0_X,
OperationObject,
PathItemObject,
PathsObject,
SecuritySchemeObject,
} from '../types/spec';
import { parseOperation } from './operation';
import { parametersArrayToObject } from './parameter';
import { parseSchema } from './schema';

type PathKeys<T extends keyof PathsObject = keyof PathsObject> =
keyof T extends infer K ? (K extends `/${string}` ? K : never) : never;

export const parseV2_0_X = (context: IR.Context<OpenApiV2_0_X>) => {
const operationIds = new Map<string, string>();
const securitySchemesMap = new Map<string, SecuritySchemeObject>();

const excludeRegExp = context.config.input.exclude
? new RegExp(context.config.input.exclude)
: undefined;
const includeRegExp = context.config.input.include
? new RegExp(context.config.input.include)
: undefined;

const shouldProcessRef = ($ref: string) =>
canProcessRef({
$ref,
excludeRegExp,
includeRegExp,
});

// TODO: parser - support security schemas

if (context.spec.definitions) {
for (const name in context.spec.definitions) {
const $ref = `#/definitions/${name}`;
if (!shouldProcessRef($ref)) {
continue;
}

const schema = context.spec.definitions[name]!;

parseSchema({
$ref,
context,
schema,
});
}
}

for (const path in context.spec.paths) {
if (path.startsWith('x-')) {
continue;
}

const pathItem = context.spec.paths[path as PathKeys]!;

const finalPathItem = pathItem.$ref
? {
...context.resolveRef<PathItemObject>(pathItem.$ref),
...pathItem,
}
: pathItem;

const commonOperation: OperationObject = {
consumes: context.spec.consumes,
produces: context.spec.produces,
responses: {},
security: context.spec.security,
};
const operationArgs: Omit<Parameters<typeof parseOperation>[0], 'method'> =
{
context,
operation: {
...commonOperation,
id: '',
parameters: parametersArrayToObject({
context,
operation: commonOperation,
parameters: finalPathItem.parameters,
}),
},
operationIds,
path: path as PathKeys,
securitySchemesMap,
};

const $refDelete = `#/paths${path}/delete`;
if (finalPathItem.delete && shouldProcessRef($refDelete)) {
const parameters = mergeParametersObjects({
source: parametersArrayToObject({
context,
operation: finalPathItem.delete,
parameters: finalPathItem.delete.parameters,
}),
target: operationArgs.operation.parameters,
});
parseOperation({
...operationArgs,
method: 'delete',
operation: {
...operationArgs.operation,
...finalPathItem.delete,
parameters,
},
});
}

const $refGet = `#/paths${path}/get`;
if (finalPathItem.get && shouldProcessRef($refGet)) {
const parameters = mergeParametersObjects({
source: parametersArrayToObject({
context,
operation: finalPathItem.get,
parameters: finalPathItem.get.parameters,
}),
target: operationArgs.operation.parameters,
});
parseOperation({
...operationArgs,
method: 'get',
operation: {
...operationArgs.operation,
...finalPathItem.get,
parameters,
},
});
}

const $refHead = `#/paths${path}/head`;
if (finalPathItem.head && shouldProcessRef($refHead)) {
const parameters = mergeParametersObjects({
source: parametersArrayToObject({
context,
operation: finalPathItem.head,
parameters: finalPathItem.head.parameters,
}),
target: operationArgs.operation.parameters,
});
parseOperation({
...operationArgs,
method: 'head',
operation: {
...operationArgs.operation,
...finalPathItem.head,
parameters,
},
});
}

const $refOptions = `#/paths${path}/options`;
if (finalPathItem.options && shouldProcessRef($refOptions)) {
const parameters = mergeParametersObjects({
source: parametersArrayToObject({
context,
operation: finalPathItem.options,
parameters: finalPathItem.options.parameters,
}),
target: operationArgs.operation.parameters,
});
parseOperation({
...operationArgs,
method: 'options',
operation: {
...operationArgs.operation,
...finalPathItem.options,
parameters,
},
});
}

const $refPatch = `#/paths${path}/patch`;
if (finalPathItem.patch && shouldProcessRef($refPatch)) {
const parameters = mergeParametersObjects({
source: parametersArrayToObject({
context,
operation: finalPathItem.patch,
parameters: finalPathItem.patch.parameters,
}),
target: operationArgs.operation.parameters,
});
parseOperation({
...operationArgs,
method: 'patch',
operation: {
...operationArgs.operation,
...finalPathItem.patch,
parameters,
},
});
}

const $refPost = `#/paths${path}/post`;
if (finalPathItem.post && shouldProcessRef($refPost)) {
const parameters = mergeParametersObjects({
source: parametersArrayToObject({
context,
operation: finalPathItem.post,
parameters: finalPathItem.post.parameters,
}),
target: operationArgs.operation.parameters,
});
parseOperation({
...operationArgs,
method: 'post',
operation: {
...operationArgs.operation,
...finalPathItem.post,
parameters,
},
});
}

const $refPut = `#/paths${path}/put`;
if (finalPathItem.put && shouldProcessRef($refPut)) {
const parameters = mergeParametersObjects({
source: parametersArrayToObject({
context,
operation: finalPathItem.put,
parameters: finalPathItem.put.parameters,
}),
target: operationArgs.operation.parameters,
});
parseOperation({
...operationArgs,
method: 'put',
operation: {
...operationArgs.operation,
...finalPathItem.put,
parameters,
},
});
}
}
};

Check warning on line 239 in packages/openapi-ts/src/openApi/2.0.x/parser/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/2.0.x/parser/index.ts#L19-L239

Added lines #L19 - L239 were not covered by tests
Loading

0 comments on commit 118f571

Please sign in to comment.