Skip to content

Commit 7d28a90

Browse files
authored
Use standard formatting function in Config error messages, closes #5709 (#5712)
1 parent 0d78615 commit 7d28a90

File tree

12 files changed

+353
-362
lines changed

12 files changed

+353
-362
lines changed

.changeset/deep-clouds-wink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Use standard formatting function in Config error messages, closes #5709

packages/effect/src/Inspectable.ts

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44
import type * as FiberRefs from "./FiberRefs.js"
55
import { globalValue } from "./GlobalValue.js"
6-
import { hasProperty, isFunction } from "./Predicate.js"
6+
import * as Predicate from "./Predicate.js"
77

88
/**
99
* @since 2.0.0
@@ -33,7 +33,7 @@ export interface Inspectable {
3333
export const toJSON = (x: unknown): unknown => {
3434
try {
3535
if (
36-
hasProperty(x, "toJSON") && isFunction(x["toJSON"]) &&
36+
Predicate.hasProperty(x, "toJSON") && Predicate.isFunction(x["toJSON"]) &&
3737
x["toJSON"].length === 0
3838
) {
3939
return x.toJSON()
@@ -46,6 +46,119 @@ export const toJSON = (x: unknown): unknown => {
4646
return redact(x)
4747
}
4848

49+
const CIRCULAR = "[Circular]"
50+
51+
/** @internal */
52+
export function formatDate(date: Date): string {
53+
try {
54+
return date.toISOString()
55+
} catch {
56+
return "Invalid Date"
57+
}
58+
}
59+
60+
function safeToString(input: any): string {
61+
try {
62+
const s = input.toString()
63+
return typeof s === "string" ? s : String(s)
64+
} catch {
65+
return "[toString threw]"
66+
}
67+
}
68+
69+
/** @internal */
70+
export function formatPropertyKey(name: PropertyKey): string {
71+
return Predicate.isString(name) ? JSON.stringify(name) : String(name)
72+
}
73+
74+
/** @internal */
75+
export function formatUnknown(
76+
input: unknown,
77+
options?: {
78+
readonly space?: number | string | undefined
79+
readonly ignoreToString?: boolean | undefined
80+
}
81+
): string {
82+
const space = options?.space ?? 0
83+
const seen = new WeakSet<object>()
84+
const gap = !space ? "" : (Predicate.isNumber(space) ? " ".repeat(space) : space)
85+
const ind = (d: number) => gap.repeat(d)
86+
87+
const wrap = (v: unknown, body: string): string => {
88+
const ctor = (v as any)?.constructor
89+
return ctor && ctor !== Object.prototype.constructor && ctor.name ? `${ctor.name}(${body})` : body
90+
}
91+
92+
const ownKeys = (o: object): Array<PropertyKey> => {
93+
try {
94+
return Reflect.ownKeys(o)
95+
} catch {
96+
return ["[ownKeys threw]"]
97+
}
98+
}
99+
100+
function go(v: unknown, d = 0): string {
101+
if (Array.isArray(v)) {
102+
if (seen.has(v)) return CIRCULAR
103+
seen.add(v)
104+
if (!gap || v.length <= 1) return `[${v.map((x) => go(x, d)).join(",")}]`
105+
const inner = v.map((x) => go(x, d + 1)).join(",\n" + ind(d + 1))
106+
return `[\n${ind(d + 1)}${inner}\n${ind(d)}]`
107+
}
108+
109+
if (Predicate.isDate(v)) return formatDate(v)
110+
111+
if (
112+
!options?.ignoreToString &&
113+
Predicate.hasProperty(v, "toString") &&
114+
Predicate.isFunction(v["toString"]) &&
115+
v["toString"] !== Object.prototype.toString &&
116+
v["toString"] !== Array.prototype.toString
117+
) {
118+
const s = safeToString(v)
119+
if (v instanceof Error && v.cause) {
120+
return `${s} (cause: ${go(v.cause, d)})`
121+
}
122+
return s
123+
}
124+
125+
if (Predicate.isString(v)) return JSON.stringify(v)
126+
127+
if (
128+
Predicate.isNumber(v) ||
129+
v == null ||
130+
Predicate.isBoolean(v) ||
131+
Predicate.isSymbol(v)
132+
) return String(v)
133+
134+
if (Predicate.isBigInt(v)) return String(v) + "n"
135+
136+
if (v instanceof Set || v instanceof Map) {
137+
if (seen.has(v)) return CIRCULAR
138+
seen.add(v)
139+
return `${v.constructor.name}(${go(Array.from(v), d)})`
140+
}
141+
142+
if (Predicate.isObject(v)) {
143+
if (seen.has(v)) return CIRCULAR
144+
seen.add(v)
145+
const keys = ownKeys(v)
146+
if (!gap || keys.length <= 1) {
147+
const body = `{${keys.map((k) => `${formatPropertyKey(k)}:${go((v as any)[k], d)}`).join(",")}}`
148+
return wrap(v, body)
149+
}
150+
const body = `{\n${
151+
keys.map((k) => `${ind(d + 1)}${formatPropertyKey(k)}: ${go((v as any)[k], d + 1)}`).join(",\n")
152+
}\n${ind(d)}}`
153+
return wrap(v, body)
154+
}
155+
156+
return String(v)
157+
}
158+
159+
return go(input, 0)
160+
}
161+
49162
/**
50163
* @since 2.0.0
51164
*/

