Skip to content

Commit

Permalink
Schema: add missing support for tuple annotations in TaggedRequest (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Feb 11, 2025
1 parent 543d36d commit 4018eae
Show file tree
Hide file tree
Showing 8 changed files with 471 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-rivers-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Schema: add missing support for tuple annotations in `TaggedRequest`.
19 changes: 19 additions & 0 deletions packages/effect/dtslint/SchemaTaggedClass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Schema } from "effect"

// ---------------------------------------------
// Annotations as tuple
// ---------------------------------------------

// @ts-expect-error
export class Annotations extends Schema.TaggedClass<Annotations>()("Annotations", {
id: Schema.Number
}, [
undefined,
undefined,
{
pretty: () =>
(
_x // $ExpectType { readonly _tag: "Annotations"; } & { readonly id: number; }
) => ""
}
]) {}
18 changes: 18 additions & 0 deletions packages/effect/dtslint/SchemaTaggedError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,21 @@ export type IdErr = Unify.Unify<Err>
export const YieldErr = Effect.gen(function*($) {
return yield* $(new Err())
})

// ---------------------------------------------
// Annotations as tuple
// ---------------------------------------------

// @ts-expect-error
export class Annotations extends Schema.TaggedError<Annotations>()("Annotations", {
id: Schema.Number
}, [
undefined,
undefined,
{
pretty: () =>
(
_x // $ExpectType { readonly _tag: "Annotations"; } & { readonly id: number; }
) => ""
}
]) {}
22 changes: 22 additions & 0 deletions packages/effect/dtslint/SchemaTaggedRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,25 @@ TRA.success

// $ExpectType typeof String$
TRA.failure

// ---------------------------------------------
// Annotations as tuple
// ---------------------------------------------

// @ts-expect-error
export class Annotations extends S.TaggedRequest<Annotations>()("Annotations", {
failure: S.String,
success: S.Number,
payload: {
id: S.Number
}
}, [
undefined,
undefined,
{
pretty: () =>
(
_x // $ExpectType { readonly _tag: "Annotations"; } & { readonly id: number; }
) => ""
}
]) {}
8 changes: 4 additions & 4 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8178,7 +8178,7 @@ type ClassAnnotations<Self, A> =
| Annotations.Schema<Self>
| readonly [
Annotations.Schema<Self> | undefined,
Annotations.Schema<Self>?,
(Annotations.Schema<Self> | undefined)?,
Annotations.Schema<A>?
]
Expand Down Expand Up @@ -8457,7 +8457,7 @@ export const TaggedClass = <Self = never>(identifier?: string) =>
<Tag extends string, Fields extends Struct.Fields>(
tag: Tag,
fieldsOr: Fields | HasFields<Fields>,
annotations?: ClassAnnotations<Self, Struct.Type<Fields>>
annotations?: ClassAnnotations<Self, Struct.Type<{ readonly _tag: tag<Tag> } & Fields>>
): [Self] extends [never] ? MissingSelfGeneric<"TaggedClass", `"Tag", `>
: TaggedClass<Self, Tag, { readonly _tag: tag<Tag> } & Fields> =>
{
Expand Down Expand Up @@ -8520,7 +8520,7 @@ export const TaggedError = <Self = never>(identifier?: string) =>
<Tag extends string, Fields extends Struct.Fields>(
tag: Tag,
fieldsOr: Fields | HasFields<Fields>,
annotations?: ClassAnnotations<Self, Struct.Type<Fields>>
annotations?: ClassAnnotations<Self, Struct.Type<{ readonly _tag: tag<Tag> } & Fields>>
): [Self] extends [never] ? MissingSelfGeneric<"TaggedError", `"Tag", `>
: TaggedErrorClass<
Self,
Expand Down Expand Up @@ -10270,7 +10270,7 @@ export const TaggedRequest =
success: Success
payload: Payload
},
annotations?: Annotations.Schema<Self>
annotations?: ClassAnnotations<Self, Struct.Type<{ readonly _tag: tag<Tag> } & Payload>>
): [Self] extends [never] ? MissingSelfGeneric<"TaggedRequest", `"Tag", SuccessSchema, FailureSchema, `>
: TaggedRequestClass<
Self,
Expand Down
140 changes: 137 additions & 3 deletions packages/effect/test/Schema/Schema/Class/TaggedClass.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { describe, it } from "@effect/vitest"
import { pipe, Struct } from "effect"
import * as S from "effect/Schema"
import { JSONSchema, pipe, Schema as S, SchemaAST as AST, Struct } from "effect"
import * as Util from "effect/test/Schema/TestUtils"
import { assertFalse, assertInstanceOf, assertTrue, deepStrictEqual, strictEqual, throws } from "effect/test/util"
import {
assertFalse,
assertInstanceOf,
assertSome,
assertTrue,
deepStrictEqual,
strictEqual,
throws
} from "effect/test/util"

