Skip to content

Commit

Permalink
add support for different parameter types - header/params/query
Browse files Browse the repository at this point in the history
  • Loading branch information
AGalabov committed Apr 15, 2022
1 parent 618ec46 commit af39b96
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 33 deletions.
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import './zod-extensions';

export * from './zod-extensions';
export { OpenAPIGenerator } from './openapi-generator';
export { SchemaRegistry } from './schema-registry';
export { ParamsRegistry } from './params-registry';
Expand Down
93 changes: 66 additions & 27 deletions src/openapi-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
ReferenceObject,
SchemaObject,
ParameterObject,
SchemasObject,
RequestBodyObject,
PathItemObject,
PathObject,
Expand All @@ -13,6 +12,7 @@ import {
TagObject,
ExternalDocumentationObject,
ComponentsObject,
ParameterLocation,
} from 'openapi3-ts';
import {
ZodArray,
Expand Down Expand Up @@ -50,12 +50,12 @@ type UnknownKeysParam = 'passthrough' | 'strict' | 'strip';

type OpenAPIDefinitions =
| { type: 'schema'; schema: ZodSchema<any> }
| { type: 'parameter'; schema: ZodSchema<any> }
| { type: 'parameter'; location: ParameterLocation; schema: ZodSchema<any> }
| { type: 'route'; route: RouteConfig };

// This is essentially OpenAPIObject without the components and paths keys.
// Omit does not work, since OpenAPIObject extends ISpecificationExtension
// and is infered as { [key: number]: any; [key: string]: any }
// and is inferred as { [key: number]: any; [key: string]: any }
interface OpenAPIObjectConfig {
openapi: string;
info: InfoObject;
Expand Down Expand Up @@ -109,7 +109,11 @@ export class OpenAPIGenerator {
definition: OpenAPIDefinitions
): SchemaObject | ParameterObject | ReferenceObject {
if (definition.type === 'parameter') {
return this.generateSingleParameter(definition.schema, true);
return this.generateSingleParameter(
definition.schema,
definition.location,
true
);
}

if (definition.type === 'schema') {
Expand All @@ -125,18 +129,30 @@ export class OpenAPIGenerator {

private generateSingleParameter(
zodSchema: ZodSchema<any>,
location: ParameterLocation,
saveIfNew: boolean,
externalName?: string
): ParameterObject | ReferenceObject {
const innerSchema = this.unwrapOptional(zodSchema);
const metadata = zodSchema._def.openapi
? zodSchema._def.openapi
: innerSchema._def.openapi;
const metadata = this.getMetadata(zodSchema);

/**
* TODOs
* External name should come as priority in case there is known schema?
* Basically a schema is one thing, it's name in query is another.
*
* The externalName should not be a reason to "use it from the object".
* An error should be thrown instead :thinking:
*/

const schemaName = metadata?.name ?? externalName;
//TODO: Throw error if missing.
const schemaName = externalName ?? metadata?.name;

if (schemaName && this.paramRefs[schemaName]) {
if (!schemaName) {
throw new Error(
'Unknown parameter name, please specify `name` and other OpenAPI props using `ZodSchema.openapi`'
);
}

if (this.paramRefs[schemaName]) {
return {
$ref: `#/components/parameters/${schemaName}`,
};
Expand All @@ -147,9 +163,8 @@ export class OpenAPIGenerator {
const schema = this.generateSingleSchema(zodSchema, false, false);

const result: ParameterObject = {
in: 'path',
// TODO: Is this valid? I think so since parameters are only defined from registries
name: schemaName as string,
in: location,
name: schemaName,
schema,
required,
// TODO: Fix types and check for possibly wrong data
Expand Down Expand Up @@ -212,12 +227,7 @@ export class OpenAPIGenerator {
}

const schema = this.generateSingleSchema(bodySchema, false);

const innerSchema = this.unwrapOptional(bodySchema);

const metadata = bodySchema._def.openapi
? bodySchema._def.openapi
: innerSchema._def.openapi;
const metadata = this.getMetadata(bodySchema);

return {
description: metadata?.description,
Expand All @@ -230,13 +240,15 @@ export class OpenAPIGenerator {
};
}

private getParamsDoc(
paramsSchema: ZodType<unknown> | undefined
private getParamsByLocation(
paramsSchema: ZodType<unknown> | undefined,
location: ParameterLocation
): (ParameterObject | ReferenceObject)[] {
if (!paramsSchema) {
return [];
}

// TODO: Should the paramsSchema be restricted to an object?
if (paramsSchema instanceof ZodObject) {
const propTypes = paramsSchema._def.shape() as ZodRawShape;

Expand All @@ -249,14 +261,39 @@ export class OpenAPIGenerator {
return undefined;
}

return this.generateSingleParameter(propSchema, false, name);
return this.generateSingleParameter(
propSchema,
location,
false,
name
);
})
);
}

return [];
}

private getParameters(
request: RouteConfig['request'] | undefined
): (ParameterObject | ReferenceObject)[] {
if (!request) {
return [];
}

const pathParams = this.getParamsByLocation(request.params, 'path');
const queryParams = this.getParamsByLocation(request.query, 'query');
const headerParams = compact(
request.headers?.map((header) =>
this.generateSingleParameter(header, 'header', false)
)
);

// What happens if a schema is defined as a parameter externally but is
// used here as a header for example
return [...pathParams, ...queryParams, ...headerParams];
}

private generateSingleRoute(route: RouteConfig) {
const responseSchema = this.generateSingleSchema(route.response, false);

Expand All @@ -266,7 +303,7 @@ export class OpenAPIGenerator {
summary: route.summary,

// TODO: Header parameters
parameters: this.getParamsDoc(route.request?.params),
parameters: this.getParameters(route.request),

requestBody: this.getBodyDoc(route.request?.body),

Expand Down Expand Up @@ -410,8 +447,10 @@ export class OpenAPIGenerator {
return {};
}

// TODO: Better error name (so that a random build of 100 schemas can be traced)
throw new Error(
'Unknown zod object type, please specify `type` and other OpenAPI props using `ZodSchema.openapi`'
'Unknown zod object type, please specify `type` and other OpenAPI props using `ZodSchema.openapi`' +
JSON.stringify(zodSchema._def)
);
}

Expand Down Expand Up @@ -448,12 +487,12 @@ export class OpenAPIGenerator {
return omitBy(omit(metadata, 'name'), isNil);
}

private getName(zodSchema: ZodSchema<any>) {
private getMetadata(zodSchema: ZodSchema<any>) {
const innerSchema = this.unwrapOptional(zodSchema);
const metadata = zodSchema._def.openapi
? zodSchema._def.openapi
: innerSchema._def.openapi;

return metadata?.name;
return metadata;
}
}
18 changes: 15 additions & 3 deletions src/params-registry.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { ParameterLocation } from 'openapi3-ts';
import { ZodSchema } from 'zod';

export class ParamsRegistry {
public readonly schemas: {
type: 'parameter';
location: ParameterLocation;
schema: ZodSchema<unknown>;
}[] = [];

constructor() {}

register<T extends ZodSchema<any>>(name: string, zodSchema: T) {
register<T extends ZodSchema<any>>(
config: { name: string; location: ParameterLocation },
zodSchema: T
) {
const currentMetadata = zodSchema._def.openapi;
const schemaWithMetadata = zodSchema.openapi({ ...currentMetadata, name });
const schemaWithMetadata = zodSchema.openapi({
...currentMetadata,
name: config.name,
});

this.schemas.push({ type: 'parameter', schema: schemaWithMetadata });
this.schemas.push({
type: 'parameter',
location: config.location,
schema: schemaWithMetadata,
});

return schemaWithMetadata;
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"include": ["src/**/*", "example.ts"],
"include": ["src/**/*"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */

Expand Down

0 comments on commit af39b96

Please sign in to comment.