diff --git a/src/__snapshots__/tags.test.ts.snap b/src/__snapshots__/tags.test.ts.snap
new file mode 100644
index 0000000..4680c79
--- /dev/null
+++ b/src/__snapshots__/tags.test.ts.snap
@@ -0,0 +1,219 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`tags rendering renders basic tag: index.md 1`] = `
+"# basic
+
+basic tag
+
+## Endpoints
+
+- [name](name.md)
+
+"
+`;
+
+exports[`tags rendering renders basic tag: name.md 1`] = `
+"
+
+# name
+
+## Request
+
+
+
+
+
+
+
+POST {.openapi__method}
+\`\`\`
+http://localhost:8080/test
+\`\`\`
+
+
+
+
+
+Generated server url
+
+
+
+
+
+## Responses
+
+
+
+## 200 OK
+
+Base 200 response
+
+
+
+### Body
+
+{% cut "application/json" %}
+
+
+\`\`\`json
+{
+ "id": 0,
+ "title": "string"
+}
+\`\`\`
+
+
+{% endcut %}
+
+
+#|||
+ **Name**
+|
+ **Description**
+||
+
+||
+ id
+|
+ **Type:** number
+
+
+
+||
+
+||
+ title
+|
+ **Type:** string
+
+
+
+|||#
+
+
+
+
+
+
+
"
+`;
+
+exports[`tags rendering renders basic tag: toc 1`] = `
+"name: openapi
+items:
+ - name: Overview
+ href: index.md
+ - name: basic
+ items:
+ - name: Overview
+ href: basic/index.md
+ - href: basic/name.md
+ name: name
+ - href: test-parameters.md
+ name: parameters test
+"
+`;
+
+exports[`tags rendering uses custom name from tag and title from operation: index.md 1`] = `
+"# Title from x-navtitle in operation
+
+## Endpoints
+
+- [name](name.md)
+
+"
+`;
+
+exports[`tags rendering uses custom name from tag and title from operation: toc 1`] = `
+"name: openapi
+items:
+ - name: Overview
+ href: index.md
+ - name: Title from x-navtitle in operation
+ items:
+ - name: Overview
+ href: slug/index.md
+ - href: slug/name.md
+ name: name
+ - href: test-parameters.md
+ name: parameters test
+"
+`;
+
+exports[`tags rendering uses custom name from tag: index.md 1`] = `
+"# TagName
+
+## Endpoints
+
+- [name](name.md)
+
+"
+`;
+
+exports[`tags rendering uses custom name from tag: toc 1`] = `
+"name: openapi
+items:
+ - name: Overview
+ href: index.md
+ - name: TagName
+ items:
+ - name: Overview
+ href: slug/index.md
+ - href: slug/name.md
+ name: name
+ - href: test-parameters.md
+ name: parameters test
+"
+`;
+
+exports[`tags rendering uses custom title from operation: index.md 1`] = `
+"# Title from x-navtitle in operation
+
+## Endpoints
+
+- [name](name.md)
+
+"
+`;
+
+exports[`tags rendering uses custom title from operation: toc 1`] = `
+"name: openapi
+items:
+ - name: Overview
+ href: index.md
+ - name: Title from x-navtitle in operation
+ items:
+ - name: Overview
+ href: TagName/index.md
+ - href: TagName/name.md
+ name: name
+ - href: test-parameters.md
+ name: parameters test
+"
+`;
+
+exports[`tags rendering uses custom title from tag override title from operation: index.md 1`] = `
+"# Title from tag
+
+## Endpoints
+
+- [name](name.md)
+
+"
+`;
+
+exports[`tags rendering uses custom title from tag override title from operation: toc 1`] = `
+"name: openapi
+items:
+ - name: Overview
+ href: index.md
+ - name: Title from tag
+ items:
+ - name: Overview
+ href: TagName/index.md
+ - href: TagName/name.md
+ name: name
+ - href: test-parameters.md
+ name: parameters test
+"
+`;
diff --git a/src/__tests__/__helpers__/run.ts b/src/__tests__/__helpers__/run.ts
index 11ed6ed..9ed2cb3 100644
--- a/src/__tests__/__helpers__/run.ts
+++ b/src/__tests__/__helpers__/run.ts
@@ -4,6 +4,17 @@ import {when} from 'jest-when';
import {dump} from 'js-yaml';
import {virtualFS} from './virtualFS';
import nodeFS from 'fs';
+import { TAG_ID_FIELD, TAG_NAMES_FIELD } from '../../includer/constants';
+
+declare module 'openapi-types' {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace OpenAPIV3 {
+ interface TagObject {
+ [TAG_NAMES_FIELD]?: string;
+ [TAG_ID_FIELD]?: string;
+ }
+ }
+}
const baseDocument = {
openapi: '3.0.2',
@@ -37,6 +48,8 @@ export class DocumentBuilder {
private responses: [code: number, response: OpenAPIV3.ResponseObject][] = [];
private parameters: OpenAPIV3.ParameterObject[] = [];
private components: Record = {};
+ private tags: Array = [];
+ private navTitles?: string[];
private requestBody?: OpenAPIV3.RequestBodyObject = undefined;
constructor(id: string) {
@@ -86,6 +99,18 @@ export class DocumentBuilder {
return this;
}
+ tag(schema: OpenAPIV3.TagObject) {
+ this.tags.push(schema);
+
+ return this;
+ }
+
+ navTitle(title: string) {
+ (this.navTitles ??= []).push(title);
+
+ return this;
+ }
+
build(): string {
if (!this.responses.length) {
throw new Error("Test case error: endpoint can't have no responses");
@@ -99,6 +124,9 @@ export class DocumentBuilder {
requestBody: this.requestBody,
operationId: this.id,
responses: Object.fromEntries(this.responses),
+ tags: this.tags.map((tag) => tag.name),
+ // @ts-expect-error custom extension
+ [TAG_NAMES_FIELD]: this.navTitles,
},
parameters: this.parameters,
},
@@ -106,6 +134,7 @@ export class DocumentBuilder {
components: {
schemas: this.components,
},
+ tags: this.tags,
};
return dump(spec);
diff --git a/src/__tests__/tags.test.ts b/src/__tests__/tags.test.ts
new file mode 100644
index 0000000..bea942f
--- /dev/null
+++ b/src/__tests__/tags.test.ts
@@ -0,0 +1,96 @@
+import {DocumentBuilder, run} from './__helpers__/run';
+
+describe('tags rendering', () => {
+ const response = {
+ description: 'Base 200 response',
+ schema: {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'number',
+ },
+ title: {
+ type: 'string',
+ },
+ },
+ },
+ } as const;
+
+ it('renders basic tag', async() => {
+ const spec = new DocumentBuilder('name')
+ .response(200, response)
+ .tag({
+ name: 'basic',
+ description: 'basic tag',
+ })
+ .build();
+
+ const fs = await run(spec);
+
+ expect(fs.match('toc.yaml')).toMatchSnapshot('toc');
+ expect(fs.match('basic/index.md')).toMatchSnapshot('index.md');
+ expect(fs.match('basic/name.md')).toMatchSnapshot('name.md');
+ });
+
+ it('uses custom title from operation', async() => {
+ const spec = new DocumentBuilder('name')
+ .response(200, response)
+ .tag({
+ name: 'TagName',
+ })
+ .navTitle('Title from x-navtitle in operation')
+ .build();
+
+ const fs = await run(spec);
+
+ expect(fs.match('toc.yaml')).toMatchSnapshot('toc');
+ expect(fs.match('TagName/index.md')).toMatchSnapshot('index.md');
+ });
+
+ it('uses custom title from tag override title from operation', async() => {
+ const spec = new DocumentBuilder('name')
+ .response(200, response)
+ .tag({
+ name: 'TagName',
+ 'x-navtitle': 'Title from tag'
+ })
+ .navTitle('Title from x-navtitle in operation')
+ .build();
+
+ const fs = await run(spec);
+
+ expect(fs.match('toc.yaml')).toMatchSnapshot('toc');
+ expect(fs.match('TagName/index.md')).toMatchSnapshot('index.md');
+ });
+
+ it('uses custom name from tag', async() => {
+ const spec = new DocumentBuilder('name')
+ .response(200, response)
+ .tag({
+ name: 'TagName',
+ 'x-slug': 'slug'
+ })
+ .build();
+
+ const fs = await run(spec);
+
+ expect(fs.match('toc.yaml')).toMatchSnapshot('toc');
+ expect(fs.match('slug/index.md')).toMatchSnapshot('index.md');
+ });
+
+ it('uses custom name from tag and title from operation', async() => {
+ const spec = new DocumentBuilder('name')
+ .response(200, response)
+ .tag({
+ name: 'TagName',
+ 'x-slug': 'slug',
+ })
+ .navTitle('Title from x-navtitle in operation')
+ .build();
+
+ const fs = await run(spec);
+
+ expect(fs.match('toc.yaml')).toMatchSnapshot('toc');
+ expect(fs.match('slug/index.md')).toMatchSnapshot('index.md');
+ });
+});
diff --git a/src/includer/constants.ts b/src/includer/constants.ts
index 61bad2c..75707ef 100644
--- a/src/includer/constants.ts
+++ b/src/includer/constants.ts
@@ -5,6 +5,7 @@ export enum LeadingPageMode {
Leaf = 'leaf',
}
export const EOL = '\n';
+export const TAG_ID_FIELD = 'x-slug';
export const TAG_NAMES_FIELD = 'x-navtitle';
export const BLOCK = EOL.repeat(2);
export const INFO_TAB_NAME = 'Info';
diff --git a/src/includer/index.ts b/src/includer/index.ts
index b93c8b1..c39f31d 100644
--- a/src/includer/index.ts
+++ b/src/includer/index.ts
@@ -155,7 +155,7 @@ async function generateToc(params: GenerateTocParams): Promise {
items: [],
};
- tags.forEach((tag, id) => {
+ tags.forEach((tag) => {
// eslint-disable-next-line no-shadow
const {name, endpoints: endpointsOfTag} = tag;
@@ -165,7 +165,7 @@ async function generateToc(params: GenerateTocParams): Promise {
};
const custom = ArgvService.tag(tag.name);
- const customId = custom?.alias || id;
+ const customId = custom?.alias || tag.id;
section.items = endpointsOfTag.map((endpoint) => handleEndpointRender(endpoint, customId));
@@ -273,11 +273,11 @@ async function generateContent(params: GenerateContentParams): Promise {
});
}
- spec.tags.forEach((tag, id) => {
+ spec.tags.forEach((tag) => {
const {endpoints} = tag;
const custom = ArgvService.tag(tag.name);
- const customId = custom?.alias || id;
+ const customId = custom?.alias || tag.id;
endpoints.forEach((endpoint) => {
results.push(handleEndpointIncluder(endpoint, join(writePath, customId), sandbox));
diff --git a/src/includer/models.ts b/src/includer/models.ts
index 6b5be0e..e895b76 100644
--- a/src/includer/models.ts
+++ b/src/includer/models.ts
@@ -5,6 +5,8 @@ import {
SPEC_RENDER_MODE_DEFAULT,
SPEC_RENDER_MODE_HIDDEN,
SUPPORTED_ENUM_TYPES,
+ TAG_ID_FIELD,
+ TAG_NAMES_FIELD,
} from './constants';
export type VarsPreset = 'internal' | 'external';
@@ -128,7 +130,7 @@ export type OpenAPIOperation = {
content: {[ContentType: string]: {schema: OpenJSONSchema}};
};
security?: Array>;
- 'x-navtitle': string[];
+ [TAG_NAMES_FIELD]: string[];
};
export type Info = {
@@ -159,6 +161,8 @@ export type Tag = {
name: string;
description?: string;
endpoints: Endpoints;
+ [TAG_NAMES_FIELD]?: string;
+ [TAG_ID_FIELD]?: string;
};
export type Endpoints = Endpoint[];
diff --git a/src/includer/parsers.ts b/src/includer/parsers.ts
index 5a972e1..36bca39 100644
--- a/src/includer/parsers.ts
+++ b/src/includer/parsers.ts
@@ -2,7 +2,7 @@
import slugify from 'slugify';
import {getStatusText} from 'http-status-codes';
-import {TAG_NAMES_FIELD} from './constants';
+import {TAG_ID_FIELD, TAG_NAMES_FIELD} from './constants';
import {
Endpoint,
@@ -110,6 +110,11 @@ function tagsFromSpec(spec: OpenAPISpec): Map {
}
}
+ parsed.forEach((tag: Tag) => {
+ tag.name = tag[TAG_NAMES_FIELD] || tag.name;
+ tag.id = tag[TAG_ID_FIELD] ?? tag.id;
+ });
+
return parsed;
}
const opid = (path: string, method: string, id?: string) => slugify(id ?? [path, method].join('-'));
diff --git a/src/runtime/types.ts b/src/runtime/types.ts
index b7c63ea..3b7acae 100644
--- a/src/runtime/types.ts
+++ b/src/runtime/types.ts
@@ -1,3 +1,4 @@
+import { TAG_NAMES_FIELD } from '../includer/constants';
import {OpenJSONSchema} from '../includer/models';
export interface Field {
@@ -68,7 +69,7 @@ export type OpenAPIOperation = {
content: {[ContentType: string]: {schema: OpenJSONSchema}};
};
security?: Array>;
- 'x-navtitle': string[];
+ [TAG_NAMES_FIELD]?: string[];
};
export type Info = {