├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── index.js ├── license ├── package.json ├── readme.md ├── schema-test.js ├── schema.js ├── test.js └── types.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Ajv = require('ajv') 4 | const map = require('map-values') 5 | const extend = require('xtend') 6 | const Schema = require('./schema') 7 | 8 | module.exports = Parser 9 | 10 | function Parser (parameters, data) { 11 | const ajv = new Ajv({ coerceTypes: 'array', jsonPointers: true }) 12 | const schema = Schema(parameters, data) 13 | const validate = ajv.compile(schema) 14 | 15 | return function parse (data, callback) { 16 | // map => copy keys, clones 2 levels 17 | data = map(data, (value) => extend(value)) 18 | if (validate(data)) return callback(null, data) 19 | callback(new Ajv.ValidationError(validate.errors)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Ben Drucker (bendrucker.me) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-parameters", 3 | "main": "index.js", 4 | "version": "1.3.2", 5 | "description": "Validate and parse request data using swagger parameters arrays", 6 | "license": "MIT", 7 | "repository": "bendrucker/swagger-parameters", 8 | "author": { 9 | "name": "Ben Drucker", 10 | "email": "bvdrucker@gmail.com", 11 | "url": "bendrucker.me" 12 | }, 13 | "scripts": { 14 | "test": "standard && tape *.js" 15 | }, 16 | "keywords": [ 17 | "swagger", 18 | "json", 19 | "api", 20 | "schema", 21 | "open api", 22 | "spec", 23 | "parameters", 24 | "http" 25 | ], 26 | "devDependencies": { 27 | "deep-sort-object": "~1.0.1", 28 | "standard": "^14.0.0", 29 | "tape": "^4.0.0" 30 | }, 31 | "files": [ 32 | "*.js", 33 | "types.json" 34 | ], 35 | "dependencies": { 36 | "ajv": "^6.6.0", 37 | "ap": "~0.2.0", 38 | "json-pointer": "~0.6.0", 39 | "map-values": "~1.0.1", 40 | "xtend": "~4.0.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # swagger-parameters [![Build Status](https://travis-ci.org/bendrucker/swagger-parameters.svg?branch=master)](https://travis-ci.org/bendrucker/swagger-parameters) [![Greenkeeper badge](https://badges.greenkeeper.io/bendrucker/swagger-parameters.svg)](https://greenkeeper.io/) 2 | 3 | > Validate and parse request data using swagger parameters arrays 4 | 5 | swagger-parameters turns your [Swagger/OpenAPI parameters](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterObject) into a full JSON schema that can be used to parse and validate HTTP request data. The library is fully server-agnostic. You're responsible for converting your request paths and queries into key-value data. swagger-parameters will perform validation and type coercion and return a copy of your parsed data (if valid). 6 | 7 | ## Install 8 | 9 | ``` 10 | $ npm install --save swagger-parameters 11 | ``` 12 | 13 | 14 | ## Usage 15 | 16 | ```js 17 | var Parser = require('swagger-parameters') 18 | // var Schema = require('swagger-parameters/schema') 19 | 20 | // /users/{id}/orders?page={page} 21 | var parse = Parser([ 22 | { 23 | name: 'id', 24 | in: 'path', 25 | type: 'integer', 26 | required: true 27 | }, 28 | { 29 | name: 'page', 30 | in: 'query', 31 | default: 1, 32 | type: 'integer' 33 | }, 34 | { 35 | name: 'token', 36 | in: 'header', 37 | required: true 38 | } 39 | ]) 40 | 41 | parse({ 42 | path: {id: '1'}, 43 | query: {page: '5'}, 44 | headers: {token: 't'} 45 | }, function (err, data) { 46 | if (err) throw err 47 | console.log(data) 48 | //=> {path: {id: 1}, query: {page: 5}, headers: {token: 't'}} 49 | }) 50 | ``` 51 | 52 | ## API 53 | 54 | #### `Parser(parameters, [data])` -> `function` 55 | 56 | ##### parameters 57 | 58 | Type: `array[object]` 59 | Default: `[]` 60 | 61 | An array of Swagger/OpenAPI [parameter definition](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameters-definitions-object). 62 | 63 | ##### data 64 | 65 | *Required* 66 | Type: `object` 67 | 68 | Data that can be resolved by `$ref` parameters. 69 | 70 | #### `parse(data, callback)` -> `undefined` 71 | 72 | ##### data 73 | 74 | *Required* 75 | Type: `object` 76 | 77 | A `{path, query, headers}` object, each with key-value data. 78 | 79 | ##### callback 80 | 81 | *Required* 82 | Type: `function` 83 | Arguments: `err, data` 84 | 85 | A callback to be called with a validation error or a parsed copy of the data. Validation errors will have an `errors` property which is an array of JSON schema error objects from [ajv](https://github.com/epoberezkin/ajv). 86 | 87 | 88 | ## License 89 | 90 | MIT © [Ben Drucker](http://bendrucker.me) 91 | -------------------------------------------------------------------------------- /schema-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const sort = require('deep-sort-object') 5 | const Schema = require('./schema') 6 | 7 | test('schema', function (t) { 8 | t.throws(Schema.bind(null, {}), /parameters/, 'parameters must be undefined or Array') 9 | 10 | t.deepEqual( 11 | sort(Schema([ 12 | { 13 | name: 'page', 14 | in: 'query', 15 | type: 'integer' 16 | } 17 | ])), 18 | sort({ 19 | title: 'HTTP parameters', 20 | type: 'object', 21 | properties: { 22 | headers: { 23 | title: 'HTTP headers', 24 | type: 'object', 25 | properties: {}, 26 | additionalProperties: true 27 | }, 28 | path: { 29 | title: 'HTTP path', 30 | type: 'object', 31 | properties: {}, 32 | additionalProperties: false 33 | }, 34 | query: { 35 | title: 'HTTP query', 36 | type: 'object', 37 | properties: { 38 | page: { 39 | type: 'integer' 40 | } 41 | }, 42 | additionalProperties: false 43 | } 44 | }, 45 | additionalProperties: false 46 | }), 47 | 'generates schema from parameter definitions' 48 | ) 49 | 50 | t.deepEqual( 51 | sort(Schema([ 52 | { 53 | name: 'page', 54 | in: 'query', 55 | type: 'integer', 56 | required: true 57 | } 58 | ])), 59 | sort({ 60 | title: 'HTTP parameters', 61 | type: 'object', 62 | properties: { 63 | headers: { 64 | title: 'HTTP headers', 65 | type: 'object', 66 | properties: {}, 67 | additionalProperties: true 68 | }, 69 | path: { 70 | title: 'HTTP path', 71 | type: 'object', 72 | properties: {}, 73 | additionalProperties: false 74 | }, 75 | query: { 76 | title: 'HTTP query', 77 | type: 'object', 78 | required: [ 79 | 'page' 80 | ], 81 | properties: { 82 | page: { 83 | type: 'integer' 84 | } 85 | }, 86 | additionalProperties: false 87 | } 88 | }, 89 | additionalProperties: false 90 | }), 91 | 'generates required parameter definitions' 92 | ) 93 | 94 | t.deepEqual( 95 | sort(Schema([ 96 | { 97 | name: 'data', 98 | in: 'body' 99 | } 100 | ])), 101 | sort({ 102 | title: 'HTTP parameters', 103 | type: 'object', 104 | properties: { 105 | headers: { 106 | title: 'HTTP headers', 107 | type: 'object', 108 | properties: {}, 109 | additionalProperties: true 110 | }, 111 | path: { 112 | title: 'HTTP path', 113 | type: 'object', 114 | properties: {}, 115 | additionalProperties: false 116 | }, 117 | query: { 118 | title: 'HTTP query', 119 | type: 'object', 120 | properties: {}, 121 | additionalProperties: false 122 | } 123 | }, 124 | additionalProperties: false 125 | }), 126 | 'ignores unhandled types' 127 | ) 128 | 129 | t.end() 130 | }) 131 | -------------------------------------------------------------------------------- /schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const extend = require('xtend') 5 | const pointer = require('json-pointer') 6 | const partial = require('ap').partial 7 | const types = require('./types.json') 8 | 9 | module.exports = Schema 10 | 11 | function Schema (parameters, data) { 12 | assert( 13 | Array.isArray(parameters) || parameters == null, 14 | 'parameters must be undefined or an array' 15 | ) 16 | 17 | const schema = initial(types) 18 | if (!parameters || !parameters.length) { 19 | return schema 20 | } 21 | 22 | return parameters 23 | .map(partial(dereference, data)) 24 | .filter(function (parameter) { 25 | return types.hasOwnProperty(parameter.in) 26 | }) 27 | .reduce(accumulateParameter, schema) 28 | } 29 | 30 | function initial (types) { 31 | return { 32 | title: 'HTTP parameters', 33 | type: 'object', 34 | properties: Object.keys(types).reduce(accumulateInitial, {}), 35 | additionalProperties: false 36 | } 37 | } 38 | 39 | function accumulateInitial (acc, key) { 40 | const type = types[key] 41 | const plural = type.plural || key 42 | 43 | const schema = { 44 | title: 'HTTP ' + plural, 45 | type: 'object', 46 | properties: {}, 47 | additionalProperties: type.additionalProperties 48 | } 49 | 50 | acc[plural] = schema 51 | 52 | return acc 53 | } 54 | 55 | function accumulateParameter (acc, parameter) { 56 | const source = parameter.in 57 | const key = parameter.name 58 | const required = parameter.required 59 | const data = extend(parameter) 60 | const destination = acc.properties[types[source].plural || source] 61 | 62 | delete data.name 63 | delete data.in 64 | delete data.required 65 | 66 | if (required) { 67 | destination.required = destination.required || [] 68 | destination.required.push(key) 69 | } 70 | 71 | destination.properties[key] = data 72 | 73 | return acc 74 | } 75 | 76 | function dereference (data, parameter) { 77 | if (!parameter.$ref) return parameter 78 | return pointer.get(data, parameter.$ref.replace(/^#/, '')) 79 | } 80 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const Parameters = require('./') 5 | 6 | test('main', function (t) { 7 | t.plan(6) 8 | 9 | // /users/{id}/orders?page={page} 10 | const parameters = Parameters([ 11 | { 12 | name: 'id', 13 | in: 'path', 14 | type: 'integer', 15 | required: true 16 | }, 17 | { 18 | name: 'page', 19 | in: 'query', 20 | default: 1, 21 | type: 'integer' 22 | }, 23 | { 24 | name: 'token', 25 | in: 'header', 26 | required: true 27 | } 28 | ]) 29 | 30 | parameters({ 31 | path: { 32 | id: '1' 33 | }, 34 | query: { 35 | page: '2' 36 | }, 37 | headers: { 38 | token: 'boop' 39 | } 40 | }, function (err, data) { 41 | if (err) return t.end(err) 42 | t.deepEqual(data, { 43 | path: { id: 1 }, 44 | query: { page: 2 }, 45 | headers: { token: 'boop' } 46 | }, 'validates and coerces valid data') 47 | }) 48 | 49 | parameters({ 50 | path: { 51 | id: 'a' 52 | }, 53 | query: { 54 | page: '2' 55 | }, 56 | headers: { 57 | token: 'boop' 58 | } 59 | }, function (err, data) { 60 | t.ok(err, 'errs on invalid data') 61 | t.ok(Array.isArray(err.errors), 'has ajv errors as err.errors') 62 | t.deepEqual(err.errors, [{ 63 | keyword: 'type', 64 | dataPath: '/path/id', 65 | schemaPath: '#/properties/path/properties/id/type', 66 | params: { type: 'integer' }, 67 | message: 'should be integer' 68 | }], 'includes error data') 69 | }) 70 | 71 | const raw = { path: { id: '1' }, query: { page: '2' }, headers: { token: 'boop' } } 72 | parameters(raw, function (err, data) { 73 | if (err) return t.end(err) 74 | t.notEqual(raw, data, 'copies data') 75 | t.equal(raw.path.id, '1', 'input is not mutated') 76 | }) 77 | }) 78 | 79 | test('references', function (t) { 80 | t.plan(6) 81 | 82 | // /users/{id}/orders?page={page} 83 | const parameters = Parameters([ 84 | { 85 | name: 'id', 86 | in: 'path', 87 | type: 'integer', 88 | required: true 89 | }, 90 | { 91 | $ref: '#/parameters/page' 92 | }, 93 | { 94 | name: 'token', 95 | in: 'header', 96 | required: true 97 | } 98 | ], { 99 | parameters: { 100 | page: { 101 | name: 'page', 102 | in: 'query', 103 | default: 1, 104 | type: 'integer' 105 | } 106 | } 107 | }) 108 | 109 | parameters({ 110 | path: { 111 | id: '1' 112 | }, 113 | query: { 114 | page: '2' 115 | }, 116 | headers: { 117 | token: 'boop' 118 | } 119 | }, function (err, data) { 120 | if (err) return t.end(err) 121 | t.deepEqual(data, { 122 | path: { id: 1 }, 123 | query: { page: 2 }, 124 | headers: { token: 'boop' } 125 | }, 'validates and coerces valid data') 126 | }) 127 | 128 | parameters({ 129 | path: { 130 | id: 'a' 131 | }, 132 | query: { 133 | page: '2' 134 | }, 135 | headers: { 136 | token: 'boop' 137 | } 138 | }, function (err, data) { 139 | t.ok(err, 'errs on invalid data') 140 | t.ok(Array.isArray(err.errors), 'has ajv errors as err.errors') 141 | t.deepEqual(err.errors, [{ 142 | keyword: 'type', 143 | dataPath: '/path/id', 144 | schemaPath: '#/properties/path/properties/id/type', 145 | params: { type: 'integer' }, 146 | message: 'should be integer' 147 | }], 'includes error data') 148 | }) 149 | 150 | const raw = { path: { id: '1' }, query: { page: '2' }, headers: { token: 'boop' } } 151 | parameters(raw, function (err, data) { 152 | if (err) return t.end(err) 153 | t.notEqual(raw, data, 'copies data') 154 | t.equal(raw.path.id, '1', 'input is not mutated') 155 | }) 156 | }) 157 | 158 | test('array', function (t) { 159 | t.plan(6) 160 | 161 | // /users/{ids...}/orders?page={page} 162 | const parameters = Parameters([ 163 | { 164 | name: 'id', 165 | in: 'path', 166 | type: 'array', 167 | required: true, 168 | items: { 169 | type: 'integer' 170 | } 171 | }, 172 | { 173 | name: 'page', 174 | in: 'query', 175 | default: 1, 176 | type: 'integer' 177 | }, 178 | { 179 | name: 'token', 180 | in: 'header', 181 | required: true 182 | } 183 | ]) 184 | 185 | parameters({ 186 | path: { 187 | id: '1' 188 | }, 189 | query: { 190 | page: '2' 191 | }, 192 | headers: { 193 | token: 'boop' 194 | } 195 | }, function (err, data) { 196 | if (err) return t.end(err) 197 | t.deepEqual(data, { 198 | path: { id: [1] }, 199 | query: { page: 2 }, 200 | headers: { token: 'boop' } 201 | }, 'validates and coerces valid data') 202 | }) 203 | 204 | parameters({ 205 | path: { 206 | id: 'a' 207 | }, 208 | query: { 209 | page: '2' 210 | }, 211 | headers: { 212 | token: 'boop' 213 | } 214 | }, function (err, data) { 215 | t.ok(err, 'errs on invalid data') 216 | t.ok(Array.isArray(err.errors), 'has ajv errors as err.errors') 217 | t.deepEqual(err.errors, [{ 218 | keyword: 'type', 219 | dataPath: '/path/id/0', 220 | schemaPath: '#/properties/path/properties/id/items/type', 221 | params: { type: 'integer' }, 222 | message: 'should be integer' 223 | }], 'includes error data') 224 | }) 225 | 226 | const raw = { path: { id: '1' }, query: { page: '2' }, headers: { token: 'boop' } } 227 | parameters(raw, function (err, data) { 228 | if (err) return t.end(err) 229 | t.notEqual(raw, data, 'copies data') 230 | t.equal(raw.path.id, '1', 'input is not mutated') 231 | }) 232 | }) 233 | -------------------------------------------------------------------------------- /types.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "additionalProperties": true, 4 | "plural": "headers" 5 | }, 6 | "path": { 7 | "additionalProperties": false 8 | }, 9 | "query": { 10 | "additionalProperties": false 11 | } 12 | } 13 | --------------------------------------------------------------------------------