Skip to content

Commit

Permalink
fix: schema parsing for v3 (#864)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonaslagoni authored Oct 9, 2023
1 parent 07f3a2e commit a3a4194
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 18 deletions.
9 changes: 6 additions & 3 deletions src/custom-operations/anonymous-naming.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { xParserMessageName, xParserSchemaId } from '../constants';
import { traverseAsyncApiDocument } from '../iterator';
import { setExtension } from '../utils';
import { setExtension, setExtensionOnJson } from '../utils';

import type {
AsyncAPIDocumentInterface,
Expand Down Expand Up @@ -59,12 +59,15 @@ function assignUidToComponentSchemas(document: AsyncAPIDocumentInterface) {
setExtension(xParserSchemaId, schema.id(), schema);
});
}

function assignUidToAnonymousSchemas(doc: AsyncAPIDocumentInterface) {
let anonymousSchemaCounter = 0;
function callback(schema: SchemaInterface) {
const json = schema.json() as any;
const isMultiFormatSchema = json.schema !== undefined;
const underlyingSchema = isMultiFormatSchema ? json.schema : json;
if (!schema.id()) {
setExtension(xParserSchemaId, `<anonymous-schema-${++anonymousSchemaCounter}>`, schema);
setExtensionOnJson(xParserSchemaId, `<anonymous-schema-${++anonymousSchemaCounter}>`, underlyingSchema);
}
}
traverseAsyncApiDocument(doc, callback);
Expand Down
19 changes: 11 additions & 8 deletions src/custom-operations/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { applyTraitsV2, applyTraitsV3 } from './apply-traits';
import { checkCircularRefs } from './check-circular-refs';
import { parseSchemasV2 } from './parse-schema';
import { anonymousNaming } from './anonymous-naming';
import { resolveCircularRefs } from './resolve-circular-refs';
import { parseSchemasV2, parseSchemasV3 } from './parse-schema';
import { anonymousNaming } from './anonymous-naming';

import type { RulesetFunctionContext } from '@stoplight/spectral-core';
import type { Parser } from '../parser';
import type { ParseOptions } from '../parse';
import type { AsyncAPIDocumentInterface } from '../models';
import type { DetailedAsyncAPI } from '../types';
import type { v2, v3 } from '../spec-types';
import { checkCircularRefs } from './check-circular-refs';

export async function customOperations(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, inventory: RulesetFunctionContext['documentInventory'], options: ParseOptions): Promise<void> {
switch (detailed.semver.major) {
Expand All @@ -28,7 +28,7 @@ async function operationsV2(parser: Parser, document: AsyncAPIDocumentInterface,
await parseSchemasV2(parser, detailed);
}

// anonymous naming and resolving circular refrences should be done after custom schemas parsing
// anonymous naming and resolving circular references should be done after custom schemas parsing
if (inventory) {
resolveCircularRefs(document, inventory);
}
Expand All @@ -41,9 +41,12 @@ async function operationsV3(parser: Parser, document: AsyncAPIDocumentInterface,
if (options.applyTraits) {
applyTraitsV3(detailed.parsed as v3.AsyncAPIObject);
}
// TODO: Support schema parsing in v3
// if (options.parseSchemas) {
// await parseSchemasV2(parser, detailed);
// }
if (options.parseSchemas) {
await parseSchemasV3(parser, detailed);
}
// anonymous naming and resolving circular references should be done after custom schemas parsing
if (inventory) {
resolveCircularRefs(document, inventory);
}
anonymousNaming(document);
}
80 changes: 80 additions & 0 deletions src/custom-operations/parse-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ const customSchemasPathsV2 = [
'$.components.messages.*',
];

const customSchemasPathsV3 = [
// channels
'$.channels.*.messages.*.payload',
'$.channels.*.messages.*.headers',
'$.components.channels.*.messages.*.payload',
'$.components.channels.*.messages.*.headers',
// operations
'$.operations.*.messages.*.payload',
'$.operations.*.messages.*.headers',
'$.components.operations.*.messages.*.payload',
'$.components.operations.*.messages.*.headers',
// messages
'$.components.messages.*.payload',
'$.components.messages.*.headers.*',
// schemas
'$.components.schemas.*',
];

export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI) {
const defaultSchemaFormat = getDefaultSchemaFormat(detailed.semver.version);
const parseItems: Array<ToParseItem> = [];
Expand Down Expand Up @@ -65,6 +83,68 @@ export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI)
return Promise.all(parseItems.map(item => parseSchemaV2(parser, item)));
}

export async function parseSchemasV3(parser: Parser, detailed: DetailedAsyncAPI) {
const defaultSchemaFormat = getDefaultSchemaFormat(detailed.semver.version);
const parseItems: Array<ToParseItem> = [];

const visited: Set<unknown> = new Set();
customSchemasPathsV3.forEach(path => {
JSONPath({
path,
json: detailed.parsed,
resultType: 'all',
callback(result) {
const value = result.value;
if (visited.has(value)) {
return;
}
visited.add(value);

const schema = value.schema;
if (!schema) {
return;
}

let schemaFormat = value.schemaFormat;
if (!schemaFormat) {
return;
}
schemaFormat = getSchemaFormat(value.schemaFormat, detailed.semver.version);

parseItems.push({
input: {
asyncapi: detailed,
data: schema,
meta: {
message: value,
},
path: [...splitPath(result.path), 'schema'],
schemaFormat,
defaultSchemaFormat,
},
value,
});
},
});
});

return Promise.all(parseItems.map(item => parseSchemaV3(parser, item)));
}

async function parseSchemaV3(parser: Parser, item: ToParseItem) {
const originalData = item.input.data;
const parsedData = await parseSchema(parser, item.input);
if (item.value?.schema !== undefined) {
item.value.schema = parsedData;
} else {
item.value = parsedData;
}
// save original payload only when data is different (returned by custom parsers)
if (originalData !== parsedData) {
item.value[xParserOriginalPayload] = originalData;
}
}

async function parseSchemaV2(parser: Parser, item: ToParseItem) {
const originalData = item.input.data;
const parsedData = item.value.payload = await parseSchema(parser, item.input);
Expand Down
2 changes: 1 addition & 1 deletion src/models/v3/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class Schema extends BaseModel<v3.MultiFormatSchemaObject, { id?: string,

$schema(): string {
if (typeof this._schemaObject === 'boolean') return 'http://json-schema.org/draft-07/schema#';
return this._schemaObject.$schema || 'http://json-schema.org/draft-07/schema#';
return this._schemaObject.$schema ?? 'http://json-schema.org/draft-07/schema#';
}

additionalItems(): boolean | SchemaInterface {
Expand Down
6 changes: 3 additions & 3 deletions src/spec-types/v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,8 @@ export interface MessageObject extends MessageTraitObject, SpecificationExtensio

export interface MessageTraitObject extends SpecificationExtensions {
messageId?: string;
headers?: SchemaObject;
headers?: MultiFormatSchemaObject;
correlationId?: CorrelationIDObject | ReferenceObject;
schemaFormat?: string;
contentType?: string;
name?: string;
title?: string;
Expand Down Expand Up @@ -403,7 +402,8 @@ export interface OAuthFlowObjectAuthorizationCode extends OAuthFlowObjectBase, S

export type SchemaObject = AsyncAPISchemaObject | ReferenceObject;
export type AsyncAPISchemaObject = AsyncAPISchemaDefinition | boolean;
export type MultiFormatSchemaObject = AsyncAPISchemaObject | { schema: AsyncAPISchemaObject, schemaFormat: string | undefined }
export type MultiFormatObject = { schema: AsyncAPISchemaObject, schemaFormat: string | undefined }
export type MultiFormatSchemaObject = AsyncAPISchemaObject | MultiFormatObject

export interface AsyncAPISchemaDefinition extends SpecificationExtensions {
$id?: string;
Expand Down
8 changes: 6 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,13 @@ export function hasHintDiagnostic(diagnostics: ISpectralDiagnostic[]): boolean {

export function setExtension(id: string, value: unknown, model: BaseModel): void {
const modelValue = model.json();
if (typeof modelValue === 'object' && modelValue) {
setExtensionOnJson(id, value, modelValue);
}

export function setExtensionOnJson(id: string, value: unknown, model: any): void {
if (typeof model === 'object' && model) {
id = id.startsWith('x-') ? id : `x-${id}`;
modelValue[String(id)] = value;
model[String(id)] = value;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Parser } from '../../src/parser';

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

describe('custom operations - parse schemas', function() {
describe('custom operations for v2 - parse schemas', function() {
const parser = new Parser();

it('should parse valid schema format', async function() {
Expand Down
135 changes: 135 additions & 0 deletions test/custom-operations/parse-schema-v3.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { AsyncAPIDocumentV3 } from '../../src/models';
import { Parser } from '../../src/parser';

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

describe('custom operations for v3 - parse schemas', function() {
const parser = new Parser();

it('should parse valid schema format and preserve reference', async function() {
const documentRaw = {
asyncapi: '3.0.0',
info: {
title: 'Valid AsyncApi document',
version: '1.0'
},
channels: {
channel: {
$ref: '#/components/channels/channel'
}
},
operations: {
operation: {
action: 'receive',
channel: {
$ref: '#/channels/channel'
},
messages: [
{
$ref: '#/components/messages/message'
}
]
}
},
components: {
channels: {
channel: {
address: 'channel',
messages: {
message: {
$ref: '#/components/messages/message'
}
}
}
},
messages: {
message: {
headers: {
$ref: '#/components/schemas/schema'
},
payload: {
$ref: '#/components/schemas/schema'
}
}
},
schemas: {
schema: {
schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0',
schema: {
type: 'object'
}
}
}
}
};
const { document, diagnostics } = await parser.parse(documentRaw);

expect(document).toBeInstanceOf(AsyncAPIDocumentV3);
expect(diagnostics.length === 0).toEqual(true);

expect(((document?.json()?.channels?.channel as v3.ChannelObject).messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
expect(((((document?.json() as any).operations?.operation as v3.OperationObject).channel as v3.ChannelObject)?.messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
expect((((document?.json() as any).operations?.operation as v3.OperationObject).messages?.[0] as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
expect(((document?.json()?.components?.channels?.channel as v3.ChannelObject).messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
expect((document?.json()?.components?.messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
expect((((document?.json() as any).components?.messages?.message as v3.MessageObject)?.headers as v3.MultiFormatObject).schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
expect((document?.json() as any).components?.schemas?.schema?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
});

it('should parse valid default schema format', async function() {
const documentRaw = {
asyncapi: '3.0.0',
info: {
title: 'Valid AsyncApi document',
version: '1.0'
},
channels: {
channel: {
address: 'channel',
messages: {
message: {
payload: {
type: 'object'
}
}
}
}
}
};
const { document, diagnostics } = await parser.parse(documentRaw);

expect(document).toBeInstanceOf(AsyncAPIDocumentV3);
expect(diagnostics.length === 0).toEqual(true);

expect(((document?.json()?.channels?.channel as v3.ChannelObject).messages?.message as v3.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
});

it('should parse invalid schema format', async function() {
const documentRaw = {
asyncapi: '3.0.0',
info: {
title: 'Valid AsyncApi document',
version: '1.0'
},
channels: {
channel: {
address: 'channel',
messages: {
message: {
payload: {
schemaFormat: 'not existing',
schema: {
type: 'object'
}
}
}
}
}
}
};
const { document, diagnostics } = await parser.parse(documentRaw);

expect(document).toBeUndefined();
expect(diagnostics.length > 0).toEqual(true);
});
});

0 comments on commit a3a4194

Please sign in to comment.