Skip to content

Commit

Permalink
Merge branch 'pr-882' into oas3.1
Browse files Browse the repository at this point in the history
  • Loading branch information
carmine committed Aug 24, 2024
2 parents fcc2cb9 + ffd7468 commit b1740a1
Show file tree
Hide file tree
Showing 49 changed files with 1,905 additions and 103 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"scripts": {
"compile": "rimraf dist && tsc",
"compile:release": "rimraf dist && tsc --sourceMap false",
"test": "mocha -r source-map-support/register -r ts-node/register --files --recursive -R spec test/**/*.spec.ts",
"test:debug": "mocha -r source-map-support/register -r ts-node/register --inspect-brk --files --recursive test/**/*.spec.ts",
"test:coverage": "nyc mocha -r source-map-support/register -r ts-node/register --recursive test/**/*.spec.ts",
"test": "mocha -r source-map-support/register -r ts-node/register --files --recursive -R spec --extension .spec.ts test",
"test:debug": "mocha -r source-map-support/register -r ts-node/register --inspect-brk --files --recursive --extension .spec.ts test",
"test:coverage": "nyc mocha -r source-map-support/register -r ts-node/register --recursive --extension .spec.ts test",
"test:reset": "rimraf node_modules && npm i && npm run compile && npm t",
"coveralls": "cat coverage/lcov.info | coveralls -v",
"codacy": "bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r coverage/lcov.info",
Expand Down
6 changes: 3 additions & 3 deletions src/framework/ajv/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,21 @@ interface SerDesSchema extends Partial<SerDes> {
}

export function createRequestAjv(
openApiSpec: OpenAPIV3.Document,
openApiSpec: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1,
options: Options = {},
): AjvDraft4 {
return createAjv(openApiSpec, options);
}

export function createResponseAjv(
openApiSpec: OpenAPIV3.Document,
openApiSpec: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1,
options: Options = {},
): AjvDraft4 {
return createAjv(openApiSpec, options, false);
}

function createAjv(
openApiSpec: OpenAPIV3.Document,
openApiSpec: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1,
options: Options = {},
request = true,
): AjvDraft4 {
Expand Down
8 changes: 4 additions & 4 deletions src/framework/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class OpenAPIFramework {
private loadSpec(
filePath: string | object,
$refParser: { mode: 'bundle' | 'dereference' } = { mode: 'bundle' },
): Promise<OpenAPIV3.Document> {
): Promise<OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1> {
// Because of this issue ( https://github.com/APIDevTools/json-schema-ref-parser/issues/101#issuecomment-421755168 )
// We need this workaround ( use '$RefParser.dereference' instead of '$RefParser.bundle' ) if asked by user
if (typeof filePath === 'string') {
Expand All @@ -87,7 +87,7 @@ export class OpenAPIFramework {
$refParser.mode === 'dereference'
? $RefParser.dereference(absolutePath)
: $RefParser.bundle(absolutePath);
return doc as Promise<OpenAPIV3.Document>;
return doc as Promise<OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1>;
} else {
throw new Error(
`${this.loggingPrefix}spec could not be read at ${filePath}`,
Expand All @@ -98,10 +98,10 @@ export class OpenAPIFramework {
$refParser.mode === 'dereference'
? $RefParser.dereference(filePath)
: $RefParser.bundle(filePath);
return doc as Promise<OpenAPIV3.Document>;
return doc as Promise<OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1>;
}

private sortApiDocTags(apiDoc: OpenAPIV3.Document): void {
private sortApiDocTags(apiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1): void {
if (apiDoc && Array.isArray(apiDoc.tags)) {
apiDoc.tags.sort((a, b): number => {
return a.name < b.name ? -1 : 1;
Expand Down
2 changes: 1 addition & 1 deletion src/framework/openapi.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface RoutePair {
openApiRoute: string;
}
export class OpenApiContext {
public readonly apiDoc: OpenAPIV3.Document;
public readonly apiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1;
public readonly expressRouteMap = {};
public readonly openApiRouteMap = {};
public readonly routes: RouteMetadata[] = [];
Expand Down
43 changes: 34 additions & 9 deletions src/framework/openapi.schema.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import AjvDraft4, {
import addFormats from 'ajv-formats';
// https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v3.0/schema.json
import * as openapi3Schema from './openapi.v3.schema.json';
// https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v3.1/schema.json with dynamic refs replaced due to AJV bug - https://github.com/ajv-validator/ajv/issues/1745
import * as openapi31Schema from './openapi.v3_1.modified.schema.json';
import { OpenAPIV3 } from './types.js';

import Ajv2020 from 'ajv/dist/2020';

export interface OpenAPISchemaValidatorOpts {
version: string;
validateApiSpec: boolean;
Expand All @@ -17,7 +21,6 @@ export class OpenAPISchemaValidator {
private validator: ValidateFunction;
constructor(opts: OpenAPISchemaValidatorOpts) {
const options: Options = {
schemaId: 'id',
allErrors: true,
validateFormats: true,
coerceTypes: false,
Expand All @@ -29,18 +32,40 @@ export class OpenAPISchemaValidator {
options.validateSchema = false;
}

const v = new AjvDraft4(options);
addFormats(v, ['email', 'regex', 'uri', 'uri-reference']);
const [ok, major, minor] = /^(\d+)\.(\d+).(\d+)?$/.exec(opts.version);

if (!ok) {
throw Error('Version missing from OpenAPI specification')
};

if (major !== '3' || minor !== '0' && minor !== '1') {
throw new Error('OpenAPI v3.0 or v3.1 specification version is required');
}

let ajvInstance;
let schema;

if (minor === '0') {
schema = openapi3Schema;
ajvInstance = new AjvDraft4(options);
} else if (minor == '1') {
schema = openapi31Schema;
ajvInstance = new Ajv2020(options);

// Open API 3.1 has a custom "media-range" attribute defined in its schema, but the spec does not define it. "It's not really intended to be validated"
// https://github.com/OAI/OpenAPI-Specification/issues/2714#issuecomment-923185689
// Since the schema is non-normative (https://github.com/OAI/OpenAPI-Specification/pull/3355#issuecomment-1915695294) we will only validate that it's a string
// as the spec states
ajvInstance.addFormat('media-range', true);
}

const ver = opts.version && parseInt(String(opts.version), 10);
if (!ver) throw Error('version missing from OpenAPI specification');
if (ver != 3) throw Error('OpenAPI v3 specification version is required');
addFormats(ajvInstance, ['email', 'regex', 'uri', 'uri-reference']);

v.addSchema(openapi3Schema);
this.validator = v.compile(openapi3Schema);
ajvInstance.addSchema(schema);
this.validator = ajvInstance.compile(schema);
}

public validate(openapiDoc: OpenAPIV3.Document): {
public validate(openapiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1): {
errors: Array<ErrorObject> | null;
} {
const valid = this.validator(openapiDoc);
Expand Down
60 changes: 31 additions & 29 deletions src/framework/openapi.spec.loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from './types';

export interface Spec {
apiDoc: OpenAPIV3.Document;
apiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1;
basePaths: string[];
routes: RouteMetadata[];
serial: number;
Expand All @@ -21,7 +21,7 @@ export interface RouteMetadata {
}

interface DiscoveredRoutes {
apiDoc: OpenAPIV3.Document;
apiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1;
basePaths: string[];
routes: RouteMetadata[];
serial: number;
Expand Down Expand Up @@ -52,42 +52,44 @@ export class OpenApiSpecLoader {
const routes: RouteMetadata[] = [];
const toExpressParams = this.toExpressParams;
// const basePaths = this.framework.basePaths;
// let apiDoc: OpenAPIV3.Document = null;
// let apiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1 = null;
// let basePaths: string[] = null;
const { apiDoc, basePaths } = await this.framework.initialize({
visitApi(ctx: OpenAPIFrameworkAPIContext): void {
const apiDoc = ctx.getApiDoc();
const basePaths = ctx.basePaths;
for (const bpa of basePaths) {
const bp = bpa.replace(/\/$/, '');
for (const [path, methods] of Object.entries(apiDoc.paths)) {
for (const [method, schema] of Object.entries(methods)) {
if (
method.startsWith('x-') ||
['parameters', 'summary', 'description'].includes(method)
) {
continue;
}
const pathParams = new Set<string>();
const parameters = [...schema.parameters ?? [], ...methods.parameters ?? []]
for (const param of parameters) {
if (param.in === 'path') {
pathParams.add(param.name);
if (apiDoc.paths) {
for (const [path, methods] of Object.entries(apiDoc.paths)) {
for (const [method, schema] of Object.entries(methods)) {
if (
method.startsWith('x-') ||
['parameters', 'summary', 'description'].includes(method)
) {
continue;
}
const pathParams = new Set<string>();
const parameters = [...schema.parameters ?? [], ...methods.parameters ?? []]
for (const param of parameters) {
if (param.in === 'path') {
pathParams.add(param.name);
}
}
const openApiRoute = `${bp}${path}`;
const expressRoute = `${openApiRoute}`
.split(':')
.map(toExpressParams)
.join('\\:');

routes.push({
basePath: bp,
expressRoute,
openApiRoute,
method: method.toUpperCase(),
pathParams: Array.from(pathParams),
});
}
const openApiRoute = `${bp}${path}`;
const expressRoute = `${openApiRoute}`
.split(':')
.map(toExpressParams)
.join('\\:');

routes.push({
basePath: bp,
expressRoute,
openApiRoute,
method: method.toUpperCase(),
pathParams: Array.from(pathParams),
});
}
}
}
Expand Down
Loading

0 comments on commit b1740a1

Please sign in to comment.