From 3783dffc4bb23059aa20ac8d74dc3238484422e7 Mon Sep 17 00:00:00 2001
From: v8tenko <v8tenko@yandex-team.ru>
Date: Wed, 29 Nov 2023 16:56:43 +0300
Subject: [PATCH] feat: new featueres

---
 src/__snapshots__/examples/array.test.ts.snap | 231 ++++++++++++++++++
 .../base.test.ts.snap}                        |   0
 src/__tests__/examples/array.test.ts          | 100 ++++++++
 .../base.test.ts}                             |   2 +-
 src/includer/index.ts                         |  21 +-
 src/includer/models.ts                        |   1 +
 src/includer/traverse/tables.ts               |  45 +++-
 src/includer/ui/main.ts                       |   6 +-
 8 files changed, 389 insertions(+), 17 deletions(-)
 create mode 100644 src/__snapshots__/examples/array.test.ts.snap
 rename src/__snapshots__/{example.test.ts.snap => examples/base.test.ts.snap} (100%)
 create mode 100644 src/__tests__/examples/array.test.ts
 rename src/__tests__/{example.test.ts => examples/base.test.ts} (97%)

diff --git a/src/__snapshots__/examples/array.test.ts.snap b/src/__snapshots__/examples/array.test.ts.snap
new file mode 100644
index 0000000..49684a4
--- /dev/null
+++ b/src/__snapshots__/examples/array.test.ts.snap
@@ -0,0 +1,231 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`openapi project with examples renders example field 1`] = `
+"<div class="openapi">
+
+# example.array
+
+## Request
+
+<div class="openapi__request__wrapper">
+
+<div class="openapi__request" style="--method: var(--dc-openapi-methods-post)">
+
+POST {.openapi__method}
+
+
+\`\`\`
+http://localhost:8080/test
+\`\`\`
+
+
+</div>
+
+</div>
+
+Generated server url{.openapi__request__description}
+
+#### Body
+
+{% cut "application/json" %}
+
+
+\`\`\`json
+[
+    {
+        "test": 1
+    },
+    {
+        "test": 2
+    }
+]
+\`\`\`
+
+
+{% endcut %}
+
+
+any[]
+
+## Responses
+
+<div class="openapi__response__code__200">
+
+## 200 OK
+
+Base 200 response
+
+#### Body
+
+{% cut "application/json" %}
+
+
+\`\`\`json
+{}
+\`\`\`
+
+
+{% endcut %}
+
+
+</div>
+<!-- markdownlint-disable-file -->
+
+</div>"
+`;
+
+exports[`openapi project with examples renders infered example 1`] = `
+"<div class="openapi">
+
+# example.array
+
+## Request
+
+<div class="openapi__request__wrapper">
+
+<div class="openapi__request" style="--method: var(--dc-openapi-methods-post)">
+
+POST {.openapi__method}
+
+
+\`\`\`
+http://localhost:8080/test
+\`\`\`
+
+
+</div>
+
+</div>
+
+Generated server url{.openapi__request__description}
+
+#### Body
+
+{% cut "application/json" %}
+
+
+\`\`\`json
+[
+    {
+        "name": "string"
+    }
+]
+\`\`\`
+
+
+{% endcut %}
+
+
+[Cat](#cat)[]
+
+### Cat
+
+#||| **Name** | **Type** | **Description** ||
+
+|| name | string |  |||#
+
+## Responses
+
+<div class="openapi__response__code__200">
+
+## 200 OK
+
+Base 200 response
+
+#### Body
+
+{% cut "application/json" %}
+
+
+\`\`\`json
+{}
+\`\`\`
+
+
+{% endcut %}
+
+
+</div>
+<!-- markdownlint-disable-file -->
+
+</div>"
+`;
+
+exports[`openapi project with examples renders nested arrays exmaple 1`] = `
+"<div class="openapi">
+
+# example.array
+
+## Request
+
+<div class="openapi__request__wrapper">
+
+<div class="openapi__request" style="--method: var(--dc-openapi-methods-post)">
+
+POST {.openapi__method}
+
+
+\`\`\`
+http://localhost:8080/test
+\`\`\`
+
+
+</div>
+
+</div>
+
+Generated server url{.openapi__request__description}
+
+#### Body
+
+{% cut "application/json" %}
+
+
+\`\`\`json
+[
+    [
+        {
+            "name": "string"
+        }
+    ]
+]
+\`\`\`
+
+
+{% endcut %}
+
+
+[Cat](#cat)[][]
+
+### Cat
+
+#||| **Name** | **Type** | **Description** ||
+
+|| name | string |  |||#
+
+## Responses
+
+<div class="openapi__response__code__200">
+
+## 200 OK
+
+Base 200 response
+
+#### Body
+
+{% cut "application/json" %}
+
+
+\`\`\`json
+{}
+\`\`\`
+
+
+{% endcut %}
+
+
+</div>
+<!-- markdownlint-disable-file -->
+
+</div>"
+`;
diff --git a/src/__snapshots__/example.test.ts.snap b/src/__snapshots__/examples/base.test.ts.snap
similarity index 100%
rename from src/__snapshots__/example.test.ts.snap
rename to src/__snapshots__/examples/base.test.ts.snap
diff --git a/src/__tests__/examples/array.test.ts b/src/__tests__/examples/array.test.ts
new file mode 100644
index 0000000..d480aa8
--- /dev/null
+++ b/src/__tests__/examples/array.test.ts
@@ -0,0 +1,100 @@
+import {DocumentBuilder, run} from '../__helpers__/run';
+
+const name = 'example.array';
+describe('openapi project with examples', () => {
+    it('renders example field', async () => {
+        const spec = new DocumentBuilder(name)
+            .request({
+                schema: {
+                    example: [
+                        {
+                            test: 1,
+                        },
+                        {
+                            test: 2,
+                        },
+                    ],
+                    type: 'array',
+                    items: {},
+                },
+            })
+            .response(200, {
+                description: 'Base 200 response',
+                schema: {
+                    type: 'object',
+                },
+            })
+            .build();
+
+        const fs = await run(spec);
+
+        const page = fs.match(name);
+
+        expect(page).toMatchSnapshot();
+    });
+
+    it('renders infered example', async () => {
+        const spec = new DocumentBuilder(name)
+            .request({
+                schema: {
+                    type: 'array',
+                    items: DocumentBuilder.ref('Cat'),
+                },
+            })
+            .response(200, {
+                description: 'Base 200 response',
+                schema: {
+                    type: 'object',
+                },
+            })
+            .component('Cat', {
+                type: 'object',
+                properties: {
+                    name: {
+                        type: 'string',
+                    },
+                },
+            })
+            .build();
+
+        const fs = await run(spec);
+
+        const page = fs.match(name);
+
+        expect(page).toMatchSnapshot();
+    });
+
+    it('renders nested arrays exmaple', async () => {
+        const spec = new DocumentBuilder(name)
+            .request({
+                schema: {
+                    type: 'array',
+                    items: {
+                        type: 'array',
+                        items: DocumentBuilder.ref('Cat'),
+                    },
+                },
+            })
+            .response(200, {
+                description: 'Base 200 response',
+                schema: {
+                    type: 'object',
+                },
+            })
+            .component('Cat', {
+                type: 'object',
+                properties: {
+                    name: {
+                        type: 'string',
+                    },
+                },
+            })
+            .build();
+
+        const fs = await run(spec);
+
+        const page = fs.match(name);
+
+        expect(page).toMatchSnapshot();
+    });
+});
diff --git a/src/__tests__/example.test.ts b/src/__tests__/examples/base.test.ts
similarity index 97%
rename from src/__tests__/example.test.ts
rename to src/__tests__/examples/base.test.ts
index 034da66..eb1302f 100644
--- a/src/__tests__/example.test.ts
+++ b/src/__tests__/examples/base.test.ts
@@ -1,4 +1,4 @@
-import {DocumentBuilder, run} from './__helpers__/run';
+import {DocumentBuilder, run} from '../__helpers__/run';
 
 const name = 'example';
 describe('openapi project with examples', () => {
diff --git a/src/includer/index.ts b/src/includer/index.ts
index 4e74220..b93c8b1 100644
--- a/src/includer/index.ts
+++ b/src/includer/index.ts
@@ -164,14 +164,20 @@ async function generateToc(params: GenerateTocParams): Promise<void> {
             items: [],
         };
 
-        section.items = endpointsOfTag.map((endpoint) => handleEndpointRender(endpoint, id));
-
         const custom = ArgvService.tag(tag.name);
+        const customId = custom?.alias || id;
+
+        section.items = endpointsOfTag.map((endpoint) => handleEndpointRender(endpoint, customId));
 
         const customLeadingPageName = custom?.name || leadingPageName;
 
         if (!custom?.hidden) {
-            addLeadingPage(section, leadingPageMode, customLeadingPageName, join(id, 'index.md'));
+            addLeadingPage(
+                section,
+                leadingPageMode,
+                customLeadingPageName,
+                join(customId, 'index.md'),
+            );
         }
 
         toc.items.push(section);
@@ -270,12 +276,13 @@ async function generateContent(params: GenerateContentParams): Promise<void> {
     spec.tags.forEach((tag, id) => {
         const {endpoints} = tag;
 
+        const custom = ArgvService.tag(tag.name);
+        const customId = custom?.alias || id;
+
         endpoints.forEach((endpoint) => {
-            results.push(handleEndpointIncluder(endpoint, join(writePath, id), sandbox));
+            results.push(handleEndpointIncluder(endpoint, join(writePath, customId), sandbox));
         });
 
-        const custom = ArgvService.tag(tag.name);
-
         if (custom?.hidden) {
             return;
         }
@@ -285,7 +292,7 @@ async function generateContent(params: GenerateContentParams): Promise<void> {
             : generators.section(tag);
 
         results.push({
-            path: join(writePath, id, 'index.md'),
+            path: join(writePath, customId, 'index.md'),
             content,
         });
     });
diff --git a/src/includer/models.ts b/src/includer/models.ts
index 3bec21c..5723925 100644
--- a/src/includer/models.ts
+++ b/src/includer/models.ts
@@ -281,6 +281,7 @@ export type CustomTag = {
     hidden?: boolean;
     name?: string;
     path?: string;
+    alias?: string;
 };
 
 export type OpenApiIncluderParams = {
diff --git a/src/includer/traverse/tables.ts b/src/includer/traverse/tables.ts
index 61857bb..ee0c101 100644
--- a/src/includer/traverse/tables.ts
+++ b/src/includer/traverse/tables.ts
@@ -37,6 +37,15 @@ export function tableFromSchema(schema: OpenJSONSchema): TableFromSchemaResult {
         return {content, tableRefs: []};
     }
 
+    if (schema.type === 'array') {
+        const {type, ref} = prepareTableRowData(schema);
+
+        return {
+            content: type,
+            tableRefs: ref ? [ref] : [],
+        };
+    }
+
     const {rows, refs} = prepareObjectSchemaTable(schema);
     let content = rows.length ? table([['Name', 'Type', 'Description'], ...rows]) : '';
 
@@ -135,8 +144,8 @@ export function prepareTableRowData(
 
         const inner = prepareTableRowData(value.items, key, parentRef);
         const innerDescription = inner.ref
-            ? description
-            : concatNewLine(description, inner.description);
+            ? concatNewLine(description, inner.description)
+            : description;
 
         if (RefsService.isRuntimeAllowed() && inner.runtimeRef) {
             RefsService.runtime(inner.runtimeRef, value.items);
@@ -178,20 +187,20 @@ export function prepareTableRowData(
 function prepareComplexDescription(baseDescription: string, value: OpenJSONSchema): string {
     let description = baseDescription;
     const enumValues = value.enum?.map((s) => `\`${s}\``).join(', ');
-    if (enumValues) {
+    if (typeof enumValues !== 'undefined') {
         description = concatNewLine(
             description,
             `<span style="color:gray;">Enum</span>: ${enumValues}`,
         );
     }
-    if (value.default) {
+    if (typeof value.default !== 'undefined') {
         description = concatNewLine(
             description,
             `<span style="color:gray;">Default</span>: \`${value.default}\``,
         );
     }
 
-    if (value.example) {
+    if (typeof value.example !== 'undefined') {
         description = concatNewLine(
             description,
             `<span style="color:gray;">Example</span>: \`${value.example}\``,
@@ -247,13 +256,29 @@ function findNonNullOneOfElement(schema: OpenJSONSchema): OpenJSONSchema {
     throw new Error(`Unable to create sample element: \n ${stringify(schema, null, 2)}`);
 }
 
-export function prepareSampleObject(schema: OpenJSONSchema, callstack: OpenJSONSchema[] = []) {
+export function prepareSampleObject(
+    schema: OpenJSONSchema,
+    callstack: OpenJSONSchema[] = [],
+): Object | Array<Object> {
     const result: {[key: string]: unknown} = {};
 
     if (schema.example) {
         return schema.example;
     }
 
+    if (schema.type === 'array') {
+        if (Array.isArray(schema.items) || typeof schema.items !== 'object') {
+            throw new Error(
+                `Unable to create sample element for ${stringify(
+                    schema,
+                    null,
+                    4,
+                )}.\n You can pass only one scheme to items`,
+            );
+        }
+        return [prepareSampleObject(schema.items)];
+    }
+
     const merged = findNonNullOneOfElement(RefsService.merge(schema));
 
     Object.entries(merged.properties || {}).forEach(([key, value]) => {
@@ -302,7 +327,13 @@ function prepareSampleElement(
             return prepareSampleObject(schema, downCallstack);
         case 'array':
             if (!schema.items || schema.items === true || Array.isArray(schema.items)) {
-                throw Error(`Unsupported array items for ${key}`);
+                throw new Error(
+                    `Unable to create sample element for ${stringify(
+                        schema,
+                        null,
+                        4,
+                    )}.\n You can pass only one scheme to items`,
+                );
             }
             return [
                 prepareSampleElement(key, schema.items, isRequired(key, schema), downCallstack),
diff --git a/src/includer/ui/main.ts b/src/includer/ui/main.ts
index 9737a21..f4f7e17 100644
--- a/src/includer/ui/main.ts
+++ b/src/includer/ui/main.ts
@@ -2,7 +2,7 @@ import ArgvService from '../services/argv';
 
 import stringify from 'json-stringify-safe';
 
-import {sep} from 'path';
+import {join} from 'path';
 
 import {block, body, code, cut, link, list, mono, page, title} from '.';
 
@@ -79,7 +79,9 @@ function sections({tags, endpoints}: Specification) {
                 return undefined;
             }
 
-            return link(name, id + sep + 'index.md');
+            const customId = custom?.alias || id;
+
+            return link(name, join(customId, 'index.md'));
         })
         .filter(Boolean) as string[];