From 96d6440ae9094982d58b703167884f555f97ec43 Mon Sep 17 00:00:00 2001 From: Julius Recep Colliander Celik Date: Thu, 2 Apr 2020 11:23:56 +0200 Subject: [PATCH] feat: allows bagger to be instantiated within application (#23) (#27) bagger module exports a Bagger class that can be instantiated for multiple instances within an application. The module also has a default export of a default bagger instance. BREAKING CHANGE: bagger can no longer be imported using *. --- lib/bagger.ts | 346 ++++++++++++++++++------------------ lib/request.ts | 17 +- lib/schema_storage.ts | 4 +- test/component.test.ts | 2 +- test/configuration.test.ts | 2 +- test/request.test.ts | 2 +- test/response.test.ts | 2 +- test/schema_storage.test.ts | 6 +- 8 files changed, 200 insertions(+), 181 deletions(-) diff --git a/lib/bagger.ts b/lib/bagger.ts index 4fcec75..fd2e45c 100644 --- a/lib/bagger.ts +++ b/lib/bagger.ts @@ -5,184 +5,192 @@ import { BaggerConfiguration, BaggerConfigurationInternal } from './configuratio import { OpenAPIObject, SecuritySchemeType } from 'openapi3-ts'; import { BaggerParameter, ParameterType } from './parameters'; import { BaggerComponentAdder, BaggerComponentGetter, BaggerSecurityComponent } from './component'; -import { schemaStorage, SchemaDefinition } from './schema_storage'; +import { SchemaStorage, SchemaDefinition } from './schema_storage'; -const internalConfiguration = new BaggerConfigurationInternal(); -const configuration = new BaggerConfiguration(internalConfiguration); -const componentAdder = new BaggerComponentAdder(internalConfiguration); -const componentGetter = new BaggerComponentGetter(internalConfiguration); +export class Bagger { + private internalConfiguration = new BaggerConfigurationInternal(); + private schemaStorage = new SchemaStorage(); + private configuration: BaggerConfiguration; + private componentAdder: BaggerComponentAdder; + private componentGetter: BaggerComponentGetter; -/** - * Creates a Response object - * @param httpCode The HTTP Code that the response represents - * @returns A bagger response that can be used to create a compiled Swagger definition. - * @example - * ```js - * const bagger = require('digitalroute/bagger'); - * - * const getBags = bagger - * .response(200) - * .description('Successfully fetched all bags') - * .content('text/plain', { type: 'string' }); - * ``` - */ -export function response(httpCode: number): BaggerResponse { - return new BaggerResponse(httpCode); -} + public constructor() { + this.configuration = new BaggerConfiguration(this.internalConfiguration); + this.componentAdder = new BaggerComponentAdder(this.internalConfiguration); + this.componentGetter = new BaggerComponentGetter(this.internalConfiguration); + } -/** - * Creates a Request object - * @param path Path representing the request - * @param method HTTP method for this path. Multiple methods can be added for a path by calling - * addRequest for the same path multiple times. - * @example - * ```js - * const bagger = require('.'); - * const joi = require('@hapi/joi'); - * - * bagger - * .addRequest('/bags', 'get') - * .addTag('bags') - * .addTag('getters') - * .addSecurity('OAuth2') - * .addParameter( - * bagger - * .parameter() - * .query('bagSize') - * .schema(joi.string().valid(['10L', '20L', '30L']) - * .required(true) - * ) - * .addResponse( - * bagger - * .response(204) - * .description('Fetched bag of given size') - * ); - * ``` - */ -export function addRequest(path: string, method: Method): BaggerRequest { - const request = new BaggerRequest(path, method); - internalConfiguration.addRequest(request); - return request; -} + /** + * Get the validation object for a specific path and method. The result can be used for validation. + * This comes from `bagger.parameter()` and `bagger.requestBody()`, and they have to be declared on the request before getting them. + * @param path The url/path to the request + * @param method HTTP method like 'GET' or 'PUT' + * @param contentType + */ + public getRequestSchema(path: string, method: string, contentType: string = 'application/json'): SchemaDefinition { + return this.schemaStorage.getRequestSchema(path, method, contentType); + } -export function securityComponent(name: string, type: SecuritySchemeType): BaggerSecurityComponent { - return new BaggerSecurityComponent(name, type); -} + /** + * Creates a Request object + * @param path Path representing the request + * @param method HTTP method for this path. Multiple methods can be added for a path by calling + * addRequest for the same path multiple times. + * @example + * ```js + * const bagger = require('.'); + * const joi = require('@hapi/joi'); + * + * bagger + * .addRequest('/bags', 'get') + * .addTag('bags') + * .addTag('getters') + * .addSecurity('OAuth2') + * .addParameter( + * bagger + * .parameter() + * .query('bagSize') + * .schema(joi.string().valid(['10L', '20L', '30L']) + * .required(true) + * ) + * .addResponse( + * bagger + * .response(204) + * .description('Fetched bag of given size') + * ); + * ``` + */ + public addRequest(path: string, method: Method): BaggerRequest { + const request = new BaggerRequest(path, method, this.schemaStorage); + this.internalConfiguration.addRequest(request); + return request; + } -/** - * Add a reusable component that can be referenced to. - */ -export function addComponent(): BaggerComponentAdder { - return componentAdder; -} + /** + * Add a reusable component that can be referenced to. + */ + public addComponent(): BaggerComponentAdder { + return this.componentAdder; + } -/** - * Get a reusable component. - */ -export function getComponent(): BaggerComponentGetter { - return componentGetter; -} + /** + * Get a reusable component. + */ + public getComponent(): BaggerComponentGetter { + return this.componentGetter; + } -/** - * Create a request body used for defining a body in a bagger request - * @example - * ```js - * const bagger = require('.'); - * const joi = require('@hapi/joi'); - * - * const body = bagger - * .requestBody() - * .description('Create a bag') - * .content('application/json', - * joi.object().keys({ - * type: joi.string().valid(['backpack', 'duffel', 'sports']).required(), - * size: joi.array().items(joi.string().valid(['10L', '20L', '30L']).required()).required(), - * description: joi.string().optional() - * }) - * ) - * .required(true); - * - * bagger - * .addRequest('/bags', 'post') - * .body(body) - * ... - * ``` - */ -export function requestBody(): BaggerRequestBody { - return new BaggerRequestBody(); -} + /** + * Get the configuration object for bagger. This defines the configuration for all endpoints. + * @example + * ```js + * const bagger = require('bagger'); + * bagger + * .configure() + * .info({ + * title: 'Bagger API', + * version: 'v1', + * description: 'Provides resources to building swagger definitions' + * }) + * .addServer({ + * url: 'https://localhost:3000', + * description: 'Local development' + * }) + * ... + * ``` + */ + public configure(): BaggerConfiguration { + return this.configuration; + } -/** - * Create a parameter used for defining query, path, cookie or header parameter in bagger requests. - * @example - * ```js - * const bagger = require('bagger'); - * const parameter = bagger - * .parameter() - * .path('bagId') - * .schema(joi.string().required()) - * .description('ID of one bag') - * .explode(true) - * .required(true); - * - * bagger - * .addRequest('/bags/{bagId}', 'get') - * .addParameter(parameter) - * ... - * ``` - */ -export function parameter(): { [key in ParameterType]: (name: string) => BaggerParameter } { - return { - query: (name: string) => new BaggerParameter('query', name), - path: (name: string) => new BaggerParameter('path', name), - cookie: (name: string) => new BaggerParameter('cookie', name), - header: (name: string) => new BaggerParameter('header', name) - }; -} + /** + * Compiles bagger into an OpenAPI schema. This has to be done _after_ all requests has been added to + * bagger. + * @returns An OpenAPI schema that can be used to render a UI. + */ + public compile(): OpenAPIObject { + return this.internalConfiguration.compile(); + } -/** - * Get the configuration object for bagger. This defines the configuration for all endpoints. - * @example - * ```js - * const bagger = require('bagger'); - * bagger - * .configure() - * .info({ - * title: 'Bagger API', - * version: 'v1', - * description: 'Provides resources to building swagger definitions' - * }) - * .addServer({ - * url: 'https://localhost:3000', - * description: 'Local development' - * }) - * ... - * ``` - */ -export function configure(): BaggerConfiguration { - return configuration; -} + /** + * Creates a Response object + * @param httpCode The HTTP Code that the response represents + * @returns A bagger response that can be used to create a compiled Swagger definition. + * @example + * ```js + * const bagger = require('digitalroute/bagger'); + * + * const getBags = bagger + * .response(200) + * .description('Successfully fetched all bags') + * .content('text/plain', { type: 'string' }); + * ``` + */ + public response(httpCode: number): BaggerResponse { + return new BaggerResponse(httpCode); + } -/** - * Compiles bagger into an OpenAPI schema. This has to be done _after_ all requests has been added to - * bagger. - * @returns An OpenAPI schema that can be used to render a UI. - */ -export function compile(): OpenAPIObject { - return internalConfiguration.compile(); -} + public securityComponent(name: string, type: SecuritySchemeType): BaggerSecurityComponent { + return new BaggerSecurityComponent(name, type); + } -/** - * Get the validation object for a specific path and method. The result can be used for validation. - * This comes from `bagger.parameter()` and `bagger.requestBody()`, and they have to be declared on the request before getting them. - * @param path The url/path to the request - * @param method HTTP method like 'GET' or 'PUT' - * @param contentType - */ -export function getRequestSchema( - path: string, - method: string, - contentType: string = 'application/json' -): SchemaDefinition { - return schemaStorage.getRequestSchema(path, method, contentType); + /** + * Create a request body used for defining a body in a bagger request + * @example + * ```js + * const bagger = require('.'); + * const joi = require('@hapi/joi'); + * + * const body = bagger + * .requestBody() + * .description('Create a bag') + * .content('application/json', + * joi.object().keys({ + * type: joi.string().valid(['backpack', 'duffel', 'sports']).required(), + * size: joi.array().items(joi.string().valid(['10L', '20L', '30L']).required()).required(), + * description: joi.string().optional() + * }) + * ) + * .required(true); + * + * bagger + * .addRequest('/bags', 'post') + * .body(body) + * ... + * ``` + */ + public requestBody(): BaggerRequestBody { + return new BaggerRequestBody(); + } + + /** + * Create a parameter used for defining query, path, cookie or header parameter in bagger requests. + * @example + * ```js + * const bagger = require('bagger'); + * const parameter = bagger + * .parameter() + * .path('bagId') + * .schema(joi.string().required()) + * .description('ID of one bag') + * .explode(true) + * .required(true); + * + * bagger + * .addRequest('/bags/{bagId}', 'get') + * .addParameter(parameter) + * ... + * ``` + */ + public parameter(): { [key in ParameterType]: (name: string) => BaggerParameter } { + return { + query: (name: string) => new BaggerParameter('query', name), + path: (name: string) => new BaggerParameter('path', name), + cookie: (name: string) => new BaggerParameter('cookie', name), + header: (name: string) => new BaggerParameter('header', name) + }; + } } + +const defaultInstance = new Bagger(); +export default defaultInstance; diff --git a/lib/request.ts b/lib/request.ts index e461f06..9ab2a3b 100644 --- a/lib/request.ts +++ b/lib/request.ts @@ -1,7 +1,7 @@ import { BaggerResponse } from './response'; import { BaggerRequestBody } from './request_body'; import { BaggerParameter } from './parameters'; -import { schemaStorage } from './schema_storage'; +import { SchemaStorage } from './schema_storage'; import { OperationObject, SecurityRequirementObject, @@ -33,14 +33,17 @@ type OperationTypes = export class BaggerRequest { private path: string; private method: Method; + private schemaStorage: SchemaStorage; + private operationContext: OperationObject = { responses: {} }; public readonly isBagger = true; - public constructor(path: string, method: Method) { + public constructor(path: string, method: Method, schemaStorage: SchemaStorage) { this.path = path; this.method = method; + this.schemaStorage = schemaStorage; } private setInContext(key: keyof OperationObject, value: OperationTypes): BaggerRequest { @@ -81,7 +84,7 @@ export class BaggerRequest { } public body(requestBody: BaggerRequestBody): BaggerRequest { - schemaStorage.addRequestSchemas(this.path, this.method, requestBody.getSchemas(), 'body'); + this.schemaStorage.addRequestSchemas(this.path, this.method, requestBody.getSchemas(), 'body'); return this.setInContext('requestBody', requestBody.compile()); } @@ -89,7 +92,13 @@ export class BaggerRequest { if (!this.operationContext.parameters) { this.operationContext.parameters = []; } - schemaStorage.addRequestSchemas(this.path, this.method, parameter.getSchemas(), parameter.getType(), parameter.getName()); + this.schemaStorage.addRequestSchemas( + this.path, + this.method, + parameter.getSchemas(), + parameter.getType(), + parameter.getName() + ); this.operationContext.parameters.push(parameter.compile()); return this; } diff --git a/lib/schema_storage.ts b/lib/schema_storage.ts index 5f8047c..f772e3d 100644 --- a/lib/schema_storage.ts +++ b/lib/schema_storage.ts @@ -35,7 +35,7 @@ interface RequestToSchema { type SwaggerLocationTypes = 'cookie' | 'header' | 'path' | 'body' | 'query'; -class SchemaStorage { +export class SchemaStorage { private readonly swaggerKeyToJoiKey = { cookie: 'cookies', header: 'headers', @@ -99,5 +99,3 @@ class SchemaStorage { return schema[contentType]; } } - -export const schemaStorage = new SchemaStorage(); diff --git a/test/component.test.ts b/test/component.test.ts index 6249ce9..4b9c455 100644 --- a/test/component.test.ts +++ b/test/component.test.ts @@ -1,4 +1,4 @@ -import * as bagger from '../lib/bagger'; +import bagger from '../lib/bagger'; import { BaggerSchemaComponent } from '../lib/component'; import joi from '@hapi/joi'; diff --git a/test/configuration.test.ts b/test/configuration.test.ts index f08e3eb..d92d508 100644 --- a/test/configuration.test.ts +++ b/test/configuration.test.ts @@ -1,4 +1,4 @@ -import * as bagger from '../lib/bagger'; +import bagger from '../lib/bagger'; import * as joi from '@hapi/joi'; describe('Bagger compiler', () => { diff --git a/test/request.test.ts b/test/request.test.ts index 0b1c94f..5389530 100644 --- a/test/request.test.ts +++ b/test/request.test.ts @@ -1,4 +1,4 @@ -import * as bagger from '../lib/bagger'; +import bagger from '../lib/bagger'; import * as joi from '@hapi/joi'; describe('Swagger Request', () => { diff --git a/test/response.test.ts b/test/response.test.ts index 870e275..c21171e 100644 --- a/test/response.test.ts +++ b/test/response.test.ts @@ -1,4 +1,4 @@ -import * as bagger from '../lib/bagger'; +import bagger from '../lib/bagger'; import * as joi from '@hapi/joi'; describe('Swagger Response', () => { diff --git a/test/schema_storage.test.ts b/test/schema_storage.test.ts index d6f1c71..8ee09f9 100644 --- a/test/schema_storage.test.ts +++ b/test/schema_storage.test.ts @@ -1,7 +1,11 @@ -import { schemaStorage } from '../lib/schema_storage'; +import { SchemaStorage } from '../lib/schema_storage'; import * as joi from '@hapi/joi'; describe('Schema storage', () => { + let schemaStorage: SchemaStorage; + beforeAll(() => { + schemaStorage = new SchemaStorage(); + }) test('Throw error for key that does not exist', () => { expect(() => schemaStorage.getRequestSchema('a-long-dusty-road', 'put')).toThrow(); });