Skip to content

Commit

Permalink
Schema: partial / required: add support for renaming property keys in… (
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Apr 19, 2024
1 parent 3f69580 commit 773b8e0
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 42 deletions.
40 changes: 40 additions & 0 deletions .changeset/great-shoes-watch.md
@@ -0,0 +1,40 @@
---
"@effect/schema": patch
---

partial / required: add support for renaming property keys in property signature transformations

Before

```ts
import { Schema } from "@effect/schema"

const TestType = Schema.Struct({
a: Schema.String,
b: Schema.propertySignature(Schema.String).pipe(Schema.fromKey("c"))
})

const PartialTestType = Schema.partial(TestType)
// throws Error: Partial: cannot handle transformations
```

Now

```ts
import { Schema } from "@effect/schema"

const TestType = Schema.Struct({
a: Schema.String,
b: Schema.propertySignature(Schema.String).pipe(Schema.fromKey("c"))
})

const PartialTestType = Schema.partial(TestType)

console.log(Schema.decodeUnknownSync(PartialTestType)({ a: "a", c: "c" })) // { a: 'a', b: 'c' }
console.log(Schema.decodeUnknownSync(PartialTestType)({ a: "a" })) // { a: 'a' }

const RequiredTestType = Schema.required(PartialTestType)

console.log(Schema.decodeUnknownSync(RequiredTestType)({ a: "a", c: "c" })) // { a: 'a', b: 'c' }
console.log(Schema.decodeUnknownSync(RequiredTestType)({ a: "a" })) // { a: 'a', b: undefined }
```
33 changes: 25 additions & 8 deletions packages/schema/src/AST.ts
Expand Up @@ -1763,6 +1763,9 @@ export class PropertySignatureTransformation {
) {}
}

const isRenamingPropertySignatureTransformation = (t: PropertySignatureTransformation) =>
t.decode === identity && t.encode === identity

