Skip to content

Commit

Permalink
fix(schema): configure missingRefs
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed May 8, 2020
1 parent e5fe1dc commit 3272d6e
Show file tree
Hide file tree
Showing 9 changed files with 48 additions and 34 deletions.
19 changes: 0 additions & 19 deletions src/__tests__/linter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { RuleCollection, Spectral } from '../spectral';

const invalidSchema = JSON.stringify(require('./__fixtures__/petstore.invalid-schema.oas3.json'));
const studioFixture = JSON.stringify(require('./__fixtures__/studio-default-fixture-oas3.json'), null, 2);
const todosInvalid = JSON.stringify(require('./__fixtures__/todos.invalid.oas2.json'));
const petstoreMergeKeys = JSON.stringify(require('./__fixtures__/petstore.merge.keys.oas3.json'));

const fnName = 'fake';
Expand Down Expand Up @@ -655,24 +654,6 @@ responses:: !!foo
]);
});

test('should report invalid schema $refs', async () => {
spectral.registerFormat('oas2', isOpenApiv2);
spectral.registerFormat('oas3', isOpenApiv3);
await spectral.loadRuleset('spectral:oas');

const result = await spectral.run(todosInvalid);

expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'oas2-valid-parameter-example',
message: "can't resolve reference #/parameters/missing from id #",
path: ['paths', '/todos/{todoId}', 'put', 'parameters', '1', 'schema', 'example'],
}),
]),
);
});

test('should report invalid $refs', async () => {
const result = await spectral.run(invalidSchema);

Expand Down
2 changes: 2 additions & 0 deletions src/functions/__tests__/alphabetical.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function runAlphabetical(target: any, keyedBy?: string) {
given: null,
original: null,
documentInventory: new DocumentInventory(new Document(safeStringify(target), Parsers.Json), {} as any),
context: 'resolved',
},
);
}
Expand Down Expand Up @@ -118,6 +119,7 @@ describe('alphabetical', () => {
given: null,
original: null,
documentInventory: new DocumentInventory(document, {} as any),
context: 'resolved',
},
),
).toEqual([
Expand Down
2 changes: 1 addition & 1 deletion src/functions/__tests__/casing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function runCasing(target: unknown, type: CasingType, disallowDigits?: boolean,
target,
{ type, disallowDigits, separator },
{ given: ['$'] },
{ given: null, original: null, documentInventory: {} as any },
{ given: null, original: null, documentInventory: {} as any, context: 'resolved' },
);
}

Expand Down
28 changes: 24 additions & 4 deletions src/functions/__tests__/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Optional } from '@stoplight/types';
import { JSONSchema4, JSONSchema6 } from 'json-schema';
import { IFunctionValues } from '../../types';
import { schema } from '../schema';

