Repository: codekie/openapi-examples-validator
Pull request #172 (2022-06-12 21:11:52)

Add support for wildcards in the map file #170
I'm using the function `glob.sync()` (that you already use to find the mapping file) to expand the wildcards in the path to the example inside the mapping files.

marc0l92: > Please also add at least one test case for invalid examples and make sure that then output for globbed paths is the same as with the non-globbed counterparts (the ordering of files can be disregarded).

How do you suggest me to do this?
codekie: I'd suggest, you create two mapping files, one with and one without globs, selecting the same files and use the same reference values to test against.

Btw. I really appreciate your efforts 👍. Thanks
marc0l92: I created the test you asked
marc0l92: > Just in case you made a force-push, please don't as GitHub can't automatically detect the changes since the last review.

Ok, in my company we try to limit the number of commits using push force, but I will not do it here
codekie: The branch will be squashed to a single commit anyways when the pull request is merged, so it does not matter how many commits in the initial branch were 👍 
marc0l92: You can merge

Patch:
diff --git a/README.md b/README.md
index 2fbcf48..2ef9d7f 100644
--- a/README.md
+++ b/README.md
@@ -67,9 +67,11 @@ To validate multiple external examples, pass a mapping file with a similar struc
     "test/data/external-examples-valid-example2.json",
     "test/data/external-examples-invalid-type.json"
   ],
