From 627a24d86cde37e7eb8cdbd5bc22ff972fd5df71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Wed, 19 Aug 2020 15:38:32 +0200 Subject: [PATCH] build(assets): $refs under functionOptions cannot be relative to the document (#1305) --- scripts/generate-assets.ts | 35 ++++++++- src/__tests__/generate-assets.jest.test.ts | 82 +++++++++++++++++++++- src/functions/schema.ts | 8 ++- 3 files changed, 120 insertions(+), 5 deletions(-) diff --git a/scripts/generate-assets.ts b/scripts/generate-assets.ts index 5a792a9d4..9de202787 100644 --- a/scripts/generate-assets.ts +++ b/scripts/generate-assets.ts @@ -12,7 +12,10 @@ import * as path from '@stoplight/path'; import * as fs from 'fs'; import { promisify } from 'util'; import * as $RefParser from '@apidevtools/json-schema-ref-parser'; -import { KNOWN_RULESETS } from "../src/formats"; +import { KNOWN_RULESETS } from '../src/formats'; +import { Dictionary } from '@stoplight/types'; +import { isLocalRef, pointerToPath } from '@stoplight/json'; +import { get } from 'lodash'; const readFileAsync = promisify(fs.readFile); const writeFileAsync = promisify(fs.writeFile); @@ -27,7 +30,7 @@ if (!fs.existsSync(baseDir)) { const generatedAssets = {}; -(async () => { +(async (): Promise => { for (const kind of KNOWN_RULESETS.map(ruleset => ruleset.replace('spectral:', ''))) { const assets = await processDirectory(path.join(__dirname, `../rulesets/${kind}`)); Object.assign(generatedAssets, assets); @@ -48,7 +51,7 @@ async function _processDirectory(assets: Record, dir: string): P } else { let content = await readFileAsync(target, 'utf8'); if (path.extname(name) === '.json') { - content = JSON.stringify(await $RefParser.bundle(target, JSON.parse(content), {})) + content = JSON.stringify(await resolveExternal$Refs(JSON.parse(content), target)); } assets[path.join('@stoplight/spectral', path.relative(path.join(__dirname, '..'), target))] = content; @@ -62,3 +65,29 @@ async function processDirectory(dir: string): Promise> { await _processDirectory(assets, dir); return assets; } + +async function resolveExternal$Refs(document: Dictionary, source: string): Promise { + for (const [key, value] of Object.entries(document)) { + if (key === '$ref') { + if (typeof value === 'string' && !isLocalRef(value)) { + const [filepath, pointer = '#'] = value.split('#'); + + const actualFilepath = path.join(path.dirname(source), filepath); + const referencedDocument = JSON.parse(await readFileAsync(actualFilepath, 'utf8')); + const jsonPath = pointerToPath(pointer); + + return await $RefParser.bundle( + actualFilepath, + jsonPath.length > 0 ? get(referencedDocument, jsonPath) : referencedDocument, + {}, + ); + } + } + + if (value !== null && typeof value === 'object') { + document[key] = await resolveExternal$Refs(value as Dictionary, source); + } + } + + return document; +} diff --git a/src/__tests__/generate-assets.jest.test.ts b/src/__tests__/generate-assets.jest.test.ts index aafd64449..c51bbfcdb 100644 --- a/src/__tests__/generate-assets.jest.test.ts +++ b/src/__tests__/generate-assets.jest.test.ts @@ -1,4 +1,10 @@ import { existsSync, readFileSync } from 'fs'; +import { Spectral } from '../spectral'; +import { setFunctionContext } from '../rulesets/evaluators'; +import { functions } from '../functions'; +import oasDocumentSchema from '../rulesets/oas/functions/oasDocumentSchema'; +import { KNOWN_FORMATS } from '../formats'; +import { DiagnosticSeverity } from '@stoplight/types/dist'; describe('generate-assets', () => { let assets: Record; @@ -37,9 +43,83 @@ describe('generate-assets', () => { ); }); - it('Does not contain test files', () => { + it('does not contain test files', () => { Object.keys(assets).forEach(key => { expect(key).not.toMatch('__tests__'); }); }); + + it('dereferences OAS schemas in the way they can be resolved by Ajv', async () => { + const key = `@stoplight/spectral/rulesets/oas/index.json`; + const ruleset = JSON.parse(assets[key]); + const spectral = new Spectral(); + + for (const [name, fn] of KNOWN_FORMATS) { + spectral.registerFormat(name, fn); + } + + spectral.setFunctions({ oasDocumentSchema: setFunctionContext({ functions }, oasDocumentSchema) }); + spectral.setRules({ + 'oas2-schema': { + ...ruleset.rules['oas2-schema'], + }, + 'oas3-schema': { + ...ruleset.rules['oas3-schema'], + }, + }); + + expect( + await spectral.run({ + openapi: '3.0.0', + info: {}, + paths: { + '/': { + '500': true, + }, + }, + }), + ).toStrictEqual([ + { + code: 'oas3-schema', + message: '`info` property should have required property `title`.', + path: ['info'], + range: expect.any(Object), + severity: DiagnosticSeverity.Error, + }, + { + code: 'oas3-schema', + message: 'Property `500` is not expected to be here.', + path: ['paths', '/'], + range: expect.any(Object), + severity: DiagnosticSeverity.Error, + }, + ]); + + expect( + await spectral.run({ + swagger: '2.0', + info: {}, + paths: { + '/': { + '500': true, + }, + }, + }), + ).toStrictEqual([ + { + code: 'oas2-schema', + message: '`info` property should have required property `title`.', + path: ['info'], + range: expect.any(Object), + severity: DiagnosticSeverity.Error, + }, + { + code: 'oas2-schema', + message: 'Property `500` is not expected to be here.', + path: ['paths', '/'], + range: expect.any(Object), + severity: DiagnosticSeverity.Error, + }, + ]); + }); }); diff --git a/src/functions/schema.ts b/src/functions/schema.ts index 08e3b79d6..71ec79588 100644 --- a/src/functions/schema.ts +++ b/src/functions/schema.ts @@ -88,7 +88,13 @@ const validators = new (class extends WeakMap { public get({ schema: schemaObj, oasVersion, allErrors }: ISchemaOptions): ValidateFunction { const ajv = getAjv(oasVersion, allErrors); const schemaId = getSchemaId(schemaObj); - let validator = schemaId !== void 0 ? ajv.getSchema(schemaId) : void 0; + let validator; + try { + validator = schemaId !== void 0 ? ajv.getSchema(schemaId) : void 0; + } catch { + validator = void 0; + } + if (validator !== void 0) { return validator; }