From f706e268b37267ac3cbdee0f2f2fe4f5015f102c Mon Sep 17 00:00:00 2001 From: manorlh <44364426+manorlh@users.noreply.github.com> Date: Thu, 31 Jan 2019 11:42:57 +0200 Subject: [PATCH] open api 3 - discriminator (#41) Add support in OpenApi 3.0 --- README.md | 5 + package-lock.json | 285 ++++++++--- package.json | 5 +- src/data_structures/tree.js | 40 ++ src/middleware.js | 122 +---- src/swagger2/index.js | 72 +++ src/swagger3/open-api3.js | 99 ++++ src/utils/ajv-utils.js | 18 + src/utils/validators.js | 54 --- src/validators/DiscriminatorValidator.js | 39 ++ src/validators/OneOfValidator.js | 22 + src/validators/SimpleValidator.js | 17 + src/validators/Validator.js | 9 + src/validators/index.js | 13 + src/validators/validator-utils.js | 15 + test/express/middleware-test.js | 7 +- test/koa/middleware-test.js | 7 +- test/openapi3/openapi3-test.js | 317 ++++++++++++ .../openapi3/pets-unlimited-recursive-test.js | 28 ++ test/openapi3/pets-unlimited-recursive.yaml | 135 ++++++ test/openapi3/pets.yaml | 457 ++++++++++++++++++ test/openapi3/test-server-pet-recursive.js | 35 ++ test/openapi3/test-server-pet.js | 48 ++ 23 files changed, 1622 insertions(+), 227 deletions(-) create mode 100644 src/data_structures/tree.js create mode 100644 src/swagger2/index.js create mode 100644 src/swagger3/open-api3.js create mode 100644 src/utils/ajv-utils.js delete mode 100644 src/utils/validators.js create mode 100644 src/validators/DiscriminatorValidator.js create mode 100644 src/validators/OneOfValidator.js create mode 100644 src/validators/SimpleValidator.js create mode 100644 src/validators/Validator.js create mode 100644 src/validators/index.js create mode 100644 src/validators/validator-utils.js create mode 100644 test/openapi3/openapi3-test.js create mode 100644 test/openapi3/pets-unlimited-recursive-test.js create mode 100644 test/openapi3/pets-unlimited-recursive.yaml create mode 100644 test/openapi3/pets.yaml create mode 100644 test/openapi3/test-server-pet-recursive.js create mode 100644 test/openapi3/test-server-pet.js diff --git a/README.md b/README.md index 243fb32..99ef434 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,11 @@ module.exports = inputValidation.init('test/pet-store-swagger.yaml', {framework: - koa support - When using this package as middleware for koa, the validations errors are being thrown. - koa packages - This package supports koa server that uses [`koa-router`](https://www.npmjs.com/package/koa-router), [`koa-bodyparser`](https://www.npmjs.com/package/koa-bodyparser) and [`koa-multer`](https://www.npmjs.com/package/koa-multer) +## Open api 3 - known issues +- supporting inheritance with discriminator , only if the ancestor object is the discriminator. +- The discriminator supports in the inheritance chain stop when getting to a child with no discriminator (a leaf in the inheritance tree), meaning a leaf can't have a field which starts a new inheritance tree. + so child with no discriminator cant point to other child with discriminator, + ## Running Tests Using mocha, istanbul and mochawesome ```bash diff --git a/package-lock.json b/package-lock.json index 086b88e..1ad8a80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "dependencies": { "@babel/code-frame": { "version": "7.0.0", - "resolved": "http://npm.zooz.co:8083/@babel%2fcode-frame/-/code-frame-7.0.0.tgz", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", "dev": true, "requires": { @@ -15,7 +15,7 @@ }, "@babel/generator": { "version": "7.2.2", - "resolved": "http://npm.zooz.co:8083/@babel%2fgenerator/-/generator-7.2.2.tgz", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.2.2.tgz", "integrity": "sha512-I4o675J/iS8k+P38dvJ3IBGqObLXyQLTxtrR4u9cSUJOURvafeEWb/pFMOTwtNrmq73mJzyF6ueTbO1BtN0Zeg==", "dev": true, "requires": { @@ -28,7 +28,7 @@ "dependencies": { "jsesc": { "version": "2.5.2", - "resolved": "http://npm.zooz.co:8083/jsesc/-/jsesc-2.5.2.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true } @@ -36,7 +36,7 @@ }, "@babel/helper-function-name": { "version": "7.1.0", - "resolved": "http://npm.zooz.co:8083/@babel%2fhelper-function-name/-/helper-function-name-7.1.0.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", "dev": true, "requires": { @@ -47,7 +47,7 @@ }, "@babel/helper-get-function-arity": { "version": "7.0.0", - "resolved": "http://npm.zooz.co:8083/@babel%2fhelper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", "dev": true, "requires": { @@ -56,7 +56,7 @@ }, "@babel/helper-split-export-declaration": { "version": "7.0.0", - "resolved": "http://npm.zooz.co:8083/@babel%2fhelper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", "dev": true, "requires": { @@ -65,7 +65,7 @@ }, "@babel/highlight": { "version": "7.0.0", - "resolved": "http://npm.zooz.co:8083/@babel%2fhighlight/-/highlight-7.0.0.tgz", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", "dev": true, "requires": { @@ -76,7 +76,7 @@ "dependencies": { "js-tokens": { "version": "4.0.0", - "resolved": "http://npm.zooz.co:8083/js-tokens/-/js-tokens-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true } @@ -84,13 +84,13 @@ }, "@babel/parser": { "version": "7.2.3", - "resolved": "http://npm.zooz.co:8083/@babel%2fparser/-/parser-7.2.3.tgz", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.2.3.tgz", "integrity": "sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA==", "dev": true }, "@babel/template": { "version": "7.2.2", - "resolved": "http://npm.zooz.co:8083/@babel%2ftemplate/-/template-7.2.2.tgz", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==", "dev": true, "requires": { @@ -101,7 +101,7 @@ }, "@babel/traverse": { "version": "7.2.3", - "resolved": "http://npm.zooz.co:8083/@babel%2ftraverse/-/traverse-7.2.3.tgz", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz", "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==", "dev": true, "requires": { @@ -118,7 +118,7 @@ "dependencies": { "debug": { "version": "4.1.1", - "resolved": "http://npm.zooz.co:8083/debug/-/debug-4.1.1.tgz", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { @@ -127,13 +127,13 @@ }, "globals": { "version": "11.10.0", - "resolved": "http://npm.zooz.co:8083/globals/-/globals-11.10.0.tgz", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.10.0.tgz", "integrity": "sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ==", "dev": true }, "ms": { "version": "2.1.1", - "resolved": "http://npm.zooz.co:8083/ms/-/ms-2.1.1.tgz", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true } @@ -141,7 +141,7 @@ }, "@babel/types": { "version": "7.2.2", - "resolved": "http://npm.zooz.co:8083/@babel%2ftypes/-/types-7.2.2.tgz", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.2.2.tgz", "integrity": "sha512-fKCuD6UFUMkR541eDWL+2ih/xFZBXPOg/7EQFeTluMDebfqR4jrpaCjLhkWlQS4hT6nRa2PMEgXKbRB5/H2fpg==", "dev": true, "requires": { @@ -152,7 +152,7 @@ "dependencies": { "to-fast-properties": { "version": "2.0.0", - "resolved": "http://npm.zooz.co:8083/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } @@ -192,14 +192,14 @@ } }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", + "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "ansi-escapes": { @@ -600,10 +600,21 @@ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "dev": true }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true }, "co-body": { "version": "6.0.0", @@ -807,6 +818,11 @@ "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=", "dev": true }, + "core-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.2.tgz", + "integrity": "sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -850,6 +866,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -1059,6 +1076,32 @@ "strip-json-comments": "~2.0.1", "table": "^4.0.1", "text-table": "~0.2.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + } } }, "eslint-config-standard": { @@ -1343,9 +1386,9 @@ "dev": true }, "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -1573,6 +1616,32 @@ "requires": { "ajv": "^5.3.0", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + } } }, "has": { @@ -1796,6 +1865,14 @@ "path-is-inside": "^1.0.1" } }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -1829,6 +1906,11 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1845,6 +1927,7 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -1870,20 +1953,30 @@ "dev": true }, "json-schema-ref-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-4.0.4.tgz", - "integrity": "sha512-jbED8wPzWeGipggE2d7oM1kohCSZpscU+kBkkF+vjqI683y72hcPRGI/Ql1OqYK3MOnSQp+t1ZcHdQQ/MRF4UQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.0.3.tgz", + "integrity": "sha512-Ds/0541IPed88JSiMb3CBeUsxfL5Eosc0r97z0QMSXiBJTYKZLhOAGZd8zFVfpkKaRb4zDAnumyFYxnHLmbQmw==", "requires": { "call-me-maybe": "^1.0.1", - "debug": "^3.1.0", - "js-yaml": "^3.10.0", - "ono": "^4.0.2" + "js-yaml": "^3.12.1", + "ono": "^4.0.11" + }, + "dependencies": { + "js-yaml": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", + "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } } }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify": { "version": "1.0.1", @@ -1906,6 +1999,16 @@ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", "dev": true }, + "jsonschema": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", + "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==" + }, + "jsonschema-draft4": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jsonschema-draft4/-/jsonschema-draft4-1.0.0.tgz", + "integrity": "sha1-8K8gBQVPDwrefqIRhhS2ncUS2GU=" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -1930,6 +2033,11 @@ "integrity": "sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==", "dev": true }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, "koa": { "version": "2.6.2", "resolved": "http://npm.zooz.co:8083/koa/-/koa-2.6.2.tgz", @@ -2304,7 +2412,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "multer": { "version": "1.3.0", @@ -2404,7 +2513,7 @@ }, "nyc": { "version": "13.1.0", - "resolved": "http://npm.zooz.co:8083/nyc/-/nyc-13.1.0.tgz", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-13.1.0.tgz", "integrity": "sha512-3GyY6TpQ58z9Frpv4GMExE1SV2tAgYqC7HSy2omEhNiCT3mhT9NyiOvIE8zkbuJVFzmvvNTnE4h/7/wQae7xLg==", "dev": true, "requires": { @@ -2803,7 +2912,7 @@ }, "istanbul-lib-instrument": { "version": "3.0.0", - "resolved": "http://npm.zooz.co:8083/istanbul-lib-instrument/-/istanbul-lib-instrument-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.0.0.tgz", "integrity": "sha512-eQY9vN9elYjdgN9Iv6NS/00bptm02EBBk70lRMaVjeA6QYocQgenVrSgC28TJurdnZa80AGO3ASdFN+w/njGiQ==", "dev": true, "requires": { @@ -3602,13 +3711,23 @@ "dev": true }, "ono": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.2.tgz", - "integrity": "sha512-EFXJFoeF+KkZW4lwmcPMKHp2ZU7o6CM+ccX2nPbEJKiJIdyqbIcS1v6pmNgeNJ6x4/vEYn0/8oz66qXSPnnmSQ==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", + "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==", "requires": { "format-util": "^1.0.3" } }, + "openapi-schema-validation": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/openapi-schema-validation/-/openapi-schema-validation-0.4.2.tgz", + "integrity": "sha512-K8LqLpkUf2S04p2Nphq9L+3bGFh/kJypxIG2NVGKX0ffzT4NQI9HirhiY6Iurfej9lCu7y4Ndm4tv+lm86Ck7w==", + "requires": { + "jsonschema": "1.2.4", + "jsonschema-draft4": "^1.0.0", + "swagger-schema-official": "2.0.0-bab6bed" + } + }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -4144,6 +4263,14 @@ "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", "dev": true }, + "shallow-clone": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.0.tgz", + "integrity": "sha512-Drg+nOI+ofeuslBf0nulyWLZhK1BZprqNvPJaiB4VvES+9gC6GG+qOVAfuO12zVSgxq9SKevcme7S3uDT6Be8w==", + "requires": { + "kind-of": "^6.0.2" + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -4361,22 +4488,22 @@ "dev": true }, "swagger-methods": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/swagger-methods/-/swagger-methods-1.0.4.tgz", - "integrity": "sha512-xrKFLbrZ6VxRsg+M3uJozJtsEpNI/aPfZsOkoEjXw8vhAqdMIqwTYGj1f4dmUgvJvCdZhV5iArgtqXgs403ltg==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/swagger-methods/-/swagger-methods-1.0.8.tgz", + "integrity": "sha512-G6baCwuHA+C5jf4FNOrosE4XlmGsdjbOjdBK4yuiDDj/ro9uR4Srj3OR84oQMT8F3qKp00tYNv0YN730oTHPZA==" }, "swagger-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-4.0.1.tgz", - "integrity": "sha512-LV2OXOcwtzboCFKPQMkMlkzisy0YLMx5mYF8rSQp65jAOvGhxD8QpAFEC/Sm0XUUawnUDEjCjv0WNKt9XW/Oog==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-6.0.3.tgz", + "integrity": "sha512-rd1w4giVtgdWELAmxWXjVbb2+xnbkc4BjzsQHBLU8YDMBhmNbnQBHQc92BpspxBwkj0zhSpF7Htor9691NJfwA==", "requires": { "call-me-maybe": "^1.0.1", - "debug": "^3.1.0", - "json-schema-ref-parser": "^4.0.4", - "ono": "^4.0.2", - "swagger-methods": "^1.0.0", + "json-schema-ref-parser": "^6.0.3", + "ono": "^4.0.11", + "openapi-schema-validation": "^0.4.2", + "swagger-methods": "^1.0.8", "swagger-schema-official": "2.0.0-bab6bed", - "z-schema": "^3.18.4" + "z-schema": "^3.24.2" } }, "swagger-schema-official": { @@ -4398,11 +4525,35 @@ "string-width": "^2.1.1" }, "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, "ajv-keywords": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", "dev": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true } } }, @@ -4514,6 +4665,21 @@ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, "urijs": { "version": "1.19.1", "resolved": "http://npm.zooz.co:8083/urijs/-/urijs-1.19.1.tgz", @@ -4543,9 +4709,9 @@ } }, "validator": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-9.2.0.tgz", - "integrity": "sha512-6Ij4Eo0KM4LkR0d0IegOwluG5453uqT5QyF5SV5Ezvm8/zmkKI/L4eoraafZGlZPC9guLkwKzgypcw8VGWWnGA==" + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", + "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" }, "vary": { "version": "1.1.2", @@ -4613,14 +4779,15 @@ "dev": true }, "z-schema": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.19.0.tgz", - "integrity": "sha512-V94f3ODuluBS4kQLLjNhwoMek0dyIXCsvNu/A17dAyJ6sMhT5KkJQwSn07R0naByLIXJWMDk+ruMfI/3G3hS4Q==", + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.24.3.tgz", + "integrity": "sha512-Hj1JIXsXZNCe9HLi3oWKHbYE8YuAFiutmwWX1JUbRgGUzuHAF3EL21mJPO+FEPckZwzG87jroXMrXLAfw03FnQ==", "requires": { "commander": "^2.7.1", + "core-js": "^2.5.7", "lodash.get": "^4.0.0", "lodash.isequal": "^4.0.0", - "validator": "^9.0.0" + "validator": "^10.0.0" } } } diff --git a/package.json b/package.json index dad813c..b45ae2f 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,9 @@ "author": "Idan Tovi", "license": "MIT", "dependencies": { - "ajv": "^5.5.2", - "swagger-parser": "^4.0.1" + "ajv": "^6.6.2", + "clone-deep": "^4.0.1", + "swagger-parser": "^6.0.2" }, "devDependencies": { "body-parser": "^1.18.2", diff --git a/src/data_structures/tree.js b/src/data_structures/tree.js new file mode 100644 index 0000000..5cf0eaa --- /dev/null +++ b/src/data_structures/tree.js @@ -0,0 +1,40 @@ + +'use strict'; +/** Class representing a node in a tree structure, which each node has value and children(nodes) saving by key. */ +class Node { + /** + * Create a node. + * @param value - The value of the node. + */ + constructor(value){ + this.value = value; + this.childrenAsKeyValue = {}; + } + /** + * Add child to the node. + * @param node - The node which going to be the child. + * @param key - The key which is the identifier of the child. + */ + addChild(node, key){ + this.childrenAsKeyValue[key] = node; + } + /** + * Override node data by other node by reference. + * @param node - The node which going to use to take his data. + */ + setData(node){ + if (node instanceof Node){ + this.value = node.value; + this.childrenAsKeyValue = node.childrenAsKeyValue; + } + }; + /** + * Get node value. + * @return The value of the node. + */ + getValue(){ + return this.value; + } +} + +module.exports = {Node}; \ No newline at end of file diff --git a/src/middleware.js b/src/middleware.js index a1f62d5..be77161 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -1,18 +1,15 @@ 'use strict'; var SwaggerParser = require('swagger-parser'), - Ajv = require('ajv'), - Validators = require('./utils/validators'), - filesKeyword = require('./customKeywords/files'), - contentKeyword = require('./customKeywords/contentTypeValidation'), InputValidationError = require('./inputValidationError'), schemaPreprocessor = require('./utils/schema-preprocessor'), + swagger3 = require('./swagger3/open-api3'), + swagger2 = require('./swagger2'), + ajvUtils = require('./utils/ajv-utils'), + Ajv = require('ajv'), sourceResolver = require('./utils/sourceResolver'); - var schemas = {}; var middlewareOptions; -var ajvConfigBody; -var ajvConfigParams; var framework; /** @@ -22,8 +19,6 @@ var framework; */ function init(swaggerPath, options) { middlewareOptions = options || {}; - ajvConfigBody = middlewareOptions.ajvConfigBody || {}; - ajvConfigParams = middlewareOptions.ajvConfigParams || {}; framework = middlewareOptions.framework ? require(`./frameworks/${middlewareOptions.framework}`) : require('./frameworks/express'); const makeOptionalAttributesNullable = middlewareOptions.makeOptionalAttributesNullable || false; @@ -39,19 +34,21 @@ function init(swaggerPath, options) { Object.keys(dereferenced.paths[currentPath]).filter(function (parameter) { return parameter !== 'parameters' }) .forEach(function (currentMethod) { schemas[parsedPath][currentMethod.toLowerCase()] = {}; - + const isOpenApi3 = dereferenced.openapi === '3.0.0'; const parameters = dereferenced.paths[currentPath][currentMethod].parameters || []; - - let bodySchema = middlewareOptions.expectFormFieldsInBody - ? parameters.filter(function (parameter) { return (parameter.in === 'body' || (parameter.in === 'formData' && parameter.type !== 'file')) }) - : parameters.filter(function (parameter) { return parameter.in === 'body' }); - - if (makeOptionalAttributesNullable) { - schemaPreprocessor.makeOptionalAttributesNullable(bodySchema); - } - if (bodySchema.length > 0) { - const validatedBodySchema = _getValidatedBodySchema(bodySchema); - schemas[parsedPath][currentMethod].body = buildBodyValidation(validatedBodySchema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath); + if (isOpenApi3){ + schemas[parsedPath][currentMethod].body = swagger3.buildBodyValidation(dereferenced, swaggers[1], currentPath, currentMethod, middlewareOptions); + } else { + let bodySchema = middlewareOptions.expectFormFieldsInBody + ? parameters.filter(function (parameter) { return (parameter.in === 'body' || (parameter.in === 'formData' && parameter.type !== 'file')) }) + : parameters.filter(function (parameter) { return parameter.in === 'body' }); + if (makeOptionalAttributesNullable) { + schemaPreprocessor.makeOptionalAttributesNullable(bodySchema); + } + if (bodySchema.length > 0) { + const validatedBodySchema = swagger2.getValidatedBodySchema(bodySchema); + schemas[parsedPath][currentMethod].body = swagger2.buildBodyValidation(validatedBodySchema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath, middlewareOptions); + } } let localParameters = parameters.filter(function (parameter) { @@ -60,7 +57,7 @@ function init(swaggerPath, options) { if (localParameters.length > 0 || middlewareOptions.contentTypeValidation) { schemas[parsedPath][currentMethod].parameters = buildParametersValidation(localParameters, - dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes); + dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes, middlewareOptions); } }); }); @@ -69,32 +66,6 @@ function init(swaggerPath, options) { return Promise.reject(error); }); } - -function _getValidatedBodySchema(bodySchema) { - let validatedBodySchema; - if (bodySchema[0].in === 'body') { - // if we are processing schema for a simple JSON payload, no additional processing needed - validatedBodySchema = bodySchema[0].schema; - } else if (bodySchema[0].in === 'formData') { - // if we are processing multipart form, assemble body schema from form field schemas - validatedBodySchema = { - required: [], - properties: {} - }; - bodySchema.forEach((formField) => { - if (formField.type !== 'file') { - validatedBodySchema.properties[formField.name] = { - type: formField.type - }; - if (formField.required) { - validatedBodySchema.required.push(formField.name); - } - } - }); - } - return validatedBodySchema; -} - /** * The middleware - should be called for each express route * @param {any} req @@ -141,55 +112,6 @@ function _validateParams(headers, pathParams, query, files, path, method) { }); } -function addCustomKeyword(ajv, formats) { - if (formats) { - formats.forEach(function (format) { - ajv.addFormat(format.name, format.pattern); - }); - } - - ajv.addKeyword('files', filesKeyword); - ajv.addKeyword('content', contentKeyword); -} - -function buildBodyValidation(schema, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath) { - const defaultAjvOptions = { - allErrors: true - // unknownFormats: 'ignore' - }; - const options = Object.assign({}, defaultAjvOptions, ajvConfigBody); - let ajv = new Ajv(options); - - addCustomKeyword(ajv, middlewareOptions.formats); - - if (schema.discriminator) { - return buildInheritance(schema.discriminator, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, ajv); - } else { - return new Validators.SimpleValidator(ajv.compile(schema)); - } -} - -function buildInheritance(discriminator, dereferencedDefinitions, swagger, currentPath, currentMethod, parsedPath, ajv) { - let bodySchema = swagger.paths[currentPath][currentMethod].parameters.filter(function (parameter) { return parameter.in === 'body' })[0]; - var inheritsObject = { - inheritance: [] - }; - inheritsObject.discriminator = discriminator; - - Object.keys(swagger.definitions).forEach(key => { - if (swagger.definitions[key].allOf) { - swagger.definitions[key].allOf.forEach(element => { - if (element['$ref'] && element['$ref'] === bodySchema.schema['$ref']) { - inheritsObject[key] = ajv.compile(dereferencedDefinitions[key]); - inheritsObject.inheritance.push(key); - } - }); - } - }, this); - - return new Validators.OneOfValidator(inheritsObject); -} - function createContentTypeHeaders(validate, contentTypes) { if (!validate || !contentTypes) return; @@ -198,16 +120,16 @@ function createContentTypeHeaders(validate, contentTypes) { }; } -function buildParametersValidation(parameters, contentTypes) { +function buildParametersValidation(parameters, contentTypes, middlewareOptions) { const defaultAjvOptions = { allErrors: true, coerceTypes: 'array' // unknownFormats: 'ignore' }; - const options = Object.assign({}, defaultAjvOptions, ajvConfigParams); + const options = Object.assign({}, defaultAjvOptions, middlewareOptions.ajvConfigParams); let ajv = new Ajv(options); - addCustomKeyword(ajv, middlewareOptions.formats); + ajvUtils.addCustomKeyword(ajv, middlewareOptions.formats); var ajvParametersSchema = { title: 'HTTP parameters', diff --git a/src/swagger2/index.js b/src/swagger2/index.js new file mode 100644 index 0000000..3d0c1ac --- /dev/null +++ b/src/swagger2/index.js @@ -0,0 +1,72 @@ + +const Validators = require('../validators'), + Ajv = require('ajv'), + ajvUtils = require('../utils/ajv-utils'); + +module.exports = { + getValidatedBodySchema, + buildBodyValidation +}; + +function getValidatedBodySchema(bodySchema) { + let validatedBodySchema; + if (bodySchema[0].in === 'body') { + // if we are processing schema for a simple JSON payload, no additional processing needed + validatedBodySchema = bodySchema[0].schema; + } else if (bodySchema[0].in === 'formData') { + // if we are processing multipart form, assemble body schema from form field schemas + validatedBodySchema = { + required: [], + properties: {} + }; + bodySchema.forEach((formField) => { + if (formField.type !== 'file') { + validatedBodySchema.properties[formField.name] = { + type: formField.type + }; + if (formField.required) { + validatedBodySchema.required.push(formField.name); + } + } + }); + } + return validatedBodySchema; +} + +function buildBodyValidation(schema, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, middlewareOptions = {}) { + const defaultAjvOptions = { + allErrors: true + // unknownFormats: 'ignore' + }; + const options = Object.assign({}, defaultAjvOptions, middlewareOptions.ajvConfigBody); + let ajv = new Ajv(options); + + ajvUtils.addCustomKeyword(ajv, middlewareOptions.formats); + + if (schema.discriminator) { + return buildInheritance(schema.discriminator, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, ajv); + } else { + return new Validators.SimpleValidator(ajv.compile(schema)); + } +} + +function buildInheritance(discriminator, dereferencedDefinitions, swagger, currentPath, currentMethod, parsedPath, ajv) { + let bodySchema = swagger.paths[currentPath][currentMethod].parameters.filter(function (parameter) { return parameter.in === 'body' })[0]; + var inheritsObject = { + inheritance: [] + }; + inheritsObject.discriminator = discriminator; + + Object.keys(swagger.definitions).forEach(key => { + if (swagger.definitions[key].allOf) { + swagger.definitions[key].allOf.forEach(element => { + if (element['$ref'] && element['$ref'] === bodySchema.schema['$ref']) { + inheritsObject[key] = ajv.compile(dereferencedDefinitions[key]); + inheritsObject.inheritance.push(key); + } + }); + } + }, this); + + return new Validators.OneOfValidator(inheritsObject); +} diff --git a/src/swagger3/open-api3.js b/src/swagger3/open-api3.js new file mode 100644 index 0000000..2bc4a46 --- /dev/null +++ b/src/swagger3/open-api3.js @@ -0,0 +1,99 @@ + +const Validators = require('../validators'), + Ajv = require('ajv'), + cloneDeep = require('clone-deep'), + ajvUtils = require('../utils/ajv-utils'), + {Node} = require('../data_structures/tree'); + +module.exports = { + buildBodyValidation +}; + +function buildBodyValidation(dereferenced, originalSwagger, currentPath, currentMethod, middlewareOptions = {}) { + const bodySchemaV3 = dereferenced.paths[currentPath][currentMethod].requestBody.content['application/json'].schema; + const defaultAjvOptions = { + allErrors: true + }; + const options = Object.assign({}, defaultAjvOptions, middlewareOptions.ajvConfigBody); + let ajv = new Ajv(options); + + ajvUtils.addCustomKeyword(ajv, middlewareOptions.formats); + + if (bodySchemaV3.discriminator) { + return buildV3Inheritance(dereferenced, originalSwagger, currentPath, currentMethod, ajv); + } else { + return new Validators.SimpleValidator(ajv.compile(bodySchemaV3)); + } +} + +function buildV3Inheritance(dereferencedDefinitions, swagger, currentPath, currentMethod, ajv) { + const RECURSIVE__MAX_DEPTH = 20; + const bodySchema = swagger.paths[currentPath][currentMethod].requestBody.content['application/json']; + const schemas = swagger.components.schemas; + const dereferencedSchemas = dereferencedDefinitions.components.schemas; + const rootKey = bodySchema.schema['$ref'].split('/components/schemas/')[1]; + const tree = new Node(); + function getKeyFromRef(ref) { + return ref.split('/components/schemas/')[1]; + } + + function recursiveDiscriminatorBuilder(ancestor, option, refValue, propertiesAcc = {required: [], properties: {}}, depth = RECURSIVE__MAX_DEPTH) { + // assume first time is discriminator. + if (depth === 0){ + throw new Error(`swagger schema exceed maximum supported depth of ${RECURSIVE__MAX_DEPTH} for swagger definitions inheritance`); + } + const discriminator = dereferencedSchemas[refValue].discriminator, + currentSchema = schemas[refValue], + currentDereferencedSchema = dereferencedSchemas[refValue]; + + if (!discriminator){ + // need to stop and just add validator on ancesstor; + const newSchema = cloneDeep(currentDereferencedSchema); + newSchema.required.push(...(propertiesAcc.required || [])); + newSchema.properties = Object.assign(newSchema.properties, propertiesAcc.properties); + ancestor.getValue().validators[option] = ajv.compile(newSchema); // think about key + return; + } + propertiesAcc = cloneDeep(propertiesAcc); + propertiesAcc.required.push(...(currentDereferencedSchema.required || [])); + propertiesAcc.properties = Object.assign(propertiesAcc.properties, currentDereferencedSchema.properties); + + const discriminatorObject = {validators: {}}; + discriminatorObject.discriminator = discriminator.propertyName; + + const currentDiscriminatorNode = new Node(discriminatorObject); + if (!ancestor.getValue()){ + ancestor.setData(currentDiscriminatorNode); + } else { + ancestor.addChild(currentDiscriminatorNode, option); + } + + if (!currentSchema.oneOf){ + throw new Error('oneOf must be part of discriminator'); + } + + const options = currentSchema.oneOf.map((refObject) => { + let option = findKey(currentSchema.discriminator.mapping, (value) => (value === refObject['$ref'])); + const ref = getKeyFromRef(refObject['$ref']); + return {option: option || ref, ref}; + }); + discriminatorObject.allowedValues = options.map((option) => option.option); + options.forEach(function (optionObject) { + recursiveDiscriminatorBuilder(currentDiscriminatorNode, optionObject.option, optionObject.ref, propertiesAcc, depth - 1); + }); + } + recursiveDiscriminatorBuilder(tree, rootKey, rootKey); + return new Validators.DiscriminatorValidator(tree); +} + +function findKey(object, searchFunc) { + if (!object){ + return; + } + const keys = Object.keys(object); + for (let i = 0; i < keys.length; i++){ + if (searchFunc(object[keys[i]])){ + return keys[i]; + } + } +} \ No newline at end of file diff --git a/src/utils/ajv-utils.js b/src/utils/ajv-utils.js new file mode 100644 index 0000000..8355d38 --- /dev/null +++ b/src/utils/ajv-utils.js @@ -0,0 +1,18 @@ + +const filesKeyword = require('../customKeywords/files'), + contentKeyword = require('../customKeywords/contentTypeValidation'); + +module.exports = { + addCustomKeyword +}; + +function addCustomKeyword(ajv, formats) { + if (formats) { + formats.forEach(function (format) { + ajv.addFormat(format.name, format.pattern); + }); + } + + ajv.addKeyword('files', filesKeyword); + ajv.addKeyword('content', contentKeyword); +} \ No newline at end of file diff --git a/src/utils/validators.js b/src/utils/validators.js deleted file mode 100644 index 14b2f56..0000000 --- a/src/utils/validators.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -class Validator { - constructor(validationFunction, schema) { - this.validate = validationFunction.bind(this, schema); - this.errors = null; - } -} - -class SimpleValidator extends Validator { - constructor(schema) { - super(simple, schema); - } -} - -class OneOfValidator extends Validator { - constructor(schema) { - super(oneOf, schema); - } -} - -function oneOf(schemas, data) { - var schema = schemas[data[schemas.discriminator]]; - var result = false; - - if (schema) { - result = schema(data); - this.errors = schema.errors; - } else { - let error = new Error('should be equal to one of the allowed values'); - error.dataPath = '.' + schemas.discriminator; - error.keyword = 'enum'; - error.params = { - allowedValues: schemas.inheritance - }; - error.schemaPath = '#/properties/' + schemas.discriminator; - this.errors = [error]; - } - - return result; -} - -function simple(ajvValidate, data) { - var result = ajvValidate(data); - this.errors = ajvValidate.errors; - - return result; -} - -module.exports = { - Validator: Validator, - OneOfValidator: OneOfValidator, - SimpleValidator: SimpleValidator -}; \ No newline at end of file diff --git a/src/validators/DiscriminatorValidator.js b/src/validators/DiscriminatorValidator.js new file mode 100644 index 0000000..2c57329 --- /dev/null +++ b/src/validators/DiscriminatorValidator.js @@ -0,0 +1,39 @@ + +const Validator = require('./Validator'), + validatorUtils = require('./validator-utils'); + +class DiscriminatorValidator extends Validator { + constructor(schema) { + super(discriminator, schema); + } +} + +function findSchemaValidation(tree, data) { + const currentValue = tree.getValue(); + if (currentValue.discriminator){ + const discriminatorValue = data[currentValue.discriminator]; + if (!tree.getValue().allowedValues.includes(discriminatorValue)){ + validatorUtils.allowedValuesError.call(this, currentValue.discriminator, currentValue.allowedValues); + return; + } + if (tree.getValue().validators[discriminatorValue]){ + return tree.getValue().validators[discriminatorValue]; + } + const newNode = tree.childrenAsKeyValue[discriminatorValue]; + return findSchemaValidation.call(this, newNode, data); + } + throw new Error('DEBUG: there is no discriminator on current value'); +} + +function discriminator(schemas, data) { + let schema = findSchemaValidation.call(this, schemas, data); + let result = false; + if (schema) { + result = schema(data); + this.errors = schema.errors; + } + + return result; +} + +module.exports = DiscriminatorValidator; diff --git a/src/validators/OneOfValidator.js b/src/validators/OneOfValidator.js new file mode 100644 index 0000000..3c3abc4 --- /dev/null +++ b/src/validators/OneOfValidator.js @@ -0,0 +1,22 @@ + +const Validator = require('./Validator'), + validatorUtils = require('./validator-utils'); + +class OneOfValidator extends Validator { + constructor(schema) { + super(oneOf, schema); + } +} +function oneOf(schemas, data) { + let schema = schemas[data[schemas.discriminator]]; + let result = false; + if (schema) { + result = schema(data); + this.errors = schema.errors; + } else { + validatorUtils.allowedValuesError.call(this, schemas.discriminator, schemas.inheritance); + } + + return result; +} +module.exports = OneOfValidator; \ No newline at end of file diff --git a/src/validators/SimpleValidator.js b/src/validators/SimpleValidator.js new file mode 100644 index 0000000..1ebf2a1 --- /dev/null +++ b/src/validators/SimpleValidator.js @@ -0,0 +1,17 @@ + +const Validator = require('./Validator'); + +class SimpleValidator extends Validator { + constructor(schema) { + super(simple, schema); + } +} + +function simple(ajvValidate, data) { + let result = ajvValidate(data); + this.errors = ajvValidate.errors; + + return result; +} + +module.exports = SimpleValidator; \ No newline at end of file diff --git a/src/validators/Validator.js b/src/validators/Validator.js new file mode 100644 index 0000000..1b3e7a0 --- /dev/null +++ b/src/validators/Validator.js @@ -0,0 +1,9 @@ + +class Validator { + constructor(validationFunction, schema) { + this.validate = validationFunction.bind(this, schema); + this.errors = null; + } +} + +module.exports = Validator; \ No newline at end of file diff --git a/src/validators/index.js b/src/validators/index.js new file mode 100644 index 0000000..005e1a4 --- /dev/null +++ b/src/validators/index.js @@ -0,0 +1,13 @@ +'use strict'; + +const Validator = require('./Validator'), + OneOfValidator = require('./OneOfValidator'), + SimpleValidator = require('./SimpleValidator'), + DiscriminatorValidator = require('./DiscriminatorValidator'); + +module.exports = { + Validator: Validator, + OneOfValidator: OneOfValidator, + SimpleValidator: SimpleValidator, + DiscriminatorValidator +}; \ No newline at end of file diff --git a/src/validators/validator-utils.js b/src/validators/validator-utils.js new file mode 100644 index 0000000..1c3aa95 --- /dev/null +++ b/src/validators/validator-utils.js @@ -0,0 +1,15 @@ + +module.exports = { + allowedValuesError +}; + +function allowedValuesError(discriminator, allowedValues) { + let error = new Error('should be equal to one of the allowed values'); + error.dataPath = '.' + discriminator; + error.keyword = 'enum'; + error.params = { + allowedValues: allowedValues + }; + error.schemaPath = '#/properties/' + discriminator; + this.errors = [error]; +} diff --git a/test/express/middleware-test.js b/test/express/middleware-test.js index cc81700..3f7da77 100644 --- a/test/express/middleware-test.js +++ b/test/express/middleware-test.js @@ -20,12 +20,7 @@ describe('input-validation middleware tests - Express', function () { it('should resolve without formats', function () { let rewire = require('rewire'); let middleware = rewire('../../src/middleware'); - let addCustomKeyword = middleware.__get__('addCustomKeyword'); - let addCustomKeywordSpy = sinon.spy(addCustomKeyword); - return middleware.init('test/pet-store-swagger.yaml') - .then(function () { - expect(addCustomKeywordSpy).to.have.not.been.called; - }); + return middleware.init('test/pet-store-swagger.yaml'); }); }); describe('Simple server - no options', function () { diff --git a/test/koa/middleware-test.js b/test/koa/middleware-test.js index 140be27..ec9db9a 100644 --- a/test/koa/middleware-test.js +++ b/test/koa/middleware-test.js @@ -20,12 +20,7 @@ describe('input-validation middleware tests - Koa', function () { it('should resolve without formats', function () { let rewire = require('rewire'); let middleware = rewire('../../src/middleware'); - let addCustomKeyword = middleware.__get__('addCustomKeyword'); - let addCustomKeywordSpy = sinon.spy(addCustomKeyword); - return middleware.init('test/pet-store-swagger.yaml') - .then(function () { - expect(addCustomKeywordSpy).to.have.not.been.called; - }); + return middleware.init('test/pet-store-swagger.yaml'); }); }); diff --git a/test/openapi3/openapi3-test.js b/test/openapi3/openapi3-test.js new file mode 100644 index 0000000..b02e809 --- /dev/null +++ b/test/openapi3/openapi3-test.js @@ -0,0 +1,317 @@ +'use strict'; + +let chai = require('chai'), + expect = chai.expect, + sinon = require('sinon'), + chaiSinon = require('chai-sinon'), + request = require('supertest'); +chai.use(chaiSinon); +let inputValidationOptions = function () { + return { + formats: [ + { name: 'double', pattern: /\d+(\.\d+)?/ }, + { name: 'int64', pattern: /^\d{1,19}$/ }, + { name: 'int32', pattern: /^\d{1,10}$/ } + ], + beautifyErrors: true, + firstError: false + }; +}; +describe('input-validation middleware tests', function () { + let app; + + describe('firstError=false', function () { + before(function () { + return require('./test-server-pet')(inputValidationOptions()).then(function (testServer) { + app = testServer; + }); + }); + it('valid dog', function (done) { + request(app) + .post('/pet') + .set('public-key', '1.0') + .send({ + bark: 'hav hav' + }) + .expect(200, function (err, res) { + if (err) { + throw err; + } + expect(res.body.result).to.equal('OK'); + done(); + }); + }); + it('invalid dog', function (done) { + request(app) + .post('/pet') + .set('public-key', '1.0') + .send({ + bark: 5 + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': "[\"body/bark should be string\",\"body should have required property 'fur'\",\"body should match exactly one schema in oneOf\"]" + }); + done(); + }); + }); + it('valid cat', function (done) { + request(app) + .post('/pet') + .set('public-key', '1.0') + .send({ + fur: '6' + }) + .expect(200, function (err, res) { + if (err) { + throw err; + } + expect(res.body.result).to.equal('OK'); + done(); + }); + }); + it('invalid cat', function (done) { + // fail it is not match to two cases + request(app) + .post('/pet') + .set('public-key', '1.0') + .send({ + fur: 'blabla' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': "[\"body should have required property 'bark'\",\"body/fur should match pattern \\\"^\\\\d+$\\\"\",\"body should match exactly one schema in oneOf\"]" + }); + done(); + }); + }); + it('missing header public key', function (done) { + request(app) + .post('/pet') + .send({ + fur: '6' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': "[\"headers should have required property 'public-key'\"]" + }); + done(); + }); + }); + + describe('discriminator-pet', function () { + it('missing discriminator field', function (done) { + request(app) + .post('/pet-discriminator') + .set('public-key', '1.0') + .send({ + fur: '6' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': '["body/type should be equal to one of the allowed values [dog_object,cat_object]"]' + }); + done(); + }); + }); + + it('when discriminator type is dog and missing field', function (done) { + request(app) + .post('/pet-discriminator') + .set('public-key', '1.0') + .send({ + type: 'dog_object' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': "[\"body should have required property 'bark'\"]" + }); + done(); + }); + }); + }); + describe('discriminator-multiple pet', function () { + it('missing discriminator field on the root', function (done) { + request(app) + .post('/pet-discriminator-multiple') + .set('public-key', '1.0') + .send({ + fur: '200' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': '["body/type should be equal to one of the allowed values [dog_multiple,cat_object]"]' + }); + done(); + }); + }); + it('missing discriminator field on the on inside discriminator', function (done) { + request(app) + .post('/pet-discriminator-multiple') + .set('public-key', '1.0') + .send({ + bark: 'hav hav', + type: 'dog_multiple' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': '["body/model should be equal to one of the allowed values [small_dog,big_dog]"]' + }); + done(); + }); + }); + + it('when discriminator type is dog_multiple and model small_dog and missing root field name and specific plane field', function (done) { + request(app) + .post('/pet-discriminator-multiple') + .set('public-key', '1.0') + .send({ + type: 'dog_multiple', + model: 'small_dog' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': "[\"body should have required property 'max_length'\",\"body should have required property 'name'\",\"body should have required property 'dog_age'\"]" + }); + done(); + }); + }); + + it('when valid discriminator type is dog_multiple and model small_dog', function (done) { + request(app) + .post('/pet-discriminator-multiple') + .set('public-key', '1.0') + .send({ + name: 'sesna', + max_length: 'max_length', + dog_age: '3', + type: 'dog_multiple', + model: 'small_dog' + }) + .expect(200, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'result': 'OK' + }); + done(); + }); + }); + }); + describe('discriminator-mapping pet', function () { + it('missing discriminator field on the root', function (done) { + request(app) + .post('/pet-discriminator-mapping') + .set('public-key', '1.0') + .send({ + fur: '6' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': '["body/type should be equal to one of the allowed values [mapped_dog,mapped_cat]"]' + }); + done(); + }); + }); + + it('when discriminator type is mapped_dog and model small_dog and missing root field name and specific dog field', function (done) { + request(app) + .post('/pet-discriminator-mapping') + .set('public-key', '1.0') + .send({ + type: 'mapped_dog', + model: 'small_dog' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': "[\"body should have required property 'max_length'\",\"body should have required property 'name'\",\"body should have required property 'dog_age'\"]" + }); + done(); + }); + }); + + it('when valid discriminator type is mapped_dog and model small_dog', function (done) { + request(app) + .post('/pet-discriminator-mapping') + .set('public-key', '1.0') + .send({ + name: 'sesna', + max_length: 'max_length', + dog_age: '200', + type: 'mapped_dog', + model: 'small_dog' + }) + .expect(200, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'result': 'OK' + }); + done(); + }); + }); + }); + describe.skip('discriminator pet type is not on the root, only on child', function () { + // does not support wright now. + }); + }); + describe('firstError=true', function () { + before(function () { + const options = inputValidationOptions(); + options.firstError = true; + return require('./test-server-pet')(options).then(function (testServer) { + app = testServer; + }); + }); + it('when discriminator type is mapped_dog and model small_dog and missing root field name and specific dog field', function (done) { + request(app) + .post('/pet-discriminator-mapping') + .set('public-key', '1.0') + .send({ + type: 'mapped_dog', + model: 'small_dog' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + expect(res.body).to.eql({ + 'more_info': "\"body should have required property 'max_length'\"" + }); + done(); + }); + }); + }); +}); diff --git a/test/openapi3/pets-unlimited-recursive-test.js b/test/openapi3/pets-unlimited-recursive-test.js new file mode 100644 index 0000000..781af34 --- /dev/null +++ b/test/openapi3/pets-unlimited-recursive-test.js @@ -0,0 +1,28 @@ +'use strict'; + +let chai = require('chai'), + expect = chai.expect, + chaiSinon = require('chai-sinon'); +chai.use(chaiSinon); + +let inputValidationOptions = function () { + return { + formats: [ + { name: 'double', pattern: /\d+(\.\d+)?/ }, + { name: 'int64', pattern: /^\d{1,19}$/ }, + { name: 'int32', pattern: /^\d{1,10}$/ } + ], + beautifyErrors: true, + firstError: false + }; +}; +describe('unlimited recursive swagger definitions', function () { + it('should throw error on init', function () { + return require('./test-server-pet-recursive')(inputValidationOptions()) + .then(function () { + throw new Error('should not get here'); + }).catch(function (err) { + expect(err.message).eql('swagger schema exceed maximum supported depth of 20 for swagger definitions inheritance'); + }); + }); +}); diff --git a/test/openapi3/pets-unlimited-recursive.yaml b/test/openapi3/pets-unlimited-recursive.yaml new file mode 100644 index 0000000..df9e28d --- /dev/null +++ b/test/openapi3/pets-unlimited-recursive.yaml @@ -0,0 +1,135 @@ +openapi: 3.0.0 +servers: + - url: 'https://api.paymentsos.com/' +info: + x-logo: + url: payos_logo_blue_pad.png + backgroundColor: white + description: > + # Overview + + version: 1.2.0 + title: PaymentsOS API +tags: + - name: Tokens + description: >- + Tokenization is a process that safeguards sensitive card data, converting + a card's details to a representative token. +x-tagGroups: + - name: Reference + tags: + - Tokens +security: + - app-id: [] + private-key: [] +paths: + /pet-recursive: + post: + summary: Create a multiple pet + security: + - public-key: [] + description: >- + tags: + - pets + operationId: create-a-token + parameters: + - $ref: '#/components/parameters/public-key' + responses: + '201': + description: pet created + headers: + x-zooz-request-id: + description: request id + schema: + type: string + '400': + description: Bad request + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '401': + description: Unauthorize + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '500': + description: Internal error + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/pet-recursive' + description: Create pet + required: true +components: + parameters: + public-key: + name: public-key + in: header + required: true + example: f8948bac-8f29-4a82-8cef-704fb1e6b7ca + schema: + type: string + schemas: + error_model: + required: + - description + - category + properties: + category: + type: string + description: Error code. + description: + type: string + description: Error message for the developer. + more_info: + type: string + description: 'More info about the error, can include link to the documentation.' + pet-recursive: + description: pet + type: object + oneOf: + - $ref: '#/components/schemas/dog_object_recursive' + - $ref: '#/components/schemas/cat_object' + discriminator: + propertyName: type + dog_object_recursive: + type: object + required: + - type_recursive + oneOf: + - $ref: '#/components/schemas/pet-recursive' + discriminator: + propertyName: type_recursive + properties: + bark: + type: string + cat_object: + type: object + required: + - fur + properties: + fur: + type: string + pattern: '^\d+$' \ No newline at end of file diff --git a/test/openapi3/pets.yaml b/test/openapi3/pets.yaml new file mode 100644 index 0000000..49aed56 --- /dev/null +++ b/test/openapi3/pets.yaml @@ -0,0 +1,457 @@ +openapi: 3.0.0 +servers: + - url: 'https://api.paymentsos.com/' +info: + x-logo: + url: payos_logo_blue_pad.png + backgroundColor: white + description: > + # Overview + + version: 1.2.0 + title: PaymentsOS API +tags: + - name: Tokens + description: >- + Tokenization is a process that safeguards sensitive card data, converting + a card's details to a representative token. +x-tagGroups: + - name: Reference + tags: + - Tokens +security: + - app-id: [] + private-key: [] +paths: + /pet: + post: + summary: Create a Pet + security: + - public-key: [] + description: >- + tags: + - pets + operationId: create-a-token + parameters: + - $ref: '#/components/parameters/public-key' + responses: + '201': + description: pet created + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/pet' + '400': + description: Bad request + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '401': + description: Unauthorize + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '500': + description: Internal error + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/pet' + description: Create pet + required: true + /pet-discriminator: + post: + summary: Create a pet + security: + - public-key: [] + description: >- + tags: + - pets + operationId: create-a-pet + parameters: + - $ref: '#/components/parameters/public-key' + responses: + '201': + description: pet created + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/pet' + '400': + description: Bad request + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '401': + description: Unauthorize + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '500': + description: Internal error + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/pet-discriminator' + description: Create pet + required: true + /pet-discriminator-multiple: + post: + summary: Create a multiple pet + security: + - public-key: [] + description: >- + tags: + - pets + operationId: create-a-token + parameters: + - $ref: '#/components/parameters/public-key' + responses: + '201': + description: pet created + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/pet' + '400': + description: Bad request + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '401': + description: Unauthorize + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '500': + description: Internal error + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/pet-discriminator-multiple' + description: Create pet + required: true + /pet-discriminator-mapping: + post: + summary: Create a multiple pet + security: + - public-key: [] + description: >- + tags: + - pets + operationId: create-a-token + parameters: + - $ref: '#/components/parameters/public-key' + responses: + '201': + description: pet created + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/pet' + '400': + description: Bad request + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '401': + description: Unauthorize + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '500': + description: Internal error + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/pet-discriminator-mapping' + description: Create pet + required: true + /pet-discriminator-on-child: + post: + summary: Create a multiple pet + security: + - public-key: [] + description: >- + tags: + - pets + operationId: create-a-token + parameters: + - $ref: '#/components/parameters/public-key' + responses: + '201': + description: pet created + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/pet' + '400': + description: Bad request + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '401': + description: Unauthorize + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + '500': + description: Internal error + headers: + x-zooz-request-id: + description: request id + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/error_model' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/pet-discriminator-on-child' + description: Create pet + required: true +components: + parameters: + public-key: + name: public-key + in: header + required: true + example: f8948bac-8f29-4a82-8cef-704fb1e6b7ca + schema: + type: string + schemas: + error_model: + required: + - description + - category + properties: + category: + type: string + description: Error code. + description: + type: string + description: Error message for the developer. + more_info: + type: string + description: 'More info about the error, can include link to the documentation.' + pet: + description: pet + type: object + oneOf: + - $ref: '#/components/schemas/dog_object' + - $ref: '#/components/schemas/cat_object' + pet-discriminator-on-child: + description: pet + type: object + required: + - pet + properties: + pet: + $ref: '#/components/schemas/pet-discriminator' + pet-discriminator: + description: pet + type: object + oneOf: + - $ref: '#/components/schemas/dog_object' + - $ref: '#/components/schemas/cat_object' + discriminator: + propertyName: type + pet-discriminator-multiple: + description: pet + type: object + oneOf: + - $ref: '#/components/schemas/dog_multiple' + - $ref: '#/components/schemas/cat_object' + discriminator: + propertyName: type + properties: + name: + type: string + required: + - name + pet-discriminator-mapping: + description: pet + type: object + oneOf: + - $ref: '#/components/schemas/dog_multiple' + - $ref: '#/components/schemas/cat_object' + discriminator: + propertyName: type + mapping: + mapped_dog: '#/components/schemas/dog_multiple' + mapped_cat: '#/components/schemas/cat_object' + properties: + name: + type: string + required: + - name + dog_object: + type: object + required: + - bark + properties: + bark: + type: string + dog_multiple: + type: object + required: + - dog_age + discriminator: + propertyName: model + oneOf: + - $ref: '#/components/schemas/small_dog' + - $ref: '#/components/schemas/big_dog' + properties: + dog_age: + type: string + cat_object: + type: object + required: + - fur + properties: + fur: + type: string + pattern: '^\d+$' + + small_dog: + type: object + required: + - max_length + properties: + max_length: + type: string + big_dog: + type: object + required: + - min_length + properties: + min_length: + type: string \ No newline at end of file diff --git a/test/openapi3/test-server-pet-recursive.js b/test/openapi3/test-server-pet-recursive.js new file mode 100644 index 0000000..f975419 --- /dev/null +++ b/test/openapi3/test-server-pet-recursive.js @@ -0,0 +1,35 @@ +'use strict'; + +var express = require('express'); +var bodyParser = require('body-parser'); +var inputValidation = require('../../src/middleware'); + +var inputValidationOptions = { + formats: [ + { name: 'double', pattern: /\d+(\.\d+)?/ }, + { name: 'int64', pattern: /^\d{1,19}$/ }, + { name: 'int32', pattern: /^\d{1,10}$/ } + ], + beautifyErrors: true, + firstError: true +}; + +module.exports = function (options) { + return inputValidation.init(`${__dirname}/pets-unlimited-recursive.yaml`, options || inputValidationOptions) + .then(function () { + var app = express(); + app.use(bodyParser.json()); + + app.post('/pet-recursive', inputValidation.validate, function (req, res, next) { + res.json({ result: 'OK' }); + }); + + app.use(function (err, req, res, next) { + if (err instanceof inputValidation.InputValidationError) { + res.status(400).json({ more_info: JSON.stringify(err.errors) }); + } + }); + + return Promise.resolve(app); + }); +}; \ No newline at end of file diff --git a/test/openapi3/test-server-pet.js b/test/openapi3/test-server-pet.js new file mode 100644 index 0000000..80b8a3c --- /dev/null +++ b/test/openapi3/test-server-pet.js @@ -0,0 +1,48 @@ +'use strict'; + +var express = require('express'); +var bodyParser = require('body-parser'); +var inputValidation = require('../../src/middleware'); + +var inputValidationOptions = { + formats: [ + { name: 'double', pattern: /\d+(\.\d+)?/ }, + { name: 'int64', pattern: /^\d{1,19}$/ }, + { name: 'int32', pattern: /^\d{1,10}$/ } + ], + beautifyErrors: true, + firstError: true +}; + +module.exports = function (options) { + return inputValidation.init(`${__dirname}/pets.yaml`, options || inputValidationOptions) + .then(function () { + var app = express(); + app.use(bodyParser.json()); + + app.post('/pet', inputValidation.validate, function (req, res, next) { + res.json({ result: 'OK' }); + }); + app.post('/pet-discriminator', inputValidation.validate, function (req, res, next) { + res.json({ result: 'OK' }); + }); + + app.post('/pet-discriminator-multiple', inputValidation.validate, function (req, res, next) { + res.json({ result: 'OK' }); + }); + app.post('/pet-discriminator-mapping', inputValidation.validate, function (req, res, next) { + res.json({ result: 'OK' }); + }); + app.post('/pet-discriminator-on-child', inputValidation.validate, function (req, res, next) { + res.json({ result: 'OK' }); + }); + + app.use(function (err, req, res, next) { + if (err instanceof inputValidation.InputValidationError) { + res.status(400).json({ more_info: JSON.stringify(err.errors) }); + } + }); + + return Promise.resolve(app); + }); +}; \ No newline at end of file