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

refactor: add needed mixins #491

Merged
merged 2 commits into from
Mar 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export const xParserOriginalSchemaFormat = 'x-parser-original-schema-format';
export const xParserOriginalTraits = 'x-parser-original-traits';

export const xParserCircular = 'x-parser-circular';

export const EXTENSION_REGEX = /^x-[\w\d\.\-\_]+$/;
37 changes: 20 additions & 17 deletions src/models/asyncapi.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { InfoInterface } from "./info";
import { BaseModel } from "./base";

import { AsyncAPIDocumentV2 } from "./v2";
import { AsyncAPIDocumentV3 } from "./v3";

export interface AsyncAPIDocumentInterface extends BaseModel {
version(): string;
info(): InfoInterface
import { ExternalDocsMixinInterface, SpecificationExtensionsMixinInterface, TagsMixinInterface } from "./mixins";

export interface AsyncAPIDocumentInterface extends BaseModel, ExternalDocsMixinInterface, SpecificationExtensionsMixinInterface, TagsMixinInterface {
version(): string;
info(): InfoInterface;
}

export function newAsyncAPIDocument(json: Record<string, any>): AsyncAPIDocumentInterface {
const version = json['asyncapi']; // Maybe this should be an arg.
if (version == undefined || version == null || version == '') {
throw new Error('Missing AsyncAPI version in document');
}
const version = json['asyncapi']; // Maybe this should be an arg.
if (version == undefined || version == null || version == '') {
throw new Error('Missing AsyncAPI version in document');
}

const major = version.split(".")[0];
switch (major) {
case '2':
return new AsyncAPIDocumentV2(json);
case '3':
return new AsyncAPIDocumentV3(json);
default:
throw new Error(`Unsupported version: ${version}`);
}
}
const major = version.split(".")[0];
switch (major) {
case '2':
return new AsyncAPIDocumentV2(json);
case '3':
return new AsyncAPIDocumentV3(json);
default:
throw new Error(`Unsupported version: ${version}`);
}
}
2 changes: 1 addition & 1 deletion src/models/base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export class BaseModel {
constructor(
private readonly _json: Record<string, any>,
protected readonly _json: Record<string, any>,
) {}

json<T = Record<string, any>>(): T;
Expand Down
33 changes: 33 additions & 0 deletions src/models/mixins/bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { BaseModel } from "../base";

export interface BindingsMixinInterface {
hasBindings(): boolean;
hasBindings(protocol: string): boolean;
bindings(): any[]; // TODO: Change type to Tag
bindings(protocol: string): any; // TODO: Change type to Tag
}

export abstract class BindingsMixin extends BaseModel implements BindingsMixinInterface {
hasBindings(): boolean;
hasBindings(protocol: string): boolean;
hasBindings(protocol?: string): boolean {
const bindings = this.bindings(protocol!);
if (typeof protocol === 'string') {
return Boolean(bindings);
}
return Object.keys(bindings || {}).length > 0;
};


bindings(): any[];
bindings(protocol: string): any;
bindings(protocol?: string): any | any[] {
if (typeof protocol === 'string') {
if (this._json.bindings && typeof this._json.bindings === 'object') {
return this._json.bindings[protocol];
}
return;
}
return this._json.bindings || {};
};
}
16 changes: 16 additions & 0 deletions src/models/mixins/description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BaseModel } from "../base";

export interface DescriptionMixinInterface {
hasDescription(): boolean;
description(): string | undefined;
}

export abstract class DescriptionMixin extends BaseModel implements DescriptionMixinInterface {
hasDescription() {
return Boolean(this._json.description);
};

description(): string | undefined {
return this._json.description;
}
}
17 changes: 17 additions & 0 deletions src/models/mixins/external-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BaseModel } from "../base";

export interface ExternalDocsMixinInterface {
hasExternalDocs(): boolean;
externalDocs(): any; // TODO: Change type to ExternalDocs
}

export abstract class ExternalDocsMixin extends BaseModel implements ExternalDocsMixinInterface {
hasExternalDocs(): boolean {
return !!(this._json.externalDocs && Object.keys(this._json.externalDocs).length);
};

// TODO: implement it when the ExternalDocs class will be implemented
externalDocs(): any {
return;
};
}
41 changes: 41 additions & 0 deletions src/models/mixins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { BaseModel } from '../base';

