effect-app 4.0.0-beta.14 → 4.0.0-beta.140
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +534 -0
- package/dist/Array.d.ts +1 -1
- package/dist/Chunk.d.ts +1 -1
- package/dist/Chunk.d.ts.map +1 -1
- package/dist/Config/SecretURL.d.ts +1 -1
- package/dist/Config/SecretURL.d.ts.map +1 -1
- package/dist/Config/SecretURL.js +2 -2
- package/dist/Config/internal/configSecretURL.d.ts +1 -1
- package/dist/Config/internal/configSecretURL.d.ts.map +1 -1
- package/dist/Config.d.ts +7 -0
- package/dist/Config.d.ts.map +1 -0
- package/dist/Config.js +6 -0
- package/dist/ConfigProvider.d.ts +39 -0
- package/dist/ConfigProvider.d.ts.map +1 -0
- package/dist/ConfigProvider.js +42 -0
- package/dist/Context.d.ts +40 -0
- package/dist/Context.d.ts.map +1 -0
- package/dist/Context.js +67 -0
- package/dist/Effect.d.ts +9 -10
- package/dist/Effect.d.ts.map +1 -1
- package/dist/Effect.js +3 -6
- package/dist/Function.d.ts +1 -1
- package/dist/Function.d.ts.map +1 -1
- package/dist/Inputify.type.d.ts +1 -1
- package/dist/Layer.d.ts +6 -5
- package/dist/Layer.d.ts.map +1 -1
- package/dist/Layer.js +1 -1
- package/dist/NonEmptySet.d.ts +1 -1
- package/dist/NonEmptySet.d.ts.map +1 -1
- package/dist/Operations.d.ts +191 -38
- package/dist/Operations.d.ts.map +1 -1
- package/dist/Option.d.ts +1 -1
- package/dist/Option.d.ts.map +1 -1
- package/dist/Pure.d.ts +5 -5
- package/dist/Pure.d.ts.map +1 -1
- package/dist/Pure.js +13 -13
- package/dist/Schema/Class.d.ts +55 -14
- package/dist/Schema/Class.d.ts.map +1 -1
- package/dist/Schema/Class.js +130 -20
- package/dist/Schema/FastCheck.d.ts +1 -1
- package/dist/Schema/FastCheck.d.ts.map +1 -1
- package/dist/Schema/Methods.d.ts +1 -1
- package/dist/Schema/SpecialJsonSchema.d.ts +33 -0
- package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
- package/dist/Schema/SpecialJsonSchema.js +122 -0
- package/dist/Schema/SpecialOpenApi.d.ts +32 -0
- package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
- package/dist/Schema/SpecialOpenApi.js +123 -0
- package/dist/Schema/brand.d.ts +11 -2
- package/dist/Schema/brand.d.ts.map +1 -1
- package/dist/Schema/brand.js +1 -1
- package/dist/Schema/email.d.ts +1 -1
- package/dist/Schema/email.d.ts.map +1 -1
- package/dist/Schema/email.js +9 -4
- package/dist/Schema/ext.d.ts +113 -48
- package/dist/Schema/ext.d.ts.map +1 -1
- package/dist/Schema/ext.js +115 -53
- package/dist/Schema/moreStrings.d.ts +111 -11
- package/dist/Schema/moreStrings.d.ts.map +1 -1
- package/dist/Schema/moreStrings.js +19 -10
- package/dist/Schema/numbers.d.ts +127 -15
- package/dist/Schema/numbers.d.ts.map +1 -1
- package/dist/Schema/numbers.js +10 -9
- package/dist/Schema/phoneNumber.d.ts +1 -1
- package/dist/Schema/phoneNumber.d.ts.map +1 -1
- package/dist/Schema/phoneNumber.js +8 -3
- package/dist/Schema/schema.d.ts +1 -1
- package/dist/Schema/strings.d.ts +37 -5
- package/dist/Schema/strings.d.ts.map +1 -1
- package/dist/Schema/strings.js +1 -1
- package/dist/Schema.d.ts +75 -56
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +85 -64
- package/dist/Set.d.ts +1 -1
- package/dist/Set.d.ts.map +1 -1
- package/dist/TypeTest.d.ts +1 -1
- package/dist/Types.d.ts +1 -1
- package/dist/Widen.type.d.ts +1 -1
- package/dist/_ext/Array.d.ts +1 -1
- package/dist/_ext/Array.d.ts.map +1 -1
- package/dist/_ext/date.d.ts +1 -1
- package/dist/_ext/misc.d.ts +1 -1
- package/dist/_ext/ord.ext.d.ts +1 -1
- package/dist/_ext/ord.ext.d.ts.map +1 -1
- package/dist/builtin.d.ts +1 -1
- package/dist/builtin.d.ts.map +1 -1
- package/dist/client/apiClientFactory.d.ts +13 -29
- package/dist/client/apiClientFactory.d.ts.map +1 -1
- package/dist/client/apiClientFactory.js +16 -17
- package/dist/client/clientFor.d.ts +7 -6
- package/dist/client/clientFor.d.ts.map +1 -1
- package/dist/client/errors.d.ts +19 -10
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +35 -10
- package/dist/client/makeClient.d.ts +74 -29
- package/dist/client/makeClient.d.ts.map +1 -1
- package/dist/client/makeClient.js +49 -23
- package/dist/client.d.ts +1 -1
- package/dist/faker.d.ts +1 -1
- package/dist/faker.d.ts.map +1 -1
- package/dist/http/Request.d.ts +2 -2
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +5 -5
- package/dist/http/internal/lib.d.ts +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/ids.d.ts +3 -3
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +3 -2
- package/dist/index.d.ts +5 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -8
- package/dist/logger.d.ts +1 -1
- package/dist/middleware.d.ts +8 -8
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +8 -8
- package/dist/rpc/MiddlewareMaker.d.ts +5 -4
- package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
- package/dist/rpc/MiddlewareMaker.js +26 -27
- package/dist/rpc/RpcContextMap.d.ts +3 -3
- package/dist/rpc/RpcContextMap.d.ts.map +1 -1
- package/dist/rpc/RpcContextMap.js +4 -4
- package/dist/rpc/RpcMiddleware.d.ts +5 -4
- package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
- package/dist/rpc/RpcMiddleware.js +1 -1
- package/dist/rpc.d.ts +1 -2
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +1 -2
- package/dist/transform.d.ts +1 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +3 -3
- package/dist/utils/effectify.d.ts +1 -1
- package/dist/utils/extend.d.ts +1 -1
- package/dist/utils/extend.d.ts.map +1 -1
- package/dist/utils/gen.d.ts +2 -2
- package/dist/utils/gen.d.ts.map +1 -1
- package/dist/utils/logLevel.d.ts +2 -2
- package/dist/utils/logLevel.d.ts.map +1 -1
- package/dist/utils/logger.d.ts +3 -3
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +3 -3
- package/dist/utils.d.ts +13 -7
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +9 -3
- package/dist/validation/validators.d.ts +1 -1
- package/dist/validation/validators.d.ts.map +1 -1
- package/dist/validation.d.ts +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/eslint.config.mjs +1 -1
- package/package.json +35 -19
- package/src/Config/SecretURL.ts +2 -1
- package/src/Config.ts +14 -0
- package/src/ConfigProvider.ts +48 -0
- package/src/{ServiceMap.ts → Context.ts} +52 -59
- package/src/Effect.ts +12 -14
- package/src/Layer.ts +5 -4
- package/src/Pure.ts +17 -18
- package/src/Schema/Class.ts +177 -36
- package/src/Schema/SpecialJsonSchema.ts +137 -0
- package/src/Schema/SpecialOpenApi.ts +130 -0
- package/src/Schema/brand.ts +18 -3
- package/src/Schema/email.ts +10 -2
- package/src/Schema/ext.ts +196 -87
- package/src/Schema/moreStrings.ts +31 -17
- package/src/Schema/numbers.ts +14 -13
- package/src/Schema/phoneNumber.ts +8 -1
- package/src/Schema/strings.ts +4 -4
- package/src/Schema.ts +195 -104
- package/src/client/apiClientFactory.ts +104 -112
- package/src/client/clientFor.ts +6 -1
- package/src/client/errors.ts +42 -17
- package/src/client/makeClient.ts +150 -61
- package/src/http/Request.ts +7 -4
- package/src/ids.ts +2 -1
- package/src/index.ts +5 -10
- package/src/middleware.ts +7 -9
- package/src/rpc/MiddlewareMaker.ts +36 -47
- package/src/rpc/RpcContextMap.ts +6 -5
- package/src/rpc/RpcMiddleware.ts +5 -4
- package/src/rpc.ts +0 -1
- package/src/transform.ts +2 -2
- package/src/utils/gen.ts +1 -1
- package/src/utils/logger.ts +2 -2
- package/src/utils.ts +10 -7
- package/test/dist/moreStrings.test.d.ts.map +1 -0
- package/test/dist/rpc.test.d.ts.map +1 -1
- package/test/dist/secretURL.test.d.ts.map +1 -0
- package/test/dist/special.test.d.ts.map +1 -0
- package/test/moreStrings.test.ts +17 -0
- package/test/rpc.test.ts +30 -6
- package/test/schema.test.ts +517 -4
- package/test/secretURL.test.ts +157 -0
- package/test/special.test.ts +909 -0
- package/test/utils.test.ts +6 -6
- package/tsconfig.base.json +0 -1
- package/tsconfig.json +0 -1
- package/dist/ServiceMap.d.ts +0 -44
- package/dist/ServiceMap.d.ts.map +0 -1
- package/dist/ServiceMap.js +0 -91
package/src/utils.ts
CHANGED
|
@@ -269,7 +269,7 @@ export type EnforceNonEmptyRecord<R> = keyof R extends never ? never : R
|
|
|
269
269
|
export function intersect<AS extends unknown[] & { 0: unknown }>(
|
|
270
270
|
...as: AS
|
|
271
271
|
): UnionToIntersection<{ [k in keyof AS]: AS[k] }[number]> {
|
|
272
|
-
return as.reduce((a: any, b: any) => (
|
|
272
|
+
return as.reduce((a: any, b: any) => Object.assign(a, b), {}) as any
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
export type IsEqualTo<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
|
|
@@ -280,8 +280,7 @@ export type IsEqualTo<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
|
|
|
280
280
|
export const unifyIndex = Symbol()
|
|
281
281
|
export type unifyIndex = typeof unifyIndex
|
|
282
282
|
|
|
283
|
-
|
|
284
|
-
export interface UnifiableIndexed<X> {}
|
|
283
|
+
export interface UnifiableIndexed<_X> {}
|
|
285
284
|
export type UnifiableIndexedURI = keyof UnifiableIndexed<any>
|
|
286
285
|
|
|
287
286
|
export interface Unifiable<X> {
|
|
@@ -313,9 +312,7 @@ function decorateNew(
|
|
|
313
312
|
if (out.descriptor) {
|
|
314
313
|
out.descriptor = Object.assign({}, out.descriptor)
|
|
315
314
|
}
|
|
316
|
-
const actualDesc: PropertyDescriptor =
|
|
317
|
-
out.descriptor || /* istanbul ignore next */ out
|
|
318
|
-
)
|
|
315
|
+
const actualDesc: PropertyDescriptor = out.descriptor || /* istanbul ignore next */ out
|
|
319
316
|
|
|
320
317
|
const originalMethod = validateAndExtractMethodFromDescriptor(actualDesc)
|
|
321
318
|
const isStatic = inp.placement === "static"
|
|
@@ -684,7 +681,7 @@ export type OptPromise<T extends () => any> = (
|
|
|
684
681
|
) => Promise<ReturnType<T>> | ReturnType<T>
|
|
685
682
|
|
|
686
683
|
export function access<T extends string, T2>(t: Record<T, T2>) {
|
|
687
|
-
return (key: T) => t[key]
|
|
684
|
+
return (key: T) => t[key]
|
|
688
685
|
}
|
|
689
686
|
|
|
690
687
|
export function todayAtUTCNoon() {
|
|
@@ -755,6 +752,12 @@ type CopyOriginSelf<A, U> = Equals<{}, U> extends true
|
|
|
755
752
|
|
|
756
753
|
// just one input param: the convention is that the ctor takes an object
|
|
757
754
|
// containing the properties of the class (I can't put object there as type because of contravariance)
|
|
755
|
+
/**
|
|
756
|
+
* By design this does not return `Self` directly.
|
|
757
|
+
*
|
|
758
|
+
* The return type is computed from `Self` and the update payload so callers can
|
|
759
|
+
* expose an explicit structural return type that remains assignable to `Self`.
|
|
760
|
+
*/
|
|
758
761
|
export const copyOrigin = <Ctor extends new(_: any) => any>(ctor: Ctor) =>
|
|
759
762
|
dual<
|
|
760
763
|
{
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moreStrings.test.d.ts","sourceRoot":"","sources":["../moreStrings.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc.test.d.ts","sourceRoot":"","sources":["../rpc.test.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rpc.test.d.ts","sourceRoot":"","sources":["../rpc.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAErF,OAAO,EAAE,CAAC,EAAE,MAAM,iBAAiB,CAAA;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;AAE7C,qBAAa,iBAAkB,SAAQ,sBAIrC;CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAQD,kBAAkB;;;;YAClB,mBAAmB;;;;YACnB,mBAAmB;;;;YACnB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AANpB,qBAAa,KAAM,SAAQ,UAQzB;CAAG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretURL.test.d.ts","sourceRoot":"","sources":["../secretURL.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"special.test.d.ts","sourceRoot":"","sources":["../special.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { S } from "effect-app"
|
|
2
|
+
import * as fc from "fast-check"
|
|
3
|
+
import { urlAlphabet } from "nanoid"
|
|
4
|
+
import { test } from "vitest"
|
|
5
|
+
|
|
6
|
+
const nanoidAlphabet = new Set(urlAlphabet)
|
|
7
|
+
|
|
8
|
+
const isNanoId = (value: string) => value.length === 21 && Array.from(value).every((char) => nanoidAlphabet.has(char))
|
|
9
|
+
|
|
10
|
+
test("StringId arbitrary generates nanoid-shaped values", () => {
|
|
11
|
+
fc.assert(
|
|
12
|
+
fc.property(S.toArbitrary(S.StringId), (value) => {
|
|
13
|
+
expect(isNanoId(value)).toBe(true)
|
|
14
|
+
expect(S.is(S.StringId)(value)).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
)
|
|
17
|
+
})
|
package/test/rpc.test.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
1
2
|
import { makeRpcClient, NotLoggedInError, UnauthorizedError } from "../src/client.js"
|
|
3
|
+
import { ForceVoid } from "../src/client/makeClient.js"
|
|
2
4
|
import { S } from "../src/index.js"
|
|
3
5
|
import { RpcContextMap } from "../src/rpc.js"
|
|
4
6
|
|
|
@@ -8,16 +10,38 @@ export class RequestContextMap extends RpcContextMap.makeMap({
|
|
|
8
10
|
test: RpcContextMap.make()(S.Never)
|
|
9
11
|
}) {}
|
|
10
12
|
|
|
11
|
-
const {
|
|
13
|
+
const { TaggedRequestFor } = makeRpcClient(RequestContextMap)
|
|
14
|
+
const TaggedRequest = TaggedRequestFor("Test").Query
|
|
12
15
|
|
|
13
16
|
export class Stats extends TaggedRequest<Stats>()("Stats", {}, {
|
|
14
17
|
allowedRoles: ["manager"],
|
|
15
18
|
success: {
|
|
16
|
-
usersActive24Hours: S.
|
|
17
|
-
usersActiveLastWeek: S.
|
|
18
|
-
newUsersLast24Hours: S.
|
|
19
|
-
newUsersLastWeek: S.
|
|
19
|
+
usersActive24Hours: S.Finite,
|
|
20
|
+
usersActiveLastWeek: S.Finite,
|
|
21
|
+
newUsersLast24Hours: S.Finite,
|
|
22
|
+
newUsersLastWeek: S.Finite
|
|
20
23
|
}
|
|
21
24
|
}) {}
|
|
22
25
|
|
|
23
|
-
declare const _stats: typeof Stats.
|
|
26
|
+
declare const _stats: typeof Stats.Type
|
|
27
|
+
declare const _statsSuccess: typeof Stats.success.Type
|
|
28
|
+
declare const _statsError: typeof Stats.error.Type
|
|
29
|
+
declare const _statsRequestType: typeof Stats.type
|
|
30
|
+
|
|
31
|
+
test("ForceVoid decodes and encodes as void", () => {
|
|
32
|
+
expect(S.decodeUnknownSync(ForceVoid)(undefined)).toBe(undefined)
|
|
33
|
+
expect(S.is(ForceVoid)(undefined)).toBe(true)
|
|
34
|
+
expect(S.decodeUnknownSync(ForceVoid)("test")).toBe(undefined)
|
|
35
|
+
expect(S.is(ForceVoid)("test")).toBe(true)
|
|
36
|
+
expect(S.encodeUnknownSync(ForceVoid)("test")).toBe(undefined)
|
|
37
|
+
expect(S.encodeUnknownSync(S.toCodecJson(ForceVoid))("test")).toBe(null)
|
|
38
|
+
expectTypeOf<typeof _stats>().toEqualTypeOf<Stats>()
|
|
39
|
+
expectTypeOf<typeof _statsSuccess>().toEqualTypeOf<{
|
|
40
|
+
readonly usersActive24Hours: number
|
|
41
|
+
readonly usersActiveLastWeek: number
|
|
42
|
+
readonly newUsersLast24Hours: number
|
|
43
|
+
readonly newUsersLastWeek: number
|
|
44
|
+
}>()
|
|
45
|
+
expectTypeOf<typeof _statsError>().toEqualTypeOf<NotLoggedInError | UnauthorizedError>()
|
|
46
|
+
expectTypeOf<typeof _statsRequestType>().toEqualTypeOf<"query">()
|
|
47
|
+
})
|
package/test/schema.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// import { generateFromArbitrary } from "@effect-app/infra/test"
|
|
2
2
|
import { Array, S } from "effect-app"
|
|
3
|
-
import {
|
|
3
|
+
import { specialJsonSchemaDocument } from "effect-app/Schema/SpecialJsonSchema"
|
|
4
|
+
import { describe, expect, expectTypeOf, test } from "vitest"
|
|
4
5
|
|
|
5
6
|
const A = S.Struct({ a: S.NonEmptyString255, email: S.NullOr(S.Email) })
|
|
6
7
|
test("works", () => {
|
|
@@ -14,12 +15,524 @@ test("works", () => {
|
|
|
14
15
|
})
|
|
15
16
|
|
|
16
17
|
test("literal default works", () => {
|
|
17
|
-
const l = S.
|
|
18
|
+
const l = S.Literals(["a", "b"])
|
|
18
19
|
expect(l.Default).toBe("a")
|
|
20
|
+
expectTypeOf(l.Default).toEqualTypeOf<"a">()
|
|
19
21
|
const s = S.Struct({ l: l.withDefault })
|
|
20
|
-
expect(s.
|
|
22
|
+
expect(s.make({}).l).toBe("a")
|
|
21
23
|
|
|
22
24
|
const l2 = l.changeDefault("b")
|
|
23
25
|
const s2 = S.Struct({ l: l2.withDefault })
|
|
24
|
-
expect(s2.
|
|
26
|
+
expect(s2.make({}).l).toBe("b")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test("NonEmptyString255.Type uses the named brand alias", () => {
|
|
30
|
+
type A = typeof S.NonEmptyString255.Type
|
|
31
|
+
type B = string & S.NonEmptyString255Brand
|
|
32
|
+
expectTypeOf<A>().toEqualTypeOf<B>()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test("S.Literals([\"A\", \"B\"]).Default is typed as \"A\"", () => {
|
|
36
|
+
const l = S.Literals(["A", "B"])
|
|
37
|
+
expect(l.Default).toBe("A")
|
|
38
|
+
expectTypeOf(l.Default).toEqualTypeOf<"A">()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("tagged union derives tag map and tags from v4 literal ast", () => {
|
|
42
|
+
const schema = S.TaggedUnion(
|
|
43
|
+
S.TaggedStruct("A", { a: S.String }),
|
|
44
|
+
S.TaggedStruct("B", { b: S.Finite }),
|
|
45
|
+
S.TaggedStruct("C", { c: S.Boolean })
|
|
46
|
+
)
|
|
47
|
+
const caseA = schema.cases["A"]
|
|
48
|
+
const caseB = schema.cases["B"]
|
|
49
|
+
const caseC = schema.cases["C"]
|
|
50
|
+
const isAOrB = schema.isAnyOf(["A", "B"])
|
|
51
|
+
|
|
52
|
+
expect(caseA.fields._tag.ast.literal).toBe("A")
|
|
53
|
+
expect(caseB.fields._tag.ast.literal).toBe("B")
|
|
54
|
+
expect(caseC.fields._tag.ast.literal).toBe("C")
|
|
55
|
+
expect(S.decodeSync(schema.tags)("A")).toBe("A")
|
|
56
|
+
expect(S.decodeSync(schema.tags)("B")).toBe("B")
|
|
57
|
+
expect(S.decodeSync(schema.tags)("C")).toBe("C")
|
|
58
|
+
expect(() => S.decodeUnknownSync(schema.tags)("D")).toThrow()
|
|
59
|
+
|
|
60
|
+
expect(schema.guards.A({ _tag: "A", a: "ok" })).toBe(true)
|
|
61
|
+
expect(schema.guards.A({ _tag: "B", b: 1 })).toBe(false)
|
|
62
|
+
expect(schema.guards.B({ _tag: "B", b: 1 })).toBe(true)
|
|
63
|
+
expect(schema.guards.B({ _tag: "A", a: "ok" })).toBe(false)
|
|
64
|
+
expect(schema.guards.C({ _tag: "C", c: true })).toBe(true)
|
|
65
|
+
expect(schema.guards.C({ _tag: "A", a: "ok" })).toBe(false)
|
|
66
|
+
|
|
67
|
+
expect(isAOrB({ _tag: "A", a: "ok" })).toBe(true)
|
|
68
|
+
expect(isAOrB({ _tag: "B", b: 1 })).toBe(true)
|
|
69
|
+
expect(isAOrB({ _tag: "C", c: true })).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("TaggedUnion tags returns a Literals schema with correct literal values", () => {
|
|
73
|
+
const schema = S.TaggedUnion(
|
|
74
|
+
S.TaggedStruct("X", { x: S.String }),
|
|
75
|
+
S.TaggedStruct("Y", { y: S.Finite })
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(schema.tags.literals).toEqual(["X", "Y"])
|
|
79
|
+
expectTypeOf(schema.tags.literals).toMatchTypeOf<readonly ["X", "Y"]>()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("TaggedUnion tags.pick returns a subset of the tag literals", () => {
|
|
83
|
+
const schema = S.TaggedUnion(
|
|
84
|
+
S.TaggedStruct("A", { a: S.String }),
|
|
85
|
+
S.TaggedStruct("B", { b: S.Finite }),
|
|
86
|
+
S.TaggedStruct("C", { c: S.Boolean })
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const subset = schema.tags.pick(["A", "C"])
|
|
90
|
+
expect(subset.literals).toEqual(["A", "C"])
|
|
91
|
+
expect(S.decodeSync(subset)("A")).toBe("A")
|
|
92
|
+
expect(S.decodeSync(subset)("C")).toBe("C")
|
|
93
|
+
expect(() => S.decodeUnknownSync(subset)("B")).toThrow()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("tags standalone function extracts tags from member schemas", () => {
|
|
97
|
+
const members = [
|
|
98
|
+
S.TaggedStruct("Foo", { foo: S.String }),
|
|
99
|
+
S.TaggedStruct("Bar", { bar: S.Finite })
|
|
100
|
+
] as const
|
|
101
|
+
|
|
102
|
+
const tagSchema = S.tags(members)
|
|
103
|
+
expect(tagSchema.literals).toEqual(["Foo", "Bar"])
|
|
104
|
+
expect(S.decodeSync(tagSchema)("Foo")).toBe("Foo")
|
|
105
|
+
expect(S.decodeSync(tagSchema)("Bar")).toBe("Bar")
|
|
106
|
+
expect(() => S.decodeUnknownSync(tagSchema)("Baz")).toThrow()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test("ExtendTaggedUnion adds tags to an existing Union", () => {
|
|
110
|
+
const union = S.Union([
|
|
111
|
+
S.TaggedStruct("P", { p: S.String }),
|
|
112
|
+
S.TaggedStruct("Q", { q: S.Finite })
|
|
113
|
+
])
|
|
114
|
+
const extended = S.ExtendTaggedUnion(union)
|
|
115
|
+
|
|
116
|
+
expect(extended.tags.literals).toEqual(["P", "Q"])
|
|
117
|
+
expect(S.decodeSync(extended.tags)("P")).toBe("P")
|
|
118
|
+
expect(S.decodeSync(extended.tags)("Q")).toBe("Q")
|
|
119
|
+
expect(() => S.decodeUnknownSync(extended.tags)("R")).toThrow()
|
|
120
|
+
expect(extended.cases["P"].fields._tag.ast.literal).toBe("P")
|
|
121
|
+
expect(extended.guards.P({ _tag: "P", p: "ok" })).toBe(true)
|
|
122
|
+
expect(extended.guards.P({ _tag: "Q", q: 1 })).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("TaggedUnion match dispatches on _tag", () => {
|
|
126
|
+
const schema = S.TaggedUnion(
|
|
127
|
+
S.TaggedStruct("A", { a: S.String }),
|
|
128
|
+
S.TaggedStruct("B", { b: S.Finite })
|
|
129
|
+
)
|
|
130
|
+
type T = S.Schema.Type<typeof schema>
|
|
131
|
+
|
|
132
|
+
const matcher = schema.match({
|
|
133
|
+
A: (v) => `got A: ${v.a}`,
|
|
134
|
+
B: (v) => `got B: ${v.b}`
|
|
135
|
+
})
|
|
136
|
+
expect(matcher({ _tag: "A", a: "hello" } as T)).toBe("got A: hello")
|
|
137
|
+
expect(matcher({ _tag: "B", b: 42 } as T)).toBe("got B: 42")
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test("TaggedUnion with single member", () => {
|
|
141
|
+
const schema = S.TaggedUnion(
|
|
142
|
+
S.TaggedStruct("Only", { val: S.String })
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
expect(schema.tags.literals).toEqual(["Only"])
|
|
146
|
+
expect(S.decodeSync(schema.tags)("Only")).toBe("Only")
|
|
147
|
+
expect(() => S.decodeUnknownSync(schema.tags)("Other")).toThrow()
|
|
148
|
+
expect(schema.guards.Only({ _tag: "Only", val: "x" })).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("TaggedUnion tags type is narrowed to the exact tag literals", () => {
|
|
152
|
+
const schema = S.TaggedUnion(
|
|
153
|
+
S.TaggedStruct("Alpha", { a: S.String }),
|
|
154
|
+
S.TaggedStruct("Beta", { b: S.Finite }),
|
|
155
|
+
S.TaggedStruct("Gamma", { c: S.Boolean })
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
type Tags = S.Schema.Type<typeof schema.tags>
|
|
159
|
+
expectTypeOf<Tags>().toEqualTypeOf<"Alpha" | "Beta" | "Gamma">()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test("TaggedUnion with encodeKeys renaming a non-tag key", () => {
|
|
163
|
+
const MemberA = S.TaggedStruct("A", { firstName: S.String }).pipe(
|
|
164
|
+
S.encodeKeys({ firstName: "first_name" })
|
|
165
|
+
)
|
|
166
|
+
const MemberB = S.TaggedStruct("B", { lastName: S.Finite }).pipe(
|
|
167
|
+
S.encodeKeys({ lastName: "last_name" })
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const schema = S.TaggedUnion(MemberA, MemberB)
|
|
171
|
+
|
|
172
|
+
expect(schema.tags.literals).toEqual(["A", "B"])
|
|
173
|
+
expect(S.decodeSync(schema.tags)("A")).toBe("A")
|
|
174
|
+
expect(S.decodeSync(schema.tags)("B")).toBe("B")
|
|
175
|
+
|
|
176
|
+
// decode from encoded (snake_case) to decoded (camelCase)
|
|
177
|
+
const decoded = S.decodeUnknownSync(schema)({ _tag: "A", first_name: "Alice" })
|
|
178
|
+
expect(decoded).toEqual({ _tag: "A", firstName: "Alice" })
|
|
179
|
+
|
|
180
|
+
const decoded2 = S.decodeUnknownSync(schema)({ _tag: "B", last_name: 42 })
|
|
181
|
+
expect(decoded2).toEqual({ _tag: "B", lastName: 42 })
|
|
182
|
+
|
|
183
|
+
// encode back to snake_case
|
|
184
|
+
type T = S.Schema.Type<typeof schema>
|
|
185
|
+
const encoded = S.encodeSync(schema)({ _tag: "A", firstName: "Alice" } as T)
|
|
186
|
+
expect(encoded).toEqual({ _tag: "A", first_name: "Alice" })
|
|
187
|
+
|
|
188
|
+
// guards work on decoded values
|
|
189
|
+
expect(schema.guards.A({ _tag: "A", firstName: "Alice" })).toBe(true)
|
|
190
|
+
expect(schema.guards.A({ _tag: "B", lastName: 42 })).toBe(false)
|
|
191
|
+
expect(schema.guards.B({ _tag: "B", lastName: 42 })).toBe(true)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test("TaggedUnion with TaggedClass members", () => {
|
|
195
|
+
class Foo extends S.TaggedClass<Foo>()("Foo", { name: S.String }) {}
|
|
196
|
+
class Bar extends S.TaggedClass<Bar>()("Bar", { count: S.Finite }) {}
|
|
197
|
+
|
|
198
|
+
const schema = S.TaggedUnion(Foo, Bar)
|
|
199
|
+
|
|
200
|
+
expect(schema.tags.literals).toEqual(["Foo", "Bar"])
|
|
201
|
+
expect(S.decodeSync(schema.tags)("Foo")).toBe("Foo")
|
|
202
|
+
expect(S.decodeSync(schema.tags)("Bar")).toBe("Bar")
|
|
203
|
+
expect(() => S.decodeUnknownSync(schema.tags)("Baz")).toThrow()
|
|
204
|
+
|
|
205
|
+
const decoded = S.decodeUnknownSync(schema)({ _tag: "Foo", name: "Alice" })
|
|
206
|
+
expect(decoded).toBeInstanceOf(Foo)
|
|
207
|
+
expect(decoded).toEqual(new Foo({ name: "Alice" }))
|
|
208
|
+
|
|
209
|
+
const decoded2 = S.decodeUnknownSync(schema)({ _tag: "Bar", count: 3 })
|
|
210
|
+
expect(decoded2).toBeInstanceOf(Bar)
|
|
211
|
+
expect(decoded2).toEqual(new Bar({ count: 3 }))
|
|
212
|
+
|
|
213
|
+
expect(schema.guards.Foo(new Foo({ name: "Alice" }))).toBe(true)
|
|
214
|
+
expect(schema.guards.Foo(new Bar({ count: 3 }))).toBe(false)
|
|
215
|
+
expect(schema.guards.Bar(new Bar({ count: 3 }))).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
describe("ReadonlySetFromArray", () => {
|
|
219
|
+
test("decodes an array of strings to a Set", () => {
|
|
220
|
+
const schema = S.ReadonlySetFromArray(S.String)
|
|
221
|
+
const decoded = S.decodeUnknownSync(schema)(["a", "b", "c"])
|
|
222
|
+
expect(decoded).toEqual(new Set(["a", "b", "c"]))
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test("encodes a Set back to an array", () => {
|
|
226
|
+
const schema = S.ReadonlySetFromArray(S.String)
|
|
227
|
+
const encoded = S.encodeSync(schema)(new Set(["a", "b"]))
|
|
228
|
+
expect(encoded).toEqual(["a", "b"])
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test("decodes with NumberFromString as value", () => {
|
|
232
|
+
const schema = S.ReadonlySetFromArray(S.NumberFromString)
|
|
233
|
+
const decoded = S.decodeUnknownSync(schema)(["1", "2", "3"])
|
|
234
|
+
expect(decoded).toEqual(new Set([1, 2, 3]))
|
|
235
|
+
expectTypeOf(decoded).toEqualTypeOf<ReadonlySet<number>>()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test("encodes with NumberFromString as value", () => {
|
|
239
|
+
const schema = S.ReadonlySetFromArray(S.NumberFromString)
|
|
240
|
+
const encoded = S.encodeSync(schema)(new Set([1, 2, 3]))
|
|
241
|
+
expect(encoded).toEqual(["1", "2", "3"])
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test("rejects invalid input", () => {
|
|
245
|
+
const schema = S.ReadonlySetFromArray(S.NumberFromString)
|
|
246
|
+
expect(() => S.decodeUnknownSync(schema)([1, 2])).toThrow()
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe("ReadonlyMapFromArray", () => {
|
|
251
|
+
test("decodes an array of tuples to a Map", () => {
|
|
252
|
+
const schema = S.ReadonlyMap({ key: S.String, value: S.Finite })
|
|
253
|
+
const decoded = S.decodeUnknownSync(schema)([["a", 1], ["b", 2]])
|
|
254
|
+
expect(decoded).toEqual(new Map([["a", 1], ["b", 2]]))
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test("encodes a Map back to an array of tuples", () => {
|
|
258
|
+
const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.Finite })
|
|
259
|
+
const encoded = S.encodeSync(schema)(new Map([["a", 1], ["b", 2]]))
|
|
260
|
+
expect(encoded).toEqual([["a", 1], ["b", 2]])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test("decodes with NumberFromString as key", () => {
|
|
264
|
+
const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
|
|
265
|
+
const decoded = S.decodeUnknownSync(schema)([["1", "one"], ["2", "two"]])
|
|
266
|
+
expect(decoded).toEqual(new Map([[1, "one"], [2, "two"]]))
|
|
267
|
+
expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<number, string>>()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test("encodes with NumberFromString as key", () => {
|
|
271
|
+
const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
|
|
272
|
+
const encoded = S.encodeSync(schema)(new Map([[1, "one"], [2, "two"]]))
|
|
273
|
+
expect(encoded).toEqual([["1", "one"], ["2", "two"]])
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test("decodes with NumberFromString as value", () => {
|
|
277
|
+
const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.NumberFromString })
|
|
278
|
+
const decoded = S.decodeUnknownSync(schema)([["a", "10"], ["b", "20"]])
|
|
279
|
+
expect(decoded).toEqual(new Map([["a", 10], ["b", 20]]))
|
|
280
|
+
expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<string, number>>()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test("encodes with NumberFromString as value", () => {
|
|
284
|
+
const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.NumberFromString })
|
|
285
|
+
const encoded = S.encodeSync(schema)(new Map([["a", 10], ["b", 20]]))
|
|
286
|
+
expect(encoded).toEqual([["a", "10"], ["b", "20"]])
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test("decodes with NumberFromString as both key and value", () => {
|
|
290
|
+
const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.NumberFromString })
|
|
291
|
+
const decoded = S.decodeUnknownSync(schema)([["1", "10"], ["2", "20"]])
|
|
292
|
+
expect(decoded).toEqual(new Map([[1, 10], [2, 20]]))
|
|
293
|
+
expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<number, number>>()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test("rejects invalid input", () => {
|
|
297
|
+
const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
|
|
298
|
+
expect(() => S.decodeUnknownSync(schema)([[1, "val"]])).toThrow()
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
describe("ReadonlySet (with withDefault)", () => {
|
|
303
|
+
test("make provides withDefault", () => {
|
|
304
|
+
const schema = S.ReadonlySet(S.NumberFromString)
|
|
305
|
+
const struct = S.Struct({ items: schema.withDefault })
|
|
306
|
+
const made = struct.make({})
|
|
307
|
+
expect(made.items).toEqual(new Set())
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test("decodes array with NumberFromString values", () => {
|
|
311
|
+
const schema = S.ReadonlySet(S.NumberFromString)
|
|
312
|
+
const decoded = S.decodeUnknownSync(schema)(["1", "2"])
|
|
313
|
+
expect(decoded).toEqual(new Set([1, 2]))
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
describe("ReadonlyMap (with withDefault)", () => {
|
|
318
|
+
test("make provides withDefault", () => {
|
|
319
|
+
const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String })
|
|
320
|
+
const struct = S.Struct({ items: schema.withDefault })
|
|
321
|
+
const made = struct.make({})
|
|
322
|
+
expect(made.items).toEqual(new Map())
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
test("decodes array of tuples with NumberFromString keys", () => {
|
|
326
|
+
const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String })
|
|
327
|
+
const decoded = S.decodeUnknownSync(schema)([["1", "one"]])
|
|
328
|
+
expect(decoded).toEqual(new Map([[1, "one"]]))
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
describe("JSON Schema", () => {
|
|
333
|
+
test("Email has format, minLength, maxLength", () => {
|
|
334
|
+
const doc = S.toJsonSchemaDocument(S.Email)
|
|
335
|
+
expect(doc).toStrictEqual({
|
|
336
|
+
dialect: "draft-2020-12",
|
|
337
|
+
schema: { "$ref": "#/$defs/Email" },
|
|
338
|
+
definitions: {
|
|
339
|
+
Email: {
|
|
340
|
+
type: "string",
|
|
341
|
+
title: "Email",
|
|
342
|
+
description: "an email according to RFC 5322",
|
|
343
|
+
format: "email",
|
|
344
|
+
allOf: [
|
|
345
|
+
{ minLength: 3 },
|
|
346
|
+
{ maxLength: 998 }
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test("Email specialJsonSchemaDocument flattens allOf", () => {
|
|
354
|
+
const doc = specialJsonSchemaDocument(S.Email)
|
|
355
|
+
expect(doc).toStrictEqual({
|
|
356
|
+
dialect: "draft-2020-12",
|
|
357
|
+
schema: { "$ref": "#/$defs/Email" },
|
|
358
|
+
definitions: {
|
|
359
|
+
Email: {
|
|
360
|
+
type: "string",
|
|
361
|
+
title: "Email",
|
|
362
|
+
description: "an email according to RFC 5322",
|
|
363
|
+
format: "email",
|
|
364
|
+
minLength: 3,
|
|
365
|
+
maxLength: 998
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
test("Date has format date-time and description", () => {
|
|
372
|
+
const doc = S.toJsonSchemaDocument(S.Date)
|
|
373
|
+
expect(doc).toStrictEqual({
|
|
374
|
+
dialect: "draft-2020-12",
|
|
375
|
+
schema: { "$ref": "#/$defs/Date" },
|
|
376
|
+
definitions: {
|
|
377
|
+
Date: {
|
|
378
|
+
type: "string",
|
|
379
|
+
description: "a string in ISO 8601 format that will be decoded as a Date",
|
|
380
|
+
format: "date-time"
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
test("DateValid has format date-time", () => {
|
|
387
|
+
const doc = S.toJsonSchemaDocument(S.DateValid)
|
|
388
|
+
expect(doc).toStrictEqual({
|
|
389
|
+
dialect: "draft-2020-12",
|
|
390
|
+
schema: { "$ref": "#/$defs/Date" },
|
|
391
|
+
definitions: {
|
|
392
|
+
Date: {
|
|
393
|
+
type: "string",
|
|
394
|
+
description: "a string in ISO 8601 format that will be decoded as a Date",
|
|
395
|
+
format: "date-time"
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test("PhoneNumber has format phone", () => {
|
|
402
|
+
const doc = specialJsonSchemaDocument(S.PhoneNumber)
|
|
403
|
+
expect(doc).toStrictEqual({
|
|
404
|
+
dialect: "draft-2020-12",
|
|
405
|
+
schema: { "$ref": "#/$defs/PhoneNumber" },
|
|
406
|
+
definitions: {
|
|
407
|
+
PhoneNumber: {
|
|
408
|
+
type: "string",
|
|
409
|
+
title: "PhoneNumber",
|
|
410
|
+
description: "a phone number with at least 7 digits",
|
|
411
|
+
format: "phone"
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test("Url has format uri", () => {
|
|
418
|
+
const doc = specialJsonSchemaDocument(S.Url)
|
|
419
|
+
expect(doc).toStrictEqual({
|
|
420
|
+
dialect: "draft-2020-12",
|
|
421
|
+
schema: { "$ref": "#/$defs/Url" },
|
|
422
|
+
definitions: {
|
|
423
|
+
Url: {
|
|
424
|
+
type: "string",
|
|
425
|
+
title: "Url",
|
|
426
|
+
format: "uri"
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
describe("generateGuards", () => {
|
|
434
|
+
const StateSchema = S.TaggedUnion(
|
|
435
|
+
S.TaggedStruct("Active", { since: S.String }),
|
|
436
|
+
S.TaggedStruct("Inactive", { reason: S.String }),
|
|
437
|
+
S.TaggedStruct("Pending", { eta: S.Finite })
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
type State = S.Schema.Type<typeof StateSchema>
|
|
441
|
+
type Entity = { readonly state: State; readonly name: string }
|
|
442
|
+
|
|
443
|
+
const { isActive, isAnyOf, isInactive, isPending } = StateSchema.generateGuards("state")
|
|
444
|
+
|
|
445
|
+
test("isActive narrows to Active member", () => {
|
|
446
|
+
const entity: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
|
|
447
|
+
expect(isActive(entity)).toBe(true)
|
|
448
|
+
if (isActive(entity)) {
|
|
449
|
+
expectTypeOf(entity.state).toEqualTypeOf<{ readonly _tag: "Active"; readonly since: string }>()
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
test("isActive returns false for non-Active", () => {
|
|
454
|
+
const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" }
|
|
455
|
+
expect(isActive(entity)).toBe(false)
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
test("isInactive narrows to Inactive member", () => {
|
|
459
|
+
const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" }
|
|
460
|
+
expect(isInactive(entity)).toBe(true)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
test("isPending narrows to Pending member", () => {
|
|
464
|
+
const entity: Entity = { state: { _tag: "Pending", eta: 42 }, name: "foo" }
|
|
465
|
+
expect(isPending(entity)).toBe(true)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test("isAnyOf narrows to union of specified members", () => {
|
|
469
|
+
const isActiveOrPending = isAnyOf(["Active", "Pending"])
|
|
470
|
+
const active: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
|
|
471
|
+
const pending: Entity = { state: { _tag: "Pending", eta: 5 }, name: "bar" }
|
|
472
|
+
const inactive: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "baz" }
|
|
473
|
+
|
|
474
|
+
expect(isActiveOrPending(active)).toBe(true)
|
|
475
|
+
expect(isActiveOrPending(pending)).toBe(true)
|
|
476
|
+
expect(isActiveOrPending(inactive)).toBe(false)
|
|
477
|
+
|
|
478
|
+
if (isActiveOrPending(active)) {
|
|
479
|
+
expectTypeOf(active.state).toEqualTypeOf<
|
|
480
|
+
{ readonly _tag: "Active"; readonly since: string } | { readonly _tag: "Pending"; readonly eta: number }
|
|
481
|
+
>()
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
test("guards use schema-based validation (built-in guards)", () => {
|
|
486
|
+
expect(StateSchema.guards.Active({ _tag: "Active" })).toBe(false)
|
|
487
|
+
expect(StateSchema.guards.Active({ _tag: "Active", since: "2024-01-01" })).toBe(true)
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
describe("generateGuardsFor", () => {
|
|
492
|
+
const StateSchema = S.TaggedUnion(
|
|
493
|
+
S.TaggedStruct("Active", { since: S.String }),
|
|
494
|
+
S.TaggedStruct("Inactive", { reason: S.String }),
|
|
495
|
+
S.TaggedStruct("Pending", { eta: S.Finite })
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
type State = S.Schema.Type<typeof StateSchema>
|
|
499
|
+
type Entity = { readonly state: State; readonly name: string }
|
|
500
|
+
|
|
501
|
+
const { isActive, isAnyOf } = StateSchema.generateGuardsFor<Entity>()("state")
|
|
502
|
+
|
|
503
|
+
test("isActive narrows to Active member", () => {
|
|
504
|
+
const entity: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
|
|
505
|
+
expect(isActive(entity)).toBe(true)
|
|
506
|
+
if (isActive(entity)) {
|
|
507
|
+
expectTypeOf(entity.state).toEqualTypeOf<{ readonly _tag: "Active"; readonly since: string }>()
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
test("isActive returns false for non-Active", () => {
|
|
512
|
+
const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" }
|
|
513
|
+
expect(isActive(entity)).toBe(false)
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
test("isAnyOf narrows to union of specified members", () => {
|
|
517
|
+
const isActiveOrPending = isAnyOf(["Active", "Pending"])
|
|
518
|
+
const active: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
|
|
519
|
+
const inactive: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "baz" }
|
|
520
|
+
|
|
521
|
+
expect(isActiveOrPending(active)).toBe(true)
|
|
522
|
+
expect(isActiveOrPending(inactive)).toBe(false)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
test("ExtendTaggedUnion also exposes generateGuardsFor", () => {
|
|
526
|
+
const union = S.Union([
|
|
527
|
+
S.TaggedStruct("X", { x: S.String }),
|
|
528
|
+
S.TaggedStruct("Y", { y: S.Finite })
|
|
529
|
+
])
|
|
530
|
+
const extended = S.ExtendTaggedUnion(union)
|
|
531
|
+
type Obj = { readonly field: S.Schema.Type<typeof extended> }
|
|
532
|
+
const { isX, isY } = extended.generateGuardsFor<Obj>()("field")
|
|
533
|
+
|
|
534
|
+
expect(isX({ field: { _tag: "X", x: "hi" } })).toBe(true)
|
|
535
|
+
expect(isX({ field: { _tag: "Y", y: 1 } })).toBe(false)
|
|
536
|
+
expect(isY({ field: { _tag: "Y", y: 1 } })).toBe(true)
|
|
537
|
+
})
|
|
25
538
|
})
|