@golemui/gui-mcp 1.5.1 → 1.6.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +17 -12
- package/cli.js +3 -3
- package/{get-concept-MFKfeD4D.js → get-concept-BqdUfdaO.js} +2452 -390
- package/json/generate-from-json-schema.d.ts +28 -10
- package/json/generate-from-openapi.d.ts +28 -10
- package/json/mapping/json-schema-to-gui.d.ts +39 -44
- package/json.js +1 -1
- package/lib.js +2 -2
- package/{list-dx-factories-D3u4qsn1.js → list-dx-factories-BKx43VHj.js} +1 -1
- package/package.json +7 -6
- package/json/mapping/validator.d.ts +0 -29
|
@@ -1984,12 +1984,12 @@ function checkSlot(expr, slot, path, out, inTemplate) {
|
|
|
1984
1984
|
});
|
|
1985
1985
|
}
|
|
1986
1986
|
}
|
|
1987
|
-
function validateFormDefinition(
|
|
1987
|
+
function validateFormDefinition(input2) {
|
|
1988
1988
|
const validate = getFormValidator();
|
|
1989
|
-
const ajvOk = validate(
|
|
1990
|
-
const { errors, warnings } = formatAjvErrors(validate.errors,
|
|
1991
|
-
const expressionWarnings = lintReactiveExpressions(
|
|
1992
|
-
const interpolationWarnings = lintStringInterpolations(
|
|
1989
|
+
const ajvOk = validate(input2.formDefinition);
|
|
1990
|
+
const { errors, warnings } = formatAjvErrors(validate.errors, input2.formDefinition);
|
|
1991
|
+
const expressionWarnings = lintReactiveExpressions(input2.formDefinition);
|
|
1992
|
+
const interpolationWarnings = lintStringInterpolations(input2.formDefinition);
|
|
1993
1993
|
if (!ajvOk && errors.length === 0) {
|
|
1994
1994
|
errors.push({
|
|
1995
1995
|
path: "/",
|
|
@@ -1997,7 +1997,7 @@ function validateFormDefinition(input) {
|
|
|
1997
1997
|
message: "Form failed schema validation but no specific error could be localized. The form may contain a widget at a position the form schema does not allow, or a structural shape that our targeted validator missed. Verify each widget against its `json_get_widget_spec` entry."
|
|
1998
1998
|
});
|
|
1999
1999
|
}
|
|
2000
|
-
const booleanValidatorWarnings = lintBooleanValidators(
|
|
2000
|
+
const booleanValidatorWarnings = lintBooleanValidators(input2.formDefinition).map(
|
|
2001
2001
|
(f) => ({
|
|
2002
2002
|
path: f.path,
|
|
2003
2003
|
message: f.message,
|
|
@@ -2028,386 +2028,2470 @@ const JSON_VALIDATE_FORM_DEFINITION_TOOL = {
|
|
|
2028
2028
|
required: ["formDefinition"]
|
|
2029
2029
|
}
|
|
2030
2030
|
};
|
|
2031
|
-
const
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2034
|
-
"
|
|
2035
|
-
"
|
|
2036
|
-
"
|
|
2037
|
-
"
|
|
2038
|
-
"
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2031
|
+
const CHECKED_FORMATS = /* @__PURE__ */ new Set(["email", "hostname", "ipv4", "ipv6", "url", "uuid", "duration"]);
|
|
2032
|
+
const FORMAT_NAMES = { uri: "url" };
|
|
2033
|
+
const NUMBER_KEYWORDS$1 = [
|
|
2034
|
+
"minimum",
|
|
2035
|
+
"maximum",
|
|
2036
|
+
"exclusiveMinimum",
|
|
2037
|
+
"exclusiveMaximum",
|
|
2038
|
+
"multipleOf"
|
|
2039
|
+
];
|
|
2040
|
+
function guiValidator(node) {
|
|
2041
|
+
const { schema } = node;
|
|
2042
|
+
const constraints = {};
|
|
2043
|
+
const copyNumbers = (keywords) => {
|
|
2044
|
+
for (const keyword of keywords) {
|
|
2045
|
+
if (typeof schema[keyword] === "number") {
|
|
2046
|
+
constraints[keyword] = schema[keyword];
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
switch (node.type) {
|
|
2051
|
+
case "string": {
|
|
2052
|
+
copyNumbers(["minLength", "maxLength"]);
|
|
2053
|
+
if (typeof schema["pattern"] === "string") {
|
|
2054
|
+
constraints["pattern"] = schema["pattern"];
|
|
2055
|
+
}
|
|
2056
|
+
const format = typeof schema["format"] === "string" ? FORMAT_NAMES[schema["format"]] ?? schema["format"] : void 0;
|
|
2057
|
+
if (format !== void 0 && CHECKED_FORMATS.has(format)) {
|
|
2058
|
+
constraints["format"] = format;
|
|
2059
|
+
}
|
|
2060
|
+
copyLiterals(schema, constraints, (value) => typeof value === "string");
|
|
2061
|
+
break;
|
|
2062
|
+
}
|
|
2063
|
+
case "number":
|
|
2064
|
+
case "integer":
|
|
2065
|
+
copyNumbers(NUMBER_KEYWORDS$1);
|
|
2066
|
+
copyLiterals(schema, constraints, (value) => typeof value === "number");
|
|
2067
|
+
break;
|
|
2068
|
+
case "boolean":
|
|
2069
|
+
copyLiterals(schema, constraints, (value) => typeof value === "boolean");
|
|
2070
|
+
break;
|
|
2071
|
+
case "array":
|
|
2072
|
+
copyNumbers(["minItems", "maxItems"]);
|
|
2073
|
+
if (schema["uniqueItems"] === true) {
|
|
2074
|
+
constraints["uniqueItems"] = true;
|
|
2075
|
+
}
|
|
2076
|
+
break;
|
|
2077
|
+
default:
|
|
2078
|
+
return void 0;
|
|
2079
|
+
}
|
|
2080
|
+
const required2 = node.required || node.type === "boolean" && constraints["const"] === true;
|
|
2081
|
+
if (!required2 && Object.keys(constraints).length === 0) {
|
|
2082
|
+
return void 0;
|
|
2083
|
+
}
|
|
2084
|
+
return { type: node.type, ...required2 ? { required: true } : {}, ...constraints };
|
|
2085
|
+
}
|
|
2086
|
+
function copyLiterals(schema, constraints, hasType) {
|
|
2087
|
+
if ("const" in schema && hasType(schema["const"])) {
|
|
2088
|
+
constraints["const"] = schema["const"];
|
|
2089
|
+
}
|
|
2090
|
+
if (Array.isArray(schema["enum"])) {
|
|
2091
|
+
const values = schema["enum"].filter(hasType);
|
|
2092
|
+
if (values.length > 0) {
|
|
2093
|
+
constraints["enum"] = values;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
const DEFAULT_SCHEMA_URL = "https://golemui.com/schemas/form.schema.json";
|
|
2098
|
+
const DEFAULT_SELECT_THRESHOLD = 6;
|
|
2099
|
+
const LONG_TEXT_MIN_LENGTH = 200;
|
|
2100
|
+
const SECRET_NAME = /(password|secret|api[_-]?key|token)/i;
|
|
2101
|
+
const DATE_AND_TIME_WIDGETS = {
|
|
2102
|
+
date: "dateInput",
|
|
2103
|
+
"date-time": "dateTimePicker",
|
|
2104
|
+
time: "timeInput"
|
|
2105
|
+
};
|
|
2106
|
+
const TAG_CONSTRAINTS = ["format", "pattern", "minLength", "maxLength", "minimum", "maximum"];
|
|
2107
|
+
function guiPreset(options = {}) {
|
|
2108
|
+
const radioThreshold = options.enumThresholds?.radio ?? 0;
|
|
2109
|
+
const selectThreshold = options.enumThresholds?.select ?? DEFAULT_SELECT_THRESHOLD;
|
|
2110
|
+
const objectTitle = options.objectTitle ?? "none";
|
|
2111
|
+
const group = (children, groupOptions) => layout("flex", { direction: "column", gap: 8 }, withTitle(children, groupOptions.title), {
|
|
2112
|
+
uid: groupOptions.uid,
|
|
2113
|
+
include: groupOptions.include
|
|
2114
|
+
});
|
|
2115
|
+
const withTitle = (children, title2) => {
|
|
2116
|
+
if (typeof title2 !== "string" || title2 === "" || objectTitle === "none") {
|
|
2117
|
+
return children;
|
|
2118
|
+
}
|
|
2119
|
+
const heading = objectTitle === "markdownText" ? { kind: "display", type: "markdownText", props: { md: `### ${title2}` } } : { kind: "display", type: "alert", props: { text: title2, level: "info" } };
|
|
2120
|
+
return [heading, ...children];
|
|
2121
|
+
};
|
|
2122
|
+
const choice = (node, context, forcedType) => {
|
|
2123
|
+
const options2 = context.enumOptions(node) ?? [];
|
|
2124
|
+
const type2 = forcedType ?? (options2.length <= radioThreshold ? "radiogroup" : options2.length <= selectThreshold ? "select" : "dropdown");
|
|
2125
|
+
if (type2 === "dropdown") {
|
|
2126
|
+
return input(type2, node, context, {
|
|
2127
|
+
items: options2,
|
|
2128
|
+
labelField: "label",
|
|
2129
|
+
valueField: "value",
|
|
2130
|
+
placeholder: exampleOf(node)
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
return input(type2, node, context, {
|
|
2134
|
+
options: options2,
|
|
2135
|
+
placeholder: type2 === "select" ? exampleOf(node) : void 0
|
|
2136
|
+
});
|
|
2137
|
+
};
|
|
2138
|
+
const multipleChoice = (node, context, forcedType) => {
|
|
2139
|
+
const item = context.item(node);
|
|
2140
|
+
const options2 = (item && context.enumOptions(item)) ?? [];
|
|
2141
|
+
const type2 = forcedType ?? (options2.length <= selectThreshold ? "multiList" : "multiDropdown");
|
|
2142
|
+
return input(type2, node, context, { items: options2, labelField: "label", valueField: "value" });
|
|
2143
|
+
};
|
|
2144
|
+
const repeater = (node, context) => {
|
|
2145
|
+
const item = context.item(node);
|
|
2146
|
+
if (item === void 0) {
|
|
2147
|
+
return void 0;
|
|
2148
|
+
}
|
|
2149
|
+
const built = context.build(item);
|
|
2150
|
+
const template = !Array.isArray(built) && built?.kind === "layout" ? built : layout("flex", { direction: "column", gap: 8 }, built ? [built].flat() : []);
|
|
2151
|
+
if (template.children?.length === 0) {
|
|
2152
|
+
context.diagnostic({
|
|
2153
|
+
severity: "error",
|
|
2154
|
+
code: "empty-repeater",
|
|
2155
|
+
message: "The array items have no property the form can render, so the array is left out."
|
|
2156
|
+
});
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
const label = context.label(node);
|
|
2160
|
+
const itemTitle = typeof item.schema["title"] === "string" ? item.schema["title"] : void 0;
|
|
2161
|
+
const itemName = itemTitle ?? (typeof label === "string" ? singular(label) : void 0);
|
|
2162
|
+
return {
|
|
2163
|
+
kind: "input",
|
|
2164
|
+
type: "repeater",
|
|
2165
|
+
path: node.path,
|
|
2166
|
+
label,
|
|
2167
|
+
defaultValue: defaultValueOf(node),
|
|
2168
|
+
validator: context.validator(node),
|
|
2169
|
+
props: {
|
|
2170
|
+
title: itemTitle,
|
|
2171
|
+
addLabel: itemName !== void 0 ? `Add ${itemName.toLowerCase()}` : "Add",
|
|
2172
|
+
removeLabel: "Remove",
|
|
2173
|
+
limit: typeof node.schema["maxItems"] === "number" ? node.schema["maxItems"] : void 0,
|
|
2174
|
+
template
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
};
|
|
2178
|
+
const tags = (node, context) => {
|
|
2179
|
+
const item = context.item(node);
|
|
2180
|
+
if (item !== void 0 && item.type !== "string") {
|
|
2181
|
+
context.diagnostic({
|
|
2182
|
+
severity: "warning",
|
|
2183
|
+
code: "item-type-mismatch",
|
|
2184
|
+
message: `The tags widget writes strings, and the schema expects ${item.type ?? "another type"} items.`
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
if (item !== void 0 && TAG_CONSTRAINTS.some((keyword) => keyword in item.schema)) {
|
|
2188
|
+
context.diagnostic({
|
|
2189
|
+
severity: "info",
|
|
2190
|
+
code: "item-constraints-dropped",
|
|
2191
|
+
message: "The tags widget does not check the constraints of each item."
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
return input("tags", node, context, { placeholder: exampleOf(node) ?? "Add and press Enter" });
|
|
2195
|
+
};
|
|
2196
|
+
const tuple = (node, context) => {
|
|
2197
|
+
const children = context.buildChildren(node);
|
|
2198
|
+
if (children.length === 0) {
|
|
2199
|
+
return null;
|
|
2200
|
+
}
|
|
2201
|
+
if (node.schema["items"] !== false && node.schema["items"] !== void 0) {
|
|
2202
|
+
context.diagnostic({
|
|
2203
|
+
severity: "info",
|
|
2204
|
+
code: "tuple-extra-items",
|
|
2205
|
+
message: "The form edits the fixed positions only. Extra items the schema allows are not editable."
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
return layout("flex", { direction: "row", gap: 12 }, withTitle(children, context.label(node)));
|
|
2209
|
+
};
|
|
2210
|
+
const object = (node, context, type2) => {
|
|
2211
|
+
const children = context.buildChildren(node);
|
|
2212
|
+
if (children.length === 0) {
|
|
2213
|
+
context.diagnostic({
|
|
2214
|
+
severity: "warning",
|
|
2215
|
+
code: "empty-object",
|
|
2216
|
+
message: "The object has no property the form can render, so it is left out."
|
|
2217
|
+
});
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
const title2 = isArrayItem(node) ? void 0 : context.label(node);
|
|
2221
|
+
return type2 === "flex" ? group(children, { title: title2 }) : layout("grid", { columnGap: 12, rowGap: 12 }, withTitle(children, title2));
|
|
2222
|
+
};
|
|
2223
|
+
const rules = [
|
|
2224
|
+
{
|
|
2225
|
+
name: "gui constant",
|
|
2226
|
+
when: (node) => "const" in node.schema && (node.type === "string" || isNumeric(node)),
|
|
2227
|
+
build: (node, context) => ({
|
|
2228
|
+
...input(node.type === "string" ? "textinput" : "number", node, context),
|
|
2229
|
+
readonly: true,
|
|
2230
|
+
defaultValue: node.schema["const"]
|
|
2231
|
+
})
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
name: "gui choice",
|
|
2235
|
+
when: (node) => !isContainer(node) && isEnumeration(node.schema),
|
|
2236
|
+
build: (node, context) => choice(node, context)
|
|
2237
|
+
},
|
|
2238
|
+
{
|
|
2239
|
+
name: "gui date and time",
|
|
2240
|
+
when: (node) => node.type === "string" && formatOf(node) in DATE_AND_TIME_WIDGETS,
|
|
2241
|
+
build: (node, context) => {
|
|
2242
|
+
if (formatOf(node) === "date-time") {
|
|
2243
|
+
context.diagnostic({
|
|
2244
|
+
severity: "info",
|
|
2245
|
+
code: "local-datetime",
|
|
2246
|
+
message: "The date-time widget writes the local date and time without a UTC offset, which a strict `date-time` check rejects."
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
return input(DATE_AND_TIME_WIDGETS[formatOf(node)], node, context);
|
|
2250
|
+
}
|
|
2251
|
+
},
|
|
2252
|
+
{
|
|
2253
|
+
name: "gui secret",
|
|
2254
|
+
when: (node) => node.type === "string" && (formatOf(node) === "password" || node.schema["writeOnly"] === true || SECRET_NAME.test(node.name ?? "")),
|
|
2255
|
+
build: (node, context) => textInput("password", node, context)
|
|
2256
|
+
},
|
|
2257
|
+
{
|
|
2258
|
+
name: "gui markdown",
|
|
2259
|
+
when: (node) => node.type === "string" && node.schema["contentMediaType"] === "text/markdown",
|
|
2260
|
+
build: (node, context) => textInput("markdown", node, context)
|
|
2261
|
+
},
|
|
2262
|
+
{
|
|
2263
|
+
name: "gui long text",
|
|
2264
|
+
when: (node) => node.type === "string" && typeof node.schema["maxLength"] === "number" && node.schema["maxLength"] >= LONG_TEXT_MIN_LENGTH,
|
|
2265
|
+
build: (node, context) => textInput("textarea", node, context)
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
name: "gui text",
|
|
2269
|
+
when: (node) => node.type === "string",
|
|
2270
|
+
build: (node, context) => textInput("textinput", node, context)
|
|
2271
|
+
},
|
|
2272
|
+
{
|
|
2273
|
+
name: "gui number",
|
|
2274
|
+
when: isNumeric,
|
|
2275
|
+
build: (node, context) => textInput("number", node, context)
|
|
2276
|
+
},
|
|
2277
|
+
{
|
|
2278
|
+
name: "gui boolean",
|
|
2279
|
+
when: (node) => node.type === "boolean",
|
|
2280
|
+
build: (node, context) => booleanInput("checkbox", node, context)
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
name: "gui array",
|
|
2284
|
+
when: (node) => node.type === "array",
|
|
2285
|
+
build: (node, context) => {
|
|
2286
|
+
if (Array.isArray(node.schema["prefixItems"])) {
|
|
2287
|
+
return tuple(node, context);
|
|
2288
|
+
}
|
|
2289
|
+
const item = context.item(node);
|
|
2290
|
+
if (item === void 0) {
|
|
2291
|
+
return void 0;
|
|
2292
|
+
}
|
|
2293
|
+
if (!isContainer(item) && isEnumeration(item.schema)) {
|
|
2294
|
+
return multipleChoice(node, context);
|
|
2295
|
+
}
|
|
2296
|
+
if (item.type === "object") {
|
|
2297
|
+
return repeater(node, context);
|
|
2298
|
+
}
|
|
2299
|
+
if (item.type === "string" || isNumeric(item)) {
|
|
2300
|
+
return tags(node, context);
|
|
2301
|
+
}
|
|
2302
|
+
return void 0;
|
|
2303
|
+
}
|
|
2304
|
+
},
|
|
2305
|
+
{
|
|
2306
|
+
name: "gui object",
|
|
2307
|
+
when: (node) => node.type === "object",
|
|
2308
|
+
build: (node, context) => object(node, context, "flex")
|
|
2309
|
+
}
|
|
2310
|
+
];
|
|
2311
|
+
const widgets = {
|
|
2312
|
+
textinput: (node, context) => textInput("textinput", node, context),
|
|
2313
|
+
textarea: (node, context) => textInput("textarea", node, context),
|
|
2314
|
+
password: (node, context) => textInput("password", node, context),
|
|
2315
|
+
markdown: (node, context) => textInput("markdown", node, context),
|
|
2316
|
+
number: (node, context) => textInput("number", node, context),
|
|
2317
|
+
currency: (node, context) => input("currency", node, context),
|
|
2318
|
+
checkbox: (node, context) => booleanInput("checkbox", node, context),
|
|
2319
|
+
toggle: (node, context) => booleanInput("toggle", node, context),
|
|
2320
|
+
select: (node, context) => choice(node, context, "select"),
|
|
2321
|
+
radiogroup: (node, context) => choice(node, context, "radiogroup"),
|
|
2322
|
+
dropdown: (node, context) => choice(node, context, "dropdown"),
|
|
2323
|
+
multiList: (node, context) => multipleChoice(node, context, "multiList"),
|
|
2324
|
+
multiDropdown: (node, context) => multipleChoice(node, context, "multiDropdown"),
|
|
2325
|
+
tags: (node, context) => tags(node, context),
|
|
2326
|
+
repeater: (node, context) => repeater(node, context),
|
|
2327
|
+
flex: (node, context) => object(node, context, "flex"),
|
|
2328
|
+
grid: (node, context) => object(node, context, "grid"),
|
|
2329
|
+
...Object.fromEntries(
|
|
2330
|
+
[
|
|
2331
|
+
"dateInput",
|
|
2332
|
+
"datePicker",
|
|
2333
|
+
"calendar",
|
|
2334
|
+
"dateTimeInput",
|
|
2335
|
+
"dateTimePicker",
|
|
2336
|
+
"dateTimeCalendar",
|
|
2337
|
+
"timeInput",
|
|
2338
|
+
"timePicker"
|
|
2339
|
+
].map((type2) => [
|
|
2340
|
+
type2,
|
|
2341
|
+
(node, context) => input(type2, node, context)
|
|
2342
|
+
])
|
|
2343
|
+
)
|
|
2344
|
+
};
|
|
2345
|
+
const root = (children) => {
|
|
2346
|
+
const widgets2 = options.submitAction === false ? children : [
|
|
2347
|
+
...children,
|
|
2348
|
+
{
|
|
2349
|
+
kind: "action",
|
|
2350
|
+
type: "button",
|
|
2351
|
+
actionType: "submit",
|
|
2352
|
+
label: options.submitLabel ?? "Submit",
|
|
2353
|
+
props: { variant: "filled" }
|
|
2354
|
+
}
|
|
2355
|
+
];
|
|
2356
|
+
if (widgets2.length === 0 || options.rootLayout === void 0 || options.rootLayout === "vertical") {
|
|
2357
|
+
return widgets2;
|
|
2358
|
+
}
|
|
2359
|
+
return [
|
|
2360
|
+
options.rootLayout === "grid" ? layout("grid", { columnGap: 12, rowGap: 12 }, widgets2) : layout("flex", { direction: "row", gap: 12 }, widgets2)
|
|
2361
|
+
];
|
|
2362
|
+
};
|
|
2363
|
+
return {
|
|
2364
|
+
name: "gui",
|
|
2365
|
+
rules,
|
|
2366
|
+
widgets,
|
|
2367
|
+
validator: guiValidator,
|
|
2368
|
+
group,
|
|
2369
|
+
root,
|
|
2370
|
+
schemaUrl: options.schemaUrl ?? DEFAULT_SCHEMA_URL
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
function input(type2, node, context, props = {}) {
|
|
2374
|
+
return {
|
|
2375
|
+
kind: "input",
|
|
2376
|
+
type: type2,
|
|
2377
|
+
path: node.path,
|
|
2378
|
+
label: context.label(node),
|
|
2379
|
+
readonly: node.schema["readOnly"] === true ? true : void 0,
|
|
2380
|
+
defaultValue: defaultValueOf(node),
|
|
2381
|
+
validator: context.validator(node),
|
|
2382
|
+
props: definedOnly({ hint: descriptionOf(node), ...props })
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
function textInput(type2, node, context) {
|
|
2386
|
+
return input(type2, node, context, { placeholder: exampleOf(node) });
|
|
2387
|
+
}
|
|
2388
|
+
function booleanInput(type2, node, context) {
|
|
2389
|
+
const widget = input(type2, node, context);
|
|
2390
|
+
if (widget["defaultValue"] === void 0 && node.required) {
|
|
2391
|
+
widget["defaultValue"] = false;
|
|
2392
|
+
}
|
|
2393
|
+
return widget;
|
|
2394
|
+
}
|
|
2395
|
+
function layout(type2, props, children, fields = {}) {
|
|
2396
|
+
return { kind: "layout", type: type2, ...fields, props, children };
|
|
2397
|
+
}
|
|
2398
|
+
function defaultValueOf(node) {
|
|
2399
|
+
if ("default" in node.schema) {
|
|
2400
|
+
return node.schema["default"];
|
|
2401
|
+
}
|
|
2402
|
+
if (node.parent !== void 0 && node.name !== void 0) {
|
|
2403
|
+
const parentDefault = defaultValueOf(node.parent);
|
|
2404
|
+
if (isRecord$2(parentDefault) && node.name in parentDefault) {
|
|
2405
|
+
return parentDefault[node.name];
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return void 0;
|
|
2409
|
+
}
|
|
2410
|
+
function descriptionOf(node) {
|
|
2411
|
+
const description2 = node.schema["description"];
|
|
2412
|
+
return typeof description2 === "string" && description2 !== "" ? description2 : void 0;
|
|
2413
|
+
}
|
|
2414
|
+
function exampleOf(node) {
|
|
2415
|
+
const examples = node.schema["examples"];
|
|
2416
|
+
const first = Array.isArray(examples) ? examples[0] : void 0;
|
|
2417
|
+
return typeof first === "string" || typeof first === "number" ? String(first) : void 0;
|
|
2418
|
+
}
|
|
2419
|
+
function formatOf(node) {
|
|
2420
|
+
return typeof node.schema["format"] === "string" ? node.schema["format"] : "";
|
|
2421
|
+
}
|
|
2422
|
+
function isNumeric(node) {
|
|
2423
|
+
return node.type === "number" || node.type === "integer";
|
|
2424
|
+
}
|
|
2425
|
+
function isContainer(node) {
|
|
2426
|
+
return node.type === "object" || node.type === "array";
|
|
2427
|
+
}
|
|
2428
|
+
function isArrayItem(node) {
|
|
2429
|
+
return node.parent?.type === "array" && node.name === void 0;
|
|
2430
|
+
}
|
|
2431
|
+
function isEnumeration(schema) {
|
|
2432
|
+
if (Array.isArray(schema["enum"])) {
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
2435
|
+
return ["oneOf", "anyOf"].some((keyword) => {
|
|
2436
|
+
const branches = schema[keyword];
|
|
2437
|
+
return Array.isArray(branches) && branches.length > 0 && branches.every((branch) => isRecord$2(branch) && "const" in branch);
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
function definedOnly(props) {
|
|
2441
|
+
const defined = Object.entries(props).filter(([, value]) => value !== void 0);
|
|
2442
|
+
return defined.length > 0 ? Object.fromEntries(defined) : void 0;
|
|
2443
|
+
}
|
|
2444
|
+
function singular(label) {
|
|
2445
|
+
if (label.endsWith("ies") && label.length > 3) {
|
|
2446
|
+
return `${label.slice(0, -3)}y`;
|
|
2447
|
+
}
|
|
2448
|
+
if (/(ss|x|ch|sh)es$/.test(label)) {
|
|
2449
|
+
return label.slice(0, -2);
|
|
2450
|
+
}
|
|
2451
|
+
if (label.endsWith("s") && !label.endsWith("ss")) {
|
|
2452
|
+
return label.slice(0, -1);
|
|
2453
|
+
}
|
|
2454
|
+
return label;
|
|
2455
|
+
}
|
|
2456
|
+
function isRecord$2(value) {
|
|
2457
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2458
|
+
}
|
|
2459
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
2460
|
+
function literal(value) {
|
|
2461
|
+
if (typeof value !== "string") {
|
|
2462
|
+
return JSON.stringify(value);
|
|
2463
|
+
}
|
|
2464
|
+
return JSON.stringify(value).replace(/\.(?=\d)/g, '." + "');
|
|
2465
|
+
}
|
|
2466
|
+
function pathExpression(scope, segments) {
|
|
2467
|
+
return segments.reduce((expression, segment, index) => {
|
|
2468
|
+
const chain = index === 0 ? "" : "?.";
|
|
2469
|
+
if (IDENTIFIER.test(segment)) {
|
|
2470
|
+
return `${expression}${chain === "" ? "." : chain}${segment}`;
|
|
2471
|
+
}
|
|
2472
|
+
return `${expression}${chain}[${literal(segment)}]`;
|
|
2473
|
+
}, scope);
|
|
2474
|
+
}
|
|
2475
|
+
function innermostItemNode(node) {
|
|
2476
|
+
for (let current = node; current; current = current.parent) {
|
|
2477
|
+
const parent = current.parent;
|
|
2478
|
+
if (parent?.type === "array" && current.path === `${parent.path}.items`) {
|
|
2479
|
+
return current;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return void 0;
|
|
2483
|
+
}
|
|
2484
|
+
function dataReference(node, path) {
|
|
2485
|
+
const row = innermostItemNode(node);
|
|
2486
|
+
if (row === void 0) {
|
|
2487
|
+
return pathExpression("$form", path.split("."));
|
|
2488
|
+
}
|
|
2489
|
+
const relative = path.slice(row.path.length + 1);
|
|
2490
|
+
return relative === "" ? "$item" : pathExpression("$item", relative.split("."));
|
|
2491
|
+
}
|
|
2492
|
+
function equalsAny(reference, values) {
|
|
2493
|
+
const tests = values.map((value) => `${reference} === ${literal(value)}`);
|
|
2494
|
+
return tests.length === 1 ? tests[0] : `(${tests.join(" || ")})`;
|
|
2495
|
+
}
|
|
2496
|
+
const ANNOTATIONS = /* @__PURE__ */ new Set([
|
|
2497
|
+
"title",
|
|
2498
|
+
"description",
|
|
2499
|
+
"$comment",
|
|
2500
|
+
"examples",
|
|
2501
|
+
"default",
|
|
2502
|
+
"deprecated"
|
|
2503
|
+
]);
|
|
2504
|
+
function compileCondition(schema, reference) {
|
|
2505
|
+
if (typeof schema === "boolean") {
|
|
2506
|
+
return { expression: String(schema), vacuous: false, reads: [] };
|
|
2507
|
+
}
|
|
2508
|
+
if (!isRecord$1(schema)) {
|
|
2509
|
+
return { unsupported: "a value that is not a schema" };
|
|
2510
|
+
}
|
|
2511
|
+
const state = { vacuous: false };
|
|
2512
|
+
const compiled = objectTests(schema, [], reference, state);
|
|
2513
|
+
if ("unsupported" in compiled) {
|
|
2514
|
+
return compiled;
|
|
2515
|
+
}
|
|
2516
|
+
const properties2 = isRecord$1(schema["properties"]) ? Object.keys(schema["properties"]) : [];
|
|
2517
|
+
const reads = [.../* @__PURE__ */ new Set([...properties2, ...requiredNames(schema)])];
|
|
2518
|
+
return { expression: joinAnd(compiled.tests), vacuous: state.vacuous, reads };
|
|
2519
|
+
}
|
|
2520
|
+
function objectTests(schema, basePath, reference, state) {
|
|
2521
|
+
for (const [keyword, value] of Object.entries(schema)) {
|
|
2522
|
+
const constrainsNothing = keyword === "properties" || keyword === "required" || ANNOTATIONS.has(keyword) || keyword === "type" && value === "object";
|
|
2523
|
+
if (!constrainsNothing) {
|
|
2524
|
+
return { unsupported: keyword };
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
const required2 = requiredNames(schema);
|
|
2528
|
+
const properties2 = isRecord$1(schema["properties"]) ? schema["properties"] : {};
|
|
2529
|
+
const tests = [];
|
|
2530
|
+
for (const [name, subschema] of Object.entries(properties2)) {
|
|
2531
|
+
const segments = [...basePath, name];
|
|
2532
|
+
const ref = reference(segments);
|
|
2533
|
+
const compiled = valueTest(subschema, segments, reference, state);
|
|
2534
|
+
if ("unsupported" in compiled) {
|
|
2535
|
+
return compiled;
|
|
2536
|
+
}
|
|
2537
|
+
if (required2.includes(name)) {
|
|
2538
|
+
tests.push(
|
|
2539
|
+
compiled.impliesPresence ? compiled.test : `${ref} !== undefined && ${compiled.test}`
|
|
2540
|
+
);
|
|
2541
|
+
} else if (compiled.test !== "true") {
|
|
2542
|
+
tests.push(`(${ref} === undefined || ${compiled.test})`);
|
|
2543
|
+
state.vacuous = true;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
for (const name of required2) {
|
|
2547
|
+
if (!(name in properties2)) {
|
|
2548
|
+
tests.push(`${reference([...basePath, name])} !== undefined`);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
return { tests };
|
|
2552
|
+
}
|
|
2553
|
+
function valueTest(schema, segments, reference, state) {
|
|
2554
|
+
if (schema === true) {
|
|
2555
|
+
return { test: "true", impliesPresence: false };
|
|
2556
|
+
}
|
|
2557
|
+
if (schema === false) {
|
|
2558
|
+
return { test: "false", impliesPresence: true };
|
|
2559
|
+
}
|
|
2560
|
+
if (!isRecord$1(schema)) {
|
|
2561
|
+
return { unsupported: "a value that is not a schema" };
|
|
2562
|
+
}
|
|
2563
|
+
const ref = reference(segments);
|
|
2564
|
+
const tests = [];
|
|
2565
|
+
let impliesPresence = false;
|
|
2566
|
+
for (const [keyword, value] of Object.entries(schema)) {
|
|
2567
|
+
if (keyword === "type" || keyword === "properties" || keyword === "required" || ANNOTATIONS.has(keyword)) {
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
if (keyword === "const" && isLiteral$1(value)) {
|
|
2571
|
+
tests.push(`${ref} === ${literal(value)}`);
|
|
2572
|
+
impliesPresence ||= value !== null;
|
|
2573
|
+
} else if (keyword === "enum" && Array.isArray(value) && value.every(isLiteral$1)) {
|
|
2574
|
+
tests.push(equalsAny(ref, value));
|
|
2575
|
+
impliesPresence ||= !value.includes(null);
|
|
2576
|
+
} else if (keyword === "not" && negatedValues(value) !== void 0) {
|
|
2577
|
+
tests.push(`!(${equalsAny(ref, negatedValues(value))})`);
|
|
2578
|
+
} else if (BOUND_OPERATORS[keyword] !== void 0 && typeof value === "number") {
|
|
2579
|
+
tests.push(`${ref} ${BOUND_OPERATORS[keyword]} ${value}`);
|
|
2580
|
+
impliesPresence = true;
|
|
2581
|
+
} else {
|
|
2582
|
+
return { unsupported: keyword };
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
if ("properties" in schema || "required" in schema) {
|
|
2586
|
+
const nested = objectTests(
|
|
2587
|
+
{ properties: schema["properties"], required: schema["required"] },
|
|
2588
|
+
segments,
|
|
2589
|
+
reference,
|
|
2590
|
+
state
|
|
2591
|
+
);
|
|
2592
|
+
if ("unsupported" in nested) {
|
|
2593
|
+
return nested;
|
|
2594
|
+
}
|
|
2595
|
+
tests.push(...nested.tests);
|
|
2596
|
+
}
|
|
2597
|
+
return { test: joinAnd(tests), impliesPresence };
|
|
2598
|
+
}
|
|
2599
|
+
const BOUND_OPERATORS = {
|
|
2600
|
+
minimum: ">=",
|
|
2601
|
+
maximum: "<=",
|
|
2602
|
+
exclusiveMinimum: ">",
|
|
2603
|
+
exclusiveMaximum: "<"
|
|
2604
|
+
};
|
|
2605
|
+
function negatedValues(schema) {
|
|
2606
|
+
if (!isRecord$1(schema) || Object.keys(schema).length !== 1) {
|
|
2607
|
+
return void 0;
|
|
2608
|
+
}
|
|
2609
|
+
if ("const" in schema && isLiteral$1(schema["const"])) {
|
|
2610
|
+
return [schema["const"]];
|
|
2611
|
+
}
|
|
2612
|
+
if (Array.isArray(schema["enum"]) && schema["enum"].every(isLiteral$1)) {
|
|
2613
|
+
return schema["enum"];
|
|
2614
|
+
}
|
|
2615
|
+
return void 0;
|
|
2616
|
+
}
|
|
2617
|
+
function joinAnd(tests) {
|
|
2618
|
+
if (tests.length === 0) {
|
|
2619
|
+
return "true";
|
|
2620
|
+
}
|
|
2621
|
+
return tests.join(" && ");
|
|
2622
|
+
}
|
|
2623
|
+
function requiredNames(schema) {
|
|
2624
|
+
return Array.isArray(schema["required"]) ? schema["required"].filter((name) => typeof name === "string") : [];
|
|
2625
|
+
}
|
|
2626
|
+
function isLiteral$1(value) {
|
|
2627
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
2628
|
+
}
|
|
2629
|
+
function isRecord$1(value) {
|
|
2630
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2631
|
+
}
|
|
2632
|
+
function isDeclarativeRule(rule) {
|
|
2633
|
+
return "match" in rule && !("when" in rule);
|
|
2634
|
+
}
|
|
2635
|
+
const NODE_FIELDS = {
|
|
2636
|
+
$type: (node) => node.type,
|
|
2637
|
+
$path: (node) => node.path,
|
|
2638
|
+
$name: (node) => node.name,
|
|
2639
|
+
$defName: (node) => node.defName,
|
|
2640
|
+
$required: (node) => node.required,
|
|
2641
|
+
$inRepeater: (node) => node.repeaterDepth > 0
|
|
2642
|
+
};
|
|
2643
|
+
function matchesDeclarativeRule(rule, node) {
|
|
2644
|
+
return Object.entries(rule.match).every(([key, expected]) => {
|
|
2645
|
+
const readField = NODE_FIELDS[key];
|
|
2646
|
+
const actual = readField ? readField(node) : node.schema[key];
|
|
2647
|
+
if (isMatchOperator(expected)) {
|
|
2648
|
+
return matchesOperator(actual, expected);
|
|
2649
|
+
}
|
|
2650
|
+
if (key === "$path" && typeof expected === "string" && typeof actual === "string") {
|
|
2651
|
+
return matchesPathGlob(expected, actual);
|
|
2652
|
+
}
|
|
2653
|
+
return jsonEqual$2(actual, expected);
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
function isMatchOperator(value) {
|
|
2657
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
2658
|
+
return false;
|
|
2659
|
+
}
|
|
2660
|
+
const keys = Object.keys(value);
|
|
2661
|
+
return keys.length === 1 && ["$in", "$exists", "$regex"].includes(keys[0]);
|
|
2662
|
+
}
|
|
2663
|
+
function matchesOperator(actual, operator) {
|
|
2664
|
+
if ("$in" in operator) {
|
|
2665
|
+
return operator.$in.some((option) => jsonEqual$2(option, actual));
|
|
2666
|
+
}
|
|
2667
|
+
if ("$exists" in operator) {
|
|
2668
|
+
return actual !== void 0 === operator.$exists;
|
|
2669
|
+
}
|
|
2670
|
+
return typeof actual === "string" && new RegExp(operator.$regex).test(actual);
|
|
2671
|
+
}
|
|
2672
|
+
function matchesPathGlob(glob, path) {
|
|
2673
|
+
const matchFrom = (globSegments, pathSegments) => {
|
|
2674
|
+
if (globSegments.length === 0) {
|
|
2675
|
+
return pathSegments.length === 0;
|
|
2676
|
+
}
|
|
2677
|
+
const [head, ...rest] = globSegments;
|
|
2678
|
+
if (head === "**") {
|
|
2679
|
+
return pathSegments.some((_, skip) => matchFrom(rest, pathSegments.slice(skip))) || matchFrom(rest, []);
|
|
2680
|
+
}
|
|
2681
|
+
if (pathSegments.length === 0) {
|
|
2682
|
+
return false;
|
|
2683
|
+
}
|
|
2684
|
+
return (head === "*" || head === pathSegments[0]) && matchFrom(rest, pathSegments.slice(1));
|
|
2685
|
+
};
|
|
2686
|
+
const segmentsOf = (dotPath) => dotPath === "" ? [] : dotPath.split(".");
|
|
2687
|
+
return matchFrom(segmentsOf(glob), segmentsOf(path));
|
|
2688
|
+
}
|
|
2689
|
+
function jsonEqual$2(first, second) {
|
|
2690
|
+
return first === second || JSON.stringify(first) === JSON.stringify(second);
|
|
2691
|
+
}
|
|
2692
|
+
function humanize(name) {
|
|
2693
|
+
return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/\s+/g, " ").trim().replace(/^./, (first) => first.toUpperCase());
|
|
2694
|
+
}
|
|
2695
|
+
function enumOptionsOf(node) {
|
|
2696
|
+
const { schema } = node;
|
|
2697
|
+
if (Array.isArray(schema["enum"])) {
|
|
2698
|
+
const names = Array.isArray(schema["enumNames"]) ? schema["enumNames"] : [];
|
|
2699
|
+
const options = [];
|
|
2700
|
+
schema["enum"].forEach((value, index) => {
|
|
2701
|
+
if (isOptionValue(value)) {
|
|
2702
|
+
const name = names[index];
|
|
2703
|
+
options.push({ label: typeof name === "string" ? name : labelOf$1(value), value });
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
return options;
|
|
2707
|
+
}
|
|
2708
|
+
for (const keyword of ["oneOf", "anyOf"]) {
|
|
2709
|
+
const branches = schema[keyword];
|
|
2710
|
+
if (isConstUnion(branches)) {
|
|
2711
|
+
return branches.filter((branch) => isOptionValue(branch["const"])).map((branch) => ({
|
|
2712
|
+
label: typeof branch["title"] === "string" ? branch["title"] : labelOf$1(branch["const"]),
|
|
2713
|
+
value: branch["const"]
|
|
2714
|
+
}));
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
return void 0;
|
|
2718
|
+
}
|
|
2719
|
+
function isConstUnion(branches) {
|
|
2720
|
+
return Array.isArray(branches) && branches.length > 0 && branches.every(isConstBranch);
|
|
2721
|
+
}
|
|
2722
|
+
function isConstBranch(branch) {
|
|
2723
|
+
return branch !== null && typeof branch === "object" && "const" in branch;
|
|
2724
|
+
}
|
|
2725
|
+
function isOptionValue(value) {
|
|
2726
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
2727
|
+
}
|
|
2728
|
+
function labelOf$1(value) {
|
|
2729
|
+
return typeof value === "string" ? humanize(value) : String(value);
|
|
2730
|
+
}
|
|
2731
|
+
const LOWER_BOUNDS = /* @__PURE__ */ new Set([
|
|
2732
|
+
"minimum",
|
|
2733
|
+
"exclusiveMinimum",
|
|
2734
|
+
"minLength",
|
|
2735
|
+
"minItems",
|
|
2736
|
+
"minProperties",
|
|
2737
|
+
"minContains"
|
|
2738
|
+
]);
|
|
2739
|
+
const UPPER_BOUNDS = /* @__PURE__ */ new Set([
|
|
2740
|
+
"maximum",
|
|
2741
|
+
"exclusiveMaximum",
|
|
2742
|
+
"maxLength",
|
|
2743
|
+
"maxItems",
|
|
2744
|
+
"maxProperties",
|
|
2745
|
+
"maxContains"
|
|
2043
2746
|
]);
|
|
2044
|
-
|
|
2045
|
-
const
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2747
|
+
const MUST_AGREE = /* @__PURE__ */ new Set([
|
|
2748
|
+
"const",
|
|
2749
|
+
"pattern",
|
|
2750
|
+
"format",
|
|
2751
|
+
"multipleOf",
|
|
2752
|
+
"prefixItems",
|
|
2753
|
+
"oneOf",
|
|
2754
|
+
"anyOf",
|
|
2755
|
+
"not",
|
|
2756
|
+
"contains"
|
|
2757
|
+
]);
|
|
2758
|
+
const SUBSCHEMA_MAPS = /* @__PURE__ */ new Set(["properties", "patternProperties"]);
|
|
2759
|
+
const CONFLICT = /* @__PURE__ */ Symbol("conflict");
|
|
2760
|
+
function mergeSchemaParts(parts, pointers) {
|
|
2761
|
+
const schema = {};
|
|
2762
|
+
const conflicts = [];
|
|
2763
|
+
let allowedTypes;
|
|
2764
|
+
for (const part of parts) {
|
|
2765
|
+
const typeIntersection = intersectTypes(allowedTypes, part.allowedTypes);
|
|
2766
|
+
if (typeIntersection === CONFLICT) {
|
|
2767
|
+
conflicts.push({ keyword: "type", pointer: part.pointer });
|
|
2768
|
+
} else {
|
|
2769
|
+
allowedTypes = typeIntersection;
|
|
2770
|
+
}
|
|
2771
|
+
for (const [keyword, value] of Object.entries(part.schema)) {
|
|
2772
|
+
if (keyword === "type") {
|
|
2773
|
+
continue;
|
|
2774
|
+
}
|
|
2775
|
+
if (!(keyword in schema)) {
|
|
2776
|
+
schema[keyword] = value;
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
const combined = combineKeyword(keyword, schema[keyword], value, pointers);
|
|
2780
|
+
if (combined === CONFLICT) {
|
|
2781
|
+
conflicts.push({ keyword, pointer: part.pointer });
|
|
2782
|
+
} else {
|
|
2783
|
+
schema[keyword] = combined;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
return { schema, allowedTypes, conflicts };
|
|
2788
|
+
}
|
|
2789
|
+
function declaredTypes(schema) {
|
|
2790
|
+
const declared = typeof schema["type"] === "string" ? [schema["type"]] : Array.isArray(schema["type"]) ? schema["type"] : void 0;
|
|
2791
|
+
if (declared === void 0) {
|
|
2792
|
+
return void 0;
|
|
2793
|
+
}
|
|
2794
|
+
const types = declared.filter(isJsonType);
|
|
2795
|
+
if (types.includes("number") && !types.includes("integer")) {
|
|
2796
|
+
types.splice(types.indexOf("number") + 1, 0, "integer");
|
|
2797
|
+
}
|
|
2798
|
+
return types;
|
|
2799
|
+
}
|
|
2800
|
+
function isJsonType(value) {
|
|
2801
|
+
return value === "string" || value === "number" || value === "integer" || value === "boolean" || value === "object" || value === "array" || value === "null";
|
|
2802
|
+
}
|
|
2803
|
+
function intersectTypes(current, next) {
|
|
2804
|
+
if (next === void 0) {
|
|
2805
|
+
return current;
|
|
2806
|
+
}
|
|
2807
|
+
if (current === void 0) {
|
|
2808
|
+
return next;
|
|
2809
|
+
}
|
|
2810
|
+
const intersection = current.filter((type2) => next.includes(type2));
|
|
2811
|
+
return intersection.length > 0 ? intersection : CONFLICT;
|
|
2812
|
+
}
|
|
2813
|
+
function combineKeyword(keyword, existing, value, pointers) {
|
|
2814
|
+
if (jsonEqual$1(existing, value)) {
|
|
2815
|
+
return existing;
|
|
2816
|
+
}
|
|
2817
|
+
if (typeof existing === "number" && typeof value === "number") {
|
|
2818
|
+
if (LOWER_BOUNDS.has(keyword)) {
|
|
2819
|
+
return Math.max(existing, value);
|
|
2820
|
+
}
|
|
2821
|
+
if (UPPER_BOUNDS.has(keyword)) {
|
|
2822
|
+
return Math.min(existing, value);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
if (keyword === "required" && Array.isArray(existing) && Array.isArray(value)) {
|
|
2826
|
+
return [...existing, ...value.filter((name) => !existing.includes(name))];
|
|
2827
|
+
}
|
|
2828
|
+
if (keyword === "enum" && Array.isArray(existing) && Array.isArray(value)) {
|
|
2829
|
+
const intersection = existing.filter(
|
|
2830
|
+
(option) => value.some((other) => jsonEqual$1(option, other))
|
|
2831
|
+
);
|
|
2832
|
+
return intersection.length > 0 ? intersection : CONFLICT;
|
|
2833
|
+
}
|
|
2834
|
+
if (keyword === "uniqueItems") {
|
|
2835
|
+
return existing === true || value === true;
|
|
2836
|
+
}
|
|
2837
|
+
if (SUBSCHEMA_MAPS.has(keyword) && isObject(existing) && isObject(value)) {
|
|
2838
|
+
return mergeSubschemaMaps(existing, value, pointers);
|
|
2839
|
+
}
|
|
2840
|
+
if (keyword === "items") {
|
|
2841
|
+
return allOfWrapper(existing, value, pointers);
|
|
2842
|
+
}
|
|
2843
|
+
if (MUST_AGREE.has(keyword)) {
|
|
2844
|
+
return CONFLICT;
|
|
2845
|
+
}
|
|
2846
|
+
return existing;
|
|
2847
|
+
}
|
|
2848
|
+
function mergeSubschemaMaps(existing, added, pointers) {
|
|
2849
|
+
const merged = { ...existing };
|
|
2850
|
+
for (const [name, subschema] of Object.entries(added)) {
|
|
2851
|
+
merged[name] = name in merged ? allOfWrapper(merged[name], subschema, pointers) : subschema;
|
|
2852
|
+
}
|
|
2853
|
+
return merged;
|
|
2854
|
+
}
|
|
2855
|
+
function allOfWrapper(first, second, pointers) {
|
|
2856
|
+
if (jsonEqual$1(first, second)) {
|
|
2857
|
+
return first;
|
|
2858
|
+
}
|
|
2859
|
+
const wrapper = { allOf: [first, second] };
|
|
2860
|
+
const firstPointer = isObject(first) ? pointers.get(first) : void 0;
|
|
2861
|
+
if (firstPointer !== void 0) {
|
|
2862
|
+
pointers.set(wrapper, firstPointer);
|
|
2863
|
+
}
|
|
2864
|
+
return wrapper;
|
|
2865
|
+
}
|
|
2866
|
+
function isObject(value) {
|
|
2867
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2868
|
+
}
|
|
2869
|
+
function jsonEqual$1(first, second) {
|
|
2870
|
+
return first === second || JSON.stringify(first) === JSON.stringify(second);
|
|
2871
|
+
}
|
|
2872
|
+
function escapePointerSegment(segment) {
|
|
2873
|
+
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2874
|
+
}
|
|
2875
|
+
function unescapePointerSegment(segment) {
|
|
2876
|
+
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
2877
|
+
}
|
|
2878
|
+
function appendPointer(pointer, ...segments) {
|
|
2879
|
+
return pointer + segments.map((segment) => `/${escapePointerSegment(String(segment))}`).join("");
|
|
2880
|
+
}
|
|
2881
|
+
function resolveRef(ref, root) {
|
|
2882
|
+
const hashIndex = ref.indexOf("#");
|
|
2883
|
+
const documentPart = hashIndex === -1 ? ref : ref.slice(0, hashIndex);
|
|
2884
|
+
const fragment = hashIndex === -1 ? "" : ref.slice(hashIndex + 1);
|
|
2885
|
+
if (documentPart !== "" && documentPart !== root["$id"]) {
|
|
2886
|
+
return { error: `Only refs inside the same document are supported, found "${ref}".` };
|
|
2887
|
+
}
|
|
2888
|
+
if (fragment !== "" && !fragment.startsWith("/")) {
|
|
2889
|
+
return { error: `Anchor refs are not supported, found "${ref}".` };
|
|
2890
|
+
}
|
|
2891
|
+
let target = root;
|
|
2892
|
+
const segments = [];
|
|
2893
|
+
for (const encodedSegment of fragment === "" ? [] : fragment.slice(1).split("/")) {
|
|
2894
|
+
let segment;
|
|
2895
|
+
try {
|
|
2896
|
+
segment = unescapePointerSegment(decodeURIComponent(encodedSegment));
|
|
2897
|
+
} catch {
|
|
2898
|
+
return { error: `"${ref}" is not a valid URI fragment.` };
|
|
2899
|
+
}
|
|
2900
|
+
if (target === null || typeof target !== "object" || !Object.prototype.hasOwnProperty.call(target, segment)) {
|
|
2901
|
+
return { error: `"${ref}" points to nothing in the document.` };
|
|
2902
|
+
}
|
|
2903
|
+
target = target[segment];
|
|
2904
|
+
segments.push(segment);
|
|
2905
|
+
}
|
|
2906
|
+
return { target, pointer: appendPointer("", ...segments) };
|
|
2907
|
+
}
|
|
2908
|
+
function definitionNameOf(pointer) {
|
|
2909
|
+
const match = /(?:^|\/)(?:\$defs|definitions|components\/schemas)\/([^/]+)$/.exec(pointer);
|
|
2910
|
+
return match ? unescapePointerSegment(match[1]) : void 0;
|
|
2911
|
+
}
|
|
2912
|
+
const RESOLUTION_ONLY_KEYWORDS = [
|
|
2913
|
+
"$defs",
|
|
2914
|
+
"definitions",
|
|
2915
|
+
"$schema",
|
|
2916
|
+
"$id",
|
|
2917
|
+
"$anchor",
|
|
2918
|
+
"$dynamicAnchor"
|
|
2919
|
+
];
|
|
2920
|
+
function normalizeNode(raw, pointer, refTrail, environment) {
|
|
2921
|
+
if (raw === false) {
|
|
2922
|
+
return void 0;
|
|
2923
|
+
}
|
|
2924
|
+
if (raw === true || raw === void 0) {
|
|
2925
|
+
return { schema: {}, nullable: false, conditionals: [], refTrail: [...refTrail] };
|
|
2926
|
+
}
|
|
2927
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2928
|
+
environment.report({
|
|
2929
|
+
severity: "error",
|
|
2930
|
+
code: "invalid-schema",
|
|
2931
|
+
message: "This value is not a JSON Schema object or boolean.",
|
|
2932
|
+
pointer
|
|
2933
|
+
});
|
|
2934
|
+
return void 0;
|
|
2935
|
+
}
|
|
2936
|
+
registerSubschemaPointers(raw, pointer, environment.pointers);
|
|
2937
|
+
const own = rewriteDraftKeywords(raw, pointer);
|
|
2938
|
+
let trail = [...refTrail];
|
|
2939
|
+
let explicitNull = own.nullableKeyword;
|
|
2940
|
+
let defName;
|
|
2941
|
+
const conditionals = [...own.conditionals];
|
|
2942
|
+
const ownPart = {
|
|
2943
|
+
schema: own.schema,
|
|
2944
|
+
pointer,
|
|
2945
|
+
allowedTypes: withNull(declaredTypes(own.schema), own.nullableKeyword)
|
|
2946
|
+
};
|
|
2947
|
+
const parts = [ownPart];
|
|
2948
|
+
const addPart = (node, partPointer, allowsNull = false) => {
|
|
2949
|
+
parts.push({
|
|
2950
|
+
schema: node.schema,
|
|
2951
|
+
pointer: partPointer,
|
|
2952
|
+
allowedTypes: withNull(node.allowedTypes, allowsNull)
|
|
2953
|
+
});
|
|
2954
|
+
conditionals.push(...node.conditionals);
|
|
2955
|
+
trail = [...trail, ...node.refTrail.slice(refTrail.length)];
|
|
2956
|
+
};
|
|
2957
|
+
const ref = own.schema["$ref"];
|
|
2958
|
+
delete own.schema["$ref"];
|
|
2959
|
+
if (typeof ref === "string") {
|
|
2960
|
+
const followed = followRef(ref, pointer, [...refTrail], environment);
|
|
2961
|
+
if (followed === void 0) {
|
|
2962
|
+
return void 0;
|
|
2963
|
+
}
|
|
2964
|
+
addPart(followed.node, followed.pointer);
|
|
2965
|
+
defName = definitionNameOf(followed.pointer) ?? followed.node.defName;
|
|
2966
|
+
}
|
|
2967
|
+
const allOf2 = own.schema["allOf"];
|
|
2968
|
+
delete own.schema["allOf"];
|
|
2969
|
+
if (Array.isArray(allOf2)) {
|
|
2970
|
+
const nodes = allOf2.map((branch, index) => {
|
|
2971
|
+
const branchPointer = appendPointer(pointer, "allOf", index);
|
|
2972
|
+
const node = normalizeNode(branch, branchPointer, refTrail, environment);
|
|
2973
|
+
if (node !== void 0) {
|
|
2974
|
+
addPart(node, branchPointer);
|
|
2975
|
+
}
|
|
2976
|
+
return node;
|
|
2977
|
+
});
|
|
2978
|
+
if (nodes.length === 1) {
|
|
2979
|
+
defName ??= nodes[0]?.defName;
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
const pair = nullablePair(own.schema);
|
|
2983
|
+
if (pair !== void 0) {
|
|
2984
|
+
delete own.schema[pair.keyword];
|
|
2985
|
+
const pairPointer = appendPointer(pointer, pair.keyword, pair.index);
|
|
2986
|
+
const node = normalizeNode(pair.branch, pairPointer, refTrail, environment);
|
|
2987
|
+
if (node === void 0) {
|
|
2988
|
+
return void 0;
|
|
2989
|
+
}
|
|
2990
|
+
addPart(node, pairPointer, true);
|
|
2991
|
+
explicitNull = true;
|
|
2992
|
+
defName ??= node.defName;
|
|
2993
|
+
}
|
|
2994
|
+
const merged = mergeSchemaParts(parts, environment.pointers);
|
|
2995
|
+
for (const conflict of merged.conflicts) {
|
|
2996
|
+
environment.report({
|
|
2997
|
+
severity: "warning",
|
|
2998
|
+
code: "allof-conflict",
|
|
2999
|
+
message: `The combined schemas disagree on \`${conflict.keyword}\`. The first value is used.`,
|
|
3000
|
+
pointer: conflict.pointer
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
const schema = merged.schema;
|
|
3004
|
+
if (Array.isArray(schema["enum"]) && schema["enum"].includes(null)) {
|
|
3005
|
+
schema["enum"] = schema["enum"].filter((option) => option !== null);
|
|
3006
|
+
explicitNull = true;
|
|
3007
|
+
}
|
|
3008
|
+
const resolved = resolveType(merged.allowedTypes, schema, pointer, environment);
|
|
3009
|
+
if (resolved.type === void 0) {
|
|
3010
|
+
delete schema["type"];
|
|
3011
|
+
} else {
|
|
3012
|
+
schema["type"] = resolved.type;
|
|
3013
|
+
}
|
|
3014
|
+
return {
|
|
3015
|
+
schema,
|
|
3016
|
+
type: resolved.type,
|
|
3017
|
+
nullable: explicitNull || resolved.nullable,
|
|
3018
|
+
defName,
|
|
3019
|
+
conditionals,
|
|
3020
|
+
refTrail: trail,
|
|
3021
|
+
allowedTypes: merged.allowedTypes
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
function followRef(ref, pointer, trail, environment) {
|
|
3025
|
+
const repetitions = trail.filter((entry) => entry === ref).length;
|
|
3026
|
+
if (repetitions >= environment.maxRefDepth) {
|
|
3027
|
+
environment.report({
|
|
3028
|
+
severity: "warning",
|
|
3029
|
+
code: "recursive-ref",
|
|
3030
|
+
message: `"${ref}" repeats ${repetitions} times on this branch, so the form stops here.`,
|
|
3031
|
+
pointer
|
|
3032
|
+
});
|
|
3033
|
+
return void 0;
|
|
3034
|
+
}
|
|
3035
|
+
const resolved = resolveRef(ref, environment.root);
|
|
3036
|
+
if ("error" in resolved) {
|
|
3037
|
+
environment.report({
|
|
3038
|
+
severity: "error",
|
|
3039
|
+
code: "unresolved-ref",
|
|
3040
|
+
message: resolved.error,
|
|
3041
|
+
pointer
|
|
3042
|
+
});
|
|
3043
|
+
return void 0;
|
|
3044
|
+
}
|
|
3045
|
+
if (resolved.target !== null && typeof resolved.target === "object") {
|
|
3046
|
+
if (!environment.pointers.has(resolved.target)) {
|
|
3047
|
+
environment.pointers.set(resolved.target, resolved.pointer);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
const node = normalizeNode(resolved.target, resolved.pointer, [...trail, ref], environment);
|
|
3051
|
+
return node === void 0 ? void 0 : { node, pointer: resolved.pointer };
|
|
3052
|
+
}
|
|
3053
|
+
function rewriteDraftKeywords(raw, pointer) {
|
|
3054
|
+
const schema = { ...raw };
|
|
3055
|
+
const conditionals = [];
|
|
3056
|
+
for (const keyword of RESOLUTION_ONLY_KEYWORDS) {
|
|
3057
|
+
delete schema[keyword];
|
|
3058
|
+
}
|
|
3059
|
+
if ("if" in schema) {
|
|
3060
|
+
conditionals.push({
|
|
3061
|
+
kind: "if",
|
|
3062
|
+
if: schema["if"],
|
|
3063
|
+
then: schema["then"],
|
|
3064
|
+
else: schema["else"],
|
|
3065
|
+
pointer
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
delete schema["if"];
|
|
3069
|
+
delete schema["then"];
|
|
3070
|
+
delete schema["else"];
|
|
3071
|
+
for (const keyword of ["dependentRequired", "dependentSchemas", "dependencies"]) {
|
|
3072
|
+
const entries = schema[keyword];
|
|
3073
|
+
delete schema[keyword];
|
|
3074
|
+
if (entries === null || typeof entries !== "object" || Array.isArray(entries)) {
|
|
3075
|
+
continue;
|
|
3076
|
+
}
|
|
3077
|
+
for (const [property, value] of Object.entries(entries)) {
|
|
3078
|
+
const entryPointer = appendPointer(pointer, keyword, property);
|
|
3079
|
+
if (Array.isArray(value)) {
|
|
3080
|
+
if (keyword !== "dependentSchemas") {
|
|
3081
|
+
const required2 = value.filter((name) => typeof name === "string");
|
|
3082
|
+
conditionals.push({
|
|
3083
|
+
kind: "dependentRequired",
|
|
3084
|
+
property,
|
|
3085
|
+
required: required2,
|
|
3086
|
+
pointer: entryPointer
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
} else if (keyword !== "dependentRequired") {
|
|
3090
|
+
conditionals.push({
|
|
3091
|
+
kind: "dependentSchemas",
|
|
3092
|
+
property,
|
|
3093
|
+
schema: value,
|
|
3094
|
+
pointer: entryPointer
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
if (Array.isArray(schema["items"])) {
|
|
3100
|
+
schema["prefixItems"] ??= schema["items"];
|
|
3101
|
+
delete schema["items"];
|
|
3102
|
+
if ("additionalItems" in schema) {
|
|
3103
|
+
schema["items"] = schema["additionalItems"];
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
delete schema["additionalItems"];
|
|
3107
|
+
for (const [exclusive, bound] of [
|
|
3108
|
+
["exclusiveMinimum", "minimum"],
|
|
3109
|
+
["exclusiveMaximum", "maximum"]
|
|
3110
|
+
]) {
|
|
3111
|
+
if (schema[exclusive] === true && typeof schema[bound] === "number") {
|
|
3112
|
+
schema[exclusive] = schema[bound];
|
|
3113
|
+
delete schema[bound];
|
|
3114
|
+
} else if (typeof schema[exclusive] === "boolean") {
|
|
3115
|
+
delete schema[exclusive];
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
if ("example" in schema) {
|
|
3119
|
+
schema["examples"] ??= [schema["example"]];
|
|
3120
|
+
delete schema["example"];
|
|
3121
|
+
}
|
|
3122
|
+
const nullableKeyword = schema["nullable"] === true;
|
|
3123
|
+
delete schema["nullable"];
|
|
3124
|
+
return { schema, conditionals, nullableKeyword };
|
|
3125
|
+
}
|
|
3126
|
+
function nullablePair(schema) {
|
|
3127
|
+
for (const keyword of ["oneOf", "anyOf"]) {
|
|
3128
|
+
const branches = schema[keyword];
|
|
3129
|
+
if (!Array.isArray(branches) || branches.length !== 2) {
|
|
3130
|
+
continue;
|
|
3131
|
+
}
|
|
3132
|
+
const nullIndex = branches.findIndex(onlyAllowsNull);
|
|
3133
|
+
if (nullIndex !== -1 && !onlyAllowsNull(branches[1 - nullIndex])) {
|
|
3134
|
+
return { keyword, branch: branches[1 - nullIndex], index: 1 - nullIndex };
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
return void 0;
|
|
3138
|
+
}
|
|
3139
|
+
function onlyAllowsNull(schema) {
|
|
3140
|
+
if (schema === null || typeof schema !== "object") {
|
|
3141
|
+
return false;
|
|
3142
|
+
}
|
|
3143
|
+
const keywords = schema;
|
|
3144
|
+
return keywords["type"] === "null" || keywords["const"] === null || Array.isArray(keywords["enum"]) && keywords["enum"].length === 1 && keywords["enum"][0] === null;
|
|
3145
|
+
}
|
|
3146
|
+
function withNull(types, addNull) {
|
|
3147
|
+
if (types === void 0 || !addNull || types.includes("null")) {
|
|
3148
|
+
return types;
|
|
3149
|
+
}
|
|
3150
|
+
return [...types, "null"];
|
|
3151
|
+
}
|
|
3152
|
+
function resolveType(allowedTypes, schema, pointer, environment) {
|
|
3153
|
+
if (allowedTypes === void 0) {
|
|
3154
|
+
return { type: inferType(schema), nullable: false };
|
|
3155
|
+
}
|
|
3156
|
+
const nonNull = allowedTypes.filter((type2) => type2 !== "null");
|
|
3157
|
+
const distinct = nonNull.includes("number") ? nonNull.filter((type2) => type2 !== "integer") : nonNull;
|
|
3158
|
+
if (distinct.length === 0) {
|
|
3159
|
+
return { type: "null", nullable: false };
|
|
3160
|
+
}
|
|
3161
|
+
if (distinct.length > 1) {
|
|
3162
|
+
environment.report({
|
|
3163
|
+
severity: "warning",
|
|
3164
|
+
code: "multi-type",
|
|
3165
|
+
message: `The schema allows several types (${distinct.join(", ")}). The form uses the first one, ${distinct[0]}.`,
|
|
3166
|
+
pointer
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
return { type: distinct[0], nullable: allowedTypes.includes("null") };
|
|
3170
|
+
}
|
|
3171
|
+
const STRING_KEYWORDS = ["format", "pattern", "minLength", "maxLength"];
|
|
3172
|
+
const NUMBER_KEYWORDS = [
|
|
3173
|
+
"minimum",
|
|
3174
|
+
"maximum",
|
|
3175
|
+
"exclusiveMinimum",
|
|
3176
|
+
"exclusiveMaximum",
|
|
3177
|
+
"multipleOf"
|
|
3178
|
+
];
|
|
3179
|
+
function inferType(schema) {
|
|
3180
|
+
if (["properties", "additionalProperties", "patternProperties", "required"].some((k) => k in schema)) {
|
|
3181
|
+
return "object";
|
|
3182
|
+
}
|
|
3183
|
+
if ("items" in schema || "prefixItems" in schema) {
|
|
3184
|
+
return "array";
|
|
3185
|
+
}
|
|
3186
|
+
const values = "const" in schema ? [schema["const"]] : Array.isArray(schema["enum"]) ? schema["enum"] : constValues(schema["oneOf"]) ?? constValues(schema["anyOf"]);
|
|
3187
|
+
if (values !== void 0 && values.length > 0) {
|
|
3188
|
+
const types = [...new Set(values.map(typeOfValue))];
|
|
3189
|
+
if (types.every((type2) => type2 === "integer" || type2 === "number")) {
|
|
3190
|
+
return types.includes("number") ? "number" : "integer";
|
|
3191
|
+
}
|
|
3192
|
+
return types.length === 1 ? types[0] : void 0;
|
|
3193
|
+
}
|
|
3194
|
+
if (STRING_KEYWORDS.some((keyword) => keyword in schema)) {
|
|
3195
|
+
return "string";
|
|
3196
|
+
}
|
|
3197
|
+
if (NUMBER_KEYWORDS.some((keyword) => keyword in schema)) {
|
|
3198
|
+
return "number";
|
|
3199
|
+
}
|
|
3200
|
+
return void 0;
|
|
3201
|
+
}
|
|
3202
|
+
function constValues(branches) {
|
|
3203
|
+
if (!Array.isArray(branches) || branches.length === 0) {
|
|
3204
|
+
return void 0;
|
|
3205
|
+
}
|
|
3206
|
+
const values = [];
|
|
3207
|
+
for (const branch of branches) {
|
|
3208
|
+
if (branch === null || typeof branch !== "object" || !("const" in branch)) {
|
|
3209
|
+
return void 0;
|
|
3210
|
+
}
|
|
3211
|
+
values.push(branch["const"]);
|
|
3212
|
+
}
|
|
3213
|
+
return values;
|
|
3214
|
+
}
|
|
3215
|
+
function typeOfValue(value) {
|
|
3216
|
+
if (value === null) {
|
|
3217
|
+
return "null";
|
|
3218
|
+
}
|
|
3219
|
+
if (Array.isArray(value)) {
|
|
3220
|
+
return "array";
|
|
3221
|
+
}
|
|
3222
|
+
if (typeof value === "number") {
|
|
3223
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
3224
|
+
}
|
|
3225
|
+
if (typeof value === "string") {
|
|
3226
|
+
return "string";
|
|
3227
|
+
}
|
|
3228
|
+
if (typeof value === "boolean") {
|
|
3229
|
+
return "boolean";
|
|
3230
|
+
}
|
|
3231
|
+
return "object";
|
|
3232
|
+
}
|
|
3233
|
+
const SINGLE_SUBSCHEMA_KEYWORDS = [
|
|
3234
|
+
"additionalProperties",
|
|
3235
|
+
"additionalItems",
|
|
3236
|
+
"items",
|
|
3237
|
+
"contains",
|
|
3238
|
+
"not",
|
|
3239
|
+
"if",
|
|
3240
|
+
"then",
|
|
3241
|
+
"else",
|
|
3242
|
+
"propertyNames"
|
|
3243
|
+
];
|
|
3244
|
+
const SUBSCHEMA_LIST_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems", "items"];
|
|
3245
|
+
const SUBSCHEMA_MAP_KEYWORDS = [
|
|
3246
|
+
"properties",
|
|
3247
|
+
"patternProperties",
|
|
3248
|
+
"dependentSchemas",
|
|
3249
|
+
"dependencies"
|
|
3250
|
+
];
|
|
3251
|
+
function registerSubschemaPointers(schema, pointer, pointers) {
|
|
3252
|
+
const register = (value, valuePointer) => {
|
|
3253
|
+
if (value !== null && typeof value === "object" && !pointers.has(value)) {
|
|
3254
|
+
pointers.set(value, valuePointer);
|
|
3255
|
+
}
|
|
3256
|
+
};
|
|
3257
|
+
for (const keyword of SINGLE_SUBSCHEMA_KEYWORDS) {
|
|
3258
|
+
if (!Array.isArray(schema[keyword])) {
|
|
3259
|
+
register(schema[keyword], appendPointer(pointer, keyword));
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
for (const keyword of SUBSCHEMA_LIST_KEYWORDS) {
|
|
3263
|
+
const list = schema[keyword];
|
|
3264
|
+
if (Array.isArray(list)) {
|
|
3265
|
+
list.forEach((value, index) => register(value, appendPointer(pointer, keyword, index)));
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
for (const keyword of SUBSCHEMA_MAP_KEYWORDS) {
|
|
3269
|
+
const map = schema[keyword];
|
|
3270
|
+
if (map !== null && typeof map === "object" && !Array.isArray(map)) {
|
|
3271
|
+
for (const [name, value] of Object.entries(map)) {
|
|
3272
|
+
register(value, appendPointer(pointer, keyword, name));
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
function discriminatorOf(unionSchema, branches) {
|
|
3278
|
+
const openApi = unionSchema["discriminator"];
|
|
3279
|
+
if (isRecord(openApi) && typeof openApi["propertyName"] === "string") {
|
|
3280
|
+
const property = openApi["propertyName"];
|
|
3281
|
+
const mapping = isRecord(openApi["mapping"]) ? openApi["mapping"] : {};
|
|
3282
|
+
const values = branches.map(
|
|
3283
|
+
(branch) => constOf(propertyOf(branch.schema, property)) ?? Object.keys(mapping).find((key) => mapping[key] === branch.ref) ?? branch.defName
|
|
3284
|
+
);
|
|
3285
|
+
return distinctValues(values) ? { property, values } : void 0;
|
|
3286
|
+
}
|
|
3287
|
+
const firstProperties = branches[0] ? propertiesOf(branches[0].schema) : {};
|
|
3288
|
+
for (const property of Object.keys(firstProperties)) {
|
|
3289
|
+
const values = branches.map((branch) => constOf(propertyOf(branch.schema, property)));
|
|
3290
|
+
if (distinctValues(values)) {
|
|
3291
|
+
return { property, values };
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
return void 0;
|
|
3295
|
+
}
|
|
3296
|
+
function distinctValues(values) {
|
|
3297
|
+
return values.every((value) => value !== void 0) && new Set(values).size === values.length;
|
|
3298
|
+
}
|
|
3299
|
+
function constOf(schema) {
|
|
3300
|
+
if (!isRecord(schema)) {
|
|
3301
|
+
return void 0;
|
|
3302
|
+
}
|
|
3303
|
+
const value = "const" in schema ? schema["const"] : Array.isArray(schema["enum"]) && schema["enum"].length === 1 ? schema["enum"][0] : void 0;
|
|
3304
|
+
return isLiteral(value) ? value : void 0;
|
|
3305
|
+
}
|
|
3306
|
+
function propertiesOf(schema) {
|
|
3307
|
+
return isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
3308
|
+
}
|
|
3309
|
+
function propertyOf(schema, property) {
|
|
3310
|
+
return propertiesOf(schema)[property];
|
|
3311
|
+
}
|
|
3312
|
+
function isLiteral(value) {
|
|
3313
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
3314
|
+
}
|
|
3315
|
+
function isRecord(value) {
|
|
3316
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3317
|
+
}
|
|
3318
|
+
function asWidgetList(result) {
|
|
3319
|
+
if (result === null || result === void 0) {
|
|
3320
|
+
return [];
|
|
3321
|
+
}
|
|
3322
|
+
return Array.isArray(result) ? result : [result];
|
|
3323
|
+
}
|
|
3324
|
+
const COPIED_PATCH_FIELDS = [
|
|
3325
|
+
"label",
|
|
3326
|
+
"validator",
|
|
3327
|
+
"defaultValue",
|
|
3328
|
+
"readonly",
|
|
3329
|
+
"size",
|
|
3330
|
+
"uid"
|
|
3331
|
+
];
|
|
3332
|
+
function applyPatch(result, patch) {
|
|
3333
|
+
const widgets = asWidgetList(result);
|
|
3334
|
+
if (patch === void 0 || widgets.length === 0) {
|
|
3335
|
+
return result;
|
|
3336
|
+
}
|
|
3337
|
+
const patched = { ...widgets[0] };
|
|
3338
|
+
for (const field of COPIED_PATCH_FIELDS) {
|
|
3339
|
+
if (patch[field] !== void 0) {
|
|
3340
|
+
patched[field] = patch[field];
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
if (patch.props !== void 0) {
|
|
3344
|
+
patched["props"] = { ...widgets[0].props, ...patch.props };
|
|
3345
|
+
}
|
|
3346
|
+
const patchedWidget = patched;
|
|
3347
|
+
return Array.isArray(result) ? [patchedWidget, ...widgets.slice(1)] : patchedWidget;
|
|
3348
|
+
}
|
|
3349
|
+
function withIncludeCondition(result, when) {
|
|
3350
|
+
const widgets = asWidgetList(result).map((widget) => {
|
|
3351
|
+
const existing = widget.include;
|
|
3352
|
+
const combined = existing !== void 0 && "when" in existing ? `(${existing.when}) && ${when}` : when;
|
|
3353
|
+
return { ...widget, include: { when: combined } };
|
|
3354
|
+
});
|
|
3355
|
+
if (result === null || result === void 0) {
|
|
3356
|
+
return result;
|
|
3357
|
+
}
|
|
3358
|
+
return Array.isArray(result) ? widgets : widgets[0];
|
|
3359
|
+
}
|
|
3360
|
+
function removeDuplicateWidgets(widgets, onRemoved) {
|
|
3361
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
3362
|
+
const seenUids = /* @__PURE__ */ new Set();
|
|
3363
|
+
const visitList = (list) => {
|
|
3364
|
+
const kept = [];
|
|
3365
|
+
for (const widget of list) {
|
|
3366
|
+
if (widget.kind === "input" && typeof widget.path === "string") {
|
|
3367
|
+
if (seenPaths.has(widget.path)) {
|
|
3368
|
+
onRemoved(widget, "path");
|
|
3369
|
+
continue;
|
|
3370
|
+
}
|
|
3371
|
+
seenPaths.add(widget.path);
|
|
3372
|
+
}
|
|
3373
|
+
if (typeof widget.uid === "string") {
|
|
3374
|
+
if (seenUids.has(widget.uid)) {
|
|
3375
|
+
onRemoved(widget, "uid");
|
|
3376
|
+
continue;
|
|
3377
|
+
}
|
|
3378
|
+
seenUids.add(widget.uid);
|
|
3379
|
+
}
|
|
3380
|
+
kept.push(visitNested(widget));
|
|
3381
|
+
}
|
|
3382
|
+
return kept;
|
|
3383
|
+
};
|
|
3384
|
+
const visitNested = (widget) => {
|
|
3385
|
+
let visited = widget;
|
|
3386
|
+
if (Array.isArray(widget.children)) {
|
|
3387
|
+
visited = { ...visited, children: visitList(widget.children) };
|
|
3388
|
+
}
|
|
3389
|
+
const template = widget.props?.["template"];
|
|
3390
|
+
if (isWidget(template)) {
|
|
3391
|
+
visited = { ...visited, props: { ...widget.props, template: visitNested(template) } };
|
|
3392
|
+
}
|
|
3393
|
+
return visited;
|
|
3394
|
+
};
|
|
3395
|
+
return visitList(widgets);
|
|
3396
|
+
}
|
|
3397
|
+
const KEY_ORDER = [
|
|
3398
|
+
"kind",
|
|
3399
|
+
"type",
|
|
3400
|
+
"uid",
|
|
3401
|
+
"actionType",
|
|
3402
|
+
"path",
|
|
3403
|
+
"label",
|
|
3404
|
+
"include",
|
|
3405
|
+
"exclude",
|
|
3406
|
+
"disabled",
|
|
3407
|
+
"readonly",
|
|
3408
|
+
"defaultValue",
|
|
3409
|
+
"validator",
|
|
3410
|
+
"size",
|
|
3411
|
+
"on",
|
|
3412
|
+
"props",
|
|
3413
|
+
"children"
|
|
3414
|
+
];
|
|
3415
|
+
function cleanWidget(widget) {
|
|
3416
|
+
const rankOf = (key) => {
|
|
3417
|
+
const rank = KEY_ORDER.indexOf(key.split(".")[0]);
|
|
3418
|
+
return rank === -1 ? KEY_ORDER.length : rank;
|
|
3419
|
+
};
|
|
3420
|
+
const keys = Object.keys(widget).filter((key) => widget[key] !== void 0).map((key, position) => ({ key, position })).sort((first, second) => {
|
|
3421
|
+
const byRank = rankOf(first.key) - rankOf(second.key);
|
|
3422
|
+
if (byRank !== 0) {
|
|
3423
|
+
return byRank;
|
|
3424
|
+
}
|
|
3425
|
+
const bySuffix = Number(first.key.includes(".")) - Number(second.key.includes("."));
|
|
3426
|
+
return bySuffix !== 0 ? bySuffix : first.position - second.position;
|
|
3427
|
+
});
|
|
3428
|
+
const cleaned = {};
|
|
3429
|
+
for (const { key } of keys) {
|
|
3430
|
+
cleaned[key] = widget[key];
|
|
3431
|
+
}
|
|
3432
|
+
if (Array.isArray(widget.children)) {
|
|
3433
|
+
cleaned["children"] = widget.children.map(cleanWidget);
|
|
3434
|
+
}
|
|
3435
|
+
if (widget.props !== void 0) {
|
|
3436
|
+
const props = Object.fromEntries(
|
|
3437
|
+
Object.entries(widget.props).filter(([, value]) => value !== void 0)
|
|
3438
|
+
);
|
|
3439
|
+
if (isWidget(props["template"])) {
|
|
3440
|
+
props["template"] = cleanWidget(props["template"]);
|
|
3441
|
+
}
|
|
3442
|
+
cleaned["props"] = props;
|
|
3443
|
+
}
|
|
3444
|
+
return cleaned;
|
|
3445
|
+
}
|
|
3446
|
+
function isWidget(value) {
|
|
3447
|
+
return value !== null && typeof value === "object" && typeof value.kind === "string" && typeof value.type === "string";
|
|
3448
|
+
}
|
|
3449
|
+
const DEFAULT_VENDOR_KEYWORD = "x-golemui";
|
|
3450
|
+
const DEFAULT_MAX_REF_DEPTH = 2;
|
|
3451
|
+
const DEFAULT_MAX_NODES = 2e3;
|
|
3452
|
+
const UNSUPPORTED_KEYWORDS = [
|
|
3453
|
+
"not",
|
|
3454
|
+
"contains",
|
|
3455
|
+
"propertyNames",
|
|
3456
|
+
"patternProperties",
|
|
3457
|
+
"unevaluatedProperties",
|
|
3458
|
+
"unevaluatedItems",
|
|
3459
|
+
"$dynamicRef",
|
|
3460
|
+
"$recursiveRef"
|
|
3461
|
+
];
|
|
3462
|
+
function fromJsonSchema(schema, options) {
|
|
3463
|
+
return new Conversion(schema, options).run();
|
|
3464
|
+
}
|
|
3465
|
+
class Conversion {
|
|
3466
|
+
constructor(schema, options) {
|
|
3467
|
+
this.schema = schema;
|
|
3468
|
+
this.options = options;
|
|
3469
|
+
this.diagnostics = [];
|
|
3470
|
+
this.pointers = /* @__PURE__ */ new WeakMap();
|
|
3471
|
+
this.normalizedNodes = /* @__PURE__ */ new WeakMap();
|
|
3472
|
+
this.childrenCache = /* @__PURE__ */ new WeakMap();
|
|
3473
|
+
this.itemCache = /* @__PURE__ */ new WeakMap();
|
|
3474
|
+
this.objectUnions = /* @__PURE__ */ new WeakMap();
|
|
3475
|
+
this.states = /* @__PURE__ */ new Map();
|
|
3476
|
+
this.stateCounters = /* @__PURE__ */ new Map();
|
|
3477
|
+
this.widgetNodes = /* @__PURE__ */ new WeakMap();
|
|
3478
|
+
this.builtNodes = 0;
|
|
3479
|
+
this.preset = options.preset;
|
|
3480
|
+
this.userRules = (options.rules ?? []).map(
|
|
3481
|
+
(rule, index) => isDeclarativeRule(rule) ? this.declarativeRuleEntry(rule, `rule ${index}`) : {
|
|
3482
|
+
name: rule.name ?? `rule ${index}`,
|
|
3483
|
+
test: (node) => rule.when(node),
|
|
3484
|
+
build: rule.build
|
|
3485
|
+
}
|
|
3486
|
+
);
|
|
3487
|
+
const presetRules = this.preset.rules.map((rule, index) => ({
|
|
3488
|
+
name: rule.name ?? `${this.preset.name} rule ${index}`,
|
|
3489
|
+
test: (node) => rule.when(node),
|
|
3490
|
+
build: rule.build
|
|
3491
|
+
}));
|
|
3492
|
+
this.allRules = [...this.userRules, ...presetRules];
|
|
3493
|
+
this.refRoot = options.refRoot ?? (typeof schema === "object" ? schema : {});
|
|
3494
|
+
this.vendorKeyword = options.vendorKeyword ?? DEFAULT_VENDOR_KEYWORD;
|
|
3495
|
+
this.maxRefDepth = options.maxRefDepth ?? DEFAULT_MAX_REF_DEPTH;
|
|
3496
|
+
this.maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
3497
|
+
}
|
|
3498
|
+
run() {
|
|
3499
|
+
const root = this.createNode(this.schema, {
|
|
3500
|
+
path: "",
|
|
3501
|
+
pointer: "",
|
|
3502
|
+
required: false,
|
|
3503
|
+
repeaterDepth: 0,
|
|
3504
|
+
refTrail: []
|
|
3505
|
+
});
|
|
3506
|
+
let widgets = [];
|
|
3507
|
+
if (this.schema === false) {
|
|
3508
|
+
this.diagnostics.push({
|
|
3509
|
+
severity: "error",
|
|
3510
|
+
code: "root-not-object",
|
|
3511
|
+
message: "The root schema is `false`, so no data is valid and there is no form.",
|
|
3512
|
+
path: "",
|
|
3513
|
+
pointer: ""
|
|
3514
|
+
});
|
|
3515
|
+
} else if (root !== void 0 && root.type !== "object") {
|
|
3516
|
+
this.report(root, {
|
|
3517
|
+
severity: "error",
|
|
3518
|
+
code: "root-not-object",
|
|
3519
|
+
message: "Only an object schema at the root can become a form."
|
|
3520
|
+
});
|
|
3521
|
+
} else if (root !== void 0) {
|
|
3522
|
+
widgets = this.buildRoot(root);
|
|
3523
|
+
}
|
|
3524
|
+
const form = removeDuplicateWidgets(this.preset.root(widgets), (widget, field) => {
|
|
3525
|
+
const node = this.widgetNodes.get(widget);
|
|
3526
|
+
this.diagnostics.push({
|
|
3527
|
+
severity: "error",
|
|
3528
|
+
code: field === "path" ? "duplicate-path" : "duplicate-uid",
|
|
3529
|
+
message: `Two widgets have the ${field} \`${String(widget[field])}\`. The later one is removed.`,
|
|
3530
|
+
path: node?.path ?? String(widget.path ?? ""),
|
|
3531
|
+
pointer: node?.pointer ?? ""
|
|
3532
|
+
});
|
|
3533
|
+
});
|
|
3534
|
+
const formDefinition = {
|
|
3535
|
+
...this.preset.schemaUrl === void 0 ? {} : { $schema: this.preset.schemaUrl },
|
|
3536
|
+
...this.states.size === 0 ? {} : {
|
|
3537
|
+
states: Object.fromEntries(
|
|
3538
|
+
[...this.states].map(([expression, name]) => [name, expression])
|
|
3539
|
+
)
|
|
3540
|
+
},
|
|
3541
|
+
form: form.map(cleanWidget)
|
|
3542
|
+
};
|
|
3543
|
+
return {
|
|
3544
|
+
formDefinition: this.options.transform?.(formDefinition) ?? formDefinition,
|
|
3545
|
+
diagnostics: this.diagnostics
|
|
3546
|
+
};
|
|
2050
3547
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
3548
|
+
/**
|
|
3549
|
+
* The root object renders its properties directly. A layer that names a widget, or a user
|
|
3550
|
+
* rule, can build it instead. The preset rules never do, they would wrap it in a group.
|
|
3551
|
+
*/
|
|
3552
|
+
buildRoot(root) {
|
|
3553
|
+
const patches = this.patchesFor(root);
|
|
3554
|
+
const namedWidget = this.namedWidget(patches);
|
|
3555
|
+
let result = namedWidget !== void 0 ? this.buildNamedWidget(namedWidget, root) : this.buildWithRules(root, 0, this.userRules);
|
|
3556
|
+
if (result === void 0) {
|
|
3557
|
+
return this.buildChildren(root);
|
|
3558
|
+
}
|
|
3559
|
+
for (const patch of patches) {
|
|
3560
|
+
result = applyPatch(result, patch);
|
|
3561
|
+
}
|
|
3562
|
+
return asWidgetList(result);
|
|
2054
3563
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
3564
|
+
/** Builds a node with every layer, see {@link fromJsonSchema}. */
|
|
3565
|
+
buildNode(node) {
|
|
3566
|
+
if (!this.countNode(node)) {
|
|
3567
|
+
return null;
|
|
3568
|
+
}
|
|
3569
|
+
const patches = this.patchesFor(node);
|
|
3570
|
+
if (patches.some((patch) => patch.skip === true)) {
|
|
3571
|
+
return null;
|
|
3572
|
+
}
|
|
3573
|
+
const namedWidget = this.namedWidget(patches);
|
|
3574
|
+
let result = namedWidget !== void 0 ? this.buildNamedWidget(namedWidget, node) : this.buildWithRules(node, 0, this.allRules);
|
|
3575
|
+
if (result === void 0) {
|
|
3576
|
+
this.report(node, {
|
|
3577
|
+
severity: "error",
|
|
3578
|
+
code: "unsupported-shape",
|
|
3579
|
+
message: `No rule can build this ${node.type ?? "untyped"} schema, so it is not rendered.`
|
|
3580
|
+
});
|
|
3581
|
+
return null;
|
|
3582
|
+
}
|
|
3583
|
+
for (const patch of patches) {
|
|
3584
|
+
result = applyPatch(result, patch);
|
|
3585
|
+
}
|
|
3586
|
+
for (const widget of asWidgetList(result)) {
|
|
3587
|
+
this.widgetNodes.set(widget, node);
|
|
3588
|
+
}
|
|
3589
|
+
return result;
|
|
2058
3590
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
3591
|
+
/** The first rule from `startIndex` on that matches and does not decline builds the node. */
|
|
3592
|
+
buildWithRules(node, startIndex, rules) {
|
|
3593
|
+
for (let index = startIndex; index < rules.length; index++) {
|
|
3594
|
+
const rule = rules[index];
|
|
3595
|
+
let result;
|
|
3596
|
+
try {
|
|
3597
|
+
if (!rule.test(node)) {
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
result = rule.build(node, this.contextFor(node, index, rules));
|
|
3601
|
+
} catch (error) {
|
|
3602
|
+
this.report(node, {
|
|
3603
|
+
severity: "warning",
|
|
3604
|
+
code: "rule-error",
|
|
3605
|
+
message: `${rule.name} threw (${errorMessage(error)}), so the next rule is used.`
|
|
3606
|
+
});
|
|
3607
|
+
continue;
|
|
3608
|
+
}
|
|
3609
|
+
if (result !== void 0) {
|
|
3610
|
+
return applyPatch(result, rule.patch);
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
return void 0;
|
|
2062
3614
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
3615
|
+
/**
|
|
3616
|
+
* Builds the widget type a layer names. A type the preset does not know is most likely a
|
|
3617
|
+
* custom widget of the app, so it becomes a plain input of that type.
|
|
3618
|
+
*/
|
|
3619
|
+
buildNamedWidget(type2, node) {
|
|
3620
|
+
const builder = this.preset.widgets[type2];
|
|
3621
|
+
const context = this.contextFor(node, -1, this.allRules);
|
|
3622
|
+
if (builder === void 0) {
|
|
3623
|
+
this.report(node, {
|
|
3624
|
+
severity: "info",
|
|
3625
|
+
code: "custom-widget",
|
|
3626
|
+
message: `The ${this.preset.name} preset has no \`${type2}\` widget, so a plain input of that type is built.`
|
|
3627
|
+
});
|
|
3628
|
+
return {
|
|
3629
|
+
kind: "input",
|
|
3630
|
+
type: type2,
|
|
3631
|
+
path: node.path,
|
|
3632
|
+
label: context.label(node),
|
|
3633
|
+
validator: context.validator(node)
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
try {
|
|
3637
|
+
return builder(node, context);
|
|
3638
|
+
} catch (error) {
|
|
3639
|
+
this.report(node, {
|
|
3640
|
+
severity: "warning",
|
|
3641
|
+
code: "rule-error",
|
|
3642
|
+
message: `The \`${type2}\` builder threw (${errorMessage(error)}), so the rules are used.`
|
|
3643
|
+
});
|
|
3644
|
+
return this.buildWithRules(node, 0, this.allRules);
|
|
2067
3645
|
}
|
|
2068
3646
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
3647
|
+
/** @param ruleIndex - The rule that builds the node, `-1` for a named widget. */
|
|
3648
|
+
contextFor(node, ruleIndex, rules) {
|
|
3649
|
+
return {
|
|
3650
|
+
next: (target = node) => this.buildWithRules(target, ruleIndex + 1, rules),
|
|
3651
|
+
build: (target) => this.buildNode(target),
|
|
3652
|
+
children: (target) => this.childrenOf(target),
|
|
3653
|
+
buildChildren: (target) => this.buildChildren(target),
|
|
3654
|
+
item: (target) => this.itemOf(target),
|
|
3655
|
+
label: (target) => labelOf(target),
|
|
3656
|
+
validator: (target) => this.preset.validator(target),
|
|
3657
|
+
enumOptions: (target) => enumOptionsOf(target),
|
|
3658
|
+
diagnostic: (diagnostic) => this.diagnostics.push({
|
|
3659
|
+
...diagnostic,
|
|
3660
|
+
path: diagnostic.path ?? node.path,
|
|
3661
|
+
pointer: diagnostic.pointer ?? node.pointer
|
|
3662
|
+
})
|
|
3663
|
+
};
|
|
2072
3664
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
3665
|
+
buildChildren(node) {
|
|
3666
|
+
const children = this.childrenOf(node);
|
|
3667
|
+
const union = this.objectUnions.get(node.schema);
|
|
3668
|
+
const widgets = union ? this.buildUnionChildren(node, children, union) : children.flatMap((child) => asWidgetList(this.buildNode(child)));
|
|
3669
|
+
return this.applyConditionals(node, children, widgets);
|
|
2076
3670
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
3671
|
+
/**
|
|
3672
|
+
* Compiles the conditionals of an object (`if/then/else`, `dependentRequired`,
|
|
3673
|
+
* `dependentSchemas`) into its widgets:
|
|
3674
|
+
* - a property only a branch defines is built once, visible for the branches that define it,
|
|
3675
|
+
* and placed after the last property the condition reads;
|
|
3676
|
+
* - a declared property that a branch constrains gets a `validator.<state>` for that branch.
|
|
3677
|
+
*/
|
|
3678
|
+
applyConditionals(owner, children, widgets) {
|
|
3679
|
+
const branches = this.compileConditionals(owner);
|
|
3680
|
+
if (branches.length === 0) {
|
|
3681
|
+
return widgets;
|
|
3682
|
+
}
|
|
3683
|
+
const declared = new Map(
|
|
3684
|
+
children.flatMap((child) => child.name === void 0 ? [] : [[child.name, child]])
|
|
3685
|
+
);
|
|
3686
|
+
const overlays = /* @__PURE__ */ new Map();
|
|
3687
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
3688
|
+
const add = (map, name, entry) => map.set(name, [...map.get(name) ?? [], entry]);
|
|
3689
|
+
for (const branch of branches) {
|
|
3690
|
+
const schema = branch.schema?.schema ?? {};
|
|
3691
|
+
const properties2 = recordOf(schema["properties"]);
|
|
3692
|
+
const required2 = [...requiredOf(schema), ...branch.requiredNames].filter(
|
|
3693
|
+
(name) => typeof name === "string"
|
|
3694
|
+
);
|
|
3695
|
+
for (const [name, raw] of Object.entries(properties2)) {
|
|
3696
|
+
if (declared.has(name)) {
|
|
3697
|
+
add(overlays, name, { branch, raw, required: required2.includes(name) });
|
|
3698
|
+
} else {
|
|
3699
|
+
add(definitions, name, {
|
|
3700
|
+
branch,
|
|
3701
|
+
raw,
|
|
3702
|
+
required: required2.includes(name),
|
|
3703
|
+
pointer: this.pointerOf(raw) ?? appendPointer(branch.schemaPointer, "properties", name)
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
for (const name of required2) {
|
|
3708
|
+
if (!(name in properties2) && declared.has(name)) {
|
|
3709
|
+
add(overlays, name, { branch, raw: void 0, required: true });
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
const withOverlays = this.applyOverlays(owner, declared, overlays, widgets);
|
|
3714
|
+
return this.insertBranchProperties(owner, definitions, withOverlays);
|
|
2085
3715
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
3716
|
+
compileConditionals(owner) {
|
|
3717
|
+
const conditionals = this.normalizedNodes.get(owner)?.conditionals ?? [];
|
|
3718
|
+
const reference = (segments) => dataReference(owner, joinPath(owner.path, segments.join(".")));
|
|
3719
|
+
const branches = [];
|
|
3720
|
+
conditionals.forEach((conditional, group) => {
|
|
3721
|
+
if (conditional.kind !== "if") {
|
|
3722
|
+
branches.push({
|
|
3723
|
+
condition: `${reference([conditional.property])} !== undefined`,
|
|
3724
|
+
statePrefix: "dep",
|
|
3725
|
+
group,
|
|
3726
|
+
schema: conditional.kind === "dependentSchemas" ? this.normalizeBranch(owner, conditional.schema, conditional.pointer) : void 0,
|
|
3727
|
+
schemaPointer: conditional.pointer,
|
|
3728
|
+
requiredNames: conditional.kind === "dependentRequired" ? conditional.required : [],
|
|
3729
|
+
reads: [conditional.property]
|
|
3730
|
+
});
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
const compiled = compileCondition(conditional.if, reference);
|
|
3734
|
+
if ("unsupported" in compiled) {
|
|
3735
|
+
this.diagnostics.push({
|
|
3736
|
+
severity: "warning",
|
|
3737
|
+
code: "if-unsupported",
|
|
3738
|
+
message: `The \`if\` uses \`${compiled.unsupported}\`, which has no expression equivalent. The \`then\` and \`else\` properties are always shown and optional.`,
|
|
3739
|
+
path: owner.path,
|
|
3740
|
+
pointer: appendPointer(conditional.pointer, "if")
|
|
3741
|
+
});
|
|
3742
|
+
} else if (compiled.vacuous) {
|
|
3743
|
+
this.diagnostics.push({
|
|
3744
|
+
severity: "info",
|
|
3745
|
+
code: "if-vacuous",
|
|
3746
|
+
message: "The `if` also holds when a property it tests is absent, because that property is not in `if.required`. JSON Schema defines it that way.",
|
|
3747
|
+
path: owner.path,
|
|
3748
|
+
pointer: appendPointer(conditional.pointer, "if")
|
|
3749
|
+
});
|
|
3750
|
+
}
|
|
3751
|
+
const condition = "unsupported" in compiled ? void 0 : compiled.expression;
|
|
3752
|
+
for (const side of ["then", "else"]) {
|
|
3753
|
+
const raw = conditional[side];
|
|
3754
|
+
if (raw === void 0) {
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
const schemaPointer = appendPointer(conditional.pointer, side);
|
|
3758
|
+
branches.push({
|
|
3759
|
+
condition: condition === void 0 ? void 0 : side === "then" ? condition : `!(${condition})`,
|
|
3760
|
+
statePrefix: "if",
|
|
3761
|
+
group,
|
|
3762
|
+
side,
|
|
3763
|
+
schema: this.normalizeBranch(owner, raw, schemaPointer),
|
|
3764
|
+
schemaPointer,
|
|
3765
|
+
requiredNames: [],
|
|
3766
|
+
reads: "unsupported" in compiled ? [] : compiled.reads
|
|
3767
|
+
});
|
|
3768
|
+
}
|
|
3769
|
+
});
|
|
3770
|
+
return branches;
|
|
2089
3771
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
3772
|
+
normalizeBranch(owner, raw, pointer) {
|
|
3773
|
+
const branchPointer = this.pointerOf(raw) ?? pointer;
|
|
3774
|
+
const node = normalizeNode(
|
|
3775
|
+
raw,
|
|
3776
|
+
branchPointer,
|
|
3777
|
+
this.refTrailOf(owner),
|
|
3778
|
+
this.environmentFor(owner.path)
|
|
3779
|
+
);
|
|
3780
|
+
if (node !== void 0 && node.conditionals.length > 0) {
|
|
3781
|
+
this.diagnostics.push({
|
|
3782
|
+
severity: "warning",
|
|
3783
|
+
code: "nested-conditional",
|
|
3784
|
+
message: "A conditional inside a conditional branch is not supported, so it is not applied.",
|
|
3785
|
+
path: owner.path,
|
|
3786
|
+
pointer: branchPointer
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
return node;
|
|
2093
3790
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
3791
|
+
/**
|
|
3792
|
+
* Adds `validator.<state>` to the widget of each declared property a branch constrains. One
|
|
3793
|
+
* state applies per field at a time, so only these combinations are expressible: required
|
|
3794
|
+
* from any number of conditions (one state that ORs them), or the branches of one conditional
|
|
3795
|
+
* (they never hold together). Anything else keeps the first conditional and warns.
|
|
3796
|
+
*/
|
|
3797
|
+
applyOverlays(owner, declared, overlays, widgets) {
|
|
3798
|
+
let updated = widgets;
|
|
3799
|
+
for (const [name, list] of overlays) {
|
|
3800
|
+
const child = declared.get(name);
|
|
3801
|
+
const compiled = list.filter((overlay) => overlay.branch.condition !== void 0);
|
|
3802
|
+
if (compiled.length === 0) {
|
|
3803
|
+
continue;
|
|
3804
|
+
}
|
|
3805
|
+
if (innermostItemNode(owner) !== void 0) {
|
|
3806
|
+
this.report(child, {
|
|
3807
|
+
severity: "warning",
|
|
3808
|
+
code: "conditional-validator-in-repeater",
|
|
3809
|
+
message: `Inside a repeater a condition can only show or hide widgets, so the conditional constraints of \`${name}\` are not applied.`
|
|
3810
|
+
});
|
|
3811
|
+
continue;
|
|
3812
|
+
}
|
|
3813
|
+
const validators = {};
|
|
3814
|
+
for (const selected of this.selectOverlays(child, compiled)) {
|
|
3815
|
+
const state = this.stateFor(selected.prefix, selected.condition);
|
|
3816
|
+
const validator = this.preset.validator(
|
|
3817
|
+
this.derivedNode(child, selected.raw, selected.required || child.required)
|
|
3818
|
+
);
|
|
3819
|
+
if (validator !== void 0) {
|
|
3820
|
+
validators[`validator.${state}`] = validator;
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
const index = updated.findIndex(
|
|
3824
|
+
(widget) => widget.kind === "input" && widget.path === child.path
|
|
3825
|
+
);
|
|
3826
|
+
if (index === -1) {
|
|
3827
|
+
this.report(child, {
|
|
3828
|
+
severity: "warning",
|
|
3829
|
+
code: "overlay-limit",
|
|
3830
|
+
message: `\`${name}\` is not built as one input, so its conditional constraints are not applied.`
|
|
3831
|
+
});
|
|
3832
|
+
continue;
|
|
3833
|
+
}
|
|
3834
|
+
updated = [...updated];
|
|
3835
|
+
updated[index] = { ...updated[index], ...validators };
|
|
3836
|
+
}
|
|
3837
|
+
return updated;
|
|
2097
3838
|
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
3839
|
+
selectOverlays(child, overlays) {
|
|
3840
|
+
if (overlays.every((overlay) => overlay.raw === void 0)) {
|
|
3841
|
+
const conditions = [
|
|
3842
|
+
...new Set(overlays.map((overlay) => overlay.branch.condition))
|
|
3843
|
+
];
|
|
3844
|
+
return [
|
|
3845
|
+
{
|
|
3846
|
+
condition: orConditions(conditions),
|
|
3847
|
+
raw: void 0,
|
|
3848
|
+
required: true,
|
|
3849
|
+
prefix: conditions.length === 1 ? overlays[0].branch.statePrefix : "required"
|
|
3850
|
+
}
|
|
3851
|
+
];
|
|
3852
|
+
}
|
|
3853
|
+
const firstGroup = overlays[0].branch.group;
|
|
3854
|
+
const inFirstGroup = overlays.filter((overlay) => overlay.branch.group === firstGroup);
|
|
3855
|
+
if (inFirstGroup.length < overlays.length) {
|
|
3856
|
+
this.report(child, {
|
|
3857
|
+
severity: "warning",
|
|
3858
|
+
code: "overlay-limit",
|
|
3859
|
+
message: `\`${child.name}\` gets constraints from several conditions. Only the first condition is applied.`
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
return inFirstGroup.map((overlay) => ({
|
|
3863
|
+
condition: overlay.branch.condition,
|
|
3864
|
+
raw: overlay.raw,
|
|
3865
|
+
required: overlay.required,
|
|
3866
|
+
prefix: overlay.branch.statePrefix
|
|
3867
|
+
}));
|
|
2101
3868
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
3869
|
+
/** The node with the constraints of a branch merged in, for the validator of that branch. */
|
|
3870
|
+
derivedNode(node, raw, required2) {
|
|
3871
|
+
if (raw === void 0) {
|
|
3872
|
+
return { ...node, required: required2 };
|
|
3873
|
+
}
|
|
3874
|
+
const merged = normalizeNode(
|
|
3875
|
+
{ allOf: [node.schema, raw] },
|
|
3876
|
+
node.pointer,
|
|
3877
|
+
this.refTrailOf(node),
|
|
3878
|
+
this.environmentFor(node.path)
|
|
3879
|
+
);
|
|
3880
|
+
return merged === void 0 ? { ...node, required: required2 } : { ...node, schema: merged.schema, type: merged.type ?? node.type, required: required2 };
|
|
2105
3881
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
3882
|
+
/**
|
|
3883
|
+
* Builds each property that only conditional branches define, once, visible when one of those
|
|
3884
|
+
* branches applies. It goes after the last property its condition reads, or at the end.
|
|
3885
|
+
*/
|
|
3886
|
+
insertBranchProperties(owner, definitions, widgets) {
|
|
3887
|
+
const result = [...widgets];
|
|
3888
|
+
const inserted = /* @__PURE__ */ new Set();
|
|
3889
|
+
for (const [name, list] of definitions) {
|
|
3890
|
+
const [first] = list;
|
|
3891
|
+
const path = joinPath(owner.path, name);
|
|
3892
|
+
if (!this.isValidPropertyName(name, path, first.pointer)) {
|
|
3893
|
+
continue;
|
|
3894
|
+
}
|
|
3895
|
+
const unconditional = list.some((definition) => definition.branch.condition === void 0);
|
|
3896
|
+
const child = this.createNode(first.raw, {
|
|
3897
|
+
parent: owner,
|
|
3898
|
+
name,
|
|
3899
|
+
path,
|
|
3900
|
+
pointer: first.pointer,
|
|
3901
|
+
required: !unconditional && list.every((definition) => definition.required),
|
|
3902
|
+
repeaterDepth: owner.repeaterDepth,
|
|
3903
|
+
refTrail: first.branch.schema?.refTrail ?? this.refTrailOf(owner)
|
|
3904
|
+
});
|
|
3905
|
+
if (child === void 0) {
|
|
3906
|
+
continue;
|
|
3907
|
+
}
|
|
3908
|
+
if (list.some((entry) => !jsonEqual(entry.raw, first.raw) || entry.required !== first.required)) {
|
|
3909
|
+
this.report(child, {
|
|
3910
|
+
severity: "warning",
|
|
3911
|
+
code: "branch-schema-conflict",
|
|
3912
|
+
message: `The branches define \`${name}\` differently. The form uses the first definition.`
|
|
3913
|
+
});
|
|
3914
|
+
}
|
|
3915
|
+
let built = this.buildNode(child);
|
|
3916
|
+
if (!unconditional && !coversBothSides(list)) {
|
|
3917
|
+
const conditions = [
|
|
3918
|
+
...new Set(list.map((definition) => definition.branch.condition))
|
|
3919
|
+
];
|
|
3920
|
+
built = withIncludeCondition(built, orConditions(conditions));
|
|
3921
|
+
}
|
|
3922
|
+
const readPaths = first.branch.reads.map((read) => joinPath(owner.path, read));
|
|
3923
|
+
let position = result.reduce(
|
|
3924
|
+
(last, widget, index) => readPaths.includes(String(widget.path)) ? index + 1 : last,
|
|
3925
|
+
result.length
|
|
3926
|
+
);
|
|
3927
|
+
if (position < result.length || readPaths.length > 0) {
|
|
3928
|
+
while (position < result.length && inserted.has(result[position])) {
|
|
3929
|
+
position++;
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
const builtWidgets = asWidgetList(built);
|
|
3933
|
+
builtWidgets.forEach((widget) => inserted.add(widget));
|
|
3934
|
+
result.splice(position, 0, ...builtWidgets);
|
|
3935
|
+
}
|
|
3936
|
+
return result;
|
|
2114
3937
|
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
3938
|
+
/** The name of the state for an expression. The same expression always gets the same state. */
|
|
3939
|
+
stateFor(prefix, expression) {
|
|
3940
|
+
const existing = this.states.get(expression);
|
|
3941
|
+
if (existing !== void 0) {
|
|
3942
|
+
return existing;
|
|
3943
|
+
}
|
|
3944
|
+
const count = (this.stateCounters.get(prefix) ?? 0) + 1;
|
|
3945
|
+
this.stateCounters.set(prefix, count);
|
|
3946
|
+
const name = `${prefix}${String(count).padStart(2, "0")}`;
|
|
3947
|
+
this.states.set(expression, name);
|
|
3948
|
+
return name;
|
|
2118
3949
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
3950
|
+
/**
|
|
3951
|
+
* Builds an object whose `oneOf`/`anyOf` branches are objects. The discriminator becomes one
|
|
3952
|
+
* selector, in its own place when the object declares it, after the declared properties
|
|
3953
|
+
* otherwise. Then every branch property is built once, visible only for the branches that
|
|
3954
|
+
* define it, so no two widgets ever share a path.
|
|
3955
|
+
*/
|
|
3956
|
+
buildUnionChildren(owner, children, union) {
|
|
3957
|
+
const { discriminator } = union;
|
|
3958
|
+
const widgets = [];
|
|
3959
|
+
let selectorBuilt = false;
|
|
3960
|
+
for (const child of children) {
|
|
3961
|
+
if (discriminator !== void 0 && child.name === discriminator.property) {
|
|
3962
|
+
widgets.push(...this.buildSelector(owner, union, discriminator));
|
|
3963
|
+
selectorBuilt = true;
|
|
3964
|
+
} else {
|
|
3965
|
+
widgets.push(...asWidgetList(this.buildNode(child)));
|
|
3966
|
+
}
|
|
2133
3967
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
3968
|
+
if (discriminator === void 0) {
|
|
3969
|
+
this.report(owner, {
|
|
3970
|
+
severity: "warning",
|
|
3971
|
+
code: "no-discriminator",
|
|
3972
|
+
message: `No property tells the \`${union.keyword}\` branches apart (a \`const\` in every branch, or \`discriminator.propertyName\`). Every branch property is rendered, optional and always visible.`
|
|
3973
|
+
});
|
|
3974
|
+
} else if (!selectorBuilt) {
|
|
3975
|
+
widgets.push(...this.buildSelector(owner, union, discriminator));
|
|
3976
|
+
}
|
|
3977
|
+
widgets.push(...this.buildUnionProperties(owner, union));
|
|
3978
|
+
return widgets;
|
|
3979
|
+
}
|
|
3980
|
+
/**
|
|
3981
|
+
* The discriminator as an enumeration of the branch values, labelled by the branch titles. A
|
|
3982
|
+
* declaration next to the union keeps its annotations, e.g. its title.
|
|
3983
|
+
*/
|
|
3984
|
+
buildSelector(owner, union, discriminator) {
|
|
3985
|
+
const { property, values } = discriminator;
|
|
3986
|
+
const declared = recordOf(owner.schema["properties"])[property];
|
|
3987
|
+
const firstBranchDeclaration = recordOf(union.branches[0].normalized.schema["properties"])[property];
|
|
3988
|
+
const choices = {
|
|
3989
|
+
oneOf: union.branches.map((branch, index) => ({
|
|
3990
|
+
const: values[index],
|
|
3991
|
+
title: branchTitle(branch, values[index])
|
|
3992
|
+
}))
|
|
3993
|
+
};
|
|
3994
|
+
const raw = declared !== void 0 ? { allOf: [declared, choices] } : { title: recordOf(firstBranchDeclaration)["title"], ...choices };
|
|
3995
|
+
const node = this.createNode(raw, {
|
|
3996
|
+
parent: owner,
|
|
3997
|
+
name: property,
|
|
3998
|
+
path: joinPath(owner.path, property),
|
|
3999
|
+
pointer: this.pointerOf(declared) ?? this.pointerOf(firstBranchDeclaration) ?? appendPointer(owner.pointer, "properties", property),
|
|
4000
|
+
required: requiredOf(owner.schema).includes(property) || union.branches.every((branch) => requiredOf(branch.normalized.schema).includes(property)),
|
|
4001
|
+
repeaterDepth: owner.repeaterDepth,
|
|
4002
|
+
refTrail: this.refTrailOf(owner)
|
|
2138
4003
|
});
|
|
4004
|
+
return node === void 0 ? [] : asWidgetList(this.buildNode(node));
|
|
2139
4005
|
}
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
4006
|
+
/** Builds each branch property once, see {@link Conversion.buildUnionChildren}. */
|
|
4007
|
+
buildUnionProperties(owner, union) {
|
|
4008
|
+
const { discriminator } = union;
|
|
4009
|
+
const declared = recordOf(owner.schema["properties"]);
|
|
4010
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
4011
|
+
union.branches.forEach((branch, branchIndex) => {
|
|
4012
|
+
const required2 = requiredOf(branch.normalized.schema);
|
|
4013
|
+
for (const [name, raw] of Object.entries(recordOf(branch.normalized.schema["properties"]))) {
|
|
4014
|
+
if (name === discriminator?.property) {
|
|
4015
|
+
continue;
|
|
4016
|
+
}
|
|
4017
|
+
const pointer = this.pointerOf(raw) ?? appendPointer(branch.pointer, "properties", name);
|
|
4018
|
+
if (name in declared) {
|
|
4019
|
+
if (!jsonEqual(raw, declared[name])) {
|
|
4020
|
+
this.diagnostics.push({
|
|
4021
|
+
severity: "warning",
|
|
4022
|
+
code: "branch-schema-conflict",
|
|
4023
|
+
message: `\`${name}\` is declared next to the union and again in a branch. The declaration next to the union is used.`,
|
|
4024
|
+
path: joinPath(owner.path, name),
|
|
4025
|
+
pointer
|
|
4026
|
+
});
|
|
4027
|
+
}
|
|
4028
|
+
continue;
|
|
4029
|
+
}
|
|
4030
|
+
const list = definitions.get(name) ?? [];
|
|
4031
|
+
list.push({
|
|
4032
|
+
branchIndex,
|
|
4033
|
+
raw,
|
|
4034
|
+
required: required2.includes(name),
|
|
4035
|
+
pointer,
|
|
4036
|
+
refTrail: branch.normalized.refTrail
|
|
4037
|
+
});
|
|
4038
|
+
definitions.set(name, list);
|
|
4039
|
+
}
|
|
2147
4040
|
});
|
|
4041
|
+
const widgets = [];
|
|
4042
|
+
for (const [name, list] of definitions) {
|
|
4043
|
+
const [first] = list;
|
|
4044
|
+
const path = joinPath(owner.path, name);
|
|
4045
|
+
if (!this.isValidPropertyName(name, path, first.pointer)) {
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
const child = this.createNode(first.raw, {
|
|
4049
|
+
parent: owner,
|
|
4050
|
+
name,
|
|
4051
|
+
path,
|
|
4052
|
+
pointer: first.pointer,
|
|
4053
|
+
// Without a discriminator no branch is known to be active, so nothing is required.
|
|
4054
|
+
required: discriminator !== void 0 && first.required,
|
|
4055
|
+
repeaterDepth: owner.repeaterDepth,
|
|
4056
|
+
refTrail: first.refTrail
|
|
4057
|
+
});
|
|
4058
|
+
if (child === void 0) {
|
|
4059
|
+
continue;
|
|
4060
|
+
}
|
|
4061
|
+
if (list.some((entry) => !jsonEqual(entry.raw, first.raw) || entry.required !== first.required)) {
|
|
4062
|
+
this.report(child, {
|
|
4063
|
+
severity: "warning",
|
|
4064
|
+
code: "branch-schema-conflict",
|
|
4065
|
+
message: `The branches define \`${name}\` differently. The form uses the first definition.`
|
|
4066
|
+
});
|
|
4067
|
+
}
|
|
4068
|
+
let result = this.buildNode(child);
|
|
4069
|
+
if (discriminator !== void 0 && list.length < union.branches.length) {
|
|
4070
|
+
const reference = dataReference(owner, joinPath(owner.path, discriminator.property));
|
|
4071
|
+
const branchValues = list.map((entry) => discriminator.values[entry.branchIndex]);
|
|
4072
|
+
result = withIncludeCondition(result, equalsAny(reference, branchValues));
|
|
4073
|
+
}
|
|
4074
|
+
widgets.push(...asWidgetList(result));
|
|
4075
|
+
}
|
|
4076
|
+
return widgets;
|
|
2148
4077
|
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
4078
|
+
/**
|
|
4079
|
+
* The union of object branches of a node, `undefined` when it has none. The branches are
|
|
4080
|
+
* normalized once, here, and the result is cached.
|
|
4081
|
+
*/
|
|
4082
|
+
objectUnionOf(normalized, pointer, path) {
|
|
4083
|
+
const cached = this.objectUnions.get(normalized.schema);
|
|
4084
|
+
if (cached !== void 0) {
|
|
4085
|
+
return cached ?? void 0;
|
|
2155
4086
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
4087
|
+
let union = null;
|
|
4088
|
+
const keyword = ["oneOf", "anyOf"].find(
|
|
4089
|
+
(candidate) => Array.isArray(normalized.schema[candidate]) && !isConstUnion(normalized.schema[candidate])
|
|
4090
|
+
);
|
|
4091
|
+
if (keyword !== void 0) {
|
|
4092
|
+
const environment = this.environmentFor(path);
|
|
4093
|
+
const branches = normalized.schema[keyword].flatMap((raw, index) => {
|
|
4094
|
+
const branchPointer = this.pointerOf(raw) ?? appendPointer(pointer, keyword, index);
|
|
4095
|
+
const node = normalizeNode(raw, branchPointer, normalized.refTrail, environment);
|
|
4096
|
+
return node === void 0 ? [] : [{ raw, pointer: branchPointer, normalized: node }];
|
|
4097
|
+
});
|
|
4098
|
+
if (branches.length > 0 && branches.every((branch) => branch.normalized.type === "object")) {
|
|
4099
|
+
const discriminator = discriminatorOf(
|
|
4100
|
+
normalized.schema,
|
|
4101
|
+
branches.map((branch) => ({
|
|
4102
|
+
schema: branch.normalized.schema,
|
|
4103
|
+
ref: refOf(branch.raw),
|
|
4104
|
+
defName: branch.normalized.defName
|
|
4105
|
+
}))
|
|
4106
|
+
);
|
|
4107
|
+
union = { keyword, branches, discriminator };
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
this.objectUnions.set(normalized.schema, union);
|
|
4111
|
+
return union ?? void 0;
|
|
2166
4112
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
4113
|
+
/** The hint, the `$defs` override and the path override of a node, least specific first. */
|
|
4114
|
+
patchesFor(node) {
|
|
4115
|
+
const overrides = this.options.overrides ?? {};
|
|
4116
|
+
const definitionOverride = node.defName !== void 0 ? overrides[`$defs/${node.defName}`] : void 0;
|
|
4117
|
+
return [node.hint, definitionOverride, overrides[node.path]].filter(
|
|
4118
|
+
(patch) => patch !== void 0
|
|
4119
|
+
);
|
|
2170
4120
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
function stripBranches(s) {
|
|
2174
|
-
const { oneOf: oneOf2, anyOf, ...rest } = s;
|
|
2175
|
-
return rest;
|
|
2176
|
-
}
|
|
2177
|
-
function mapProperty(name, schema, required2, parentPath, unmapped) {
|
|
2178
|
-
const path = parentPath ? `${parentPath}.${name}` : name;
|
|
2179
|
-
const label = humanLabel(schema.title ?? name);
|
|
2180
|
-
const t = Array.isArray(schema.type) ? schema.type.find((x) => x !== "null") : schema.type;
|
|
2181
|
-
if (Array.isArray(schema.enum) && schema.enum.every((v) => typeof v === "string")) {
|
|
2182
|
-
return buildEnumField(path, name, label, schema, required2);
|
|
4121
|
+
namedWidget(patches) {
|
|
4122
|
+
return [...patches].reverse().find((patch) => patch.widget !== void 0)?.widget;
|
|
2183
4123
|
}
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
4124
|
+
declarativeRuleEntry(rule, fallbackName) {
|
|
4125
|
+
return {
|
|
4126
|
+
name: rule.name ?? fallbackName,
|
|
4127
|
+
test: (node) => matchesDeclarativeRule(rule, node),
|
|
4128
|
+
build: (node, context) => {
|
|
4129
|
+
if (rule.skip === true) {
|
|
4130
|
+
return null;
|
|
4131
|
+
}
|
|
4132
|
+
return rule.widget !== void 0 ? this.buildNamedWidget(rule.widget, node) : context.next();
|
|
4133
|
+
},
|
|
4134
|
+
patch: {
|
|
4135
|
+
label: rule.label,
|
|
4136
|
+
props: rule.props,
|
|
4137
|
+
validator: rule.validator,
|
|
4138
|
+
defaultValue: rule.defaultValue,
|
|
4139
|
+
readonly: rule.readonly,
|
|
4140
|
+
size: rule.size,
|
|
4141
|
+
uid: rule.uid
|
|
4142
|
+
}
|
|
4143
|
+
};
|
|
4144
|
+
}
|
|
4145
|
+
/** The child nodes of an object or a tuple, see {@link BuildContext.children}. */
|
|
4146
|
+
childrenOf(node) {
|
|
4147
|
+
const cached = this.childrenCache.get(node);
|
|
4148
|
+
if (cached !== void 0) {
|
|
4149
|
+
return cached;
|
|
4150
|
+
}
|
|
4151
|
+
const children = node.type === "object" ? this.propertyNodes(node) : node.type === "array" ? this.tupleNodes(node) : [];
|
|
4152
|
+
this.childrenCache.set(node, children);
|
|
4153
|
+
return children;
|
|
4154
|
+
}
|
|
4155
|
+
propertyNodes(node) {
|
|
4156
|
+
const properties2 = node.schema["properties"];
|
|
4157
|
+
if (properties2 === null || typeof properties2 !== "object") {
|
|
4158
|
+
return [];
|
|
4159
|
+
}
|
|
4160
|
+
const required2 = Array.isArray(node.schema["required"]) ? node.schema["required"] : [];
|
|
4161
|
+
const children = [];
|
|
4162
|
+
for (const name of this.orderedNames(node, Object.keys(properties2))) {
|
|
4163
|
+
const path = joinPath(node.path, name);
|
|
4164
|
+
const pointer = appendPointer(node.pointer, "properties", name);
|
|
4165
|
+
if (!this.isValidPropertyName(name, path, pointer)) {
|
|
4166
|
+
continue;
|
|
4167
|
+
}
|
|
4168
|
+
const child = this.createNode(properties2[name], {
|
|
4169
|
+
parent: node,
|
|
4170
|
+
name,
|
|
2198
4171
|
path,
|
|
2199
|
-
|
|
4172
|
+
pointer,
|
|
4173
|
+
required: required2.includes(name),
|
|
4174
|
+
repeaterDepth: node.repeaterDepth,
|
|
4175
|
+
refTrail: this.refTrailOf(node)
|
|
2200
4176
|
});
|
|
2201
|
-
|
|
4177
|
+
if (child !== void 0) {
|
|
4178
|
+
children.push(child);
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
return children;
|
|
2202
4182
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
4183
|
+
/** False for a name that cannot be a path segment. Warns about a name the form reads as an index. */
|
|
4184
|
+
isValidPropertyName(name, path, pointer) {
|
|
4185
|
+
if (name === "" || name.includes(".")) {
|
|
4186
|
+
this.diagnostics.push({
|
|
4187
|
+
severity: "error",
|
|
4188
|
+
code: "invalid-property-name",
|
|
4189
|
+
message: `The property name "${name}" cannot be part of a form path, so it is not rendered.`,
|
|
4190
|
+
path,
|
|
4191
|
+
pointer
|
|
4192
|
+
});
|
|
4193
|
+
return false;
|
|
4194
|
+
}
|
|
4195
|
+
if (/^\d+$/.test(name)) {
|
|
4196
|
+
this.diagnostics.push({
|
|
4197
|
+
severity: "warning",
|
|
4198
|
+
code: "numeric-property-name",
|
|
4199
|
+
message: `The form writes "${name}" as an array index, so this object becomes an array.`,
|
|
4200
|
+
path,
|
|
4201
|
+
pointer
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
return true;
|
|
2210
4205
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
4206
|
+
/**
|
|
4207
|
+
* Property names in the `order` of the most specific layer that has one: the path override,
|
|
4208
|
+
* the `$defs` override, the hint, then the first matching declarative rule.
|
|
4209
|
+
*/
|
|
4210
|
+
orderedNames(node, names) {
|
|
4211
|
+
const order = [...this.patchesFor(node)].reverse().find((patch) => patch.order)?.order ?? this.declarativeOrder(node);
|
|
4212
|
+
if (order === void 0) {
|
|
4213
|
+
return names;
|
|
4214
|
+
}
|
|
4215
|
+
const listed = order.filter((name) => name !== "*" && names.includes(name));
|
|
4216
|
+
const rest = names.filter((name) => !listed.includes(name));
|
|
4217
|
+
const restIndex = order.indexOf("*");
|
|
4218
|
+
if (restIndex === -1) {
|
|
4219
|
+
return [...listed, ...rest];
|
|
4220
|
+
}
|
|
4221
|
+
const before = order.slice(0, restIndex).filter((name) => listed.includes(name));
|
|
4222
|
+
const after = order.slice(restIndex + 1).filter((name) => listed.includes(name));
|
|
4223
|
+
return [...before, ...rest, ...after];
|
|
2213
4224
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
4225
|
+
declarativeOrder(node) {
|
|
4226
|
+
for (const rule of this.options.rules ?? []) {
|
|
4227
|
+
if (!isDeclarativeRule(rule) || rule.order === void 0) {
|
|
4228
|
+
continue;
|
|
4229
|
+
}
|
|
4230
|
+
try {
|
|
4231
|
+
if (matchesDeclarativeRule(rule, node)) {
|
|
4232
|
+
return rule.order;
|
|
4233
|
+
}
|
|
4234
|
+
} catch {
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
return void 0;
|
|
2216
4238
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
4239
|
+
tupleNodes(node) {
|
|
4240
|
+
const positions = node.schema["prefixItems"];
|
|
4241
|
+
if (!Array.isArray(positions)) {
|
|
4242
|
+
return [];
|
|
4243
|
+
}
|
|
4244
|
+
const minItems = typeof node.schema["minItems"] === "number" ? node.schema["minItems"] : 0;
|
|
4245
|
+
return positions.flatMap((raw, index) => {
|
|
4246
|
+
const child = this.createNode(raw, {
|
|
4247
|
+
parent: node,
|
|
4248
|
+
path: joinPath(node.path, String(index)),
|
|
4249
|
+
pointer: appendPointer(node.pointer, "prefixItems", index),
|
|
4250
|
+
required: index < minItems,
|
|
4251
|
+
repeaterDepth: node.repeaterDepth,
|
|
4252
|
+
refTrail: this.refTrailOf(node)
|
|
4253
|
+
});
|
|
4254
|
+
return child === void 0 ? [] : [child];
|
|
2225
4255
|
});
|
|
2226
4256
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
4257
|
+
itemOf(node) {
|
|
4258
|
+
if (this.itemCache.has(node)) {
|
|
4259
|
+
return this.itemCache.get(node);
|
|
4260
|
+
}
|
|
4261
|
+
const items = node.schema["items"];
|
|
4262
|
+
const item = node.type === "array" && items !== void 0 && items !== false ? this.createNode(items, {
|
|
4263
|
+
parent: node,
|
|
4264
|
+
path: joinPath(node.path, "items"),
|
|
4265
|
+
pointer: appendPointer(node.pointer, "items"),
|
|
4266
|
+
required: false,
|
|
4267
|
+
repeaterDepth: node.repeaterDepth + 1,
|
|
4268
|
+
refTrail: this.refTrailOf(node)
|
|
4269
|
+
}) : void 0;
|
|
4270
|
+
this.itemCache.set(node, item);
|
|
4271
|
+
return item;
|
|
4272
|
+
}
|
|
4273
|
+
createNode(raw, placement) {
|
|
4274
|
+
const pointer = this.pointerOf(raw) ?? placement.pointer;
|
|
4275
|
+
const normalized = normalizeNode(
|
|
4276
|
+
raw,
|
|
4277
|
+
pointer,
|
|
4278
|
+
placement.refTrail,
|
|
4279
|
+
this.environmentFor(placement.path)
|
|
4280
|
+
);
|
|
4281
|
+
if (normalized === void 0) {
|
|
4282
|
+
return void 0;
|
|
4283
|
+
}
|
|
4284
|
+
if (this.objectUnionOf(normalized, pointer, placement.path) && normalized.type === void 0) {
|
|
4285
|
+
normalized.type = "object";
|
|
4286
|
+
normalized.schema["type"] = "object";
|
|
4287
|
+
}
|
|
4288
|
+
const node = {
|
|
4289
|
+
schema: normalized.schema,
|
|
4290
|
+
type: normalized.type,
|
|
4291
|
+
nullable: normalized.nullable,
|
|
4292
|
+
path: placement.path,
|
|
4293
|
+
name: placement.name,
|
|
4294
|
+
defName: normalized.defName,
|
|
4295
|
+
pointer,
|
|
4296
|
+
required: placement.required,
|
|
4297
|
+
repeaterDepth: placement.repeaterDepth,
|
|
4298
|
+
hint: this.hintOf(normalized, placement.path, pointer),
|
|
4299
|
+
parent: placement.parent
|
|
4300
|
+
};
|
|
4301
|
+
this.normalizedNodes.set(node, normalized);
|
|
4302
|
+
this.reportUnsupported(node, normalized);
|
|
4303
|
+
return node;
|
|
4304
|
+
}
|
|
4305
|
+
hintOf(normalized, path, pointer) {
|
|
4306
|
+
if (this.vendorKeyword === false || !(this.vendorKeyword in normalized.schema)) {
|
|
4307
|
+
return void 0;
|
|
4308
|
+
}
|
|
4309
|
+
const hint = normalized.schema[this.vendorKeyword];
|
|
4310
|
+
if (hint !== null && typeof hint === "object" && !Array.isArray(hint)) {
|
|
4311
|
+
return hint;
|
|
4312
|
+
}
|
|
4313
|
+
this.diagnostics.push({
|
|
4314
|
+
severity: "warning",
|
|
4315
|
+
code: "invalid-hint",
|
|
4316
|
+
message: `\`${this.vendorKeyword}\` must be an object, so it is ignored.`,
|
|
2243
4317
|
path,
|
|
2244
|
-
|
|
2245
|
-
validator,
|
|
2246
|
-
props: {
|
|
2247
|
-
options: values.map((v) => ({ label: humanLabel(v), value: v }))
|
|
2248
|
-
}
|
|
4318
|
+
pointer
|
|
2249
4319
|
});
|
|
4320
|
+
return void 0;
|
|
2250
4321
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
4322
|
+
reportUnsupported(node, normalized) {
|
|
4323
|
+
for (const keyword of UNSUPPORTED_KEYWORDS) {
|
|
4324
|
+
if (keyword in node.schema) {
|
|
4325
|
+
this.report(node, {
|
|
4326
|
+
severity: "warning",
|
|
4327
|
+
code: "unsupported-keyword",
|
|
4328
|
+
message: `\`${keyword}\` has no form equivalent, so the form does not check it.`
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
const additional = node.schema["additionalProperties"];
|
|
4333
|
+
if (additional !== null && typeof additional === "object") {
|
|
4334
|
+
this.report(node, {
|
|
4335
|
+
severity: "warning",
|
|
4336
|
+
code: "unsupported-keyword",
|
|
4337
|
+
message: "No widget edits free-form keys (`additionalProperties` with a schema). Only the declared properties are rendered."
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
4340
|
+
const objectUnion = this.objectUnions.get(node.schema);
|
|
4341
|
+
for (const keyword of ["oneOf", "anyOf"]) {
|
|
4342
|
+
const branches = node.schema[keyword];
|
|
4343
|
+
if (Array.isArray(branches) && !isConstUnion(branches) && objectUnion?.keyword !== keyword) {
|
|
4344
|
+
this.report(node, {
|
|
4345
|
+
severity: "warning",
|
|
4346
|
+
code: "unsupported-keyword",
|
|
4347
|
+
message: `\`${keyword}\` is only supported with object branches, so its branches are not rendered.`
|
|
4348
|
+
});
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
if (node.type !== "object") {
|
|
4352
|
+
for (const conditional of normalized.conditionals) {
|
|
4353
|
+
this.diagnostics.push({
|
|
4354
|
+
severity: "warning",
|
|
4355
|
+
code: "unsupported-keyword",
|
|
4356
|
+
message: `\`${conditional.kind}\` is only supported on objects, so it is not applied.`,
|
|
4357
|
+
path: node.path,
|
|
4358
|
+
pointer: conditional.pointer
|
|
4359
|
+
});
|
|
4360
|
+
}
|
|
2261
4361
|
}
|
|
2262
|
-
});
|
|
2263
|
-
}
|
|
2264
|
-
function buildNumberField(path, label, schema, required2, t) {
|
|
2265
|
-
const validator = buildNumberValidator(schema, required2, t);
|
|
2266
|
-
return cleanFields({
|
|
2267
|
-
kind: "input",
|
|
2268
|
-
type: "number",
|
|
2269
|
-
path,
|
|
2270
|
-
label,
|
|
2271
|
-
validator,
|
|
2272
|
-
props: schema.description ? { hint: schema.description } : void 0
|
|
2273
|
-
});
|
|
2274
|
-
}
|
|
2275
|
-
function buildBooleanField(path, label, schema, required2) {
|
|
2276
|
-
const validator = buildBooleanValidator(schema, required2);
|
|
2277
|
-
return cleanFields({ kind: "input", type: "checkbox", path, label, validator });
|
|
2278
|
-
}
|
|
2279
|
-
function buildObjectGroup(path, _name, _label, schema, unmapped) {
|
|
2280
|
-
const required2 = new Set(schema.required ?? []);
|
|
2281
|
-
const children = [];
|
|
2282
|
-
for (const [childName, childSchema] of Object.entries(schema.properties ?? {})) {
|
|
2283
|
-
const w = mapProperty(childName, unwrap(childSchema), required2.has(childName), path, unmapped);
|
|
2284
|
-
if (w) children.push(w);
|
|
2285
4362
|
}
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
return null;
|
|
4363
|
+
refTrailOf(node) {
|
|
4364
|
+
return this.normalizedNodes.get(node)?.refTrail ?? [];
|
|
2289
4365
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
props: { direction: "column", gap: 8 },
|
|
2294
|
-
children
|
|
2295
|
-
};
|
|
2296
|
-
}
|
|
2297
|
-
function buildArrayField(path, _name, label, schema, required2, unmapped) {
|
|
2298
|
-
const items = schema.items ? unwrap(schema.items) : void 0;
|
|
2299
|
-
if (!items) {
|
|
2300
|
-
unmapped.push({ path, reason: "Array without `items` schema cannot be mapped." });
|
|
2301
|
-
return null;
|
|
4366
|
+
/** The document pointer recorded for a subschema object. */
|
|
4367
|
+
pointerOf(raw) {
|
|
4368
|
+
return raw !== null && typeof raw === "object" ? this.pointers.get(raw) : void 0;
|
|
2302
4369
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
const required22 = new Set(items.required ?? []);
|
|
2306
|
-
const childParent = `${path}.items`;
|
|
2307
|
-
for (const [childName, childSchema] of Object.entries(items.properties ?? {})) {
|
|
2308
|
-
const w = mapProperty(
|
|
2309
|
-
childName,
|
|
2310
|
-
unwrap(childSchema),
|
|
2311
|
-
required22.has(childName),
|
|
2312
|
-
childParent,
|
|
2313
|
-
unmapped
|
|
2314
|
-
);
|
|
2315
|
-
if (w) templateChildren.push(w);
|
|
2316
|
-
}
|
|
2317
|
-
if (!templateChildren.length) {
|
|
2318
|
-
unmapped.push({ path, reason: "Array of objects has no mappable item properties." });
|
|
2319
|
-
return null;
|
|
2320
|
-
}
|
|
4370
|
+
/** Normalization problems get the form data path of the node being normalized. */
|
|
4371
|
+
environmentFor(path) {
|
|
2321
4372
|
return {
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
props: {
|
|
2327
|
-
addLabel: `Add ${singular(label).toLowerCase()}`,
|
|
2328
|
-
removeLabel: "Remove",
|
|
2329
|
-
template: {
|
|
2330
|
-
kind: "layout",
|
|
2331
|
-
type: "flex",
|
|
2332
|
-
props: { direction: "column", gap: 8 },
|
|
2333
|
-
children: templateChildren
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
4373
|
+
root: this.refRoot,
|
|
4374
|
+
maxRefDepth: this.maxRefDepth,
|
|
4375
|
+
pointers: this.pointers,
|
|
4376
|
+
report: (problem) => this.diagnostics.push({ ...problem, path })
|
|
2336
4377
|
};
|
|
2337
4378
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
}
|
|
4379
|
+
/** Counts a node, false once `maxNodes` is reached. The limit is reported once. */
|
|
4380
|
+
countNode(node) {
|
|
4381
|
+
this.builtNodes++;
|
|
4382
|
+
if (this.builtNodes === this.maxNodes + 1) {
|
|
4383
|
+
this.report(node, {
|
|
4384
|
+
severity: "error",
|
|
4385
|
+
code: "max-nodes",
|
|
4386
|
+
message: `The schema has more than ${this.maxNodes} nodes, so the rest is not rendered.`
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
return this.builtNodes <= this.maxNodes;
|
|
2348
4390
|
}
|
|
2349
|
-
|
|
2350
|
-
path,
|
|
2351
|
-
reason: "Array shape is not mappable (only arrays of objects → repeater, or arrays of strings/numbers → tags)."
|
|
2352
|
-
});
|
|
2353
|
-
return null;
|
|
2354
|
-
}
|
|
2355
|
-
function buildArrayValidator(s, required2) {
|
|
2356
|
-
const v = { type: "array" };
|
|
2357
|
-
let used = false;
|
|
2358
|
-
if (required2) {
|
|
2359
|
-
v.required = true;
|
|
2360
|
-
used = true;
|
|
4391
|
+
report(node, diagnostic) {
|
|
4392
|
+
this.diagnostics.push({ ...diagnostic, path: node.path, pointer: node.pointer });
|
|
2361
4393
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
4394
|
+
}
|
|
4395
|
+
function labelOf(node) {
|
|
4396
|
+
const title2 = node.schema["title"];
|
|
4397
|
+
if (typeof title2 === "string" && title2 !== "") {
|
|
4398
|
+
return title2;
|
|
2365
4399
|
}
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
4400
|
+
return node.name !== void 0 ? humanize(node.name) : void 0;
|
|
4401
|
+
}
|
|
4402
|
+
function orConditions(conditions) {
|
|
4403
|
+
return conditions.length === 1 ? conditions[0] : conditions.map((condition) => `(${condition})`).join(" || ");
|
|
4404
|
+
}
|
|
4405
|
+
function coversBothSides(definitions) {
|
|
4406
|
+
return definitions.some(
|
|
4407
|
+
(definition) => definition.branch.side === "then" && definitions.some(
|
|
4408
|
+
(other) => other.branch.group === definition.branch.group && other.branch.side === "else"
|
|
4409
|
+
)
|
|
4410
|
+
);
|
|
4411
|
+
}
|
|
4412
|
+
function branchTitle(branch, value) {
|
|
4413
|
+
const title2 = branch.normalized.schema["title"];
|
|
4414
|
+
if (typeof title2 === "string" && title2 !== "") {
|
|
4415
|
+
return title2;
|
|
2369
4416
|
}
|
|
2370
|
-
if (
|
|
2371
|
-
|
|
2372
|
-
used = true;
|
|
4417
|
+
if (branch.normalized.defName !== void 0) {
|
|
4418
|
+
return humanize(branch.normalized.defName);
|
|
2373
4419
|
}
|
|
2374
|
-
return
|
|
4420
|
+
return typeof value === "string" ? humanize(value) : String(value);
|
|
2375
4421
|
}
|
|
2376
|
-
function
|
|
2377
|
-
|
|
2378
|
-
return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/\s+/g, " ").trim().replace(/^./, (c) => c.toUpperCase());
|
|
4422
|
+
function recordOf(value) {
|
|
4423
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2379
4424
|
}
|
|
2380
|
-
function
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
4425
|
+
function requiredOf(schema) {
|
|
4426
|
+
return Array.isArray(schema["required"]) ? schema["required"] : [];
|
|
4427
|
+
}
|
|
4428
|
+
function refOf(raw) {
|
|
4429
|
+
const ref = recordOf(raw)["$ref"];
|
|
4430
|
+
return typeof ref === "string" ? ref : void 0;
|
|
2384
4431
|
}
|
|
2385
|
-
function
|
|
2386
|
-
|
|
2387
|
-
|
|
4432
|
+
function jsonEqual(first, second) {
|
|
4433
|
+
return first === second || JSON.stringify(first) === JSON.stringify(second);
|
|
4434
|
+
}
|
|
4435
|
+
function joinPath(parent, segment) {
|
|
4436
|
+
return parent === "" ? segment : `${parent}.${segment}`;
|
|
4437
|
+
}
|
|
4438
|
+
function errorMessage(error) {
|
|
4439
|
+
return error instanceof Error ? error.message : String(error);
|
|
4440
|
+
}
|
|
4441
|
+
const FORM_SCHEMA_URL = "https://golemui.com/schemas/form.schema.json";
|
|
4442
|
+
const CUSTOMIZATION_INPUT_PROPERTIES = {
|
|
4443
|
+
rules: {
|
|
4444
|
+
type: "array",
|
|
4445
|
+
description: 'Optional rules that choose or change widgets, checked in order before the gui defaults. Each rule has a `match` object plus widget fields. `match` compares schema keywords by deep equality or with `{ "$in": [...] }`, `{ "$exists": true }` or `{ "$regex": "..." }`, and node fields with `$type`, `$path` (a glob: `*` is one segment, `**` any number), `$name`, `$defName`, `$required` and `$inRepeater`. The widget fields are `widget` (a GolemUI widget type), `label`, `props`, `validator`, `defaultValue`, `readonly`, `size`, `uid`, `skip` and `order`. A rule without `widget` only changes the default widget. Example: `{ "match": { "format": "email" }, "props": { "icon": "mail" } }`.',
|
|
4446
|
+
items: {
|
|
4447
|
+
type: "object",
|
|
4448
|
+
additionalProperties: true,
|
|
4449
|
+
properties: { match: { type: "object", additionalProperties: true } },
|
|
4450
|
+
required: ["match"]
|
|
4451
|
+
}
|
|
4452
|
+
},
|
|
4453
|
+
overrides: {
|
|
4454
|
+
type: "object",
|
|
4455
|
+
description: 'Optional widget fields by form data path, e.g. `{ "address.street": { "widget": "textarea" } }`. Inside arrays the path uses the `items` token (`lines.items.quantity`), and `""` is the root. A key `$defs/<Name>` applies to every node that comes from that definition. The fields are the same as in `rules`. A path override wins over a definition override, and both win over `rules` and over the `x-golemui` keyword in the schema.',
|
|
4456
|
+
additionalProperties: { type: "object", additionalProperties: true }
|
|
2388
4457
|
}
|
|
2389
|
-
|
|
4458
|
+
};
|
|
4459
|
+
function jsonSchemaToGui(schema, options = {}) {
|
|
4460
|
+
const { formDefinition, diagnostics } = fromJsonSchema(schema, {
|
|
4461
|
+
preset: guiPreset({
|
|
4462
|
+
submitAction: options.submitAction,
|
|
4463
|
+
submitLabel: options.submitLabel,
|
|
4464
|
+
rootLayout: options.layout,
|
|
4465
|
+
schemaUrl: FORM_SCHEMA_URL
|
|
4466
|
+
}),
|
|
4467
|
+
rules: options.rules,
|
|
4468
|
+
overrides: options.overrides,
|
|
4469
|
+
refRoot: options.refRoot
|
|
4470
|
+
});
|
|
4471
|
+
const unmapped = diagnostics.filter((diagnostic) => diagnostic.severity !== "info").map((diagnostic) => ({ path: diagnostic.path, reason: diagnostic.message }));
|
|
4472
|
+
return { formDefinition, unmapped, diagnostics };
|
|
2390
4473
|
}
|
|
2391
|
-
function generateFromJsonSchema(
|
|
2392
|
-
const
|
|
2393
|
-
submitAction:
|
|
2394
|
-
submitLabel:
|
|
2395
|
-
layout:
|
|
2396
|
-
|
|
2397
|
-
|
|
4474
|
+
function generateFromJsonSchema(input2) {
|
|
4475
|
+
const { formDefinition, unmapped, diagnostics } = jsonSchemaToGui(input2.jsonSchema, {
|
|
4476
|
+
submitAction: input2.submitAction,
|
|
4477
|
+
submitLabel: input2.submitLabel,
|
|
4478
|
+
layout: input2.layout,
|
|
4479
|
+
rules: input2.rules,
|
|
4480
|
+
overrides: input2.overrides
|
|
4481
|
+
});
|
|
2398
4482
|
const validation = validateFormDefinition({ formDefinition });
|
|
2399
|
-
return { formDefinition, unmapped, validation };
|
|
4483
|
+
return { formDefinition, unmapped, diagnostics, validation };
|
|
2400
4484
|
}
|
|
2401
4485
|
const JSON_GENERATE_FROM_SCHEMA_TOOL = {
|
|
2402
4486
|
name: "json_generate_from_schema",
|
|
2403
|
-
description: "Generate a GolemUI form definition from a JSON Schema describing the form data shape (typically an API request body or a Zod-derived schema). The result is validated against the GolemUI JSON Schemas before being returned, so it is guaranteed to be syntactically correct.
|
|
4487
|
+
description: "Generate a GolemUI form definition from a JSON Schema describing the form data shape (typically an API request body or a Zod-derived schema). The result is validated against the GolemUI JSON Schemas before being returned, so it is guaranteed to be syntactically correct. `diagnostics` lists everything the form cannot express exactly, each with a `severity` (`error`: not rendered, `warning`: rendered approximately, `info`: a note), a `code`, the data `path` and the JSON `pointer` into the input. `unmapped` repeats the errors and warnings as `{ path, reason }`: surface them to the user. Pass `rules` or `overrides` to choose other widgets.",
|
|
2404
4488
|
inputSchema: {
|
|
2405
4489
|
type: "object",
|
|
2406
4490
|
properties: {
|
|
2407
4491
|
jsonSchema: {
|
|
2408
4492
|
type: "object",
|
|
2409
4493
|
additionalProperties: true,
|
|
2410
|
-
description: "A standard JSON Schema. The top level must be an
|
|
4494
|
+
description: "A standard JSON Schema (draft-07 to 2020-12). The top level must be an object schema. Supports primitive types with formats and constraints, enums, local `$ref`/`$defs`, `allOf`, nested objects, arrays (repeaters, multi-selects, tags, tuples), discriminated `oneOf`/`anyOf`, and `if`/`then`/`else`, `dependentRequired`, `dependentSchemas` compiled to conditions and form states."
|
|
2411
4495
|
},
|
|
2412
4496
|
submitAction: {
|
|
2413
4497
|
type: "boolean",
|
|
@@ -2421,45 +4505,51 @@ const JSON_GENERATE_FROM_SCHEMA_TOOL = {
|
|
|
2421
4505
|
type: "string",
|
|
2422
4506
|
enum: ["vertical", "horizontal", "grid"],
|
|
2423
4507
|
description: "Top-level layout. Defaults to vertical (one field per row)."
|
|
2424
|
-
}
|
|
4508
|
+
},
|
|
4509
|
+
...CUSTOMIZATION_INPUT_PROPERTIES
|
|
2425
4510
|
},
|
|
2426
4511
|
required: ["jsonSchema"]
|
|
2427
4512
|
}
|
|
2428
4513
|
};
|
|
2429
4514
|
const METHODS = ["get", "post", "put", "patch", "delete"];
|
|
2430
|
-
async function generateFromOpenapi(
|
|
2431
|
-
if (!
|
|
4515
|
+
async function generateFromOpenapi(input2) {
|
|
4516
|
+
if (!input2.document && !input2.documentUrl) {
|
|
2432
4517
|
throw new Error("Provide either `document` (parsed OpenAPI object) or `documentUrl`.");
|
|
2433
4518
|
}
|
|
2434
|
-
const doc =
|
|
2435
|
-
const resolved = findOperation(doc,
|
|
4519
|
+
const doc = input2.document ?? await fetchDoc(input2.documentUrl);
|
|
4520
|
+
const resolved = findOperation(doc, input2.operation);
|
|
2436
4521
|
if (!resolved) {
|
|
2437
4522
|
throw new Error(
|
|
2438
|
-
`Operation \`${
|
|
4523
|
+
`Operation \`${input2.operation}\` not found. Use the form "METHOD /path" (e.g. "POST /users") or an exact operationId.`
|
|
2439
4524
|
);
|
|
2440
4525
|
}
|
|
2441
4526
|
const { method, path, operation } = resolved;
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
} else {
|
|
2450
|
-
throw new Error(
|
|
2451
|
-
`Operation \`${method.toUpperCase()} ${path}\` has no JSON request body and no parameters — nothing to render as a form.`
|
|
2452
|
-
);
|
|
2453
|
-
}
|
|
2454
|
-
const { formDefinition, unmapped } = jsonSchemaToGui(assembled, {
|
|
2455
|
-
submitAction: input.submitAction ?? true,
|
|
2456
|
-
submitLabel: input.submitLabel ?? defaultSubmitLabel(method, operation)
|
|
4527
|
+
const convert = (schema) => jsonSchemaToGui(schema, {
|
|
4528
|
+
submitAction: input2.submitAction ?? true,
|
|
4529
|
+
submitLabel: input2.submitLabel ?? defaultSubmitLabel(method, operation),
|
|
4530
|
+
rules: input2.rules,
|
|
4531
|
+
overrides: input2.overrides,
|
|
4532
|
+
// `$ref`s in the operation point into the document, e.g. `#/components/schemas/User`.
|
|
4533
|
+
refRoot: doc
|
|
2457
4534
|
});
|
|
4535
|
+
const bodySchema = operation.requestBody?.content?.["application/json"]?.schema;
|
|
4536
|
+
let converted = bodySchema ? convert(bodySchema) : void 0;
|
|
4537
|
+
const bodyIsObject = converted !== void 0 && !converted.diagnostics.some((diagnostic) => diagnostic.code === "root-not-object");
|
|
4538
|
+
if (!bodyIsObject) {
|
|
4539
|
+
if (!operation.parameters?.length) {
|
|
4540
|
+
throw new Error(
|
|
4541
|
+
`Operation \`${method.toUpperCase()} ${path}\` has no JSON request body and no parameters — nothing to render as a form.`
|
|
4542
|
+
);
|
|
4543
|
+
}
|
|
4544
|
+
converted = convert(paramsToSchema(operation.parameters));
|
|
4545
|
+
}
|
|
4546
|
+
const { formDefinition, unmapped, diagnostics } = converted;
|
|
2458
4547
|
const validation = validateFormDefinition({ formDefinition });
|
|
2459
4548
|
return {
|
|
2460
4549
|
resolvedOperation: { method: method.toUpperCase(), path, operationId: operation.operationId },
|
|
2461
4550
|
formDefinition,
|
|
2462
4551
|
unmapped,
|
|
4552
|
+
diagnostics,
|
|
2463
4553
|
validation
|
|
2464
4554
|
};
|
|
2465
4555
|
}
|
|
@@ -2501,13 +4591,15 @@ function findOperation(doc, ref) {
|
|
|
2501
4591
|
}
|
|
2502
4592
|
return null;
|
|
2503
4593
|
}
|
|
2504
|
-
function paramsToSchema(parameters
|
|
4594
|
+
function paramsToSchema(parameters) {
|
|
2505
4595
|
const properties2 = {};
|
|
2506
4596
|
const required2 = [];
|
|
2507
|
-
for (const
|
|
2508
|
-
const
|
|
2509
|
-
properties2[
|
|
2510
|
-
if (
|
|
4597
|
+
for (const parameter of parameters) {
|
|
4598
|
+
const schema = parameter.schema ?? { type: "string" };
|
|
4599
|
+
properties2[parameter.name] = parameter.description === void 0 ? schema : { ...schema, description: parameter.description };
|
|
4600
|
+
if (parameter.required) {
|
|
4601
|
+
required2.push(parameter.name);
|
|
4602
|
+
}
|
|
2511
4603
|
}
|
|
2512
4604
|
return { type: "object", properties: properties2, required: required2 };
|
|
2513
4605
|
}
|
|
@@ -2522,40 +4614,9 @@ function defaultSubmitLabel(method, op) {
|
|
|
2522
4614
|
};
|
|
2523
4615
|
return verbs[method.toLowerCase()] ?? "Submit";
|
|
2524
4616
|
}
|
|
2525
|
-
function derefSchema(schema, doc, depth = 0) {
|
|
2526
|
-
if (depth > 16) return schema;
|
|
2527
|
-
if (typeof schema.$ref === "string") {
|
|
2528
|
-
const target = resolveLocalRef(schema.$ref, doc);
|
|
2529
|
-
if (target) return derefSchema(target, doc, depth + 1);
|
|
2530
|
-
return schema;
|
|
2531
|
-
}
|
|
2532
|
-
const result = { ...schema };
|
|
2533
|
-
if (result.properties) {
|
|
2534
|
-
result.properties = Object.fromEntries(
|
|
2535
|
-
Object.entries(result.properties).map(([k, v]) => [k, derefSchema(v, doc, depth + 1)])
|
|
2536
|
-
);
|
|
2537
|
-
}
|
|
2538
|
-
if (result.items) result.items = derefSchema(result.items, doc, depth + 1);
|
|
2539
|
-
if (result.oneOf) result.oneOf = result.oneOf.map((s) => derefSchema(s, doc, depth + 1));
|
|
2540
|
-
if (result.anyOf) result.anyOf = result.anyOf.map((s) => derefSchema(s, doc, depth + 1));
|
|
2541
|
-
return result;
|
|
2542
|
-
}
|
|
2543
|
-
function resolveLocalRef(ref, doc) {
|
|
2544
|
-
if (!ref.startsWith("#/")) return null;
|
|
2545
|
-
const parts = ref.slice(2).split("/");
|
|
2546
|
-
let cur = doc;
|
|
2547
|
-
for (const p of parts) {
|
|
2548
|
-
if (cur && typeof cur === "object") {
|
|
2549
|
-
cur = cur[p];
|
|
2550
|
-
} else {
|
|
2551
|
-
return null;
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2554
|
-
return cur ?? null;
|
|
2555
|
-
}
|
|
2556
4617
|
const JSON_GENERATE_FROM_OPENAPI_TOOL = {
|
|
2557
4618
|
name: "json_generate_from_openapi",
|
|
2558
|
-
description: 'Generate a GolemUI form for a specific OpenAPI 3.x operation (e.g. "POST /users").
|
|
4619
|
+
description: 'Generate a GolemUI form for a specific OpenAPI 3.x operation (e.g. "POST /users"). Converts the operation\'s JSON request body, with its `$ref`s into the document, to a form definition that is validated against the GolemUI JSON Schemas before being returned, so it is guaranteed syntactically correct. Falls back to operation parameters when no object request body is present. Returns `diagnostics` and `unmapped` like `json_generate_from_schema`: surface them to the user. Pass either a parsed `document` or a `documentUrl` to fetch, and `rules` or `overrides` to choose other widgets.',
|
|
2559
4620
|
inputSchema: {
|
|
2560
4621
|
type: "object",
|
|
2561
4622
|
properties: {
|
|
@@ -2579,7 +4640,8 @@ const JSON_GENERATE_FROM_OPENAPI_TOOL = {
|
|
|
2579
4640
|
submitLabel: {
|
|
2580
4641
|
type: "string",
|
|
2581
4642
|
description: "Label for the submit button. Defaults to the operation summary or a verb derived from the HTTP method."
|
|
2582
|
-
}
|
|
4643
|
+
},
|
|
4644
|
+
...CUSTOMIZATION_INPUT_PROPERTIES
|
|
2583
4645
|
},
|
|
2584
4646
|
required: ["operation"]
|
|
2585
4647
|
}
|
|
@@ -2962,20 +5024,20 @@ function synthesizeExample(widgetType, schema) {
|
|
|
2962
5024
|
if (props?.["label"]) ex["label"] = "Field";
|
|
2963
5025
|
return ex;
|
|
2964
5026
|
}
|
|
2965
|
-
function getWidgetSpec(
|
|
2966
|
-
const schema = COMPONENT_SCHEMAS[
|
|
5027
|
+
function getWidgetSpec(input2) {
|
|
5028
|
+
const schema = COMPONENT_SCHEMAS[input2.widgetType];
|
|
2967
5029
|
if (!schema) {
|
|
2968
5030
|
const known = Object.keys(COMPONENT_SCHEMAS).sort().join(", ");
|
|
2969
|
-
throw new Error(`Unknown widget type \`${
|
|
5031
|
+
throw new Error(`Unknown widget type \`${input2.widgetType}\`. Known: ${known}.`);
|
|
2970
5032
|
}
|
|
2971
5033
|
const props = schema["properties"];
|
|
2972
5034
|
const kindProp = props?.["kind"]?.const ?? "unknown";
|
|
2973
5035
|
return {
|
|
2974
|
-
widgetType:
|
|
5036
|
+
widgetType: input2.widgetType,
|
|
2975
5037
|
schema,
|
|
2976
5038
|
kind: kindProp,
|
|
2977
|
-
example: EXAMPLES[
|
|
2978
|
-
notes: NOTES[
|
|
5039
|
+
example: EXAMPLES[input2.widgetType] ?? synthesizeExample(input2.widgetType, schema),
|
|
5040
|
+
notes: NOTES[input2.widgetType] ?? []
|
|
2979
5041
|
};
|
|
2980
5042
|
}
|
|
2981
5043
|
const JSON_GET_WIDGET_SPEC_TOOL = {
|
|
@@ -3714,11 +5776,11 @@ const CONCEPTS = {
|
|
|
3714
5776
|
icons: ICONS_CONCEPT,
|
|
3715
5777
|
"host-services": HOST_SERVICES_CONCEPT
|
|
3716
5778
|
};
|
|
3717
|
-
function getConcept(
|
|
3718
|
-
const result = CONCEPTS[
|
|
5779
|
+
function getConcept(input2) {
|
|
5780
|
+
const result = CONCEPTS[input2.concept];
|
|
3719
5781
|
if (!result) {
|
|
3720
5782
|
const known = Object.keys(CONCEPTS).sort().map((c) => `\`${c}\``).join(", ");
|
|
3721
|
-
throw new Error(`Unknown concept \`${
|
|
5783
|
+
throw new Error(`Unknown concept \`${input2.concept}\`. Known concepts: ${known}.`);
|
|
3722
5784
|
}
|
|
3723
5785
|
return result;
|
|
3724
5786
|
}
|