Skip to content

Commit

Permalink
Merge b7a090d into a0f2114
Browse files Browse the repository at this point in the history
  • Loading branch information
seriousme committed Oct 9, 2021
2 parents a0f2114 + b7a090d commit feb9476
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 12 deletions.
65 changes: 63 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Where `<filename>` refers to a YAML or JSON file containing the specification.
- [`<instance>.specification`](#specification)
- [`<instance>.version`](#version)
- [`<instance>.resolveRefs(options)`](#resolveRefs)
- [`<instance>.addSpecRef(uri, subSpecification)`](#addSpecRef)
- [`Validator.supportedVersions`](#supportedVersions)

<a name="newValidator"></a>
Expand All @@ -89,7 +90,7 @@ This function tries to validata a specification against the OpenApi schemas. `sp
- a YAML string
- a filename

External references are *not* automatically resolved so you need to inline them yourself if required.
External references are *not* automatically resolved so you need to inline them yourself if required e.g by using `<instance>.addSpecRef()`
The result is an object:
```
{
Expand All @@ -112,11 +113,71 @@ The openApi specification only specifies major/minor versions as separate schema
<a name="resolveRefs"></a>
### `<instance>.resolveRefs(options)`

This function tries to resolve all internal references. External references are *not* automatically resolved so you need to inline them yourself if required. By default it will use the last specification passed to `<instance>.validate()`
This function tries to resolve all internal references. External references are *not* automatically resolved so you need to inline them yourself if required e.g by using `<instance>.addSpecRef()`. By default it will use the last specification passed to `<instance>.validate()`
but you can explicity pass a specification by passing `{specification:<object>}` as options.
The result is an `object` where all references have been resolved.
Resolution of references is `shallow` This should normally not be a problem for this use case.

<a name="addSpecRef"></a>
### `<instance>.addSpecRef(subSpecification, uri)`
`subSpecification` can be one of:

- a JSON object
- a JSON object encoded as string
- a YAML string
- a filename
`uri` must be a string (e.g. `http://www.example.com/subspec`), but is not required if the subSpecification holds a `$id` attribute at top level. If you specify a value for `uri` it will overwrite the definition in the `$id` attribute at top level.

Sometimes a specification is composed of multiple files that each contain parts of the specification. The specification refers to these sub specifications using `external references`. Since references are based on URI's (so Identifier and Location as in URL's!) there needs to be a way to tell the validator how to resolve those references. This is where this function comes in:

E.g.: we have a main specification in `main-spec.yaml` containing:
```yaml
...
paths:
/pet:
post:
tags:
- pet
summary: Add a new pet to the store
description: ''
operationId: addPet
responses:
'405':
description: Invalid input
requestBody:
$ref: 'http://www.example.com/subspec#/components/requestBodies/Pet'
```

And the reference is in `sub-spec.yaml`, containing:
```yaml
components:
requestBodies:
Pet:
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
description: Pet object that needs to be added to the store
required: true
...
```

Then the validation can be performed as follows:

```javascript
const validator = new Validator();
await validator.addSpecRef('http://www.example.com/subspec','./sub-spec.yaml');
const res = await validator.validate("./main-spec.yaml");
// res now contains the results of the validation across main-spec and sub-spec
const specification = validator.specification;
// specification now contains a Javascript object containing the specification
// with the subspec inlined

```

<a name="supportedVersions"></a>
### `Validator.supportedVersions`

Expand Down
29 changes: 27 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ const fs = require("fs");
const readFile = util.promisify(fs.readFile);

const { resolve } = require("./resolve.js");
const { type } = require("os");

const openApiVersions = new Set(["2.0", "3.0", "3.1"]);
const ajvVersions = {
"http://json-schema.org/draft-04/schema#": Ajv04,
"https://json-schema.org/draft/2020-12/schema": Ajv2020,
};
const inlinedRefs = "x-inlined-refs";

function getOpenApiVersion(specification) {
for (const version of openApiVersions) {
Expand Down Expand Up @@ -56,13 +58,33 @@ class Validator {
}
this.ajvOptions = ajvOptions;
this.ajvValidators = {};
this.externalRefs = {};
return this;
}

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

async addSpecRef(data, uri) {
const spec = await getSpecFromData(data);
if (spec === undefined) {
throw new Error("Cannot find JSON, YAML or filename in data");
}
if (uri === undefined){
if (spec['$id'] === undefined){
throw new Error("uri parameter or $id attribute must be present");
}
uri = spec['$id'];
}

if (typeof uri !== "string") {
throw new Error("uri parameter or $id attribute must be a string");
}
spec["$id"] = uri;
this.externalRefs[uri] = spec;
}

async validate(data) {
const specification = await getSpecFromData(data);
this.specification = specification;
Expand All @@ -72,6 +94,9 @@ class Validator {
errors: "Cannot find JSON, YAML or filename in data",
};
}
if (Object.keys(this.externalRefs).length > 0) {
specification[inlinedRefs] = this.externalRefs;
}
const version = getOpenApiVersion(specification);
this.version = version;
if (!version) {
Expand All @@ -92,13 +117,13 @@ class Validator {
}

getAjvValidator(version) {
if (!this.ajvValidators[version]){
if (!this.ajvValidators[version]) {
const schema = require(`./schemas/v${version}/schema.json`);
const schemaVersion = schema.$schema;
const AjvClass = ajvVersions[schemaVersion];
const ajv = new AjvClass(this.ajvOptions);
addFormats(ajv);
ajv.addFormat('media-range',true); // used in 3.1
ajv.addFormat("media-range", true); // used in 3.1
this.ajvValidators[version] = ajv.compile(schema);
}
return this.ajvValidators[version];
Expand Down
7 changes: 7 additions & 0 deletions resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ function resolveUri(uri, anchors) {
if (!anchors[prefix]) {
throw err;
}

if (path === undefined){
return anchors[prefix];
}

const paths = path.split("/").slice(1);
try {
const result = paths.reduce(
Expand All @@ -53,6 +58,8 @@ function resolveUri(uri, anchors) {
} catch (_) {
throw err;
}


}

function resolve(tree) {
Expand Down
84 changes: 76 additions & 8 deletions test/test-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ const localFile = (fileName) =>
const emptySpec = require(`./validation/empty.json`);
const invalidSpec = require(`./validation/invalid-spec.json`);
const yamlFileName = localFile(`./validation/petstore-openapi.v3.yaml`);
const mainSpecYamlFileName = localFile(`./validation/main-spec.v3.yaml`);
const subSpecYamlFileName = localFile(`./validation/sub-spec.v3.yaml`);
const subSpec2YamlFileName = localFile(`./validation/sub-spec2.v3.yaml`);
const subSpecUri = "http://www.example.com/subspec";
const subSpecUri2 = "subspec2";
const inlinedRefs = "x-inlined-refs";

async function testVersion(version) {
test(`version ${version} works`, async (t) => {
Expand Down Expand Up @@ -40,7 +46,7 @@ test(`empty specification should fail`, async (t) => {
t.equal(
res.errors,
"Cannot find supported swagger/openapi version in specification, version must be a string.",
"correct error message"
"correct error message",
);
});

Expand All @@ -60,7 +66,7 @@ test(`undefined specification should fail`, async (t) => {
t.equal(
res.errors,
"Cannot find JSON, YAML or filename in data",
"correct error message"
"correct error message",
);
});

Expand Down Expand Up @@ -101,7 +107,7 @@ test(`Invalid yaml specification as string gives an error`, async (t) => {
t.equal(
res.errors,
"Cannot find JSON, YAML or filename in data",
"error message matches expection"
"error message matches expection",
);
});

Expand All @@ -125,13 +131,13 @@ test(`original petstore spec works`, async (t) => {
t.equal(
ver,
"2.0",
"original petstore spec version matches expected version"
"original petstore spec version matches expected version",
);
const resolvedSpec = validator.resolveRefs();
t.equal(
resolvedSpec.paths["/pet"].post.parameters[0].schema.required[0],
"name",
"$refs are correctly resolved"
"$refs are correctly resolved",
);
});

Expand All @@ -148,13 +154,13 @@ test(`original petstore spec works with AJV strict:"log" option`, async (t) => {
t.equal(
ver,
"2.0",
"original petstore spec version matches expected version"
"original petstore spec version matches expected version",
);
const resolvedSpec = validator.resolveRefs();
t.equal(
resolvedSpec.paths["/pet"].post.parameters[0].schema.required[0],
"name",
"$refs are correctly resolved"
"$refs are correctly resolved",
);
t.equal(logcount > 0, true, "warnings are being logged");
});
Expand All @@ -168,6 +174,68 @@ test(`Invalid filename returns an error`, async (t) => {
t.equal(
res.errors,
"Cannot find JSON, YAML or filename in data",
"error message matches expection"
"error message matches expection",
);
});

test(`addSpecRef: non string URI returns an error`, (t) => {
t.plan(1);
const validator = new Validator();
t.rejects(
validator.addSpecRef(subSpecYamlFileName, null),
"uri parameter or $id attribute must be a string",
"error message matches expection",
{}
);
});

test(`addSpecRef: Invalid filename returns an error`, (t) => {
t.plan(1);
const validator = new Validator();
t.rejects(
validator.addSpecRef("nonExistingFilename", "extraUri"),
"Cannot find JSON, YAML or filename in data",
"error message matches expection",
{}
);
});

test(`addSpecRef: no uri and no $id attribute returns an error`, (t) => {
t.plan(1);
const validator = new Validator();
t.rejects(
validator.addSpecRef(subSpecYamlFileName),
"uri parameter or $id attribute must be present",
"error message matches expection",
{}
);
});

test(`addSpecRef works`, async (t) => {
t.plan(5);
const validator = new Validator();
await validator.addSpecRef(subSpecYamlFileName, subSpecUri);
await validator.addSpecRef(subSpec2YamlFileName);
const res = await validator.validate(mainSpecYamlFileName);
t.equal(res.valid, true, "main spec + subspec is valid");
t.equal(
validator.specification[inlinedRefs][subSpecUri].components.requestBodies
.Pet.required,
true,
);
t.equal(
validator.specification[inlinedRefs][subSpecUri2].get.summary,
"Finds Pets by status",
);
const resolvedSpec = validator.resolveRefs();
t.equal(
resolvedSpec.paths["/pet"].post.requestBody.required,
true,
"$refs from main spec to sub spec are correctly resolved",
);
t.equal(
resolvedSpec.paths["/pet/findByStatus"].get.summary,
"Finds Pets by status",
"$refs from main spec to sub2 spec are correctly resolved",
);
});
27 changes: 27 additions & 0 deletions test/validation/main-spec.v3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
openapi: 3.0.0
servers:
- url: 'http://petstore.swagger.io/v2'
info:
description: >-
This is a sample server Petstore server. For this sample, you can use the api key
`special-key` to test the authorization filters.
version: 1.0.0
title: OpenAPI Petstore
license:
name: Apache-2.0
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
paths:
/pet:
post:
tags:
- pet
summary: Add a new pet to the store
description: ''
operationId: addPet
responses:
'405':
description: Invalid input
requestBody:
$ref: 'http://www.example.com/subspec#/components/requestBodies/Pet'
/pet/findByStatus:
$ref: 'subspec2'

0 comments on commit feb9476

Please sign in to comment.