Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Document generator #16

Merged
merged 9 commits into from
Jul 1, 2022
Merged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"koa-compose": "^4.1.0",
"lodash": "^4.17.21",
"nunjucks": "^3.2.3",
"openapi3-ts": "^2.0.2",
"reflect-metadata": "^0.1.13",
"tslib": "^2.3.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FieldInType } from '@vulcan/core';
import { SchemaParserMiddleware } from './middleware';

// Add the "required" validator when the parameters are in path
export const addRequiredValidatorForPath =
(): SchemaParserMiddleware => async (schemas, next) => {
await next();
const requests = schemas.request || [];
for (const request of requests) {
if (request.fieldIn !== FieldInType.PATH) continue;
if (!request.validators) request.validators = [];
if (
!request.validators?.some(
(validator) => (validator as any).name === 'required'
)
) {
request.validators?.push({
name: 'required',
args: {},
});
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FieldDataType } from '@vulcan/core';
import { DeepPartial } from 'ts-essentials';
import { RawResponseProperty, SchemaParserMiddleware } from './middleware';

const generateResponsePropertyType = (
property: DeepPartial<RawResponseProperty>
) => {
if (!property.type) {
property.type = FieldDataType.STRING;
} else if (Array.isArray(property.type)) {
((property.type as DeepPartial<RawResponseProperty>[]) || []).forEach(
(property) => generateResponsePropertyType(property)
);
}
};

// Fallback to string when type is not defined.
// TODO: Guess the type by validators.
export const generateDataType =
(): SchemaParserMiddleware => async (schemas, next) => {
await next();
(schemas.request || []).forEach((request) => {
if (!request.type) {
request.type = FieldDataType.STRING;
}
});
(schemas.response || []).forEach((property) =>
generateResponsePropertyType(property)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { FieldDataType, FieldInType } from '@vulcan/core';
import { SchemaParserMiddleware } from './middleware';

// /user/{id} => {request: [{fieldName: 'id', fieldIn: 'path' ....}]}
export const generatePathParameters =
(): SchemaParserMiddleware => async (schema, next) => {
await next();
const pattern = /:([^/]+)/g;
const pathParameters: string[] = [];

let param = pattern.exec(schema.urlPath || '');
while (param) {
pathParameters.push(param[1]);
param = pattern.exec(schema.urlPath || '');
}

const request = schema.request || [];
pathParameters
.filter((param) => !request.some((req) => req.fieldName === param))
.forEach((param) =>
request.push({
fieldName: param,
fieldIn: FieldInType.PATH,
type: FieldDataType.STRING,
validators: [
{
name: 'required',
args: {},
},
],
})
);
schema.request = request;
};
6 changes: 6 additions & 0 deletions packages/build/src/lib/schema-parser/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ export * from './generateTemplateSource';
export * from './checkParameter';
export * from './addMissingErrors';
export * from './fallbackErrors';
export * from './normalizeFieldIn';
export * from './generateDataType';
export * from './normalizeDataType';
export * from './generatePathParameters';
export * from './addRequiredValidatorForPath';
export * from './setConstraints';
16 changes: 14 additions & 2 deletions packages/build/src/lib/schema-parser/middleware/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { APISchema, RequestParameter, ValidatorDefinition } from '@vulcan/core';
import {
APISchema,
FieldDataType,
RequestParameter,
ResponseProperty,
ValidatorDefinition,
} from '@vulcan/core';
import { DeepPartial } from 'ts-essentials';

export interface RawRequestParameter
extends Omit<RequestParameter, 'validators'> {
validators: Array<ValidatorDefinition | string>;
}

export interface RawAPISchema extends DeepPartial<Omit<APISchema, 'request'>> {
export interface RawResponseProperty extends Omit<ResponseProperty, 'type'> {
type: string | FieldDataType | Array<RawResponseProperty>;
}

export interface RawAPISchema
extends DeepPartial<Omit<APISchema, 'request' | 'response'>> {
/** Indicate the identifier of this schema from the source, it might be uuid, file path, url ...etc, depend on the provider */
sourceName: string;
request?: DeepPartial<RawRequestParameter[]>;
response?: DeepPartial<RawResponseProperty[]>;
}

export interface SchemaParserMiddleware {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FieldDataType } from '@vulcan/core';
import { DeepPartial } from 'ts-essentials';
import { RawResponseProperty, SchemaParserMiddleware } from './middleware';

const normalizedResponsePropertyType = (
property: DeepPartial<RawResponseProperty>
) => {
if (typeof property.type === 'string') {
property.type = property.type.toUpperCase() as FieldDataType;
} else {
((property.type as DeepPartial<RawResponseProperty>[]) || []).forEach(
(property) => normalizedResponsePropertyType(property)
);
}
};

// type: string => FieldIn FieldDataType.STRING
export const normalizeDataType =
(): SchemaParserMiddleware => async (schemas, next) => {
// Request
(schemas.request || []).forEach((request) => {
if (request.type) {
request.type = request.type.toUpperCase() as FieldDataType;
}
});
// Response
(schemas.response || []).forEach((property) =>
normalizedResponsePropertyType(property)
);

return next();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FieldInType } from '@vulcan/core';
import { SchemaParserMiddleware } from './middleware';

// FieldIn: query => FieldIn FieldInType.QUERY
export const normalizeFieldIn =
(): SchemaParserMiddleware => async (schemas, next) => {
(schemas.request || []).forEach((request) => {
if (request.fieldIn) {
request.fieldIn = request.fieldIn.toUpperCase() as FieldInType;
}
});
return next();
};
36 changes: 36 additions & 0 deletions packages/build/src/lib/schema-parser/middleware/setConstraints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { APISchema, ValidatorLoader } from '@vulcan/core';
import { chain } from 'lodash';
import { SchemaParserMiddleware } from './middleware';

export const setConstraints =
(loader: ValidatorLoader): SchemaParserMiddleware =>
async (rawSchema, next) => {
await next();
const schema = rawSchema as APISchema;
for (const request of schema.request || []) {
request.constraints = chain(request.validators || [])
.map((validator) => ({
validator: loader.getLoader(validator.name),
args: validator.args,
}))
.filter(({ validator }) => !!validator.getConstraints)
.flatMap(({ validator, args }) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
validator.getConstraints!(args)
)
// Group by constraint class (RequiredConstraint, MinValueConstraint ....)
.groupBy((constraint) => constraint.constructor.name)
.values()
.map((constraints) => {
let mergeConstraint = constraints[0];
constraints
.slice(1)
.forEach(
(constraint) =>
(mergeConstraint = mergeConstraint.compose(constraint))
);
return mergeConstraint;
})
.value();
}
};
12 changes: 12 additions & 0 deletions packages/build/src/lib/schema-parser/schemaParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import {
checkParameter,
fallbackErrors,
addMissingErrors,
normalizeFieldIn,
generateDataType,
normalizeDataType,
generatePathParameters,
addRequiredValidatorForPath,
setConstraints,
} from './middleware';
import * as compose from 'koa-compose';
import { inject, injectable, interfaces } from 'inversify';
Expand Down Expand Up @@ -45,6 +51,12 @@ export class SchemaParser {
this.use(transformValidator());
this.use(checkValidator(validatorLoader));
this.use(fallbackErrors());
this.use(normalizeFieldIn());
this.use(generateDataType());
this.use(normalizeDataType());
this.use(generatePathParameters());
this.use(addRequiredValidatorForPath());
this.use(setConstraints(validatorLoader));
}

public async parse({
Expand Down
1 change: 1 addition & 0 deletions packages/build/src/lib/spec-generator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './oas3';
1 change: 1 addition & 0 deletions packages/build/src/lib/spec-generator/oas3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './oas3SpecGenerator';
Loading