diff --git a/.changeset/chilly-parrots-compare.md b/.changeset/chilly-parrots-compare.md new file mode 100644 index 000000000..610adacde --- /dev/null +++ b/.changeset/chilly-parrots-compare.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +add Serializable module diff --git a/.changeset/late-ladybugs-cheat.md b/.changeset/late-ladybugs-cheat.md new file mode 100644 index 000000000..1574c5f9f --- /dev/null +++ b/.changeset/late-ladybugs-cheat.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +add FiberId schema diff --git a/.changeset/purple-laws-run.md b/.changeset/purple-laws-run.md new file mode 100644 index 000000000..b7851dd12 --- /dev/null +++ b/.changeset/purple-laws-run.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +add Exit schema diff --git a/.changeset/witty-ladybugs-protect.md b/.changeset/witty-ladybugs-protect.md new file mode 100644 index 000000000..12c51f8ec --- /dev/null +++ b/.changeset/witty-ladybugs-protect.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +add Cause schema diff --git a/docs/modules/Schema.ts.md b/docs/modules/Schema.ts.md index 2c0f176fe..5db5a77bf 100644 --- a/docs/modules/Schema.ts.md +++ b/docs/modules/Schema.ts.md @@ -31,6 +31,10 @@ Added in v1.0.0 - [BigDecimal transformations](#bigdecimal-transformations) - [clampBigDecimal](#clampbigdecimal) - [negateBigDecimal](#negatebigdecimal) +- [Cause](#cause) + - [CauseFrom (type alias)](#causefrom-type-alias) + - [cause](#cause-1) + - [causeFromSelf](#causefromself) - [Chunk transformations](#chunk-transformations) - [chunk](#chunk) - [chunkFromSelf](#chunkfromself) @@ -55,6 +59,14 @@ Added in v1.0.0 - [EitherFrom (type alias)](#eitherfrom-type-alias) - [either](#either) - [eitherFromSelf](#eitherfromself) +- [Exit](#exit) + - [ExitFrom (type alias)](#exitfrom-type-alias) + - [exit](#exit-1) + - [exitFromSelf](#exitfromself) +- [FiberId](#fiberid) + - [FiberId](#fiberid-1) + - [FiberIdFrom (type alias)](#fiberidfrom-type-alias) + - [FiberIdFromSelf](#fiberidfromself) - [Option transformations](#option-transformations) - [OptionFrom (type alias)](#optionfrom-type-alias) - [option](#option) @@ -120,9 +132,9 @@ Added in v1.0.0 - [TaggedClass](#taggedclass) - [TaggedError](#taggederror) - [TaggedRequest](#taggedrequest) + - [TaggedRequest (interface)](#taggedrequest-interface) - [TaggedRequest (namespace)](#taggedrequest-namespace) - - [Base (interface)](#base-interface) - - [ResultSchemas (interface)](#resultschemas-interface) + - [Any (type alias)](#any-type-alias) - [combinators](#combinators) - [array](#array) - [attachPropertySignature](#attachpropertysignature) @@ -562,6 +574,69 @@ export declare const negateBigDecimal: (self Added in v1.0.0 +# Cause + +## CauseFrom (type alias) + +**Signature** + +```ts +export type CauseFrom = + | { + readonly _tag: "Die" + readonly defect: unknown + } + | { + readonly _tag: "Empty" + } + | { + readonly _tag: "Fail" + readonly error: E + } + | { + readonly _tag: "Interrupt" + readonly fiberId: FiberIdFrom + } + | { + readonly _tag: "Parallel" + readonly left: CauseFrom + readonly right: CauseFrom + } + | { + readonly _tag: "Sequential" + readonly left: CauseFrom + readonly right: CauseFrom + } +``` + +Added in v1.0.0 + +## cause + +**Signature** + +```ts +export declare const cause: ( + error: Schema, + defect?: Schema +) => Schema, Cause.Cause> +``` + +Added in v1.0.0 + +## causeFromSelf + +**Signature** + +```ts +export declare const causeFromSelf: ( + error: Schema, + defect?: Schema +) => Schema, Cause.Cause> +``` + +Added in v1.0.0 + # Chunk transformations ## chunk @@ -780,6 +855,99 @@ export declare const eitherFromSelf: ( Added in v1.0.0 +# Exit + +## ExitFrom (type alias) + +**Signature** + +```ts +export type ExitFrom = + | { + readonly _tag: "Failure" + readonly cause: CauseFrom + } + | { + readonly _tag: "Success" + readonly value: A + } +``` + +Added in v1.0.0 + +## exit + +**Signature** + +```ts +export declare const exit: ( + error: Schema, + value: Schema, + defect?: Schema +) => Schema, Exit.Exit> +``` + +Added in v1.0.0 + +## exitFromSelf + +**Signature** + +```ts +export declare const exitFromSelf: ( + error: Schema, + value: Schema, + defect?: Schema +) => Schema, Exit.Exit> +``` + +Added in v1.0.0 + +# FiberId + +## FiberId + +**Signature** + +```ts +export declare const FiberId: Schema +``` + +Added in v1.0.0 + +## FiberIdFrom (type alias) + +**Signature** + +```ts +export type FiberIdFrom = + | { + readonly _tag: "Composite" + readonly left: FiberIdFrom + readonly right: FiberIdFrom + } + | { + readonly _tag: "None" + } + | { + readonly _tag: "Runtime" + readonly id: number + readonly startTimeMillis: number + } +``` + +Added in v1.0.0 + +## FiberIdFromSelf + +**Signature** + +```ts +export declare const FiberIdFromSelf: Schema +``` + +Added in v1.0.0 + # Option transformations ## OptionFrom (type alias) @@ -1487,8 +1655,8 @@ Added in v1.0.0 ```ts export declare const TaggedRequest: () => ( tag: Tag, - failure: Schema, - success: Schema, + Failure: Schema, + Success: Schema, fields: Fields ) => [unknown] extends [Self] ? 'Missing `Self` generic - use `class Self extends TaggedRequest()("Tag", SuccessSchema, FailureSchema, { ... })`' @@ -1505,38 +1673,48 @@ export declare const TaggedRequest: () => , Simplify>, Self, - Request.Request - > & - TaggedRequest.ResultSchemas + TaggedRequest< + Tag, + Simplify< + { readonly _tag: Tag } & { + readonly [K in Exclude>]: Schema.From + } & { readonly [K in FromOptionalKeys]?: Schema.From | undefined } + >, + Self, + EI, + EA, + AI, + AA + > + > ``` Added in v1.0.0 -## TaggedRequest (namespace) - -Added in v1.0.0 - -### Base (interface) +## TaggedRequest (interface) **Signature** ```ts -export interface Base & { readonly _tag: string }> - extends Schema, - TaggedRequest.ResultSchemas {} +export interface TaggedRequest + extends Request.Request, + Serializable.SerializableWithResult { + readonly _tag: Tag +} ``` Added in v1.0.0 -### ResultSchemas (interface) +## TaggedRequest (namespace) + +Added in v1.0.0 + +### Any (type alias) **Signature** ```ts -export interface ResultSchemas { - readonly Failure: Schema - readonly Success: Schema -} +export type Any = TaggedRequest ``` Added in v1.0.0 diff --git a/docs/modules/Serializable.ts.md b/docs/modules/Serializable.ts.md new file mode 100644 index 000000000..63b7f6549 --- /dev/null +++ b/docs/modules/Serializable.ts.md @@ -0,0 +1,260 @@ +--- +title: Serializable.ts +nav_order: 11 +parent: Modules +--- + +## Serializable overview + +Added in v1.0.0 + +Serializable represents an object that has self-contained Schema(s) + +--- + +

Table of contents

+ +- [accessor](#accessor) + - [exitSchema](#exitschema) + - [failureSchema](#failureschema) + - [selfSchema](#selfschema) + - [successSchema](#successschema) +- [decoding](#decoding) + - [deserialize](#deserialize) + - [deserializeExit](#deserializeexit) + - [deserializeFailure](#deserializefailure) + - [deserializeSuccess](#deserializesuccess) +- [encoding](#encoding) + - [serialize](#serialize) + - [serializeExit](#serializeexit) + - [serializeFailure](#serializefailure) + - [serializeSuccess](#serializesuccess) +- [model](#model) + - [Serializable (interface)](#serializable-interface) + - [SerializableWithResult (interface)](#serializablewithresult-interface) + - [WithResult (interface)](#withresult-interface) +- [symbol](#symbol) + - [symbol](#symbol-1) + - [symbolResult](#symbolresult) + +--- + +# accessor + +## exitSchema + +**Signature** + +```ts +export declare const exitSchema: ( + self: WithResult +) => Schema.Schema, Exit.Exit> +``` + +Added in v1.0.0 + +## failureSchema + +**Signature** + +```ts +export declare const failureSchema: (self: WithResult) => Schema.Schema +``` + +Added in v1.0.0 + +## selfSchema + +**Signature** + +```ts +export declare const selfSchema: (self: Serializable) => Schema.Schema +``` + +Added in v1.0.0 + +## successSchema + +**Signature** + +```ts +export declare const successSchema: (self: WithResult) => Schema.Schema +``` + +Added in v1.0.0 + +# decoding + +## deserialize + +**Signature** + +```ts +export declare const deserialize: { + (value: unknown): (self: Serializable) => Effect.Effect + (self: Serializable, value: unknown): Effect.Effect +} +``` + +Added in v1.0.0 + +## deserializeExit + +**Signature** + +```ts +export declare const deserializeExit: { + ( + value: unknown + ): (self: WithResult) => Effect.Effect> + ( + self: WithResult, + value: unknown + ): Effect.Effect> +} +``` + +Added in v1.0.0 + +## deserializeFailure + +**Signature** + +```ts +export declare const deserializeFailure: { + (value: unknown): (self: WithResult) => Effect.Effect + (self: WithResult, value: unknown): Effect.Effect +} +``` + +Added in v1.0.0 + +## deserializeSuccess + +**Signature** + +```ts +export declare const deserializeSuccess: { + (value: unknown): (self: WithResult) => Effect.Effect + (self: WithResult, value: unknown): Effect.Effect +} +``` + +Added in v1.0.0 + +# encoding + +## serialize + +**Signature** + +```ts +export declare const serialize: (self: Serializable) => Effect.Effect +``` + +Added in v1.0.0 + +## serializeExit + +**Signature** + +```ts +export declare const serializeExit: { + ( + value: Exit.Exit + ): (self: WithResult) => Effect.Effect> + ( + self: WithResult, + value: Exit.Exit + ): Effect.Effect> +} +``` + +Added in v1.0.0 + +## serializeFailure + +**Signature** + +```ts +export declare const serializeFailure: { + (value: E): (self: WithResult) => Effect.Effect + (self: WithResult, value: E): Effect.Effect +} +``` + +Added in v1.0.0 + +## serializeSuccess + +**Signature** + +```ts +export declare const serializeSuccess: { + (value: A): (self: WithResult) => Effect.Effect + (self: WithResult, value: A): Effect.Effect +} +``` + +Added in v1.0.0 + +# model + +## Serializable (interface) + +**Signature** + +```ts +export interface Serializable { + readonly [symbol]: Schema.Schema +} +``` + +Added in v1.0.0 + +## SerializableWithResult (interface) + +**Signature** + +```ts +export interface SerializableWithResult extends Serializable, WithResult {} +``` + +Added in v1.0.0 + +## WithResult (interface) + +**Signature** + +```ts +export interface WithResult { + readonly [symbolResult]: { + readonly Failure: Schema.Schema + readonly Success: Schema.Schema + } +} +``` + +Added in v1.0.0 + +# symbol + +## symbol + +**Signature** + +```ts +export declare const symbol: typeof symbol +``` + +Added in v1.0.0 + +## symbolResult + +**Signature** + +```ts +export declare const symbolResult: typeof symbolResult +``` + +Added in v1.0.0 diff --git a/docs/modules/TreeFormatter.ts.md b/docs/modules/TreeFormatter.ts.md index ba9fc8f0c..abf211b43 100644 --- a/docs/modules/TreeFormatter.ts.md +++ b/docs/modules/TreeFormatter.ts.md @@ -1,6 +1,6 @@ --- title: TreeFormatter.ts -nav_order: 11 +nav_order: 12 parent: Modules --- diff --git a/docs/modules/index.ts.md b/docs/modules/index.ts.md index 659716c12..46b549066 100644 --- a/docs/modules/index.ts.md +++ b/docs/modules/index.ts.md @@ -22,6 +22,7 @@ Added in v1.0.0 - [From "./Parser.js"](#from-parserjs) - [From "./Pretty.js"](#from-prettyjs) - [From "./Schema.js"](#from-schemajs) + - [From "./Serializable.js"](#from-serializablejs) - [From "./TreeFormatter.js"](#from-treeformatterjs) --- @@ -136,6 +137,20 @@ export * as Schema from "./Schema.js" Added in v1.0.0 +## From "./Serializable.js" + +Re-exports all named exports from the "./Serializable.js" module as `Serializable`. + +**Signature** + +```ts +export * as Serializable from "./Serializable.js" +``` + +Added in v1.0.0 + +Serializable represents an object that has self-contained Schema(s) + ## From "./TreeFormatter.js" Re-exports all named exports from the "./TreeFormatter.js" module as `TreeFormatter`. diff --git a/src/Schema.ts b/src/Schema.ts index f311eb4ff..60f03350f 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -5,6 +5,7 @@ import * as BigDecimal from "effect/BigDecimal" import * as BigInt_ from "effect/BigInt" import * as Brand from "effect/Brand" +import * as Cause from "effect/Cause" import * as Chunk from "effect/Chunk" import * as Data from "effect/Data" import * as Duration from "effect/Duration" @@ -13,6 +14,8 @@ import * as Either from "effect/Either" import * as Encoding from "effect/Encoding" import * as Equal from "effect/Equal" import * as Equivalence from "effect/Equivalence" +import * as Exit from "effect/Exit" +import * as FiberId from "effect/FiberId" import type { LazyArg } from "effect/Function" import { dual, identity } from "effect/Function" import * as N from "effect/Number" @@ -34,9 +37,11 @@ import * as InternalBigInt from "./internal/bigint.js" import * as filters from "./internal/filters.js" import * as hooks from "./internal/hooks.js" 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 type * as Serializable from "./Serializable.js" // --------------------------------------------- // model @@ -3347,12 +3352,14 @@ export { * @category Option transformations * @since 1.0.0 */ -export type OptionFrom = { - readonly _tag: "None" -} | { - readonly _tag: "Some" - readonly value: I -} +export type OptionFrom = + | { + readonly _tag: "None" + } + | { + readonly _tag: "Some" + readonly value: I + } const optionFrom = (value: Schema): Schema, OptionFrom> => union( @@ -3365,8 +3372,8 @@ const optionFrom = (value: Schema): Schema, OptionFrom }) ) -const optionDecode = (o: OptionFrom): Option.Option => - o._tag === "None" ? Option.none() : Option.some(o.value) +const optionDecode = (input: OptionFrom): Option.Option => + input._tag === "None" ? Option.none() : Option.some(input.value) const optionArbitrary = (value: Arbitrary): Arbitrary> => { const placeholder = lazy(() => any).pipe(annotations({ @@ -3422,8 +3429,8 @@ export const option = ( optionFromSelf(to(value)), optionDecode, Option.match({ - onNone: () => ({ _tag: "None" as const }), - onSome: (value) => ({ _tag: "Some" as const, value }) + onNone: () => ({ _tag: "None" }) as const, + onSome: (value) => ({ _tag: "Some", value }) as const }) ) @@ -3444,13 +3451,15 @@ export const optionFromNullable = ( * @category Either transformations * @since 1.0.0 */ -export type EitherFrom = { - readonly _tag: "Left" - readonly left: IE -} | { - readonly _tag: "Right" - readonly right: IA -} +export type EitherFrom = + | { + readonly _tag: "Left" + readonly left: IE + } + | { + readonly _tag: "Right" + readonly right: IA + } const eitherFrom = ( left: Schema, @@ -3467,8 +3476,8 @@ const eitherFrom = ( }) ) -const eitherDecode = (e: EitherFrom): Either.Either => - e._tag === "Left" ? Either.left(e.left) : Either.right(e.right) +const eitherDecode = (input: EitherFrom): Either.Either => + input._tag === "Left" ? Either.left(input.left) : Either.right(input.right) const eitherArbitrary = ( left: Arbitrary, @@ -3533,8 +3542,8 @@ export const either = ( eitherFromSelf(to(left), to(right)), eitherDecode, Either.match({ - onLeft: (left) => ({ _tag: "Left" as const, left }), - onRight: (right) => ({ _tag: "Right" as const, right }) + onLeft: (left) => ({ _tag: "Left", left }) as const, + onRight: (right) => ({ _tag: "Right", right }) as const }) ) @@ -4271,28 +4280,22 @@ export const TaggedError = () => * @category classes * @since 1.0.0 */ -export declare namespace TaggedRequest { - /** - * @category classes - * @since 1.0.0 - */ - export interface Base< - EI, - EA, - AI, - AA, - I, - Req extends Request.Request & { readonly _tag: string } - > extends Schema, TaggedRequest.ResultSchemas {} +export interface TaggedRequest + extends Request.Request, Serializable.SerializableWithResult +{ + readonly _tag: Tag +} +/** + * @category classes + * @since 1.0.0 + */ +export declare namespace TaggedRequest { /** * @category classes * @since 1.0.0 */ - export interface ResultSchemas { - readonly Failure: Schema - readonly Success: Schema - } + export type Any = TaggedRequest } /** @@ -4303,31 +4306,42 @@ export const TaggedRequest = () => ( tag: Tag, - failure: Schema, - success: Schema, + Failure: Schema, + Success: Schema, fields: Fields ): [unknown] extends [Self] ? MissingSelfGeneric<"TaggedRequest", `"Tag", SuccessSchema, FailureSchema, `> - : - & Class< + : Class< + Simplify<{ readonly _tag: Tag } & FromStruct>, + Simplify<{ readonly _tag: Tag } & ToStruct>, + Simplify>, + Self, + TaggedRequest< + Tag, Simplify<{ readonly _tag: Tag } & FromStruct>, - Simplify<{ readonly _tag: Tag } & ToStruct>, - Simplify>, Self, - Request.Request + EI, + EA, + AI, + AA > - & TaggedRequest.ResultSchemas => + > => { + class SerializableRequest extends Request.Class { + get [InternalSerializable.symbol]() { + return this.constructor + } + get [InternalSerializable.symbolResult]() { + return { Failure, Success } + } + } const fieldsWithTag = { ...fields, _tag: literal(tag) } - const Base = makeClass( + return makeClass( struct(fieldsWithTag), fieldsWithTag, - Request.Class, + SerializableRequest, { _tag: tag } ) - Base.Failure = failure - Base.Success = success - return Base } const makeClass = ( @@ -4418,3 +4432,442 @@ const makeClass = ( } } } + +// --------------------------------------------- +// FiberId +// --------------------------------------------- + +/** + * @category FiberId + * @since 1.0.0 + */ +export type FiberIdFrom = + | { + readonly _tag: "Composite" + readonly left: FiberIdFrom + readonly right: FiberIdFrom + } + | { + readonly _tag: "None" + } + | { + readonly _tag: "Runtime" + readonly id: number + readonly startTimeMillis: number + } + +const FiberIdFrom: Schema = union( + struct({ + _tag: literal("Composite"), + left: lazy(() => FiberIdFrom), + right: lazy(() => FiberIdFrom) + }), + struct({ + _tag: literal("None") + }), + struct({ + _tag: literal("Runtime"), + id: Int.pipe(nonNegative({ + title: "id", + description: "id" + })), + startTimeMillis: Int.pipe(nonNegative({ + title: "startTimeMillis", + description: "startTimeMillis" + })) + }) +) + +const fiberIdFromArbitrary = arbitrary.unsafeTo(FiberIdFrom) + +const fiberIdArbitrary: Arbitrary = (fc) => + fiberIdFromArbitrary(fc).map(fiberIdDecode) + +const fiberIdPretty: Pretty = (fiberId) => { + switch (fiberId._tag) { + case "None": + return "FiberId.none" + case "Runtime": + return `FiberId.runtime(${fiberId.id}, ${fiberId.startTimeMillis})` + case "Composite": + return `FiberId.composite(${fiberIdPretty(fiberId.right)}, ${fiberIdPretty(fiberId.left)})` + } +} + +/** + * @category FiberId + * @since 1.0.0 + */ +export const FiberIdFromSelf: Schema = declare( + [], + FiberIdFrom, + () => (input, _, ast) => + FiberId.isFiberId(input) + ? ParseResult.succeed(input) + : ParseResult.fail(ParseResult.type(ast, input)), + { + [AST.IdentifierAnnotationId]: "FiberId", + [hooks.PrettyHookId]: () => fiberIdPretty, + [hooks.ArbitraryHookId]: () => fiberIdArbitrary, + [hooks.EquivalenceHookId]: () => Equal.equals + } +) + +const fiberIdDecode = (input: FiberIdFrom): FiberId.FiberId => { + switch (input._tag) { + case "Composite": + return FiberId.composite(fiberIdDecode(input.left), fiberIdDecode(input.right)) + case "None": + return FiberId.none + case "Runtime": + return FiberId.runtime(input.id, input.startTimeMillis) + } +} + +const fiberIdEncode = (input: FiberId.FiberId): FiberIdFrom => { + switch (input._tag) { + case "None": + return { _tag: "None" } + case "Runtime": + return { _tag: "Runtime", id: input.id, startTimeMillis: input.startTimeMillis } + case "Composite": + return { + _tag: "Composite", + left: fiberIdEncode(input.left), + right: fiberIdEncode(input.right) + } + } +} + +const _FiberId: Schema = transform( + FiberIdFrom, + FiberIdFromSelf, + fiberIdDecode, + fiberIdEncode +) + +export { + /** + * @category FiberId + * @since 1.0.0 + */ + _FiberId as FiberId +} + +// --------------------------------------------- +// Cause +// --------------------------------------------- + +/** + * @category Cause + * @since 1.0.0 + */ +export type CauseFrom = + | { + readonly _tag: "Die" + readonly defect: unknown + } + | { + readonly _tag: "Empty" + } + | { + readonly _tag: "Fail" + readonly error: E + } + | { + readonly _tag: "Interrupt" + readonly fiberId: FiberIdFrom + } + | { + readonly _tag: "Parallel" + readonly left: CauseFrom + readonly right: CauseFrom + } + | { + readonly _tag: "Sequential" + readonly left: CauseFrom + readonly right: CauseFrom + } + +const causeFrom = ( + error: Schema, + defect: Schema +): Schema, CauseFrom> => + union( + struct({ + _tag: literal("Die"), + defect + }), + struct({ + _tag: literal("Empty") + }), + struct({ + _tag: literal("Fail"), + error + }), + struct({ + _tag: literal("Interrupt"), + fiberId: FiberIdFrom + }), + struct({ + _tag: literal("Parallel"), + left: lazy(() => causeFrom(error, defect)), + right: lazy(() => causeFrom(error, defect)) + }), + struct({ + _tag: literal("Sequential"), + left: lazy(() => causeFrom(error, defect)), + right: lazy(() => causeFrom(error, defect)) + }) + ) + +const causeArbitrary = ( + error: Arbitrary, + defect: Arbitrary +): Arbitrary> => { + const placeholderError = lazy(() => any).pipe(annotations({ + [hooks.ArbitraryHookId]: () => error + })) + const placeholderDefect = lazy(() => any).pipe(annotations({ + [hooks.ArbitraryHookId]: () => defect + })) + const arb = arbitrary.unsafeTo(causeFrom(placeholderError, placeholderDefect)) + return (fc) => arb(fc).map(causeDecode) +} + +const causePretty = (error: Pretty): Pretty> => (cause) => { + const f = (cause: Cause.Cause): string => { + switch (cause._tag) { + case "Empty": + return "Cause.empty" + case "Die": + return `Cause.die(${Cause.pretty(cause)})` + case "Interrupt": + return `Cause.interrupt(${fiberIdPretty(cause.fiberId)})` + case "Fail": + return `Cause.fail(${error(cause.error)})` + case "Sequential": + return `Cause.sequential(${f(cause.left)}, ${f(cause.right)})` + case "Parallel": + return `Cause.parallel(${f(cause.left)}, ${f(cause.right)})` + } + } + return f(cause) +} + +/** + * @category Cause + * @since 1.0.0 + */ +export const causeFromSelf = ( + error: Schema, + defect: Schema = unknown +): Schema, Cause.Cause> => { + return declare( + [error, defect], + causeFrom(error, defect), + (isDecoding, error) => { + const parse = isDecoding + ? Parser.parse(causeFrom(error, defect)) + : Parser.encode(causeFrom(error, defect)) + return (u, options, ast) => { + if (Cause.isCause(u)) { + return ParseResult.map(parse(causeEncode(u), options), causeDecode) + } + return ParseResult.fail(ParseResult.type(ast, u)) + } + }, + { + [AST.IdentifierAnnotationId]: "Cause", + [hooks.PrettyHookId]: causePretty, + [hooks.ArbitraryHookId]: causeArbitrary, + [hooks.EquivalenceHookId]: () => Equal.equals + } + ) +} + +function causeDecode(cause: CauseFrom): Cause.Cause { + switch (cause._tag) { + case "Die": + return Cause.die(cause.defect) + case "Empty": + return Cause.empty + case "Interrupt": + return Cause.interrupt(fiberIdDecode(cause.fiberId)) + case "Fail": + return Cause.fail(cause.error) + case "Parallel": + return Cause.parallel(causeDecode(cause.left), causeDecode(cause.right)) + case "Sequential": + return Cause.sequential(causeDecode(cause.left), causeDecode(cause.right)) + } +} + +function causeEncode(cause: Cause.Cause): CauseFrom { + switch (cause._tag) { + case "Empty": + return { _tag: "Empty" } + case "Die": + return { _tag: "Die", defect: cause.defect } + case "Interrupt": + return { _tag: "Interrupt", fiberId: cause.fiberId } + case "Fail": + return { _tag: "Fail", error: cause.error } + case "Sequential": + return { + _tag: "Sequential", + left: causeEncode(cause.left), + right: causeEncode(cause.right) + } + case "Parallel": + return { + _tag: "Parallel", + left: causeEncode(cause.left), + right: causeEncode(cause.right) + } + } +} + +const causeDefectPretty: Schema = transform( + unknown, + unknown, + identity, + (defect) => { + if (Predicate.isObject(defect)) { + return Cause.pretty(Cause.die(defect)) + } + return String(defect) + } +) + +/** + * @category Cause + * @since 1.0.0 + */ +export const cause = ( + error: Schema, + defect: Schema = causeDefectPretty +): Schema, Cause.Cause> => + transform( + causeFrom(error, defect), + causeFromSelf(to(error), to(defect)), + causeDecode, + causeEncode + ) + +// --------------------------------------------- +// Exit +// --------------------------------------------- + +/** + * @category Exit + * @since 1.0.0 + */ +export type ExitFrom = + | { + readonly _tag: "Failure" + readonly cause: CauseFrom + } + | { + readonly _tag: "Success" + readonly value: A + } + +const exitFrom = ( + error: Schema, + value: Schema, + defect: Schema +): Schema, ExitFrom> => + union( + struct({ + _tag: literal("Failure"), + cause: causeFrom(error, defect) + }), + struct({ + _tag: literal("Success"), + value + }) + ) + +const exitDecode = (input: ExitFrom): Exit.Exit => { + switch (input._tag) { + case "Failure": + return Exit.failCause(causeDecode(input.cause)) + case "Success": + return Exit.succeed(input.value) + } +} + +const exitArbitrary = ( + error: Arbitrary, + value: Arbitrary, + defect: Arbitrary +): Arbitrary> => { + const placeholderError = lazy(() => any).pipe(annotations({ + [hooks.ArbitraryHookId]: () => error + })) + const placeholderValue = lazy(() => any).pipe(annotations({ + [hooks.ArbitraryHookId]: () => value + })) + const placeholderDefect = lazy(() => any).pipe(annotations({ + [hooks.ArbitraryHookId]: () => defect + })) + const arb = arbitrary.unsafeTo(exitFrom(placeholderError, placeholderValue, placeholderDefect)) + return (fc) => arb(fc).map(exitDecode) +} + +const exitPretty = (error: Pretty, value: Pretty): Pretty> => (exit) => + exit._tag === "Failure" + ? `Exit.failCause(${causePretty(error)(exit.cause)})` + : `Exit.succeed(${value(exit.value)})` + +/** + * @category Exit + * @since 1.0.0 + */ +export const exitFromSelf = ( + error: Schema, + value: Schema, + defect: Schema = unknown +): Schema, Exit.Exit> => + declare( + [error, value, defect], + exitFrom(error, value, defect), + (isDecoding, error, value) => { + const parseCause = isDecoding + ? Parser.parse(causeFromSelf(error, defect)) + : Parser.encode(causeFromSelf(error, defect)) + const parseValue = isDecoding ? Parser.parse(value) : Parser.encode(value) + return (u, options, ast) => + !Exit.isExit(u) ? + ParseResult.fail(ParseResult.type(ast, u)) : + Exit.isFailure(u) ? + ParseResult.map(parseCause(u.cause, options), Exit.failCause) : + ParseResult.map(parseValue(u.value, options), Exit.succeed) + }, + { + [AST.IdentifierAnnotationId]: "Exit", + [hooks.PrettyHookId]: exitPretty, + [hooks.ArbitraryHookId]: exitArbitrary, + [hooks.EquivalenceHookId]: () => Equal.equals + } + ) + +/** + * @category Exit + * @since 1.0.0 + */ +export const exit = ( + error: Schema, + value: Schema, + defect: Schema = causeDefectPretty +): Schema, Exit.Exit> => + transform( + exitFrom(error, value, defect), + exitFromSelf(to(error), to(value), to(defect)), + exitDecode, + (exit) => + exit._tag === "Failure" + ? { _tag: "Failure", cause: exit.cause } as const + : { _tag: "Success", value: exit.value } as const + ) diff --git a/src/Serializable.ts b/src/Serializable.ts new file mode 100644 index 000000000..a21d06b6b --- /dev/null +++ b/src/Serializable.ts @@ -0,0 +1,261 @@ +/** + * @since 1.0.0 + * + * Serializable represents an object that has self-contained Schema(s) + */ +import type * as Effect from "effect/Effect" +import type * as Exit from "effect/Exit" +import { dual } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import * as Internal from "./internal/serializable.js" +import * as Parser from "./Parser.js" +import type * as ParseResult from "./ParseResult.js" +import * as Schema from "./Schema.js" + +/** + * @since 1.0.0 + * @category symbol + */ +export const symbol: unique symbol = Internal.symbol as any + +/** + * @since 1.0.0 + * @category model + */ +export interface Serializable { + readonly [symbol]: Schema.Schema +} + +/** + * @since 1.0.0 + * @category accessor + */ +export const selfSchema = (self: Serializable): Schema.Schema => self[symbol] + +/** + * @since 1.0.0 + * @category symbol + */ +export const symbolResult: unique symbol = Internal.symbolResult as any + +/** + * @since 1.0.0 + * @category model + */ +export interface WithResult { + readonly [symbolResult]: { + readonly Failure: Schema.Schema + readonly Success: Schema.Schema + } +} + +/** + * @since 1.0.0 + * @category accessor + */ +export const failureSchema = ( + self: WithResult +): Schema.Schema => self[symbolResult].Failure + +/** + * @since 1.0.0 + * @category accessor + */ +export const successSchema = ( + self: WithResult +): Schema.Schema => self[symbolResult].Success + +const exitSchemaCache = globalValue( + "@effect/schema/Serializable/exitSchemaCache", + () => new WeakMap>() +) + +/** + * @since 1.0.0 + * @category accessor + */ +export const exitSchema = ( + self: WithResult +): Schema.Schema, Exit.Exit> => { + const proto = Object.getPrototypeOf(self) + if (!(symbolResult in proto)) { + return Schema.exit(failureSchema(self), successSchema(self)) + } + let schema = exitSchemaCache.get(proto) + if (schema === undefined) { + schema = Schema.exit(failureSchema(self), successSchema(self)) + exitSchemaCache.set(proto, schema) + } + return schema +} + +/** + * @since 1.0.0 + * @category model + */ +export interface SerializableWithResult + extends Serializable, WithResult +{} + +/** + * @since 1.0.0 + * @category encoding + */ +export const serialize = ( + self: Serializable +): Effect.Effect => Parser.encode(self[symbol])(self as A) + +/** + * @since 1.0.0 + * @category decoding + */ +export const deserialize: { + ( + value: unknown + ): (self: Serializable) => Effect.Effect + (self: Serializable, value: unknown): Effect.Effect +} = dual< + (value: unknown) => ( + self: Serializable + ) => Effect.Effect, + ( + self: Serializable, + value: unknown + ) => Effect.Effect +>(2, (self, value) => Parser.parse(self[symbol])(value)) + +/** + * @since 1.0.0 + * @category encoding + */ +export const serializeFailure: { + ( + value: E + ): (self: WithResult) => Effect.Effect + ( + self: WithResult, + value: E + ): Effect.Effect +} = dual< + (value: E) => ( + self: WithResult + ) => Effect.Effect, + ( + self: WithResult, + value: E + ) => Effect.Effect +>(2, (self, value) => Parser.encode(self[symbolResult].Failure)(value)) + +/** + * @since 1.0.0 + * @category decoding + */ +export const deserializeFailure: { + (value: unknown): ( + self: WithResult + ) => Effect.Effect + ( + self: WithResult, + value: unknown + ): Effect.Effect +} = dual< + (value: unknown) => ( + self: WithResult + ) => Effect.Effect, + ( + self: WithResult, + value: unknown + ) => Effect.Effect +>(2, (self, value) => Parser.parse(self[symbolResult].Failure)(value)) + +/** + * @since 1.0.0 + * @category encoding + */ +export const serializeSuccess: { + ( + value: A + ): (self: WithResult) => Effect.Effect + ( + self: WithResult, + value: A + ): Effect.Effect +} = dual< + (value: A) => ( + self: WithResult + ) => Effect.Effect, + ( + self: WithResult, + value: A + ) => Effect.Effect +>(2, (self, value) => Parser.encode(self[symbolResult].Success)(value)) + +/** + * @since 1.0.0 + * @category decoding + */ +export const deserializeSuccess: { + ( + value: unknown + ): ( + self: WithResult + ) => Effect.Effect + ( + self: WithResult, + value: unknown + ): Effect.Effect +} = dual< + (value: unknown) => ( + self: WithResult + ) => Effect.Effect, + ( + self: WithResult, + value: unknown + ) => Effect.Effect +>(2, (self, value) => Parser.parse(self[symbolResult].Success)(value)) + +/** + * @since 1.0.0 + * @category encoding + */ +export const serializeExit: { + ( + value: Exit.Exit + ): ( + self: WithResult + ) => Effect.Effect> + ( + self: WithResult, + value: Exit.Exit + ): Effect.Effect> +} = dual< + (value: Exit.Exit) => ( + self: WithResult + ) => Effect.Effect>, + ( + self: WithResult, + value: Exit.Exit + ) => Effect.Effect> +>(2, (self, value) => Parser.encode(exitSchema(self))(value)) + +/** + * @since 1.0.0 + * @category decoding + */ +export const deserializeExit: { + (value: unknown): ( + self: WithResult + ) => Effect.Effect> + ( + self: WithResult, + value: unknown + ): Effect.Effect> +} = dual< + (value: unknown) => ( + self: WithResult + ) => Effect.Effect>, + ( + self: WithResult, + value: unknown + ) => Effect.Effect> +>(2, (self, value) => Parser.parse(exitSchema(self))(value)) diff --git a/src/index.ts b/src/index.ts index c84e44cfe..f6549c485 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,13 @@ export * as Pretty from "./Pretty.js" */ export * as Schema from "./Schema.js" +/** + * @since 1.0.0 + * + * Serializable represents an object that has self-contained Schema(s) + */ +export * as Serializable from "./Serializable.js" + /** * @since 1.0.0 */ diff --git a/src/internal/serializable.ts b/src/internal/serializable.ts new file mode 100644 index 000000000..eff1d9678 --- /dev/null +++ b/src/internal/serializable.ts @@ -0,0 +1,9 @@ +/** @internal */ +export const symbol: unique symbol = Symbol.for( + "@effect/schema/Serializable/symbol" +) + +/** @internal */ +export const symbolResult: unique symbol = Symbol.for( + "@effect/schema/Serializable/symbolResult" +) diff --git a/test/Cause/cause.test.ts b/test/Cause/cause.test.ts new file mode 100644 index 000000000..d68d3d590 --- /dev/null +++ b/test/Cause/cause.test.ts @@ -0,0 +1,134 @@ +import * as S from "@effect/schema/Schema" +import * as Util from "@effect/schema/test/util" +import { Cause, FiberId } from "effect" +import { assert, describe, it } from "vitest" + +describe("Cause/cause", () => { + it("property tests", () => { + Util.roundtrip(S.cause(S.NumberFromString, S.unknown)) + }) + + it("decoding", async () => { + const schema = S.cause(S.NumberFromString) + await Util.expectParseSuccess( + schema, + { _tag: "Fail", error: "1" }, + Cause.fail(1) + ) + await Util.expectParseSuccess( + schema, + { _tag: "Empty" }, + Cause.empty + ) + await Util.expectParseSuccess( + schema, + { + _tag: "Parallel", + left: { _tag: "Fail", error: "1" }, + right: { _tag: "Empty" } + }, + Cause.parallel(Cause.fail(1), Cause.empty) + ) + await Util.expectParseSuccess( + schema, + { + _tag: "Sequential", + left: { _tag: "Fail", error: "1" }, + right: { _tag: "Empty" } + }, + Cause.sequential(Cause.fail(1), Cause.empty) + ) + await Util.expectParseSuccess( + schema, + { + _tag: "Die", + defect: { stack: "fail", message: "error" } + }, + Cause.die({ stack: "fail", message: "error" }) + ) + await Util.expectParseSuccess( + schema, + { + _tag: "Interrupt", + fiberId: { + _tag: "Composite", + left: { + _tag: "Runtime", + id: 1, + startTimeMillis: 1000 + }, + right: { + _tag: "None" + } + } + }, + Cause.interrupt(FiberId.composite(FiberId.runtime(1, 1000), FiberId.none)) + ) + + await Util.expectParseFailure( + schema, + null, + `Expected , actual null` + ) + await Util.expectParseFailure( + schema, + {}, + `/_tag is missing` + ) + await Util.expectParseFailure( + schema, + { _tag: "Parallel", left: { _tag: "Fail" }, right: { _tag: "Interrupt" } }, + `union member: /left union member: /error is missing` + ) + }) + + it("encoding", async () => { + const schema = S.cause(S.NumberFromString) + const schemaUnknown = S.cause(S.NumberFromString, S.unknown) + + await Util.expectEncodeSuccess(schema, Cause.fail(1), { _tag: "Fail", error: "1" }) + await Util.expectEncodeSuccess(schema, Cause.empty, { _tag: "Empty" }) + await Util.expectEncodeSuccess(schema, Cause.parallel(Cause.fail(1), Cause.empty), { + _tag: "Parallel", + left: { _tag: "Fail", error: "1" }, + right: { _tag: "Empty" } + }) + await Util.expectEncodeSuccess(schema, Cause.sequential(Cause.fail(1), Cause.empty), { + _tag: "Sequential", + left: { _tag: "Fail", error: "1" }, + right: { _tag: "Empty" } + }) + await Util.expectEncodeSuccess(schema, Cause.die("fail"), { + _tag: "Die", + defect: "fail" + }) + await Util.expectEncodeSuccess( + schema, + Cause.interrupt(FiberId.composite(FiberId.runtime(1, 1000), FiberId.none)), + { + _tag: "Interrupt", + fiberId: { + _tag: "Composite", + left: { + _tag: "Runtime", + id: 1, + startTimeMillis: 1000 + }, + right: { + _tag: "None" + } + } + } + ) + + 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") + + 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") + }) +}) diff --git a/test/Cause/causeFromSelf.test.ts b/test/Cause/causeFromSelf.test.ts new file mode 100644 index 000000000..e428a1f01 --- /dev/null +++ b/test/Cause/causeFromSelf.test.ts @@ -0,0 +1,53 @@ +import * as Pretty from "@effect/schema/Pretty" +import * as S from "@effect/schema/Schema" +import * as Util from "@effect/schema/test/util" +import * as Cause from "effect/Cause" +import * as FiberId from "effect/FiberId" +import { describe, expect, it } from "vitest" + +describe("Cause/causeFromSelf", () => { + it("property tests", () => { + Util.roundtrip(S.causeFromSelf(S.NumberFromString)) + }) + + it("decoding", async () => { + const schema = S.causeFromSelf(S.NumberFromString) + + await Util.expectParseSuccess(schema, Cause.fail("1"), Cause.fail(1)) + + await Util.expectParseFailure(schema, null, `Expected Cause, actual null`) + await Util.expectParseFailure( + schema, + Cause.fail("a"), + `union member: /error Expected string <-> number, actual "a"` + ) + await Util.expectParseFailure( + schema, + Cause.parallel(Cause.die("error"), Cause.fail("a")), + `union member: /right union member: /error Expected string <-> number, actual "a"` + ) + }) + + it("encoding", async () => { + const schema = S.causeFromSelf(S.NumberFromString) + + await Util.expectEncodeSuccess(schema, Cause.fail(1), Cause.fail("1")) + }) + + it("pretty", () => { + const schema = S.causeFromSelf(S.string) + const pretty = Pretty.to(schema) + expect(pretty(Cause.die("error"))).toEqual(`Cause.die(Error: error)`) + expect(pretty(Cause.empty)).toEqual(`Cause.empty`) + expect(pretty(Cause.fail("error"))).toEqual(`Cause.fail("error")`) + expect(pretty(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.none)))).toEqual( + `Cause.interrupt(FiberId.composite(FiberId.none, FiberId.none))` + ) + expect(pretty(Cause.parallel(Cause.die("error"), Cause.fail("error")))).toEqual( + `Cause.parallel(Cause.die(Error: error), Cause.fail("error"))` + ) + expect(pretty(Cause.sequential(Cause.die("error"), Cause.fail("error")))).toEqual( + `Cause.sequential(Cause.die(Error: error), Cause.fail("error"))` + ) + }) +}) diff --git a/test/Either/eitherFromSelf.test.ts b/test/Either/eitherFromSelf.test.ts index 90409833c..b2bbcea45 100644 --- a/test/Either/eitherFromSelf.test.ts +++ b/test/Either/eitherFromSelf.test.ts @@ -29,7 +29,7 @@ describe("Either/eitherFromSelf", () => { await Util.expectParseSuccess(schema, E.right("1"), E.right(1)) }) - it("encoding", () => { + it("pretty", () => { const schema = S.eitherFromSelf(S.string, S.number) const pretty = Pretty.to(schema) expect(pretty(E.left("a"))).toEqual(`left("a")`) diff --git a/test/Exit/exit.test.ts b/test/Exit/exit.test.ts new file mode 100644 index 000000000..7b9d580a7 --- /dev/null +++ b/test/Exit/exit.test.ts @@ -0,0 +1,33 @@ +import * as S from "@effect/schema/Schema" +import * as Util from "@effect/schema/test/util" +import { Exit } from "effect" +import { describe, it } from "vitest" + +describe("Exit/exit", () => { + it("property tests", () => { + Util.roundtrip(S.exit(S.string, S.number)) + }) + + it("decoding", async () => { + const schema = S.exit(S.string, S.number) + await Util.expectParseSuccess( + schema, + { _tag: "Failure", cause: { _tag: "Fail", error: "error" } }, + Exit.fail("error") + ) + await Util.expectParseSuccess( + schema, + { _tag: "Success", value: 123 }, + Exit.succeed(123) + ) + }) + + it("encoding", async () => { + const schema = S.exit(S.string, S.number) + await Util.expectEncodeSuccess(schema, Exit.fail("error"), { + _tag: "Failure", + cause: { _tag: "Fail", error: "error" } + }) + await Util.expectEncodeSuccess(schema, Exit.succeed(123), { _tag: "Success", value: 123 }) + }) +}) diff --git a/test/FiberId/FiberId.test.ts b/test/FiberId/FiberId.test.ts new file mode 100644 index 000000000..0d0b9c36e --- /dev/null +++ b/test/FiberId/FiberId.test.ts @@ -0,0 +1,32 @@ +import * as S from "@effect/schema/Schema" +import * as Util from "@effect/schema/test/util" +import * as FiberId from "effect/FiberId" +import { describe, it } from "vitest" + +describe("FiberId", () => { + it("property tests", () => { + Util.roundtrip(S.FiberId) + }) + + it("decoding", async () => { + const schema = S.FiberId + + await Util.expectParseSuccess(schema, { _tag: "None" }, FiberId.none) + await Util.expectParseSuccess( + schema, + { _tag: "Runtime", id: 1, startTimeMillis: 100 }, + FiberId.runtime(1, 100) + ) + await Util.expectParseSuccess( + schema, + { _tag: "Composite", left: { _tag: "None" }, right: { _tag: "None" } }, + FiberId.composite(FiberId.none, FiberId.none) + ) + + await Util.expectParseFailure( + schema, + { _tag: "Composite", left: { _tag: "None" }, right: { _tag: "-" } }, + `union member: /right /_tag Expected "Composite" or "Runtime" or "None", actual "-"` + ) + }) +}) diff --git a/test/FiberId/FiberIdFromSelf.test.ts b/test/FiberId/FiberIdFromSelf.test.ts new file mode 100644 index 000000000..04a54e03c --- /dev/null +++ b/test/FiberId/FiberIdFromSelf.test.ts @@ -0,0 +1,31 @@ +import * as Pretty from "@effect/schema/Pretty" +import * as S from "@effect/schema/Schema" +import * as Util from "@effect/schema/test/util" +import * as FiberId from "effect/FiberId" +import { describe, expect, it } from "vitest" + +describe("FiberIdFromSelf", () => { + it("property tests", () => { + Util.roundtrip(S.FiberIdFromSelf) + }) + + it("decoding", async () => { + const schema = S.FiberIdFromSelf + + await Util.expectParseSuccess(schema, FiberId.none) + await Util.expectParseSuccess(schema, FiberId.runtime(1, 100)) + await Util.expectParseSuccess(schema, FiberId.composite(FiberId.none, FiberId.none)) + + await Util.expectParseFailure(schema, null, `Expected FiberId, actual null`) + }) + + it("pretty", () => { + const schema = S.FiberIdFromSelf + const pretty = Pretty.to(schema) + expect(pretty(FiberId.none)).toEqual(`FiberId.none`) + expect(pretty(FiberId.runtime(1, 100))).toEqual(`FiberId.runtime(1, 100)`) + expect(pretty(FiberId.composite(FiberId.none, FiberId.none))).toEqual( + `FiberId.composite(FiberId.none, FiberId.none)` + ) + }) +}) diff --git a/test/Schema/Class.test.ts b/test/Schema/Class.test.ts index f3cb633a1..958e8189f 100644 --- a/test/Schema/Class.test.ts +++ b/test/Schema/Class.test.ts @@ -1,8 +1,9 @@ import * as AST from "@effect/schema/AST" import * as PR from "@effect/schema/ParseResult" import * as S from "@effect/schema/Schema" +import * as Serializable from "@effect/schema/Serializable" import * as Util from "@effect/schema/test/util" -import { Effect } from "effect" +import { Effect, Exit } from "effect" import * as Data from "effect/Data" import * as Equal from "effect/Equal" import * as O from "effect/Option" @@ -258,27 +259,40 @@ describe("Schema/Class", () => { expect(req._tag).toEqual("MyRequest") expect(req.id).toEqual(1) expect(Request.isRequest(req)).toEqual(true) - - S.decodeSync(MyRequest.Success)(123) - S.decodeSync(MyRequest.Failure)("fail") }) - it("TaggedRequest assignable to TaggedRequest.Base", () => { - class MyRequest extends S.TaggedRequest()("MyRequest", S.string, S.number, { - id: S.number - }) {} - - const makeCache = < - EI, - EA, - AI, - AA, - I, - Req extends Request.Request & { readonly _tag: string } - >( - schema: S.TaggedRequest.Base - ) => schema - - makeCache(MyRequest) + it("TaggedRequest extends SerializableWithExit", () => { + class MyRequest + extends S.TaggedRequest()("MyRequest", S.string, S.NumberFromString, { + id: S.number + }) + {} + + const req = new MyRequest({ id: 1 }) + assert.deepStrictEqual( + Serializable.serialize(req).pipe(Effect.runSync), + { _tag: "MyRequest", id: 1 } + ) + assert(Equal.equals( + Serializable.deserialize(req, { _tag: "MyRequest", id: 1 }).pipe(Effect.runSync), + req + )) + assert.deepStrictEqual( + Serializable.serializeExit(req, Exit.fail("fail")).pipe(Effect.runSync), + { _tag: "Failure", cause: { _tag: "Fail", error: "fail" } } + ) + assert.deepStrictEqual( + Serializable.deserializeExit(req, { _tag: "Failure", cause: { _tag: "Fail", error: "fail" } }) + .pipe(Effect.runSync), + Exit.fail("fail") + ) + assert.deepStrictEqual( + Serializable.serializeExit(req, Exit.succeed(123)).pipe(Effect.runSync), + { _tag: "Success", value: "123" } + ) + assert.deepStrictEqual( + Serializable.deserializeExit(req, { _tag: "Success", value: "123" }).pipe(Effect.runSync), + Exit.succeed(123) + ) }) }) diff --git a/test/Serializable.test.ts b/test/Serializable.test.ts new file mode 100644 index 000000000..1b6dbf38c --- /dev/null +++ b/test/Serializable.test.ts @@ -0,0 +1,117 @@ +import * as S from "@effect/schema/Schema" +import * as Serializable from "@effect/schema/Serializable" +import { Effect, Exit } from "effect" +import { assert, describe, test } from "vitest" + +class Person extends S.Class()({ + id: S.number, + name: S.string.pipe(S.nonEmpty()) +}) {} + +class GetPersonById extends S.Class()({ + id: S.number +}) { + get [Serializable.symbol]() { + return GetPersonById + } + get [Serializable.symbolResult]() { + return { + Success: Person, + Failure: S.string + } as const + } +} + +describe("Serializable", () => { + test("serialize", () => { + const req = new GetPersonById({ id: 123 }) + assert.deepStrictEqual(Effect.runSync(Serializable.serialize(req)), { + id: 123 + }) + }) + + test("deserialize", () => { + const req = new GetPersonById({ id: 123 }) + assert.deepStrictEqual( + Effect.runSync(Serializable.deserialize(req, { + id: 456 + })), + new GetPersonById({ id: 456 }) + ) + }) + + test("serializeFailure", () => { + const req = new GetPersonById({ id: 123 }) + assert.deepStrictEqual( + Effect.runSync( + Serializable.serializeFailure(req, "fail") + ), + "fail" + ) + }) + + test("serializeSuccess", () => { + const req = new GetPersonById({ id: 123 }) + assert.deepStrictEqual( + Effect.runSync( + Serializable.serializeSuccess(req, new Person({ id: 123, name: "foo" })) + ), + { id: 123, name: "foo" } + ) + }) + + test("serializeExit", () => { + const req = new GetPersonById({ id: 123 }) + assert.deepStrictEqual( + Effect.runSync( + Serializable.serializeExit(req, Exit.succeed(new Person({ id: 123, name: "foo" }))) + ), + { _tag: "Success", value: { id: 123, name: "foo" } } + ) + assert.deepStrictEqual( + Effect.runSync( + Serializable.serializeExit(req, Exit.fail("fail")) + ), + { _tag: "Failure", cause: { _tag: "Fail", error: "fail" } } + ) + }) + + test("deserializeFailure", () => { + const req = new GetPersonById({ id: 123 }) + assert.deepStrictEqual( + Effect.runSync( + Serializable.deserializeFailure(req, "fail") + ), + "fail" + ) + }) + + test("deserializeSuccess", () => { + const req = new GetPersonById({ id: 123 }) + assert.deepStrictEqual( + Effect.runSync( + Serializable.deserializeSuccess(req, { id: 123, name: "foo" }) + ), + new Person({ id: 123, name: "foo" }) + ) + }) + + test("deserializeExit", () => { + const req = new GetPersonById({ id: 123 }) + assert.deepStrictEqual( + Effect.runSync( + Serializable.deserializeExit(req, { _tag: "Success", value: { id: 123, name: "foo" } }) + ), + Exit.succeed(new Person({ id: 123, name: "foo" })) + ) + assert.deepStrictEqual( + Effect.runSync( + Serializable.deserializeExit(req, { + _tag: "Failure", + cause: { _tag: "Fail", error: "fail" } + }) + ), + Exit.fail("fail") + ) + }) +}) diff --git a/test/util.ts b/test/util.ts index e5ff5b44d..0ad917a1b 100644 --- a/test/util.ts +++ b/test/util.ts @@ -350,3 +350,9 @@ export const rejects = async (promise: Promise) => { // ok } } + +export const sample = (schema: S.Schema, n: number) => { + const arbitrary = A.to(schema) + const arb = arbitrary(fc) + console.log(JSON.stringify(fc.sample(arb, n), null, 2)) +}