Skip to content

Commit

Permalink
support splits of the specification file across files #130
Browse files Browse the repository at this point in the history
  • Loading branch information
Carmine DiMascio committed Nov 25, 2019
1 parent c530829 commit 42c776a
Show file tree
Hide file tree
Showing 14 changed files with 241 additions and 293 deletions.
56 changes: 50 additions & 6 deletions package-lock.json

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

7 changes: 4 additions & 3 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "express-openapi-validator",
"version": "2.16.0",
"version": "2.17.0",
"description": "Automatically validate API requests and responses with OpenAPI 3 and Express.",
"main": "dist/index.js",
"scripts": {
Expand All @@ -27,13 +27,14 @@
"license": "MIT",
"dependencies": {
"ajv": "^6.10.2",
"deasync": "^0.1.16",
"js-yaml": "^3.13.1",
"json-schema-ref-parser": "^7.1.2",
"lodash": "^4.17.15",
"lodash.merge": "^4.6.2",
"multer": "^1.4.2",
"ono": "^5.0.1",
"path-to-regexp": "^6.0.0",
"ts-log": "^2.1.4"
"path-to-regexp": "^6.0.0"
},
"devDependencies": {
"@types/ajv": "^1.0.0",
Expand Down
150 changes: 79 additions & 71 deletions src/framework/index.ts
@@ -1,85 +1,57 @@
import * as fs from 'fs';
import * as jsYaml from 'js-yaml';
import * as path from 'path';
import $RefParser from './json.ref.schema';
import { OpenAPISchemaValidator } from './openapi.schema.validator';
import BasePath from './base.path';
import {
ConsoleDebugAdapterLogger,
IOpenAPIFramework,
OpenAPIFrameworkAPIContext,
OpenAPIFrameworkArgs,
OpenAPIFrameworkConstructorArgs,
OpenAPIFrameworkPathContext,
OpenAPIFrameworkPathObject,
OpenAPIFrameworkVisitor,
OpenAPIV3,
} from './types';
import {
copy,
getBasePathsFromServers,
loadSpecFile,
handleYaml,
sortApiDocTags,
} from './util';

export {
BasePath,
OpenAPIFrameworkArgs,
OpenAPIFrameworkConstructorArgs,
OpenAPIFrameworkPathContext,
OpenAPIFrameworkPathObject,
OpenAPIFrameworkAPIContext,
};
export default class OpenAPIFramework implements IOpenAPIFramework {
public readonly apiDoc;
public readonly basePaths: BasePath[];
public readonly featureType;
public readonly loggingPrefix;
public readonly name;

private originalApiDoc;
private validateApiDoc;
private validator;
private logger;

constructor(protected args = {} as OpenAPIFrameworkConstructorArgs) {
this.name = args.name;
this.featureType = args.featureType;
this.loggingPrefix = args.name ? `${this.name}: ` : '';
this.logger = args.logger ? args.logger : new ConsoleDebugAdapterLogger();

const apiDoc =
typeof args.apiDoc === 'string'
? handleYaml(loadSpecFile(args.apiDoc))
: args.apiDoc;

this.originalApiDoc = apiDoc;

if (!this.originalApiDoc) {
throw new Error(`spec could not be read at ${args.apiDoc}`);
}
this.apiDoc = copy(this.originalApiDoc);

this.basePaths = this.apiDoc.openapi
? getBasePathsFromServers(this.apiDoc.servers)
: [
new BasePath({
url: (this.apiDoc.basePath || '').replace(/\/$/, ''),
}),
];

export default class OpenAPIFramework {
public readonly apiDoc: OpenAPIV3.Document;
public readonly basePaths: string[];

private readonly validateApiDoc: boolean;
private readonly validator: OpenAPISchemaValidator;
private readonly basePathObs: BasePath[];
private readonly loggingPrefix = 'openapi.validator: ';

constructor(args = {} as OpenAPIFrameworkConstructorArgs) {
this.apiDoc = this.copy(this.loadSpec(args.apiDoc));
this.basePathObs = this.getBasePathsFromServers(this.apiDoc.servers);
this.basePaths = Array.from(
this.basePathObs.reduce((acc, bp) => {
bp.all().forEach(path => acc.add(path));
return acc;
}, new Set<string>()),
);
this.validateApiDoc =
'validateApiDoc' in args ? !!args.validateApiDoc : true;

this.validator = new OpenAPISchemaValidator({
version: this.apiDoc.openapi,
extensions: this.apiDoc[`x-${this.name}-schema-extension`],
// extensions: this.apiDoc[`x-${args.name}-schema-extension`],
});

if (this.validateApiDoc) {
const apiDocValidation = this.validator.validate(this.apiDoc);

if (apiDocValidation.errors.length) {
this.logger.error(
`${this.loggingPrefix}Validating schema before populating paths`,
);
this.logger.error(
console.error(`${this.loggingPrefix}Validating schema`);
console.error(
`${this.loggingPrefix}validation errors`,
JSON.stringify(apiDocValidation.errors, null, ' '),
);
Expand All @@ -92,33 +64,69 @@ export default class OpenAPIFramework implements IOpenAPIFramework {

public initialize(visitor: OpenAPIFrameworkVisitor) {
const getApiDoc = () => {
return copy(this.apiDoc);
return this.copy(this.apiDoc);
};

sortApiDocTags(this.apiDoc);
this.sortApiDocTags(this.apiDoc);

if (this.validateApiDoc) {
const apiDocValidation = this.validator.validate(this.apiDoc);
if (visitor.visitApi) {
const basePaths = this.basePathObs;
visitor.visitApi({
basePaths,
getApiDoc,
});
}
}

if (apiDocValidation.errors.length) {
this.logger.error(
`${this.loggingPrefix}Validating schema after populating paths`,
);
this.logger.error(
`${this.loggingPrefix}validation errors`,
JSON.stringify(apiDocValidation.errors, null, ' '),
);
private loadSpec(filePath: string | object): OpenAPIV3.Document {
if (typeof filePath === 'string') {
const origCwd = process.cwd();
const specDir = path.resolve(origCwd, path.dirname(filePath));
const absolutePath = path.resolve(origCwd, filePath);
if (fs.existsSync(absolutePath)) {
// Get document, or throw exception on error
try {
process.chdir(specDir);
const docWithRefs = jsYaml.safeLoad(
fs.readFileSync(absolutePath, 'utf8'),
{ json: true },
);
return $RefParser.bundle(docWithRefs);
} finally {
process.chdir(origCwd);
}
} else {
throw new Error(
`${this.loggingPrefix}args.apiDoc was invalid after populating paths. See the output.`,
`${this.loggingPrefix}spec could not be read at ${filePath}`,
);
}
}
return $RefParser.bundle(filePath);
}

if (visitor.visitApi) {
visitor.visitApi({
basePaths: this.basePaths,
getApiDoc,
private copy(obj) {
return JSON.parse(JSON.stringify(obj));
}

private sortApiDocTags(apiDoc) {
if (apiDoc && Array.isArray(apiDoc.tags)) {
apiDoc.tags.sort((a, b) => {
return a.name > b.name;
});
}
}

private getBasePathsFromServers(
servers: OpenAPIV3.ServerObject[],
): BasePath[] {
if (!servers) {
return [new BasePath({ url: '' })];
}
const basePathsMap: { [key: string]: BasePath } = {};
for (const server of servers) {
const basePath = new BasePath(server);
basePathsMap[basePath.path] = basePath;
}
return Object.keys(basePathsMap).map(key => basePathsMap[key]);
}
}
21 changes: 21 additions & 0 deletions src/framework/json.ref.schema.ts
@@ -0,0 +1,21 @@
import * as $RefParser from 'json-schema-ref-parser';
import { loopWhile } from 'deasync';

export default {
bundle: (schema, options?) => {
var savedError,
savedResult,
done = false;

$RefParser.bundle(schema, options, (error, result) => {
savedError = error;
savedResult = result;
done = true;
});

loopWhile(() => !done);

if (savedError) throw savedError;
return savedResult;
},
};
19 changes: 7 additions & 12 deletions src/framework/openapi.context.ts
@@ -1,22 +1,17 @@
import { OpenApiSpecLoader } from './openapi.spec.loader';
import { OpenAPIFrameworkArgs } from './index';
import { OpenAPIV3 } from './types';
import { Spec } from './openapi.spec.loader';

export class OpenApiContext {
// TODO cleanup structure (group related functionality)
public readonly apiDoc: OpenAPIV3.Document;
public readonly expressRouteMap = {};
public readonly openApiRouteMap = {};
public readonly routes = [];
public readonly apiDoc: OpenAPIV3.Document;
private basePaths: Set<string>;

constructor(opts: OpenAPIFrameworkArgs) {
const openApiRouteDiscovery = new OpenApiSpecLoader(opts);
const { apiDoc, basePaths, routes } = openApiRouteDiscovery.load();
private basePaths: string[];

this.apiDoc = apiDoc;
this.basePaths = <Set<string>>basePaths;
this.routes = this.initializeRoutes(routes);
constructor(spec: Spec) {
this.apiDoc = spec.apiDoc;
this.basePaths = spec.basePaths;
this.routes = this.initializeRoutes(spec.routes);
}

private initializeRoutes(routes) {
Expand Down
2 changes: 1 addition & 1 deletion src/framework/openapi.schema.validator.ts
Expand Up @@ -6,7 +6,7 @@ import * as openapi3Schema from './openapi.v3.schema.json';

export class OpenAPISchemaValidator {
private validator: Ajv.ValidateFunction;
constructor({ version, extensions }) {
constructor({ version, extensions }: { version: string; extensions?: any }) {
const v = new Ajv({ schemaId: 'auto', allErrors: true });
v.addMetaSchema(draftSchema);

Expand Down

0 comments on commit 42c776a

Please sign in to comment.