function runSchema(target: any, schemaObj: object, oasVersion?: Optional<number>) {
return schema(target, { schema: schemaObj, oasVersion }, { given: [] }, { given: null, original: null } as any);
function runSchema(target: any, schemaObj: object, oasVersion?: Optional<number>, context?: Partial<IFunctionValues>) {
return schema(target, { schema: schemaObj, oasVersion }, { given: [] }, {
given: null,
original: null,
...context,
} as IFunctionValues);
}

describe('schema', () => {
Expand Down Expand Up @@ -321,10 +326,25 @@ describe('schema', () => {
test('pretty-prints path-less property', () => {
const input = { foo: true };
expect(runSchema(input, { additionalProperties: false })).toEqual([
expect.objectContaining({
{
message: 'Property `foo` is not expected to be here',
path: [],
}),
},
]);
});

describe('when schema has a $ref left', () => {
test('given unresolved context, reports an error', () => {
expect(runSchema({}, { $ref: '#/foo' }, void 0, { context: 'unresolved' })).toEqual([
{
message: '{{property|gravis|append-property|optional-typeof|capitalize}}$ref can`t resolve reference #/foo',
path: [],
},
]);
});

test('given resolved context, ignores', () => {
expect(runSchema({}, { $ref: '#/bar' }, void 0, { context: 'resolved' })).toEqual([]);
});
});
});
21 changes: 15 additions & 6 deletions src/functions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@ const logger = {

const ajvInstances = {};

function getAjv(oasVersion: Optional<number>, allErrors: Optional<boolean>): AJV.Ajv {
const type: string = oasVersion && oasVersion >= 2 ? 'oas' + oasVersion : 'jsonschema';
function getAjv(oasVersion: Optional<number>, allErrors: Optional<boolean>, shouldReportMissingRefs: boolean): AJV.Ajv {
const type = [
oasVersion !== void 0 && oasVersion >= 2 ? `oas${oasVersion}` : 'jsonschema',
allErrors,
shouldReportMissingRefs,
].join('');

if (typeof ajvInstances[type] !== 'undefined') {
return ajvInstances[type];
}
Expand All @@ -61,6 +66,10 @@ function getAjv(oasVersion: Optional<number>, allErrors: Optional<boolean>): AJV
schemaId: 'auto',
allErrors,
jsonPointers: true,
// let's ignore any $ref errors if schema fn is provided with already resolved content,
// if our resolver fails to resolve them,
// ajv is unlikely to do it either, since it won't have access to the whole document, but a small portion of it
missingRefs: shouldReportMissingRefs ? 'fail' : 'ignore',
unknownFormats: 'ignore',
nullable: oasVersion === 3, // Support nullable for OAS3
logger,
Expand Down Expand Up @@ -91,8 +100,8 @@ function getSchemaId(schemaObj: JSONSchema): void | string {
}

const validators = new (class extends WeakMap<JSONSchema, ValidateFunction> {
public get({ schema: schemaObj, oasVersion, allErrors }: ISchemaOptions) {
const ajv = getAjv(oasVersion, allErrors);
public assign({ schema: schemaObj, oasVersion, allErrors }: ISchemaOptions, context: string) {
const ajv = getAjv(oasVersion, allErrors, context === 'unresolved');
const schemaId = getSchemaId(schemaObj);
let validator = schemaId !== void 0 ? ajv.getSchema(schemaId) : void 0;
if (validator !== void 0) {
Expand Down Expand Up @@ -143,7 +152,7 @@ const cleanAJVErrorMessage = (message: string, path: Optional<string>, suggestio
}`;
};

export const schema: ISchemaFunction = (targetVal, opts, paths) => {
export const schema: ISchemaFunction = (targetVal, opts, paths, { context }) => {
const results: IFunctionResult[] = [];

const path = paths.target || paths.given;
Expand All @@ -162,7 +171,7 @@ export const schema: ISchemaFunction = (targetVal, opts, paths) => {

try {
// we used the compiled validation now, hence this lookup here (see the logic above for more info)
const validator = opts.ajv ?? validators.get(opts);
const validator = opts.ajv ?? validators.assign(opts, context);
if (!validator(targetVal) && validator.errors) {
opts.prepareResults?.(validator.errors);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ function runPayloadValidation(targetVal: any, field: string) {
targetVal,
{ field },
{ given: ['$', 'components', 'messages', 'aMessage'] },
{ given: null, original: null, documentInventory: {} as any },
{ given: null, original: null, documentInventory: {} as any, context: 'resolved' },
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/rulesets/oas/functions/__tests__/typedEnum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function runTypedEnum(targetVal: any, reportingThreshold: any) {
targetVal,
{ reportingThreshold },
{ given: ['$'] },
{ given: null, original: null, documentInventory: {} as any },
{ given: null, original: null, documentInventory: {} as any, context: 'resolved' },
);
}

Expand Down
5 changes: 3 additions & 2 deletions src/runner/lintNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Document } from '../document';
import { Rule } from '../rule';
import { IMessageVars, message } from '../rulesets/message';
import { getDiagnosticSeverity } from '../rulesets/severity';
import { IFunctionResult, IGivenNode } from '../types';
import { IFunctionResult, IFunctionValues, IGivenNode } from '../types';
import { decodeSegmentFragment, getClosestJsonPath, printPath, PrintStyle } from '../utils';
import { IRunnerInternalContext } from './types';
import { getLintTargets, IExceptionLocation, isAKnownException } from './utils';
Expand All @@ -16,10 +16,11 @@ export const lintNode = (
rule: Rule,
exceptionLocations: Optional<IExceptionLocation[]>,
): void => {
const fnContext = {
const fnContext: IFunctionValues = {
original: node.value,
given: node.value,
documentInventory: context.documentInventory,
context: rule.resolved ? 'resolved' : 'unresolved',
};

const givenPath = node.path.length > 0 && node.path[0] === '$' ? node.path.slice(1) : node.path;
Expand Down
1 change: 1 addition & 0 deletions src/types/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface IFunctionValues {
original: any;
given: any;
documentInventory: DocumentInventory;
context: 'resolved' | 'unresolved';
}

export interface IFunctionResult {
Expand Down

0 comments on commit 3272d6e

Please sign in to comment.