diff --git a/.changeset/young-mangos-provide.md b/.changeset/young-mangos-provide.md new file mode 100644 index 000000000..44a1bfccc --- /dev/null +++ b/.changeset/young-mangos-provide.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +handle excess properties for records diff --git a/src/AST.ts b/src/AST.ts index 3881e8750..f18e8781a 100644 --- a/src/AST.ts +++ b/src/AST.ts @@ -1462,7 +1462,8 @@ const unify = (candidates: ReadonlyArray): ReadonlyArray => { return out } -const getParameterBase = ( +/** @internal */ +export const getParameterBase = ( ast: Parameter ): StringKeyword | SymbolKeyword | TemplateLiteral => { switch (ast._tag) { diff --git a/src/Parser.ts b/src/Parser.ts index f50e88174..5cd0cd4bd 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -589,6 +589,9 @@ const go = untracedMethod(() => const indexSignatures = ast.indexSignatures.map((is) => [go(is.parameter, isBoundary), go(is.type, isBoundary)] as const ) + const parameter = go(AST.createUnion( + ast.indexSignatures.map((is) => AST.getParameterBase(is.parameter)) + )) const expectedKeys: any = {} for (let i = 0; i < propertySignatures.length; i++) { expectedKeys[ast.propertySignatures[i].name] = null @@ -605,15 +608,19 @@ const go = untracedMethod(() => // handle excess properties // --------------------------------------------- const onExcessPropertyError = options?.onExcessProperty === "error" - if (onExcessPropertyError && indexSignatures.length === 0) { + if (onExcessPropertyError) { for (const key of I.ownKeys(input)) { if (!(Object.prototype.hasOwnProperty.call(expectedKeys, key))) { - const e = PR.key(key, [PR.unexpected(input[key])]) - if (allErrors) { - es.push([stepKey++, e]) - continue - } else { - return PR.failures(mutableAppend(sortByIndex(es), e)) + const te = parameter(key) + const eu = PR.eitherOrUndefined(te) + if (eu && E.isLeft(eu)) { + const e = PR.key(key, [PR.unexpected(input[key])]) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return PR.failures(mutableAppend(sortByIndex(es), e)) + } } } } @@ -695,78 +702,76 @@ const go = untracedMethod(() => // --------------------------------------------- // handle index signatures // --------------------------------------------- - if (indexSignatures.length > 0) { - for (let i = 0; i < indexSignatures.length; i++) { - const parameter = indexSignatures[i][0] - const type = indexSignatures[i][1] - const keys = I.getKeysForIndexSignature(input, ast.indexSignatures[i].parameter) - for (const key of keys) { - if (Object.prototype.hasOwnProperty.call(expectedKeys, key)) { - continue - } - // --------------------------------------------- - // handle keys - // --------------------------------------------- - const keu = PR.eitherOrUndefined(parameter(key, options)) - if (keu) { - if (E.isLeft(keu)) { - const e = PR.key(key, keu.left.errors) - if (allErrors) { - es.push([stepKey++, e]) - continue - } else { - return PR.failures(mutableAppend(sortByIndex(es), e)) - } + for (let i = 0; i < indexSignatures.length; i++) { + const parameter = indexSignatures[i][0] + const type = indexSignatures[i][1] + const keys = I.getKeysForIndexSignature(input, ast.indexSignatures[i].parameter) + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(expectedKeys, key)) { + continue + } + // --------------------------------------------- + // handle keys + // --------------------------------------------- + const keu = PR.eitherOrUndefined(parameter(key, options)) + if (keu) { + if (E.isLeft(keu)) { + const e = PR.key(key, keu.left.errors) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return PR.failures(mutableAppend(sortByIndex(es), e)) } } - // there's no else here because index signature parameters are restricted to primitives + } + // there's no else here because index signature parameters are restricted to primitives - // --------------------------------------------- - // handle values - // --------------------------------------------- - const vpr = type(input[key], options) - const veu = PR.eitherOrUndefined(vpr) - if (veu) { - if (E.isLeft(veu)) { - const e = PR.key(key, veu.left.errors) - if (allErrors) { - es.push([stepKey++, e]) - continue - } else { - return PR.failures(mutableAppend(sortByIndex(es), e)) - } + // --------------------------------------------- + // handle values + // --------------------------------------------- + const vpr = type(input[key], options) + const veu = PR.eitherOrUndefined(vpr) + if (veu) { + if (E.isLeft(veu)) { + const e = PR.key(key, veu.left.errors) + if (allErrors) { + es.push([stepKey++, e]) + continue } else { - output[key] = veu.right + return PR.failures(mutableAppend(sortByIndex(es), e)) } } else { - const nk = stepKey++ - const index = key - if (!queue) { - queue = [] - } - queue.push( - untracedMethod(() => - ({ es, output }: State) => - Effect.flatMap( - Effect.either(vpr), - (tv) => { - if (E.isLeft(tv)) { - const e = PR.key(index, tv.left.errors) - if (allErrors) { - es.push([nk, e]) - return Effect.unit() - } else { - return PR.failures(mutableAppend(sortByIndex(es), e)) - } - } else { - output[key] = tv.right + output[key] = veu.right + } + } else { + const nk = stepKey++ + const index = key + if (!queue) { + queue = [] + } + queue.push( + untracedMethod(() => + ({ es, output }: State) => + Effect.flatMap( + Effect.either(vpr), + (tv) => { + if (E.isLeft(tv)) { + const e = PR.key(index, tv.left.errors) + if (allErrors) { + es.push([nk, e]) return Effect.unit() + } else { + return PR.failures(mutableAppend(sortByIndex(es), e)) } + } else { + output[key] = tv.right + return Effect.unit() } - ) - ) + } + ) ) - } + ) } } } diff --git a/test/Decoder.ts b/test/Decoder.ts index 96ccc9c20..99de40ebf 100644 --- a/test/Decoder.ts +++ b/test/Decoder.ts @@ -534,6 +534,18 @@ describe.concurrent("Decoder", () => { { a: "a" }, `/a Expected number, actual "a"` ) + const b = Symbol.for("@effect/schema/test/b") + await Util.expectParseFailure( + schema, + { a: 1, [b]: "b" }, + "/Symbol(@effect/schema/test/b) is unexpected", + Util.onExcessPropertyError + ) + await Util.expectParseSuccess( + schema, + { a: 1, [b]: "b" }, + { a: 1 } + ) }) it("struct/ record(symbol, number)", async () => { @@ -552,6 +564,17 @@ describe.concurrent("Decoder", () => { { [a]: "a" }, `/Symbol(@effect/schema/test/a) Expected number, actual "a"` ) + await Util.expectParseFailure( + schema, + { [a]: 1, b: "b" }, + "/b is unexpected", + Util.onExcessPropertyError + ) + await Util.expectParseSuccess( + schema, + { [a]: 1, b: "b" }, + { [a]: 1 } + ) }) it("struct/ record(never, number)", async () => { @@ -731,8 +754,7 @@ describe.concurrent("Decoder", () => { await Util.expectParseSuccess( schema, { a: "a", c: 1 }, - { a: "a" }, - Util.onExcessPropertyIgnore + { a: "a" } ) await Util.expectParseSuccess( schema, diff --git a/test/Encoder.ts b/test/Encoder.ts index fe4d5457c..637b5c159 100644 --- a/test/Encoder.ts +++ b/test/Encoder.ts @@ -313,8 +313,7 @@ describe.concurrent("Encoder", () => { await Util.expectEncodeSuccess( schema, { a: "a", c: 1 }, - { a: "a" }, - Util.onExcessPropertyIgnore + { a: "a" } ) await Util.expectEncodeSuccess( schema, diff --git a/test/dev.ts b/test/dev.ts index 6d941ed1b..6cc026a37 100644 --- a/test/dev.ts +++ b/test/dev.ts @@ -1,13 +1,15 @@ -import { pipe } from "@effect/data/Function" import * as S from "@effect/schema/Schema" +import * as Util from "@effect/schema/test/util" describe.concurrent("dev", () => { it.skip("dev", async () => { - const schema = pipe( - S.string, - S.nonEmpty(), - S.message(() => "bla") + const schema = S.record(S.string, S.number) + const b = Symbol.for("@effect/schema/test/b") + await Util.expectParseFailure( + schema, + { a: 1, [b]: "b" }, + "/Symbol(@effect/schema/test/b) is unexpected", + Util.onExcessPropertyError ) - console.log(S.parse(schema)("")) }) }) diff --git a/test/onExcess.ts b/test/onExcess.ts index 9d91b380a..e8ba2f449 100644 --- a/test/onExcess.ts +++ b/test/onExcess.ts @@ -5,12 +5,11 @@ import * as Util from "@effect/schema/test/util" describe.concurrent("onExcess", () => { it("ignore should not change tuple behaviour", async () => { const schema = S.tuple(S.number) - await Util.expectParseFailure(schema, [1, "b"], "/1 is unexpected", Util.onExcessPropertyIgnore) + await Util.expectParseFailure(schema, [1, "b"], "/1 is unexpected") await Util.expectEncodeFailure( schema, [1, "b"] as any, - `/1 is unexpected`, - Util.onExcessPropertyIgnore + `/1 is unexpected` ) }) @@ -22,8 +21,7 @@ describe.concurrent("onExcess", () => { await Util.expectParseSuccess( schema, { a: 1, b: "b", c: true }, - { a: 1, b: "b" }, - Util.onExcessPropertyIgnore + { a: 1, b: "b" } ) await Util.expectParseFailure( schema, @@ -34,8 +32,7 @@ describe.concurrent("onExcess", () => { await Util.expectEncodeSuccess( schema, { a: 1, b: "b" }, - { a: 1, b: "b" }, - Util.onExcessPropertyIgnore + { a: 1, b: "b" } ) await Util.expectEncodeSuccess( schema, @@ -52,8 +49,7 @@ describe.concurrent("onExcess", () => { await Util.expectParseFailure( schema, [1, "b", true], - `union member: /2 is unexpected, union member: /1 is unexpected`, - Util.onExcessPropertyIgnore + `union member: /2 is unexpected, union member: /1 is unexpected` ) await Util.expectParseFailure( schema, @@ -64,8 +60,7 @@ describe.concurrent("onExcess", () => { await Util.expectEncodeSuccess( schema, [1, "b"], - [1, "b"], - Util.onExcessPropertyIgnore + [1, "b"] ) await Util.expectEncodeSuccess( schema, @@ -82,8 +77,7 @@ describe.concurrent("onExcess", () => { await Util.expectParseSuccess( schema, [{ b: 1, c: "c" }], - [{ b: 1 }], - Util.onExcessPropertyIgnore + [{ b: 1 }] ) }) @@ -92,8 +86,7 @@ describe.concurrent("onExcess", () => { await Util.expectParseSuccess( schema, [{ b: 1, c: "c" }], - [{ b: 1 }], - Util.onExcessPropertyIgnore + [{ b: 1 }] ) }) @@ -103,8 +96,7 @@ describe.concurrent("onExcess", () => { await Util.expectParseSuccess( schema, [{ b: 1, c: "c" }], - [{ b: 1 }], - Util.onExcessPropertyIgnore + [{ b: 1 }] ) }) @@ -113,8 +105,7 @@ describe.concurrent("onExcess", () => { await Util.expectParseSuccess( schema, { a: 1, b: "b" }, - { a: 1 }, - Util.onExcessPropertyIgnore + { a: 1 } ) }) @@ -125,8 +116,7 @@ describe.concurrent("onExcess", () => { { a: { b: 1, c: "c" } }, { a: { b: 1 } - }, - Util.onExcessPropertyIgnore + } ) }) @@ -135,8 +125,7 @@ describe.concurrent("onExcess", () => { await Util.expectParseSuccess( schema, { a: { b: 1, c: "c" } }, - { a: { b: 1 } }, - Util.onExcessPropertyIgnore + { a: { b: 1 } } ) }) }) diff --git a/test/util.ts b/test/util.ts index 7f38c3834..747808ee8 100644 --- a/test/util.ts +++ b/test/util.ts @@ -122,10 +122,6 @@ export const roundtrip = (schema: Schema) => { } } -export const onExcessPropertyIgnore: ParseOptions = { - onExcessProperty: "ignore" -} - export const onExcessPropertyError: ParseOptions = { onExcessProperty: "error" }