Skip to content

Commit 27c5a9e

Browse files
authored
Add additional schema qualifiers (#112)
1 parent caa0e34 commit 27c5a9e

File tree

5 files changed

+279
-28
lines changed

5 files changed

+279
-28
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"scripts": {
1111
"prepare": "husky install",
1212
"watch": "lerna run watch --parallel",
13-
"watch:demo": "nodemon --watch \"./packages/*/lib/**/*.*\" --exec \"yarn start\"",
13+
"watch:demo": "nodemon --watch \"./packages/*/lib/**/*.*\" --exec \"yarn start --no-open\"",
1414
"build": "lerna run build",
1515
"build-packages": "lerna run build --no-private",
1616
"serve": "yarn workspace demo serve",

packages/docusaurus-plugin-openapi/src/markdown/createParamsTable.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { escape } from "lodash";
1010
import { ApiItem } from "../types";
1111
import { createDescription } from "./createDescription";
1212
import { createFullWidthTable } from "./createFullWidthTable";
13-
import { getSchemaName } from "./schema";
13+
import { getQualifierMessage, getSchemaName } from "./schema";
1414
import { create, guard } from "./utils";
1515

1616
interface Props {
@@ -64,25 +64,10 @@ export function createParamsTable({ parameters, type }: Props) {
6464
children: " REQUIRED",
6565
}),
6666
]),
67-
// TODO: This feels a little hacky. We should have a more resilient way to generate enum descriptions.
68-
guard(param.schema?.items?.enum, (options) =>
67+
guard(getQualifierMessage(param.schema), (message) =>
6968
create("div", {
7069
style: { marginTop: "var(--ifm-table-cell-padding)" },
71-
children: `Items Enum: ${options
72-
.map((option) =>
73-
create("code", { children: `"${option}"` })
74-
)
75-
.join(", ")}`,
76-
})
77-
),
78-
guard(param.schema?.enum, (options) =>
79-
create("div", {
80-
style: { marginTop: "var(--ifm-table-cell-padding)" },
81-
children: `Enum: ${options
82-
.map((option) =>
83-
create("code", { children: `"${option}"` })
84-
)
85-
.join(", ")}`,
70+
children: createDescription(message),
8671
})
8772
),
8873
guard(param.description, (description) =>

packages/docusaurus-plugin-openapi/src/markdown/createSchemaTable.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { MediaTypeObject, SchemaObject } from "../openapi/types";
99
import { createDescription } from "./createDescription";
1010
import { createFullWidthTable } from "./createFullWidthTable";
11-
import { getSchemaName } from "./schema";
11+
import { getQualifierMessage, getSchemaName } from "./schema";
1212
import { create, guard } from "./utils";
1313

1414
function resolveAllOf(allOf: SchemaObject[]) {
@@ -60,12 +60,10 @@ function createRow({ name, schema, required }: RowProps) {
6060
children: " REQUIRED",
6161
}),
6262
]),
63-
guard(schema.enum, (options) =>
63+
guard(getQualifierMessage(schema), (message) =>
6464
create("div", {
6565
style: { marginTop: "var(--ifm-table-cell-padding)" },
66-
children: `Enum: ${options
67-
.map((option) => create("code", { children: `"${option}"` }))
68-
.join(", ")}`,
66+
children: createDescription(message),
6967
})
7068
),
7169
guard(schema.description, (description) =>
@@ -188,12 +186,10 @@ function createRowsRoot({ schema }: RowsRootProps) {
188186
style: { opacity: "0.6" },
189187
children: ` ${schema.type}`,
190188
}),
191-
guard(schema.enum, (options) =>
189+
guard(getQualifierMessage(schema), (message) =>
192190
create("div", {
193191
style: { marginTop: "var(--ifm-table-cell-padding)" },
194-
children: `Enum: ${options
195-
.map((option) => create("code", { children: `"${option}"` }))
196-
.join(", ")}`,
192+
children: createDescription(message),
197193
})
198194
),
199195
guard(schema.description, (description) =>
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/* ============================================================================
2+
* Copyright (c) Cloud Annotations
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
* ========================================================================== */
7+
8+
import { getQualifierMessage } from "./schema";
9+
10+
describe("getQualifierMessage", () => {
11+
it("should render nothing", () => {
12+
const actual = getQualifierMessage({});
13+
expect(actual).toBeUndefined();
14+
});
15+
16+
//
17+
// minLength + maxLength
18+
//
19+
it("should render minLength", () => {
20+
const expected = "**Possible values:** 1 ≤ length";
21+
const actual = getQualifierMessage({ minLength: 1 });
22+
expect(actual).toBe(expected);
23+
});
24+
25+
it("should render maxLength", () => {
26+
const expected = "**Possible values:** length ≤ 40";
27+
const actual = getQualifierMessage({ maxLength: 40 });
28+
expect(actual).toBe(expected);
29+
});
30+
31+
it("should render minLength and maxLength", () => {
32+
const expected = "**Possible values:** 1 ≤ length ≤ 40";
33+
const actual = getQualifierMessage({ minLength: 1, maxLength: 40 });
34+
expect(actual).toBe(expected);
35+
});
36+
37+
//
38+
// pattern
39+
//
40+
it("should render pattern", () => {
41+
const expected =
42+
"**Possible values:** Value must match regular expression `^[a-zA-Z0-9_-]*$`";
43+
const actual = getQualifierMessage({ pattern: "^[a-zA-Z0-9_-]*$" });
44+
expect(actual).toBe(expected);
45+
});
46+
47+
it("should render multiple string qualifiers", () => {
48+
const expected =
49+
"**Possible values:** 1 ≤ length ≤ 40, Value must match regular expression `^[a-zA-Z0-9_-]*$`";
50+
const actual = getQualifierMessage({
51+
minLength: 1,
52+
maxLength: 40,
53+
pattern: "^[a-zA-Z0-9_-]*$",
54+
});
55+
expect(actual).toBe(expected);
56+
});
57+
58+
//
59+
// enum
60+
//
61+
it("should render enum", () => {
62+
const expected = "**Possible values:** [`cat`, `dog`, `mouse`]";
63+
const actual = getQualifierMessage({ enum: ["cat", "dog", "mouse"] });
64+
expect(actual).toBe(expected);
65+
});
66+
67+
//
68+
// minimum + maximum + exclusiveMinimum + exclusiveMaximum
69+
//
70+
it("should render minimum", () => {
71+
const expected = "**Possible values:** 1 ≤ value";
72+
const actual = getQualifierMessage({ minimum: 1 });
73+
expect(actual).toBe(expected);
74+
});
75+
76+
it("should render maximum", () => {
77+
const expected = "**Possible values:** value ≤ 40";
78+
const actual = getQualifierMessage({ maximum: 40 });
79+
expect(actual).toBe(expected);
80+
});
81+
82+
it("should render numeric exclusiveMinimum", () => {
83+
const expected = "**Possible values:** 1 < value";
84+
const actual = getQualifierMessage({ exclusiveMinimum: 1 });
85+
expect(actual).toBe(expected);
86+
});
87+
88+
it("should render numeric exclusiveMaximum", () => {
89+
const expected = "**Possible values:** value < 40";
90+
const actual = getQualifierMessage({ exclusiveMaximum: 40 });
91+
expect(actual).toBe(expected);
92+
});
93+
94+
it("should render boolean exclusiveMinimum", () => {
95+
const expected = "**Possible values:** 1 < value";
96+
const actual = getQualifierMessage({ minimum: 1, exclusiveMinimum: true });
97+
expect(actual).toBe(expected);
98+
});
99+
100+
it("should render boolean exclusiveMaximum", () => {
101+
const expected = "**Possible values:** value < 40";
102+
const actual = getQualifierMessage({ maximum: 40, exclusiveMaximum: true });
103+
expect(actual).toBe(expected);
104+
});
105+
106+
it("should render minimum when exclusiveMinimum is false", () => {
107+
const expected = "**Possible values:** 1 ≤ value";
108+
const actual = getQualifierMessage({ minimum: 1, exclusiveMinimum: false });
109+
expect(actual).toBe(expected);
110+
});
111+
112+
it("should render maximum when exclusiveMaximum is false", () => {
113+
const expected = "**Possible values:** value ≤ 40";
114+
const actual = getQualifierMessage({
115+
maximum: 40,
116+
exclusiveMaximum: false,
117+
});
118+
expect(actual).toBe(expected);
119+
});
120+
121+
it("should render minimum and maximum", () => {
122+
const expected = "**Possible values:** 1 ≤ value ≤ 40";
123+
const actual = getQualifierMessage({ minimum: 1, maximum: 40 });
124+
expect(actual).toBe(expected);
125+
});
126+
127+
it("should render boolean exclusiveMinimum and maximum", () => {
128+
const expected = "**Possible values:** 1 < value ≤ 40";
129+
const actual = getQualifierMessage({
130+
minimum: 1,
131+
maximum: 40,
132+
exclusiveMinimum: true,
133+
});
134+
expect(actual).toBe(expected);
135+
});
136+
137+
it("should render minimum and boolean exclusiveMaximum", () => {
138+
const expected = "**Possible values:** 1 ≤ value < 40";
139+
const actual = getQualifierMessage({
140+
minimum: 1,
141+
maximum: 40,
142+
exclusiveMaximum: true,
143+
});
144+
expect(actual).toBe(expected);
145+
});
146+
147+
it("should render numeric exclusiveMinimum and maximum", () => {
148+
const expected = "**Possible values:** 1 < value ≤ 40";
149+
const actual = getQualifierMessage({
150+
exclusiveMinimum: 1,
151+
maximum: 40,
152+
});
153+
expect(actual).toBe(expected);
154+
});
155+
156+
it("should render minimum and numeric exclusiveMaximum", () => {
157+
const expected = "**Possible values:** 1 ≤ value < 40";
158+
const actual = getQualifierMessage({
159+
minimum: 1,
160+
exclusiveMaximum: 40,
161+
});
162+
expect(actual).toBe(expected);
163+
});
164+
165+
it("should render numeric exclusiveMinimum and boolean exclusiveMaximum", () => {
166+
const expected = "**Possible values:** 1 < value < 40";
167+
const actual = getQualifierMessage({
168+
exclusiveMinimum: 1,
169+
maximum: 40,
170+
exclusiveMaximum: true,
171+
});
172+
expect(actual).toBe(expected);
173+
});
174+
175+
it("should render nothing with empty boolean exclusiveMinimum", () => {
176+
const actual = getQualifierMessage({
177+
exclusiveMinimum: true,
178+
});
179+
expect(actual).toBeUndefined();
180+
});
181+
182+
it("should render nothing with empty boolean exclusiveMaximum", () => {
183+
const actual = getQualifierMessage({
184+
exclusiveMaximum: true,
185+
});
186+
expect(actual).toBeUndefined();
187+
});
188+
189+
it("should render nothing with empty boolean exclusiveMinimum and exclusiveMaximum", () => {
190+
const actual = getQualifierMessage({
191+
exclusiveMinimum: true,
192+
exclusiveMaximum: true,
193+
});
194+
expect(actual).toBeUndefined();
195+
});
196+
});

packages/docusaurus-plugin-openapi/src/markdown/schema.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,77 @@ export function getSchemaName(
3939

4040
return prettyName(schema, circular) ?? "";
4141
}
42+
43+
export function getQualifierMessage(schema?: SchemaObject): string | undefined {
44+
// TODO:
45+
// - maxItems
46+
// - minItems
47+
// - uniqueItems
48+
// - maxProperties
49+
// - minProperties
50+
// - multipleOf
51+
if (!schema) {
52+
return undefined;
53+
}
54+
55+
if (schema.items) {
56+
return getQualifierMessage(schema.items);
57+
}
58+
59+
let message = "**Possible values:** ";
60+
61+
let qualifierGroups = [];
62+
63+
if (schema.minLength || schema.maxLength) {
64+
let lengthQualifier = "";
65+
if (schema.minLength) {
66+
lengthQualifier += `${schema.minLength} ≤ `;
67+
}
68+
lengthQualifier += "length";
69+
if (schema.maxLength) {
70+
lengthQualifier += ` ≤ ${schema.maxLength}`;
71+
}
72+
qualifierGroups.push(lengthQualifier);
73+
}
74+
75+
if (
76+
schema.minimum ||
77+
schema.maximum ||
78+
typeof schema.exclusiveMinimum === "number" ||
79+
typeof schema.exclusiveMaximum === "number"
80+
) {
81+
let minmaxQualifier = "";
82+
if (typeof schema.exclusiveMinimum === "number") {
83+
minmaxQualifier += `${schema.exclusiveMinimum} < `;
84+
} else if (schema.minimum && !schema.exclusiveMinimum) {
85+
minmaxQualifier += `${schema.minimum} ≤ `;
86+
} else if (schema.minimum && schema.exclusiveMinimum === true) {
87+
minmaxQualifier += `${schema.minimum} < `;
88+
}
89+
minmaxQualifier += "value";
90+
if (typeof schema.exclusiveMaximum === "number") {
91+
minmaxQualifier += ` < ${schema.exclusiveMaximum}`;
92+
} else if (schema.maximum && !schema.exclusiveMaximum) {
93+
minmaxQualifier += ` ≤ ${schema.maximum}`;
94+
} else if (schema.maximum && schema.exclusiveMaximum === true) {
95+
minmaxQualifier += ` < ${schema.maximum}`;
96+
}
97+
qualifierGroups.push(minmaxQualifier);
98+
}
99+
100+
if (schema.pattern) {
101+
qualifierGroups.push(
102+
`Value must match regular expression \`${schema.pattern}\``
103+
);
104+
}
105+
106+
if (schema.enum) {
107+
qualifierGroups.push(`[${schema.enum.map((e) => `\`${e}\``).join(", ")}]`);
108+
}
109+
110+
if (qualifierGroups.length === 0) {
111+
return undefined;
112+
}
113+
114+
return message + qualifierGroups.join(", ");
115+
}

0 commit comments

Comments
 (0)