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 5, 2025
1 parent 8d563b5 commit e885b22
Show file tree
Hide file tree
Showing 95 changed files with 6,448 additions and 503 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
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
70 changes: 70 additions & 0 deletions packages/openapi-ts/src/openApi/2.0.x/parser/mediaType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { IRMediaType } from '../../../ir/mediaType';
import {
isMediaTypeFileLike,
mediaTypeToIrMediaType,
} from '../../../ir/mediaType';
import type {
ReferenceObject,
ResponseObject,
SchemaObject,
} from '../types/spec';

interface Content {
mediaType: string;
schema: SchemaObject | ReferenceObject | undefined;
type: IRMediaType | undefined;
}

export const contentToSchema = ({
content,
}: {
content: Content;
}): SchemaObject | undefined => {
const { mediaType, schema } = content;

if (schema && '$ref' in schema) {
return {
allOf: [{ ...schema }],
};
}

if (!schema) {
if (isMediaTypeFileLike({ mediaType })) {
return {
format: 'binary',
type: 'string',
};
}
return;
}

if (
schema.type === 'string' &&
!schema.format &&
isMediaTypeFileLike({ mediaType })
) {
return {
...schema,
format: 'binary',
};
}

return schema;
};

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

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/2.0.x/parser/mediaType.ts#L19-L53

Added lines #L19 - L53 were not covered by tests

export const mediaTypeObject = ({
mimeTypes,
response,
}: {
mimeTypes: ReadonlyArray<string> | undefined;
response: Pick<ResponseObject, 'schema'>;
}): Content | undefined => {
// return the first supported MIME type
for (const mediaType of mimeTypes ?? []) {
return {
mediaType,
schema: response.schema,
type: mediaTypeToIrMediaType({ mediaType }),
};
}
};

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

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/2.0.x/parser/mediaType.ts#L56-L70

Added lines #L56 - L70 were not covered by tests
Loading

0 comments on commit e885b22

Please sign in to comment.