Skip to content

Commit

Permalink
feat(#384): Create collection with first variant of each route create…
Browse files Browse the repository at this point in the history
…d from OpenAPI definition
  • Loading branch information
javierbrea committed Aug 24, 2022
1 parent 774b079 commit c205517
Show file tree
Hide file tree
Showing 14 changed files with 281 additions and 38 deletions.
3 changes: 3 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added
- feat: Support asynchronies in files. Files now can export a function. In that case, the loader will receive the result of the function. If function returns a promise, it will receive the result of the promise once it is resolved (rejected promises are treated as file load errors).

### Fixed
- fix: Collections and routes validation was throwing when undefined was passed as value


## [3.10.0] - 2022-08-11

Expand Down
2 changes: 1 addition & 1 deletion packages/core/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = {

// The glob patterns Jest uses to detect test files
testMatch: ["<rootDir>/test/**/*.spec.js"],
// testMatch: ["<rootDir>/test/**/Server.spec.js"],
// testMatch: ["<rootDir>/test/**/validations.spec.js"],

// The test environment that will be used for testing
testEnvironment: "node",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/mock/validations.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ function customValidationSingleMessage(errors) {
.join(". ");
}

function validationSingleMessage(schema, data, errors) {
function validationSingleMessage(schema, data = {}, errors) {
const formattedJson = betterAjvErrors(schema, data, errors, {
format: "js",
});
Expand Down
6 changes: 6 additions & 0 deletions packages/core/test/mock/validations.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,12 @@ describe("mocks validations", () => {
).toEqual(null);
});

it("should return error if collection is undefined", () => {
const errors = collectionValidationErrors();
expect(errors.message).toEqual(expect.stringContaining("type must be object"));
expect(errors.errors.length).toEqual(4);
});

it("should return error if mock has not id", () => {
const errors = collectionValidationErrors({
routes: [],
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-openapi/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = {

// The glob patterns Jest uses to detect test files
testMatch: ["<rootDir>/test/**/*.spec.js"],
// testMatch: ["<rootDir>/test/**/refs.spec.js"],
// testMatch: ["<rootDir>/test/**/collections.spec.js"],

// The test environment that will be used for testing
testEnvironment: "node",
Expand Down
92 changes: 76 additions & 16 deletions packages/plugin-openapi/src/Plugin.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,119 @@
import type { Routes, Core, MockLoaders, FilesContents } from "@mocks-server/core";
import type { Route, Routes, Collections, Collection, Core, MockLoaders, FilesContents, ConfigOption } from "@mocks-server/core";

import { openApisRoutes } from "./openapi";
import { openApiRoutes } from "./openapi";
import type { OpenApiDefinition } from "./types";

const PLUGIN_ID = "openapi";
const DEFAULT_FOLDER = "openapi";

const COLLECTION_NAMESPACE = "collection";

const COLLECTION_OPTIONS = [
{
description: "Name for the collection created from OpenAPI definitions",
name: "id",
type: "string",
default: "openapi",
},
{
description: "Name of the collection to extend from",
name: "from",
type: "string"
},
];

interface RoutesAndCollections {
routes: Routes,
collections: Collections,
}

function getRoutesCollection(routes: Routes, collectionOptions?: OpenApiDefinition.Collection): Collection | null {
if (!collectionOptions) {
return null;
}
return routes.reduce((collection, route: Route) => {
if (route.variants && route.variants.length) {
collection.routes.push(`${route.id}:${route.variants[0].id}`)
}
return collection;
}, { id: collectionOptions.id, from: collectionOptions.from, routes: [] } as Collection);
}

class Plugin {
static get id() {
return PLUGIN_ID;
}

private _config: Core["config"]
private _logger: Core["logger"]
private _alerts: Core["alerts"]
private _files: Core["files"]
private _loadRoutes: MockLoaders["loadRoutes"]
private _loadCollections: MockLoaders["loadCollections"]
private _documentsAlerts: Core["alerts"]
private _collectionNameOption: ConfigOption
private _collectionFromOption: ConfigOption

constructor({ logger, alerts, mock, files }: Core) {
constructor({ logger, alerts, mock, files, config }: Core) {
this._config = config;
this._logger = logger;
this._alerts = alerts;
this._files = files;

const configCollection = this._config.addNamespace(COLLECTION_NAMESPACE);
[this._collectionNameOption, this._collectionFromOption] = configCollection.addOptions(COLLECTION_OPTIONS);

this._documentsAlerts = this._alerts.collection("documents");

const { loadRoutes } = mock.createLoaders();
const { loadRoutes, loadCollections } = mock.createLoaders();
this._loadRoutes = loadRoutes;
this._loadCollections = loadCollections;
this._files.createLoader({
id: PLUGIN_ID,
src: `${DEFAULT_FOLDER}/**/*`,
onLoad: this._onLoadFiles.bind(this),
})
}

async _getRoutesFromFilesContents(filesContents: FilesContents): Promise<Routes> {
const openApiMockDocuments = await Promise.all(
async _getRoutesAndCollectionsFromFilesContents(filesContents: FilesContents): Promise<RoutesAndCollections> {
const openApiRoutesAndCollections = await Promise.all(
filesContents.map((fileDetails) => {
const fileContent = fileDetails.content;
// TODO, validate file content
this._logger.debug(`Creating routes from openApi definitions: '${JSON.stringify(fileContent)}'`);
return openApisRoutes(fileContent, {
defaultLocation: fileDetails.path,
logger: this._logger,
alerts: this._documentsAlerts
return fileContent.map((openAPIDefinition: OpenApiDefinition.Definition) => {
this._logger.debug(`Creating routes from openApi definition: '${JSON.stringify(openAPIDefinition)}'`);
return openApiRoutes(openAPIDefinition, {
defaultLocation: fileDetails.path,
logger: this._logger,
alerts: this._documentsAlerts
}).then((routes) => {
return {
routes,
collection: getRoutesCollection(routes, openAPIDefinition.collection)
}
});
});
})
}).flat()
);
return openApiMockDocuments.flat();

return openApiRoutesAndCollections.reduce((allRoutesAndCollections, definitionRoutesAndCollections) => {
allRoutesAndCollections.routes = allRoutesAndCollections.routes.concat(definitionRoutesAndCollections.routes);
if(definitionRoutesAndCollections.collection) {
allRoutesAndCollections.collections = allRoutesAndCollections.collections.concat(definitionRoutesAndCollections.collection);
}
return allRoutesAndCollections;
}, { routes: [], collections: []});
}

async _onLoadFiles(filesContents: FilesContents) {
this._documentsAlerts.clean();
const routes = await this._getRoutesFromFilesContents(filesContents);
const { routes, collections } = await this._getRoutesAndCollectionsFromFilesContents(filesContents);
const folderTrace = `from OpenAPI definitions found in folder '${this._files.path}/${DEFAULT_FOLDER}'`;
this._logger.debug(`Routes to load from openApi definitions: '${JSON.stringify(routes)}'`);
this._logger.verbose(`Loading ${routes.length} routes from openApi definitions found in folder '${this._files.path}/${DEFAULT_FOLDER}'`);
this._logger.verbose(`Loading ${routes.length} routes ${folderTrace}`);
this._loadRoutes(routes);
this._logger.debug(`Collections to load from OpenAPI definitions: '${JSON.stringify(collections)}'`);
this._logger.verbose(`Loading ${collections.length} collections ${folderTrace}`);
this._loadCollections(collections);
}
}

Expand Down
30 changes: 28 additions & 2 deletions packages/plugin-openapi/src/mocks-server-core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,35 @@ declare module "@mocks-server/core" {
variants: RouteVariants,
}

interface Collection {
id: string,
from: string,
routes: string[],
}

interface OptionProperties {
description: string,
name: string,
type: string,
default?: unknown,
}

interface ConfigOption {
addNamespace(): Config
addOptions(): Config
}

interface Config {
addNamespace(name: string): Config
addOptions(options: OptionProperties[]): ConfigOption[]
}

type Routes = Route[]
type Collections = Collection[]

interface MockLoaders {
loadRoutes(routes: Routes): void
loadRoutes(routes: Routes): void,
loadCollections(collections: Collections): void
}

interface Mock {
Expand All @@ -98,6 +123,7 @@ declare module "@mocks-server/core" {
logger: Logger
alerts: Alerts
files: Files
mock: Mock
mock: Mock,
config: Config,
}
}
18 changes: 5 additions & 13 deletions packages/plugin-openapi/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { resolveRefs } from "json-refs";

import type { ResolvedRefsResults, UnresolvedRefDetails } from "json-refs";
import type { Alerts, HTTPHeaders, Routes, RouteVariant, RouteVariants, RouteVariantTypes } from "@mocks-server/core";
import type { OpenApiRoutes, OpenAPIV3 } from "./types";
import type { OpenApiDefinition, OpenAPIV3 } from "./types";

import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID, VariantTypes, CONTENT_TYPE_HEADER } from "./constants";

Expand Down Expand Up @@ -184,7 +184,7 @@ function openApiPathToRoutes(path: string, basePath = "", openApiPathObject?: Op
}).filter(notEmpty);
}

function openApiDocumentToRoutes(openApiMockDocument: OpenApiRoutes.Document): Routes {
function openApiDocumentToRoutes(openApiMockDocument: OpenApiDefinition.Definition): Routes {
const openApiDocument = openApiMockDocument.document;
const basePath = openApiMockDocument.basePath;

Expand All @@ -210,7 +210,7 @@ function addOpenApiRefAlert(alerts: Alerts, error: Error): void {
alerts.set(String(alerts.flat.length), "Error resolving openapi $ref", error);
}

function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiRoutes.RefsOptions, { alerts, logger }: OpenApiRoutes.Options): Promise<OpenAPIV3.Document | null> {
function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiDefinition.Refs, { alerts, logger }: OpenApiDefinition.Options): Promise<OpenAPIV3.Document | null> {
return resolveRefs(document, refsOptions).then((res) => {
if (logger) {
logger.silly(`Document with resolved refs: '${JSON.stringify(res)}'`);
Expand All @@ -235,7 +235,7 @@ function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiR
});
}

async function resolveOpenApiDocumentRefs(documentMock: OpenApiRoutes.Document, { defaultLocation, alerts, logger }: OpenApiRoutes.Options = {}): Promise<OpenApiRoutes.Document | null> {
async function resolveOpenApiDocumentRefs(documentMock: OpenApiDefinition.Definition, { defaultLocation, alerts, logger }: OpenApiDefinition.Options = {}): Promise<OpenApiDefinition.Definition | null> {
const document = await resolveDocumentRefs(documentMock.document, {location: defaultLocation, ...documentMock.refs}, { alerts, logger });
if(document) {
return {
Expand All @@ -246,18 +246,10 @@ async function resolveOpenApiDocumentRefs(documentMock: OpenApiRoutes.Document,
return null;
}

export async function openApiRoutes(openApiMockDocument: OpenApiRoutes.Document, advancedOptions?: OpenApiRoutes.Options): Promise<Routes> {
export async function openApiRoutes(openApiMockDocument: OpenApiDefinition.Definition, advancedOptions?: OpenApiDefinition.Options): Promise<Routes> {
const openApiDocument = await resolveOpenApiDocumentRefs(openApiMockDocument, advancedOptions);
if(!openApiDocument) {
return [];
}
return openApiDocumentToRoutes(openApiDocument);
}

export function openApisRoutes(openApiMockDocuments: OpenApiRoutes.Document[], advancedOptions?: OpenApiRoutes.Options): Promise<Routes> {
return Promise.all(openApiMockDocuments.map((openApiMockDocument) => {
return openApiRoutes(openApiMockDocument, advancedOptions);
})).then((allRoutes) => {
return allRoutes.flat();
});
}
14 changes: 10 additions & 4 deletions packages/plugin-openapi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export namespace OpenAPIV3 {
}

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace OpenApiRoutes {
export namespace OpenApiDefinition {
export interface Options {
defaultLocation?: string,
// TODO, add alerts type when exported by core
Expand All @@ -37,14 +37,20 @@ export namespace OpenApiRoutes {
logger?: any
}

export interface RefsOptions {
export interface Collection {
id: string,
from: string,
}

export interface Refs {
location?: string,
subDocPath?: string,
}

export interface Document {
export interface Definition {
basePath: string,
refs?: RefsOptions,
refs?: Refs,
collection?: Collection,
document: OpenAPIV3.Document
}
}

0 comments on commit c205517

Please sign in to comment.