Skip to content

Commit

Permalink
refactor: add needed mixins (asyncapi#491)
Browse files Browse the repository at this point in the history
  • Loading branch information
magicmatatjahu committed Oct 3, 2022
1 parent f03ccbe commit 5bd8e6e
Show file tree
Hide file tree
Showing 18 changed files with 553 additions and 33 deletions.
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

0 comments on commit 5bd8e6e

Please sign in to comment.