Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

feat: added toString for Schema classes #605

Merged
merged 7 commits into from
Dec 11, 2023
Merged
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
5 changes: 5 additions & 0 deletions .changeset/plenty-spies-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

added toString for schema classes
2 changes: 1 addition & 1 deletion docs/modules/Schema.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -4241,7 +4241,7 @@ export interface FilterAnnotations<A> extends DocAnnotations<A> {
*/
readonly jsonSchema?: AST.JSONSchemaAnnotation
readonly arbitrary?: (...args: ReadonlyArray<Arbitrary<any>>) => Arbitrary<any>
readonly pretty?: (...args: ReadonlyArray<Pretty<any>>) => Pretty<any>
readonly pretty?: (...args: ReadonlyArray<Pretty.Pretty<any>>) => Pretty.Pretty<any>
readonly equivalence?: () => Equivalence.Equivalence<A>
}
```
Expand Down
56 changes: 34 additions & 22 deletions src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import * as InternalSchema from "./internal/schema.js"
import * as InternalSerializable from "./internal/serializable.js"
import * as Parser from "./Parser.js"
import * as ParseResult from "./ParseResult.js"
import type { Pretty } from "./Pretty.js"
import * as Pretty from "./Pretty.js"
import type * as Serializable from "./Serializable.js"

// ---------------------------------------------
Expand Down Expand Up @@ -1503,7 +1503,7 @@ export interface FilterAnnotations<A> extends DocAnnotations<A> {
*/
readonly jsonSchema?: AST.JSONSchemaAnnotation
readonly arbitrary?: (...args: ReadonlyArray<Arbitrary<any>>) => Arbitrary<any>
readonly pretty?: (...args: ReadonlyArray<Pretty<any>>) => Pretty<any>
readonly pretty?: (...args: ReadonlyArray<Pretty.Pretty<any>>) => Pretty.Pretty<any>
readonly equivalence?: () => Equivalence.Equivalence<A>
}

Expand Down Expand Up @@ -3010,7 +3010,7 @@ export const DurationFromSelf: Schema<Duration.Duration> = declare(
: ParseResult.fail(ParseResult.type(ast, u)),
{
[AST.IdentifierAnnotationId]: "Duration",
[hooks.PrettyHookId]: (): Pretty<Duration.Duration> => (duration) => String(duration),
[hooks.PrettyHookId]: (): Pretty.Pretty<Duration.Duration> => (duration) => String(duration),
[hooks.ArbitraryHookId]: (): Arbitrary<Duration.Duration> => (fc) =>
fc.oneof(
fc.constant(Duration.infinity),
Expand Down Expand Up @@ -3303,7 +3303,7 @@ export const Uint8ArrayFromSelf: Schema<Uint8Array> = declare(
: ParseResult.fail(ParseResult.type(ast, u)),
{
[AST.IdentifierAnnotationId]: "Uint8Array",
[hooks.PrettyHookId]: (): Pretty<Uint8Array> => (u8arr) =>
[hooks.PrettyHookId]: (): Pretty.Pretty<Uint8Array> => (u8arr) =>
`new Uint8Array(${JSON.stringify(Array.from(u8arr))})`,
[hooks.ArbitraryHookId]: (): Arbitrary<Uint8Array> => (fc) => fc.uint8Array(),
[hooks.EquivalenceHookId]: () => ReadonlyArray.getEquivalence(Equivalence.strict())
Expand Down Expand Up @@ -3376,7 +3376,7 @@ const makeEncodingTransform = <A extends string>(
{ strict: false }
).pipe(annotations({
[AST.IdentifierAnnotationId]: id,
[hooks.PrettyHookId]: (): Pretty<Uint8Array> => (u) => `${id}(${encode(u)})`,
[hooks.PrettyHookId]: (): Pretty.Pretty<Uint8Array> => (u) => `${id}(${encode(u)})`,
[hooks.ArbitraryHookId]: () => arbitrary
}))

Expand Down Expand Up @@ -3570,7 +3570,7 @@ export const validDate =

const dateArbitrary = (): Arbitrary<Date> => (fc) => fc.date({ noInvalidDate: false })

const datePretty = (): Pretty<Date> => (date) => `new Date(${JSON.stringify(date)})`
const datePretty = (): Pretty.Pretty<Date> => (date) => `new Date(${JSON.stringify(date)})`

/**
* Represents a schema for handling potentially **invalid** `Date` instances (e.g., `new Date("Invalid Date")` is not rejected).
Expand Down Expand Up @@ -3676,7 +3676,7 @@ const optionArbitrary = <A>(value: Arbitrary<A>): Arbitrary<Option.Option<A>> =>
return (fc) => arb(fc).map(optionDecode)
}

const optionPretty = <A>(value: Pretty<A>): Pretty<Option.Option<A>> =>
const optionPretty = <A>(value: Pretty.Pretty<A>): Pretty.Pretty<Option.Option<A>> =>
Option.match({
onNone: () => "none()",
onSome: (a) => `some(${value(a)})`
Expand Down Expand Up @@ -3780,7 +3780,10 @@ const eitherArbitrary = <E, A>(
return (fc) => arb(fc).map(eitherDecode)
}

const eitherPretty = <E, A>(left: Pretty<E>, right: Pretty<A>): Pretty<Either.Either<E, A>> =>
const eitherPretty = <E, A>(
left: Pretty.Pretty<E>,
right: Pretty.Pretty<A>
): Pretty.Pretty<Either.Either<E, A>> =>
Either.match({
onLeft: (e) => `left(${left(e)})`,
onRight: (a) => `right(${right(a)})`
Expand Down Expand Up @@ -3847,9 +3850,9 @@ const readonlyMapArbitrary = <K, V>(
(fc) => fc.array(fc.tuple(key(fc), value(fc))).map((as) => new Map(as))

const readonlyMapPretty = <K, V>(
key: Pretty<K>,
value: Pretty<V>
): Pretty<ReadonlyMap<K, V>> =>
key: Pretty.Pretty<K>,
value: Pretty.Pretty<V>
): Pretty.Pretty<ReadonlyMap<K, V>> =>
(map) =>
`new Map([${
Array.from(map.entries())
Expand Down Expand Up @@ -3922,7 +3925,7 @@ const isSet = (u: unknown): u is Set<unknown> => u instanceof Set
const readonlySetArbitrary = <A>(item: Arbitrary<A>): Arbitrary<ReadonlySet<A>> => (fc) =>
fc.array(item(fc)).map((as) => new Set(as))

const readonlySetPretty = <A>(item: Pretty<A>): Pretty<ReadonlySet<A>> => (set) =>
const readonlySetPretty = <A>(item: Pretty.Pretty<A>): Pretty.Pretty<ReadonlySet<A>> => (set) =>
`new Set([${Array.from(set.values()).map((a) => item(a)).join(", ")}])`

const readonlySetEquivalence = <A>(
Expand Down Expand Up @@ -3979,7 +3982,7 @@ export const readonlySet = <I, A>(item: Schema<I, A>): Schema<ReadonlyArray<I>,
// BigDecimal transformations
// ---------------------------------------------

const bigDecimalPretty = (): Pretty<BigDecimal.BigDecimal> => (val) =>
const bigDecimalPretty = (): Pretty.Pretty<BigDecimal.BigDecimal> => (val) =>
`BigDecimal(${BigDecimal.format(BigDecimal.normalize(val))})`

const bigDecimalArbitrary = (): Arbitrary<BigDecimal.BigDecimal> => (fc) =>
Expand Down Expand Up @@ -4323,7 +4326,7 @@ export const negateBigDecimal = <I, A extends BigDecimal.BigDecimal>(
const chunkArbitrary = <A>(item: Arbitrary<A>): Arbitrary<Chunk.Chunk<A>> => (fc) =>
fc.array(item(fc)).map(Chunk.fromIterable)

const chunkPretty = <A>(item: Pretty<A>): Pretty<Chunk.Chunk<A>> => (c) =>
const chunkPretty = <A>(item: Pretty.Pretty<A>): Pretty.Pretty<Chunk.Chunk<A>> => (c) =>
`Chunk(${Chunk.toReadonlyArray(c).map(item).join(", ")})`

/**
Expand Down Expand Up @@ -4382,8 +4385,8 @@ const dataArbitrary = <A extends Readonly<Record<string, any>> | ReadonlyArray<a
(fc) => item(fc).map(toData)

const dataPretty = <A extends Readonly<Record<string, any>> | ReadonlyArray<any>>(
item: Pretty<A>
): Pretty<Data.Data<A>> =>
item: Pretty.Pretty<A>
): Pretty.Pretty<Data.Data<A>> =>
(d) => `Data(${item(d)})`

/**
Expand Down Expand Up @@ -4638,6 +4641,7 @@ const makeClass = <I, A>(
additionalProps?: any
): any => {
const validator = Parser.validateSync(selfSchema)
const pretty = Pretty.to(selfSchema)

return class extends Base {
constructor(props: any = {}, disableValidation = false) {
Expand All @@ -4649,6 +4653,10 @@ const makeClass = <I, A>(

static [TypeId] = variance

toString() {
return `${this.constructor.name}(${pretty(this as any)})`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be added to the ast as a pretty hook?

Copy link
Member

@tim-smart tim-smart Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking this:

diff --git a/src/Schema.ts b/src/Schema.ts
index 3a76808..1c461c5 100644
--- a/src/Schema.ts
+++ b/src/Schema.ts
@@ -4433,7 +4433,6 @@ const makeClass = <I, A>(
   additionalProps?: any
 ): any => {
   const validator = Parser.validateSync(selfSchema)
-  const pretty = Pretty.to(selfSchema)
 
   return class extends Base {
     constructor(props: any = {}, disableValidation = false) {
@@ -4446,7 +4445,7 @@ const makeClass = <I, A>(
     static [TypeId] = variance
 
     toString() {
-      return `${this.constructor.name}(${pretty(this as any)})`
+      return Pretty.to(this.constructor as any)(this)
     }
 
     static pipe() {
@@ -4462,7 +4461,8 @@ const makeClass = <I, A>(
             ParseResult.succeed(input)
             : ParseResult.fail(ParseResult.type(ast, input)), {
           [AST.DescriptionAnnotationId]: `an instance of ${this.name}`,
-          [hooks.PrettyHookId]: () => (props: any) => new this(props).toString(),
+          [hooks.PrettyHookId]: (struct: any) => (self: any) =>
+            `${self.constructor.name}(${struct(self)})`,
           [hooks.ArbitraryHookId]: (struct: any) => (fc: any) =>
             struct(fc).map((props: any) => new this(props))
         }),

Copy link
Contributor Author

@jessekelly881 jessekelly881 Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I don't know. Currently the Duration.toString and BigDecimal.toString both include the name of the type. It probably makes since to include it here too. Also, it makes sense to compile the pretty fn outside of toString for efficiency and potentially safety(it could throw).
https://github.com/Effect-TS/effect/blob/2c64bf0f003c9542b0b6efd571c516337b32b6af/src/BigDecimal.ts#L66

The change to the pretty hook makes sense though. I updated that.

}

static pipe() {
return pipeArguments(this, arguments)
}
Expand All @@ -4662,6 +4670,8 @@ const makeClass = <I, A>(
ParseResult.succeed(input)
: ParseResult.fail(ParseResult.type(ast, input)), {
[AST.DescriptionAnnotationId]: `an instance of ${this.name}`,
[hooks.PrettyHookId]: (struct: any) => (self: any) =>
`${self.constructor.name}(${struct(self)})`,
[hooks.ArbitraryHookId]: (struct: any) => (fc: any) =>
struct(fc).map((props: any) => new this(props))
}),
Expand Down Expand Up @@ -4770,7 +4780,7 @@ const fiberIdFromArbitrary = arbitrary.unsafe(FiberIdFrom)
const fiberIdArbitrary: Arbitrary<FiberId.FiberId> = (fc) =>
fiberIdFromArbitrary(fc).map(fiberIdDecode)

const fiberIdPretty: Pretty<FiberId.FiberId> = (fiberId) => {
const fiberIdPretty: Pretty.Pretty<FiberId.FiberId> = (fiberId) => {
switch (fiberId._tag) {
case "None":
return "FiberId.none"
Expand Down Expand Up @@ -4918,7 +4928,7 @@ const causeArbitrary = <E>(
return (fc) => arb(fc).map(causeDecode)
}

const causePretty = <E>(error: Pretty<E>): Pretty<Cause.Cause<E>> => (cause) => {
const causePretty = <E>(error: Pretty.Pretty<E>): Pretty.Pretty<Cause.Cause<E>> => (cause) => {
const f = (cause: Cause.Cause<E>): string => {
switch (cause._tag) {
case "Empty":
Expand Down Expand Up @@ -5090,10 +5100,12 @@ const exitArbitrary = <E, A>(
return (fc) => arb(fc).map(exitDecode)
}

const exitPretty = <E, A>(error: Pretty<E>, value: Pretty<A>): Pretty<Exit.Exit<E, A>> => (exit) =>
exit._tag === "Failure"
? `Exit.failCause(${causePretty(error)(exit.cause)})`
: `Exit.succeed(${value(exit.value)})`
const exitPretty =
<E, A>(error: Pretty.Pretty<E>, value: Pretty.Pretty<A>): Pretty.Pretty<Exit.Exit<E, A>> =>
(exit) =>
exit._tag === "Failure"
? `Exit.failCause(${causePretty(error)(exit.cause)})`
: `Exit.succeed(${value(exit.value)})`

/**
* @category Exit
Expand Down
20 changes: 20 additions & 0 deletions test/Schema/Class.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as AST from "@effect/schema/AST"
import * as PR from "@effect/schema/ParseResult"
import * as Pretty from "@effect/schema/Pretty"
import * as S from "@effect/schema/Schema"
import * as Serializable from "@effect/schema/Serializable"
import * as Util from "@effect/schema/test/util"
Expand Down Expand Up @@ -170,6 +171,10 @@ describe("Schema/Class", () => {
it("Data.Class", () => {
const person = new Person({ id: 1, name: "John" })
const personAge = new PersonWithAge({ id: 1, name: "John", age: 30 })

expect(String(person)).toEqual(`Person({ "id": 1, "name": "John" })`)
expect(String(personAge)).toEqual(`PersonWithAge({ "id": 1, "age": 30, "name": "John" })`)

expect(person instanceof Data.Class).toEqual(true)
expect(personAge instanceof Data.Class).toEqual(true)

Expand All @@ -180,6 +185,13 @@ describe("Schema/Class", () => {
expect(!Equal.equals(person, person3)).toEqual(true)
})

it("Pretty/to", () => {
const pretty = Pretty.to(Person)
expect(pretty(new Person({ id: 1, name: "John" }))).toEqual(
`Person({ "id": 1, "name": "John" })`
)
})

it("transform", () => {
const decode = S.decodeSync(PersonWithTransform)
const person = decode({
Expand Down Expand Up @@ -208,6 +220,10 @@ describe("Schema/Class", () => {

it("TaggedClass", () => {
let person = new TaggedPersonWithAge({ id: 1, name: "John", age: 30 })

expect(String(person)).toEqual(
`TaggedPersonWithAge({ "_tag": "TaggedPerson", "id": 1, "age": 30, "name": "John" })`
)
expect(person._tag).toEqual("TaggedPerson")
expect(person.upperName).toEqual("JOHN")

Expand All @@ -232,6 +248,8 @@ describe("Schema/Class", () => {
}) {}

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

expect(String(err)).toEqual(`MyError({ "_tag": "MyError", "id": 1 })`)
expect(err.stack).toContain("Class.test.ts:")
expect(err._tag).toEqual("MyError")
expect(err.id).toEqual(1)
Expand All @@ -251,6 +269,8 @@ describe("Schema/Class", () => {
}) {}

let req = new MyRequest({ id: 1 })

expect(String(req)).toEqual(`MyRequest({ "_tag": "MyRequest", "id": 1 })`)
expect(req._tag).toEqual("MyRequest")
expect(req.id).toEqual(1)
expect(Request.isRequest(req)).toEqual(true)
Expand Down
Loading