Repository: codekie/openapi-examples-validator (js)

Issue:
Custom String format
Hi there! I noticed that examples using custom string format are being interpreted as errors

Is this a know issue? 

`"type": "Validation",
"message": "unknown format \"country-code-2\" is used in schema at path \"#/properties/data/items/properties/location\"",`

Pull request:
feat: add option to ignore (unknown) data type formats
Closes #144 

Patch:
diff --git a/src/cli.js b/src/cli.js
index 0006385..6cbc388 100755
--- a/src/cli.js
+++ b/src/cli.js
@@ -29,6 +29,8 @@ program
     .option('-c, --cwd-to-mapping-file', "changes to the directory of the mapping-file, before resolving the example's"
         + ' paths. Use this option, if your mapping-files use relative paths for the examples')
     .option('-n, --no-additional-properties', 'don\'t allow properties that are not described in the schema')
+    .option('-o, --ignore-formats <ignored-formats...>', 'Datatype formats to ignore '
+        + '(to prevent "unknown format" errors.)')
     .action(processAction);
 program.on('--help', () => {
     console.log('\n\n  Example for external example-file:\n');
@@ -42,17 +44,28 @@ module.exports = program.parseAsync(process.argv);
 
 async function processAction(filepath, options) {
     const { schemaJsonpath, exampleFilepath, mappingFilepath, cwdToMappingFile } = options,
-        noAdditionalProperties = !options.additionalProperties;
+        noAdditionalProperties = !options.additionalProperties,
+        ignoreFormats = _prepareIgnoreFormats(options.ignoreFormats);
     let result;
     if (mappingFilepath) {
         console.log('Validating with mapping file');
-        result = await validateExamplesByMap(filepath, mappingFilepath, { cwdToMappingFile, noAdditionalProperties });
+        result = await validateExamplesByMap(filepath, mappingFilepath, {
+            cwdToMappingFile,
+            noAdditionalProperties,
+            ignoreFormats
+        });
     } else if (schemaJsonpath && exampleFilepath) {
         console.log('Validating single external example');
-        result = await validateExample(filepath, schemaJsonpath, exampleFilepath, { noAdditionalProperties });
+        result = await validateExample(filepath, schemaJsonpath, exampleFilepath, {
+            noAdditionalProperties,
+            ignoreFormats
+        });
     } else {
         console.log('Validating examples');
-        result = await validateFile(filepath, { noAdditionalProperties });
+        result = await validateFile(filepath, {
+            noAdditionalProperties,
+            ignoreFormats
+        });
     }
     _handleResult(result);
 }
@@ -87,3 +100,11 @@ function _printStatistics(statistics) {
     }
     process.stdout.write(`${ strStatistics.join('\n') }\n`);
 }
+
+function _prepareIgnoreFormats(ignoreFormats) {
+    if (ignoreFormats == null || !Array.isArray(ignoreFormats)) { return ignoreFormats; }
+    if (ignoreFormats.length !== 1) { return ignoreFormats; }
+    // If only one argument has been passed, with all formats separated by newlines
+    if (ignoreFormats[0].indexOf('\n') === -1) { return ignoreFormats; }
+    return ignoreFormats[0].split('\n').filter(entry => !entry.match(/^\s*$/));
+}
diff --git a/src/index.js b/src/index.js
index 6129d5d..a4016f3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -93,9 +93,13 @@ module.exports = {
  * Validates OpenAPI-spec with embedded examples.
  * @param {Object}  openapiSpec OpenAPI-spec
  * @param {boolean} [noAdditionalProperties=false]  Don't allow properties that are not defined in the schema
+ * @param {Array.<string>} [ignoreFormats]          List of datatype formats that shall be ignored (to prevent
+ *                                                  "unsupported format" errors). If an Array with only one string is
+ *                                                  provided where the formats are separated with `\n`, the entries
+ *                                                  will be expanded to a new array containing all entries.
  * @returns {ValidationResponse}
  */
-async function validateExamples(openapiSpec, { noAdditionalProperties } = {}) {
+async function validateExamples(openapiSpec, { noAdditionalProperties, ignoreFormats } = {}) {
     const impl = Determiner.getImplementation(openapiSpec);
     openapiSpec = await refParser.dereference(openapiSpec);
     openapiSpec = impl.prepare(openapiSpec, { noAdditionalProperties });
@@ -104,23 +108,27 @@ async function validateExamples(openapiSpec, { noAdditionalProperties } = {}) {
             return res.concat(_extractExamplePaths(openapiSpec, pathToExamples));
         }, [])
         .map(impl.escapeExampleName);
-    return _validateExamplesPaths({ impl }, pathsExamples, openapiSpec);
+    return _validateExamplesPaths({ impl }, pathsExamples, openapiSpec, { ignoreFormats });
 }
 
 /**
  * Validates OpenAPI-spec with embedded examples.
  * @param {string}  filePath                        File-path to the OpenAPI-spec
  * @param {boolean} [noAdditionalProperties=false]  Don't allow properties that are not defined in the schema
+ * @param {Array.<string>} [ignoreFormats]          List of datatype formats that shall be ignored (to prevent
+ *                                                  "unsupported format" errors). If an Array with only one string is
+ *                                                  provided where the formats are separated with `\n`, the entries
+ *                                                  will be expanded to a new array containing all entries.
  * @returns {ValidationResponse}
  */
-async function validateFile(filePath, { noAdditionalProperties } = {}) {
+async function validateFile(filePath, { noAdditionalProperties, ignoreFormats } = {}) {
     let openapiSpec = null;
     try {
         openapiSpec = await _parseSpec(filePath);
     } catch (err) {
         return createValidationResponse({ errors: [ApplicationError.create(err)] });
     }
-    return validateExamples(openapiSpec, { noAdditionalProperties });
+    return validateExamples(openapiSpec, { noAdditionalProperties, ignoreFormats });
 }
 
 /**
@@ -132,10 +140,14 @@ async function validateFile(filePath, { noAdditionalProperties } = {}) {
  * @param {boolean} [cwdToMappingFile=false]    Change working directory for resolving the example-paths (relative to
  *                                              the mapping-file)
  * @param {boolean} [noAdditionalProperties=false] Don't allow properties that are not defined in the schema
+ * @param {Array.<string>} [ignoreFormats]      List of datatype formats that shall be ignored (to prevent
+ *                                              "unsupported format" errors). If an Array with only one string is
+ *                                              provided where the formats are separated with `\n`, the entries
+ *                                              will be expanded to a new array containing all entries.
  * @returns {ValidationResponse}
  */
 async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
-    { cwdToMappingFile, noAdditionalProperties } = {}
+    { cwdToMappingFile, noAdditionalProperties, ignoreFormats } = {}
 ) {
     let matchingFilePathsMapping = 0;
     const filePathsMaps = glob.sync(
@@ -167,7 +179,8 @@ async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
                     return _handleExamplesByMapValidation(
                         openapiSpec, mapExternalExamples, statistics, {
                             cwdToMappingFile,
-                            dirPathMapExternalExamples: path.dirname(filePathMapExternalExamples)
+                            dirPathMapExternalExamples: path.dirname(filePathMapExternalExamples),
+                            ignoreFormats
                         }
                     ).map(
                         (/** @type ApplicationError */ error) => Object.assign(error, {
@@ -195,9 +208,16 @@ async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
  * @param {String}  pathSchema                      JSON-path to the schema
  * @param {String}  filePathExample                 File-path to the external example-file
  * @param {boolean} [noAdditionalProperties=false]  Don't allow properties that are not described in the schema
+ * @param {Array.<string>} [ignoreFormats]          List of datatype formats that shall be ignored (to prevent
+ *                                                  "unsupported format" errors). If an Array with only one string is
+ *                                                  provided where the formats are separated with `\n`, the entries
+ *                                                  will be expanded to a new array containing all entries.
  * @returns {ValidationResponse}
  */
-async function validateExample(filePathSchema, pathSchema, filePathExample, { noAdditionalProperties } = {}) {
+async function validateExample(filePathSchema, pathSchema, filePathExample, {
+    noAdditionalProperties,
+    ignoreFormats
+} = {}) {
     let example = null,
         schema = null,
         openapiSpec = null;
@@ -212,7 +232,7 @@ async function validateExample(filePathSchema, pathSchema, filePathExample, { no
     }
     return _validate(
         statistics => _validateExample({
-            createValidator: _initValidatorFactory(openapiSpec),
+            createValidator: _initValidatorFactory(openapiSpec, { ignoreFormats }),
             schema,
             example,
             statistics,
@@ -283,11 +303,15 @@ function _validate(validationHandler) {
  * @param {boolean}                 [cwdToMappingFile=false]        Change working directory for resolving the example-
  *                                                                  paths (relative to the mapping-file)
  * @param {string}                  [dirPathMapExternalExamples]    The directory-path of the mapping-file
+ * @param {Array.<string>} [ignoreFormats]          List of datatype formats that shall be ignored (to prevent
+ *                                                  "unsupported format" errors). If an Array with only one string is
+ *                                                  provided where the formats are separated with `\n`, the entries
+ *                                                  will be expanded to a new array containing all entries.
  * @returns {Array.<ApplicationError>}
  * @private
  */
 function _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statistics,
-    { cwdToMappingFile = false, dirPathMapExternalExamples }
+    { cwdToMappingFile = false, dirPathMapExternalExamples, ignoreFormats }
 ) {
     return flatMap(Object.entries(mapExternalExamples), ([pathSchema, filePathsExample]) => {
         let schema = null;
@@ -310,7 +334,7 @@ function _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statis
                     return [ApplicationError.create(err)];
                 }
                 return _validateExample({
-                    createValidator: _initValidatorFactory(openapiSpec),
+                    createValidator: _initValidatorFactory(openapiSpec, { ignoreFormats }),
                     schema,
                     example,
                     statistics,
@@ -373,17 +397,21 @@ function _extractExamplePaths(openapiSpec, jsonPathToExamples) {
  * @param {Object}          impl            Spec-dependant validator
  * @param {Array.<String>}  pathsExamples   JSON-paths to examples
  * @param {Object}          openapiSpec     OpenAPI-spec
+ * @param {Array.<string>} [ignoreFormats]  List of datatype formats that shall be ignored (to prevent
+ *                                          "unsupported format" errors). If an Array with only one string is
+ *                                          provided where the formats are separated with `\n`, the entries
+ *                                          will be expanded to a new array containing all entries.
  * @returns {ValidationResponse}
  * @private
  */
-function _validateExamplesPaths({ impl }, pathsExamples, openapiSpec) {
+function _validateExamplesPaths({ impl }, pathsExamples, openapiSpec, { ignoreFormats }) {
     const statistics = _initStatistics(),
         validationResult = {
             valid: true,
             statistics,
             errors: []
         },
-        createValidator = _initValidatorFactory(openapiSpec);
+        createValidator = _initValidatorFactory(openapiSpec, { ignoreFormats });
     let validationMap;
     try {
         // Create mapping between JSON-schemas and examples
@@ -528,11 +556,15 @@ function _validateExample({ createValidator, schema, example, statistics, filePa
  * @returns {ajv}
  * @private
  */
-function _initValidatorFactory(specSchema) {
+function _initValidatorFactory(specSchema, { ignoreFormats }) {
     return getValidatorFactory(specSchema, {
         schemaId: 'auto',
         allErrors: true,
-        nullable: true
+        nullable: true,
+        formats: ignoreFormats && ignoreFormats.reduce((result, entry) => {
+            result[entry] = () => true;
+            return result;
+        }, {})
     });
 }
 


Test patch:
diff --git a/test/data/output-help.js b/test/data/output-help.js
index e8f1083..2c12f40 100644
--- a/test/data/output-help.js
+++ b/test/data/output-help.js
@@ -13,6 +13,7 @@ Options:
   -m, --mapping-filepath <mapping-filepath>  file path to map, containing schema-paths as key and the file-path(s) to examples as value. If wildcards are used, the parameter has to be put in quotes.
   -c, --cwd-to-mapping-file                  changes to the directory of the mapping-file, before resolving the example's paths. Use this option, if your mapping-files use relative paths for the examples
   -n, --no-additional-properties             don't allow properties that are not described in the schema
+  -o, --ignore-formats <ignored-formats...>  Datatype formats to ignore (to prevent "unknown format" errors.)
   -h, --help                                 display help for command
 
 
diff --git a/test/data/v3/unknown-formats.json b/test/data/v3/unknown-formats.json
new file mode 100644
index 0000000..ba6b3c9
--- /dev/null
+++ b/test/data/v3/unknown-formats.json
@@ -0,0 +1,58 @@
+{
+  "openapi": "3.0.0",
+  "info": {
+    "version": "1.0.0",
+    "title": "Sample number formats",
+    "license": {
+      "name": "MIT"
+    }
+  },
+  "servers": [
+    {
+      "url": "http://example.com/v1"
+    }
+  ],
+  "paths": {
+    "/get-numbers": {
+      "get": {
+        "summary": "Gets some big numbers",
+        "operationId": "getNumbers",
+        "responses": {
+          "200": {
+            "description": "Some big numbers",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "name": {
+                      "type": "string"
+                    },
+                    "status": {
+                      "type": "string",
+                      "format": "continental-status"
+                    },
+                    "licensePlate": {
+                      "type": "string",
+                      "format": "license-plate"
+                    },
+                    "country": {
+                      "type": "string",
+                      "format": "country-code-2"
+                    }
+                  }
+                },
+                "example": {
+                  "name": "John Wick",
+                  "status": "Excommunicado",
+                  "licensePlate": "XAB 235",
+                  "country": "US"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/test/specs/cli.js b/test/specs/cli.js
index 5601a5b..6e546e9 100644
--- a/test/specs/cli.js
+++ b/test/specs/cli.js
@@ -152,6 +152,35 @@ describe('CLI-module', function() {
             });
         });
     });
+    describe('ignore datatype formats', function() {
+        it('should errors when no formats have been passed', async function() {
+            const pathSchema = getPathOfTestData('v3/unknown-formats.json', true);
+            try {
+                await exec(
+                    `${CMD__RUN} --ignore-formats country-code-2 continental-status license-plate -- ${pathSchema}`
+                );
+            } catch ({ stdout, stderr }) {
+                stdout.should.not.equal('');
+                stderr.should.not.equal('');
+            }
+        });
+        it('should show no error when formats have been passed', async function() {
+            const pathSchema = getPathOfTestData('v3/unknown-formats.json', true);
+            const { stdout, stderr } = await exec(
+                `${CMD__RUN} --ignore-formats country-code-2 continental-status license-plate -- ${pathSchema}`
+            );
+            stdout.should.not.equal('');
+            stderr.should.equal('');
+        });
+        it('should show no error when formats have been passed, with newline separated', async function() {
+            const pathSchema = getPathOfTestData('v3/unknown-formats.json', true);
+            const { stdout, stderr } = await exec(
+                `${CMD__RUN} --ignore-formats "country-code-2\ncontinental-status\nlicense-plate" -- ${pathSchema}`
+            );
+            stdout.should.not.equal('');
+            stderr.should.equal('');
+        });
+    });
     describe('execute as module, via `require` (for mutation-tests)', function() {
         before(function() {
             // Preparations to run the CLI as module and not as child-process
diff --git a/test/specs/impl/v3/index.js b/test/specs/impl/v3/index.js
index 291394e..27f59fd 100644
--- a/test/specs/impl/v3/index.js
+++ b/test/specs/impl/v3/index.js
@@ -68,7 +68,8 @@ const JSON_PATH__CONTEXT_MUTUALLY_EXCLUSIVE = '/paths/~1pets/get/responses/200/c
     FILE_PATH__INVALID_EXAMPLES_EXCLUSIVE_MINIMUM
         = path.join(__dirname, '../../../data/v3/simple-api-with-examples-exclusive-minimum-invalid.json'),
     FILE_PATH__EXAMPLE_NAMES_TO_BE_ESCAPED
-        = path.join(__dirname, '../../../data/v3/simple-api-with-example-names-to-be-escaped.json');
+        = path.join(__dirname, '../../../data/v3/simple-api-with-example-names-to-be-escaped.json'),
+    FILE_PATH__UNKNOWN_FORMATS = path.join(__dirname, '../../../data/v3/unknown-formats.json');
 
 describe('Main-module, for v3 should', function() {
     describe('recognize', function() {