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

Add ref check to validation #120

Merged
merged 4 commits into from
Nov 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
## [Unreleased]
### Changed

## [v2.1.3] 12-11-2023
### Changed
- fix: added addSpecRef() to index.d.ts
- Updated dependencies
- @biomejs/biome ^1.2.2 → ^1.3.3

## [v2.1.2] 27-09-2023
### Changed
- fix: added addSpecRef() to index.d.ts
Expand Down
18 changes: 12 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import addFormats from "ajv-formats";
import Ajv2020 from "ajv/dist/2020.js";
import { readFile } from "fs/promises";
import { JSON_SCHEMA, load } from "js-yaml";
import { resolve } from "./resolve.js";
import { checkRefs, replaceRefs } from "./resolve.js";

const openApiVersions = new Set(["2.0", "3.0", "3.1"]);
const ajvVersions = {
Expand Down Expand Up @@ -79,7 +79,7 @@ export class Validator {
static supportedVersions = openApiVersions;

resolveRefs(opts = {}) {
return resolve(this.specification || opts.specification);
return replaceRefs(this.specification || opts.specification);
}

async addSpecRef(data, uri) {
Expand Down Expand Up @@ -121,12 +121,18 @@ export class Validator {
"Cannot find supported swagger/openapi version in specification, version must be a string.",
};
}
const validate = this.getAjvValidator(version);
const validateSchema = this.getAjvValidator(version);
// check if the specification matches the JSONschema
const schemaResult = validateSchema(specification);
// check if the references are valid as those can't be validated bu JSONschema
if (schemaResult) {
return checkRefs(specification);
}
const result = {
valid: validate(specification),
valid: schemaResult,
};
if (validate.errors) {
result.errors = validate.errors;
if (validateSchema.errors) {
result.errors = validateSchema.errors;
}
return result;
}
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"author": "Hans Klunder",
"license": "MIT",
"devDependencies": {
"@biomejs/biome": "^1.2.2",
"@biomejs/biome": "^1.3.3",
"c8": "^8.0.1"
},
"directories": {
Expand Down
28 changes: 24 additions & 4 deletions resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,20 @@ function resolveUri(uri, anchors) {
}
}

export function resolve(tree) {
export function replaceRefs(tree) {
return resolve(tree, true);
}

export function checkRefs(tree) {
try {
resolve(tree, false);
return { valid: true };
} catch (err) {
return { valid: false, errors: err.message };
}
}

function resolve(tree, replace) {
let treeObj = tree;
if (!isObject(treeObj)) {
return undefined;
Expand Down Expand Up @@ -87,7 +100,9 @@ export function resolve(tree) {
for (const prop in obj) {
if (pointerWords.has(prop)) {
pointers[prop].push({ ref: obj[prop], obj, prop, path, id: objId });
obj[prop] = undefined;
if (replace) {
obj[prop] = undefined;
}
}
parse(obj[prop], `${path}/${escapeJsonPointer(prop)}`, objId);
}
Expand Down Expand Up @@ -130,15 +145,20 @@ export function resolve(tree) {
const { ref, id, path } = item;
const decodedRef = decodeURIComponent(ref);
const fullRef = decodedRef[0] !== "#" ? decodedRef : `${id}${decodedRef}`;
applyRef(path, resolveUri(fullRef, anchors));
const uri = resolveUri(fullRef, anchors);
if (replace) {
applyRef(path, uri);
}
}

for (const item of pointers.$dynamicRef) {
const { ref, path } = item;
if (!dynamicAnchors[ref]) {
throw new Error(`Can't resolve $dynamicAnchor : '${ref}'`);
}
applyRef(path, dynamicAnchors[ref]);
if (replace) {
applyRef(path, dynamicAnchors[ref]);
}
}

return treeObj;
Expand Down
21 changes: 21 additions & 0 deletions test/test-validation-refs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { strict as assert } from "node:assert/strict";
import { test } from "node:test";
import { URL, fileURLToPath } from "url";
import { Validator } from "../index.js";

function localFile(fileName) {
return fileURLToPath(new URL(fileName, import.meta.url));
}

const invalidRefsSpec = localFile("./validation/invalid-refs.yaml");

test("invalid refs in YAML fail validation", async (t) => {
const validator = new Validator();
const res = await validator.validate(invalidRefsSpec);
assert.equal(res.valid, false, "validation fails");
assert.equal(
res.errors,
"Can't resolve #/components/schemas/nonExisting1, only internal refs are supported.",
"correct error message",
);
});
38 changes: 38 additions & 0 deletions test/validation/invalid-refs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
openapi: 3.0.0
info:
title: test
version: 1.0.0
paths:
/get:
get:
summary: test
description: test
parameters:
- name: param
in: query
description: param
required: true
schema:
type: string
example: "foo"
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: "#/components/schemas/nonExisting1"
components:
schemas:
testObject:
type: object
properties:
prop1:
type: string
searchResults:
type: object
properties:
testObjects:
type: array
items:
$ref: "#/components/schemas/nonExisting2"
Loading