Repository: Redocly/openapi-cli
Pull request #168 (2020-07-18 07:00:58)

chore(deps): bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/lodash/lodash/releases">lodash's releases</a>.</em></p>
<blockquote>
<h2>4.17.16</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/lodash/lodash/commit/d7fbc52ee0466a6d248f047b5d5c3e6d1e099056"><code>d7fbc52</code></a> Bump to v4.17.19</li>
<li><a href="https://github.com/lodash/lodash/commit/2e1c0f22f425e9c013815b2cd7c2ebd51f49a8d6"><code>2e1c0f2</code></a> Add npm-package</li>
<li><a href="https://github.com/lodash/lodash/commit/1b6c282299f4e0271f932b466c67f0f822aa308e"><code>1b6c282</code></a> Bump to v4.17.18</li>
<li><a href="https://github.com/lodash/lodash/commit/a370ac81408de2da77a82b3c4b61a01a3b9c2fac"><code>a370ac8</code></a> Bump to v4.17.17</li>
<li><a href="https://github.com/lodash/lodash/commit/1144918f3578a84fcc4986da9b806e63a6175cbb"><code>1144918</code></a> Rebuild lodash and docs</li>
<li><a href="https://github.com/lodash/lodash/commit/3a3b0fd339c2109563f7e8167dc95265ed82ef3e"><code>3a3b0fd</code></a> Bump to v4.17.16</li>
<li><a href="https://github.com/lodash/lodash/commit/c84fe82760fb2d3e03a63379b297a1cc1a2fce12"><code>c84fe82</code></a> fix(zipObjectDeep): prototype pollution (<a href="https://github-redirect.dependabot.com/lodash/lodash/issues/4759">#4759</a>)</li>
<li><a href="https://github.com/lodash/lodash/commit/e7b28ea6cb17b4ca021e7c9d66218c8c89782f32"><code>e7b28ea</code></a> Sanitize sourceURL so it cannot affect evaled code (<a href="https://github-redirect.dependabot.com/lodash/lodash/issues/4518">#4518</a>)</li>
<li><a href="https://github.com/lodash/lodash/commit/0cec225778d4ac26c2bac95031ecc92a94f08bbb"><code>0cec225</code></a> Fix lodash.isEqual for circular references (<a href="https://github-redirect.dependabot.com/lodash/lodash/issues/4320">#4320</a>) (<a href="https://github-redirect.dependabot.com/lodash/lodash/issues/4515">#4515</a>)</li>
<li><a href="https://github.com/lodash/lodash/commit/94c3a8133cb4fcdb50db72b4fd14dd884b195cd5"><code>94c3a81</code></a> Document matches* shorthands for over* methods (<a href="https://github-redirect.dependabot.com/lodash/lodash/issues/4510">#4510</a>) (<a href="https://github-redirect.dependabot.com/lodash/lodash/issues/4514">#4514</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/lodash/lodash/compare/4.17.15...4.17.19">compare view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a href="https://www.npmjs.com/~mathias">mathias</a>, a new releaser for lodash since your current version.</p>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=lodash&package-manager=npm_and_yarn&previous-version=4.17.15&new-version=4.17.19)](https://help.github.com/articles/configuring-automated-security-fixes)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/Redocly/openapi-cli/network/alerts).

</details>


Patch:
diff --git a/package-lock.json b/package-lock.json
index 2de92df9..053dc09f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@redocly/openapi-cli",
-  "version": "1.0.0-beta.4",
+  "version": "1.0.0-beta.5",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -3120,9 +3120,9 @@
       }
     },
     "lodash": {
-      "version": "4.17.15",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+      "version": "4.17.19",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
+      "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
     },
     "lodash.isequal": {
       "version": "4.5.0",
diff --git a/package.json b/package.json
index 0df78ba7..7862aede 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@redocly/openapi-cli",
-  "version": "1.0.0-beta.4",
+  "version": "1.0.0-beta.5",
   "description": "",
   "main": "lib/index.js",
   "scripts": {
diff --git a/src/cli/preview-docs.ts b/src/cli/preview-docs.ts
index cea7ddb3..740be633 100644
--- a/src/cli/preview-docs.ts
+++ b/src/cli/preview-docs.ts
@@ -8,7 +8,6 @@ import startPreviewServer from './preview-server/preview-server';
 import { RedoclyClient } from '../redocly';
 import { ResolveError, YamlParseError } from '../resolve';
 
-
 export async function previewDocs(argv: {
   port: number;
   'use-community-edition'?: boolean;
@@ -23,7 +22,10 @@ export async function previewDocs(argv: {
   let redocOptions: any = {};
   let config = await reloadConfig();
 
-  const entrypoint = getFallbackEntryPointsOrExit(argv.entrypoint ? [argv.entrypoint] : [], config)[0];
+  const entrypoint = getFallbackEntryPointsOrExit(
+    argv.entrypoint ? [argv.entrypoint] : [],
+    config,
+  )[0];
 
   let cachedBundle: any;
   const deps = new Set<string>();
@@ -70,9 +72,13 @@ export async function previewDocs(argv: {
     cachedBundle = updateBundle();
   }); // initial cache
 
-  const isAuthorized = (isAuthorizedWithRedocly || redocOptions.licenseKey)
+  const isAuthorized = isAuthorizedWithRedocly || redocOptions.licenseKey;
   if (!isAuthorized) {
-    process.stderr.write(`Using Redoc community edition.\nLogin with openapi-cli ${colorette.blue('login')} or use an enterprise license key to preview with the premium docs.\n\n`)
+    process.stderr.write(
+      `Using Redoc community edition.\nLogin with openapi-cli ${colorette.blue(
+        'login',
+      )} or use an enterprise license key to preview with the premium docs.\n\n`,
+    );
   }
 
   const hotClients = await startPreviewServer(argv.port, {
@@ -81,7 +87,7 @@ export async function previewDocs(argv: {
     useRedocPro: isAuthorized && !redocOptions.useCommunityEdition,
   });
 
-  const watchPaths = [entrypoint, config.configFile!].filter(e => !!e);
+  const watchPaths = [entrypoint, config.configFile!].filter((e) => !!e);
   const watcher = chockidar.watch(watchPaths, {
     disableGlobbing: true,
     ignoreInitial: true,
diff --git a/src/cli/preview-server/preview-server.ts b/src/cli/preview-server/preview-server.ts
index 269e7939..f37ccf6c 100644
--- a/src/cli/preview-server/preview-server.ts
+++ b/src/cli/preview-server/preview-server.ts
@@ -80,7 +80,10 @@ export default async function startPreviewServer(
         respondWithGzip(
           JSON.stringify({
             openapi: '3.0.0',
-            info: { description: '<code> Failed to generate bundle: check out console output for more details </code>' },
+            info: {
+              description:
+                '<code> Failed to generate bundle: check out console output for more details </code>',
+            },
             paths: {},
           }),
           request,
diff --git a/src/config/builtIn.ts b/src/config/builtIn.ts
index a3965624..01fdb5f6 100644
--- a/src/config/builtIn.ts
+++ b/src/config/builtIn.ts
@@ -1,6 +1,6 @@
 import recommended from './recommended';
 import all from './all';
-import minimal from './minimal'
+import minimal from './minimal';
 import { LintRawConfig } from './config';
 
 export const builtInConfigs: Record<string, LintRawConfig> = {
diff --git a/src/config/config.ts b/src/config/config.ts
index d95d0b85..7953d8ae 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -409,7 +409,7 @@ export async function loadConfig(configPath?: string, customExtends?: string[]):
     }
   }
 
-  if (rawConfig?.lint?.extends == undefined && customExtends !== undefined) {
+  if (customExtends !== undefined) {
     rawConfig.lint = rawConfig.lint || {};
     rawConfig.lint.extends = customExtends;
   }
@@ -569,7 +569,11 @@ function mergeExtends(rulesConfList: LintRawConfig[]) {
   for (let rulesConf of rulesConfList) {
     if (rulesConf.extends) {
       throw new Error(
-        `\`extends\` is not supported in shared configs yet: ${JSON.stringify(rulesConf, null, 2)}.`,
+        `\`extends\` is not supported in shared configs yet: ${JSON.stringify(
+          rulesConf,
+          null,
+          2,
+        )}.`,
       );
     }
     Object.assign(result.rules, rulesConf.rules);
diff --git a/src/format/format.ts b/src/format/format.ts
index 330d6475..b1a9ab51 100644
--- a/src/format/format.ts
+++ b/src/format/format.ts
@@ -10,12 +10,7 @@ import {
   red,
 } from 'colorette';
 
-import {
-  NormalizedProblem,
-  ProblemSeverity,
-  LineColLocationObject,
-  LocationObject,
-} from '../walk';
+import { NormalizedProblem, ProblemSeverity, LineColLocationObject, LocationObject } from '../walk';
 import { getCodeframe, getLineColLocation } from './codeframes';
 
 const BG_COLORS = {
@@ -117,11 +112,7 @@ export function formatProblems(
     );
   }
 
-  function formatStylish(
-    problem: OnlyLineColProblem,
-    locationPad: number,
-    ruleIdPad: number,
-  ) {
+  function formatStylish(problem: OnlyLineColProblem, locationPad: number, ruleIdPad: number) {
     const color = COLORS[problem.severity];
     const severityName = color(SEVERITY_NAMES[problem.severity].toLowerCase().padEnd(7));
     const { start } = problem.location[0];
diff --git a/src/ref-utils.ts b/src/ref-utils.ts
index e3904123..5fba7c98 100644
--- a/src/ref-utils.ts
+++ b/src/ref-utils.ts
@@ -33,7 +33,7 @@ export class Location {
 }
 
 export function unescapePointer(fragment: string): string {
-  return fragment.replace(/~1/g, '/').replace(/~0/g, '~');
+  return decodeURIComponent(fragment.replace(/~1/g, '/').replace(/~0/g, '~'));
 }
 
 export function escapePointer<T extends string | number>(fragment: T): T {
@@ -45,7 +45,7 @@ export function parseRef(ref: string): { uri: string | null; pointer: string[] }
   const [uri, pointer] = ref.split('#/');
   return {
     uri: uri || null,
-    pointer: pointer ? pointer.split('/').map(escapePointer).filter(Boolean) : [],
+    pointer: pointer ? pointer.split('/').map(unescapePointer).filter(Boolean) : [],
   };
 }
 
diff --git a/src/rules/common/no-ambiguous-paths.ts b/src/rules/common/no-ambiguous-paths.ts
index b4273bba..3bcdf827 100644
--- a/src/rules/common/no-ambiguous-paths.ts
+++ b/src/rules/common/no-ambiguous-paths.ts
@@ -9,7 +9,9 @@ export const NoAmbiguousPaths: Oas3Rule | Oas2Rule = () => {
       const seenPaths = [];
 
       for (const currentPath of Object.keys(pathMap)) {
-        const ambiguousPath = seenPaths.find((seenPath) => arePathsAmbiguous(seenPath, currentPath));
+        const ambiguousPath = seenPaths.find((seenPath) =>
+          arePathsAmbiguous(seenPath, currentPath),
+        );
         if (ambiguousPath) {
           report({
             message: `Paths should resolve unambiguously. Found two ambiguous paths: \`${ambiguousPath}\` and \`${currentPath}\`.`,
@@ -45,4 +47,4 @@ function arePathsAmbiguous(a: string, b: string) {
   }
 
   return ambiguous && aVars === bVars;
-}
\ No newline at end of file
+}
diff --git a/src/rules/common/path-declaration-must-exist.ts b/src/rules/common/path-declaration-must-exist.ts
index d456bd3c..c311fcb7 100644
--- a/src/rules/common/path-declaration-must-exist.ts
+++ b/src/rules/common/path-declaration-must-exist.ts
@@ -6,7 +6,7 @@ export const PathDeclarationMustExist: Oas3Rule | Oas2Rule = () => {
     PathItem(_path: object, { report, key }: UserContext) {
       if ((key as string).indexOf('{}') !== -1) {
         report({
-          message: 'Path parameter declarations must be non-empty. \`{}\` is invalid.',
+          message: 'Path parameter declarations must be non-empty. `{}` is invalid.',
           location: { reportOnKey: true },
         });
       }
diff --git a/src/rules/common/path-not-include-query.ts b/src/rules/common/path-not-include-query.ts
index 463b940e..b8672733 100644
--- a/src/rules/common/path-not-include-query.ts
+++ b/src/rules/common/path-not-include-query.ts
@@ -3,13 +3,15 @@ import { UserContext } from '../../walk';
 
 export const PathNotIncludeQuery: Oas3Rule | Oas2Rule = () => {
   return {
-    PathItem(_operation: object, { report, key }: UserContext) {
-      if (key.toString().includes('?')) {
-        report({
-          message: `Don't put query string items in the path, they belong in parameters with \`in: query\`.`,
-          location: { reportOnKey: true },
-        });
-      }
-    },
+    PathMap: {
+      PathItem(_operation: object, { report, key }: UserContext) {
+        if (key.toString().includes('?')) {
+          report({
+            message: `Don't put query string items in the path, they belong in parameters with \`in: query\`.`,
+            location: { reportOnKey: true },
+          });
+        }
+      },
+    }
   };
 };
diff --git a/src/rules/common/tags-alphabetical.ts b/src/rules/common/tags-alphabetical.ts
index 534ad2a7..0f6fbdee 100644
--- a/src/rules/common/tags-alphabetical.ts
+++ b/src/rules/common/tags-alphabetical.ts
@@ -10,7 +10,7 @@ export const TagsAlphabetical: Oas3Rule | Oas2Rule = () => {
       for (let i = 0; i < root.tags.length - 1; i++) {
         if (root.tags[i].name > root.tags[i + 1].name) {
           report({
-            message: 'The \`tags\` array should be in alphabetical order.',
+            message: 'The `tags` array should be in alphabetical order.',
             location: location.child(['tags', i]),
           });
         }
diff --git a/src/types/oas3.ts b/src/types/oas3.ts
index 7979230e..20f4f341 100644
--- a/src/types/oas3.ts
+++ b/src/types/oas3.ts
@@ -182,6 +182,11 @@ const Parameter: NodeType = {
   required: ['name', 'in'],
 };
 
+const Callback = {
+  properties: {},
+  additionalProperties: 'PathItem'
+}
+
 const Operation: NodeType = {
   properties: {
     tags: {
@@ -207,7 +212,7 @@ const Operation: NodeType = {
     deprecated: {
       type: 'boolean',
     },
-    callbacks: 'PathMap',
+    callbacks: mapOf('Callback'),
     'x-codeSamples': listOf('XCodeSample'),
     'x-code-samples': listOf('XCodeSample'), // deprecated
   },
@@ -536,6 +541,7 @@ export const Oas3Types: Record<string, NodeType> = {
   PathItem,
   Parameter,
   Operation,
+  Callback,
   RequestBody,
   MediaTypeMap,
   MediaType,
diff --git a/src/utils.ts b/src/utils.ts
index af62dfe4..452ba380 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -30,7 +30,7 @@ export function dumpBundle(obj: any, format: BundleOutputFormat, dereference?: b
   if (format === 'json') {
     try {
       return JSON.stringify(obj, null, 2);
-    } catch(e) {
+    } catch (e) {
       if (e.message.indexOf('circular') > -1) {
         throw new CircularJSONNotSupportedError(e);
       }
diff --git a/src/validate.ts b/src/validate.ts
index 49ba56a7..874a6c90 100644
--- a/src/validate.ts
+++ b/src/validate.ts
@@ -101,6 +101,10 @@ export function detectOpenAPI(root: any): OasVersion {
     throw new Error(`Document must be JSON object, got ${typeof root}`);
   }
 
+  if (!(root.openapi || root.swagger)) {
+    throw new Error('This doesn’t look like an OpenAPI document.\n');
+  }
+
   if (root.openapi && root.openapi.startsWith('3.0')) {
     return OasVersion.Version3_0;
   }
diff --git a/src/visitors.ts b/src/visitors.ts
index d8c051cd..2de53717 100644
--- a/src/visitors.ts
+++ b/src/visitors.ts
@@ -133,6 +133,7 @@ type Oas3FlatVisitor = {
   License?: VisitFunctionOrObject<Oas3License>;
   PathMap?: VisitFunctionOrObject<Record<string, Oas3PathItem>>;
   PathItem?: VisitFunctionOrObject<Oas3PathItem>;
+  Callback?: VisitFunctionOrObject<Record<string, Oas3PathItem>>;
   Parameter?: VisitFunctionOrObject<Oas3Parameter>;
   Operation?: VisitFunctionOrObject<Oas3Operation>;
   RequestBody?: VisitFunctionOrObject<Oas3RequestBody>;
diff --git a/src/walk.ts b/src/walk.ts
index af7cef12..9da6690b 100644
--- a/src/walk.ts
+++ b/src/walk.ts
@@ -359,11 +359,7 @@ export function walkDocument<T>(opts: {
       return { location: newLocation, node, error };
     }
 
-    function reportFn(
-      ruleId: string,
-      severity: ProblemSeverity,
-      opts: Problem,
-    ) {
+    function reportFn(ruleId: string, severity: ProblemSeverity, opts: Problem) {
       const loc = opts.location
         ? Array.isArray(opts.location)
           ? opts.location


Test patch:
diff --git a/src/__tests__/ref-utils.test.ts b/src/__tests__/ref-utils.test.ts
new file mode 100644
index 00000000..5874f0d4
--- /dev/null
+++ b/src/__tests__/ref-utils.test.ts
@@ -0,0 +1,99 @@
+import outdent from 'outdent';
+
+import { parseYamlToDocument } from './utils';
+import { parseRef } from '../ref-utils';
+import { validateDocument } from '../validate';
+import { LintConfig } from '../config/config';
+import { BaseResolver } from '../resolve';
+
+describe('ref-utils', () => {
+  it(`should unescape refs with '/'`, () => {
+    const reference = 'somefile.yaml#/components/schemas/scope~1domain-schema';
+    expect(parseRef(reference)).toMatchInlineSnapshot(`
+      Object {
+        "pointer": Array [
+          "components",
+          "schemas",
+          "scope/domain-schema",
+        ],
+        "uri": "somefile.yaml",
+      }
+    `);
+  });
+
+  it(`should unescape refs with '~'`, () => {
+    const reference = 'somefile.yaml#/components/schemas/complex~0name';
+    expect(parseRef(reference)).toMatchInlineSnapshot(`
+      Object {
+        "pointer": Array [
+          "components",
+          "schemas",
+          "complex~name",
+        ],
+        "uri": "somefile.yaml",
+      }
+    `);
+  });
+
+  it(`should unescape complex urlencoded paths`, () => {
+    const referene = 'somefile.yaml#/components/schemas/scope%2Fcomplex~name';
+    expect(parseRef(referene)).toMatchInlineSnapshot(`
+      Object {
+        "pointer": Array [
+          "components",
+          "schemas",
+          "scope/complex~name",
+        ],
+        "uri": "somefile.yaml",
+      }
+    `);
+  });
+
+  it(`should validate definition with urlencoded paths`, async () => {
+    const document = parseYamlToDocument(
+      outdent` 
+        openapi: "3.0.0"
+        info:
+          version: 1.0.0
+          title: Swagger Petstore
+          description: Test definition
+          license:
+            name: MIT
+            url: https://opensource.org/licenses/MIT
+        servers:
+          - url: http://petstore.swagger.io/v1
+        paths:
+          /pet:
+            get:
+              summary: List all pets
+              operationId: listPets
+              responses:
+                '200':
+                  description: A paged array of pets
+                  content:
+                    application/json:
+                      schema:
+                        $ref: "#/components/schemas/scope%3A%7Banimals~1Pet%7D"
+        components:
+          schemas:
+            scope:{animals/Pet}:
+              type: object
+              properties:
+                id:
+                  type: integer
+                  format: int64
+                name:
+                  type: string
+      `,
+      '',
+    );
+
+    const result = await validateDocument({
+      document,
+      externalRefResolver: new BaseResolver(),
+      config: new LintConfig({}),
+    });
+
+    expect(result).toMatchInlineSnapshot(`Array []`);
+  });
+});
diff --git a/src/rules/oas3/__tests__/spec/callbacks.test.ts b/src/rules/oas3/__tests__/spec/callbacks.test.ts
new file mode 100644
index 00000000..65fdea67
--- /dev/null
+++ b/src/rules/oas3/__tests__/spec/callbacks.test.ts
@@ -0,0 +1,41 @@
+import { outdent } from 'outdent';
+import { validateDoc } from './utils';
+
+it('should not fail on valid callbacks object', async () => {
+  const source = outdent`
+    openapi: 3.0.2
+    info:
+      title: Test
+      version: '1.0'
+
+    servers:
+      - url: http://google.com
+
+    paths:
+      '/ping':
+        summary: test
+        get:
+          callbacks:
+            test:
+              'http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}':
+                post:
+                  requestBody:
+                    description: Callback payload
+                    content:
+                      'application/json':
+                        schema:
+                          $ref: '#/components/schemas/SomePayload'
+                  responses:
+                    '200':
+                      description: webhook successfully processed and no retries will be performed
+          responses:
+            '200':
+              description: example description
+  `;
+
+  expect(
+    await validateDoc(source, {
+      spec: 'error',
+    }),
+  ).toMatchInlineSnapshot(`Array []`);
+});