Skip to content

Commit e84acd9

Browse files
support for nest oneOf and anyOf
1 parent 3956670 commit e84acd9

13 files changed

+264
-95
lines changed
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"person": {
5+
"type": "object",
6+
"properties": {
7+
"name": {
8+
"type": "string"
9+
},
10+
"details": {
11+
"anyOf": [
12+
{
13+
"type": "object",
14+
"properties": {
15+
"address": {
16+
"type": "string"
17+
},
18+
"age": {
19+
"type": "number"
20+
}
21+
},
22+
"required": ["address"]
23+
},
24+
{
25+
"type": "object",
26+
"properties": {
27+
"hobbies": {
28+
"type": "array",
29+
"items": {
30+
"anyOf": [
31+
{
32+
"type": "string"
33+
},
34+
{
35+
"type": "object",
36+
"properties": {
37+
"hobbyName": {
38+
"type": "string"
39+
},
40+
"duration": {
41+
"type": "number"
42+
}
43+
},
44+
"required": ["hobbyName", "duration"]
45+
}
46+
]
47+
}
48+
}
49+
},
50+
"required": ["hobbies"]
51+
}
52+
]
53+
}
54+
},
55+
"required": ["name", "details"]
56+
}
57+
}
58+
}
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object",
4+
"properties": {
5+
"person": {
6+
"type": "object",
7+
"oneOf": [
8+
{
9+
"properties": {
10+
"student": {
11+
"type": "object",
12+
"oneOf": [
13+
{
14+
"properties": {
15+
"level": {
16+
"enum": [
17+
"undergraduate",
18+
"postgraduate"
19+
]
20+
},
21+
"course": {
22+
"type": "string"
23+
}
24+
},
25+
"required": [
26+
"level",
27+
"course"
28+
]
29+
},
30+
{
31+
"properties": {
32+
"partTime": {
33+
"type": "boolean"
34+
}
35+
},
36+
"required": [
37+
"partTime"
38+
]
39+
}
40+
]
41+
}
42+
},
43+
"required": [
44+
"student"
45+
]
46+
},
47+
{
48+
"properties": {
49+
"employee": {
50+
"type": "object",
51+
"properties": {
52+
"position": {
53+
"type": "string"
54+
},
55+
"department": {
56+
"type": "string"
57+
}
58+
},
59+
"required": [
60+
"position",
61+
"department"
62+
]
63+
}
64+
},
65+
"required": [
66+
"employee"
67+
]
68+
}
69+
]
70+
}
71+
},
72+
"required": [
73+
"person"
74+
]
75+
}

index.html

+12-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<script type="module" src="/src/any-of-element.ts"></script>
1111
<script type="module" src="/src/body-element.ts"></script>
1212
<script type="module" src="/src/ui/checkbox-group-element.ts"></script>
13+
<script type="module" src="/src/ui/checkbox-element.ts"></script>
1314
<script type="module" src="/src/ui/string-element.ts"></script>
1415
<script type="module" src="/src/ui/single-dropdown-element.ts"></script>
1516
<script type="module" src="/src/ui/number-element.ts"></script>
@@ -20,9 +21,18 @@
2021
const el = document.createElement("json-ui-element");
2122

2223
Object.assign(el, {
23-
schema: await (await fetch("./fixtures/coa-schema.json")).json(),
24+
schema: await (
25+
await fetch("./fixtures/schema-with-nested-one-of.json")
26+
).json(),
2427
path: location.hash.replaceAll("/", ".").slice(1),
25-
value: JSON.parse(localStorage.getItem("JSON_UI_VALUE")),
28+
value: {
29+
person: {
30+
student: {
31+
partTime: true,
32+
},
33+
},
34+
},
35+
// value: JSON.parse(localStorage.getItem("JSON_UI_VALUE")),
2636
});
2737

