From 3ff055ec453c7ec6b5991f4b1b34a90a38a7bd33 Mon Sep 17 00:00:00 2001 From: Daniel Kuznetsov Date: Sun, 16 Jun 2024 18:36:50 +0200 Subject: [PATCH 1/2] feat: sort parameters and object schema props, hoise required ones to the top --- jest.config.js | 4 +- jest.snapshots.js | 4 +- package-lock.json | 10 +- package.json | 2 + src/__snapshots__/basic.test.ts.snap | 8 +- .../combiners/allOf.test.ts.snap | 22 +- .../combiners/complex.test.ts.snap | 36 +-- .../combiners/oneOf.test.ts.snap | 40 +-- src/__snapshots__/description.test.ts.snap | 8 +- .../description/description.test.ts.snap | 89 ------- src/__snapshots__/length.test.ts.snap | 20 +- .../objectPropertyOrder.test.ts.snap | 243 ++++++++++++++++++ src/__snapshots__/parameterOrder.test.ts.snap | 179 +++++++++++++ src/__snapshots__/required.test.ts.snap | 34 ++- .../required/required.test.ts.snap | 89 ------- src/__tests__/__helpers__/run.ts | 2 +- src/__tests__/objectPropertyOrder.test.ts | 80 ++++++ src/__tests__/parameterOrder.test.ts | 80 ++++++ src/includer/traverse/tables.ts | 12 +- src/includer/ui/common.ts | 2 +- src/includer/ui/endpoint.ts | 83 ++++-- .../orderedProps/getOrderedPropList.test.ts | 20 ++ .../orderedProps/getOrderedPropList.ts | 45 ++++ .../orderedProps/hoistRequired.test.ts | 29 +++ .../orderedProps/hoistRequired.ts | 25 ++ .../ui/presentationUtils/partitionParams.ts | 0 26 files changed, 886 insertions(+), 280 deletions(-) delete mode 100644 src/__snapshots__/description/description.test.ts.snap create mode 100644 src/__snapshots__/objectPropertyOrder.test.ts.snap create mode 100644 src/__snapshots__/parameterOrder.test.ts.snap delete mode 100644 src/__snapshots__/required/required.test.ts.snap create mode 100644 src/__tests__/objectPropertyOrder.test.ts create mode 100644 src/__tests__/parameterOrder.test.ts create mode 100644 src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.test.ts create mode 100644 src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.ts create mode 100644 src/includer/ui/presentationUtils/orderedProps/hoistRequired.test.ts create mode 100644 src/includer/ui/presentationUtils/orderedProps/hoistRequired.ts create mode 100644 src/includer/ui/presentationUtils/partitionParams.ts diff --git a/jest.config.js b/jest.config.js index dd24575..4af4544 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testRegex: '.test.ts$', - rootDir: 'src/__tests__', - snapshotResolver: '../../jest.snapshots.js', + rootDir: '.', + snapshotResolver: './jest.snapshots.js', }; diff --git a/jest.snapshots.js b/jest.snapshots.js index e0c7597..c93aeb6 100644 --- a/jest.snapshots.js +++ b/jest.snapshots.js @@ -2,12 +2,12 @@ module.exports = { testPathForConsistencyCheck: 'src/__tests__/basic.test.ts', resolveSnapshotPath: (testPath, snapshotExtension) => { - return testPath.replace('src/__tests__', 'src/__snapshots__/') + snapshotExtension; + return testPath.replace('src/__tests__', 'src/__snapshots__') + snapshotExtension; }, resolveTestPath: (snapshotFilePath, snapshotExtension) => { return snapshotFilePath - .replace('src/__snapshots__/', 'src/__tests__') + .replace('src/__snapshots__', 'src/__tests__') .slice(0, -snapshotExtension.length); }, }; diff --git a/package-lock.json b/package-lock.json index 7943d24..e77ca4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,14 +29,16 @@ "@types/js-yaml": "^4.0.5", "@types/json-schema": "^7.0.11", "@types/json-stringify-safe": "^5.0.0", + "@types/lodash": "^4.17.5", "@types/markdown-it": "^13.0.7", "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", "esbuild": "^0.19.10", "esbuild-sass-plugin": "^2.16.1", - "glob": "^8.1.0", + "glob": "^8.0.3", "jest": "^29.5.0", "jest-when": "^3.5.2", + "lodash": "^4.17.21", "markdown-it": "^13.0.2", "npm-run-all": "^4.1.5", "openapi-types": "^12.1.3", @@ -2901,6 +2903,12 @@ "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "dev": true + }, "node_modules/@types/markdown-it": { "version": "13.0.7", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz", diff --git a/package.json b/package.json index b3e0a1e..a32beea 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/js-yaml": "^4.0.5", "@types/json-schema": "^7.0.11", "@types/json-stringify-safe": "^5.0.0", + "@types/lodash": "^4.17.5", "@types/markdown-it": "^13.0.7", "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", @@ -74,6 +75,7 @@ "glob": "^8.0.3", "jest": "^29.5.0", "jest-when": "^3.5.2", + "lodash": "^4.17.21", "markdown-it": "^13.0.2", "npm-run-all": "^4.1.5", "openapi-types": "^12.1.3", diff --git a/src/__snapshots__/basic.test.ts.snap b/src/__snapshots__/basic.test.ts.snap index 601330a..56d1c0e 100644 --- a/src/__snapshots__/basic.test.ts.snap +++ b/src/__snapshots__/basic.test.ts.snap @@ -61,7 +61,7 @@ Base 200 response || || - type + foo | **Type:** string @@ -70,7 +70,7 @@ Base 200 response || || - foo + type | **Type:** string @@ -113,7 +113,7 @@ Base 200 response || || - type + bar | **Type:** string @@ -122,7 +122,7 @@ Base 200 response || || - bar + type | **Type:** string diff --git a/src/__snapshots__/combiners/allOf.test.ts.snap b/src/__snapshots__/combiners/allOf.test.ts.snap index 22b0d68..b660788 100644 --- a/src/__snapshots__/combiners/allOf.test.ts.snap +++ b/src/__snapshots__/combiners/allOf.test.ts.snap @@ -54,7 +54,7 @@ Generated server url || || - type + foo | **Type:** string @@ -63,7 +63,7 @@ Generated server url || || - foo + name | **Type:** string @@ -72,7 +72,7 @@ Generated server url || || - name + type | **Type:** string @@ -115,24 +115,24 @@ Base 200 response || || - name + baz | **Type:** string -Default: \`a\` + + || || - type + name | **Type:** string - - +Default: \`a\` || || - baz + type | **Type:** string @@ -226,7 +226,7 @@ Cat class || || - type + foo | **Type:** string @@ -235,7 +235,7 @@ Cat class || || - foo + type | **Type:** string diff --git a/src/__snapshots__/combiners/complex.test.ts.snap b/src/__snapshots__/combiners/complex.test.ts.snap index 21312e1..0b78406 100644 --- a/src/__snapshots__/combiners/complex.test.ts.snap +++ b/src/__snapshots__/combiners/complex.test.ts.snap @@ -57,20 +57,20 @@ Generated server url || || - name + age | - **Type:** string + **Type:** any -Default: \`b\` + + || || - age + name | - **Type:** any - + **Type:** string - +Default: \`b\` || || @@ -104,7 +104,7 @@ Dog class || || - type + bar | **Type:** string @@ -113,7 +113,7 @@ Dog class || || - bar + type | **Type:** string @@ -136,7 +136,7 @@ Cat class || || - type + foo | **Type:** string @@ -145,7 +145,7 @@ Cat class || || - foo + type | **Type:** string @@ -192,20 +192,20 @@ Base 200 response || || - name + age | - **Type:** string + **Type:** any -Default: \`b\` + + || || - age + name | - **Type:** any - + **Type:** string - +Default: \`b\` || || diff --git a/src/__snapshots__/combiners/oneOf.test.ts.snap b/src/__snapshots__/combiners/oneOf.test.ts.snap index 292ceca..3eb6314 100644 --- a/src/__snapshots__/combiners/oneOf.test.ts.snap +++ b/src/__snapshots__/combiners/oneOf.test.ts.snap @@ -83,7 +83,7 @@ Dog class || || - type + baz | **Type:** string @@ -92,7 +92,7 @@ Dog class || || - baz + type | **Type:** string @@ -115,7 +115,7 @@ Cat class || || - type + foo | **Type:** string @@ -124,7 +124,7 @@ Cat class || || - foo + type | **Type:** string @@ -243,18 +243,18 @@ Generated server url || || - name + age | - **Type:** string + **Type:** number || || - age + name | - **Type:** number + **Type:** string @@ -291,7 +291,7 @@ Dog class || || - type + baz | **Type:** string @@ -300,7 +300,7 @@ Dog class || || - baz + type | **Type:** string @@ -323,7 +323,7 @@ Cat class || || - type + foo | **Type:** string @@ -332,7 +332,7 @@ Cat class || || - foo + type | **Type:** string @@ -375,18 +375,18 @@ Cat class || || - name + age | - **Type:** string + **Type:** number || || - age + name | - **Type:** number + **Type:** string @@ -513,7 +513,7 @@ Dog class || || - type + baz | **Type:** string @@ -522,7 +522,7 @@ Dog class || || - baz + type | **Type:** string @@ -545,7 +545,7 @@ Cat class || || - type + foo | **Type:** string @@ -554,7 +554,7 @@ Cat class || || - foo + type | **Type:** string diff --git a/src/__snapshots__/description.test.ts.snap b/src/__snapshots__/description.test.ts.snap index 1790d29..4b80988 100644 --- a/src/__snapshots__/description.test.ts.snap +++ b/src/__snapshots__/description.test.ts.snap @@ -120,7 +120,7 @@ Cat class || || - type + foo | **Type:** string @@ -129,7 +129,7 @@ Cat class || || - foo + type | **Type:** string @@ -152,7 +152,7 @@ Dog class || || - type + bar | **Type:** string @@ -161,7 +161,7 @@ Dog class || || - bar + type | **Type:** string diff --git a/src/__snapshots__/description/description.test.ts.snap b/src/__snapshots__/description/description.test.ts.snap deleted file mode 100644 index e246399..0000000 --- a/src/__snapshots__/description/description.test.ts.snap +++ /dev/null @@ -1,89 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`description renders correct description 1`] = ` -"
- -# description - -## Request - -#||| **method** | **url** | **description** || - -|| -\`\`\` -POST -\`\`\` - | -\`\`\` -http://localhost:8080/test -\`\`\` - | -\`\`\` -Generated server url -\`\`\` - |||# - -## Responses - -## 200 OK - -#### Body - -{% cut "application/json" %} - - -\`\`\` -{ - "pet": { - "type": "string", - "foo": "string" - }, - "petWithoutDescription": { - "type": "string", - "foo": "string" - }, - "refToSchemaWithDescription": { - "type": "string", - "bar": "string" - }, - "simpleDescription": {} -} -\`\`\` - - -{% endcut %} - - -#||| **Name** | **Type** | **Description** || - -|| pet | [Cat](#cat) | From response || - -|| petWithoutDescription | [Cat](#cat) | Cat class || - -|| refToSchemaWithDescription | [Dog](#dog) | Dog class || - -|| simpleDescription | object | Simple description |||# - -### Cat - -Cat class - -#||| **Name** | **Type** | **Description** || - -|| type | string | || - -|| foo | string | |||# - -### Dog - -Dog class - -#||| **Name** | **Type** | **Description** || - -|| type | string | || - -|| bar | string | |||# - - -
" -`; diff --git a/src/__snapshots__/length.test.ts.snap b/src/__snapshots__/length.test.ts.snap index edb7684..46155de 100644 --- a/src/__snapshots__/length.test.ts.snap +++ b/src/__snapshots__/length.test.ts.snap @@ -110,20 +110,20 @@ Cat class || || - name + foo | **Type:** string - - +Min length: \`3\` || || - foo + name | **Type:** string -Min length: \`3\` + + |||# @@ -141,20 +141,20 @@ Dog class || || - name + bar | **Type:** string -Pet name -
Max length: \`100\` +Min length: \`1\`
Max length: \`99\` || || - bar + name | **Type:** string -Min length: \`1\`
Max length: \`99\` +Pet name +
Max length: \`100\` |||# diff --git a/src/__snapshots__/objectPropertyOrder.test.ts.snap b/src/__snapshots__/objectPropertyOrder.test.ts.snap new file mode 100644 index 0000000..6217bab --- /dev/null +++ b/src/__snapshots__/objectPropertyOrder.test.ts.snap @@ -0,0 +1,243 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Property rows in tables describing object schemas are hoisted to the top of the table if marked as required by the spec 1`] = ` +"
+ +# ObjectPropertyOrder + +## Request + +
+ +
+ +
+ +POST {.openapi__method} +\`\`\` +http://localhost:8080/test +\`\`\` + + + +
+ +Generated server url + +
+ +
+ +
+ +### Body + +{% cut "application/json" %} + + +\`\`\`json +{ + "id": "c3073b9d-edd0-49f2-a28d-b7ded8ff9a8b", + "luminosityClass": "string", + "name": "string", + "catalogueDesignationCCDM": "string" +} +\`\`\` + + +{% endcut %} + + +#||| + **Name** +| + **Description** +|| + +|| + id* +| + **Type:** string<uuid> + +Internal ID for this star + +|| + +|| + luminosityClass* +| + **Type:** string + +Morgan-Keenan luminosity class for this star + +|| + +|| + name* +| + **Type:** string + +Name of this star + +|| + +|| + catalogueDesignationCCDM +| + **Type:** string + +CCDM catalogue designation for this star + +|||# + +
+ +## Responses + +
+ +## 204 No Content + +
+ +### Body + +{% cut "application/json" %} + + +\`\`\`json +{} +\`\`\` + + +{% endcut %} + + +
+ +
+ + +
" +`; + +exports[`Property rows in tables describing object schemas are ordered lexicographically by default 1`] = ` +"
+ +# ObjectPropertyOrder + +## Request + +
+ +
+ +
+ +POST {.openapi__method} +\`\`\` +http://localhost:8080/test +\`\`\` + + + +
+ +Generated server url + +
+ +
+ +
+ +### Body + +{% cut "application/json" %} + + +\`\`\`json +{ + "id": "c3073b9d-edd0-49f2-a28d-b7ded8ff9a8b", + "luminosityClass": "string", + "name": "string", + "catalogueDesignationCCDM": "string" +} +\`\`\` + + +{% endcut %} + + +#||| + **Name** +| + **Description** +|| + +|| + catalogueDesignationCCDM +| + **Type:** string + +CCDM catalogue designation for this star + +|| + +|| + id +| + **Type:** string<uuid> + +Internal ID for this star + +|| + +|| + luminosityClass +| + **Type:** string + +Morgan-Keenan luminosity class for this star + +|| + +|| + name +| + **Type:** string + +Name of this star + +|||# + +
+ +## Responses + +
+ +## 204 No Content + +
+ +### Body + +{% cut "application/json" %} + + +\`\`\`json +{} +\`\`\` + + +{% endcut %} + + +
+ +
+ + +
" +`; diff --git a/src/__snapshots__/parameterOrder.test.ts.snap b/src/__snapshots__/parameterOrder.test.ts.snap new file mode 100644 index 0000000..090aba2 --- /dev/null +++ b/src/__snapshots__/parameterOrder.test.ts.snap @@ -0,0 +1,179 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Endpoint parameters in tables are hoisted to the top of the table if marked as required by the spec 1`] = ` +"
+ +# ParameterOrder + +## Request + +
+ +
+ +
+ +POST {.openapi__method} +\`\`\` +http://localhost:8080/test +\`\`\` + + + +
+ +Generated server url + +
+ +
+ +### Query parameters + +#||| + **Name** +| + **Description** +|| + +|| + id* +| + **Type:** string<uuid> + +Internal ID for the requested star +|| + +|| + catalogueCCDM +| + **Type:** string + +CCDM designation for the requested star +|| + +|| + name +| + **Type:** string + +Name for the requested star +|||# + +## Responses + +
+ +## 204 No Content + +
+ +### Body + +{% cut "application/json" %} + + +\`\`\`json +{} +\`\`\` + + +{% endcut %} + + +
+ +
+ + +
" +`; + +exports[`Endpoint parameters in tables are ordered lexicographically by default 1`] = ` +"
+ +# ParameterOrder + +## Request + +
+ +
+ +
+ +POST {.openapi__method} +\`\`\` +http://localhost:8080/test +\`\`\` + + + +
+ +Generated server url + +
+ +
+ +### Query parameters + +#||| + **Name** +| + **Description** +|| + +|| + catalogueCCDM +| + **Type:** string + +CCDM designation for the requested star +|| + +|| + id +| + **Type:** string<uuid> + +Internal ID for the requested star +|| + +|| + name +| + **Type:** string + +Name for the requested star +|||# + +## Responses + +
+ +## 204 No Content + +
+ +### Body + +{% cut "application/json" %} + + +\`\`\`json +{} +\`\`\` + + +{% endcut %} + + +
+ +
+ + +
" +`; diff --git a/src/__snapshots__/required.test.ts.snap b/src/__snapshots__/required.test.ts.snap index 130f1e7..b15651f 100644 --- a/src/__snapshots__/required.test.ts.snap +++ b/src/__snapshots__/required.test.ts.snap @@ -28,6 +28,32 @@ Generated server url +### Query parameters + +#||| + **Name** +| + **Description** +|| + +|| + c* +| + **Type:** number +|| + +|| + a +| + **Type:** number +|| + +|| + b +| + **Type:** number +|||# +
### Body @@ -71,7 +97,7 @@ Generated server url || || - c + d* | **Type:** number @@ -80,7 +106,7 @@ Generated server url || || - d* + c | **Type:** number @@ -132,7 +158,7 @@ Cat class || || - type + foo | **Type:** string @@ -141,7 +167,7 @@ Cat class || || - foo + type | **Type:** string diff --git a/src/__snapshots__/required/required.test.ts.snap b/src/__snapshots__/required/required.test.ts.snap deleted file mode 100644 index b1b06f9..0000000 --- a/src/__snapshots__/required/required.test.ts.snap +++ /dev/null @@ -1,89 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`description renders correct description 1`] = ` -"
- -# TEST ENDPOINT - -## Request - -#||| **method** | **url** | **description** || - -|| -\`\`\` -POST -\`\`\` - | -\`\`\` -http://localhost:8080/description -\`\`\` - | -\`\`\` -Generated server url -\`\`\` - |||# - -### Query parameters - -#||| **Name** | **Type** | **Description** || - -|| a | number | || - -|| b | number | || - -|| c* | number | |||# - -#### Body - -{% cut "application/json" %} - - -\`\`\` -{ - "a": 0, - "b": 0, - "c": 0 -} -\`\`\` - - -{% endcut %} - - -#||| **Name** | **Type** | **Description** || - -|| a* | number | || - -|| b | number | || - -|| c | number | |||# - -## Responses - -## 200 OK - -#### Body - -{% cut "application/json" %} - - -\`\`\` -{ - "type": "string", - "foo": "string" -} -\`\`\` - - -{% endcut %} - - -#||| **Name** | **Type** | **Description** || - -|| type | string | || - -|| foo | string | |||# - - -
" -`; diff --git a/src/__tests__/__helpers__/run.ts b/src/__tests__/__helpers__/run.ts index 11ed6ed..ee64aeb 100644 --- a/src/__tests__/__helpers__/run.ts +++ b/src/__tests__/__helpers__/run.ts @@ -99,8 +99,8 @@ export class DocumentBuilder { requestBody: this.requestBody, operationId: this.id, responses: Object.fromEntries(this.responses), + parameters: this.parameters, }, - parameters: this.parameters, }, }, components: { diff --git a/src/__tests__/objectPropertyOrder.test.ts b/src/__tests__/objectPropertyOrder.test.ts new file mode 100644 index 0000000..b344cea --- /dev/null +++ b/src/__tests__/objectPropertyOrder.test.ts @@ -0,0 +1,80 @@ +import {DocumentBuilder, run} from './__helpers__/run'; + +const mockDocumentName = 'ObjectPropertyOrder'; + +describe('Property rows in tables describing object schemas', () => { + it('are ordered lexicographically by default', async () => { + const spec = new DocumentBuilder(mockDocumentName) + .component('StarDto', { + type: 'object', + properties: { + id: { + description: 'Internal ID for this star', + type: 'string', + format: 'uuid', + }, + luminosityClass: { + description: 'Morgan-Keenan luminosity class for this star', + type: 'string', + }, + name: { + description: 'Name of this star', + type: 'string', + }, + catalogueDesignationCCDM: { + description: 'CCDM catalogue designation for this star', + type: 'string', + }, + }, + }) + .request({ + schema: DocumentBuilder.ref('StarDto'), + }) + .response(204, {}) + .build(); + + const fs = await run(spec); + + const page = fs.match(mockDocumentName); + + expect(page).toMatchSnapshot(); + }); + + it('are hoisted to the top of the table if marked as required by the spec', async () => { + const spec = new DocumentBuilder(mockDocumentName) + .component('StarDto', { + type: 'object', + properties: { + id: { + description: 'Internal ID for this star', + type: 'string', + format: 'uuid', + }, + luminosityClass: { + description: 'Morgan-Keenan luminosity class for this star', + type: 'string', + }, + name: { + description: 'Name of this star', + type: 'string', + }, + catalogueDesignationCCDM: { + description: 'CCDM catalogue designation for this star', + type: 'string', + }, + }, + required: ['id', 'name', 'luminosityClass'], + }) + .request({ + schema: DocumentBuilder.ref('StarDto'), + }) + .response(204, {}) + .build(); + + const fs = await run(spec); + + const page = fs.match(mockDocumentName); + + expect(page).toMatchSnapshot(); + }); +}); diff --git a/src/__tests__/parameterOrder.test.ts b/src/__tests__/parameterOrder.test.ts new file mode 100644 index 0000000..b6a63aa --- /dev/null +++ b/src/__tests__/parameterOrder.test.ts @@ -0,0 +1,80 @@ +import {DocumentBuilder, run} from './__helpers__/run'; + +const mockDocumentName = 'ParameterOrder'; + +describe('Endpoint parameters in tables', () => { + it('are ordered lexicographically by default', async () => { + const spec = new DocumentBuilder(mockDocumentName) + .parameter({ + in: 'query', + name: 'name', + description: 'Name for the requested star', + schema: { + type: 'string', + }, + }) + .parameter({ + in: 'query', + name: 'id', + description: 'Internal ID for the requested star', + schema: { + type: 'string', + format: 'uuid', + }, + }) + .parameter({ + in: 'query', + name: 'catalogueCCDM', + description: 'CCDM designation for the requested star', + schema: { + type: 'string', + }, + }) + .response(204, {}) + .build(); + + const fs = await run(spec); + + const page = fs.match(mockDocumentName); + + expect(page).toMatchSnapshot(); + }); + + it('are hoisted to the top of the table if marked as required by the spec', async () => { + const spec = new DocumentBuilder(mockDocumentName) + .parameter({ + in: 'query', + name: 'name', + description: 'Name for the requested star', + schema: { + type: 'string', + }, + }) + .parameter({ + in: 'query', + name: 'id', + required: true, + description: 'Internal ID for the requested star', + schema: { + type: 'string', + format: 'uuid', + }, + }) + .parameter({ + in: 'query', + name: 'catalogueCCDM', + description: 'CCDM designation for the requested star', + schema: { + type: 'string', + }, + }) + .response(204, {}) + .build(); + + const fs = await run(spec); + + const page = fs.match(mockDocumentName); + + expect(page).toMatchSnapshot(); + }); +}); diff --git a/src/includer/traverse/tables.ts b/src/includer/traverse/tables.ts index 6657287..1823248 100644 --- a/src/includer/traverse/tables.ts +++ b/src/includer/traverse/tables.ts @@ -6,6 +6,7 @@ import {concatNewLine} from '../utils'; import {OpenJSONSchema, OpenJSONSchemaDefinition} from '../models'; import {collectRefs, extractOneOfElements, inferType, typeToText} from './types'; import {prepareComplexDescription} from './description'; +import {getOrderedParamOrPropList} from '../ui/presentationUtils/orderedProps/getOrderedPropList'; type TableRow = [string, string]; @@ -56,7 +57,16 @@ function prepareObjectSchemaTable(schema: OpenJSONSchema): PrepareObjectSchemaTa const result: PrepareObjectSchemaTableResult = {rows: [], refs: []}; - Object.entries(merged.properties || {}).forEach(([key, v]) => { + const wellOrderedProperties = getOrderedParamOrPropList({ + propList: Object.entries(merged.properties || {}), + iteratee: ([propName]) => ({ + paramOrPropName: propName, + isRequired: isRequired(propName, merged), + }), + shouldApplyLexSort: true, + }); + + wellOrderedProperties.forEach(([key, v]) => { const value = RefsService.merge(v); const name = tableParameterName(key, isRequired(key, merged)); const {type, description, ref, runtimeRef} = prepareTableRowData(value, key, tableRef); diff --git a/src/includer/ui/common.ts b/src/includer/ui/common.ts index 42a151d..98da6d2 100644 --- a/src/includer/ui/common.ts +++ b/src/includer/ui/common.ts @@ -32,7 +32,7 @@ function link(text: string, src: string) { } function title(depth: TitleDepth) { - return (content?: string) => content?.length && '#'.repeat(depth) + ` ${content}`; + return (content?: string) => (content?.length ? '#'.repeat(depth) + ` ${content}` : ''); } function body(text?: string) { diff --git a/src/includer/ui/endpoint.ts b/src/includer/ui/endpoint.ts index f4fe36e..3163b01 100644 --- a/src/includer/ui/endpoint.ts +++ b/src/includer/ui/endpoint.ts @@ -2,6 +2,8 @@ import stringify from 'json-stringify-safe'; import RefsService from '../services/refs'; import {dump} from 'js-yaml'; +import groupBy from 'lodash/groupBy'; + import { COOKIES_SECTION_NAME, HEADERS_SECTION_NAME, @@ -23,6 +25,7 @@ import { import { Endpoint, + In, OpenJSONSchema, Parameter, Parameters, @@ -49,6 +52,7 @@ import { tabs, title, } from './common'; +import {getOrderedParamOrPropList} from './presentationUtils/orderedProps/getOrderedPropList'; function endpoint(data: Endpoint, sandboxPlugin: {host?: string; tabName?: string} | undefined) { // try to remember, which tables we are already printed on page @@ -161,6 +165,18 @@ function request(data: Endpoint) { return block(result); } +function getParameterSourceTableContents(parameterList: readonly Parameter[]) { + const rowsAndRefs = parameterList.map((param) => parameterRow(param)); + + const additionalRefs = rowsAndRefs + .flatMap(({ref}) => ref) + .filter((maybeRef): maybeRef is string => typeof maybeRef !== 'undefined'); + + const contentRows = rowsAndRefs.map(({cells}) => cells); + + return {additionalRefs, contentRows}; +} + function parameters(pagePrintedRefs: Set, params?: Parameters) { const sections = { path: PATH_PARAMETERS_SECTION_NAME, @@ -168,26 +184,47 @@ function parameters(pagePrintedRefs: Set, params?: Parameters) { header: HEADERS_SECTION_NAME, cookie: COOKIES_SECTION_NAME, }; - const tables = []; - for (const [inValue, heading] of Object.entries(sections)) { - const inParams = params?.filter((param: Parameter) => param.in === inValue); - if (inParams?.length) { - const rows: string[][] = []; - const tableRefs: TableRef[] = []; - for (const param of inParams) { - const {cells, ref} = parameterRow(param); - rows.push(cells); - if (ref) { - // there may be enums, which should be printed in separate tables - tableRefs.push(...ref); - } - } - tables.push(title(3)(heading)); - tables.push(table([['Name', 'Description'], ...rows])); - tables.push(...printAllTables(pagePrintedRefs, tableRefs)); - } - } - return block(tables); + + const partitionedParameters = groupBy(params, (parameterSpec) => parameterSpec.in) as Record< + In, + Parameter[] | undefined + >; + + const content = Object.keys(sections) + .map( + (parameterSource) => + [ + parameterSource as In, + partitionedParameters[parameterSource as In] ?? [], + ] as const, + ) + .filter(([, parameterList]) => parameterList.length) + .reduce((contentAccumulator, [parameterSource, parameterList]) => { + const wellOrderedParameters = getOrderedParamOrPropList({ + propList: parameterList, + iteratee: ({name, required}) => ({ + paramOrPropName: name, + // required can actually be `undefined` in runtime + isRequired: Boolean(required), + }), + shouldApplyLexSort: true, + }); + + const {contentRows, additionalRefs} = + getParameterSourceTableContents(wellOrderedParameters); + + const tableHeading = sections[parameterSource]; + + contentAccumulator.push( + title(3)(tableHeading), + table([['Name', 'Description'], ...contentRows]), + ...printAllTables(pagePrintedRefs, additionalRefs), + ); + + return contentAccumulator; + }, []); + + return block(content); } function parameterRow(param: Parameter): {cells: string[]; ref?: TableRef[]} { @@ -220,15 +257,15 @@ function openapiBody(pagePrintedRefs: Set, obj?: Schema) { const {type = 'schema', schema} = obj; const sectionTitle = title(3)('Body'); - let result: any[] = [sectionTitle]; + let result: (string | null)[] = [sectionTitle]; if (isPrimitive(schema.type)) { result = [ ...result, type, `${bold('Type:')} ${schema.type}`, - schema.format && `${bold('Format:')} ${schema.format}`, - schema.description && `${bold('Description:')} ${schema.description}`, + schema.format ? `${bold('Format:')} ${schema.format}` : null, + schema.description ? `${bold('Description:')} ${schema.description}` : null, ]; return block(result); diff --git a/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.test.ts b/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.test.ts new file mode 100644 index 0000000..737e1e0 --- /dev/null +++ b/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.test.ts @@ -0,0 +1,20 @@ +import {getOrderedParamOrPropList} from './getOrderedPropList'; + +const mockIteratee = (s: string) => ({ + paramOrPropName: s.replace(/\*$/, ''), + isRequired: s.endsWith('*'), +}); + +describe('getOrderedPropList helper function', () => { + it('preserves lexicographic order even after hoisting the required entries', () => { + const mockElements = ['traits', 'weight*', 'id*', 'species', 'xenoClass*']; + + const ordered = getOrderedParamOrPropList({ + propList: mockElements, + iteratee: mockIteratee, + shouldApplyLexSort: true, + }); + + expect(ordered).toEqual(['id*', 'weight*', 'xenoClass*', 'species', 'traits']); + }); +}); diff --git a/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.ts b/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.ts new file mode 100644 index 0000000..eb5691e --- /dev/null +++ b/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.ts @@ -0,0 +1,45 @@ +import sortBy from 'lodash/sortBy'; +import {hoistRequiredParamsOrProps} from './hoistRequired'; + +type IterateeReturn = { + paramOrPropName: string; + isRequired: boolean; +}; + +type Iteratee = (listElement: T) => IterateeReturn; + +type GetOrderedParamOrPropListArguments = { + /** + * List of parameters/object schema props to process and order. + */ + propList: readonly T[]; + /** + * A getter function to resolve whether a param/prop is required, as well as its name. + */ + iteratee: Iteratee; + /** + * Whether or not to apply lexicographic sort before hoisting the required properties. + */ + shouldApplyLexSort: boolean; +}; + +/** + * Get a well-ordered list of parameters/object schema props (i.e., required params/props hoisted to + * the start of the list, lexicographic sort applied as necessary). + * @param {GetOrderedParamOrPropListArguments} param0 Arguments for the operation. + * @returns {ReadonlyArray} The resulting well-ordered list. + */ +export const getOrderedParamOrPropList = ({ + propList, + iteratee, + shouldApplyLexSort, +}: GetOrderedParamOrPropListArguments): readonly T[] => { + const preprocessed = shouldApplyLexSort + ? sortBy(propList, (listElement) => iteratee(listElement).paramOrPropName) + : propList; + + return hoistRequiredParamsOrProps( + preprocessed, + (listElement) => iteratee(listElement).isRequired, + ); +}; diff --git a/src/includer/ui/presentationUtils/orderedProps/hoistRequired.test.ts b/src/includer/ui/presentationUtils/orderedProps/hoistRequired.test.ts new file mode 100644 index 0000000..d1c4639 --- /dev/null +++ b/src/includer/ui/presentationUtils/orderedProps/hoistRequired.test.ts @@ -0,0 +1,29 @@ +import {hoistRequiredParamsOrProps} from './hoistRequired'; + +const mockIsRequiredGetter = (s: string) => s.endsWith('*'); + +describe('hoistRequired helper function', () => { + it('preserves original order when all original properties are optional', () => { + const mockElements = ['idLte', 'idGte', 'nameLte', 'nameGte', 'limit']; + + const ordered = hoistRequiredParamsOrProps(mockElements, mockIsRequiredGetter); + + expect(ordered).toEqual(mockElements); + }); + + it('preserves original order when all original properties are required', () => { + const mockElements = ['id*', 'fullName*', 'salary*', 'dept*']; + + const ordered = hoistRequiredParamsOrProps(mockElements, mockIsRequiredGetter); + + expect(ordered).toEqual(mockElements); + }); + + it('does actually hoist required properties to the top of the list', () => { + const mockElements = ['catName*', 'hasThoughts', 'isLazy', 'breed*', 'likesCatnip*']; + + const ordered = hoistRequiredParamsOrProps(mockElements, mockIsRequiredGetter); + + expect(ordered).toEqual(['catName*', 'breed*', 'likesCatnip*', 'hasThoughts', 'isLazy']); + }); +}); diff --git a/src/includer/ui/presentationUtils/orderedProps/hoistRequired.ts b/src/includer/ui/presentationUtils/orderedProps/hoistRequired.ts new file mode 100644 index 0000000..f3cbbe0 --- /dev/null +++ b/src/includer/ui/presentationUtils/orderedProps/hoistRequired.ts @@ -0,0 +1,25 @@ +type IsRequiredGetter = (element: T) => boolean; + +const makeSortComparator = + (isRequiredGetter: IsRequiredGetter) => + (lhs: T, rhs: T): number => { + const [isLhsRequired, isRhsRequired] = [lhs, rhs].map(isRequiredGetter).map(Boolean); + + // In this scenario, we define an element's "magnitude" as 1 if it's required, + // 0 otherwise. Normal math comparison then can be applied. + // However, the sort order should be reversed, since bigger (required) + // elements should come first in the resulting list, hence we use |rhs| - |lhs| + return Number(isRhsRequired) - Number(isLhsRequired); + }; + +/** + * Hoist required parameters or object properties, preserving the original order otherwise. + * @param {ReadonlyArray} propList A list of params/properties that need to be ordered. + * @param {IsRequiredGetter} isRequiredGetter A function which will be called on each element of + * `propList` to determine whether a property is required or not. + * @returns {ReadonlyArray} Ordered prop/param list. + */ +export const hoistRequiredParamsOrProps = ( + propList: readonly T[], + isRequiredGetter: IsRequiredGetter, +): readonly T[] => [...propList].sort(makeSortComparator(isRequiredGetter)); diff --git a/src/includer/ui/presentationUtils/partitionParams.ts b/src/includer/ui/presentationUtils/partitionParams.ts new file mode 100644 index 0000000..e69de29 From fff0aa9378a752571d1283b9b3db84b73ec616ac Mon Sep 17 00:00:00 2001 From: Daniel Kuznetsov Date: Fri, 21 Jun 2024 22:30:48 +0200 Subject: [PATCH 2/2] fix: small refactor after code review --- src/includer/traverse/tables.ts | 7 +++--- src/includer/ui/endpoint.ts | 7 +++--- .../orderedProps/getOrderedPropList.test.ts | 6 ++--- .../orderedProps/getOrderedPropList.ts | 23 ++++++++----------- .../orderedProps/hoistRequired.test.ts | 8 +++---- .../orderedProps/hoistRequired.ts | 2 +- 6 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/includer/traverse/tables.ts b/src/includer/traverse/tables.ts index 1823248..204504a 100644 --- a/src/includer/traverse/tables.ts +++ b/src/includer/traverse/tables.ts @@ -6,7 +6,7 @@ import {concatNewLine} from '../utils'; import {OpenJSONSchema, OpenJSONSchemaDefinition} from '../models'; import {collectRefs, extractOneOfElements, inferType, typeToText} from './types'; import {prepareComplexDescription} from './description'; -import {getOrderedParamOrPropList} from '../ui/presentationUtils/orderedProps/getOrderedPropList'; +import {getOrderedPropList} from '../ui/presentationUtils/orderedProps/getOrderedPropList'; type TableRow = [string, string]; @@ -57,13 +57,12 @@ function prepareObjectSchemaTable(schema: OpenJSONSchema): PrepareObjectSchemaTa const result: PrepareObjectSchemaTableResult = {rows: [], refs: []}; - const wellOrderedProperties = getOrderedParamOrPropList({ + const wellOrderedProperties = getOrderedPropList({ propList: Object.entries(merged.properties || {}), iteratee: ([propName]) => ({ - paramOrPropName: propName, + name: propName, isRequired: isRequired(propName, merged), }), - shouldApplyLexSort: true, }); wellOrderedProperties.forEach(([key, v]) => { diff --git a/src/includer/ui/endpoint.ts b/src/includer/ui/endpoint.ts index 3163b01..6562f0d 100644 --- a/src/includer/ui/endpoint.ts +++ b/src/includer/ui/endpoint.ts @@ -52,7 +52,7 @@ import { tabs, title, } from './common'; -import {getOrderedParamOrPropList} from './presentationUtils/orderedProps/getOrderedPropList'; +import {getOrderedPropList} from './presentationUtils/orderedProps/getOrderedPropList'; function endpoint(data: Endpoint, sandboxPlugin: {host?: string; tabName?: string} | undefined) { // try to remember, which tables we are already printed on page @@ -200,14 +200,13 @@ function parameters(pagePrintedRefs: Set, params?: Parameters) { ) .filter(([, parameterList]) => parameterList.length) .reduce((contentAccumulator, [parameterSource, parameterList]) => { - const wellOrderedParameters = getOrderedParamOrPropList({ + const wellOrderedParameters = getOrderedPropList({ propList: parameterList, iteratee: ({name, required}) => ({ - paramOrPropName: name, + name, // required can actually be `undefined` in runtime isRequired: Boolean(required), }), - shouldApplyLexSort: true, }); const {contentRows, additionalRefs} = diff --git a/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.test.ts b/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.test.ts index 737e1e0..0b41720 100644 --- a/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.test.ts +++ b/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.test.ts @@ -1,7 +1,7 @@ -import {getOrderedParamOrPropList} from './getOrderedPropList'; +import {getOrderedPropList} from './getOrderedPropList'; const mockIteratee = (s: string) => ({ - paramOrPropName: s.replace(/\*$/, ''), + name: s.replace(/\*$/, ''), isRequired: s.endsWith('*'), }); @@ -9,7 +9,7 @@ describe('getOrderedPropList helper function', () => { it('preserves lexicographic order even after hoisting the required entries', () => { const mockElements = ['traits', 'weight*', 'id*', 'species', 'xenoClass*']; - const ordered = getOrderedParamOrPropList({ + const ordered = getOrderedPropList({ propList: mockElements, iteratee: mockIteratee, shouldApplyLexSort: true, diff --git a/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.ts b/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.ts index eb5691e..bea25c9 100644 --- a/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.ts +++ b/src/includer/ui/presentationUtils/orderedProps/getOrderedPropList.ts @@ -1,14 +1,14 @@ import sortBy from 'lodash/sortBy'; -import {hoistRequiredParamsOrProps} from './hoistRequired'; +import {hoistRequired} from './hoistRequired'; type IterateeReturn = { - paramOrPropName: string; + name: string; isRequired: boolean; }; type Iteratee = (listElement: T) => IterateeReturn; -type GetOrderedParamOrPropListArguments = { +type GetOrderedListArguments = { /** * List of parameters/object schema props to process and order. */ @@ -20,26 +20,23 @@ type GetOrderedParamOrPropListArguments = { /** * Whether or not to apply lexicographic sort before hoisting the required properties. */ - shouldApplyLexSort: boolean; + shouldApplyLexSort?: boolean; }; /** * Get a well-ordered list of parameters/object schema props (i.e., required params/props hoisted to * the start of the list, lexicographic sort applied as necessary). - * @param {GetOrderedParamOrPropListArguments} param0 Arguments for the operation. + * @param {GetOrderedListArguments} param0 Arguments for the operation. * @returns {ReadonlyArray} The resulting well-ordered list. */ -export const getOrderedParamOrPropList = ({ +export const getOrderedPropList = ({ propList, iteratee, - shouldApplyLexSort, -}: GetOrderedParamOrPropListArguments): readonly T[] => { + shouldApplyLexSort = true, +}: GetOrderedListArguments): readonly T[] => { const preprocessed = shouldApplyLexSort - ? sortBy(propList, (listElement) => iteratee(listElement).paramOrPropName) + ? sortBy(propList, (listElement) => iteratee(listElement).name) : propList; - return hoistRequiredParamsOrProps( - preprocessed, - (listElement) => iteratee(listElement).isRequired, - ); + return hoistRequired(preprocessed, (listElement) => iteratee(listElement).isRequired); }; diff --git a/src/includer/ui/presentationUtils/orderedProps/hoistRequired.test.ts b/src/includer/ui/presentationUtils/orderedProps/hoistRequired.test.ts index d1c4639..45fc21c 100644 --- a/src/includer/ui/presentationUtils/orderedProps/hoistRequired.test.ts +++ b/src/includer/ui/presentationUtils/orderedProps/hoistRequired.test.ts @@ -1,4 +1,4 @@ -import {hoistRequiredParamsOrProps} from './hoistRequired'; +import {hoistRequired} from './hoistRequired'; const mockIsRequiredGetter = (s: string) => s.endsWith('*'); @@ -6,7 +6,7 @@ describe('hoistRequired helper function', () => { it('preserves original order when all original properties are optional', () => { const mockElements = ['idLte', 'idGte', 'nameLte', 'nameGte', 'limit']; - const ordered = hoistRequiredParamsOrProps(mockElements, mockIsRequiredGetter); + const ordered = hoistRequired(mockElements, mockIsRequiredGetter); expect(ordered).toEqual(mockElements); }); @@ -14,7 +14,7 @@ describe('hoistRequired helper function', () => { it('preserves original order when all original properties are required', () => { const mockElements = ['id*', 'fullName*', 'salary*', 'dept*']; - const ordered = hoistRequiredParamsOrProps(mockElements, mockIsRequiredGetter); + const ordered = hoistRequired(mockElements, mockIsRequiredGetter); expect(ordered).toEqual(mockElements); }); @@ -22,7 +22,7 @@ describe('hoistRequired helper function', () => { it('does actually hoist required properties to the top of the list', () => { const mockElements = ['catName*', 'hasThoughts', 'isLazy', 'breed*', 'likesCatnip*']; - const ordered = hoistRequiredParamsOrProps(mockElements, mockIsRequiredGetter); + const ordered = hoistRequired(mockElements, mockIsRequiredGetter); expect(ordered).toEqual(['catName*', 'breed*', 'likesCatnip*', 'hasThoughts', 'isLazy']); }); diff --git a/src/includer/ui/presentationUtils/orderedProps/hoistRequired.ts b/src/includer/ui/presentationUtils/orderedProps/hoistRequired.ts index f3cbbe0..d6547e4 100644 --- a/src/includer/ui/presentationUtils/orderedProps/hoistRequired.ts +++ b/src/includer/ui/presentationUtils/orderedProps/hoistRequired.ts @@ -19,7 +19,7 @@ const makeSortComparator = * `propList` to determine whether a property is required or not. * @returns {ReadonlyArray} Ordered prop/param list. */ -export const hoistRequiredParamsOrProps = ( +export const hoistRequired = ( propList: readonly T[], isRequiredGetter: IsRequiredGetter, ): readonly T[] => [...propList].sort(makeSortComparator(isRequiredGetter));