Repository: Redocly/openapi-cli
Pull request #181 (2020-08-15 19:59:36)

feat: use all parts of pointer to generate uniq component name if needed
Currently only the last part of the pointer (e.g. #/foo/bar -> bar) is
used for naming the component. If it's already taken, the base name of
the file reference is prepended. If it's still not unique, a sequence
number is appended to the name and warning is printed (`Two schemas are
referenced with the same name but different content.`).

The warning message is quite misleading and unnecessary because it's not
the user's fault that we used only the last part of the pointer to
generate unique component name.

To show some real-world example, let's consider file `examples.yml` with
examples for multiple types where each type may have multiple variants:

- #/Car/basic
- #/Car/electric
- #/Bike/basic
- #/Scooter/basic

The function `getComponentName()` will generate: basic, electric,
examples_basic, examples_basic-2, and warn about conflicting name of the
last. That's quite irritating, especially since the user cannot hide
these messages.

This commit solves this problem by changing the name generation
algorithm in `getComponentName()` to be gradually adding all the
components of the pointer from left to right before prepending the base
of the file reference and appending a serial.

Now for the example above the function will generate: basic, electric,
bike-basic, scooter-basic.


Patch:
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/format/format.ts b/src/format/format.ts
index e615cc44..2a7ba32b 100644
--- a/src/format/format.ts
+++ b/src/format/format.ts
@@ -108,15 +108,15 @@ export function formatProblems(
           location: p.location.map(location => ({
             ...location,
             source: {
-              absoluteRef: location.source.absoluteRef,
+              ref: path.relative(cwd, location.source.absoluteRef),
             },
-          }) as LocationObject),
-          from: {
+          })),
+          from: p.from ? {
             ...p.from,
             source: {
-              absoluteRef: p.from?.source.absoluteRef,
+              ref: path.relative(cwd, p.from?.source.absoluteRef || cwd),
             }
-          }
+          } : undefined,
         };
 
         if (process.env.FORMAT_JSON_WITH_CODEFRAMES) {
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