2838
el.addEventListener("change", (e) => {

src/any-of-element.ts

+1-11
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ export class AnyOfElement extends LitElement {
1111
@property({ type: Object })
1212
schema!: JSONSchema7;
1313

14-
@property({ type: String })
15-
path!: string;
16-
1714
@property({ type: Array })
1815
value?: number[];
1916

@@ -26,14 +23,7 @@ export class AnyOfElement extends LitElement {
2623
}
2724

2825
render() {
29-
const options = anyOfOptions(
30-
this.schema!,
31-
this.path!,
32-
this.value ?? []
33-
)?.map((option) => option.title);
34-
35-
console.log({ options }, this.schema, this.path, this.value);
36-
26+
const options = anyOfOptions(this.schema);
3727
return options
3828
? html`
3929
<checkbox-group-element

src/body-element.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { humanizeKey, humanizeValue } from "./utils/humanize";
1616
import { choose } from "lit/directives/choose.js";
1717
import { when } from "lit/directives/when.js";
1818
import { isUndefined } from "lodash";
19-
import { navigate } from "./utils/path";
19+
import { navigateSchema } from "./utils/path";
2020

2121
/**
2222
* The body represents the main content of the JSON UI.
@@ -54,6 +54,7 @@ export class BodyElement extends LitElement {
5454
["string", () => this.renderPrimitive("string")],
5555
["number", () => this.renderPrimitive("string")],
5656
["enum", () => this.renderPrimitive("string")],
57+
["boolean", () => this.renderPrimitive("boolean")],
5758
],
5859
() => html`Unknown type <strong>${type}</strong>.`
5960
)}
@@ -69,12 +70,12 @@ export class BodyElement extends LitElement {
6970
}
7071

7172
private renderPrimitive(
72-
type: "string" | "number" | "enum",
73+
type: "string" | "number" | "enum" | "boolean",
7374
key?: string | number,
7475
skipHeader = false
7576
) {
7677
const value = !isUndefined(key) ? this.value?.[key] : this.value;
77-
const schema = navigate(this.schema!, key);
78+
const schema = navigateSchema(this.schema!, key);
7879
return html`<label class="flex flex-col gap-2 w-full">
7980
${when(!skipHeader && key, () =>
8081
renderLabel(String(key), this.required.includes(String(key)))
@@ -109,6 +110,14 @@ export class BodyElement extends LitElement {
109110
></single-dropdown-element>`;
110111
},
111112
],
113+
[
114+
"boolean",
115+
() => html`<checkbox-element
116+
@change=${dispatchChange(this, String(key ?? ""))}
117+
.value=${value}
118+
.label=${humanizeKey(String(key))}
119+
></checkbox-element>`,
120+
],
112121
])}
113122
</label>`;
114123
}
@@ -129,8 +138,9 @@ export class BodyElement extends LitElement {
129138
["string", () => this.renderPrimitive("string", key)],
130139
["number", () => this.renderPrimitive("number", key)],
131140
["enum", () => this.renderPrimitive("enum", key)],
141+
["boolean", () => this.renderPrimitive("boolean", key, true)],
132142
],
133-
() => html`<label class="inline-flex flex-col gap-2"
143+
() => html`<label class="w-full inline-flex flex-col gap-2"
134144
>${renderLabel(key, this.required.includes(key))}
135145
${this.renderValuePreview(key, value)}
136146
</label>`

src/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { JSONSchema7TypeName } from "json-schema";
33
export const PROPERTIES_KEY = "properties";
44
export const REQUIRED_KEY = "required";
55
export const REF_KEY = "$ref";
6+
export const ANY_OF_KEY = "anyOf";
7+
export const ANY_OF_REF_KEY = "$ref:anyOf";
68

79
export const PATH_SEPARATOR = ".";
810

src/json-ui-element.ts

+11-12
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import styles from "./index.css?inline";
44
import { resolveRefs } from "./parser/resolve-refs";
55
import { allOf } from "./parser/all-of";
66
import { JSONSchema7 } from "json-schema";
7-
import { joinPaths, navigate } from "./utils/path";
7+
import { joinPaths, navigateSchema } from "./utils/path";
88
import { inferOneOfOption, oneOf } from "./parser/one-of";
99
import { anyOf, inferAnyOfOptions } from "./parser/any-of";
1010
import { get, isEmpty, isNull, isUndefined, set } from "lodash";
@@ -82,7 +82,9 @@ export class JsonUiElement extends LitElement {
8282
}
8383

8484
console.debug(
85-
`🚀 [DEBUG] Performing a level ${updateLevel} schema update.`
85+
`${
86+
["🔴", "🟠", "🟡", "🟢"][updateLevel]
87+
} [DEBUG] Performing a level ${updateLevel} schema update.`
8688
);
8789

8890
if (updateLevel === 0) {
@@ -97,9 +99,10 @@ export class JsonUiElement extends LitElement {
9799
if (updateLevel <= 1) {
98100
this.dispatchEvent(new CustomEvent("navigate", { detail: this.path }));
99101

100-
const navigatedSchema = navigate(
102+
const navigatedSchema = navigateSchema(
101103
this.resolvedSchemas.resolvedAllOf,
102-
this.path
104+
this.path,
105+
this.value
103106
);
104107

105108
if (isUndefined(navigatedSchema)) {
@@ -123,7 +126,6 @@ export class JsonUiElement extends LitElement {
123126

124127
this.anyOfIndices = inferAnyOfOptions(
125128
this.resolvedSchemas.resolvedOneOf,
126-
this.path,
127129
this.resolvedValue
128130
);
129131
}
@@ -151,9 +153,8 @@ export class JsonUiElement extends LitElement {
151153
}
152154

153155
render() {
154-
console.log("RENDER", this.schema);
155-
const { resolvedAnyOf, resolvedAllOf, navigated, resolvedOneOf } =
156-
this.resolvedSchemas;
156+
console.debug(`🧠 [DEBUG] Rendering JSON UI.`);
157+
const { resolvedAnyOf, navigated, resolvedOneOf } = this.resolvedSchemas;
157158

158159
return html`
159160
<div class="grid grid-cols-1 gap-8">
@@ -168,8 +169,7 @@ export class JsonUiElement extends LitElement {
168169
html`<any-of-element
169170
@change=${(ev: CustomEvent<ChangeEventDetails<number[]>>) =>
170171
(this.anyOfIndices = ev.detail.value)}
171-
.schema=${resolvedAllOf}
172-
.path=${this.path}
172+
.schema=${navigated}
173173
.value=${this.anyOfIndices}
174174
></any-of-element>`}
175175
@@ -227,9 +227,8 @@ export class JsonUiElement extends LitElement {
227227

228228
private renderErrors() {
229229
const errors = this.ajvValidateFn?.errors ?? [];
230-
console.log(errors);
231230
return html`
232-
<div class="flex flex-col p-8 ring-2 ring-red-500 gap-4">
231+
<div class="hidden flex flex-col p-8 ring-2 ring-red-500 gap-4">
233232
${errors.map(
234233
(e) =>
235234
html`<div class="text-red-500 flex flex-col">

src/parser/any-of.spec.ts

+1-9
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,6 @@ test("anyOf applied correctly", async () => {
1111
test("anyOf options parsed correctly", async () => {
1212
const schema = (await import("../../fixtures/schema-with-any-of.json"))
1313
.default as unknown as JSONSchema7;
14-
const result = anyOfOptions(schema, "", []);
15-
expect(result).toMatchSnapshot();
16-
});
17-
18-
test("selected anyOf options parsed correctly", async () => {
19-
const schema = (await import("../../fixtures/schema-with-any-of.json"))
20-
.default as unknown as JSONSchema7;
21-
const selectedIndices = [0, 1];
22-
const result = anyOfOptions(schema, "", selectedIndices);
14+
const result = anyOfOptions(schema);
2315
expect(result).toMatchSnapshot();
2416
});

0 commit comments

Comments
 (0)