From dbb8e11bcac6c68bde1c0d6454c499ba8d55827c Mon Sep 17 00:00:00 2001 From: v8tenko Date: Fri, 10 Nov 2023 14:31:44 +0300 Subject: [PATCH] feat: refactor, oneOf/allOf fixes --- .eslintrc | 14 -- .eslintrc.js | 15 ++ package-lock.json | 102 +++-------- package.json | 2 +- .../combiners/complex.test.ts.snap | 14 +- .../combiners/oneOf.test.ts.snap | 43 +++-- src/__snapshots__/description.test.ts.snap | 2 +- src/includer/generators/index.ts | 6 - src/includer/generators/types.ts | 89 ---------- src/includer/index.ts | 5 +- src/includer/models.ts | 8 +- src/includer/services/anonymous.ts | 29 --- src/includer/services/refs.ts | 20 ++- .../traverse.ts => traverse/tables.ts} | 127 ++++++++------ src/includer/traverse/types.ts | 166 ++++++++++++++++++ src/includer/ui/common.ts | 6 +- src/includer/ui/endpoint.ts | 63 ++++--- src/includer/ui/index.ts | 10 +- src/includer/{generators => ui}/main.ts | 8 +- src/includer/utils.ts | 35 +--- 20 files changed, 388 insertions(+), 376 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.js delete mode 100644 src/includer/generators/index.ts delete mode 100644 src/includer/generators/types.ts delete mode 100644 src/includer/services/anonymous.ts rename src/includer/{generators/traverse.ts => traverse/tables.ts} (80%) create mode 100644 src/includer/traverse/types.ts rename src/includer/{generators => ui}/main.ts (95%) diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index ca53e9e..0000000 --- a/.eslintrc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": ["@diplodoc/eslint-config", "@diplodoc/eslint-config/prettier"], - "root": true, - "overrides": [ - { - "files": ["*.ts", "*.tsx"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "project": ["./tsconfig.test.json", "./tsconfig.json"] - } - } - ] -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..8a04cd4 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + extends: ['@diplodoc/eslint-config', '@diplodoc/eslint-config/prettier'], + root: true, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + project: ['./tsconfig.test.json', './tsconfig.json'], + tsconfigRootDir: __dirname, + }, + }, + ], +}; diff --git a/package-lock.json b/package-lock.json index 77490d9..276bbc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.4.0", "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", - "@diplodoc/transform": "^4.2.1", + "@diplodoc/transform": "^4.3.0", "bem-cn-lite": "^4.1.0", "html-escaper": "^3.0.3", "http-status-codes": "^2.2.0", @@ -22,6 +22,7 @@ "@babel/core": "^7.23.2", "@babel/eslint-parser": "^7.22.15", "@diplodoc/eslint-config": "^1.0.15", + "@diplodoc/prettier-config": "^1.0.0", "@diplodoc/tsconfig": "^1.0.2", "@gravity-ui/uikit": "^5.18.0", "@swc/cli": "^0.1.62", @@ -44,6 +45,7 @@ "markdown-it": "^13.0.1", "npm-run-all": "^4.1.5", "openapi-types": "^12.1.3", + "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "ts-jest": "^29.1.1", @@ -1010,6 +1012,15 @@ "eslint-plugin-security": "1.7.1" } }, + "node_modules/@diplodoc/prettier-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@diplodoc/prettier-config/-/prettier-config-1.0.0.tgz", + "integrity": "sha512-g4dShJe/sRPTorvBX0VnTQj38dvTHRVdAB6wxuSU3lEpR4jUl/kKYShvKwAYZFhNmERj0dXPV0DJFr3krI4K/Q==", + "dev": true, + "peerDependencies": { + "prettier": "*" + } + }, "node_modules/@diplodoc/tabs-extension": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/@diplodoc/tabs-extension/-/tabs-extension-2.0.12.tgz", @@ -1024,9 +1035,9 @@ } }, "node_modules/@diplodoc/transform": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@diplodoc/transform/-/transform-4.2.1.tgz", - "integrity": "sha512-e9rU5Sdoe9ntdDn3vRNrgJ9/NqG5Vu6PHoiqEhIRcnK/x2Tp/GqUgZYcU5CLKNupx0SMqfv4GgGNzg+uJiWzXQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@diplodoc/transform/-/transform-4.3.0.tgz", + "integrity": "sha512-68TwLtZtWqBbEelN4CJ0Y6Sg8ujIxUbxrtmuvplLwAJu/KJv+3z4A+N2JAWUvTmS1BCoyEeYyVtILJk0jT+ubw==", "dependencies": { "@diplodoc/tabs-extension": "2.0.12", "chalk": "4.1.2", @@ -1044,7 +1055,7 @@ "markdown-it-sup": "1.0.0", "markdownlint": "^0.25.1", "markdownlint-rule-helpers": "0.17.2", - "sanitize-html": "2.7.3", + "sanitize-html": "^2.11.0", "slugify": "1.6.5" }, "peerDependencies": { @@ -10152,9 +10163,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -10848,7 +10859,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11615,66 +11625,18 @@ } }, "node_modules/sanitize-html": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.3.tgz", - "integrity": "sha512-jMaHG29ak4miiJ8wgqA1849iInqORgNv7SLfSw9LtfOhEUQ1C0YHKH73R+hgyufBW9ZFeJrb057k9hjlfBCVlw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", - "htmlparser2": "^6.0.0", + "htmlparser2": "^8.0.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, - "node_modules/sanitize-html/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/sanitize-html/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/sanitize-html/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/sanitize-html/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/sanitize-html/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -11686,24 +11648,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sanitize-html/node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, "node_modules/sass": { "version": "1.63.6", "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz", diff --git a/package.json b/package.json index 7a53d04..87913b3 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ }, "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", - "@diplodoc/transform": "^4.2.1", + "@diplodoc/transform": "^4.3.0", "bem-cn-lite": "^4.1.0", "html-escaper": "^3.0.3", "http-status-codes": "^2.2.0", diff --git a/src/__snapshots__/combiners/complex.test.ts.snap b/src/__snapshots__/combiners/complex.test.ts.snap index fe4fba2..aff298d 100644 --- a/src/__snapshots__/combiners/complex.test.ts.snap +++ b/src/__snapshots__/combiners/complex.test.ts.snap @@ -32,8 +32,7 @@ Generated server url{.openapi__request__description} \`\`\`json { - "name": "b", - "age": 0 + "name": "b" } \`\`\` @@ -45,9 +44,9 @@ Generated server url{.openapi__request__description} || name | string | Default: \`b\` || -|| age | number, boolean | || +|| age | any | || -|| ...rest | oneOf | [Dog](#dog) +|| ...rest | oneOf | [Dog](#dog) or [Cat](#cat) |||# #### Or value from: @@ -87,8 +86,7 @@ Base 200 response \`\`\`json { - "name": "b", - "age": 0 + "name": "b" } \`\`\` @@ -100,9 +98,9 @@ Base 200 response || name | string | Default: \`b\` || -|| age | number, boolean | || +|| age | any | || -|| ...rest | oneOf | [Dog](#dog) +|| ...rest | oneOf | [Dog](#dog) or [Cat](#cat) |||# #### Or value from: diff --git a/src/__snapshots__/combiners/oneOf.test.ts.snap b/src/__snapshots__/combiners/oneOf.test.ts.snap index fa7813a..54aa780 100644 --- a/src/__snapshots__/combiners/oneOf.test.ts.snap +++ b/src/__snapshots__/combiners/oneOf.test.ts.snap @@ -31,7 +31,10 @@ Generated server url{.openapi__request__description} \`\`\`json -{} +{ + "type": "string", + "baz": "string" +} \`\`\` @@ -40,7 +43,7 @@ Generated server url{.openapi__request__description} #||| **Name** | **Type** | **Description** || -|| ...rest | oneOf | [Dog](#dog) +|| ...rest | oneOf | [Dog](#dog) or [Cat](#cat) |||# #### Or value from: @@ -77,7 +80,10 @@ Cat class \`\`\`json -{} +{ + "type": "string", + "baz": "string" +} \`\`\` @@ -86,7 +92,8 @@ Cat class #||| **Name** | **Type** | **Description** || -|| ...rest | oneOf | [Dog](#dog) +|| ...rest | oneOf | Base 200 response +[Dog](#dog) or [Cat](#cat) |||# #### Or value from: @@ -144,7 +151,7 @@ Generated server url{.openapi__request__description} || age | number | || -|| ...rest | oneOf | [Dog](#dog) +|| ...rest | oneOf | [Dog](#dog) or [Cat](#cat) |||# #### Or value from: @@ -197,7 +204,8 @@ Cat class || age | number | || -|| ...rest | oneOf | [Dog](#dog) +|| ...rest | oneOf | Base 200 response +[Dog](#dog) or [Cat](#cat) |||# #### Or value from: @@ -240,7 +248,10 @@ Generated server url{.openapi__request__description} \`\`\`json { - "pet": {} + "pet": { + "type": "string", + "baz": "string" + } } \`\`\` @@ -250,10 +261,10 @@ Generated server url{.openapi__request__description} #||| **Name** | **Type** | **Description** || -|| pet | object | [Dog](#dog) -or [Cat](#cat) || +|| pet | [Dog](#dog) +or [Cat](#cat) | || -|| ...rest | oneOf | [Dog](#dog) +|| ...rest | oneOf | [Dog](#dog) or [Cat](#cat) |||# #### Or value from: @@ -291,7 +302,10 @@ Cat class \`\`\`json { - "pet": {} + "pet": { + "type": "string", + "baz": "string" + } } \`\`\` @@ -301,10 +315,11 @@ Cat class #||| **Name** | **Type** | **Description** || -|| pet | object | [Dog](#dog) -or [Cat](#cat) || +|| pet | [Dog](#dog) +or [Cat](#cat) | || -|| ...rest | oneOf | [Dog](#dog) +|| ...rest | oneOf | Base 200 response +[Dog](#dog) or [Cat](#cat) |||# #### Or value from: diff --git a/src/__snapshots__/description.test.ts.snap b/src/__snapshots__/description.test.ts.snap index d2582ca..d0ef10b 100644 --- a/src/__snapshots__/description.test.ts.snap +++ b/src/__snapshots__/description.test.ts.snap @@ -36,7 +36,7 @@ Generated server url{.openapi__request__description} {% cut "application/json" %} -\`\`\` +\`\`\`json { "pet": { "type": "string", diff --git a/src/includer/generators/index.ts b/src/includer/generators/index.ts deleted file mode 100644 index 656426b..0000000 --- a/src/includer/generators/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {main} from './main'; -import {endpoint, section} from '../ui'; - -export {main, section, endpoint}; - -export default {main, section, endpoint}; diff --git a/src/includer/generators/types.ts b/src/includer/generators/types.ts deleted file mode 100644 index cf7cacc..0000000 --- a/src/includer/generators/types.ts +++ /dev/null @@ -1,89 +0,0 @@ -import stringify from 'json-stringify-safe'; -import RefsService from '../services/refs'; - -import { - JSONSchemaType, - JSONSchemaUnionType, - JsType, - OpenJSONSchema, - SupportedEnumType, -} from '../models'; - -import {SUPPORTED_ENUM_TYPES} from '../constants'; - -function inferType(value: OpenJSONSchema): JSONSchemaType { - if (value === null) { - return 'null'; - } - - if (value.type) { - return value.type; - } - - if (value.enum) { - const enumType = typeof value.enum[0]; - if (isSupportedEnumType(enumType)) { - return enumType; - } - - throw new Error(`Unsupported enum type in value: ${stringify(value)}`); - } - - if (value.default) { - const type = typeof value.default; - if (isSupportedEnumType(type)) { - return type; - } - } - - const ref = RefsService.find(value); - - if (value.oneOf?.length) { - const unionOf = (value.oneOf.filter(Boolean) as OpenJSONSchema[]).flatMap(inferType); - - if (unionOf.length === 1) { - return unionOf[0]; - } - - return { - ref, - unionOf, - }; - } - - return 'any'; -} - -function isComplexType(type: JSONSchemaType): type is JSONSchemaUnionType { - if (Array.isArray(type)) { - return false; - } - - if (typeof type !== 'object') { - return false; - } - - return (type.unionOf?.length || 0) > 1; -} - -function typeToText(value: OpenJSONSchema): string { - const type = inferType(value); - - if (isComplexType(type)) { - const {unionOf} = type; - - if (unionOf) { - return unionOf.join(' or '); - } - - throw new Error(`Can not infer type of ${value}`); - } - - return `${type}`; -} - -function isSupportedEnumType(enumType: JsType): enumType is SupportedEnumType { - return SUPPORTED_ENUM_TYPES.some((type) => enumType === type); -} - -export {inferType, isComplexType, typeToText, isSupportedEnumType}; diff --git a/src/includer/index.ts b/src/includer/index.ts index 3124222..2a662c3 100644 --- a/src/includer/index.ts +++ b/src/includer/index.ts @@ -1,5 +1,4 @@ import assert from 'assert'; -import AnonymousService from './services/anonymous'; import {dirname, join, resolve} from 'path'; import {mkdir, writeFile} from 'fs/promises'; @@ -8,7 +7,7 @@ import {matchFilter} from './utils'; import {dump} from 'js-yaml'; import parsers from './parsers'; -import generators from './generators'; +import generators from './ui'; import SwaggerParser from '@apidevtools/swagger-parser'; @@ -269,8 +268,6 @@ function handleEndpointIncluder(endpoint: Endpoint, pathPrefix: string, sandbox? const path = join(pathPrefix, mdPath(endpoint)); const content = generators.endpoint(endpoint, sandbox); - AnonymousService.clear(); - return {path, content}; } diff --git a/src/includer/models.ts b/src/includer/models.ts index a781adb..8752964 100644 --- a/src/includer/models.ts +++ b/src/includer/models.ts @@ -1,4 +1,5 @@ import {JSONSchema6, JSONSchema6Definition} from 'json-schema'; + import { LeadingPageMode, SPEC_RENDER_MODE_DEFAULT, @@ -277,6 +278,7 @@ export type OpenApiFilter = { }; export type OpenApiIncluderParams = { + allowAnonymousObjects?: boolean; input: string; leadingPage?: LeadingPageParams; filter?: OpenApiFilter; @@ -290,6 +292,7 @@ export type OpenApiIncluderParams = { export type OpenJSONSchema = JSONSchema6 & { _runtime?: true; + _emptyDescription?: true; example?: unknown; properties?: { [key: string]: JSONSchema6Definition & { @@ -299,10 +302,13 @@ export type OpenJSONSchema = JSONSchema6 & { }; export type OpenJSONSchemaDefinition = OpenJSONSchema | boolean; +export type FoundRefType = { + ref: string; +}; export type BaseJSONSchemaType = Exclude; export type JSONSchemaUnionType = { ref?: string; /* Not oneOf because of collision with OpenJSONSchema['oneOf'] */ unionOf?: JSONSchemaType[]; }; -export type JSONSchemaType = BaseJSONSchemaType | JSONSchemaUnionType; +export type JSONSchemaType = BaseJSONSchemaType | JSONSchemaUnionType | FoundRefType; diff --git a/src/includer/services/anonymous.ts b/src/includer/services/anonymous.ts deleted file mode 100644 index 8b77bcf..0000000 --- a/src/includer/services/anonymous.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {EOL} from '../constants'; -import {OpenJSONSchema, OpenJSONSchemaDefinition} from '../models'; - -const pendingTables: Map = new Map(); - -function add(ref: string | undefined, schema: OpenJSONSchemaDefinition) { - if (typeof schema === 'boolean') { - return; - } - - if (!ref) { - return; - } - - pendingTables.set(ref, schema); -} - -function render(): string { - const entries = [...pendingTables.entries()]; - - return entries.map(([, v]) => JSON.stringify(v, null, 2)).join(EOL); -} - -function clear(): void { - pendingTables.clear(); -} - -export {add, render, clear}; -export default {add, render, clear}; diff --git a/src/includer/services/refs.ts b/src/includer/services/refs.ts index 374e077..941cdc4 100644 --- a/src/includer/services/refs.ts +++ b/src/includer/services/refs.ts @@ -1,10 +1,13 @@ +import {descriptionForOneOfElement} from '../traverse/types'; import {OpenJSONSchema, OpenJSONSchemaDefinition, Refs} from '../models'; -import {concatNewLine, descriptionForOneOfElement} from '../utils'; +import {concatNewLine} from '../utils'; +let allowRuntime: boolean; let _refs: Refs = {}; -function init(allRefs: Refs) { +function init(allRefs: Refs, allowAnonymousObjects?: boolean) { _refs = allRefs; + allowRuntime = Boolean(allowAnonymousObjects); } /** @@ -98,7 +101,7 @@ function merge(schema: OpenJSONSchemaDefinition, needToSaveRef = true): OpenJSON if (value.oneOf?.length) { const description = descriptionForOneOfElement(value); - return {...value, description}; + return {...value, description, _emptyDescription: true}; } let description = value.description || ''; @@ -145,7 +148,7 @@ function removeInternalProperty(schema: OpenJSONSchema): OpenJSONSchema { return schema; } -function get(ref: string): OpenJSONSchema { +function get(ref: string): OpenJSONSchema | undefined { return _refs[ref]; } @@ -157,10 +160,18 @@ function has(name: string): boolean { * To add runtime objects like anonymous oneOf */ function runtime(ref: string, value: OpenJSONSchema) { + if (!allowRuntime) { + return; + } + value._runtime = true; _refs[ref] = value; } +function isRuntimeAllowed() { + return allowRuntime; +} + function refs() { return _refs; } @@ -173,4 +184,5 @@ export default { runtime, refs, merge, + isRuntimeAllowed, }; diff --git a/src/includer/generators/traverse.ts b/src/includer/traverse/tables.ts similarity index 80% rename from src/includer/generators/traverse.ts rename to src/includer/traverse/tables.ts index 3357863..7642cb7 100644 --- a/src/includer/generators/traverse.ts +++ b/src/includer/traverse/tables.ts @@ -1,10 +1,17 @@ import RefsService from '../services/refs'; +import stringify from 'json-stringify-safe'; import {anchor, table, tableParameterName, title} from '../ui'; -import {concatNewLine, descriptionForOneOfElement, extractOneOfElements} from '../utils'; +import {concatNewLine} from '../utils'; import {EOL} from '../constants'; import {OpenJSONSchema, OpenJSONSchemaDefinition} from '../models'; -import {inferType, isComplexType, typeToText} from './types'; +import { + descriptionForOneOfElement, + extractOneOfElements, + extractRefFromType, + inferType, + typeToText, +} from './types'; type TableRow = [string, string, string]; @@ -20,10 +27,13 @@ export function tableFromSchema(schema: OpenJSONSchema): TableFromSchemaResult { if (schema.enum) { // enum description will be in table description const description = prepareComplexDescription('', schema); + const type = inferType(schema); + const content = table([ ['Type', 'Description'], - [typeToText(schema), description], + [typeToText(type), description], ]); + return {content, tableRefs: []}; } @@ -89,7 +99,7 @@ function prepareObjectSchemaTable(schema: OpenJSONSchema): PrepareObjectSchemaTa }); if (schema.oneOf?.length) { - const restElementsDescription = descriptionForOneOfElement(schema); + const restElementsDescription = descriptionForOneOfElement(schema, true); result.rows.push(['...rest', 'oneOf', restElementsDescription]); } @@ -101,8 +111,9 @@ type PrepareRowResult = { type: string; description: string; ref?: TableRef; - /* if object has no ref in RefsService - * then we will create runtime ref and render it later via AnonymousService + /* + * if object has no ref in RefsService + * then we will create runtime ref and render it later */ runtimeRef?: string; }; @@ -115,12 +126,6 @@ export function prepareTableRowData( const description = value.description || ''; const propertyRef = parentRef && key && `${parentRef}-${key}`; - const ref = RefsService.find(value); - - if (ref) { - return {type: anchor(ref), description, ref}; - } - const type = inferType(value); if (type === 'array') { @@ -133,11 +138,11 @@ export function prepareTableRowData( ? description : concatNewLine(description, inner.description); - if (inner.runtimeRef) { + if (RefsService.isRuntimeAllowed() && inner.runtimeRef) { RefsService.runtime(inner.runtimeRef, value.items); return { - type: `${anchor(inner.runtimeRef)}[]`, + type: `${anchor(inner.runtimeRef, key)}[]`, runtimeRef: inner.runtimeRef, description: innerDescription, }; @@ -151,56 +156,22 @@ export function prepareTableRowData( }; } - if (propertyRef && type === 'object') { + if (RefsService.isRuntimeAllowed() && propertyRef && type === 'object') { RefsService.runtime(propertyRef, value); return { - type: anchor(propertyRef), + type: anchor(propertyRef, key), runtimeRef: propertyRef, description: prepareComplexDescription(description, value), }; } - if (isComplexType(type)) { - let runtimeRef: string | undefined; - - const {unionOf} = type; - - if (unionOf) { - const unique = unionOf - .map((el) => { - if (!isComplexType(el)) { - return el; - } - - const oneOfRef = el.ref || propertyRef; - - if (propertyRef) { - runtimeRef = propertyRef; - } - - return oneOfRef && anchor(oneOfRef); - }) - .filter(Boolean) - .sort((a, b) => a!.length - b!.length); - - if (runtimeRef) { - RefsService.runtime(runtimeRef, value); - } - - return { - type: unique.join(' or '), - description, - runtimeRef, - }; - } - } - const format = value.format === undefined ? '' : `<${value.format}>`; return { - type: typeToText(value) + format, + type: typeToText(type) + format, description: prepareComplexDescription(description, value), + ref: extractRefFromType(type), }; } @@ -230,19 +201,55 @@ function prepareComplexDescription(baseDescription: string, value: OpenJSONSchem return description; } -// sample key-value JSON body +function findNonNullOneOfElement(schema: OpenJSONSchema): OpenJSONSchema { + const isValid = (v: OpenJSONSchema) => { + if (typeof inferType(v) === 'string') { + return true; + } + + if (Object.keys(v.properties || {}).length) { + return true; + } + + return false; + }; + + if (isValid(schema)) { + return RefsService.merge(schema); + } + + const stack = [...(schema.oneOf || [])]; + + while (stack.length) { + const v = stack.shift(); + + if (!v || typeof v === 'boolean') { + continue; + } + + if (isValid(v)) { + return RefsService.merge(v); + } + + stack.push(...(v.oneOf || [])); + } + + throw new Error(`Unable to create sample element: \n ${stringify(schema, null, 2)}`); +} + export function prepareSampleObject(schema: OpenJSONSchema, callstack: OpenJSONSchema[] = []) { - const result: {[key: string]: any} = {}; + const result: {[key: string]: unknown} = {}; if (schema.example) { return schema.example; } - const merged = RefsService.merge(schema); + const merged = findNonNullOneOfElement(RefsService.merge(schema)); Object.entries(merged.properties || {}).forEach(([key, value]) => { const required = isRequired(key, merged); const possibleValue = prepareSampleElement(key, value, required, callstack); + if (possibleValue !== undefined) { result[key] = possibleValue; } @@ -256,25 +263,29 @@ function prepareSampleElement( v: OpenJSONSchemaDefinition, required: boolean, callstack: OpenJSONSchema[], -): any { +): unknown { const value = RefsService.merge(v); if (value.example) { return value.example; } + if (value.enum?.length) { return value.enum[0]; } + if (value.default !== undefined) { return value.default; } + if (!required && callstack.includes(value)) { // stop recursive cyclic links return undefined; } + const downCallstack = callstack.concat(value); const type = inferType(value); - const schema = value.oneOf?.length ? (value.oneOf[1] as OpenJSONSchema) : value; + const schema = findNonNullOneOfElement(value); switch (type) { case 'object': @@ -301,10 +312,12 @@ function prepareSampleElement( case 'boolean': return false; } + if (schema.properties) { // if no "type" specified return prepareSampleObject(schema, downCallstack); } + return undefined; } diff --git a/src/includer/traverse/types.ts b/src/includer/traverse/types.ts new file mode 100644 index 0000000..635b9a8 --- /dev/null +++ b/src/includer/traverse/types.ts @@ -0,0 +1,166 @@ +import RefsService from '../services/refs'; +import stringify from 'json-stringify-safe'; + +import {EOL, SUPPORTED_ENUM_TYPES} from '../constants'; + +import { + JSONSchemaType, + JSONSchemaUnionType, + JsType, + OpenJSONSchema, + SupportedEnumType, +} from '../models'; + +import {anchor} from '../ui'; + +function inferType(value: OpenJSONSchema): JSONSchemaType { + if (value === null) { + return 'null'; + } + + const ref = RefsService.find(value); + + if (value.oneOf?.length) { + const unionOf = (value.oneOf.filter(Boolean) as OpenJSONSchema[]).map((el) => { + const foundRef = RefsService.find(el); + + if (foundRef) { + return {ref: foundRef}; + } + const type = inferType(el); + + return type; + }); + + if (unionOf.length === 1) { + return unionOf[0]; + } + + return { + ref, + unionOf: [...new Set(unionOf)], + }; + } + + if (ref) { + return {ref}; + } + + if (value.type) { + return value.type; + } + + if (value.enum) { + const enumType = typeof value.enum[0]; + if (isSupportedEnumType(enumType)) { + return enumType; + } + + throw new Error(`Unsupported enum type in value: ${stringify(value)}`); + } + + if (value.default) { + const type = typeof value.default; + if (isSupportedEnumType(type)) { + return type; + } + } + + return 'any'; +} + +function extractRefFromType(type: JSONSchemaType): string | undefined { + if (typeof type === 'string' || isUnionType(type)) { + return undefined; + } + + if (Array.isArray(type)) { + return undefined; + } + + return type.ref; +} + +function isUnionType(type: JSONSchemaType): type is JSONSchemaUnionType { + if (Array.isArray(type)) { + return false; + } + + if (typeof type !== 'object') { + return false; + } + + return 'unionOf' in type && Boolean(type.unionOf?.length); +} + +function typeToText(type: JSONSchemaType): string { + if (typeof type === 'string') { + return `${type}`; + } + + if (Array.isArray(type)) { + return 'array'; + } + + if (isUnionType(type)) { + return [...new Set(type.unionOf)].map(typeToText).join(' \nor '); + } + + if (type.ref) { + return anchor(type.ref); + } + + throw new Error(`Unbale to stringify type: ${type}`); +} + +function isSupportedEnumType(enumType: JsType): enumType is SupportedEnumType { + return SUPPORTED_ENUM_TYPES.some((type) => enumType === type); +} + +function extractOneOfElements(from: OpenJSONSchema): OpenJSONSchema[] { + if (!from.oneOf?.length) { + return []; + } + + const elements = from.oneOf.filter(Boolean) as OpenJSONSchema[]; + + return elements; +} + +function anchorToSchema(item: OpenJSONSchema): string | undefined { + const ref = RefsService.find(item); + + return ref ? anchor(ref) : undefined; +} + +function descriptionForOneOfElement(target: OpenJSONSchema, withTypes?: boolean): string { + let description = target.description || ''; + + const elements = extractOneOfElements(target); + + if (elements.length === 0) { + return description; + } + + if (withTypes) { + if (description.length) { + description += EOL; + } + + description += extractOneOfElements(target) + .map(anchorToSchema) + .filter(Boolean) + .join(' \nor '); + } + + return description; +} + +export { + inferType, + typeToText, + isSupportedEnumType, + extractOneOfElements, + extractRefFromType, + descriptionForOneOfElement, +}; diff --git a/src/includer/ui/common.ts b/src/includer/ui/common.ts index d0267b4..6351403 100644 --- a/src/includer/ui/common.ts +++ b/src/includer/ui/common.ts @@ -48,7 +48,7 @@ function bold(text: string) { } function code(text: string, type = '') { - const appliedType = type && text.length <= 200 ? type : ''; + const appliedType = type && text.length <= 2000 ? type : ''; return EOL + ['```' + appliedType, text, '```'].join(EOL) + EOL; } @@ -102,8 +102,8 @@ function tabs(tabsObj: Record) { ]); } -function anchor(ref: string) { - return link(ref, `#${slugify(ref).toLowerCase()}`); +function anchor(ref: string, name?: string) { + return link(name || ref, `#${slugify(ref).toLowerCase()}`); } function tableParameterName(key: string, required?: boolean) { diff --git a/src/includer/ui/endpoint.ts b/src/includer/ui/endpoint.ts index fcb2757..7d27384 100644 --- a/src/includer/ui/endpoint.ts +++ b/src/includer/ui/endpoint.ts @@ -1,23 +1,6 @@ import stringify from 'json-stringify-safe'; -import AnonymousService from '../services/anonymous'; import RefsService from '../services/refs'; -import { - block, - body, - bold, - code, - cut, - meta, - method, - openapiBlock, - page, - table, - tableParameterName, - tabs, - title, -} from './common'; - import { COOKIES_SECTION_NAME, HEADERS_SECTION_NAME, @@ -30,6 +13,13 @@ import { SANDBOX_TAB_NAME, } from '../constants'; +import { + TableRef, + prepareSampleObject, + prepareTableRowData, + tableFromSchema, +} from '../traverse/tables'; + import { Endpoint, OpenJSONSchema, @@ -41,14 +31,25 @@ import { Security, Server, } from '../models'; -import { - TableRef, - prepareSampleObject, - prepareTableRowData, - tableFromSchema, -} from '../generators/traverse'; + import {concatNewLine} from '../utils'; +import { + block, + body, + bold, + code, + cut, + meta, + method, + openapiBlock, + page, + table, + tableParameterName, + tabs, + title, +} from './common'; + function endpoint(data: Endpoint, sandboxPlugin: {host?: string; tabName?: string} | undefined) { // try to remember, which tables we are already printed on page const pagePrintedRefs = new Set(); @@ -78,7 +79,6 @@ function endpoint(data: Endpoint, sandboxPlugin: {host?: string; tabName?: strin parameters(pagePrintedRefs, data.parameters), openapiBody(pagePrintedRefs, data.requestBody), responses(pagePrintedRefs, data.responses), - AnonymousService.render(), ]), ), ]); @@ -256,12 +256,21 @@ function printAllTables(pagePrintedRefs: Set, tableRefs: TableRef[]): st } const schema = RefsService.get(tableRef); + + if (!schema) { + continue; + } + const schemaTable = tableFromSchema(schema); const titleLevel = schema._runtime ? 4 : 3; - console.log(tableRef, schema._runtime); - - result.push(block([title(titleLevel)(tableRef), schema.description, schemaTable.content])); + result.push( + block([ + title(titleLevel)(tableRef), + schema._emptyDescription ? '' : schema.description, + schemaTable.content, + ]), + ); tableRefs.push(...schemaTable.tableRefs); pagePrintedRefs.add(tableRef); } diff --git a/src/includer/ui/index.ts b/src/includer/ui/index.ts index 57da8d8..6d72b22 100644 --- a/src/includer/ui/index.ts +++ b/src/includer/ui/index.ts @@ -1,3 +1,9 @@ export * from './common'; -export * from './endpoint'; -export * from './section'; + +import {endpoint} from './endpoint'; +import {section} from './section'; +import {main} from './main'; + +export {main, section, endpoint}; + +export default {main, section, endpoint}; diff --git a/src/includer/generators/main.ts b/src/includer/ui/main.ts similarity index 95% rename from src/includer/generators/main.ts rename to src/includer/ui/main.ts index 7b7df60..0223f0b 100644 --- a/src/includer/generators/main.ts +++ b/src/includer/ui/main.ts @@ -2,7 +2,8 @@ import stringify from 'json-stringify-safe'; import {sep} from 'path'; -import {block, body, code, cut, link, list, mono, page, title} from '../ui'; +import {block, body, code, cut, link, list, mono, page, title} from '.'; + import { CONTACTS_SECTION_NAME, ENDPOINTS_SECTION_NAME, @@ -20,10 +21,11 @@ import { Specification, Tag, } from '../models'; + import {mdPath, sectionName} from '../index'; export type MainParams = { - data: any; + data: unknown; info: Info; spec: Specification; leadingPageSpecRenderMode: LeadingPageSpecRenderMode; @@ -85,7 +87,7 @@ function sections({tags, endpoints}: Specification) { return content.length && block(content); } -function specification(data: any, renderMode: LeadingPageSpecRenderMode) { +function specification(data: unknown, renderMode: LeadingPageSpecRenderMode) { return ( renderMode === SPEC_RENDER_MODE_DEFAULT && block([title(2)(SPEC_SECTION_NAME), cut(code(stringify(data, null, 4)), SPEC_SECTION_TYPE)]) diff --git a/src/includer/utils.ts b/src/includer/utils.ts index 2c3f996..185f9a2 100644 --- a/src/includer/utils.ts +++ b/src/includer/utils.ts @@ -1,8 +1,5 @@ -import RefsService from './services/refs'; - -import {Endpoint, OpenApiIncluderParams, OpenJSONSchema, Specification, Tag} from './models'; +import {Endpoint, OpenApiIncluderParams, Specification, Tag} from './models'; import {evalExp} from '@diplodoc/transform/lib/liquid/evaluation'; -import {anchor} from './ui'; export function concatNewLine(prefix: string, suffix: string) { return prefix.trim().length ? `${prefix}
${suffix}` : suffix; @@ -48,33 +45,3 @@ export function matchFilter( } }; } - -export function extractOneOfElements(from: OpenJSONSchema): OpenJSONSchema[] { - if (!from.oneOf?.length) { - return []; - } - - const elements = from.oneOf.filter(Boolean) as OpenJSONSchema[]; - - return elements; -} - -export function descriptionForOneOfElement(target: OpenJSONSchema): string { - const elements = extractOneOfElements(target); - - if (elements.length === 0) { - return ''; - } - - const description = elements - .map((item) => createOneOfDescription(item)) - .filter(Boolean) - .join('\nor '); - - return description; -} - -export function createOneOfDescription(item: OpenJSONSchema): string | undefined { - const ref = RefsService.find(item); - return ref ? anchor(ref) : item.description; -}