Skip to content

Commit f1989d6

Browse files
refactor: add needed mixins (#491)
1 parent ddc0383 commit f1989d6

18 files changed

+553
-33
lines changed

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export const xParserOriginalSchemaFormat = 'x-parser-original-schema-format';
88
export const xParserOriginalTraits = 'x-parser-original-traits';
99

1010
export const xParserCircular = 'x-parser-circular';
11+
12+
export const EXTENSION_REGEX = /^x-[\w\d\.\-\_]+$/;

src/models/asyncapi.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
11
import { InfoInterface } from "./info";
22
import { BaseModel } from "./base";
3+
34
import { AsyncAPIDocumentV2 } from "./v2";
45
import { AsyncAPIDocumentV3 } from "./v3";
56

6-
export interface AsyncAPIDocumentInterface extends BaseModel {
7-
version(): string;
8-
info(): InfoInterface
7+
import { ExternalDocsMixinInterface, SpecificationExtensionsMixinInterface, TagsMixinInterface } from "./mixins";
8+
9+
export interface AsyncAPIDocumentInterface extends BaseModel, ExternalDocsMixinInterface, SpecificationExtensionsMixinInterface, TagsMixinInterface {
10+
version(): string;
11+
info(): InfoInterface;
912
}
1013

1114
export function newAsyncAPIDocument(json: Record<string, any>): AsyncAPIDocumentInterface {
12-
const version = json['asyncapi']; // Maybe this should be an arg.
13-
if (version == undefined || version == null || version == '') {
14-
throw new Error('Missing AsyncAPI version in document');
15-
}
15+
const version = json['asyncapi']; // Maybe this should be an arg.
16+
if (version == undefined || version == null || version == '') {
17+
throw new Error('Missing AsyncAPI version in document');
18+
}
1619

17-
const major = version.split(".")[0];
18-
switch (major) {
19-
case '2':
20-
return new AsyncAPIDocumentV2(json);
21-
case '3':
22-
return new AsyncAPIDocumentV3(json);
23-
default:
24-
throw new Error(`Unsupported version: ${version}`);
25-
}
26-
}
20+
const major = version.split(".")[0];
21+
switch (major) {
22+
case '2':
23+
return new AsyncAPIDocumentV2(json);
24+
case '3':
25+
return new AsyncAPIDocumentV3(json);
26+
default:
27+
throw new Error(`Unsupported version: ${version}`);
28+
}
29+
}

src/models/base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export class BaseModel {
22
constructor(
3-
private readonly _json: Record<string, any>,
3+
protected readonly _json: Record<string, any>,
44
) {}
55

66
json<T = Record<string, any>>(): T;

src/models/mixins/bindings.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { BaseModel } from "../base";
2+
3+
export interface BindingsMixinInterface {
4+
hasBindings(): boolean;
5+
hasBindings(protocol: string): boolean;
6+
bindings(): any[]; // TODO: Change type to Tag
7+
bindings(protocol: string): any; // TODO: Change type to Tag
8+
}
9+
10+
export abstract class BindingsMixin extends BaseModel implements BindingsMixinInterface {
11+
hasBindings(): boolean;
12+
hasBindings(protocol: string): boolean;
13+
hasBindings(protocol?: string): boolean {
14+
const bindings = this.bindings(protocol!);
15+
if (typeof protocol === 'string') {
16+
return Boolean(bindings);
17+
}
18+
return Object.keys(bindings || {}).length > 0;
19+
};
20+
21+
22+
bindings(): any[];
23+
bindings(protocol: string): any;
24+
bindings(protocol?: string): any | any[] {
25+
if (typeof protocol === 'string') {
26+
if (this._json.bindings && typeof this._json.bindings === 'object') {
27+
return this._json.bindings[protocol];
28+
}
29+
return;
30+
}
31+
return this._json.bindings || {};
32+
};
33+
}

src/models/mixins/description.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { BaseModel } from "../base";
2+
3+
export interface DescriptionMixinInterface {
4+
hasDescription(): boolean;
5+
description(): string | undefined;
6+
}
7+
8+
export abstract class DescriptionMixin extends BaseModel implements DescriptionMixinInterface {
9+
hasDescription() {
10+
return Boolean(this._json.description);
11+
};
12+
13+
description(): string | undefined {
14+
return this._json.description;
15+
}
16+
}

src/models/mixins/external-docs.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { BaseModel } from "../base";
2+
3+
export interface ExternalDocsMixinInterface {
4+
hasExternalDocs(): boolean;
5+
externalDocs(): any; // TODO: Change type to ExternalDocs
6+
}
7+
8+
export abstract class ExternalDocsMixin extends BaseModel implements ExternalDocsMixinInterface {
9+
hasExternalDocs(): boolean {
10+
return !!(this._json.externalDocs && Object.keys(this._json.externalDocs).length);
11+
};
12+
13+
// TODO: implement it when the ExternalDocs class will be implemented
14+
externalDocs(): any {
15+
return;
16+
};
17+
}

src/models/mixins/index.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { BaseModel } from '../base';
2+
3+
export * from './bindings';
4+
export * from './description';
5+
export * from './external-docs';
6+
export * from './specification-extensions';
7+
export * from './tags';
8+
9+
export interface Constructor<T = any> extends Function {
10+
new (...any: any[]): T;
11+
}
12+
13+
export interface MixinType<T = any> extends Function {
14+
prototype: T;
15+
}
16+
17+
export function Mixin(a: typeof BaseModel): typeof BaseModel;
18+
export function Mixin<A>(a: typeof BaseModel, b: MixinType<A>): typeof BaseModel & Constructor<A>;
19+
export function Mixin<A, B>(a: typeof BaseModel, b: MixinType<A>, c: MixinType<B>): typeof BaseModel & Constructor<A> & Constructor<B>;
20+
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>;
21+
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>;
22+
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>;
23+
export function Mixin(baseModel: typeof BaseModel, ...constructors: any[]) {
24+
return mixin(class extends baseModel {}, constructors);
25+
}
26+
27+
function mixin(derivedCtor: any, constructors: any[]): typeof BaseModel {
28+
constructors.forEach((baseCtor) => {
29+
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
30+
if (name === 'constructor') {
31+
return;
32+
}
33+
Object.defineProperty(
34+
derivedCtor.prototype,
35+
name,
36+
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null),
37+
);
38+
});
39+
});
40+
return derivedCtor;
41+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { BaseModel } from "../base";
2+
3+
import { EXTENSION_REGEX } from '../../constants';
4+
5+
export interface SpecificationExtensionsMixinInterface {
6+
hasExtensions(): boolean;
7+
hasExtensions(name: string): boolean;
8+
extensions(): Record<string, any>;
9+
extensions(name: string): any;
10+
}
11+
12+
export abstract class SpecificationExtensionsMixin extends BaseModel implements SpecificationExtensionsMixinInterface {
13+
hasExtensions(): boolean;
14+
hasExtensions(name: string): boolean;
15+
hasExtensions(name?: string): boolean {
16+
const extensions = this.extensions(name!);
17+
if (typeof name === 'string') {
18+
return Boolean(extensions);
19+
}
20+
return Object.keys(extensions || {}).length > 0;
21+
};
22+
23+
extensions(): any[];
24+
extensions(name: string): any;
25+
extensions(name?: string): any | any[] {
26+
if (typeof name === 'string') {
27+
name = name.startsWith('x-') ? name : `x-${name}`;
28+
return this._json[name];
29+
}
30+
31+
const result: Record<string, any> = {};
32+
Object.entries(this._json).forEach(([key, value]) => {
33+
if (EXTENSION_REGEX.test(key)) {
34+
result[String(key)] = value;
35+
}
36+
});
37+
return result;
38+
};
39+
}

