Skip to content

Commit

Permalink
fix: handle 1XX response status codes
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlubos committed May 27, 2024
1 parent 7014e44 commit 0d38bed
Show file tree
Hide file tree
Showing 27 changed files with 538 additions and 426 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-deers-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hey-api/openapi-ts': patch
---

fix: handle 1XX response status codes
6 changes: 5 additions & 1 deletion packages/openapi-ts/src/openApi/common/interfaces/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface OperationParameters extends Pick<Model, '$refs' | 'imports'> {

export interface OperationResponse extends Model {
in: 'header' | 'response';
code: number | 'default';
code: number | 'default' | '1XX' | '2XX' | '3XX' | '4XX' | '5XX';
}

export type Method =
Expand All @@ -55,6 +55,10 @@ export interface Operation extends OperationParameters {
name: string;
path: string;
responseHeader: string | null;
/**
* All operation responses defined in OpenAPI specification.
* Sorted by status code.
*/
results: OperationResponse[];
/**
* Service name, might be without postfix. This will be used to name the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';

import { setConfig } from '../../../../utils/config';
import { getOperationName, getOperationResponseCode } from '../operation';
import { getOperationName, parseResponseStatusCode } from '../operation';

describe('getOperationName', () => {
const options1: Parameters<typeof setConfig>[0] = {
Expand Down Expand Up @@ -221,16 +221,17 @@ describe('getOperationName', () => {
);
});

describe('getOperationResponseCode', () => {
describe('parseResponseStatusCode', () => {
it.each([
{ expected: null, input: '' },
{ expected: 'default', input: 'default' },
{ expected: 200, input: '200' },
{ expected: 300, input: '300' },
{ expected: 400, input: '400' },
{ expected: '4XX', input: '4XX' },
{ expected: null, input: 'abc' },
{ expected: 100, input: '-100' },
])('getOperationResponseCode($input) -> $expected', ({ input, expected }) => {
expect(getOperationResponseCode(input)).toBe(expected);
{ expected: null, input: '-100' },
])('parseResponseStatusCode($input) -> $expected', ({ input, expected }) => {
expect(parseResponseStatusCode(input)).toBe(expected);
});
});
89 changes: 74 additions & 15 deletions packages/openapi-ts/src/openApi/common/parser/operation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import camelCase from 'camelcase';

import { getConfig } from '../../../utils/config';
import type { OperationResponse } from '../interfaces/client';
import type { Model, OperationResponse } from '../interfaces/client';
import { sanitizeNamespaceIdentifier } from './sanitize';

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);
}
return equal;
};

/**
* Convert the input value to a correct operation (method) class name.
* This will use the operation ID - if available - and otherwise fallback
Expand Down Expand Up @@ -40,28 +49,78 @@ export const getOperationResponseHeader = (
return null;
};

export const getOperationResponseCode = (
value: string | 'default',
): number | 'default' | null => {
/**
* Attempts to parse response status code from string into number.
* @param value string status code from OpenAPI definition
* @returns Parsed status code or null if invalid value
*/
export const parseResponseStatusCode = (
value: string,
): OperationResponse['code'] | null => {
if (value === 'default') {
return 'default';
}

// Check if we can parse the code and return of successful.
if (/[0-9]+/g.test(value)) {
const code = Number.parseInt(value);
if (Number.isInteger(code)) {
return Math.abs(code);
if (value === '1XX') {
return '1XX';

Check warning on line 65 in packages/openapi-ts/src/openApi/common/parser/operation.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/common/parser/operation.ts#L65

Added line #L65 was not covered by tests
}

if (value === '2XX') {
return '2XX';

Check warning on line 69 in packages/openapi-ts/src/openApi/common/parser/operation.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/common/parser/operation.ts#L69

Added line #L69 was not covered by tests
}

if (value === '3XX') {
return '3XX';

Check warning on line 73 in packages/openapi-ts/src/openApi/common/parser/operation.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/common/parser/operation.ts#L73

Added line #L73 was not covered by tests
}

if (value === '4XX') {
return '4XX';
}

if (value === '5XX') {
return '5XX';

Check warning on line 81 in packages/openapi-ts/src/openApi/common/parser/operation.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/common/parser/operation.ts#L81

Added line #L81 was not covered by tests
}

if (/\d{3}/g.test(value)) {
const code = Number.parseInt(value, 10);
if (code >= 100 && code < 600) {
return code;
}
}

return null;
};

export const getOperationErrors = (
operationResponses: OperationResponse[],
): OperationResponse[] =>
operationResponses.filter(
({ code, description }) =>
typeof code === 'number' && code >= 300 && description,
/**
* Returns only error status code responses.
*/
export const getErrorResponses = (
responses: OperationResponse[],
): OperationResponse[] => {
const results = responses.filter(
({ code }) =>
code === '3XX' ||
code === '4XX' ||
code === '5XX' ||
(typeof code === 'number' && code >= 300),
);
return results;
};

/**
* Returns only successful status code responses.
*/
export const getSuccessResponses = (
responses: OperationResponse[],
): OperationResponse[] => {
const results = responses.filter(
({ code }) =>
code === 'default' ||
code === '2XX' ||
(typeof code === 'number' && code >= 200 && code < 300),
);
return results.filter(
(result, index, arr) =>
arr.findIndex((item) => areEqual(item, result)) === index,
);
};
46 changes: 31 additions & 15 deletions packages/openapi-ts/src/openApi/v2/parser/getOperation.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Client } from '../../../types/client';
import { getOperationResults } from '../../../utils/operation';
import type {
Operation,
OperationParameters,
} from '../../common/interfaces/client';
import {
getOperationErrors,
getErrorResponses,
getOperationName,
getOperationResponseHeader,
getSuccessResponses,
} from '../../common/parser/operation';
import { getServiceName } from '../../common/parser/service';
import { toSortedByRequired } from '../../common/parser/sort';
Expand Down Expand Up @@ -66,13 +66,28 @@ export const getOperation = ({
parameters: op.parameters,
types,
});
operation.imports.push(...parameters.imports);
operation.parameters.push(...parameters.parameters);
operation.parametersPath.push(...parameters.parametersPath);
operation.parametersQuery.push(...parameters.parametersQuery);
operation.parametersForm.push(...parameters.parametersForm);
operation.parametersHeader.push(...parameters.parametersHeader);
operation.parametersCookie.push(...parameters.parametersCookie);
operation.imports = [...operation.imports, ...parameters.imports];
operation.parameters = [...operation.parameters, ...parameters.parameters];
operation.parametersPath = [
...operation.parametersPath,
...parameters.parametersPath,
];
operation.parametersQuery = [
...operation.parametersQuery,
...parameters.parametersQuery,
];
operation.parametersForm = [
...operation.parametersForm,
...parameters.parametersForm,
];
operation.parametersHeader = [
...operation.parametersHeader,
...parameters.parametersHeader,
];
operation.parametersCookie = [
...operation.parametersCookie,
...parameters.parametersCookie,
];
operation.parametersBody = parameters.parametersBody;
}

Expand All @@ -83,13 +98,14 @@ export const getOperation = ({
responses: op.responses,
types,
});
const operationResults = getOperationResults(operationResponses);
operation.errors = getOperationErrors(operationResponses);
operation.responseHeader = getOperationResponseHeader(operationResults);
operation.errors = getErrorResponses(operationResponses);

operationResults.forEach((operationResult) => {
operation.results.push(operationResult);
operation.imports.push(...operationResult.imports);
const successResponses = getSuccessResponses(operationResponses);
operation.responseHeader = getOperationResponseHeader(successResponses);

successResponses.forEach((operationResult) => {
operation.results = [...operation.results, operationResult];
operation.imports = [...operation.imports, ...operationResult.imports];
});
}

Expand Down
92 changes: 50 additions & 42 deletions packages/openapi-ts/src/openApi/v2/parser/getOperationResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const getOperationResponse = ({
}: {
openApi: OpenApi;
response: OpenApiResponse;
code: number | 'default';
code: OperationResponse['code'];
types: Client['types'];
}): OperationResponse => {
const operationResponse: OperationResponse = {
Expand All @@ -40,64 +40,72 @@ export const getOperationResponse = ({
type: code !== 204 ? 'unknown' : 'void',
};

// If this response has a schema, then we need to check two things:
// if this is a reference then the parameter is just the 'name' of
// this reference type. Otherwise, it might be a complex schema,
// and then we need to parse the schema!
let schema = response.schema;
if (schema) {
if (schema.$ref?.startsWith('#/responses/')) {
schema = getRef<OpenApiSchema>(openApi, schema);
}

if (schema.$ref) {
const model = getType({ type: schema.$ref });
operationResponse.export = 'reference';
operationResponse.type = model.type;
operationResponse.base = model.base;
operationResponse.template = model.template;
operationResponse.imports.push(...model.imports);
return operationResponse;
} else {
const model = getModel({ definition: schema, openApi, types });
operationResponse.export = model.export;
operationResponse.type = model.type;
operationResponse.base = model.base;
operationResponse.template = model.template;
operationResponse.link = model.link;
operationResponse.isReadOnly = model.isReadOnly;
operationResponse.isRequired = model.isRequired;
operationResponse.isNullable = model.isNullable;
operationResponse.format = model.format;
operationResponse.maximum = model.maximum;
operationResponse.exclusiveMaximum = model.exclusiveMaximum;
operationResponse.minimum = model.minimum;
operationResponse.exclusiveMinimum = model.exclusiveMinimum;
operationResponse.multipleOf = model.multipleOf;
operationResponse.maxLength = model.maxLength;
operationResponse.minLength = model.minLength;
operationResponse.maxItems = model.maxItems;
operationResponse.minItems = model.minItems;
operationResponse.uniqueItems = model.uniqueItems;
operationResponse.maxProperties = model.maxProperties;
operationResponse.minProperties = model.minProperties;
operationResponse.pattern = getPattern(model.pattern);
operationResponse.imports.push(...model.imports);
operationResponse.enum.push(...model.enum);
operationResponse.enums.push(...model.enums);
operationResponse.properties.push(...model.properties);
operationResponse.imports = [
...operationResponse.imports,
...model.imports,
];
return operationResponse;
}

const model = getModel({ definition: schema, openApi, types });
operationResponse.export = model.export;
operationResponse.type = model.type;
operationResponse.base = model.base;
operationResponse.template = model.template;
operationResponse.link = model.link;
operationResponse.isReadOnly = model.isReadOnly;
operationResponse.isRequired = model.isRequired;
operationResponse.isNullable = model.isNullable;
operationResponse.format = model.format;
operationResponse.maximum = model.maximum;
operationResponse.exclusiveMaximum = model.exclusiveMaximum;
operationResponse.minimum = model.minimum;
operationResponse.exclusiveMinimum = model.exclusiveMinimum;
operationResponse.multipleOf = model.multipleOf;
operationResponse.maxLength = model.maxLength;
operationResponse.minLength = model.minLength;
operationResponse.maxItems = model.maxItems;
operationResponse.minItems = model.minItems;
operationResponse.uniqueItems = model.uniqueItems;
operationResponse.maxProperties = model.maxProperties;
operationResponse.minProperties = model.minProperties;
operationResponse.pattern = getPattern(model.pattern);
operationResponse.imports = [
...operationResponse.imports,
...model.imports,
];
operationResponse.enum = [...operationResponse.enum, ...model.enum];
operationResponse.enums = [...operationResponse.enums, ...model.enums];
operationResponse.properties = [
...operationResponse.properties,
...model.properties,
];
return operationResponse;
}

// We support basic properties from response headers, since both
// fetch and XHR client just support string types.
Object.keys(response.headers ?? {}).forEach((name) => {
operationResponse.in = 'header';
operationResponse.name = name;
operationResponse.type = 'string';
operationResponse.base = 'string';
return operationResponse;
});
if (response.headers) {
for (const name in response.headers) {
operationResponse.in = 'header';
operationResponse.name = name;
operationResponse.type = 'string';
operationResponse.base = 'string';
return operationResponse;
}
}

return operationResponse;
};
Loading

0 comments on commit 0d38bed

Please sign in to comment.