Repository: Redocly/openapi-cli
Pull request #182 (2020-08-15 20:22:32)

fix: remove extra blank line printed by printLintTotals()
`ignored` already ends with a newline, so when no rules are ignored, two
empty lines are printed after the green validation result message.
It's quite irritating when you run multiple linters in sequence...


Patch:
diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index 0087dca8..53f37382 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -1,8 +1,9 @@
 name: CD to NPM & AWS S3
 
 on:
-  tag:
-    - v[0-9]*.[0-9]*.[0-9]*
+  push:
+    tags: 
+      - v[0-9]*.[0-9]*.[0-9]*
     
 jobs:
   test:
diff --git a/src/bundle.ts b/src/bundle.ts
index b60a7ed9..c09f1683 100644
--- a/src/bundle.ts
+++ b/src/bundle.ts
@@ -8,7 +8,7 @@ import { Oas2Types } from './types/oas2';
 import { NormalizedNodeType, normalizeTypes, NodeType } from './types';
 import { WalkContext, walkDocument, UserContext } from './walk';
 import { detectOpenAPI, openAPIMajor, OasMajorVersion } from './validate';
-import { Location, pointerBaseName, refBaseName } from './ref-utils';
+import { Location, refBaseName } from './ref-utils';
 import { Config, LintConfig } from './config/config';
 import { initRules } from './config/rules';
 import { reportUnresolvedRef } from './rules/no-unresolved-refs';
@@ -232,19 +232,21 @@ function makeBundleVisitor(version: OasMajorVersion, dereference: boolean, rootD
     ctx: UserContext,
   ) {
     const [fileRef, pointer] = [target.location.source.absoluteRef, target.location.pointer];
+    const componentsGroup = components[componentType];
 
-    const pointerBase = pointerBaseName(pointer);
-    const refBase = refBaseName(fileRef);
+    let name = '';
 
-    let name = pointerBase || refBase;
+    const refParts = pointer.slice(2).split('/').filter(Boolean);  // slice(2) removes "#/"
+    while (refParts.length > 0) {
+      name = refParts.pop() + (name ? `-${name}` : '');
+      if (!componentsGroup || !componentsGroup[name] || isEqual(componentsGroup[name], target.node)) {
+        return name;
+      }
+    }
 
-    const componentsGroup = components[componentType];
-    if (!componentsGroup || !componentsGroup[name] || isEqual(componentsGroup[name], target.node))
+    name = refBaseName(fileRef) + (name ? `_${name}` : '')
+    if (!componentsGroup[name] || isEqual(componentsGroup[name], target.node)) {
       return name;
-
-    if (pointerBase) {
-      name = `${refBase}_${pointerBase}`;
-      if (!componentsGroup[name] || isEqual(componentsGroup[name], target.node)) return name;
     }
 
     const prevName = name;
diff --git a/src/cli.ts b/src/cli.ts
index dc00206e..e0288856 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -120,6 +120,7 @@ yargs
             formatProblems(results, {
               format: argv.format,
               maxProblems: argv['max-problems'],
+              totals: fileTotals,
             });
           }
 
@@ -250,6 +251,7 @@ yargs
           formatProblems(problems, {
             format: argv.format,
             maxProblems: argv['max-problems'],
+            totals: fileTotals,
           });
 
           const elapsed =
@@ -401,7 +403,7 @@ function handleError(e: Error, ref: string) {
 
 function printLintTotals(totals: Totals, definitionsCount: number) {
   const ignored = totals.ignored
-    ? yellow(`${totals.ignored} ${pluralize('problem is', totals.ignored)} explicitly ignored.\n`)
+    ? yellow(`${totals.ignored} ${pluralize('problem is', totals.ignored)} explicitly ignored.\n\n`)
     : '';
 
   if (totals.errors > 0) {
@@ -411,7 +413,7 @@ function printLintTotals(totals: Totals, definitionsCount: number) {
           totals.warnings > 0
             ? ` and ${totals.warnings} ${pluralize('warning', totals.warnings)}`
             : ''
-        }.\n${ignored}\n`,
+        }.\n${ignored}`,
       ),
     );
   } else if (totals.warnings > 0) {
@@ -419,7 +421,7 @@ function printLintTotals(totals: Totals, definitionsCount: number) {
       green(`Woohoo! Your OpenAPI ${pluralize('definition is', definitionsCount)} valid. 🎉\n`),
     );
     process.stderr.write(
-      yellow(`You have ${totals.warnings} ${pluralize('warning', totals.warnings)}.\n${ignored}\n`),
+      yellow(`You have ${totals.warnings} ${pluralize('warning', totals.warnings)}.\n${ignored}`),
     );
   } else {
     process.stderr.write(
@@ -427,7 +429,7 @@ function printLintTotals(totals: Totals, definitionsCount: number) {
         `Woohoo! Your OpenAPI ${pluralize(
           'definition is',
           definitionsCount,
-        )} valid. 🎉\n${ignored}\n`,
+        )} valid. 🎉\n${ignored}`,
       ),
     );
   }
diff --git a/src/format/format.ts b/src/format/format.ts
index e615cc44..2e28a648 100644
--- a/src/format/format.ts
+++ b/src/format/format.ts
@@ -12,6 +12,7 @@ import {
 
 import { NormalizedProblem, ProblemSeverity, LineColLocationObject, LocationObject } from '../walk';
 import { getCodeframe, getLineColLocation } from './codeframes';
+import { Totals } from '../cli';
 
 const BG_COLORS = {
   warn: (str: string) => bgYellow(black(str)),
@@ -43,6 +44,7 @@ export function formatProblems(
     cwd?: string;
     format?: OutputFormat;
     color?: boolean;
+    totals: Totals;
   },
 ) {
   const {
@@ -50,6 +52,7 @@ export function formatProblems(
     cwd = process.cwd(),
     format = 'codeframe',
     color = colorOptions.enabled,
+    totals,
   } = opts;
 
   colorOptions.enabled = color; // force colors if specified
@@ -64,7 +67,7 @@ export function formatProblems(
 
   if (!totalProblems && format !== 'json') return;
 
-  switch(format) {
+  switch (format) {
     case 'json':
       outputJSON();
       break;
@@ -98,35 +101,37 @@ export function formatProblems(
       )}\n`,
     );
   }
