Skip to content

Commit be5ddb2

Browse files
author
Quentin Nativel
committed
feat: add method to generate openApi documentation
1 parent bb9055d commit be5ddb2

File tree

5 files changed

+283
-0
lines changed

5 files changed

+283
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './lambdaHandler';
22
export * from './fetchRequest';
33
export * from './axiosRequest';
4+
export * from './openApiDocumentation';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { getContractDocumentation } from './openApiDocumentation';
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { z } from 'zod';
2+
3+
import { ApiGatewayContract } from 'apiGateway/ApiGatewayContract';
4+
import { HttpStatusCodes } from 'types/http';
5+
6+
import { getContractDocumentation } from './openApiDocumentation';
7+
8+
describe('apiGateway openApi contract documentation', () => {
9+
const pathParametersSchema = z.object({
10+
userId: z.string(),
11+
pageNumber: z.string(),
12+
});
13+
14+
const queryStringParametersSchema = z.object({
15+
testId: z.string(),
16+
});
17+
18+
const headersSchema = z.object({
19+
myHeader: z.string(),
20+
});
21+
22+
const bodySchema = z.object({
23+
foo: z.string(),
24+
});
25+
26+
const outputSchema = z.object({
27+
id: z.string(),
28+
name: z.string(),
29+
});
30+
31+
const unauthorizedSchema = z.object({
32+
message: z.string(),
33+
});
34+
35+
const outputSchemas = {
36+
[HttpStatusCodes.OK]: outputSchema,
37+
[HttpStatusCodes.UNAUTHORIZED]: unauthorizedSchema,
38+
};
39+
40+
describe('httpApi, when all parameters are set', () => {
41+
const httpApiContract = new ApiGatewayContract({
42+
id: 'testContract',
43+
path: '/users/{userId}',
44+
method: 'GET',
45+
integrationType: 'httpApi',
46+
pathParametersSchema,
47+
queryStringParametersSchema,
48+
headersSchema,
49+
bodySchema,
50+
outputSchemas,
51+
});
52+
53+
it('should generate open api documentation', () => {
54+
expect(getContractDocumentation(httpApiContract)).toEqual({
55+
path: '/users/{userId}',
56+
method: 'get',
57+
documentation: {
58+
parameters: [
59+
{
60+
in: 'header',
61+
name: 'myHeader',
62+
required: true,
63+
schema: {
64+
type: 'string',
65+
},
66+
},
67+
{
68+
in: 'query',
69+
name: 'testId',
70+
required: true,
71+
schema: {
72+
type: 'string',
73+
},
74+
},
75+
{
76+
in: 'path',
77+
name: 'userId',
78+
required: true,
79+
schema: {
80+
type: 'string',
81+
},
82+
},
83+
{
84+
in: 'path',
85+
name: 'pageNumber',
86+
required: true,
87+
schema: {
88+
type: 'string',
89+
},
90+
},
91+
],
92+
requestBody: {
93+
content: {
94+
'application/json': {
95+
schema: {
96+
type: 'object',
97+
properties: {
98+
foo: {
99+
type: 'string',
100+
},
101+
},
102+
required: ['foo'],
103+
},
104+
},
105+
},
106+
},
107+
responses: {
108+
'200': {
109+
description: 'Response: 200',
110+
content: {
111+
'application/json': {
112+
schema: {
113+
type: 'object',
114+
properties: {
115+
id: {
116+
type: 'string',
117+
},
118+
name: {
119+
type: 'string',
120+
},
121+
},
122+
required: ['id', 'name'],
123+
},
124+
},
125+
},
126+
},
127+
'401': {
128+
description: 'Response: 401',
129+
content: {
130+
'application/json': {
131+
schema: {
132+
type: 'object',
133+
properties: {
134+
message: {
135+
type: 'string',
136+
},
137+
},
138+
required: ['message'],
139+
},
140+
},
141+
},
142+
},
143+
},
144+
},
145+
});
146+
});
147+
});
148+
149+
describe('restApi, when it is instanciated with a subset of schemas', () => {
150+
const restApiContract = new ApiGatewayContract({
151+
id: 'testContract',
152+
path: 'coucou',
153+
method: 'POST',
154+
integrationType: 'restApi',
155+
});
156+
157+
it('should generate open api documentation', () => {
158+
expect(getContractDocumentation(restApiContract)).toEqual({
159+
path: 'coucou',
160+
method: 'post',
161+
documentation: {
162+
responses: {}, // no response is configured
163+
},
164+
});
165+
});
166+
});
167+
});
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { generateSchema } from '@anatine/zod-openapi';
2+
import isUndefined from 'lodash/isUndefined';
3+
import omitBy from 'lodash/omitBy';
4+
import { OpenAPIV3 } from 'openapi-types';
5+
6+
import { GenericApiGatewayContract } from 'apiGateway/ApiGatewayContract';
7+
import { ContractOpenApiDocumentation } from 'types/contractOpenApiDocumentation';
8+
9+
export const getContractDocumentation = <
10+
Contract extends GenericApiGatewayContract,
11+
>(
12+
contract: Contract,
13+
): ContractOpenApiDocumentation => {
14+
const initialDocumentation: OpenAPIV3.OperationObject = {
15+
responses: {},
16+
};
17+
18+
const definedOutputSchema = omitBy(contract.outputSchemas, isUndefined);
19+
console.log(definedOutputSchema);
20+
21+
// add responses to the object
22+
const contractDocumentation = Object.keys(definedOutputSchema).reduce(
23+
(config: OpenAPIV3.OperationObject, responseCode) => {
24+
const schema = definedOutputSchema[responseCode];
25+
26+
if (schema === undefined) {
27+
return config;
28+
}
29+
30+
const openApiSchema = generateSchema(schema);
31+
32+
return {
33+
...config,
34+
responses: {
35+
...config.responses,
36+
[responseCode]: {
37+
description: `Response: ${responseCode}`,
38+
content: {
39+
'application/json': {
40+
schema: openApiSchema as OpenAPIV3.SchemaObject,
41+
},
42+
},
43+
},
44+
},
45+
};
46+
},
47+
initialDocumentation,
48+
);
49+
50+
if (contract.pathParametersSchema !== undefined) {
51+
contractDocumentation.parameters = [
52+
...Object.entries(contract.pathParametersSchema.shape).map(
53+
([variableName, variableDefinition]) => ({
54+
name: variableName,
55+
in: 'path',
56+
schema: generateSchema(variableDefinition) as OpenAPIV3.SchemaObject,
57+
required: !variableDefinition.isOptional(),
58+
}),
59+
),
60+
...(contractDocumentation.parameters ?? []),
61+
];
62+
}
63+
64+
if (contract.queryStringParametersSchema !== undefined) {
65+
contractDocumentation.parameters = [
66+
...Object.entries(contract.queryStringParametersSchema.shape).map(
67+
([variableName, variableDefinition]) => ({
68+
name: variableName,
69+
in: 'query',
70+
schema: generateSchema(variableDefinition) as OpenAPIV3.SchemaObject,
71+
required: !variableDefinition.isOptional(),
72+
}),
73+
),
74+
...(contractDocumentation.parameters ?? []),
75+
];
76+
}
77+
78+
if (contract.headersSchema !== undefined) {
79+
contractDocumentation.parameters = [
80+
...Object.entries(contract.headersSchema.shape).map(
81+
([variableName, variableDefinition]) => ({
82+
name: variableName,
83+
in: 'header',
84+
schema: generateSchema(variableDefinition) as OpenAPIV3.SchemaObject,
85+
required: !variableDefinition.isOptional(),
86+
}),
87+
),
88+
...(contractDocumentation.parameters ?? []),
89+
];
90+
}
91+
92+
if (contract.bodySchema !== undefined) {
93+
contractDocumentation.requestBody = {
94+
content: {
95+
'application/json': {
96+
schema: generateSchema(contract.bodySchema) as OpenAPIV3.SchemaObject,
97+
},
98+
},
99+
};
100+
}
101+
102+
return {
103+
path: contract.path,
104+
method: contract.method.toLowerCase(),
105+
documentation: contractDocumentation,
106+
};
107+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { OpenAPIV3 } from 'openapi-types';
2+
3+
export interface ContractOpenApiDocumentation {
4+
path: string;
5+
method: string;
6+
documentation: OpenAPIV3.OperationObject;
7+
}

0 commit comments

Comments
 (0)