fix: decline empty enums, drop mislabelling enumNames, treat $ref as opaque
Copilot round 11. Two of the three are the enum-strip logic this PR added
misbehaving on degenerate input; the third is one more keyword for the
fail-closed rule.
1. An empty `enum` permits nothing at all, but the strip reported "unchanged"
(nothing was removed) and the collapse built a Select with no options — a
required field then had no interaction able to supply a value. It now
reaches the same conclusion as `[null]`: no selectable member, so decline.
The outcome is renamed `unselectable`, since "only-null" no longer describes
both routes to it.
2. Filtering `null` out of `enum` shortens it, which could make an ALREADY
mismatched `enumNames` come out the same length — so `enum: [null, "b"]`
with `enumNames: ["None"]` started labelling `b` as "None", turning labels
both renderers had correctly ignored into a confident mislabel. A list that
was not parallel before filtering is now dropped rather than kept.
3. `$ref` joins the opaque set. It is never resolved here and applies alongside
its siblings, so `{ type: "string", $ref: "#/$defs/intOnly" }` can be
unsatisfiable. The wrapper path already declined on it by virtue of the
annotation allowlist; this extends the same rule to branches, the
`type: [T, "null"]` path, and `admitsNull`.
No round 12 was requested: the review loop was stopped at the user's direction.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
562709a79a
commit
18053d4cb2
1 changed files with 85 additions and 23 deletions
|
|
@ -341,7 +341,27 @@ describe("normalizeNullableUnion", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("leaves a mismatched enumNames alone rather than inventing an alignment", () => {
|
||||
// Keeping a mismatched list is not neutral: filtering shortens `enum`, so a
|
||||
// list both renderers had correctly *ignored* can come out the same length
|
||||
// and start labelling — `enum: [null, "b"]` + `enumNames: ["None"]` would
|
||||
// otherwise label `b` as "None". Dropping it preserves the ignore.
|
||||
it("drops a mismatched enumNames rather than letting filtering align it", () => {
|
||||
expect(
|
||||
normalizeNullableUnion({
|
||||
type: ["string", "null"],
|
||||
enum: [null, "b"],
|
||||
enumNames: ["None"],
|
||||
}),
|
||||
).toEqual({
|
||||
type: "string",
|
||||
enum: ["b"],
|
||||
enumNames: undefined,
|
||||
anyOf: undefined,
|
||||
nullable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("drops a mismatched enumNames that stays mismatched too", () => {
|
||||
expect(
|
||||
normalizeNullableUnion({
|
||||
type: ["string", "null"],
|
||||
|
|
@ -351,12 +371,43 @@ describe("normalizeNullableUnion", () => {
|
|||
).toEqual({
|
||||
type: "string",
|
||||
enum: ["a"],
|
||||
enumNames: ["Alpha"],
|
||||
enumNames: undefined,
|
||||
anyOf: undefined,
|
||||
nullable: true,
|
||||
});
|
||||
});
|
||||
|
||||
// An empty enum permits nothing at all, so it reaches the same conclusion as
|
||||
// `[null]`: no value a dropdown could offer, so do not build one.
|
||||
it("declines to collapse an empty enum", () => {
|
||||
const typeArray = { type: ["string", "null"], enum: [] };
|
||||
expect(normalizeNullableUnion(typeArray)).toBe(typeArray);
|
||||
const union = {
|
||||
anyOf: [{ type: "string", enum: [] }, { type: "null" as const }],
|
||||
};
|
||||
expect(normalizeNullableUnion(union)).toBe(union);
|
||||
});
|
||||
|
||||
// `$ref` applies alongside its siblings and is never resolved here, so a
|
||||
// branch carrying one can be unsatisfiable.
|
||||
it("treats $ref as opaque wherever it appears", () => {
|
||||
const onBranch = {
|
||||
anyOf: [
|
||||
{ type: "string", $ref: "#/$defs/intOnly" },
|
||||
{ type: "null" as const },
|
||||
],
|
||||
};
|
||||
expect(normalizeNullableUnion(onBranch)).toBe(onBranch);
|
||||
const onTypeArray = { type: ["string", "null"], $ref: "#/$defs/intOnly" };
|
||||
expect(normalizeNullableUnion(onTypeArray)).toBe(onTypeArray);
|
||||
expect(admitsNull({ type: ["string", "null"], $ref: "#/$defs/x" })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(admitsNull({ anyOf: [{ type: "null", $ref: "#/$defs/x" }] })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// The type-array path keeps the schema's own keywords, but `collapsed` clears
|
||||
// `anyOf` and the renderers ignore the other applicators — so a compound
|
||||
// schema would be widened into an unconstrained field rather than collapsed.
|
||||
|
|
|
|||
Loading…
Reference in a new issue