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..3b1617c
--- /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