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

support external file refs #38

Merged
merged 10 commits into from
Dec 11, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ Array that contains:
##### Options

Options currently supports:.
- `formats` - Array of formats that can be added to `ajv` configuration, each element in the array should include `name` and `pattern`.
- `keywords` - Array of keywords that can be added to `ajv` configuration, each element in the array can be either an object or a function.
If the element is an object, it must include `name` and `definition`. If the element is a function, it should accept `ajv` as its first argument and inside the function you need to call `ajv.addKeyword` to add your custom keyword
- `makeOptionalAttributesNullable` - Boolean that forces preprocessing of Swagger schema to include 'null' as possible type for all non-required properties. Main use-case for this is to ensure correct handling of null values when Ajv type coercion is enabled
Expand All @@ -83,14 +82,16 @@ If the element is an object, it must include `name` and `definition`. If the ele
- `expectFormFieldsInBody` - Boolean that indicates whether form fields of non-file type that are specified in the schema should be validated against request body (e. g. Multer is copying text form fields to body)
- `buildRequests` - Boolean that indicates whether if create validators for requests, default is true.
- `buildResponses` - Boolean that indicates whether if create validators for responses, default is false.
- `basePath` - Base path of the external definition files referenced in the given schema. This is required whenever passing json schema instead of `PathToSwaggerFile` to the constructor or the external files are not stored in the same path of `PathToSwaggerFile`
- `formats` - Array of formats that can be added to `ajv` configuration, each element in the array should include `name` and `pattern`.

```js
formats: [
{ name: 'double', pattern: /\d+\.(\d+)+/ },
{ name: 'int64', pattern: /^\d{1,19}$/ },
{ name: 'int32', pattern: /^\d{1,10}$/ }
]
```
```js
formats: [
{ name: 'double', pattern: /\d+\.(\d+)+/ },
{ name: 'int64', pattern: /^\d{1,19}$/ },
{ name: 'int32', pattern: /^\d{1,10}$/ }
]
```

### api-schema-builder.buildSchema(jsonSchema, options)

Expand Down
5 changes: 2 additions & 3 deletions 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 @@ -48,7 +48,7 @@
"clone-deep": "^4.0.1",
"decimal.js": "^10.2.0",
"js-yaml": "^3.13.1",
"json-schema-deref-sync": "^0.10.1",
"json-schema-deref-sync": "github:cvent/json-schema-deref-sync",
kobik marked this conversation as resolved.
Show resolved Hide resolved
"object.values": "^1.1.0",
"swagger-parser": "^6.0.5"
},
Expand Down
24 changes: 8 additions & 16 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
const get = require('lodash.get');
const Ajv = require('ajv');
const deref = require('json-schema-deref-sync');
const fs = require('fs');
const yaml = require('js-yaml');
const SwaggerParser = require('swagger-parser');

const { defaultFormatsValidators } = require('./validators/formatValidators.js');
const schemaPreprocessor = require('./utils/schema-preprocessor');
const schemaLoaders = require('./utils/schemaLoaders');
const oai3 = require('./parsers/open-api3');
const oai2 = require('./parsers/open-api2');
const ajvUtils = require('./utils/ajv-utils');
Expand All @@ -32,24 +31,17 @@ function buildSchema(swaggerPath, options) {
}

function buildSchemaSync(pathOrSchema, options) {
const jsonSchema = getJsonSchema(pathOrSchema);
const dereferencedSchema = deref(jsonSchema);
const jsonSchema = schemaUtils.getJsonSchema(pathOrSchema);
const basePath = schemaUtils.getSchemaBasePath(pathOrSchema, options);
const dereferencedSchema = deref(jsonSchema, {
baseFolder: basePath,
failOnMissing: true,
loaders: schemaLoaders
});

return buildValidations(jsonSchema, dereferencedSchema, options);
}

function getJsonSchema(pathOrSchema) {
if (typeof pathOrSchema === 'string') {
// file
const fileContents = fs.readFileSync(pathOrSchema);
const jsonSchema = yaml.load(fileContents, 'utf8');
return jsonSchema;
} else {
// json schema
return pathOrSchema;
}
}

function buildValidations(referenced, dereferenced, receivedOptions) {
const options = getOptions(receivedOptions);

Expand Down
40 changes: 40 additions & 0 deletions src/utils/schemaLoaders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const fs = require('fs');
const path = require('path');
const jsyaml = require('js-yaml');

const cwd = process.cwd();

const file = function (refValue, options) {
kobik marked this conversation as resolved.
Show resolved Hide resolved
let refPath = refValue;
const baseFolder = options.baseFolder ? path.resolve(cwd, options.baseFolder) : cwd;

if (refPath.indexOf('file:') === 0) {
refPath = refPath.substring(5);
} else {
refPath = path.resolve(baseFolder, refPath);
}

const filePath = getRefFilePath(refPath);
const filePathLowerCase = filePath.toLowerCase();

try {
var data = fs.readFileSync(filePath, 'utf8');
if (filePathLowerCase.endsWith('.json')) {
return JSON.parse(data);
} else if (filePathLowerCase.endsWith('.yml') || filePathLowerCase.endsWith('.yaml')) {
return jsyaml.load(data);
}
} catch (e) { }
};

function getRefFilePath(refPath) {
let filePath = refPath;
const hashIndex = filePath.indexOf('#');
if (hashIndex > 0) {
filePath = refPath.substring(0, hashIndex);
}

return filePath;
}

module.exports = { file };
33 changes: 32 additions & 1 deletion src/utils/schemaUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const values = require('object.values');
const fs = require('fs');
const yaml = require('js-yaml');
const path = require('path');

const { readOnly, writeOnly, validationTypes, allDataTypes } = require('./common');

Expand Down Expand Up @@ -144,11 +147,39 @@ function getSchemaType(dereferencedSchema) {
}
}