packages/effect/src/ParseResult.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1873,7 +1873,7 @@ function getDefaultTypeMessage(issue: Type): string {
18731873
return issue.message
18741874
}
18751875
const expected = AST.isRefinement(issue.ast) ? getRefinementExpected(issue.ast) : String(issue.ast)
1876-
return `Expected ${expected}, actual ${util_.formatUnknown(issue.actual)}`
1876+
return `Expected ${expected}, actual ${Inspectable.formatUnknown(issue.actual)}`
18771877
}
18781878

18791879
const formatTypeMessage = (issue: Type): Effect.Effect<string> =>

packages/effect/src/Pretty.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @since 3.10.0
33
*/
44
import * as Arr from "./Array.js"
5+
import * as Inspectable from "./Inspectable.js"
56
import * as errors_ from "./internal/schema/errors.js"
67
import * as util_ from "./internal/schema/util.js"
78
import * as Option from "./Option.js"
@@ -43,7 +44,7 @@ const toString = getMatcher((a) => String(a))
4344

4445
const stringify = getMatcher((a) => JSON.stringify(a))
4546

46-
const formatUnknown = getMatcher(util_.formatUnknown)
47+
const formatUnknown = getMatcher(Inspectable.formatUnknown)
4748

4849
/**
4950
* @since 3.10.0
@@ -142,7 +143,7 @@ export const match: AST.Match<Pretty<any>> = {
142143
continue
143144
}
144145
output.push(
145-
`${util_.formatPropertyKey(name)}: ${propertySignaturesTypes[i](input[name])}`
146+
`${Inspectable.formatPropertyKey(name)}: ${propertySignaturesTypes[i](input[name])}`
146147
)
147148
}
148149
// ---------------------------------------------
@@ -156,7 +157,7 @@ export const match: AST.Match<Pretty<any>> = {
156157
if (Object.prototype.hasOwnProperty.call(expectedKeys, key)) {
157158
continue
158159
}
159-
output.push(`${util_.formatPropertyKey(key)}: ${type(input[key])}`)
160+
output.push(`${Inspectable.formatPropertyKey(key)}: ${type(input[key])}`)
160161
}
161162
}
162163
}

packages/effect/src/Schema.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { dual, identity } from "./Function.js"
2929
import { globalValue } from "./GlobalValue.js"
3030
import * as hashMap_ from "./HashMap.js"
3131
import * as hashSet_ from "./HashSet.js"
32+
import * as Inspectable from "./Inspectable.js"
3233
import * as internalCause_ from "./internal/cause.js"
3334
import * as errors_ from "./internal/schema/errors.js"
3435
import * as schemaId_ from "./internal/schema/schemaId.js"
@@ -6647,8 +6648,8 @@ export const lessThanDate = <S extends Schema.Any>(
66476648
filter((a: Date) => a < max, {
66486649
schemaId: LessThanDateSchemaId,
66496650
[LessThanDateSchemaId]: { max },
6650-
title: `lessThanDate(${util_.formatDate(max)})`,
6651-
description: `a date before ${util_.formatDate(max)}`,
6651+
title: `lessThanDate(${Inspectable.formatDate(max)})`,
6652+
description: `a date before ${Inspectable.formatDate(max)}`,
66526653
...annotations
66536654
})
66546655
)
@@ -6674,8 +6675,8 @@ export const lessThanOrEqualToDate = <S extends Schema.Any>(
66746675
filter((a: Date) => a <= max, {
66756676
schemaId: LessThanOrEqualToDateSchemaId,
66766677
[LessThanOrEqualToDateSchemaId]: { max },
6677-
title: `lessThanOrEqualToDate(${util_.formatDate(max)})`,
6678-
description: `a date before or equal to ${util_.formatDate(max)}`,
6678+
title: `lessThanOrEqualToDate(${Inspectable.formatDate(max)})`,
6679+
description: `a date before or equal to ${Inspectable.formatDate(max)}`,
66796680
...annotations
66806681
})
66816682
)
@@ -6699,8 +6700,8 @@ export const greaterThanDate = <S extends Schema.Any>(
66996700
filter((a: Date) => a > min, {
67006701
schemaId: GreaterThanDateSchemaId,
67016702
[GreaterThanDateSchemaId]: { min },
6702-
title: `greaterThanDate(${util_.formatDate(min)})`,
6703-
description: `a date after ${util_.formatDate(min)}`,
6703+
title: `greaterThanDate(${Inspectable.formatDate(min)})`,
6704+
description: `a date after ${Inspectable.formatDate(min)}`,
67046705
...annotations
67056706
})
67066707
)
@@ -6726,8 +6727,8 @@ export const greaterThanOrEqualToDate = <S extends Schema.Any>(
67266727
filter((a: Date) => a >= min, {
67276728
schemaId: GreaterThanOrEqualToDateSchemaId,
67286729
[GreaterThanOrEqualToDateSchemaId]: { min },
6729-
title: `greaterThanOrEqualToDate(${util_.formatDate(min)})`,
6730-
description: `a date after or equal to ${util_.formatDate(min)}`,
6730+
title: `greaterThanOrEqualToDate(${Inspectable.formatDate(min)})`,
6731+
description: `a date after or equal to ${Inspectable.formatDate(min)}`,
67316732
...annotations
67326733
})
67336734
)
@@ -6752,8 +6753,8 @@ export const betweenDate = <S extends Schema.Any>(
67526753
filter((a: Date) => a <= max && a >= min, {
67536754
schemaId: BetweenDateSchemaId,
67546755
[BetweenDateSchemaId]: { max, min },
6755-
title: `betweenDate(${util_.formatDate(min)}, ${util_.formatDate(max)})`,
6756-
description: `a date between ${util_.formatDate(min)} and ${util_.formatDate(max)}`,
6756+
title: `betweenDate(${Inspectable.formatDate(min)}, ${Inspectable.formatDate(max)})`,
6757+
description: `a date between ${Inspectable.formatDate(min)} and ${Inspectable.formatDate(max)}`,
67576758
...annotations
67586759
})
67596760
)
@@ -6822,7 +6823,7 @@ export class DateFromString extends transform(
68226823
{
68236824
strict: true,
68246825
decode: (i) => new Date(i),
6825-
encode: (a) => util_.formatDate(a)
6826+
encode: (a) => Inspectable.formatDate(a)
68266827
}
68276828
).annotations({ identifier: "DateFromString" }) {}
68286829

@@ -6885,7 +6886,8 @@ export class DateTimeUtcFromSelf extends declare(
68856886
const decodeDateTimeUtc = <A extends dateTime.DateTime.Input>(input: A, ast: AST.AST) =>
68866887
ParseResult.try({
68876888
try: () => dateTime.unsafeMake(input),
6888-
catch: () => new ParseResult.Type(ast, input, `Unable to decode ${util_.formatUnknown(input)} into a DateTime.Utc`)
6889+
catch: () =>
6890+
new ParseResult.Type(ast, input, `Unable to decode ${Inspectable.formatUnknown(input)} into a DateTime.Utc`)
68896891
})
68906892

68916893
/**
@@ -8847,7 +8849,7 @@ export const TaggedError = <Self = never>(identifier?: string) =>
88478849
get() {
88488850
return `{ ${
88498851
Reflect.ownKeys(fields)
8850-
.map((p: any) => `${util_.formatPropertyKey(p)}: ${util_.formatUnknown((this)[p])}`)
8852+
.map((p: any) => `${Inspectable.formatPropertyKey(p)}: ${Inspectable.formatUnknown((this)[p])}`)
88518853
.join(", ")
88528854
} }`
88538855
},
@@ -9112,7 +9114,9 @@ const makeClass = <Fields extends Struct.Fields>(
91129114
Object.defineProperty(klass.prototype, "toString", {
91139115
value() {
91149116
return `${identifier}({ ${
9115-
Reflect.ownKeys(fields).map((p: any) => `${util_.formatPropertyKey(p)}: ${util_.formatUnknown(this[p])}`)
9117+
Reflect.ownKeys(fields).map((p: any) =>
9118+
`${Inspectable.formatPropertyKey(p)}: ${Inspectable.formatUnknown(this[p])}`
9119+
)
91169120
.join(", ")
91179121
} })`
91189122
},

packages/effect/src/SchemaAST.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Effect } from "./Effect.js"
77
import type { Equivalence } from "./Equivalence.js"
88
import { dual, identity } from "./Function.js"
99
import { globalValue } from "./GlobalValue.js"
10+
import * as Inspectable from "./Inspectable.js"
1011
import * as errors_ from "./internal/schema/errors.js"
1112
import * as util_ from "./internal/schema/util.js"
1213
import * as Number from "./Number.js"
@@ -533,7 +534,7 @@ export class Literal implements Annotated {
533534
* @since 3.10.0
534535
*/
535536
toString() {
536-
return Option.getOrElse(getExpected(this), () => util_.formatUnknown(this.literal))
537+
return Option.getOrElse(getExpected(this), () => Inspectable.formatUnknown(this.literal))
537538
}
538539
/**
539540
* @since 3.10.0
@@ -577,7 +578,7 @@ export class UniqueSymbol implements Annotated {
577578
* @since 3.10.0
578579
*/
579580
toString() {
580-
return Option.getOrElse(getExpected(this), () => util_.formatUnknown(this.symbol))
581+
return Option.getOrElse(getExpected(this), () => Inspectable.formatUnknown(this.symbol))
581582
}
582583
/**
583584
* @since 3.10.0
@@ -2968,7 +2969,7 @@ const formatKeyword = (ast: AST): string => Option.getOrElse(getExpected(ast), (
29682969
function getBrands(ast: Annotated): string {
29692970
return Option.match(getBrandAnnotation(ast), {
29702971
onNone: () => "",
2971-
onSome: (brands) => brands.map((brand) => ` & Brand<${util_.formatUnknown(brand)}>`).join("")
2972+
onSome: (brands) => brands.map((brand) => ` & Brand<${Inspectable.formatUnknown(brand)}>`).join("")
29722973
})
29732974
}
29742975

0 commit comments

Comments
 (0)