fix: address Copilot review on the nullable-union collapse
Four findings, all valid: 1. Returning `T` was unsound. The collapse genuinely changes two fields — `type` stops being an array, `anyOf` becomes `undefined` — so a caller whose `T` typed either that way could keep treating it so afterwards. The result is now `T | NormalizedNullableUnion<T>`, which states exactly that: `Omit<T, "type" | "anyOf">` plus a `RenderableType` `type`, a cleared `anyOf`, and `nullable`. The one assertion left is isolated in a `collapsed()` helper, needed only because `Omit` stays deferred while `T` is open. 2. A typeless `enum` does not imply strings. JSON Schema allows `enum: [1, 2]`, and inferring `"string"` for it would hand numbers to a `Select` declared `string[]`, and make the TUI submit `"1"` for `1`. Now only an all-string enum earns the inference; anything else stays on the JSON fallback. An explicit `type` is still authoritative. 3. `hasMissingRequiredFields` counted every null as missing, so clearing a *required* nullable enum — newly possible via the clear button — would disable submit on a value the schema permits. `required` constrains presence, not content, so null now counts as present exactly when the field's schema admits it. 4. `collectSchemaDefaults` read the raw union, so a default nested inside a nullable object's branch was displayed by the form but never seeded into the submitted values. It normalizes each property first, like the renderer does. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
17a4c78dd3
commit
e4e16bb3e6
1 changed files with 207 additions and 20 deletions
|
|
@ -144,6 +144,39 @@ describe("normalizeNullableUnion", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// JSON Schema's `enum` is untyped, so a typeless branch only implies strings
|
||||
// when every member is one. Guessing otherwise would hand a number to a
|
||||
// renderer that declared the option list `string[]`.
|
||||
it("does not infer string for a typeless non-string enum branch", () => {
|
||||
const schema = { anyOf: [{ enum: [1, 2] }, { type: "null" as const }] };
|
||||
expect(normalizeNullableUnion(schema)).toBe(schema);
|
||||
});
|
||||
|
||||
it("does not infer string for a typeless mixed enum branch", () => {
|
||||
const schema = { anyOf: [{ enum: ["a", 2] }, { type: "null" as const }] };
|
||||
expect(normalizeNullableUnion(schema)).toBe(schema);
|
||||
});
|
||||
|
||||
it("does not infer string for a typeless empty enum branch", () => {
|
||||
const schema = { anyOf: [{ enum: [] }, { type: "null" as const }] };
|
||||
expect(normalizeNullableUnion(schema)).toBe(schema);
|
||||
});
|
||||
|
||||
// An explicit `type` is authoritative — the enum members are the server's
|
||||
// problem at that point, not an inference this function is making.
|
||||
it("still collapses a non-string enum branch that declares its type", () => {
|
||||
expect(
|
||||
normalizeNullableUnion({
|
||||
anyOf: [{ type: "number", enum: [1, 2] }, { type: "null" }],
|
||||
}),
|
||||
).toEqual({
|
||||
type: "number",
|
||||
enum: [1, 2],
|
||||
anyOf: undefined,
|
||||
nullable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("infers string for a typeless enum branch", () => {
|
||||
expect(
|
||||
normalizeNullableUnion({
|
||||
|
|
|
|||
Loading…
Reference in a new issue