Skip to content

Commit

Permalink
support component response headers
Browse files Browse the repository at this point in the history
  • Loading branch information
inkognitro committed Oct 6, 2024
1 parent e44ea1a commit 91ad0a8
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 57 deletions.
32 changes: 32 additions & 0 deletions src/oas3/codegen/endpointUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,43 @@ import {CodeGenerator} from './core';
import {
ConcreteParameterLocation,
findConcreteParameter,
findConcreteResponseHeader,
ObjectSchema,
ObjectSchemaProps,
Parameter,
ResponseHeaderByNameMap,
} from '@/oas3/specification';

// todo: rename to createResponseHeadersObjectSchema
export function createHeadersObjectSchema(
codeGenerator: CodeGenerator,
responseHeaderByName: ResponseHeaderByNameMap
): ObjectSchema {
const requiredProps: string[] = [];
const props: ObjectSchemaProps = {};
for (const headerName in responseHeaderByName) {
const responseHeader = responseHeaderByName[headerName];
const concreteResponseHeader = findConcreteResponseHeader(
codeGenerator.getSpecification(),
responseHeader
);
if (!concreteResponseHeader) {
throw new Error(
`could not find concrete response header from: ${JSON.stringify(responseHeader)}`
);
}
if (concreteResponseHeader.required) {
requiredProps.push(headerName);
}
props[headerName] = concreteResponseHeader.schema;
}
return {
type: 'object',
properties: props,
required: requiredProps,
};
}

