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

feat: add support for multi-format schema #814

Merged
merged 9 commits into from
Jul 28, 2023
3 changes: 2 additions & 1 deletion src/models/message-trait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type { SchemaInterface } from './schema';

export interface MessageTraitInterface extends BaseModel, BindingsMixinInterface, DescriptionMixinInterface, ExtensionsMixinInterface, ExternalDocumentationMixinInterface, TagsMixinInterface {
id(): string;
schemaFormat(): string;
hasSchemaFormat(): boolean;
schemaFormat(): string | undefined;
hasMessageId(): boolean;
messageId(): string | undefined;
hasCorrelationId(): boolean;
Expand Down
1 change: 1 addition & 0 deletions src/models/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface SchemaInterface extends BaseModel<v2.AsyncAPISchemaObject>, Ext
propertyNames(): SchemaInterface | undefined;
readOnly(): boolean | undefined;
required(): Array<string> | undefined;
schemaFormat(): string
smoya marked this conversation as resolved.
Show resolved Hide resolved
then(): SchemaInterface | undefined;
title(): string | undefined;
type(): string | Array<string> | undefined;
Expand Down
7 changes: 6 additions & 1 deletion src/models/v2/message-trait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { MessageExample } from './message-example';
import { Schema } from './schema';

import { xParserMessageName } from '../../constants';
import { getDefaultSchemaFormat } from '../../schema-parser';
import { bindings, hasDescription, description, extensions, hasExternalDocs, externalDocs, tags } from './mixins';

import type { BindingsInterface } from '../bindings';
Expand All @@ -17,13 +16,19 @@ import type { MessageTraitInterface } from '../message-trait';
import type { SchemaInterface } from '../schema';
import type { TagsInterface } from '../tags';

import { getDefaultSchemaFormat } from '../../schema-parser';
smoya marked this conversation as resolved.
Show resolved Hide resolved

import type { v2 } from '../../spec-types';

export class MessageTrait<J extends v2.MessageTraitObject = v2.MessageTraitObject> extends BaseModel<J, { id: string }> implements MessageTraitInterface {
id(): string {
return this.messageId() || this._meta.id || this.json(xParserMessageName) as string;
}

hasSchemaFormat(): boolean {
return true;
}

schemaFormat(): string {
return this._json.schemaFormat || getDefaultSchemaFormat(this._meta.asyncapi.semver.version);
}
Expand Down
2 changes: 1 addition & 1 deletion src/models/v2/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class Message extends MessageTrait<v2.MessageObject> implements MessageIn

payload(): SchemaInterface | undefined {
if (!this._json.payload) return undefined;
return this.createModel(Schema, this._json.payload, { pointer: `${this._meta.pointer}/payload` });
return this.createModel(Schema, this._json.payload, { pointer: `${this._meta.pointer}/payload`, schemaFormat: this._json.schemaFormat });
}

servers(): ServersInterface {
Expand Down
8 changes: 7 additions & 1 deletion src/models/v2/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import type { ExtensionsInterface } from '../extensions';
import type { ExternalDocumentationInterface } from '../external-docs';
import type { SchemaInterface } from '../schema';

import { getDefaultSchemaFormat } from '../../schema-parser';

import type { v2 } from '../../spec-types';

export class Schema extends BaseModel<v2.AsyncAPISchemaObject, { id?: string, parent?: Schema }> implements SchemaInterface {
export class Schema extends BaseModel<v2.AsyncAPISchemaObject, { id?: string, parent?: Schema, schemaFormat?: string }> implements SchemaInterface {
id(): string {
return this.$id() || this._meta.id || this.json(xParserSchemaId as any) as string;
}
Expand Down Expand Up @@ -267,6 +269,10 @@ export class Schema extends BaseModel<v2.AsyncAPISchemaObject, { id?: string, pa
return this._json.required;
}

schemaFormat(): string {
return this._meta.schemaFormat || getDefaultSchemaFormat(this._meta.asyncapi.semver.version);
}
smoya marked this conversation as resolved.
Show resolved Hide resolved

then(): SchemaInterface | undefined {
if (typeof this._json === 'boolean' || typeof this._json.then !== 'object') return;
return this.createModel(Schema, this._json.then, { pointer: `${this._meta.pointer}/then`, parent: this });
Expand Down
13 changes: 8 additions & 5 deletions src/models/v3/message-trait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { MessageExample } from './message-example';
import { Schema } from './schema';

import { xParserMessageName } from '../../constants';
import { getDefaultSchemaFormat } from '../../schema-parser';
import { CoreModel } from './mixins';

import type { CorrelationIdInterface } from '../correlation-id';
Expand All @@ -19,14 +18,18 @@ export class MessageTrait<J extends v3.MessageTraitObject = v3.MessageTraitObjec
return this.messageId() || this._meta.id || this.extensions().get(xParserMessageName)?.value<string>() as string;
}

schemaFormat(): string {
return this._json.schemaFormat || getDefaultSchemaFormat(this._meta.asyncapi.semver.version);
}

hasMessageId(): boolean {
return !!this._json.messageId;
}

hasSchemaFormat(): boolean {
return false;
}

schemaFormat(): string | undefined {
return undefined;
}

messageId(): string | undefined {
return this._json.messageId;
}
Expand Down
2 changes: 1 addition & 1 deletion src/models/v3/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class Message extends MessageTrait<v3.MessageObject> implements MessageIn

payload(): SchemaInterface | undefined {
if (!this._json.payload) return undefined;
return this.createModel(Schema, this._json.payload, { pointer: this.jsonPath('payload') });
return this.createModel(Schema, this._json.payload, { pointer: this.jsonPath('payload'), schemaFormat: this._json.payload.schemaFormat });
}

servers(): ServersInterface {
Expand Down
20 changes: 7 additions & 13 deletions src/models/v3/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,15 @@ import { BaseModel } from '../base';

import { xParserSchemaId } from '../../constants';
import { extensions, hasExternalDocs, externalDocs } from './mixins';
import { retrievePossibleRef, hasRef } from '../../utils';

import type { ModelMetadata } from '../base';
import type { ExtensionsInterface } from '../extensions';
import type { ExternalDocumentationInterface } from '../external-docs';
import type { SchemaInterface } from '../schema';

import type { v3 } from '../../spec-types';
import { getDefaultSchemaFormat } from '../../schema-parser';

export class Schema extends BaseModel<v3.AsyncAPISchemaObject, { id?: string, parent?: Schema }> implements SchemaInterface {
constructor(
_json: v3.AsyncAPISchemaObject,
_meta: ModelMetadata & { id?: string, parent?: Schema } = {} as any,
) {
_json = retrievePossibleRef(_json, _meta.pointer as string, _meta.asyncapi?.parsed);
super(_json, _meta);
}
import type { v3 } from '../../spec-types';

export class Schema extends BaseModel<v3.AsyncAPISchemaObject, { id?: string, parent?: Schema, schemaFormat?: string }> implements SchemaInterface {
id(): string {
return this.$id() || this._meta.id || this.json(xParserSchemaId as any) as string;
}
Expand Down Expand Up @@ -162,7 +153,6 @@ export class Schema extends BaseModel<v3.AsyncAPISchemaObject, { id?: string, pa
}

isCircular(): boolean {
if (hasRef(this._json)) return true;
let parent = this._meta.parent;
while (parent) {
if (parent._json === this._json) return true;
Expand Down Expand Up @@ -276,6 +266,10 @@ export class Schema extends BaseModel<v3.AsyncAPISchemaObject, { id?: string, pa
return this._json.required;
}

schemaFormat(): string {
return this._meta.schemaFormat || getDefaultSchemaFormat(this._meta.asyncapi.semver.version);
}

then(): SchemaInterface | undefined {
if (typeof this._json === 'boolean' || typeof this._json.then !== 'object') return;
return this.createModel(Schema, this._json.then, { pointer: `${this._meta.pointer}/then`, parent: this });
Expand Down
15 changes: 0 additions & 15 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,21 +144,6 @@ export function findSubArrayIndex(arr: Array<any>, subarr: Array<any>, fromIndex
return -1;
}

export function retrievePossibleRef(data: any, pathOfData: string, spec: any = {}): any {
if (!hasRef(data)) {
return data;
}

const refPath = serializePath(data.$ref);
if (pathOfData.startsWith(refPath)) { // starts by given path
return retrieveDeepData(spec, splitPath(refPath)) || data;
} else if (pathOfData.includes(refPath)) { // circular path in substring of path
const substringPath = pathOfData.split(refPath)[0];
return retrieveDeepData(spec, splitPath(`${substringPath}${refPath}`)) || data;
}
return data;
}

export function resolveServerUrl(url: string): { host: string, pathname: string | undefined } {
// eslint-disable-next-line prefer-const
let [maybeProtocol, maybeHost] = url.split('://');
Expand Down
14 changes: 0 additions & 14 deletions test/models/v2/message.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,6 @@ describe('Message model', function() {
});
});

describe('.schemaFormat()', function() {
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the tests when we still have the functions? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reality is that message should not have the schemaFormat method but only the MessageTrait. See https://github.com/asyncapi/parser-api/pull/86/files#diff-ac35ac6e0b0ad3629802a0e04076aa18e94da614f5392e1164dcaacdfe3ee198L157

Copy link
Member Author

@smoya smoya Jul 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i will need to change the way inheritance is being made atm... Just adding a bit of composition with a intermediate model MessageBase that both Message + MessageTrait extend from.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking again and again, makes sense to keep schemaFormat + add hasSchemaFormat to messageObject as well so we reduce the BC impact. I applied the changes + changes in parser-api. See asyncapi/parser-api#86 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this test method is back now

it('should return defined schemaFormat', function() {
const doc = { schemaFormat: 'customSchemaFormat' };
const d = new Message(doc, { asyncapi: {} as any, pointer: '', id: 'message' });
expect(d.schemaFormat()).toEqual('customSchemaFormat');
});

it('should return default schemaFormat if schemaFormat field is absent', function() {
const doc = {};
const d = new Message(doc, { asyncapi: { semver: { version: '2.0.0' } } as any, pointer: '', id: 'message' });
expect(d.schemaFormat()).toEqual('application/vnd.aai.asyncapi;version=2.0.0');
});
});

describe('.hasPayload()', function() {
it('should return true when there is a value', function() {
const doc = { payload: {} };
Expand Down
16 changes: 16 additions & 0 deletions test/models/v2/schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Schema } from '../../../src/models/v2/schema';
import { assertExtensions, assertExternalDocumentation } from './utils';
import { xParserSchemaId } from '../../../src/constants';

import { getDefaultSchemaFormat } from '../../../src/schema-parser';

import type { v2 } from '../../../src/spec-types';

describe('Channel model', function() {
Expand Down Expand Up @@ -751,6 +753,20 @@ describe('Channel model', function() {
});
});

describe('.schemaFormat()', function() {
it('should return the format of the schema', function() {
const actualSchemaFormat = 'application/vnd.apache.avro;version=1.9.0';
const d = new Schema({}, {asyncapi: {} as any, pointer: '', schemaFormat: actualSchemaFormat});
expect(d.schemaFormat()).toEqual(actualSchemaFormat);
});

it('should return the default schema format where there is no value', function() {
const doc = {asyncapi: '2.6.0' };
const d = new Schema(doc, {asyncapi: { semver: { version: doc.asyncapi } }});
expect(d.schemaFormat()).toEqual(getDefaultSchemaFormat(doc.asyncapi));
});
});

describe('.then()', function() {
it('should return value', function() {
const doc = { then: {} };
Expand Down
14 changes: 8 additions & 6 deletions test/models/v3/message-trait.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ describe('MessageTrait model', function() {
});

describe('.schemaFormat()', function() {
it('should return defined schemaFormat', function() {
it('should return always undefined', function() {
const doc = { schemaFormat: 'customSchemaFormat' };
const d = new MessageTrait(doc, { asyncapi: {} as any, pointer: '', id: 'message' });
expect(d.schemaFormat()).toEqual('customSchemaFormat');
expect(d.schemaFormat()).toBeUndefined();
});
});

it('should return default schemaFormat if schemaFormat field is absent', function() {
const doc = {};
const d = new MessageTrait(doc, { asyncapi: { semver: { version: '2.0.0' } } as any, pointer: '', id: 'message' });
expect(d.schemaFormat()).toEqual('application/vnd.aai.asyncapi;version=2.0.0');
describe('.hasSchemaFormat()', function() {
it('should return always false', function() {
const doc = { schemaFormat: 'customSchemaFormat' };
const d = new MessageTrait(doc, { asyncapi: {} as any, pointer: '', id: 'message' });
expect(d.hasSchemaFormat()).toBeFalsy();
});
});

Expand Down
20 changes: 0 additions & 20 deletions test/models/v3/message.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { Channels } from '../../../src/models/v3/channels';
import { Channel } from '../../../src/models/v3/channel';
import { Message } from '../../../src/models/v3/message';
import { MessageTraits } from '../../../src/models/v3/message-traits';
import { MessageTrait } from '../../../src/models/v3/message-trait';
import { Operations } from '../../../src/models/v3/operations';
import { Operation } from '../../../src/models/v3/operation';
import { Schema } from '../../../src/models/v3/schema';
import { Servers } from '../../../src/models/v3/servers';
import { Server } from '../../../src/models/v3/server';

import { assertCoreModel } from './utils';

Expand All @@ -26,20 +20,6 @@ describe('Message model', function() {
});
});

describe('.schemaFormat()', function() {
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the tests when we still have the functions? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reality is that message should not have the schemaFormat method but only the MessageTrait. See https://github.com/asyncapi/parser-api/pull/86/files#diff-ac35ac6e0b0ad3629802a0e04076aa18e94da614f5392e1164dcaacdfe3ee198L157

it('should return defined schemaFormat', function() {
const doc = { schemaFormat: 'customSchemaFormat' };
const d = new Message(doc, { asyncapi: {} as any, pointer: '', id: 'message' });
expect(d.schemaFormat()).toEqual('customSchemaFormat');
});

it('should return default schemaFormat if schemaFormat field is absent', function() {
const doc = {};
const d = new Message(doc, { asyncapi: { semver: { version: '2.0.0' } } as any, pointer: '', id: 'message' });
expect(d.schemaFormat()).toEqual('application/vnd.aai.asyncapi;version=2.0.0');
});
});

describe('.hasPayload()', function() {
it('should return true when there is a value', function() {
const doc = { payload: {} };
Expand Down
16 changes: 16 additions & 0 deletions test/models/v3/schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Schema } from '../../../src/models/v3/schema';
import { assertExtensions, assertExternalDocumentation } from './utils';
import { xParserSchemaId } from '../../../src/constants';

import { getDefaultSchemaFormat } from '../../../src/schema-parser';

import type { v3 } from '../../../src/spec-types';

describe('Schema model', function() {
Expand Down Expand Up @@ -732,6 +734,20 @@ describe('Schema model', function() {
});
});

describe('.schemaFormat()', function() {
it('should return the format of the schema', function() {
const actualSchemaFormat = 'application/vnd.apache.avro;version=1.9.0';
const d = new Schema({}, {asyncapi: {} as any, pointer: '', schemaFormat: actualSchemaFormat});
expect(d.schemaFormat()).toEqual(actualSchemaFormat);
});

it('should return the default schema format where there is no value', function() {
const doc = {asyncapi: '3.0.0' };
const d = new Schema(doc, {asyncapi: { semver: { version: doc.asyncapi } }});
expect(d.schemaFormat()).toEqual(getDefaultSchemaFormat(doc.asyncapi));
});
});

describe('.then()', function() {
it('should return value', function() {
const doc = { then: {} };
Expand Down