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

Commit

Permalink
Suspend: merge outer annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Dec 31, 2023
1 parent 5fc77be commit d7c5483
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 70 deletions.
8 changes: 4 additions & 4 deletions docs/modules/Format.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Added in v1.0.0
- [formatting](#formatting)
- [format](#format)
- [formatAST](#formatast)
- [formatSchema](#formatschema)
- [formatUnknown](#formatunknown)

---

Expand All @@ -26,7 +26,7 @@ Added in v1.0.0
**Signature**

```ts
export declare const format: (u: unknown) => string
export declare const format: <I, A>(schema: Schema.Schema<I, A>) => string
```
Added in v1.0.0
Expand All @@ -41,12 +41,12 @@ export declare const formatAST: (ast: AST.AST, verbose?: boolean) => string
Added in v1.0.0
## formatSchema
## formatUnknown
**Signature**
```ts
export declare const formatSchema: <I, A>(schema: Schema.Schema<I, A>) => string
export declare const formatUnknown: (u: unknown) => string
```
Added in v1.0.0
20 changes: 12 additions & 8 deletions src/AST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1181,20 +1181,24 @@ export const isTypeLiteralTransformation = (
*
* @since 1.0.0
*/
export const mergeAnnotations = (ast: AST, annotations: Annotated["annotations"]): AST => ({
...ast,
annotations: { ...ast.annotations, ...annotations }
})
export const mergeAnnotations = (ast: AST, annotations: Annotated["annotations"]): AST => {
return {
...ast,
annotations: { ...ast.annotations, ...annotations }
}
}

/**
* Adds an annotation, potentially overwriting the existing annotation with the specified id.
*
* @since 1.0.0
*/
export const setAnnotation = (ast: AST, sym: symbol, value: unknown): AST => ({
...ast,
annotations: { ...ast.annotations, [sym]: value }
})
export const setAnnotation = (ast: AST, sym: symbol, value: unknown): AST => {
return {
...ast,
annotations: { ...ast.annotations, [sym]: value }
}
}

/**
* Adds a rest element to the end of a tuple, or throws an exception if the rest element is already present.
Expand Down
31 changes: 12 additions & 19 deletions src/Arbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* @since 1.0.0
*/

import { pipe } from "effect/Function"
import * as Option from "effect/Option"
import * as Predicate from "effect/Predicate"
import * as ReadonlyArray from "effect/ReadonlyArray"
Expand Down Expand Up @@ -274,26 +273,20 @@ export const go = (ast: AST.AST, options: Options): Arbitrary<any> => {
case "Refinement": {
const constraints = combineConstraints(options.constraints, getConstraints(ast))
const from = go(ast.from, constraints ? { ...options, constraints } : options)
return pipe(
getHook(ast),
Option.match({
onNone: () => (fc) =>
from(fc).filter((a) => Option.isNone(ast.filter(a, Parser.defaultParseOption, ast))),
onSome: (handler) => handler(from)
})
)
return Option.match(getHook(ast), {
onNone: () => (fc) =>
from(fc).filter((a) => Option.isNone(ast.filter(a, Parser.defaultParseOption, ast))),
onSome: (handler) => handler(from)
})
}
case "Suspend": {
return pipe(
getHook(ast),
Option.match({
onNone: () => {
const get = Internal.memoizeThunk(() => go(ast.f(), { ...options, isSuspend: true }))
return (fc) => fc.constant(null).chain(() => get()(fc))
},
onSome: (handler) => handler()
})
)
return Option.match(getHook(ast), {
onNone: () => {
const get = Internal.memoizeThunk(() => go(ast.f(), { ...options, isSuspend: true }))
return (fc) => fc.constant(null).chain(() => get()(fc))
},
onSome: (handler) => handler()
})
}
case "Transform":
throw new Error("cannot build an Arbitrary for transformations")
Expand Down
1 change: 0 additions & 1 deletion src/ArrayFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ const format = (self: ParseIssue, path: ReadonlyArray<PropertyKey> = []): Array<
(key) => ReadonlyArray.flatMap(key.errors, (e) => format(e, [...path, key.key]))
)
case "Transform":
return ReadonlyArray.flatMap(self.errors, (e) => format(e, path))
case "Refinement":
return ReadonlyArray.flatMap(self.errors, (e) => format(e, path))
}
Expand Down
13 changes: 8 additions & 5 deletions src/Format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type * as Schema from "./Schema.js"
* @category formatting
* @since 1.0.0
*/
export const formatSchema = <I, A>(schema: Schema.Schema<I, A>): string => formatAST(schema.ast)
export const format = <I, A>(schema: Schema.Schema<I, A>): string => formatAST(schema.ast)

/**
* @category formatting
Expand All @@ -31,9 +31,9 @@ export const formatAST = (ast: AST.AST, verbose: boolean = false): string => {
case "NeverKeyword":
return Option.getOrElse(getExpected(ast, verbose), () => ast._tag)
case "Literal":
return Option.getOrElse(getExpected(ast, verbose), () => format(ast.literal))
return Option.getOrElse(getExpected(ast, verbose), () => formatUnknown(ast.literal))
case "UniqueSymbol":
return Option.getOrElse(getExpected(ast, verbose), () => format(ast.symbol))
return Option.getOrElse(getExpected(ast, verbose), () => formatUnknown(ast.symbol))
case "Union":
return Option.getOrElse(
getExpected(ast, verbose),
Expand All @@ -54,7 +54,10 @@ export const formatAST = (ast: AST.AST, verbose: boolean = false): string => {
}>`
)
case "Suspend":
return Option.getOrElse(getExpected(ast, verbose), () => "<suspended schema>")
return getExpected(ast, verbose).pipe(
Option.orElse(() => getExpected(ast.f(), verbose)),
Option.getOrElse(() => "<suspended schema>")
)
case "Declaration":
return Option.getOrElse(getExpected(ast, verbose), () => "<declaration schema>")
case "Refinement":
Expand All @@ -74,7 +77,7 @@ export const formatTransformation = (from: string, to: string): string => `(${fr
* @category formatting
* @since 1.0.0
*/
export const format = (u: unknown): string => {
export const formatUnknown = (u: unknown): string => {
if (Predicate.isString(u)) {
return JSON.stringify(u)
} else if (
Expand Down
4 changes: 3 additions & 1 deletion src/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,9 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser<any, any> => {
}
}
case "Suspend": {
const get = Internal.memoizeThunk(() => goMemo(ast.f(), isDecoding))
const get = Internal.memoizeThunk(() =>
goMemo(AST.mergeAnnotations(ast.f(), ast.annotations), isDecoding)
)
return (a, options) => get()(a, options)
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/Pretty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export const match: AST.Match<Pretty<any>> = {
"UniqueSymbol": toString,
"TemplateLiteral": stringify,
"UndefinedKeyword": toString,
"UnknownKeyword": () => Format.format,
"AnyKeyword": () => Format.format,
"ObjectKeyword": () => Format.format,
"UnknownKeyword": () => Format.formatUnknown,
"AnyKeyword": () => Format.formatUnknown,
"ObjectKeyword": () => Format.formatUnknown,
"StringKeyword": stringify,
"NumberKeyword": toString,
"BooleanKeyword": toString,
Expand Down
22 changes: 9 additions & 13 deletions src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3614,7 +3614,7 @@ export const optionFromSelf = <I, A>(
: ParseResult.fail(ParseResult.type(ast, u))
},
{
[AST.DescriptionAnnotationId]: `Option<${Format.formatSchema(value)}>`,
[AST.DescriptionAnnotationId]: `Option<${Format.format(value)}>`,
[hooks.PrettyHookId]: optionPretty,
[hooks.ArbitraryHookId]: optionArbitrary,
[hooks.EquivalenceHookId]: Option.getEquivalence
Expand Down Expand Up @@ -3740,9 +3740,7 @@ export const eitherFromSelf = <IE, E, IA, A>(
: ParseResult.fail(ParseResult.type(ast, u))
},
{
[AST.DescriptionAnnotationId]: `Either<${Format.formatSchema(left)}, ${
Format.formatSchema(right)
}>`,
[AST.DescriptionAnnotationId]: `Either<${Format.format(left)}, ${Format.format(right)}>`,
[hooks.PrettyHookId]: eitherPretty,
[hooks.ArbitraryHookId]: eitherArbitrary,
[hooks.EquivalenceHookId]: Either.getEquivalence
Expand Down Expand Up @@ -3822,9 +3820,7 @@ export const readonlyMapFromSelf = <IK, K, IV, V>(
: ParseResult.fail(ParseResult.type(ast, u))
},
{
[AST.IdentifierAnnotationId]: `ReadonlyMap<${Format.formatSchema(key)}, ${
Format.formatSchema(value)
}>`,
[AST.IdentifierAnnotationId]: `ReadonlyMap<${Format.format(key)}, ${Format.format(value)}>`,
[hooks.PrettyHookId]: readonlyMapPretty,
[hooks.ArbitraryHookId]: readonlyMapArbitrary,
[hooks.EquivalenceHookId]: readonlyMapEquivalence
Expand Down Expand Up @@ -3886,7 +3882,7 @@ export const readonlySetFromSelf = <I, A>(
: ParseResult.fail(ParseResult.type(ast, u))
},
{
[AST.DescriptionAnnotationId]: `ReadonlySet<${Format.formatSchema(item)}>`,
[AST.DescriptionAnnotationId]: `ReadonlySet<${Format.format(item)}>`,
[hooks.PrettyHookId]: readonlySetPretty,
[hooks.ArbitraryHookId]: readonlySetArbitrary,
[hooks.EquivalenceHookId]: readonlySetEquivalence
Expand Down Expand Up @@ -4292,7 +4288,7 @@ export const chunkFromSelf = <I, A>(item: Schema<I, A>): Schema<Chunk.Chunk<I>,
}
},
{
[AST.DescriptionAnnotationId]: `Chunk<${Format.formatSchema(item)}>`,
[AST.DescriptionAnnotationId]: `Chunk<${Format.format(item)}>`,
[hooks.PrettyHookId]: chunkPretty,
[hooks.ArbitraryHookId]: chunkArbitrary,
[hooks.EquivalenceHookId]: Chunk.getEquivalence
Expand Down Expand Up @@ -4346,7 +4342,7 @@ export const dataFromSelf = <
: ParseResult.fail(ParseResult.type(ast, u))
},
{
[AST.DescriptionAnnotationId]: `Data<${Format.formatSchema(item)}>`,
[AST.DescriptionAnnotationId]: `Data<${Format.format(item)}>`,
[hooks.PrettyHookId]: dataPretty,
[hooks.ArbitraryHookId]: dataArbitrary,
[hooks.EquivalenceHookId]: () => Equal.equals
Expand Down Expand Up @@ -4856,8 +4852,8 @@ const causeFrom = <EI, E>(
error: Schema<EI, E>,
defect: Schema<unknown, unknown>
): Schema<CauseFrom<EI>, CauseFrom<E>> => {
const desc = `CauseFrom<${Format.formatSchema(error)}>`
const recur = suspend(() => out).pipe(description(desc))
const desc = `CauseFrom<${Format.format(error)}>`
const recur = suspend(() => out)
const out: Schema<CauseFrom<EI>, CauseFrom<E>> = union(
causeDieFrom(defect),
CauseEmptyFrom,
Expand Down Expand Up @@ -4918,7 +4914,7 @@ export const causeFromSelf = <IE, E>(
}
},
{
[AST.DescriptionAnnotationId]: `Cause<${Format.formatSchema(error)}>`,
[AST.DescriptionAnnotationId]: `Cause<${Format.format(error)}>`,
[hooks.PrettyHookId]: causePretty,
[hooks.ArbitraryHookId]: causeArbitrary,
[hooks.EquivalenceHookId]: () => Equal.equals
Expand Down
7 changes: 3 additions & 4 deletions src/TreeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const formatMessage = (e: Type): string =>
getMessage(e.ast, e.actual).pipe(
Option.orElse(() => e.message),
Option.getOrElse(() =>
`Expected ${Format.formatAST(e.ast, true)}, actual ${Format.format(e.actual)}`
`Expected ${Format.formatAST(e.ast, true)}, actual ${Format.formatUnknown(e.actual)}`
)
)

Expand All @@ -101,7 +101,7 @@ const go = (e: ParseIssue): Tree<string> => {
case "Unexpected":
return make(`is unexpected, expected ${Format.formatAST(e.expected, true)}`)
case "Key":
return make(`[${Format.format(e.key)}]`, e.errors.map(go))
return make(`[${Format.formatUnknown(e.key)}]`, e.errors.map(go))
case "Missing":
return make("is missing")
case "Union":
Expand Down Expand Up @@ -141,14 +141,13 @@ const go = (e: ParseIssue): Tree<string> => {
make(Format.formatAST(e.ast), [make(formatTransformationKind(e.kind), e.errors.map(go))]),
onSome: make
})
case "Refinement": {
case "Refinement":
return Option.match(getRefinementMessage(e, e.actual), {
onNone: () =>
make(Format.formatAST(e.ast), [
make(formatRefinementKind(e.kind), e.errors.map(go))
]),
onSome: make
})
}
}
}
20 changes: 10 additions & 10 deletions test/Format.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { format, formatSchema } from "@effect/schema/Format"
import { format, formatUnknown } from "@effect/schema/Format"
import * as S from "@effect/schema/Schema"
import * as _ from "@effect/schema/TreeFormatter"
import { describe, expect, it } from "vitest"

describe("Format", () => {
describe("formatSchema", () => {
describe("format", () => {
it("refinement", () => {
const schema = S.string.pipe(S.minLength(2))
expect(formatSchema(schema)).toEqual("a string at least 2 character(s) long")
expect(format(schema)).toEqual("a string at least 2 character(s) long")
})

it("union", () => {
const schema = S.union(S.string, S.string.pipe(S.minLength(2)))
expect(formatSchema(schema)).toEqual(
expect(format(schema)).toEqual(
"a string at least 2 character(s) long | string"
)
})
Expand All @@ -22,28 +22,28 @@ describe("Format", () => {
const schema: S.Schema<A> = S.suspend( // intended outer suspend
() => S.tuple(S.number, S.union(schema, S.literal(null)))
)
expect(formatSchema(schema)).toEqual("<suspended schema>")
expect(format(schema)).toEqual("<suspended schema>")
})
})

describe("format", () => {
describe("formatUnknown", () => {
it("should handle unexpected errors", () => {
const circular: any = { a: null }
circular.a = circular
expect(format(circular)).toEqual("[object Object]")
expect(formatUnknown(circular)).toEqual("[object Object]")
})

it("should detect data types with a custom `toString` implementation", () => {
const noToString = { a: 1 }
expect(format(noToString)).toEqual(`{"a":1}`)
expect(formatUnknown(noToString)).toEqual(`{"a":1}`)
const ToString = Object.create({
toString() {
return "toString custom implementation"
}
})
expect(format(ToString)).toEqual("toString custom implementation")
expect(formatUnknown(ToString)).toEqual("toString custom implementation")
// should not detect arrays
expect(format([1, 2, 3])).toEqual("[1,2,3]")
expect(formatUnknown([1, 2, 3])).toEqual("[1,2,3]")
})
})
})
Loading

0 comments on commit d7c5483

Please sign in to comment.