export function findObjectSchemaFromLocationParameters(
codeGenerator: CodeGenerator,
requestParameters: Parameter[],
Expand Down
33 changes: 1 addition & 32 deletions src/oas3/codegen/response.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import {
ObjectSchema,
ObjectSchemaProps,
ConcreteResponse,
ResponseBodyContent,
ResponseHeaderByNameMap,
isStringSchema,
Response,
isResponseComponentRef,
isConcreteResponse,
findConcreteSchema,
ResponseComponentRef,
} from '@/oas3/specification';
import {
Expand All @@ -27,34 +23,7 @@ import {
templateResponseUnionType,
} from './template';
import {applyNullableFormDataTypeDefinition} from './formData';

function createHeadersObjectSchema(
codeGenerator: CodeGenerator,
headersSchema: ResponseHeaderByNameMap
): ObjectSchema {
const requiredProps: string[] = [];
const props: ObjectSchemaProps = {};
for (const headerName in headersSchema) {
requiredProps.push(headerName);
const headerSchema = headersSchema[headerName].schema;
const concreteHeaderSchema = findConcreteSchema(
codeGenerator.getSpecification(),
headerSchema
);
if (isStringSchema(concreteHeaderSchema)) {
props[headerName] = headerSchema;
continue;
}
props[headerName] = {
type: 'string',
};
}
return {
type: 'object',
properties: props,
required: requiredProps,
};
}
import {createHeadersObjectSchema} from './endpointUtils';

function applyResponseHeaders(
codeGenerator: CodeGenerator,
Expand Down
25 changes: 5 additions & 20 deletions src/oas3/codegen/responseSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ import {
ConcreteResponse,
isConcreteResponse,
isResponseComponentRef,
ObjectSchema,
ObjectSchemaProps,
RequestBodyContent,
Response,
ResponseBodyContentByContentTypeMap,
ResponseComponentRef,
ResponseHeaderByNameMap,
} from '@/oas3/specification';
import {
CodeGenerationOutput,
Expand All @@ -20,6 +17,7 @@ import {
} from './core';
import {Context} from './generator';
import {applyZodSchema} from './zodSchema';
import {createHeadersObjectSchema} from './endpointUtils';

type ApplyResponseBodyResult = {
contentType: string;
Expand Down Expand Up @@ -118,22 +116,6 @@ function applyResponseBodyByContentTypeMap(
};
}

function createHeadersObjectSchema(
headersSchema: ResponseHeaderByNameMap
): ObjectSchema {
const requiredProps: string[] = [];
const props: ObjectSchemaProps = {};
for (const headerName in headersSchema) {
requiredProps.push(headerName);
props[headerName] = headersSchema[headerName].schema;
}
return {
type: 'object',
properties: props,
required: requiredProps,
};
}

function applyConcreteResponseSchema(
codeGenerator: CodeGenerator,
schema: ConcreteResponse,
Expand All @@ -142,7 +124,10 @@ function applyConcreteResponseSchema(
): CodeGenerationOutput {
let headersZodSchemaCodeOutput: undefined | CodeGenerationOutput;
if (ctx.config.withZod && schema.headers) {
const headersObjectSchema = createHeadersObjectSchema(schema.headers);
const headersObjectSchema = createHeadersObjectSchema(
codeGenerator,
schema.headers
);
headersZodSchemaCodeOutput = applyZodSchema(
codeGenerator,
headersObjectSchema,
Expand Down
16 changes: 16 additions & 0 deletions src/oas3/specification/componentRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,19 @@ export function isSecuritySchemesComponentRef(
): anyValue is SecurityComponentRef {
return zSecuritySchemeComponentRef.safeParse(anyValue).success;
}

export const responseHeaderComponentRefPrefix = '#/components/headers/';

export const zResponseHeaderComponentRef = z.object({
$ref: z.string().startsWith(responseHeaderComponentRefPrefix),
});

export type ResponseHeaderComponentRef = z.infer<
typeof zResponseHeaderComponentRef
>;

export function isResponseHeaderComponentRef(
anyValue: unknown
): anyValue is ResponseHeaderComponentRef {
return zResponseHeaderComponentRef.safeParse(anyValue).success;
}
24 changes: 22 additions & 2 deletions src/oas3/specification/response.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {zSchema} from './schema';
import {zResponseComponentRef} from './componentRef';
import {
zResponseComponentRef,
zResponseHeaderComponentRef,
} from './componentRef';
import {z} from 'zod';

export const zResponseBodyContent = z.object({
Expand All @@ -15,12 +18,29 @@ export type ResponseBodyContentByContentTypeMap = z.infer<
typeof zResponseBodyContentByContentTypeMap
>;

export const zResponseHeader = z.object({
export const zConcreteResponseHeader = z.object({
schema: zSchema,
required: z.boolean().optional(),
description: z.string().optional(),
});

export function isConcreteResponseHeader(
anyValue: unknown
): anyValue is ConcreteResponseHeader {
return zConcreteResponseHeader.safeParse(anyValue).success;
}

export type ConcreteResponseHeader = z.infer<typeof zConcreteResponseHeader>;

export const zResponseHeader = z.union([
zConcreteResponseHeader,
zResponseHeaderComponentRef,
]);

export const zResponseHeaderByNameMap = z.record(zResponseHeader);

export type ResponseHeader = z.infer<typeof zResponseHeader>;

export type ResponseHeaderByNameMap = z.infer<typeof zResponseHeaderByNameMap>;

export const zConcreteResponse = z.object({
Expand Down
5 changes: 3 additions & 2 deletions src/oas3/specification/specification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {zConcreteResponse} from './response';
import {zResponse, zResponseHeaderByNameMap} from './response';
import {zSchema} from './schema';
import {zSecurityScheme} from './security';
import {zEndpoint} from './endpoint';
Expand All @@ -10,7 +10,7 @@ export type RequestByMethodMap = z.infer<typeof zRequestByMethodMap>;

const zRequestDefinitionsByPathMap = z.record(zRequestByMethodMap);

const zResponseByNameMap = z.record(zConcreteResponse);
const zResponseByNameMap = z.record(zResponse);

const zSchemaByNameMap = z.record(zSchema);

Expand All @@ -21,6 +21,7 @@ const zRequestParameterByNameMap = z.record(zParameter);
const zComponentDefinitions = z.object({
parameters: zRequestParameterByNameMap.optional(),
responses: zResponseByNameMap.optional(),
headers: zResponseHeaderByNameMap.optional(),
schemas: zSchemaByNameMap.optional(),
securitySchemes: zSecuritySchemeByNameMap.optional(),
});
Expand Down
47 changes: 46 additions & 1 deletion src/oas3/specification/util.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import {
isParameterComponentRef,
isResponseComponentRef,
isResponseHeaderComponentRef,
isSchemaComponentRef,
parameterComponentRefPrefix,
responseComponentRefPrefix,
responseHeaderComponentRefPrefix,
schemaComponentRefPrefix,
} from './componentRef';
import {ConcreteSchema, isConcreteSchema, Schema} from './schema';
import {ConcreteParameter, isConcreteParameter, Parameter} from './endpoint';
import {ConcreteResponse, isConcreteResponse, Response} from './response';
import {
ConcreteResponse,
ConcreteResponseHeader,
isConcreteResponse,
isConcreteResponseHeader,
Response,
ResponseHeader,
} from './response';
import {Specification} from './specification';

export function findComponentSchemaByRef(
Expand Down Expand Up @@ -106,3 +115,39 @@ export function findConcreteResponse(
}
return findConcreteResponse(spec, componentResponse);
}

export function findComponentResponseHeaderByRef(
spec: Specification,
componentRef: string
): null | ResponseHeader {
const components = spec.components;
if (
componentRef.startsWith(responseHeaderComponentRefPrefix) &&
components.headers
) {
const name = componentRef.replace(responseHeaderComponentRefPrefix, '');
const responseHeader = components.headers[name];
return responseHeader ?? null;
}
return null;
}

export function findConcreteResponseHeader(
spec: Specification,
responseHeader: ResponseHeader
): null | ConcreteResponseHeader {
if (isConcreteResponseHeader(responseHeader)) {
return responseHeader;
}
if (!isResponseHeaderComponentRef(responseHeader)) {
return null;
}
const componentResponseHeader = findComponentResponseHeaderByRef(
spec,
responseHeader.$ref
);
if (!componentResponseHeader) {
return null;
}
return findConcreteResponseHeader(spec, componentResponseHeader);
}

0 comments on commit 91ad0a8

Please sign in to comment.