Skip to content

Commit

Permalink
refactor: add custom resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
magicmatatjahu committed Sep 5, 2022
1 parent 376ef2e commit 3062f85
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 62 deletions.
40 changes: 2 additions & 38 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@types/js-yaml": "^4.0.5",
"@types/json-schema": "^7.0.11",
"@types/lodash": "^4.14.179",
"@types/urijs": "^1.19.19",
"conventional-changelog-conventionalcommits": "^4.2.3",
"cross-env": "^7.0.3",
"eslint": "^7.27.0",
Expand All @@ -52,10 +53,10 @@
"@asyncapi/specs": "^3.1.0",
"@openapi-contrib/openapi-schema-to-json-schema": "^3.2.0",
"@stoplight/json-ref-readers": "^1.2.2",
"@stoplight/json-ref-resolver": "^3.1.4",
"@stoplight/spectral-core": "^1.13.1",
"@stoplight/spectral-functions": "^1.7.1",
"@stoplight/spectral-parsers": "^1.0.2",
"@stoplight/spectral-ref-resolver": "^1.0.1",
"@stoplight/spectral-rulesets": "^1.12.0",
"ajv": "^8.11.0",
"avsc": "^5.7.4",
Expand Down
41 changes: 41 additions & 0 deletions src/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { newAsyncAPIDocument, AsyncAPIDocumentV2, AsyncAPIDocumentV3 } from './models';
import { unstringify } from './stringify';
import { createDetailedAsyncAPI } from './utils';

import {
xParserSpecParsed,
xParserSpecStringified,
} from './constants';

import type { AsyncAPIDocumentInterface } from './models';

export function toAsyncAPIDocument(maybeDoc: unknown): AsyncAPIDocumentInterface | undefined {
if (isAsyncAPIDocument(maybeDoc)) {
return maybeDoc;
}
if (!isParsedDocument(maybeDoc)) {
return;
}
return unstringify(maybeDoc) || newAsyncAPIDocument(createDetailedAsyncAPI(maybeDoc, maybeDoc as any));
}

export function isAsyncAPIDocument(maybeDoc: unknown): maybeDoc is AsyncAPIDocumentInterface {
return maybeDoc instanceof AsyncAPIDocumentV2 || maybeDoc instanceof AsyncAPIDocumentV3;
}

export function isParsedDocument(maybeDoc: unknown): maybeDoc is Record<string, unknown> {
if (typeof maybeDoc !== 'object' || maybeDoc === null) {
return false;
}
return Boolean((maybeDoc as Record<string, unknown>)[xParserSpecParsed]);
}

export function isStringifiedDocument(maybeDoc: unknown): maybeDoc is Record<string, unknown> {
if (typeof maybeDoc !== 'object' || maybeDoc === null) {
return false;
}
return (
Boolean((maybeDoc as Record<string, unknown>)[xParserSpecParsed]) &&
Boolean((maybeDoc as Record<string, unknown>)[xParserSpecStringified])
);
}
3 changes: 2 additions & 1 deletion src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AsyncAPIDocumentInterface, newAsyncAPIDocument } from "./models";

import { customOperations } from './custom-operations';
import { validate } from "./lint";
import { unfreeze } from "./stringify";
import { createDetailedAsyncAPI, normalizeInput, toAsyncAPIDocument, unfreezeObject } from "./utils";

import { xParserSpecParsed } from './constants';
Expand Down Expand Up @@ -47,7 +48,7 @@ export async function parse(parser: Parser, asyncapi: ParseInput, options?: Pars
}

// unfreeze the object - Spectral makes resolved document "freezed"
const validatedDoc = unfreezeObject(validated);
const validatedDoc = unfreeze(validated as Record<string, any>);
validatedDoc[String(xParserSpecParsed)] = true;

const detailed = createDetailedAsyncAPI(asyncapi as string | Record<string, unknown>, validatedDoc);
Expand Down
4 changes: 3 additions & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import type { Spectral } from "@stoplight/spectral-core";
import type { ParseInput, ParseOptions } from "./parse";
import type { LintOptions, ValidateOptions } from "./lint";
import type { SchemaParser } from './schema-parser';
import type { ResolverOptions } from './resolver';

export interface ParserOptions {
schemaParsers?: Array<SchemaParser>;
__unstableResolver?: ResolverOptions;
}