-  "$.paths./.get.responses.300.schema": "test/data/external-examples-invalid-missing-link.json"
+  "$.paths./.get.responses.300.schema": "test/data/external-examples-invalid-missing-link.json",
+  "$.paths./.post.parameters[?(@.in==="body")].schema": "test/data/v2/post-request/*.json"
 }
 ```
+It is possible to use wildcards in the command line to select multiple mapping files, and it is possible to use wildcards inside the mapping file to select multiple examples.
 
 Errors will be written to `stderr`.
 
diff --git a/src/index.js b/src/index.js
index 431cdd3..8ce43a5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -324,22 +324,35 @@ function _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statis
         return flatMap(
             flatten([filePathsExample]),
             filePathExample => {
-                let example = null;
+                let examples = [];
                 try {
                     const resolvedFilePathExample = cwdToMappingFile
                         ? path.join(dirPathMapExternalExamples, filePathExample)
                         : filePathExample;
-                    example = JSON.parse(fs.readFileSync(resolvedFilePathExample, 'utf-8'));
+                    const globResolvedFilePathExample = glob.sync(resolvedFilePathExample);
+                    if (globResolvedFilePathExample.length === 0) {
+                        return [ApplicationError.create({
+                            type: ErrorType.jsENOENT,
+                            message: `No such file or directory: '${resolvedFilePathExample}'`,
+                            path: resolvedFilePathExample
+                        })];
+                    }
+                    for (const filePathExample of globResolvedFilePathExample) {
+                        examples.push({
+                            path: path.normalize(filePathExample),
+                            content: JSON.parse(fs.readFileSync(filePathExample, 'utf-8'))
+                        });
+                    }
                 } catch (err) {
                     return [ApplicationError.create(err)];
                 }
-                return _validateExample({
+                return flatMap(examples, example => _validateExample({
                     createValidator: _initValidatorFactory(openapiSpec, { ignoreFormats }),
                     schema,
-                    example,
+                    example: example.content,
                     statistics,
-                    filePathExample
-                });
+                    filePathExample: example.path
+                }));
             }
         );
     });


Test patch:
diff --git a/test/data/v2/map-external-examples-with-wildcards.json b/test/data/v2/map-external-examples-with-wildcards.json
new file mode 100644
index 0000000..cad19a5
--- /dev/null
+++ b/test/data/v2/map-external-examples-with-wildcards.json
@@ -0,0 +1,8 @@
+{
+  "$.paths./.get.responses.200.schema": [
+    "test/data/v2/external-examples-valid-example1.json",
+    "test/data/v2/external-*-valid-*.json",
+    "test/data/v2/external-*-invalid-*.json"
+  ],
+  "$.paths./.get.responses.300.schema": "test/data/v2/external-examples-valid-*.json"
+}
diff --git a/test/data/v2/map-external-examples-without-wildcards.json b/test/data/v2/map-external-examples-without-wildcards.json
new file mode 100644
index 0000000..a1d5463
--- /dev/null
+++ b/test/data/v2/map-external-examples-without-wildcards.json
@@ -0,0 +1,13 @@
+{
+  "$.paths./.get.responses.200.schema": [
+    "test/data/v2/external-examples-valid-example1.json",
+    "test/data/v2/external-examples-valid-example1.json",
+    "test/data/v2/external-examples-valid-example2.json",
+    "test/data/v2/external-examples-invalid-missing-link.json",
+    "test/data/v2/external-examples-invalid-type.json"
+  ],
+  "$.paths./.get.responses.300.schema": [
+    "test/data/v2/external-examples-valid-example1.json",
+    "test/data/v2/external-examples-valid-example2.json"
+  ]
+}
\ No newline at end of file
diff --git a/test/specs/impl/v2/index.js b/test/specs/impl/v2/index.js
index 36bcca2..3635f0f 100644
--- a/test/specs/impl/v2/index.js
+++ b/test/specs/impl/v2/index.js
@@ -13,7 +13,11 @@ const PATH__SCHEMA_EXTERNAL_EXAMPLE = '$.paths./.get.responses.200.schema',
     FILE_PATH__EXTERNAL_EXAMPLES_GLOB_INVALID1 = path.join(FILE_PATH__DATA, 'map-external-examples-glob-invalid1.json'),
     FILE_PATH__EXTERNAL_EXAMPLES_GLOB_INVALID2 = path.join(FILE_PATH__DATA, 'map-external-examples-glob-invalid2.json'),
     FILE_PATH__EXTERNAL_EXAMPLES_MAP = path.join(FILE_PATH__DATA, 'map-external-examples.json'),
-    FILE_PATH__EXTERNAL_EXAMPLES_MAP__RELATIVE = path.join(FILE_PATH__DATA, 'map-external-examples-relative.json'),
+    FILE_PATH__EXTERNAL_EXAMPLES_MAP_RELATIVE = path.join(FILE_PATH__DATA, 'map-external-examples-relative.json'),
+    FILE_PATH__EXTERNAL_EXAMPLES_MAP_WITH_WILDCARDS = path.join(FILE_PATH__DATA,
+        'map-external-examples-with-wildcards.json'),
+    FILE_PATH__EXTERNAL_EXAMPLES_MAP_WITHOUT_WILDCARDS = path.join(FILE_PATH__DATA,
+        'map-external-examples-without-wildcards.json'),
     FILE_PATH__EXTERNAL_EXAMPLES_MAP_WITH_WRONG_SCHEMA_PATH = path.join(FILE_PATH__DATA,
         'map-external-examples-map-with-wrong-schema-path.json'),
     FILE_PATH__EXTERNAL_EXAMPLES_MAP_WITH_MISSING_EXAMPLE = path.join(FILE_PATH__DATA,
@@ -186,6 +190,52 @@ describe('Main-module, for v2 should', () => {
                 })
             ]);
         });
+        it('should be able to expand examples wildcards', async() => {
+            const result = await validateExamplesByMap(FILE_PATH__EXTERNAL_EXAMPLES_SCHEMA,
+                FILE_PATH__EXTERNAL_EXAMPLES_MAP_WITH_WILDCARDS);
+            result.valid.should.equal(false);
+            result.statistics.should.deep.equal({
+                examplesTotal: 7,
+                examplesWithoutSchema: 0,
+                matchingFilePathsMapping: 1,
+                schemasWithExamples: 2
+            });
+            result.errors.should.deep.equal([{
+                type: 'Validation',
+                message: "should have required property 'links'",
+                keyword: 'required',
+                dataPath: '.versions[0]',
+                schemaPath: '#/properties/versions/items/required',
+                exampleFilePath: path.normalize('test/data/v2/external-examples-invalid-missing-link.json'),
+                mapFilePath: path.normalize(
+                    path.join(__dirname, '../../../../test/data/v2/map-external-examples-with-wildcards.json')),
+                params: {
+                    missingProperty: 'links'
+                }
+            }, {
+                type: 'Validation',
+                message: 'should be string',
+                keyword: 'type',
+                dataPath: '.versions[0].id',
+                schemaPath: '#/properties/versions/items/properties/id/type',
+                exampleFilePath: path.normalize('test/data/v2/external-examples-invalid-type.json'),
+                mapFilePath: path.normalize(
+                    path.join(__dirname, '../../../../test/data/v2/map-external-examples-with-wildcards.json')),
+                params: {
+                    type: 'string'
+                }
+            }]);
+        });
+        it('map of examples with wildcards should be equal to without wildcards', async() => {
+            const resultWithWildcards = await validateExamplesByMap(FILE_PATH__EXTERNAL_EXAMPLES_SCHEMA,
+                FILE_PATH__EXTERNAL_EXAMPLES_MAP_WITH_WILDCARDS);
+            const resultWithoutWildcards = await validateExamplesByMap(FILE_PATH__EXTERNAL_EXAMPLES_SCHEMA,
+                FILE_PATH__EXTERNAL_EXAMPLES_MAP_WITHOUT_WILDCARDS);
+            resultWithWildcards.valid.should.equal(resultWithoutWildcards.valid);
+            resultWithWildcards.statistics.should.deep.equal(resultWithoutWildcards.statistics);
+            removeMapFilePath(resultWithWildcards.errors).should.deep.equal(
+                removeMapFilePath(resultWithoutWildcards.errors));
+        });
     });
     describe('should throw errors', () => {
         describe("when files can't be found:", () => {
@@ -210,7 +260,7 @@ describe('Main-module, for v2 should', () => {
                 result.errors.should.deep.equal([
                     new ApplicationError(ErrorType.jsENOENT, {
                         mapFilePath: FILE_PATH__EXTERNAL_EXAMPLES_MAP_WITH_MISSING_EXAMPLE,
-                        message: "ENOENT: no such file or directory, open 'test/data/v2/blegh forgot the sugar in the"
+                        message: "No such file or directory: 'test/data/v2/blegh forgot the sugar in the"
                             + " coffee'",
                         params: {
                             path: 'test/data/v2/blegh forgot the sugar in the coffee'
@@ -338,7 +388,7 @@ describe('Main-module, for v2 should', () => {
     describe('with set `cwd-to-mapping-file`-flag', () => {
         it('resolve the relative paths in the mapping-files', async() => {
             const result = await validateExamplesByMap(FILE_PATH__EXTERNAL_EXAMPLES_SCHEMA,
-                FILE_PATH__EXTERNAL_EXAMPLES_MAP__RELATIVE, { cwdToMappingFile: true });
+                FILE_PATH__EXTERNAL_EXAMPLES_MAP_RELATIVE, { cwdToMappingFile: true });
             result.valid.should.equal(true);
             result.statistics.should.deep.equal({
                 schemasWithExamples: 2,
@@ -349,3 +399,10 @@ describe('Main-module, for v2 should', () => {
         });
     });
 });
+
+function removeMapFilePath(errors) {
+    return errors.map(error => {
+        delete error.mapFilePath;
+        return error;
+    });
+}
diff --git a/test/specs/impl/v3/index.js b/test/specs/impl/v3/index.js
index 004e879..2b9e26d 100644
--- a/test/specs/impl/v3/index.js
+++ b/test/specs/impl/v3/index.js
@@ -396,7 +396,10 @@ describe('Main-module, for v3 should', function() {
                     FILE_PATH__SCHEMA__WITH_ADDITIONAL_PROPERTIES,
                     FILE_PATH__MAP__WITH_ADDITIONAL_PROPERTIES,
                     { noAdditionalProperties: true }
-                )).errors.should.deep.equal(_expandFilePathOfErrors(errorsAdditionalPropertiesMap, 'mapFilePath'));
+                )).errors.should.deep.equal(
+                    _normalizeFilePathOfErrors(
+                        _expandFilePathOfErrors(errorsAdditionalPropertiesMap, 'mapFilePath'), 'exampleFilePath')
+                );
             });
         });
     });
@@ -419,7 +422,14 @@ describe('Main-module, for v3 should', function() {
 
 function _expandFilePathOfErrors(errors, propertyName) {
     errors.forEach(error => {
-        error[propertyName] = path.join(__dirname, '../../../..', error[propertyName]);
+        error[propertyName] = path.normalize(path.join(__dirname, '../../../..', error[propertyName]));
+    });
+    return errors;
+}
+
+function _normalizeFilePathOfErrors(errors, propertyName) {
+    errors.forEach(error => {
+        error[propertyName] = path.normalize(error[propertyName]);
     });
     return errors;
 }