src/models/mixins/tags.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { BaseModel } from "../base";
2+
3+
export interface TagsMixinInterface {
4+
hasTags(): boolean;
5+
hasTags(name: string): boolean;
6+
tags(): any[]; // TODO: Change type to Tag
7+
tags(name: string): any; // TODO: Change type to Tag
8+
}
9+
10+
export abstract class TagsMixin extends BaseModel implements TagsMixinInterface {
11+
hasTags(): boolean;
12+
hasTags(name: string): boolean;
13+
hasTags(name?: string): boolean {
14+
if (!Array.isArray(this._json.tags) || !this._json.tags.length) {
15+
return false;
16+
}
17+
if (typeof name === 'string') {
18+
return this._json.tags.some((t: any) => t.name === name);
19+
}
20+
return true;
21+
};
22+
23+
24+
// TODO: return instance(s) of Tag model when the Tag class will be implemented
25+
tags(): any[]; // TODO: Change type to Tag
26+
tags(name: string): any; // TODO: Change type to Tag
27+
tags(name?: string): any | any[] { // TODO: Change type to Tag
28+
if (typeof name === 'string') {
29+
if (Array.isArray(this._json.tags)) {
30+
return this._json.tags.find((t: any) => t.name === name);
31+
}
32+
return;
33+
}
34+
return this._json.tags || [];
35+
};
36+
}

src/models/v2/asyncapi.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import { AsyncAPIDocumentInterface } from "../../models";
22
import { BaseModel } from "../base";
33
import { Info } from "./info";
44

