From e572b076e9b4369d9cc8e55414006eef376c93d9 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 15 Feb 2024 01:53:31 +1300 Subject: [PATCH] allow passing structs when encoding schema classes (#2128) Co-authored-by: gcanti --- .changeset/shaggy-trees-do.md | 26 +++++++ packages/schema/src/Schema.ts | 19 ++++- packages/schema/test/Schema/Class.test.ts | 85 ++++++++++++++++------- 3 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 .changeset/shaggy-trees-do.md diff --git a/.changeset/shaggy-trees-do.md b/.changeset/shaggy-trees-do.md new file mode 100644 index 00000000000..50f6b52ea85 --- /dev/null +++ b/.changeset/shaggy-trees-do.md @@ -0,0 +1,26 @@ +--- +"@effect/schema": patch +--- + +allow passing structs when encoding schema classes + +The following will no longer throw an error: + +```ts +import * as S from "@effect/schema/Schema"; + +class C extends S.Class()({ + n: S.NumberFromString, +}) { + get b() { + return 1; + } +} +class D extends S.Class()({ + n: S.NumberFromString, + b: S.number, +}) {} + +console.log(S.encodeSync(D)(new C({ n: 1 }))); +// Output: { b: 1, n: '1' } +``` diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 045c875f6a4..bb81d90738c 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -4896,7 +4896,7 @@ const makeClass = ( Base: any, additionalProps?: any ): any => { - const validator = Parser.validateSync(selfSchema) + const validate = Parser.validateSync(selfSchema) return class extends Base { constructor(props?: any, disableValidation = false) { @@ -4904,7 +4904,7 @@ const makeClass = ( props = { ...additionalProps, ...props } } if (disableValidation !== true) { - props = validator(props) + props = validate(props) } super(props, true) } @@ -4921,10 +4921,23 @@ const makeClass = ( static get ast() { const toSchema = to(selfSchema) + const encode = Parser.encodeUnknown(toSchema) const pretty = Pretty.make(toSchema) const arb = arbitrary.make(toSchema) const declaration: Schema = declare( - (input): input is any => input instanceof this, + [], + () => (input, _, ast) => + input instanceof this ? ParseResult.succeed(input) : ParseResult.fail(ParseResult.type(ast, input)), + () => (input, _, ast) => + input instanceof this + ? ParseResult.succeed(input) + : ParseResult.mapError( + ParseResult.map( + encode(input), + (props) => new this(props, true) + ), + () => ParseResult.type(ast, input) + ), { identifier: this.name, title: this.name, diff --git a/packages/schema/test/Schema/Class.test.ts b/packages/schema/test/Schema/Class.test.ts index 7ff77479921..04d4a88df98 100644 --- a/packages/schema/test/Schema/Class.test.ts +++ b/packages/schema/test/Schema/Class.test.ts @@ -480,32 +480,69 @@ describe("Schema > Class", () => { ) }) - it.skip("encode works with struct", async () => { - assert.doesNotThrow(() => S.encodeSync(Person)({ id: 1, name: "John" } as Person)) - class A extends S.Class()({ - n: S.NumberFromString - }) {} - class B extends S.Class()({ - a: A - }) {} - await Util.expectEncodeSuccess(S.union(B, S.NumberFromString), 1, "1") - await Util.expectEncodeSuccess(B, { a: { n: 1 } }, { a: { n: "1" } }) - - class C extends S.Class()({ - n: S.NumberFromString - }) { - get b() { - return 1 + describe("encode", () => { + it("struct + a class without methods nor getters", async () => { + class A extends S.Class()({ + n: S.NumberFromString + }) {} + await Util.expectEncodeSuccess(A, { n: 1 }, { n: "1" }) + }) + + it("struct + a class with a getter", async () => { + class A extends S.Class()({ + n: S.NumberFromString + }) { + get s() { + return "s" + } } - } - class D extends S.Class()({ - n: S.NumberFromString, - b: S.number - }) {} + await Util.expectEncodeSuccess(A, { n: 1 } as any, { n: "1" }) + }) - await Util.expectEncodeSuccess(D, new C({ n: 1 }), { n: "1", b: 1 }) + it("struct + nested classes", async () => { + class A extends S.Class()({ + n: S.NumberFromString + }) {} + class B extends S.Class()({ + a: A + }) {} + await Util.expectEncodeSuccess(S.union(B, S.NumberFromString), 1, "1") + await Util.expectEncodeSuccess(B, { a: { n: 1 } }, { a: { n: "1" } }) + }) - class E extends S.Class()({ a: S.string }) {} - await Util.expectEncodeFailure(S.to(E), null as any, "Expected E (an instance of E), actual null") + it("class + a class with a getter", async () => { + class A extends S.Class()({ + n: S.NumberFromString + }) { + get s() { + return "s" + } + } + class B extends S.Class()({ + n: S.NumberFromString, + s: S.string + }) {} + + await Util.expectEncodeSuccess(B, new A({ n: 1 }), { n: "1", s: "s" }) + }) + + describe("encode(S.to(Class))", () => { + it("should always return an instance", async () => { + class A extends S.Class()({ + n: S.NumberFromString + }) {} + const schema = S.to(A) + await Util.expectEncodeSuccess(schema, new A({ n: 1 }), new A({ n: 1 })) + await Util.expectEncodeSuccess(schema, { n: 1 }, new A({ n: 1 })) + }) + + it("should fail on bad values", async () => { + class A extends S.Class()({ + n: S.NumberFromString + }) {} + const schema = S.to(A) + await Util.expectEncodeFailure(schema, null as any, "Expected A (an instance of A), actual null") + }) + }) }) })