function getJsonSchema(pathOrSchema) {
if (typeof pathOrSchema === 'string') {
// file path
const fileContents = fs.readFileSync(pathOrSchema);
const jsonSchema = yaml.load(fileContents, 'utf8');
return jsonSchema;
} else {
// json schema
return pathOrSchema;
}
}

function getSchemaBasePath(pathOrSchema, options = {}) {
// always return basePath from options if exists
if (options.basePath) {
return options.basePath;
}

// in case a path to defintions file was given
if (typeof pathOrSchema === 'string') {
const fullPath = path.resolve(pathOrSchema).split('/');
kobik marked this conversation as resolved.
Show resolved Hide resolved
fullPath.pop();
return fullPath.join('/');
}
}

module.exports = {
DEFAULT_RESPONSE_CONTENT_TYPE,
DEFAULT_REQUEST_CONTENT_TYPE,
getAllResponseContentTypes,
addOAI3Support,
getSchemaType,
isOpenApi3
isOpenApi3,
getJsonSchema,
getSchemaBasePath
};
12 changes: 12 additions & 0 deletions test/openapi3/general/external-ref/file-refs-permissions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"components": {
"schemas": {
"Permissions": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
16 changes: 16 additions & 0 deletions test/openapi3/general/external-ref/file-refs-user.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
components:
schemas:
User:
type: object
required:
- email
- password
- name
properties:
email:
type: string
password:
type: string
name:
type: string
nullable: true
104 changes: 104 additions & 0 deletions test/openapi3/general/external-ref/file-refs.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use strict';

const chai = require('chai').use(require('chai-as-promised'));
const path = require('path');
const fs = require('fs');
const jsyaml = require('js-yaml');
const { expect } = chai;

const schemaValidatorGenerator = require('../../../../src');

describe('Loading defintions file with file refs', () => {
describe('calling buildSchemaSync with path to defintions file', () => {
kobik marked this conversation as resolved.
Show resolved Hide resolved
const swaggerPath = path.join(__dirname, 'file-refs.yaml');
const schema = schemaValidatorGenerator.buildSchemaSync(swaggerPath, {});
const validator = schema['/users'].post.body['application/json'];

it('should validate accroding to yaml file schema', () => {
kobik marked this conversation as resolved.
Show resolved Hide resolved
validator.validate({
id: 'dsadasda',
permissions: []
});
expect(validator.errors).to.eql([
{
keyword: 'required',
dataPath: '',
schemaPath: '#/allOf/0/required',
params: { missingProperty: 'email' },
message: 'should have required property \'email\''
},
{
keyword: 'required',
dataPath: '',
schemaPath: '#/allOf/0/required',
params: { missingProperty: 'password' },
message: 'should have required property \'password\''
},
{
keyword: 'required',
dataPath: '',
schemaPath: '#/allOf/0/required',
params: { missingProperty: 'name' },
message: 'should have required property \'name\''
},
{
dataPath: '',
keyword: 'type',
message: 'should be array',
params: {
type: 'array'
},
schemaPath: '#/allOf/1/type'
}
]);
});
it('should validate accroding to json file schema', () => {
validator.validate({
email: 'sarabia@gmail.com',
password: 'qwerty',
name: 'Sarabia',
permissions: 'permissions'
});
expect(validator.errors).to.eql([
{
dataPath: '',
keyword: 'type',
message: 'should be array',
params: {
type: 'array'
},
schemaPath: '#/allOf/1/type'
}
]);
});
});
describe('calling buildSchemaSync with json schema and baseDir', () => {
const swaggerPath = path.join(__dirname, 'file-refs.yaml');
const file = fs.readFileSync(swaggerPath);
const jsonSchema = jsyaml.load(file);
const schema = schemaValidatorGenerator.buildSchemaSync(jsonSchema, {
basePath: __dirname
});
const validator = schema['/users'].post.body['application/json'];

it('should validate according to external file schema', () => {
validator.validate({
email: 'sarabia@gmail.com',
password: 'qwerty',
name: 'Sarabia',
permissions: 'permissions'
});
expect(validator.errors).to.eql([
{
dataPath: '',
keyword: 'type',
message: 'should be array',
params: {
type: 'array'
},
schemaPath: '#/allOf/1/type'
}
]);
});
});
});
24 changes: 24 additions & 0 deletions test/openapi3/general/external-ref/file-refs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.0.1
info:
version: 1.0.0
title: Users
paths:
/users:
post:
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: "./file-refs-user.yaml#/components/schemas/User"
- $ref: "./file-refs-permissions.json#/components/schemas/Permissions"
responses:
200:
description: OK
content:
application/json:
schema:
allOf:
- $ref: "./file-refs-user.yaml#/components/schemas/User"
- $ref: "./file-refs-permissions.json#/components/schemas/Permissions"