export * from './bindings';
export * from './description';
export * from './external-docs';
export * from './specification-extensions';
export * from './tags';

export interface Constructor<T = any> extends Function {
new (...any: any[]): T;
}

export interface MixinType<T = any> extends Function {
prototype: T;
}

export function Mixin(a: typeof BaseModel): typeof BaseModel;
export function Mixin<A>(a: typeof BaseModel, b: MixinType<A>): typeof BaseModel & Constructor<A>;
export function Mixin<A, B>(a: typeof BaseModel, b: MixinType<A>, c: MixinType<B>): typeof BaseModel & Constructor<A> & Constructor<B>;
export function Mixin<A, B, C>(a: typeof BaseModel, b: MixinType<A>, c: MixinType<B>, d: MixinType<C>): typeof BaseModel & Constructor<A> & Constructor<B> & Constructor<C>;
export function Mixin<A, B, C, D>(a: typeof BaseModel, b: MixinType<A>, c: MixinType<B>, d: MixinType<C>, e: MixinType<D>): typeof BaseModel & Constructor<B> & Constructor<C> & Constructor<D> & Constructor<D>;
export function Mixin<A, B, C, D, E>(a: typeof BaseModel, b: MixinType<A>, c: MixinType<B>, d: MixinType<C>, e: MixinType<D>, f: MixinType<E>): typeof BaseModel & Constructor<A> & Constructor<B> & Constructor<C> & Constructor<D> & Constructor<E>;
export function Mixin(baseModel: typeof BaseModel, ...constructors: any[]) {
return mixin(class extends baseModel {}, constructors);
}

function mixin(derivedCtor: any, constructors: any[]): typeof BaseModel {
constructors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
if (name === 'constructor') {
return;
}
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null),
);
});
});
return derivedCtor;
}
39 changes: 39 additions & 0 deletions src/models/mixins/specification-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { BaseModel } from "../base";

import { EXTENSION_REGEX } from '../../constants';

export interface SpecificationExtensionsMixinInterface {
hasExtensions(): boolean;
hasExtensions(name: string): boolean;
extensions(): Record<string, any>;
extensions(name: string): any;
}

export abstract class SpecificationExtensionsMixin extends BaseModel implements SpecificationExtensionsMixinInterface {
hasExtensions(): boolean;
hasExtensions(name: string): boolean;
hasExtensions(name?: string): boolean {
const extensions = this.extensions(name!);
if (typeof name === 'string') {
return Boolean(extensions);
}
return Object.keys(extensions || {}).length > 0;
};

extensions(): any[];
extensions(name: string): any;
extensions(name?: string): any | any[] {
if (typeof name === 'string') {
name = name.startsWith('x-') ? name : `x-${name}`;
return this._json[name];
}

const result: Record<string, any> = {};
Object.entries(this._json).forEach(([key, value]) => {
if (EXTENSION_REGEX.test(key)) {
result[String(key)] = value;
}
});
return result;
};
}
36 changes: 36 additions & 0 deletions src/models/mixins/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { BaseModel } from "../base";

export interface TagsMixinInterface {
hasTags(): boolean;
hasTags(name: string): boolean;
tags(): any[]; // TODO: Change type to Tag
tags(name: string): any; // TODO: Change type to Tag
}

export abstract class TagsMixin extends BaseModel implements TagsMixinInterface {
hasTags(): boolean;
hasTags(name: string): boolean;
hasTags(name?: string): boolean {
if (!Array.isArray(this._json.tags) || !this._json.tags.length) {
return false;
}
if (typeof name === 'string') {
return this._json.tags.some((t: any) => t.name === name);
}
return true;
};


// TODO: return instance(s) of Tag model when the Tag class will be implemented
tags(): any[]; // TODO: Change type to Tag
tags(name: string): any; // TODO: Change type to Tag
tags(name?: string): any | any[] { // TODO: Change type to Tag
if (typeof name === 'string') {
if (Array.isArray(this._json.tags)) {
return this._json.tags.find((t: any) => t.name === name);
}
return;
}
return this._json.tags || [];
};
}
21 changes: 13 additions & 8 deletions src/models/v2/asyncapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import { AsyncAPIDocumentInterface } from "../../models";
import { BaseModel } from "../base";
import { Info } from "./info";

