Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 49 additions & 20 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3441,6 +3441,7 @@ interface fieldsAssign<NewFields extends Struct.Fields> extends Lambda {
export function fieldsAssign<const NewFields extends Struct.Fields>(fields: NewFields) {
return Struct_.lambda<fieldsAssign<NewFields>>((struct) => struct.mapFields(Struct_.assign(fields)))
}
const EncodeKeysTypeId = "~effect/Schema/encodeKeys"
/**
* Type-level representation returned by {@link encodeKeys}.
*
Expand All @@ -3461,8 +3462,13 @@ export interface encodeKeys<
}
>
>
{}

{
readonly [EncodeKeysTypeId]: typeof EncodeKeysTypeId
readonly fields: S["fields"]
}
function isEncodeKeys$(schema: unknown): schema is encodeKeys<Struct<Struct.Fields>, any> {
return Predicate.hasProperty(schema, EncodeKeysTypeId) && schema[EncodeKeysTypeId] === EncodeKeysTypeId
}
const canonicalPropertyKey = (key: PropertyKey): string | symbol =>
typeof key === "symbol" ? key : globalThis.String(key)
/**
Expand Down Expand Up @@ -3515,13 +3521,19 @@ export function encodeKeys<
reverseMapping[encodedKey] = k
}
}
return Struct(fields).pipe(decodeTo(
const schema = Struct(fields).pipe(decodeTo(
self,
SchemaTransformation.transform<any, any>({
decode: Struct_.renameKeys(reverseMapping),
encode: Struct_.renameKeys(appliedMapping)
})
)) as any
))
return make(schema.ast, {
[EncodeKeysTypeId]: EncodeKeysTypeId,
to: schema.to,
from: schema.from,
fields: self.fields
}) as any
}
}
/**
Expand Down Expand Up @@ -13681,18 +13693,23 @@ export function Result<A extends Constraint, E extends Constraint>(
* @category models
* @since 3.10.0
*/
export interface Class<Self, S extends Constraint & { readonly fields: Struct.Fields }, Inherited>
extends
BottomLazyWithoutNew<
SchemaAST.Declaration,
decodeTo<declareConstructor<Self, S["Encoded"], readonly [S], S["Iso"]>, S>,
readonly [S],
S["~type.mutability"],
S["~type.optionality"],
S["~type.constructor.default"],
S["~encoded.mutability"],
S["~encoded.optionality"]
>
export interface Class<
Self,
S extends
| Constraint & { readonly fields: Struct.Fields }
| encodeKeys<Struct<Struct.Fields>, any>,
Inherited
> extends
BottomLazyWithoutNew<
SchemaAST.Declaration,
decodeTo<declareConstructor<Self, S["Encoded"], readonly [S], S["Iso"]>, S>,
readonly [S],
S["~type.mutability"],
S["~type.optionality"],
S["~type.constructor.default"],
S["~encoded.mutability"],
S["~encoded.optionality"]
>
{
/**
* `make`, `makeOption`, and `makeEffect` preserve an existing instance of
Expand Down Expand Up @@ -13789,7 +13806,7 @@ const payloadToken = {}

function makeClass<
Self,
S extends Struct<Struct.Fields>,
S extends Struct<Struct.Fields> | encodeKeys<Struct<Struct.Fields>, any>,
Inherited extends new(...args: ReadonlyArray<any>) => any
>(
Inherited: Inherited,
Expand Down Expand Up @@ -13856,6 +13873,11 @@ function makeClass<
schema: Struct.Fields | Struct<Struct.Fields>,
annotations?: Annotations.Declaration<any, readonly [any]>
) => {
if (isEncodeKeys$(struct)) {
throw new globalThis.Error(
`extend is not supported on a Class built from an encodeKeys schema (identifier: "${identifier}")`
)
}
const extension = isStruct(schema) ? schema : Struct(schema)
const fields = { ...struct.fields, ...extension.fields }
const ast = SchemaAST.struct(fields, struct.ast.checks, { identifier })
Expand All @@ -13874,7 +13896,7 @@ function makeClass<
readonly unsafePreserveChecks?: boolean | undefined
} | undefined
): Struct<Simplify<Readonly<To>>> {
return struct.mapFields(f, options)
return isEncodeKeys$(struct) ? struct.to.mapFields(f, options) : struct.mapFields(f, options)
}
}

Expand Down Expand Up @@ -13948,9 +13970,12 @@ function getClassSchemaFactory<S extends Constraint>(
}
}

function isStruct(schema: Struct.Fields | Struct<Struct.Fields>): schema is Struct<Struct.Fields> {
function isStruct(
schema: Struct.Fields | Struct<Struct.Fields> | encodeKeys<Struct<Struct.Fields>, any>
): schema is Struct<Struct.Fields> | encodeKeys<Struct<Struct.Fields>, any> {
return isSchema(schema)
}

/**
* Creates a schema-backed class whose constructor validates input against a
* {@link Struct} schema. Construction throws an `Error` with a
Expand Down Expand Up @@ -14030,10 +14055,14 @@ export const Class: {
schema: S,
annotations?: Annotations.Declaration<Self, readonly [S]>
): [Self] extends [never] ? MissingSelfGeneric<"Schema.Class"> : Class<Self, S, Brand>
<S extends encodeKeys<Struct<Struct.Fields>, any>>(
schema: S,
annotations?: Annotations.Declaration<Self, readonly [S]>
): [Self] extends [never] ? MissingSelfGeneric<"Schema.Class"> : Class<Self, S, Brand>
}
} = <Self, Brand = {}>(identifier: string) =>
(
schema: Struct.Fields | Struct<Struct.Fields>,
schema: Struct.Fields | Struct<Struct.Fields> | encodeKeys<Struct<Struct.Fields>, any>,
annotations?: Annotations.Declaration<Self, readonly [Struct<Struct.Fields>]>
): [Self] extends [never] ? MissingSelfGeneric<"Schema.Class"> : Class<Self, Struct<Struct.Fields>, Brand> => {
const struct = isStruct(schema) ? schema : Struct(schema)
Expand Down
91 changes: 91 additions & 0 deletions packages/effect/test/schema/Schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,15 @@ describe("Schema", () => {
a: Schema.String
})) {}
class B extends Schema.Opaque<B>()(Schema.Struct({ a: Schema.String })) {}
class C extends Schema.Class<C>("C")(
Schema.Struct({
a: Schema.String
}).pipe(Schema.encodeKeys({ "a": "A" }))
) {}
assertTrue(Schema.isSchema(Schema.String))
assertTrue(Schema.isSchema(A))
assertTrue(Schema.isSchema(B))
assertTrue(Schema.isSchema(C))
assertFalse(Schema.isSchema({}))
})

Expand Down Expand Up @@ -6861,6 +6867,16 @@ Expected a value between -2147483648 and 2147483647`
deepStrictEqual(schema.fields, { a: Schema.String, b: Schema.Number })
})

it("mapFields on a Class built from a Struct with encodeKeys", () => {
class A extends Schema.Class<A>("A")(
Schema.Struct({
a: Schema.String
}).pipe(Schema.encodeKeys({ "a": "c" }))
) {}
const schema = A.mapFields((fields) => ({ ...fields, b: Schema.Number }))
deepStrictEqual(schema.fields, { a: Schema.String, b: Schema.Number })
})

it("Struct with nested Class", async () => {
class A extends Schema.Class<A, { readonly brand: unique symbol }>("A")(Schema.Struct({
a: Schema.String
Expand Down Expand Up @@ -7235,6 +7251,23 @@ Expected a value between -2147483648 and 2147483647`
await make.fail({ a: 1, b: 0 }, `Expected positive b`)
})

it("Class built from an encodeKeys schema is not supported", async () => {
class A extends Schema.Class<A>("A")(
Schema.Struct({
a: Schema.Number
}).pipe(Schema.encodeKeys({ "a": "c" }))
) {}
throws(
() =>
class B extends A.extend<B>("B")(
Schema.Struct({
b: Schema.Number
})
) {},
new Error(`extend is not supported on a Class built from an encodeKeys schema (identifier: "B")`)
)
})

it("static members", async () => {
class A extends Schema.Class<A>("A")({
a: Schema.String
Expand Down Expand Up @@ -8994,6 +9027,64 @@ Expected a value between -2147483648 and 2147483647`
await encoding.succeed(new A({ a: 1, b: "b" }), { c: "1", b: "b" })
})

it("Class from Struct with encodeKeys", async () => {
class A extends Schema.Class<A>("A")(
Schema.Struct({
a: Schema.String
}).pipe(Schema.encodeKeys({ "a": "c" }))
) {
readonly _a = 1
}
const asserts = new TestSchema.Asserts(A)

// should be a schema
assertTrue(Schema.isSchema(A))
// should expose the fields
deepStrictEqual(A.fields, { a: Schema.String })
// should expose the identifier
strictEqual(A.identifier, "A")

strictEqual(A.name, "A")

assertTrue(new A({ a: "a" }) instanceof A)
assertTrue(A.make({ a: "a" }) instanceof A)

// test additional fields
strictEqual(new A({ a: "a" })._a, 1)
strictEqual(A.make({ a: "a" })._a, 1)

// test Equal.equals
assertTrue(Equal.equals(new A({ a: "a" }), new A({ a: "a" })))
assertFalse(Equal.equals(new A({ a: "a" }), new A({ a: "b" })))

const make = asserts.make()
await make.succeed(new A({ a: "a" }))
await make.succeed({ a: "a" }, new A({ a: "a" }))

if (verifyGeneration) {
asserts.arbitrary().verifyGeneration()
}

const decoding = asserts.decoding()
await decoding.succeed({ c: "a" }, new A({ a: "a" }))
await decoding.fail(
{ c: 1 },
`Expected string
at ["c"]`
)

const encoding = asserts.encoding()
await encoding.succeed(new A({ a: "a" }), { c: "a" })
await encoding.fail(
null,
"Expected A"
)
await encoding.fail(
{ a: "a" },
`Expected A`
)
})

it("supports symbol source keys", () => {
const field = Symbol("field")
const schema = Schema.Struct({
Expand Down
26 changes: 8 additions & 18 deletions packages/effect/typetest/schema/Schema.tst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1686,32 +1686,26 @@ describe("Schema", () => {
}).pipe(Schema.encodeKeys({ a: "c" }))

expect(schema).type.toBe<
Schema.decodeTo<
Schema.encodeKeys<
Schema.Struct<{
readonly a: Schema.FiniteFromString
readonly b: Schema.String
}>,
Schema.Struct<{
readonly c: Schema.toEncoded<Schema.FiniteFromString>
readonly b: Schema.toEncoded<Schema.String>
}>
{ readonly a: "c" }
>
>()
})

it("should ignore encoded key mappings for missing decoded fields", () => {
const schema = Schema.Struct({
a: Schema.String
}).pipe(Schema.encodeKeys({ a: "c", b: "d" }))

expect(schema).type.toBe<
Schema.decodeTo<
Schema.encodeKeys<
Schema.Struct<{
readonly a: Schema.String
}>,
Schema.Struct<{
readonly c: Schema.toEncoded<Schema.String>
}>
{ readonly a: "c"; readonly b: "d" }
>
>()
})
Expand All @@ -1725,13 +1719,11 @@ describe("Schema", () => {
}).pipe(Schema.encodeKeys({ [decoded]: "decoded" }))

expect(source).type.toBe<
Schema.decodeTo<
Schema.encodeKeys<
Schema.Struct<{
readonly [decoded]: Schema.String
}>,
Schema.Struct<{
readonly decoded: Schema.toEncoded<Schema.String>
}>
{ readonly [decoded]: "decoded" }
>
>()

Expand All @@ -1740,13 +1732,11 @@ describe("Schema", () => {
}).pipe(Schema.encodeKeys({ decoded: encoded }))

expect(destination).type.toBe<
Schema.decodeTo<
Schema.encodeKeys<
Schema.Struct<{
readonly decoded: Schema.String
}>,
Schema.Struct<{
readonly [encoded]: Schema.toEncoded<Schema.String>
}>
{ readonly decoded: typeof encoded }
>
>()
})
Expand Down