Skip to content

Commit

Permalink
preserve defect information & TaggedError message (#2359)
Browse files Browse the repository at this point in the history
Co-authored-by: Giulio Canti <giulio.canti@gmail.com>
  • Loading branch information
tim-smart and gcanti authored Mar 22, 2024
1 parent 706b64d commit 9392de6
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/fluffy-bananas-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

preserve defect information in Cause.Die
5 changes: 5 additions & 0 deletions .changeset/real-peaches-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

use provided message in Schema.TaggedError .toString
54 changes: 41 additions & 13 deletions packages/schema/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6044,16 +6044,14 @@ export const TaggedClass = <Self = never>(identifier?: string) =>
{},
{}
> =>
{
return makeClass({
makeClass({
kind: "TaggedClass",
identifier: identifier ?? tag,
fields: extendFields({ _tag: literal(tag) }, fields),
Base: Data.Class,
tag: { _tag: tag },
annotations
})
}
/**
* @category classes
Expand All @@ -6076,13 +6074,25 @@ export const TaggedError = <Self = never>(identifier?: string) =>
Cause.YieldableError
> =>
{
class Base extends Data.Error {}
;(Base.prototype as any).name = tag
return makeClass({
kind: "TaggedError",
identifier: identifier ?? tag,
fields: extendFields({ _tag: literal(tag) }, fields),
Base: Data.Error,
Base,
tag: { _tag: tag },
annotations
annotations,
toStringOverride(self) {
if (!(Predicate.isString(self.message) && self.message.length > 0)) {
return Pretty.make(self.constructor as any)(self)
}
let message = `${self._tag}: ${self.message}`
if (Predicate.isString(self.stack)) {
message = `${message}\n${self.stack.split("\n").slice(1).join("\n")}`
}
return message
}
})
}
Expand Down Expand Up @@ -6173,14 +6183,15 @@ const extendFields = (a: Struct.Fields, b: Struct.Fields): Struct.Fields => {
return out
}
const makeClass = ({ Base, annotations, fields, fromSchema, identifier, kind, tag }: {
const makeClass = ({ Base, annotations, fields, fromSchema, identifier, kind, tag, toStringOverride }: {
kind: string
identifier: string
fields: Struct.Fields
Base: new(...args: ReadonlyArray<any>) => any
fromSchema?: Schema.Any | undefined
tag?: { _tag: AST.LiteralValue } | undefined
annotations?: Annotations.Schema<any> | undefined
toStringOverride?: ((self: any) => string) | undefined
}): any => {
const classSymbol = Symbol.for(`@effect/schema/${kind}/${identifier}`)
const schema = fromSchema ?? struct(fields)
Expand All @@ -6207,7 +6218,7 @@ const makeClass = ({ Base, annotations, fields, fromSchema, identifier, kind, ta
}
toString() {
return Pretty.make(this.constructor as any)(this)
return toStringOverride !== undefined ? toStringOverride(this) : Pretty.make(this.constructor as any)(this)
}
static pipe() {
Expand Down Expand Up @@ -6636,13 +6647,30 @@ function causeEncode<E>(cause: Cause.Cause<E>): CauseEncoded<E> {
}
}
const causeDefectPretty: Schema<unknown> = transform(
/**
* @category Cause transformations
* @since 1.0.0
*/
export const causeDefectUnknown: $unknown = transform(
unknown,
unknown,
identity,
(u) => {
if (Predicate.isObject(u) && "message" in u && typeof u.message === "string") {
const err = new Error(u.message, { cause: u })
if ("name" in u && typeof u.name === "string") {
err.name = u.name
}
err.stack = "stack" in u && typeof u.stack === "string" ? u.stack : ""
return err
}
return String(u)
},
(defect) => {
if (Predicate.isObject(defect)) {
return Cause.pretty(Cause.die(defect))
if (defect instanceof Error) {
return {
name: defect.name,
message: defect.message
}
}
return String(defect)
}
Expand All @@ -6665,7 +6693,7 @@ export interface cause<E extends Schema.All, DR> extends
* @category Cause transformations
* @since 1.0.0
*/
export const cause = <E extends Schema.All, DR = never>({ defect = causeDefectPretty, error }: {
export const cause = <E extends Schema.All, DR = never>({ defect = causeDefectUnknown, error }: {
readonly error: E
readonly defect?: Schema<unknown, unknown, DR> | undefined
}): cause<E, DR> => {
Expand Down Expand Up @@ -6817,7 +6845,7 @@ export interface exit<A extends Schema.All, E extends Schema.All, DR> extends
* @since 1.0.0
*/
export const exit = <A extends Schema.All, E extends Schema.All, DR = never>(
{ defect = causeDefectPretty, failure, success }: {
{ defect = causeDefectUnknown, failure, success }: {
readonly failure: E
readonly success: A
readonly defect?: Schema<unknown, unknown, DR> | undefined
Expand Down
11 changes: 6 additions & 5 deletions packages/schema/test/Cause/cause.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("Cause > cause", () => {
_tag: "Die",
defect: { stack: "fail", message: "error" }
},
Cause.die({ stack: "fail", message: "error" })
Cause.die(new Error("error"))
)
await Util.expectDecodeUnknownSuccess(
schema,
Expand Down Expand Up @@ -140,12 +140,13 @@ describe("Cause > cause", () => {

let failWithStack = S.encodeSync(schema)(Cause.die(new Error("fail")))
assert(failWithStack._tag === "Die")
assert.include(failWithStack.defect, "Error: fail")
assert.include(failWithStack.defect, "cause.test.ts")
assert.deepStrictEqual(failWithStack.defect, {
name: "Error",
message: "fail"
})

failWithStack = S.encodeSync(schemaUnknown)(Cause.die(new Error("fail")))
assert(failWithStack._tag === "Die")
assert.strictEqual((failWithStack.defect as any).message, "fail")
assert.include((failWithStack.defect as any).stack, "cause.test.ts")
assert.strictEqual((failWithStack.defect as Error).message, "fail")
})
})
18 changes: 18 additions & 0 deletions packages/schema/test/Schema/Class.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,24 @@ describe("Schema > Class APIs", () => {
expect(err.id).toEqual(1)
})

it("TaggedError/message", () => {
class MyError extends S.TaggedError<MyError>()("MyError", {
id: S.number
}) {
get message() {
return `bad id: ${this.id}`
}
}

const err = new MyError({ id: 1 })

expect(String(err)).include(`MyError: bad id: 1`)
expect(String(err)).toContain("Class.test.ts:")
expect(err.stack).toContain("Class.test.ts:")
expect(err._tag).toEqual("MyError")
expect(err.id).toEqual(1)
})

describe("TaggedRequest", () => {
it("should expose the fields", () => {
class TRA extends S.TaggedRequest<TRA>()("TRA", S.string, S.number, {
Expand Down

0 comments on commit 9392de6

Please sign in to comment.