export class Parser {
Expand All @@ -20,7 +22,7 @@ export class Parser {
constructor(
private readonly options?: ParserOptions
) {
this.spectral = createSpectral(this);
this.spectral = createSpectral(this, options);
this.registerSchemaParser(AsyncAPISchemaParser());
this.options?.schemaParsers?.forEach(parser => this.registerSchemaParser(parser));
}
Expand Down
82 changes: 64 additions & 18 deletions src/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,81 @@
import { Resolver as SpectralResolver } from '@stoplight/json-ref-resolver';
import { Resolver as SpectralResolver } from '@stoplight/spectral-ref-resolver';
import { resolveFile, resolveHttp } from '@stoplight/json-ref-readers';

import type Uri from 'urijs';

export interface Resolver {
schema: 'file' | 'http' | 'https' | string;
order?: number;
canRead?: boolean | ((input: ResolverInput) => boolean);
read: (input: ResolverInput) => string | Buffer | Promise<string | Buffer>;
canRead?: boolean | ((uri: Uri, ctx?: any) => boolean);
read: (uri: Uri, ctx?: any) => string | undefined | Promise<string | undefined>;
}

export interface ResolverInput {
url: string;
extension: string;
export interface ResolverOptions {
resolvers?: Array<Resolver>;
}

interface ResolverOptions {
resolvers: Array<Resolver>;
}
export function createResolver(options: ResolverOptions = {}): SpectralResolver {
const availableResolvers: Array<Resolver> = [
...createDefaultResolvers(),
...(options?.resolvers || [])
].map(r => ({
...r,
order: r.order || Number.MAX_SAFE_INTEGER,
canRead: typeof r.canRead === 'undefined' ? true: r.canRead,
}));
const availableSchemas = [...new Set(availableResolvers.map(r => r.schema))];
const resolvers = availableSchemas.reduce((acc, schema) => {
acc[schema] = { resolve: createSchemaResolver(schema, availableResolvers) };
return acc;
}, {} as Record<string, { resolve: (uri: Uri, ctx?: any) => string | Promise<string> }>);

export function createResolver(options?: ResolverOptions): SpectralResolver {
return new SpectralResolver({
resolvers: {
https: { resolve: resolveHttp },
http: { resolve: resolveHttp },
file: { resolve: resolveFile },
},
resolvers: resolvers as any,
});
}

export function createFileResolver() {
function createDefaultResolvers(): Array<Resolver> {
return [
{
schema: 'file',
read: resolveFile as (input: Uri, ctx?: any) => string | Promise<string>,
},
{
schema: 'https',
read: resolveHttp as (input: Uri, ctx?: any) => string | Promise<string>,
},
{
schema: 'http',
read: resolveHttp as (input: Uri, ctx?: any) => string | Promise<string>,
},
]
}

function createSchemaResolver(schema: string, allResolvers: Array<Resolver>): (uri: Uri, ctx?: any) => string | Promise<string> {
const resolvers = allResolvers.filter(r => r.schema === schema).sort((a, b) => { return (a.order as number) - (b.order as number); });
return async (uri, ctx) => {
let result: string | undefined = undefined;
let lastError: Error | undefined;
for (const resolver of resolvers) {
try {
if (!canRead(resolver, uri, ctx)) continue;

result = await resolver.read(uri, ctx);
if (typeof result === 'string') {
break;
}
} catch(e: any) {
lastError = e;
continue;
}
}
if (typeof result !== 'string') {
throw lastError || new Error(`None of the available resolvers for "${schema}" can resolve the given reference.`);
}
return result;
}
}

export function createHttpResolver() {

function canRead(resolver: Resolver, uri: Uri, ctx?: any) {
return typeof resolver.canRead === 'function' ? resolver.canRead(uri, ctx) : resolver.canRead;
}
7 changes: 4 additions & 3 deletions src/spectral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import { Spectral } from "@stoplight/spectral-core";
import { createRulesetFunction } from '@stoplight/spectral-core';
import { asyncapi as aasRuleset } from "@stoplight/spectral-rulesets";

import { createResolver } from './resolver';
import { asyncApi2SchemaParserRule } from './schema-parser/spectral-rule-v2';
import { specVersions } from './constants';
import { isObject } from './utils';

import type { RuleDefinition, RulesetDefinition } from "@stoplight/spectral-core";
import type { Parser } from "./parser";
import type { Parser, ParserOptions } from "./parser";
import type { MaybeAsyncAPI } from "./types";

export function createSpectral(parser: Parser) {
const spectral = new Spectral();
export function createSpectral(parser: Parser, options: ParserOptions = {}) {
const spectral = new Spectral({ resolver: createResolver(options.__unstableResolver) });
const ruleset = configureRuleset(parser);
spectral.setRuleset(ruleset);
return spectral;
Expand Down
7 changes: 7 additions & 0 deletions src/stringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ export function unstringify(document: unknown): AsyncAPIDocumentInterface | unde
return newAsyncAPIDocument(createDetailedAsyncAPI(document as string, parsed as DetailedAsyncAPI['parsed'] ));
}

export function unfreeze(data: Record<string, any>) {
const stringifiedData = JSON.stringify(data, refReplacer());
const unstringifiedData = JSON.parse(stringifiedData);
traverseStringifiedDoc(unstringifiedData, undefined, unstringifiedData, new Map(), new Map());
return unstringifiedData;
}

function refReplacer() {
const modelPaths = new Map();
const paths = new Map();
Expand Down
5 changes: 5 additions & 0 deletions test/mocks/simple-message.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
payload:
type: object
properties:
someProperty:
type: string
Loading

0 comments on commit 3062f85

Please sign in to comment.