describe("TaggedClass", () => {
it("the constructor should add a `_tag` field", () => {
Expand Down Expand Up @@ -225,4 +232,131 @@ details: Duplicate key "_tag"`)
strictEqual(ta._tag, "TA")
strictEqual(ta.a(), "1a")
})

describe("should support annotations when declaring the Class", () => {
it("single argument", async () => {
class A extends S.TaggedClass<A>()("A", {
a: S.NonEmptyString
}, { title: "mytitle" }) {}

strictEqual(A.ast.to.annotations[AST.TitleAnnotationId], "mytitle")

await Util.assertions.encoding.fail(
A,
{ _tag: "A", a: "" },
`(A (Encoded side) <-> A)
└─ Type side transformation failure
└─ mytitle
└─ ["a"]
└─ NonEmptyString
└─ Predicate refinement failure
└─ Expected a non empty string, actual ""`
)
})

it("tuple argument", async () => {
class A extends S.TaggedClass<A>()("A", {
a: S.NonEmptyString
}, [
{ identifier: "TypeID", description: "TypeDescription" },
{ identifier: "TransformationID" },
{ identifier: "EncodedID" }
]) {}
assertSome(AST.getIdentifierAnnotation(A.ast.to), "TypeID")
assertSome(AST.getIdentifierAnnotation(A.ast), "TransformationID")
assertSome(AST.getIdentifierAnnotation(A.ast.from), "EncodedID")

await Util.assertions.decoding.fail(
A,
{},
`TransformationID
└─ Encoded side transformation failure
└─ EncodedID
└─ ["a"]
└─ is missing`
)

await Util.assertions.encoding.fail(
A,
{ _tag: "A", a: "" },
`TransformationID
└─ Type side transformation failure
└─ TypeID
└─ ["a"]
└─ NonEmptyString
└─ Predicate refinement failure
└─ Expected a non empty string, actual ""`
)

const ctor = { make: A.make.bind(A) }

Util.assertions.make.fail(
ctor,
null,
`TypeID
└─ ["a"]
└─ is missing`
)

deepStrictEqual(JSONSchema.make(S.typeSchema(A)), {
"$defs": {
"NonEmptyString": {
"title": "nonEmptyString",
"description": "a non empty string",
"minLength": 1,
"type": "string"
},
"TypeID": {
"additionalProperties": false,
"description": "TypeDescription",
"properties": {
"_tag": {
"enum": [
"A"
],
"type": "string"
},
"a": {
"$ref": "#/$defs/NonEmptyString"
}
},
"required": ["a", "_tag"],
"type": "object"
}
},
"$ref": "#/$defs/TypeID",
"$schema": "http://json-schema.org/draft-07/schema#"
})

deepStrictEqual(JSONSchema.make(A), {
"$defs": {
"NonEmptyString": {
"title": "nonEmptyString",
"description": "a non empty string",
"minLength": 1,
"type": "string"
},
"TransformationID": {
"additionalProperties": false,
"description": "TypeDescription",
"properties": {
"_tag": {
"enum": [
"A"
],
"type": "string"
},
"a": {
"$ref": "#/$defs/NonEmptyString"
}
},
"required": ["a", "_tag"],
"type": "object"
}
},
"$ref": "#/$defs/TransformationID",
"$schema": "http://json-schema.org/draft-07/schema#"
})
})
})
})
Loading

0 comments on commit 4018eae

Please sign in to comment.