export class AsyncAPIDocument extends BaseModel implements AsyncAPIDocumentInterface {
version(): string {
return this.json("asyncapi");
}

info(): Info {
return new Info(this.json("info"));
}
import { Mixin, ExternalDocsMixin, SpecificationExtensionsMixin, TagsMixin } from '../mixins';

export class AsyncAPIDocument
extends Mixin(BaseModel, ExternalDocsMixin, SpecificationExtensionsMixin, TagsMixin)
implements AsyncAPIDocumentInterface {

version(): string {
return this.json("asyncapi");
}

info(): Info {
return new Info(this.json("info"));
}
}
19 changes: 12 additions & 7 deletions src/models/v3/asyncapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import { AsyncAPIDocumentInterface } from "../../models/asyncapi";
import { BaseModel } from "../base";
import { Info } from "./info";

export class AsyncAPIDocument extends BaseModel implements AsyncAPIDocumentInterface {
version(): string {
return this.json("asyncapi");
}
import { Mixin, ExternalDocsMixin, SpecificationExtensionsMixin, TagsMixin } from '../mixins';

info(): Info {
return new Info(this.json("info"));
}
export class AsyncAPIDocument
extends Mixin(BaseModel, ExternalDocsMixin, SpecificationExtensionsMixin, TagsMixin)
implements AsyncAPIDocumentInterface {

version(): string {
return this.json("asyncapi");
}

info(): Info {
return new Info(this.json("info"));
}
}
49 changes: 49 additions & 0 deletions test/models/mixins/bindings.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { BaseModel } from '../../../src/models/base';
import { BindingsMixin, Mixin } from '../../../src/models/mixins';

class Model extends Mixin(BaseModel, BindingsMixin) {};

const doc1 = { bindings: { amqp: { test: 'test1' } } };
const doc2 = { bindings: {} };
const doc3 = {};
const d1 = new Model(doc1);
const d2 = new Model(doc2);
const d3 = new Model(doc3);

describe('Bindings mixin', function() {
describe('.hasBindings()', function() {
it('should return a boolean indicating if the object has bindings', function() {
expect(d1.hasBindings()).toEqual(true);
expect(d2.hasBindings()).toEqual(false);
expect(d3.hasBindings()).toEqual(false);
});

it('should return a boolean indicating if the bindings object has appropriate binding by name', function() {
expect(d1.hasBindings('amqp')).toEqual(true);
expect(d1.hasBindings('http')).toEqual(false);
expect(d2.hasBindings('amqp')).toEqual(false);
expect(d3.hasBindings('amqp')).toEqual(false);
});
});

describe('.bindings()', function() {
it('should return a map of bindings', function() {
expect(d1.bindings()).toEqual(doc1.bindings);
});

it('should return an empty object', function() {
expect(d2.bindings()).toEqual({});
expect(d3.bindings()).toEqual({});
});

it('should return a binding object', function() {
expect(d1.bindings('amqp')).toEqual(doc1.bindings.amqp);
});

it('should return a undefined', function() {
expect(d1.bindings('http')).toEqual(undefined);
expect(d2.bindings('amqp')).toEqual(undefined);
expect(d3.bindings('amqp')).toEqual(undefined);
});
});
});
32 changes: 32 additions & 0 deletions test/models/mixins/description.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BaseModel } from '../../../src/models/base';
import { DescriptionMixin, Mixin } from '../../../src/models/mixins';

class Model extends Mixin(BaseModel, DescriptionMixin) {};

const doc1 = { description: 'Testing' };
const doc2 = { description: '' };
const doc3 = {};
const d1 = new Model(doc1);
const d2 = new Model(doc2);
const d3 = new Model(doc3);

describe('Description mixin', function() {
describe('.hasDescription()', function() {
it('should return a boolean indicating if the object has description', function() {
expect(d1.hasDescription()).toEqual(true);
expect(d2.hasDescription()).toEqual(false);
expect(d3.hasDescription()).toEqual(false);
});
});

describe('.description()', function() {
it('should return a value', function() {
expect(d1.description()).toEqual(doc1.description);
expect(d2.description()).toEqual('');
});

it('should return an undefined', function() {
expect(d3.description()).toEqual(undefined);
});
});
});
Loading