-  
+
   function outputJSON() {
     const resultObject = {
-      total: problems.length,
-      problems: problems.map(p => {
+      totals,
+      problems: problems.map((p) => {
         let problem = {
           ...p,
-          location: p.location.map(location => ({
+          location: p.location.map((location) => ({
             ...location,
             source: {
-              absoluteRef: location.source.absoluteRef,
+              ref: path.relative(cwd, location.source.absoluteRef),
             },
-          }) as LocationObject),
-          from: {
-            ...p.from,
-            source: {
-              absoluteRef: p.from?.source.absoluteRef,
-            }
-          }
+          })),
+          from: p.from
+            ? {
+                ...p.from,
+                source: {
+                  ref: path.relative(cwd, p.from?.source.absoluteRef || cwd),
+                },
+              }
+            : undefined,
         };
 
         if (process.env.FORMAT_JSON_WITH_CODEFRAMES) {
           const location = p.location[0]; // TODO: support multiple locations
-          const loc = getLineColLocation(location); 
+          const loc = getLineColLocation(location);
           (problem as any).codeframe = getCodeframe(loc, color);
         }
         return problem;
       }),
-    }
+    };
     process.stdout.write(JSON.stringify(resultObject, null, 2));
   }
 
diff --git a/src/index.ts b/src/index.ts
index d342e729..f647ecfc 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,11 @@
-export type { Oas3Rule, OasRule, Oas3Preprocessor, OasPreprocessor } from './visitors';
+export type {
+  Oas3Rule,
+  OasRule,
+  Oas3Preprocessor,
+  OasPreprocessor,
+  OasDecorator,
+  Oas3Decorator,
+} from './visitors';
 export type { NodeType } from './types/index';
 export type {
   Config,
diff --git a/src/types/oas3.ts b/src/types/oas3.ts
index 20f4f341..12f43441 100644
--- a/src/types/oas3.ts
+++ b/src/types/oas3.ts
@@ -13,6 +13,7 @@ const DefinitionRoot: NodeType = {
     externalDocs: 'ExternalDocs',
     paths: 'PathMap',
     components: 'Components',
+    'x-webhooks': 'WebhooksMap',
   },
   required: ['openapi', 'paths', 'info'],
 };
@@ -123,6 +124,11 @@ const PathMap: NodeType = {
     key.startsWith('/') ? 'PathItem' : undefined,
 };
 
+const WebhooksMap: NodeType = {
+  properties: {},
+  additionalProperties: () => 'PathItem',
+};
+
 const PathItem: NodeType = {
   properties: {
     $ref: 'PathItem', // TODO: verify special $ref handling for Path Item
@@ -573,4 +579,5 @@ export const Oas3Types: Record<string, NodeType> = {
   SecuritySchemeFlows,
   SecurityScheme,
   XCodeSample,
+  WebhooksMap,
 };


Test patch:
diff --git a/src/__tests__/__snapshots__/bundle.test.ts.snap b/src/__tests__/__snapshots__/bundle.test.ts.snap
index 19199b82..d7d9c2e1 100644
--- a/src/__tests__/__snapshots__/bundle.test.ts.snap
+++ b/src/__tests__/__snapshots__/bundle.test.ts.snap
@@ -24,12 +24,27 @@ components:
       name: param_a
       schema:
         $ref: '#/components/schemas/schema-a'
+      examples:
+        first:
+          $ref: '#/components/examples/param-a-first'
+        second:
+          $ref: '#/components/examples/second'
     path-param:
       name: path_param
+      examples:
+        first:
+          $ref: '#/components/examples/first'
     param-c:
       name: param_c
     param-b:
       name: param_b
+  examples:
+    first:
+      value: b1
+    param-a-first:
+      value: a1
+    second:
+      value: a2
   schemas:
     schema-a:
       type: string
diff --git a/src/__tests__/fixtures/refs/examples.yaml b/src/__tests__/fixtures/refs/examples.yaml
new file mode 100644
index 00000000..1ade76b9
--- /dev/null
+++ b/src/__tests__/fixtures/refs/examples.yaml
@@ -0,0 +1,8 @@
+param-a:
+  first:
+    value: a1
+  second:
+    value: a2
+path-param:
+  first:
+    value: b1
diff --git a/src/__tests__/fixtures/refs/openapi-with-external-refs.yaml b/src/__tests__/fixtures/refs/openapi-with-external-refs.yaml
index c0ab3f90..80dfb548 100644
--- a/src/__tests__/fixtures/refs/openapi-with-external-refs.yaml
+++ b/src/__tests__/fixtures/refs/openapi-with-external-refs.yaml
@@ -21,5 +21,13 @@ components:
       name: param_a
       schema:
         $ref: ./schema-a.yaml
+      examples:
+        first:
+          $ref: ./examples.yaml#/param-a/first
+        second:
+          $ref: ./examples.yaml#/param-a/second
     path-param:
-      name: path_param
\ No newline at end of file
+      name: path_param
+      examples:
+        first:
+          $ref: ./examples.yaml#/path-param/first
\ No newline at end of file