Skip to content

Commit

Permalink
Merge pull request #28 from asteasolutions/custom-openapi-components
Browse files Browse the repository at this point in the history
Custom OpenAPI components, response headers, z.discriminatedUnion & prettier
  • Loading branch information
georgyangelov authored Aug 4, 2022
2 parents fbd243e + 09fdc3a commit 0ca013a
Show file tree
Hide file tree
Showing 20 changed files with 443 additions and 125 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
node-version: [14.x, 16.x, 17.x]
node-version: [14.x, 16.x, 18.x]

steps:
- uses: actions/checkout@v2
Expand All @@ -27,5 +27,5 @@ jobs:

- run: npm ci
- run: npm run build
# - run: npm run lint
- run: npm run lint
- run: npm test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/node_modules
.node-version
.nvm
.vscode
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/dist
README.md
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"arrowParens": "avoid"
}
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A library that uses [zod schemas](https://github.com/colinhacks/zod) to generate
3. [The Registry](#the-registry)
4. [Defining schemas](#defining-schemas)
5. [Defining routes](#defining-routes)
6. [Defining custom components](#defining-custom-components)
6. [A full example](#a-full-example)
7. [Adding it as part of your build](#adding-it-as-part-of-your-build)
3. [Zod schema types](#zod-schema-types)
Expand Down Expand Up @@ -317,6 +318,10 @@ return generator.generateDocument({
});
```

### Defining custom components

You can define components that are not OpenAPI schemas, including security schemes, response headers and others. See [this test file](spec/custom-components.spec.ts) for examples.

### A full example

A full example code can be found [here](./example/index.ts). And the YAML representation of its result - [here](./example/openapi-docs.yml)
Expand Down
7 changes: 7 additions & 0 deletions example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@ const UserSchema = registry.register(
})
);

const bearerAuth = registry.registerComponent('securitySchemes', 'bearerAuth', {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
});

registry.registerPath({
method: 'get',
path: '/users/{id}',
description: 'Get user data by its id',
summary: 'Get a single user',
security: [{ [bearerAuth.name]: [] }],
request: {
params: z.object({ id: UserIdSchema }),
},
Expand Down
14 changes: 7 additions & 7 deletions example/openapi-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ components:
properties:
id:
type: string
example: "1212121"
example: '1212121'
name:
type: string
example: John Doe
Expand All @@ -29,21 +29,21 @@ components:
name: id
schema:
type: string
example: "1212121"
example: '1212121'
required: true
paths:
"/users/{id}":
'/users/{id}':
get:
description: Get user data by its id
summary: Get a single user
parameters:
- $ref: "#/components/parameters/UserId"
- $ref: '#/components/parameters/UserId'
responses:
"200":
'200':
description: Object with user data.
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"204":
$ref: '#/components/schemas/User'
'204':
description: No content - successful operation
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
};
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "1.2.3",
"description": "Builds OpenAPI schemas from Zod schemas",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"package.json",
Expand All @@ -24,6 +25,8 @@
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "jest",
"prettier": "prettier --write .",
"lint": "prettier --check .",
"prepublishOnly": "npm run build"
},
"dependencies": {
Expand All @@ -35,6 +38,7 @@
"devDependencies": {
"@types/jest": "^27.4.1",
"jest": "^27.5.1",
"prettier": "^2.7.1",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3",
"yaml": "^2.0.0",
Expand Down
99 changes: 99 additions & 0 deletions spec/custom-components.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { OpenAPIGenerator } from '../src/openapi-generator';
import { OpenAPIRegistry } from '../src/openapi-registry';
import { z } from 'zod';
import { extendZodWithOpenApi } from '../src/zod-extensions';

extendZodWithOpenApi(z);

const testDocConfig = {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'Swagger Petstore',
description: 'A sample API',
termsOfService: 'http://swagger.io/terms/',
license: {
name: 'Apache 2.0',
url: 'https://www.apache.org/licenses/LICENSE-2.0.html',
},
},
servers: [{ url: 'v1' }],
};

describe('Custom components', () => {
it('can register and generate security schemes', () => {
const registry = new OpenAPIRegistry();

const bearerAuth = registry.registerComponent(
'securitySchemes',
'bearerAuth',
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
}
);

registry.registerPath({
path: '/units',
method: 'get',
security: [{ [bearerAuth.name]: [] }],
responses: {
200: {
mediaType: 'application/json',
schema: z.string().openapi({ description: 'Sample response' }),
},
},
});

const builder = new OpenAPIGenerator(registry.definitions);
const document = builder.generateDocument(testDocConfig);

expect(document.paths['/units'].get.security).toEqual([{ bearerAuth: [] }]);

expect(document.components!.securitySchemes).toEqual({
bearerAuth: {
bearerFormat: 'JWT',
scheme: 'bearer',
type: 'http',
},
});
});

it('can register and generate headers', () => {
const registry = new OpenAPIRegistry();

const apiKeyHeader = registry.registerComponent('headers', 'api-key', {
example: '1234',
required: true,
description: 'The API Key you were given in the developer portal',
});

registry.registerPath({
path: '/units',
method: 'get',
responses: {
200: {
mediaType: 'application/json',
headers: { 'x-api-key': apiKeyHeader.ref },
schema: z.string().openapi({ description: 'Sample response' }),
},
},
});

const builder = new OpenAPIGenerator(registry.definitions);
const document = builder.generateDocument(testDocConfig);

expect(document.paths['/units'].get.responses['200'].headers).toEqual({
'x-api-key': { $ref: '#/components/headers/api-key' },
});

expect(document.components!.headers).toEqual({
'api-key': {
example: '1234',
required: true,
description: 'The API Key you were given in the developer portal',
},
});
});
});
6 changes: 2 additions & 4 deletions spec/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import type { SchemasObject } from 'openapi3-ts';
import type { ZodSchema } from 'zod';

export function createSchemas(zodSchemas: ZodSchema<any>[]) {
const definitions = zodSchemas.map((schema) => ({
const definitions = zodSchemas.map(schema => ({
type: 'schema' as const,
schema,
}));

const { components } = new OpenAPIGenerator(
definitions
).generateComponents();
const { components } = new OpenAPIGenerator(definitions).generateComponents();

return components;
}
Expand Down
42 changes: 36 additions & 6 deletions spec/params.spec.ts → spec/routes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { OpenAPIGenerator } from '../src/openapi-generator';
import { OperationObject, PathItemObject } from 'openapi3-ts';
import { z, ZodSchema } from 'zod';

import { OperationObject, PathItemObject } from 'openapi3-ts';
import { OpenAPIGenerator } from '../src/openapi-generator';
import { extendZodWithOpenApi } from '../src/zod-extensions';
import { RouteConfig } from '../src/openapi-registry';
import { OpenAPIRegistry, RouteConfig } from '../src/openapi-registry';

function createTestRoute(props: Partial<RouteConfig> = {}): RouteConfig {
return {
Expand Down Expand Up @@ -38,7 +37,38 @@ const testDocConfig = {
extendZodWithOpenApi(z);

describe('Routes', () => {
describe('Parameters', () => {
describe('response definitions', () => {
it('can set description through the response definition or through the schema', () => {
const registry = new OpenAPIRegistry();

registry.registerPath({
method: 'get',
path: '/',
responses: {
200: {
mediaType: 'application/json',
description: 'Simple response',
schema: z.string(),
},

404: {
mediaType: 'application/json',
schema: z.string().openapi({ description: 'Missing object' }),
},
},
});

const document = new OpenAPIGenerator(
registry.definitions
).generateDocument(testDocConfig);
const responses = document.paths['/'].get.responses;

expect(responses['200'].description).toEqual('Simple response');
expect(responses['404'].description).toEqual('Missing object');
});
});

describe('parameters', () => {
it('generates a query parameter for route', () => {
const routeParameters = generateParamsForRoute({
request: { query: z.object({ test: z.string() }) },
Expand Down Expand Up @@ -213,7 +243,7 @@ describe('Routes', () => {
const route = createTestRoute(props);

const paramDefinitions =
paramsToRegister?.map((schema) => ({
paramsToRegister?.map(schema => ({
type: 'parameter' as const,
schema,
})) ?? [];
Expand Down
14 changes: 6 additions & 8 deletions spec/separate-zod-instance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@ describe('Separate Zod instance', () => {
extendZodWithOpenApi(zod2);

it('can check object types of different zod instances', () => {
expectSchema(
[zod1.string().openapi({ refId: 'SimpleString' })],
{ SimpleString: { type: 'string' } }
);
expectSchema([zod1.string().openapi({ refId: 'SimpleString' })], {
SimpleString: { type: 'string' },
});

expectSchema(
[zod2.string().openapi({ refId: 'SimpleString' })],
{ SimpleString: { type: 'string' } }
);
expectSchema([zod2.string().openapi({ refId: 'SimpleString' })], {
SimpleString: { type: 'string' },
});
});
});
Loading

0 comments on commit 0ca013a

Please sign in to comment.