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 = {