5-
export class AsyncAPIDocument extends BaseModel implements AsyncAPIDocumentInterface {
6-
version(): string {
7-
return this.json("asyncapi");
8-
}
9-
10-
info(): Info {
11-
return new Info(this.json("info"));
12-
}
5+
import { Mixin, ExternalDocsMixin, SpecificationExtensionsMixin, TagsMixin } from '../mixins';
6+
7+
export class AsyncAPIDocument
8+
extends Mixin(BaseModel, ExternalDocsMixin, SpecificationExtensionsMixin, TagsMixin)
9+
implements AsyncAPIDocumentInterface {
10+
11+
version(): string {
12+
return this.json("asyncapi");
13+
}
14+
15+
info(): Info {
16+
return new Info(this.json("info"));
17+
}
1318
}

src/models/v3/asyncapi.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import { AsyncAPIDocumentInterface } from "../../models/asyncapi";
22
import { BaseModel } from "../base";
33
import { Info } from "./info";
44

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

10-
info(): Info {
11-
return new Info(this.json("info"));
12-
}
7+
export class AsyncAPIDocument
8+
extends Mixin(BaseModel, ExternalDocsMixin, SpecificationExtensionsMixin, TagsMixin)
9+
implements AsyncAPIDocumentInterface {
10+
11+
version(): string {
12+
return this.json("asyncapi");
13+
}
14+
15+
info(): Info {
16+
return new Info(this.json("info"));
17+
}
1318
}

test/models/mixins/bindings.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { BaseModel } from '../../../src/models/base';
2+
import { BindingsMixin, Mixin } from '../../../src/models/mixins';
3+
4+
class Model extends Mixin(BaseModel, BindingsMixin) {};
5+
6+
const doc1 = { bindings: { amqp: { test: 'test1' } } };
7+
const doc2 = { bindings: {} };
8+
const doc3 = {};
9+
const d1 = new Model(doc1);
10+
const d2 = new Model(doc2);
11+
const d3 = new Model(doc3);
12+
13+
describe('Bindings mixin', function() {
14+
describe('.hasBindings()', function() {
15+
it('should return a boolean indicating if the object has bindings', function() {
16+
expect(d1.hasBindings()).toEqual(true);
17+
expect(d2.hasBindings()).toEqual(false);
18+
expect(d3.hasBindings()).toEqual(false);
19+
});
20+
21+
it('should return a boolean indicating if the bindings object has appropriate binding by name', function() {
22+
expect(d1.hasBindings('amqp')).toEqual(true);
23+
expect(d1.hasBindings('http')).toEqual(false);
24+
expect(d2.hasBindings('amqp')).toEqual(false);
25+
expect(d3.hasBindings('amqp')).toEqual(false);
26+
});
27+
});
28+
29+
describe('.bindings()', function() {
30+
it('should return a map of bindings', function() {
31+
expect(d1.bindings()).toEqual(doc1.bindings);
32+
});
33+
34+
it('should return an empty object', function() {
35+
expect(d2.bindings()).toEqual({});
36+
expect(d3.bindings()).toEqual({});
37+
});
38+
39+
it('should return a binding object', function() {
40+
expect(d1.bindings('amqp')).toEqual(doc1.bindings.amqp);
41+
});
42+
43+
it('should return a undefined', function() {
44+
expect(d1.bindings('http')).toEqual(undefined);
45+
expect(d2.bindings('amqp')).toEqual(undefined);
46+
expect(d3.bindings('amqp')).toEqual(undefined);
47+
});
48+
});
49+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { BaseModel } from '../../../src/models/base';
2+
import { DescriptionMixin, Mixin } from '../../../src/models/mixins';
3+
4+
class Model extends Mixin(BaseModel, DescriptionMixin) {};
5+
6+
const doc1 = { description: 'Testing' };
7+
const doc2 = { description: '' };
8+
const doc3 = {};
9+
const d1 = new Model(doc1);
10+
const d2 = new Model(doc2);
11+
const d3 = new Model(doc3);
12+
13+
describe('Description mixin', function() {
14+
describe('.hasDescription()', function() {
15+
it('should return a boolean indicating if the object has description', function() {
16+
expect(d1.hasDescription()).toEqual(true);
17+
expect(d2.hasDescription()).toEqual(false);
18+
expect(d3.hasDescription()).toEqual(false);
19+
});
20+
});
21+
22+
describe('.description()', function() {
23+
it('should return a value', function() {
24+
expect(d1.description()).toEqual(doc1.description);
25+
expect(d2.description()).toEqual('');
26+
});
27+
28+
it('should return an undefined', function() {
29+
expect(d3.description()).toEqual(undefined);
30+
});
31+
});
32+
});

0 commit comments

Comments
 (0)