/**
* @category model
* @since 1.0.0
Expand Down Expand Up @@ -2098,11 +2101,18 @@ export const partial = (ast: AST, options?: { readonly exact: true }): AST => {
case "Suspend":
return new Suspend(() => partial(ast.f(), options))
case "Declaration":
throw new Error(errors_.getAPIErrorMessage("Partial", "cannot handle declarations"))
throw new Error(errors_.getAPIErrorMessage("partial", "cannot handle declarations"))
case "Refinement":
throw new Error(errors_.getAPIErrorMessage("Partial", "cannot handle refinements"))
case "Transformation":
throw new Error(errors_.getAPIErrorMessage("Partial", "cannot handle transformations"))
throw new Error(errors_.getAPIErrorMessage("partial", "cannot handle refinements"))
case "Transformation": {
if (
isTypeLiteralTransformation(ast.transformation) &&
ast.transformation.propertySignatureTransformations.every(isRenamingPropertySignatureTransformation)
) {
return new Transformation(partial(ast.from, options), partial(ast.to, options), ast.transformation)
}
throw new Error(errors_.getAPIErrorMessage("partial", "cannot handle transformations"))
}
}
return ast
}
Expand Down Expand Up @@ -2130,11 +2140,18 @@ export const required = (ast: AST): AST => {
case "Suspend":
return new Suspend(() => required(ast.f()))
case "Declaration":
throw new Error(errors_.getAPIErrorMessage("Required", "cannot handle declarations"))
throw new Error(errors_.getAPIErrorMessage("required", "cannot handle declarations"))
case "Refinement":
throw new Error(errors_.getAPIErrorMessage("Required", "cannot handle refinements"))
case "Transformation":
throw new Error(errors_.getAPIErrorMessage("Required", "cannot handle transformations"))
throw new Error(errors_.getAPIErrorMessage("required", "cannot handle refinements"))
case "Transformation": {
if (
isTypeLiteralTransformation(ast.transformation) &&
ast.transformation.propertySignatureTransformations.every(isRenamingPropertySignatureTransformation)
) {
return new Transformation(required(ast.from), required(ast.to), ast.transformation)
}
throw new Error(errors_.getAPIErrorMessage("required", "cannot handle transformations"))
}
}
return ast
}
Expand Down
81 changes: 64 additions & 17 deletions packages/schema/test/Schema/partial.test.ts
Expand Up @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"

describe("partial", () => {
describe("{ exact: false }", () => {
it("struct", async () => {
it("Struct", async () => {
const schema = S.partial(S.Struct({ a: S.Number }))
await Util.expectDecodeUnknownSuccess(schema, {})
await Util.expectDecodeUnknownSuccess(schema, { a: 1 })
Expand All @@ -24,14 +24,14 @@ describe("partial", () => {
)
})

it("record", async () => {
it("Record", async () => {
const schema = S.partial(S.Record(S.String, S.NumberFromString))
await Util.expectDecodeUnknownSuccess(schema, {}, {})
await Util.expectDecodeUnknownSuccess(schema, { a: "1" }, { a: 1 })
await Util.expectDecodeUnknownSuccess(schema, { a: undefined })
})

describe("tuple", () => {
describe("Tuple", () => {
it("e", async () => {
const schema = S.partial(S.Tuple(S.NumberFromString))
await Util.expectDecodeUnknownSuccess(schema, ["1"], [1])
Expand All @@ -49,7 +49,7 @@ describe("partial", () => {
})
})

it("array", async () => {
it("Array", async () => {
const schema = S.partial(S.Array(S.Number))
await Util.expectDecodeUnknownSuccess(schema, [])
await Util.expectDecodeUnknownSuccess(schema, [1])
Expand All @@ -70,7 +70,7 @@ describe("partial", () => {
})

describe("{ exact: true }", () => {
it("struct", async () => {
it("Struct", async () => {
const schema = S.partial(S.Struct({ a: S.Number }), { exact: true })
await Util.expectDecodeUnknownSuccess(schema, {})
await Util.expectDecodeUnknownSuccess(schema, { a: 1 })
Expand All @@ -84,14 +84,14 @@ describe("partial", () => {
)
})

it("record", async () => {
it("Record", async () => {
const schema = S.partial(S.Record(S.String, S.NumberFromString), { exact: true })
await Util.expectDecodeUnknownSuccess(schema, {}, {})
await Util.expectDecodeUnknownSuccess(schema, { a: "1" }, { a: 1 })
await Util.expectDecodeUnknownSuccess(schema, { a: undefined })
})

describe("tuple", () => {
describe("Tuple", () => {
it("e", async () => {
const schema = S.partial(S.Tuple(S.NumberFromString), { exact: true })
await Util.expectDecodeUnknownSuccess(schema, ["1"], [1])
Expand Down Expand Up @@ -127,7 +127,7 @@ describe("partial", () => {
})
})

it("array", async () => {
it("Array", async () => {
const schema = S.partial(S.Array(S.Number), { exact: true })
await Util.expectDecodeUnknownSuccess(schema, [])
await Util.expectDecodeUnknownSuccess(schema, [1])
Expand All @@ -146,7 +146,7 @@ describe("partial", () => {
)
})

it("union", async () => {
it("Union", async () => {
const schema = S.partial(S.Union(S.Array(S.Number), S.String), { exact: true })
await Util.expectDecodeUnknownSuccess(schema, "a")
await Util.expectDecodeUnknownSuccess(schema, [])
Expand Down Expand Up @@ -199,24 +199,71 @@ describe("partial", () => {
└─ Expected null, actual 1`
)
})
})

it("declarations should throw", async () => {
describe("unsupported schemas", () => {
it("declarations should throw", () => {
expect(() => S.partial(S.OptionFromSelf(S.String))).toThrow(
new Error("partial: cannot handle declarations")
)
expect(() => S.partial(S.OptionFromSelf(S.String), { exact: true })).toThrow(
new Error("Partial: cannot handle declarations")
new Error("partial: cannot handle declarations")
)
})

it("refinements should throw", async () => {
it("refinements should throw", () => {
expect(() => S.partial(S.String.pipe(S.minLength(2)))).toThrow(
new Error("partial: cannot handle refinements")
)
expect(() => S.partial(S.String.pipe(S.minLength(2)), { exact: true })).toThrow(
new Error("Partial: cannot handle refinements")
new Error("partial: cannot handle refinements")
)
})

it("transformations should throw", async () => {
expect(() => S.partial(S.transform(S.String, S.String, { decode: identity, encode: identity }), { exact: true }))
.toThrow(
new Error("Partial: cannot handle transformations")
describe("Transformation", () => {
describe("should support property key renamings", () => {
it("{ exact: false }", async () => {
const original = S.Struct({
a: S.String,
b: S.propertySignature(S.String).pipe(S.fromKey("c"))
})
const schema = S.partial(original)
expect(S.format(schema)).toBe(
"({ a?: string | undefined; c?: string | undefined } <-> { a?: string | undefined; b?: string | undefined })"
)
await Util.expectDecodeUnknownSuccess(schema, {})
await Util.expectDecodeUnknownSuccess(schema, { a: undefined })
await Util.expectDecodeUnknownSuccess(schema, { c: undefined }, { b: undefined })
await Util.expectDecodeUnknownSuccess(schema, { a: "a" })
await Util.expectDecodeUnknownSuccess(schema, { c: "b" }, { b: "b" })
await Util.expectDecodeUnknownSuccess(schema, { a: undefined, c: undefined }, { a: undefined, b: undefined })
await Util.expectDecodeUnknownSuccess(schema, { a: "a", c: undefined }, { a: "a", b: undefined })
await Util.expectDecodeUnknownSuccess(schema, { a: undefined, c: "b" }, { a: undefined, b: "b" })
await Util.expectDecodeUnknownSuccess(schema, { a: "a", c: "b" }, { a: "a", b: "b" })
})

it("{ exact: true }", async () => {
const original = S.Struct({
a: S.String,
b: S.propertySignature(S.String).pipe(S.fromKey("c"))
})
const schema = S.partial(original, { exact: true })
expect(S.format(schema)).toBe("({ a?: string; c?: string } <-> { a?: string; b?: string })")
await Util.expectDecodeUnknownSuccess(schema, {})
await Util.expectDecodeUnknownSuccess(schema, { a: "a" })
await Util.expectDecodeUnknownSuccess(schema, { c: "b" }, { b: "b" })
await Util.expectDecodeUnknownSuccess(schema, { a: "a", c: "b" }, { a: "a", b: "b" })
})
})

it("transformations should throw", () => {
expect(() => S.partial(S.transform(S.String, S.String, { decode: identity, encode: identity }))).toThrow(
new Error("partial: cannot handle transformations")
)
expect(() =>
S.partial(S.transform(S.String, S.String, { decode: identity, encode: identity }), { exact: true })
).toThrow(new Error("partial: cannot handle transformations"))
})
})
})
})
49 changes: 32 additions & 17 deletions packages/schema/test/Schema/required.test.ts
Expand Up @@ -8,7 +8,7 @@ describe("required", () => {
expect(S.required(S.String).ast).toEqual(S.String.ast)
})

it("struct", async () => {
it("Struct", async () => {
const schema = S.required(S.Struct({
a: S.optional(S.NumberFromString.pipe(S.greaterThan(0)), { exact: true })
}))
Expand All @@ -32,7 +32,7 @@ describe("required", () => {
)
})

describe("tuple", () => {
describe("Tuple", () => {
it("e?", async () => {
// type A = readonly [string?]
// type B = Required<A>
Expand Down Expand Up @@ -135,7 +135,7 @@ describe("required", () => {
})
})

it("union", async () => {
it("Union", async () => {
const schema = S.required(S.Union(
S.Struct({ a: S.optional(S.String, { exact: true }) }),
S.Struct({ b: S.optional(S.Number, { exact: true }) })
Expand Down Expand Up @@ -191,21 +191,36 @@ describe("required", () => {
)
})

it("declarations should throw", async () => {
expect(() => S.required(S.OptionFromSelf(S.String))).toThrow(
new Error("Required: cannot handle declarations")
)
})
describe("unsupported schemas", () => {
it("declarations should throw", async () => {
expect(() => S.required(S.OptionFromSelf(S.String))).toThrow(
new Error("required: cannot handle declarations")
)
})

it("refinements should throw", async () => {
expect(() => S.required(S.String.pipe(S.minLength(2)))).toThrow(
new Error("Required: cannot handle refinements")
)
})
it("refinements should throw", async () => {
expect(() => S.required(S.String.pipe(S.minLength(2)))).toThrow(
new Error("required: cannot handle refinements")
)
})

it("transformations should throw", async () => {
expect(() => S.required(S.transform(S.String, S.String, { decode: identity, encode: identity }))).toThrow(
new Error("Required: cannot handle transformations")
)
describe("Transformation", () => {
it("should support property key renamings", () => {
const original = S.Struct({
a: S.String,
b: S.propertySignature(S.String).pipe(S.fromKey("c"))
})
const schema = S.required(S.partial(original))
expect(S.format(schema)).toBe(
"({ a: string | undefined; c: string | undefined } <-> { a: string | undefined; b: string | undefined })"
)
})

it("transformations should throw", async () => {
expect(() => S.required(S.transform(S.String, S.String, { decode: identity, encode: identity }))).toThrow(
new Error("required: cannot handle transformations")
)
})
})
})
})

0 comments on commit 773b8e0

Please sign in to comment.