effect-app 4.0.0-beta.31 → 4.0.0-beta.310
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 +1486 -0
- package/dist/Array.d.ts +2 -1
- package/dist/Array.d.ts.map +1 -1
- package/dist/Array.js +4 -4
- package/dist/Chunk.d.ts.map +1 -1
- package/dist/Config/SecretURL.d.ts +3 -1
- package/dist/Config/SecretURL.d.ts.map +1 -1
- package/dist/Config/SecretURL.js +3 -6
- package/dist/Config/internal/configSecretURL.d.ts.map +1 -1
- package/dist/Config/internal/configSecretURL.js +2 -2
- package/dist/Config.d.ts +2 -0
- package/dist/Config.d.ts.map +1 -0
- package/dist/Config.js +8 -0
- package/dist/ConfigProvider.d.ts +2 -0
- package/dist/ConfigProvider.d.ts.map +1 -0
- package/dist/ConfigProvider.js +6 -0
- package/dist/{ServiceMap.d.ts → Context.d.ts} +18 -20
- package/dist/Context.d.ts.map +1 -0
- package/dist/Context.js +67 -0
- package/dist/DataDependencies.d.ts +280 -0
- package/dist/DataDependencies.d.ts.map +1 -0
- package/dist/DataDependencies.js +112 -0
- package/dist/Effect.d.ts +10 -9
- package/dist/Effect.d.ts.map +1 -1
- package/dist/Effect.js +5 -8
- package/dist/Emailer.d.ts +51 -0
- package/dist/Emailer.d.ts.map +1 -0
- package/dist/Emailer.js +7 -0
- package/dist/Function.d.ts.map +1 -1
- package/dist/Layer.d.ts +10 -6
- package/dist/Layer.d.ts.map +1 -1
- package/dist/Layer.js +3 -2
- package/dist/Model/Repository/Registry.d.ts +22 -0
- package/dist/Model/Repository/Registry.d.ts.map +1 -0
- package/dist/Model/Repository/Registry.js +18 -0
- package/dist/Model/Repository/dependency.d.ts +6 -0
- package/dist/Model/Repository/dependency.d.ts.map +1 -0
- package/dist/Model/Repository/dependency.js +29 -0
- package/dist/Model/Repository/ext.d.ts +60 -0
- package/dist/Model/Repository/ext.d.ts.map +1 -0
- package/dist/Model/Repository/ext.js +124 -0
- package/dist/Model/Repository/internal/internal.d.ts +68 -0
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -0
- package/dist/Model/Repository/internal/internal.js +537 -0
- package/dist/Model/Repository/legacy.d.ts +21 -0
- package/dist/Model/Repository/legacy.d.ts.map +1 -0
- package/dist/Model/Repository/legacy.js +2 -0
- package/dist/Model/Repository/makeRepo.d.ts +63 -0
- package/dist/Model/Repository/makeRepo.d.ts.map +1 -0
- package/dist/Model/Repository/makeRepo.js +27 -0
- package/dist/Model/Repository/service.d.ts +121 -0
- package/dist/Model/Repository/service.d.ts.map +1 -0
- package/dist/Model/Repository/service.js +2 -0
- package/dist/Model/Repository/validation.d.ts +58 -0
- package/dist/Model/Repository/validation.d.ts.map +1 -0
- package/dist/Model/Repository/validation.js +32 -0
- package/dist/Model/Repository.d.ts +8 -0
- package/dist/Model/Repository.d.ts.map +1 -0
- package/dist/Model/Repository.js +8 -0
- package/dist/Model/dsl.d.ts +33 -0
- package/dist/Model/dsl.d.ts.map +1 -0
- package/dist/Model/dsl.js +43 -0
- package/dist/Model/filter/filterApi.d.ts +30 -0
- package/dist/Model/filter/filterApi.d.ts.map +1 -0
- package/dist/Model/filter/filterApi.js +2 -0
- package/dist/Model/filter/types/errors.d.ts +29 -0
- package/dist/Model/filter/types/errors.d.ts.map +1 -0
- package/dist/Model/filter/types/errors.js +2 -0
- package/dist/Model/filter/types/fields.d.ts +15 -0
- package/dist/Model/filter/types/fields.d.ts.map +1 -0
- package/dist/Model/filter/types/fields.js +2 -0
- package/dist/Model/filter/types/path/common.d.ts +316 -0
- package/dist/Model/filter/types/path/common.d.ts.map +1 -0
- package/dist/Model/filter/types/path/common.js +2 -0
- package/dist/Model/filter/types/path/eager.d.ts +94 -0
- package/dist/Model/filter/types/path/eager.d.ts.map +1 -0
- package/dist/Model/filter/types/path/eager.js +36 -0
- package/dist/Model/filter/types/path/index.d.ts +4 -0
- package/dist/Model/filter/types/path/index.d.ts.map +1 -0
- package/dist/Model/filter/types/path/index.js +3 -0
- package/dist/Model/filter/types/utils.d.ts +79 -0
- package/dist/Model/filter/types/utils.d.ts.map +1 -0
- package/dist/Model/filter/types/utils.js +2 -0
- package/dist/Model/filter/types/validator.d.ts +30 -0
- package/dist/Model/filter/types/validator.d.ts.map +1 -0
- package/dist/Model/filter/types/validator.js +2 -0
- package/dist/Model/filter/types.d.ts +5 -0
- package/dist/Model/filter/types.d.ts.map +1 -0
- package/dist/Model/filter/types.js +7 -0
- package/dist/Model/query/dsl.d.ts +614 -0
- package/dist/Model/query/dsl.d.ts.map +1 -0
- package/dist/Model/query/dsl.js +376 -0
- package/dist/Model/query/new-kid-interpreter.d.ts +132 -0
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -0
- package/dist/Model/query/new-kid-interpreter.js +361 -0
- package/dist/Model/query.d.ts +15 -0
- package/dist/Model/query.d.ts.map +1 -0
- package/dist/Model/query.js +3 -0
- package/dist/Model.d.ts +6 -0
- package/dist/Model.d.ts.map +1 -0
- package/dist/Model.js +6 -0
- package/dist/NonEmptySet.d.ts +3 -1
- package/dist/NonEmptySet.d.ts.map +1 -1
- package/dist/NonEmptySet.js +2 -2
- package/dist/Option.d.ts +1 -0
- package/dist/Option.d.ts.map +1 -1
- package/dist/Option.js +3 -1
- package/dist/Pure.d.ts +7 -5
- package/dist/Pure.d.ts.map +1 -1
- package/dist/Pure.js +7 -4
- package/dist/QueueMaker.d.ts +11 -0
- package/dist/QueueMaker.d.ts.map +1 -0
- package/dist/QueueMaker.js +11 -0
- package/dist/RequestContext.d.ts +87 -0
- package/dist/RequestContext.d.ts.map +1 -0
- package/dist/RequestContext.js +45 -0
- package/dist/RequestScopedDependencies.d.ts +21 -0
- package/dist/RequestScopedDependencies.d.ts.map +1 -0
- package/dist/RequestScopedDependencies.js +16 -0
- package/dist/Schema/Class.d.ts +188 -19
- package/dist/Schema/Class.d.ts.map +1 -1
- package/dist/Schema/Class.js +260 -17
- package/dist/Schema/FastCheck.d.ts.map +1 -1
- package/dist/Schema/SchemaParser.d.ts +5 -0
- package/dist/Schema/SchemaParser.d.ts.map +1 -0
- package/dist/Schema/SchemaParser.js +6 -0
- package/dist/Schema/SpecialJsonSchema.d.ts +34 -0
- package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
- package/dist/Schema/SpecialJsonSchema.js +118 -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 +4 -2
- package/dist/Schema/brand.d.ts.map +1 -1
- package/dist/Schema/brand.js +3 -1
- package/dist/Schema/email.d.ts.map +1 -1
- package/dist/Schema/email.js +7 -4
- package/dist/Schema/ext.d.ts +374 -56
- package/dist/Schema/ext.d.ts.map +1 -1
- package/dist/Schema/ext.js +393 -54
- package/dist/Schema/moreStrings.d.ts +82 -36
- package/dist/Schema/moreStrings.d.ts.map +1 -1
- package/dist/Schema/moreStrings.js +49 -42
- package/dist/Schema/numbers.d.ts +34 -21
- package/dist/Schema/numbers.d.ts.map +1 -1
- package/dist/Schema/numbers.js +55 -12
- package/dist/Schema/phoneNumber.d.ts.map +1 -1
- package/dist/Schema/phoneNumber.js +6 -3
- package/dist/Schema/schema.d.ts +1 -1
- package/dist/Schema/schema.d.ts.map +1 -1
- package/dist/Schema/schema.js +2 -2
- package/dist/Schema/strings.d.ts +18 -4
- package/dist/Schema/strings.d.ts.map +1 -1
- package/dist/Schema/strings.js +1 -5
- package/dist/Schema.d.ts +222 -8
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +191 -12
- package/dist/SchemaAST.d.ts +2 -0
- package/dist/SchemaAST.d.ts.map +1 -0
- package/dist/SchemaAST.js +2 -0
- package/dist/Set.d.ts +4 -1
- package/dist/Set.d.ts.map +1 -1
- package/dist/Set.js +3 -2
- package/dist/Store.d.ts +163 -0
- package/dist/Store.d.ts.map +1 -0
- package/dist/Store.js +113 -0
- package/dist/_ext/Array.d.ts +1 -1
- package/dist/_ext/Array.d.ts.map +1 -1
- package/dist/_ext/Array.js +4 -2
- package/dist/_ext/misc.d.ts +4 -1
- package/dist/_ext/misc.d.ts.map +1 -1
- package/dist/_ext/misc.js +4 -2
- package/dist/_ext/ord.ext.d.ts +2 -1
- package/dist/_ext/ord.ext.d.ts.map +1 -1
- package/dist/_ext/ord.ext.js +2 -2
- package/dist/builtin.d.ts +2 -14
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.js +1 -1
- package/dist/client/InvalidationKeys.d.ts +30 -0
- package/dist/client/InvalidationKeys.d.ts.map +1 -0
- package/dist/client/InvalidationKeys.js +37 -0
- package/dist/client/apiClientFactory.d.ts +19 -31
- package/dist/client/apiClientFactory.d.ts.map +1 -1
- package/dist/client/apiClientFactory.js +155 -35
- package/dist/client/clientFor.d.ts +54 -18
- package/dist/client/clientFor.d.ts.map +1 -1
- package/dist/client/clientFor.js +9 -1
- package/dist/client/errors.d.ts +82 -27
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +75 -19
- package/dist/client/makeClient.d.ts +490 -34
- package/dist/client/makeClient.d.ts.map +1 -1
- package/dist/client/makeClient.js +62 -36
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -1
- package/dist/faker.d.ts.map +1 -1
- package/dist/http/Request.d.ts +1 -1
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +2 -2
- package/dist/ids.d.ts +42 -14
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +30 -5
- package/dist/index.d.ts +6 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/middleware.d.ts +13 -7
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +14 -8
- package/dist/rpc/Invalidation.d.ts +2276 -0
- package/dist/rpc/Invalidation.d.ts.map +1 -0
- package/dist/rpc/Invalidation.js +178 -0
- package/dist/rpc/MiddlewareMaker.d.ts +11 -7
- package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
- package/dist/rpc/MiddlewareMaker.js +59 -38
- 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 +14 -10
- package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
- package/dist/rpc/RpcMiddleware.js +1 -1
- package/dist/rpc.d.ts +1 -1
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +2 -2
- package/dist/runtime.d.ts +19 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +40 -0
- package/dist/setupRequest.d.ts +20 -0
- package/dist/setupRequest.d.ts.map +1 -0
- package/dist/setupRequest.js +72 -0
- package/dist/toast.d.ts +51 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +34 -0
- package/dist/transform.d.ts +1 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +4 -5
- package/dist/utils/effectify.d.ts +1 -1
- package/dist/utils/effectify.d.ts.map +1 -1
- package/dist/utils/effectify.js +2 -2
- package/dist/utils/extend.d.ts.map +1 -1
- package/dist/utils/gen.d.ts +4 -4
- 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 +4 -3
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +4 -4
- package/dist/utils.d.ts +34 -39
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +33 -37
- package/dist/validation/validators.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/withToast.d.ts +30 -0
- package/dist/withToast.d.ts.map +1 -0
- package/dist/withToast.js +64 -0
- package/package.json +27 -251
- package/src/Array.ts +5 -5
- package/src/Chunk.ts +3 -3
- package/src/Config/SecretURL.ts +6 -3
- package/src/Config/internal/configSecretURL.ts +2 -2
- package/src/Config.ts +7 -0
- package/src/ConfigProvider.ts +5 -0
- package/src/{ServiceMap.ts → Context.ts} +56 -63
- package/src/DataDependencies.ts +171 -0
- package/src/Effect.ts +14 -16
- package/src/Emailer.ts +51 -0
- package/src/Inputify.type.ts +1 -1
- package/src/Layer.ts +11 -7
- package/src/Model/Repository/Registry.ts +35 -0
- package/src/Model/Repository/dependency.ts +32 -0
- package/src/Model/Repository/ext.ts +374 -0
- package/src/Model/Repository/internal/internal.ts +905 -0
- package/src/Model/Repository/legacy.ts +29 -0
- package/src/Model/Repository/makeRepo.ts +156 -0
- package/src/Model/Repository/service.ts +676 -0
- package/src/Model/Repository/validation.ts +31 -0
- package/src/Model/Repository.ts +7 -0
- package/src/Model/dsl.ts +129 -0
- package/src/Model/filter/filterApi.ts +60 -0
- package/src/Model/filter/types/errors.ts +47 -0
- package/src/Model/filter/types/fields.ts +50 -0
- package/src/Model/filter/types/path/common.ts +404 -0
- package/src/Model/filter/types/path/eager.ts +329 -0
- package/src/Model/filter/types/path/index.ts +4 -0
- package/src/Model/filter/types/utils.ts +128 -0
- package/src/Model/filter/types/validator.ts +46 -0
- package/src/Model/filter/types.ts +6 -0
- package/src/Model/query/dsl.ts +2911 -0
- package/src/Model/query/new-kid-interpreter.ts +526 -0
- package/src/Model/query.ts +13 -0
- package/src/Model.ts +5 -0
- package/src/NonEmptySet.ts +6 -4
- package/src/Option.ts +2 -0
- package/src/Pure.ts +10 -7
- package/src/QueueMaker.ts +11 -0
- package/src/RequestContext.ts +48 -0
- package/src/RequestScopedDependencies.ts +45 -0
- package/src/Schema/Class.ts +633 -59
- package/src/Schema/SchemaParser.ts +12 -0
- package/src/Schema/SpecialJsonSchema.ts +139 -0
- package/src/Schema/SpecialOpenApi.ts +130 -0
- package/src/Schema/brand.ts +22 -2
- package/src/Schema/email.ts +9 -4
- package/src/Schema/ext.ts +517 -93
- package/src/Schema/moreStrings.ts +147 -68
- package/src/Schema/numbers.ts +97 -28
- package/src/Schema/phoneNumber.ts +9 -5
- package/src/Schema/schema.ts +1 -1
- package/src/Schema/strings.ts +23 -14
- package/src/Schema.ts +397 -26
- package/src/SchemaAST.ts +1 -0
- package/src/Set.ts +6 -2
- package/src/Store.ts +273 -0
- package/src/_ext/Array.ts +4 -2
- package/src/_ext/misc.ts +4 -1
- package/src/_ext/ord.ext.ts +2 -1
- package/src/builtin-json.d.ts +24 -0
- package/src/builtin.ts +16 -21
- package/src/client/InvalidationKeys.ts +55 -0
- package/src/client/apiClientFactory.ts +299 -134
- package/src/client/clientFor.ts +107 -34
- package/src/client/errors.ts +100 -29
- package/src/client/makeClient.ts +593 -92
- package/src/client.ts +6 -4
- package/src/http/Request.ts +3 -3
- package/src/http.ts +1 -1
- package/src/ids.ts +33 -6
- package/src/index.ts +20 -23
- package/src/middleware.ts +13 -9
- package/src/rpc/Invalidation.ts +278 -0
- package/src/rpc/MiddlewareMaker.ts +88 -80
- package/src/rpc/README.md +2 -2
- package/src/rpc/RpcContextMap.ts +7 -6
- package/src/rpc/RpcMiddleware.ts +19 -13
- package/src/rpc.ts +4 -4
- package/src/runtime.ts +56 -0
- package/src/setupRequest.ts +138 -0
- package/src/toast.ts +54 -0
- package/src/transform.ts +4 -4
- package/src/utils/effectify.ts +1 -1
- package/src/utils/gen.ts +8 -8
- package/src/utils/logLevel.ts +1 -1
- package/src/utils/logger.ts +4 -3
- package/src/utils.ts +85 -158
- package/src/validation.ts +2 -2
- package/src/withToast.ts +133 -0
- package/test/dist/rpc.test.d.ts.map +1 -1
- package/test/dist/rpcMiddlewareContext.test.d.ts.map +1 -0
- 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 +1 -1
- package/test/rpc.test.ts +42 -13
- package/test/rpcMiddlewareContext.test.ts +63 -0
- package/test/schema.test.ts +504 -30
- package/test/secretURL.test.ts +160 -0
- package/test/special.test.ts +1258 -0
- package/test/utils.test.ts +7 -7
- package/tsconfig.base.json +6 -5
- package/tsconfig.json +9 -2
- package/tsconfig.src.json +29 -29
- package/tsconfig.test.json +2 -2
- package/dist/Operations.d.ts +0 -123
- package/dist/Operations.d.ts.map +0 -1
- package/dist/Operations.js +0 -29
- package/dist/ServiceMap.d.ts.map +0 -1
- package/dist/ServiceMap.js +0 -91
- package/eslint.config.mjs +0 -26
- package/src/Operations.ts +0 -55
- package/tsconfig.json.bak +0 -12
package/test/schema.test.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// import { generateFromArbitrary } from "@effect-app/infra/test"
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import * as Array from "effect-app/Array"
|
|
3
|
+
import * as S from "effect-app/Schema"
|
|
4
|
+
import { specialJsonSchemaDocument } from "effect-app/Schema/SpecialJsonSchema"
|
|
5
|
+
import * as Effect from "effect/Effect"
|
|
6
|
+
import * as Struct from "effect/Struct"
|
|
7
|
+
import { describe, expect, expectTypeOf, test } from "vitest"
|
|
4
8
|
|
|
5
9
|
const A = S.Struct({ a: S.NonEmptyString255, email: S.NullOr(S.Email) })
|
|
6
10
|
test("works", () => {
|
|
@@ -14,22 +18,170 @@ test("works", () => {
|
|
|
14
18
|
})
|
|
15
19
|
|
|
16
20
|
test("literal default works", () => {
|
|
17
|
-
const l = S.
|
|
21
|
+
const l = S.Literals(["a", "b"])
|
|
18
22
|
expect(l.Default).toBe("a")
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
expectTypeOf(l.Default).toEqualTypeOf<"a">()
|
|
24
|
+
const s = S.Struct({ l: l.withConstructorDefault })
|
|
25
|
+
expect(s.make({}).l).toBe("a")
|
|
21
26
|
|
|
22
27
|
const l2 = l.changeDefault("b")
|
|
23
|
-
const s2 = S.Struct({ l: l2.
|
|
24
|
-
expect(s2.
|
|
28
|
+
const s2 = S.Struct({ l: l2.withConstructorDefault })
|
|
29
|
+
expect(s2.make({}).l).toBe("b")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Toy schema for `dropConstructorDefault`: a field with a constructor
|
|
33
|
+
// default that should NOT auto-populate when deriving a partial-update
|
|
34
|
+
// schema from a "create" schema.
|
|
35
|
+
const Person = S.Struct({
|
|
36
|
+
name: S.String,
|
|
37
|
+
notes: S.NullOr(S.String).pipe(S.optionalKey, S.withConstructorDefault(Effect.succeed(null)))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("plain Struct.omit leaks the inherited constructor default (the bug)", () => {
|
|
41
|
+
const UpdatePersonBuggy = S.Struct(Struct.omit(Person.fields, [] as const))
|
|
42
|
+
expect(UpdatePersonBuggy.make({ name: "Mario" })).toEqual({ name: "Mario", notes: null })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("Struct.map(S.dropConstructorDefault) strips the inherited constructor default (the fix)", () => {
|
|
46
|
+
const UpdatePersonFixed = S.Struct(Struct.map(Struct.omit(Person.fields, [] as const), S.dropConstructorDefault))
|
|
47
|
+
expect(UpdatePersonFixed.make({ name: "Mario" })).toEqual({ name: "Mario" })
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("Struct.map(S.dropConstructorDefault) still omits the requested keys, like Struct.omit", () => {
|
|
51
|
+
const NameOnly = S.Struct(Struct.map(Struct.omit(Person.fields, ["notes"] as const), S.dropConstructorDefault))
|
|
52
|
+
expect(NameOnly.make({ name: "Mario" })).toEqual({ name: "Mario" })
|
|
53
|
+
expectTypeOf<typeof NameOnly.Type>().toEqualTypeOf<{ readonly name: string }>()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("makeExactOptional drops the inherited constructor default too", () => {
|
|
57
|
+
const UpdatePerson = S.Struct(S.makeExactOptional(Person.fields))
|
|
58
|
+
expect(UpdatePerson.make({ name: "Mario" })).toEqual({ name: "Mario" })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test("dropConstructorDefault stays 'no-default' after annotate/annotateKey (Rebuild fix)", () => {
|
|
62
|
+
const notesDropped = S.dropConstructorDefault(Person.fields.notes)
|
|
63
|
+
expectTypeOf<(typeof notesDropped)["~type.constructor.default"]>().toEqualTypeOf<"no-default">()
|
|
64
|
+
|
|
65
|
+
const notesAnnotated = notesDropped.annotate({ description: "no longer defaulted" })
|
|
66
|
+
expectTypeOf<(typeof notesAnnotated)["~type.constructor.default"]>().toEqualTypeOf<"no-default">()
|
|
67
|
+
|
|
68
|
+
const notesKeyAnnotated = notesDropped.annotateKey({ description: "no longer defaulted (key)" })
|
|
69
|
+
expectTypeOf<(typeof notesKeyAnnotated)["~type.constructor.default"]>().toEqualTypeOf<"no-default">()
|
|
70
|
+
|
|
71
|
+
const UpdatePerson = S.Struct({ name: Person.fields.name, notes: notesAnnotated })
|
|
72
|
+
expect(UpdatePerson.make({ name: "Mario" })).toEqual({ name: "Mario" })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("NonEmptyString255.Type uses the named brand alias", () => {
|
|
76
|
+
type A = typeof S.NonEmptyString255.Type
|
|
77
|
+
type B = string & S.NonEmptyString255Brand
|
|
78
|
+
expectTypeOf<A>().toEqualTypeOf<B>()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test("Opaque accepts an explicit Encoded type", () => {
|
|
82
|
+
interface User {
|
|
83
|
+
readonly id: string
|
|
84
|
+
readonly _tag: "User"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface UserEncoded {
|
|
88
|
+
readonly id: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const baseSchema = S.Struct({ id: S.String, name: S.String })
|
|
92
|
+
const _UserSchema = S.Opaque<User, UserEncoded>()(baseSchema)
|
|
93
|
+
|
|
94
|
+
expectTypeOf<S.Codec.Encoded<typeof _UserSchema>>().toEqualTypeOf<UserEncoded>()
|
|
95
|
+
expectTypeOf<S.Schema.Type<typeof _UserSchema>>().toEqualTypeOf<User>()
|
|
96
|
+
expectTypeOf<S.Codec.Encoded<typeof _UserSchema>>().not.toEqualTypeOf<S.Codec.Encoded<typeof baseSchema>>()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("Opaque with one generic keeps the base encoded shape", () => {
|
|
100
|
+
interface User {
|
|
101
|
+
readonly id: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const baseSchema = S.Struct({ id: S.String })
|
|
105
|
+
const _UserSchema = S.Opaque<User>()(baseSchema)
|
|
106
|
+
|
|
107
|
+
expectTypeOf<S.Codec.Encoded<typeof _UserSchema>>().toEqualTypeOf<{ readonly id: string }>()
|
|
108
|
+
expectTypeOf<S.Schema.Type<typeof _UserSchema>>().toEqualTypeOf<User>()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("Opaque preserves optional Struct.make input", () => {
|
|
112
|
+
interface User {
|
|
113
|
+
readonly a?: string | undefined
|
|
114
|
+
readonly b?: number | undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const schema = S.Opaque<User>()(S.Struct({
|
|
118
|
+
a: S.optional(S.String),
|
|
119
|
+
b: S.optional(S.Number)
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
const made = schema.make()
|
|
123
|
+
expect(made).toEqual({})
|
|
124
|
+
expectTypeOf(made).toEqualTypeOf<User>()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test("Opaque preserves optional TaggedStruct.make input", () => {
|
|
128
|
+
interface OnlyTag {
|
|
129
|
+
readonly _tag: "OnlyTag"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const schema = S.Opaque<OnlyTag>()(S.TaggedStruct("OnlyTag", {}))
|
|
133
|
+
|
|
134
|
+
const made = schema.make()
|
|
135
|
+
expect(made).toEqual({ _tag: "OnlyTag" })
|
|
136
|
+
expectTypeOf(made).toEqualTypeOf<OnlyTag>()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test("S.Literals([\"A\", \"B\"]).Default is typed as \"A\"", () => {
|
|
140
|
+
const l = S.Literals(["A", "B"])
|
|
141
|
+
expect(l.Default).toBe("A")
|
|
142
|
+
expectTypeOf(l.Default).toEqualTypeOf<"A">()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test("Struct.make accepts void when all fields are optional", () => {
|
|
146
|
+
const schema = S.Struct({
|
|
147
|
+
a: S.optional(S.String),
|
|
148
|
+
b: S.optional(S.Number)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const made = schema.make()
|
|
152
|
+
expect(made).toEqual({})
|
|
153
|
+
expectTypeOf(made).toEqualTypeOf<{ readonly a?: string | undefined; readonly b?: number | undefined }>()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test("StructNestedEncoded resolves Struct and from.Encoded", () => {
|
|
157
|
+
const plain = S.Struct({ a: S.String, b: S.NullOr(S.String) })
|
|
158
|
+
const encodedKeys = plain.pipe(S.encodeKeys({ b: "b_encoded" }))
|
|
159
|
+
|
|
160
|
+
expectTypeOf<S.StructNestedEncoded<typeof plain>>().toEqualTypeOf<{
|
|
161
|
+
readonly a: string
|
|
162
|
+
readonly b: string | null
|
|
163
|
+
}>()
|
|
164
|
+
|
|
165
|
+
expectTypeOf<S.StructNestedEncoded<typeof encodedKeys>>().toEqualTypeOf<{
|
|
166
|
+
readonly a: string
|
|
167
|
+
readonly b_encoded: string | null
|
|
168
|
+
}>()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test("TaggedStruct.make accepts void when only constructor-default fields exist", () => {
|
|
172
|
+
const schema = S.TaggedStruct("OnlyTag", {})
|
|
173
|
+
|
|
174
|
+
const made = schema.make()
|
|
175
|
+
expect(made).toEqual({ _tag: "OnlyTag" })
|
|
176
|
+
expectTypeOf(made).toEqualTypeOf<{ readonly _tag: "OnlyTag" }>()
|
|
25
177
|
})
|
|
26
178
|
|
|
27
179
|
test("tagged union derives tag map and tags from v4 literal ast", () => {
|
|
28
|
-
const schema = S.TaggedUnion(
|
|
180
|
+
const schema = S.TaggedUnion([
|
|
29
181
|
S.TaggedStruct("A", { a: S.String }),
|
|
30
|
-
S.TaggedStruct("B", { b: S.
|
|
182
|
+
S.TaggedStruct("B", { b: S.Finite }),
|
|
31
183
|
S.TaggedStruct("C", { c: S.Boolean })
|
|
32
|
-
)
|
|
184
|
+
])
|
|
33
185
|
const caseA = schema.cases["A"]
|
|
34
186
|
const caseB = schema.cases["B"]
|
|
35
187
|
const caseC = schema.cases["C"]
|
|
@@ -56,21 +208,21 @@ test("tagged union derives tag map and tags from v4 literal ast", () => {
|
|
|
56
208
|
})
|
|
57
209
|
|
|
58
210
|
test("TaggedUnion tags returns a Literals schema with correct literal values", () => {
|
|
59
|
-
const schema = S.TaggedUnion(
|
|
211
|
+
const schema = S.TaggedUnion([
|
|
60
212
|
S.TaggedStruct("X", { x: S.String }),
|
|
61
|
-
S.TaggedStruct("Y", { y: S.
|
|
62
|
-
)
|
|
213
|
+
S.TaggedStruct("Y", { y: S.Finite })
|
|
214
|
+
])
|
|
63
215
|
|
|
64
216
|
expect(schema.tags.literals).toEqual(["X", "Y"])
|
|
65
217
|
expectTypeOf(schema.tags.literals).toMatchTypeOf<readonly ["X", "Y"]>()
|
|
66
218
|
})
|
|
67
219
|
|
|
68
220
|
test("TaggedUnion tags.pick returns a subset of the tag literals", () => {
|
|
69
|
-
const schema = S.TaggedUnion(
|
|
221
|
+
const schema = S.TaggedUnion([
|
|
70
222
|
S.TaggedStruct("A", { a: S.String }),
|
|
71
|
-
S.TaggedStruct("B", { b: S.
|
|
223
|
+
S.TaggedStruct("B", { b: S.Finite }),
|
|
72
224
|
S.TaggedStruct("C", { c: S.Boolean })
|
|
73
|
-
)
|
|
225
|
+
])
|
|
74
226
|
|
|
75
227
|
const subset = schema.tags.pick(["A", "C"])
|
|
76
228
|
expect(subset.literals).toEqual(["A", "C"])
|
|
@@ -82,7 +234,7 @@ test("TaggedUnion tags.pick returns a subset of the tag literals", () => {
|
|
|
82
234
|
test("tags standalone function extracts tags from member schemas", () => {
|
|
83
235
|
const members = [
|
|
84
236
|
S.TaggedStruct("Foo", { foo: S.String }),
|
|
85
|
-
S.TaggedStruct("Bar", { bar: S.
|
|
237
|
+
S.TaggedStruct("Bar", { bar: S.Finite })
|
|
86
238
|
] as const
|
|
87
239
|
|
|
88
240
|
const tagSchema = S.tags(members)
|
|
@@ -95,7 +247,7 @@ test("tags standalone function extracts tags from member schemas", () => {
|
|
|
95
247
|
test("ExtendTaggedUnion adds tags to an existing Union", () => {
|
|
96
248
|
const union = S.Union([
|
|
97
249
|
S.TaggedStruct("P", { p: S.String }),
|
|
98
|
-
S.TaggedStruct("Q", { q: S.
|
|
250
|
+
S.TaggedStruct("Q", { q: S.Finite })
|
|
99
251
|
])
|
|
100
252
|
const extended = S.ExtendTaggedUnion(union)
|
|
101
253
|
|
|
@@ -109,10 +261,10 @@ test("ExtendTaggedUnion adds tags to an existing Union", () => {
|
|
|
109
261
|
})
|
|
110
262
|
|
|
111
263
|
test("TaggedUnion match dispatches on _tag", () => {
|
|
112
|
-
const schema = S.TaggedUnion(
|
|
264
|
+
const schema = S.TaggedUnion([
|
|
113
265
|
S.TaggedStruct("A", { a: S.String }),
|
|
114
|
-
S.TaggedStruct("B", { b: S.
|
|
115
|
-
)
|
|
266
|
+
S.TaggedStruct("B", { b: S.Finite })
|
|
267
|
+
])
|
|
116
268
|
type T = S.Schema.Type<typeof schema>
|
|
117
269
|
|
|
118
270
|
const matcher = schema.match({
|
|
@@ -124,9 +276,9 @@ test("TaggedUnion match dispatches on _tag", () => {
|
|
|
124
276
|
})
|
|
125
277
|
|
|
126
278
|
test("TaggedUnion with single member", () => {
|
|
127
|
-
const schema = S.TaggedUnion(
|
|
279
|
+
const schema = S.TaggedUnion([
|
|
128
280
|
S.TaggedStruct("Only", { val: S.String })
|
|
129
|
-
)
|
|
281
|
+
])
|
|
130
282
|
|
|
131
283
|
expect(schema.tags.literals).toEqual(["Only"])
|
|
132
284
|
expect(S.decodeSync(schema.tags)("Only")).toBe("Only")
|
|
@@ -135,11 +287,11 @@ test("TaggedUnion with single member", () => {
|
|
|
135
287
|
})
|
|
136
288
|
|
|
137
289
|
test("TaggedUnion tags type is narrowed to the exact tag literals", () => {
|
|
138
|
-
const schema = S.TaggedUnion(
|
|
290
|
+
const schema = S.TaggedUnion([
|
|
139
291
|
S.TaggedStruct("Alpha", { a: S.String }),
|
|
140
|
-
S.TaggedStruct("Beta", { b: S.
|
|
292
|
+
S.TaggedStruct("Beta", { b: S.Finite }),
|
|
141
293
|
S.TaggedStruct("Gamma", { c: S.Boolean })
|
|
142
|
-
)
|
|
294
|
+
])
|
|
143
295
|
|
|
144
296
|
type Tags = S.Schema.Type<typeof schema.tags>
|
|
145
297
|
expectTypeOf<Tags>().toEqualTypeOf<"Alpha" | "Beta" | "Gamma">()
|
|
@@ -149,11 +301,11 @@ test("TaggedUnion with encodeKeys renaming a non-tag key", () => {
|
|
|
149
301
|
const MemberA = S.TaggedStruct("A", { firstName: S.String }).pipe(
|
|
150
302
|
S.encodeKeys({ firstName: "first_name" })
|
|
151
303
|
)
|
|
152
|
-
const MemberB = S.TaggedStruct("B", { lastName: S.
|
|
304
|
+
const MemberB = S.TaggedStruct("B", { lastName: S.Finite }).pipe(
|
|
153
305
|
S.encodeKeys({ lastName: "last_name" })
|
|
154
306
|
)
|
|
155
307
|
|
|
156
|
-
const schema = S.TaggedUnion(MemberA, MemberB)
|
|
308
|
+
const schema = S.TaggedUnion([MemberA, MemberB])
|
|
157
309
|
|
|
158
310
|
expect(schema.tags.literals).toEqual(["A", "B"])
|
|
159
311
|
expect(S.decodeSync(schema.tags)("A")).toBe("A")
|
|
@@ -179,9 +331,9 @@ test("TaggedUnion with encodeKeys renaming a non-tag key", () => {
|
|
|
179
331
|
|
|
180
332
|
test("TaggedUnion with TaggedClass members", () => {
|
|
181
333
|
class Foo extends S.TaggedClass<Foo>()("Foo", { name: S.String }) {}
|
|
182
|
-
class Bar extends S.TaggedClass<Bar>()("Bar", { count: S.
|
|
334
|
+
class Bar extends S.TaggedClass<Bar>()("Bar", { count: S.Finite }) {}
|
|
183
335
|
|
|
184
|
-
const schema = S.TaggedUnion(Foo, Bar)
|
|
336
|
+
const schema = S.TaggedUnion([Foo, Bar])
|
|
185
337
|
|
|
186
338
|
expect(schema.tags.literals).toEqual(["Foo", "Bar"])
|
|
187
339
|
expect(S.decodeSync(schema.tags)("Foo")).toBe("Foo")
|
|
@@ -200,3 +352,325 @@ test("TaggedUnion with TaggedClass members", () => {
|
|
|
200
352
|
expect(schema.guards.Foo(new Bar({ count: 3 }))).toBe(false)
|
|
201
353
|
expect(schema.guards.Bar(new Bar({ count: 3 }))).toBe(true)
|
|
202
354
|
})
|
|
355
|
+
|
|
356
|
+
describe("ReadonlySetFromArray", () => {
|
|
357
|
+
test("decodes an array of strings to a Set", () => {
|
|
358
|
+
const schema = S.ReadonlySetFromArray(S.String)
|
|
359
|
+
const decoded = S.decodeUnknownSync(schema)(["a", "b", "c"])
|
|
360
|
+
expect(decoded).toEqual(new Set(["a", "b", "c"]))
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
test("encodes a Set back to an array", () => {
|
|
364
|
+
const schema = S.ReadonlySetFromArray(S.String)
|
|
365
|
+
const encoded = S.encodeSync(schema)(new Set(["a", "b"]))
|
|
366
|
+
expect(encoded).toEqual(["a", "b"])
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test("decodes with NumberFromString as value", () => {
|
|
370
|
+
const schema = S.ReadonlySetFromArray(S.NumberFromString)
|
|
371
|
+
const decoded = S.decodeUnknownSync(schema)(["1", "2", "3"])
|
|
372
|
+
expect(decoded).toEqual(new Set([1, 2, 3]))
|
|
373
|
+
expectTypeOf(decoded).toEqualTypeOf<ReadonlySet<number>>()
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
test("encodes with NumberFromString as value", () => {
|
|
377
|
+
const schema = S.ReadonlySetFromArray(S.NumberFromString)
|
|
378
|
+
const encoded = S.encodeSync(schema)(new Set([1, 2, 3]))
|
|
379
|
+
expect(encoded).toEqual(["1", "2", "3"])
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test("rejects invalid input", () => {
|
|
383
|
+
const schema = S.ReadonlySetFromArray(S.NumberFromString)
|
|
384
|
+
expect(() => S.decodeUnknownSync(schema)([1, 2])).toThrow()
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
describe("ReadonlyMapFromArray", () => {
|
|
389
|
+
test("decodes an array of tuples to a Map", () => {
|
|
390
|
+
const schema = S.ReadonlyMap({ key: S.String, value: S.Finite })
|
|
391
|
+
const decoded = S.decodeUnknownSync(schema)([["a", 1], ["b", 2]])
|
|
392
|
+
expect(decoded).toEqual(new Map([["a", 1], ["b", 2]]))
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
test("encodes a Map back to an array of tuples", () => {
|
|
396
|
+
const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.Finite })
|
|
397
|
+
const encoded = S.encodeSync(schema)(new Map([["a", 1], ["b", 2]]))
|
|
398
|
+
expect(encoded).toEqual([["a", 1], ["b", 2]])
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test("decodes with NumberFromString as key", () => {
|
|
402
|
+
const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
|
|
403
|
+
const decoded = S.decodeUnknownSync(schema)([["1", "one"], ["2", "two"]])
|
|
404
|
+
expect(decoded).toEqual(new Map([[1, "one"], [2, "two"]]))
|
|
405
|
+
expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<number, string>>()
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test("encodes with NumberFromString as key", () => {
|
|
409
|
+
const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
|
|
410
|
+
const encoded = S.encodeSync(schema)(new Map([[1, "one"], [2, "two"]]))
|
|
411
|
+
expect(encoded).toEqual([["1", "one"], ["2", "two"]])
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
test("decodes with NumberFromString as value", () => {
|
|
415
|
+
const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.NumberFromString })
|
|
416
|
+
const decoded = S.decodeUnknownSync(schema)([["a", "10"], ["b", "20"]])
|
|
417
|
+
expect(decoded).toEqual(new Map([["a", 10], ["b", 20]]))
|
|
418
|
+
expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<string, number>>()
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
test("encodes with NumberFromString as value", () => {
|
|
422
|
+
const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.NumberFromString })
|
|
423
|
+
const encoded = S.encodeSync(schema)(new Map([["a", 10], ["b", 20]]))
|
|
424
|
+
expect(encoded).toEqual([["a", "10"], ["b", "20"]])
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test("decodes with NumberFromString as both key and value", () => {
|
|
428
|
+
const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.NumberFromString })
|
|
429
|
+
const decoded = S.decodeUnknownSync(schema)([["1", "10"], ["2", "20"]])
|
|
430
|
+
expect(decoded).toEqual(new Map([[1, 10], [2, 20]]))
|
|
431
|
+
expectTypeOf(decoded).toEqualTypeOf<ReadonlyMap<number, number>>()
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
test("rejects invalid input", () => {
|
|
435
|
+
const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String })
|
|
436
|
+
expect(() => S.decodeUnknownSync(schema)([[1, "val"]])).toThrow()
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
describe("ReadonlySet (with withConstructorDefault)", () => {
|
|
441
|
+
test("make provides withConstructorDefault", () => {
|
|
442
|
+
const schema = S.ReadonlySet(S.NumberFromString)
|
|
443
|
+
const struct = S.Struct({ items: schema.withConstructorDefault })
|
|
444
|
+
const made = struct.make({})
|
|
445
|
+
expect(made.items).toEqual(new Set())
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
test("decodes array with NumberFromString values", () => {
|
|
449
|
+
const schema = S.ReadonlySet(S.NumberFromString)
|
|
450
|
+
const decoded = S.decodeUnknownSync(schema)(["1", "2"])
|
|
451
|
+
expect(decoded).toEqual(new Set([1, 2]))
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
describe("ReadonlyMap (with withConstructorDefault)", () => {
|
|
456
|
+
test("make provides withConstructorDefault", () => {
|
|
457
|
+
const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String })
|
|
458
|
+
const struct = S.Struct({ items: schema.withConstructorDefault })
|
|
459
|
+
const made = struct.make({})
|
|
460
|
+
expect(made.items).toEqual(new Map())
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
test("decodes array of tuples with NumberFromString keys", () => {
|
|
464
|
+
const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String })
|
|
465
|
+
const decoded = S.decodeUnknownSync(schema)([["1", "one"]])
|
|
466
|
+
expect(decoded).toEqual(new Map([[1, "one"]]))
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
describe("JSON Schema", () => {
|
|
471
|
+
test("Email has format, minLength, maxLength", () => {
|
|
472
|
+
const doc = S.toJsonSchemaDocument(S.Email)
|
|
473
|
+
expect(doc).toStrictEqual({
|
|
474
|
+
dialect: "draft-2020-12",
|
|
475
|
+
schema: { "$ref": "#/$defs/Email" },
|
|
476
|
+
definitions: {
|
|
477
|
+
Email: {
|
|
478
|
+
type: "string",
|
|
479
|
+
title: "Email",
|
|
480
|
+
description: "an email according to RFC 5322",
|
|
481
|
+
format: "email",
|
|
482
|
+
allOf: [
|
|
483
|
+
{ minLength: 3 },
|
|
484
|
+
{ maxLength: 998 }
|
|
485
|
+
]
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
test("Email specialJsonSchemaDocument flattens allOf", () => {
|
|
492
|
+
const doc = specialJsonSchemaDocument(S.Email)
|
|
493
|
+
expect(doc).toStrictEqual({
|
|
494
|
+
dialect: "draft-2020-12",
|
|
495
|
+
schema: { "$ref": "#/$defs/Email" },
|
|
496
|
+
definitions: {
|
|
497
|
+
Email: {
|
|
498
|
+
type: "string",
|
|
499
|
+
title: "Email",
|
|
500
|
+
description: "an email according to RFC 5322",
|
|
501
|
+
format: "email",
|
|
502
|
+
minLength: 3,
|
|
503
|
+
maxLength: 998
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
test("Date has identifier DateOrInvalid and ISO 8601 description", () => {
|
|
510
|
+
const doc = S.toJsonSchemaDocument(S.Date)
|
|
511
|
+
expect(doc).toStrictEqual({
|
|
512
|
+
dialect: "draft-2020-12",
|
|
513
|
+
schema: { "$ref": "#/$defs/DateOrInvalid" },
|
|
514
|
+
definitions: {
|
|
515
|
+
DateOrInvalid: {
|
|
516
|
+
type: "string",
|
|
517
|
+
description: "an ISO 8601 date string that will be decoded as a Date (may be invalid)",
|
|
518
|
+
format: "date-time"
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
test("DateValid has identifier Date and ISO 8601 description", () => {
|
|
525
|
+
const doc = S.toJsonSchemaDocument(S.DateValid)
|
|
526
|
+
expect(doc).toStrictEqual({
|
|
527
|
+
dialect: "draft-2020-12",
|
|
528
|
+
schema: { "$ref": "#/$defs/Date" },
|
|
529
|
+
definitions: {
|
|
530
|
+
Date: {
|
|
531
|
+
type: "string",
|
|
532
|
+
description: "a valid ISO 8601 date string that will be decoded as a Date",
|
|
533
|
+
format: "date-time"
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
test("PhoneNumber has format phone", () => {
|
|
540
|
+
const doc = specialJsonSchemaDocument(S.PhoneNumber)
|
|
541
|
+
expect(doc).toStrictEqual({
|
|
542
|
+
dialect: "draft-2020-12",
|
|
543
|
+
schema: { "$ref": "#/$defs/PhoneNumber" },
|
|
544
|
+
definitions: {
|
|
545
|
+
PhoneNumber: {
|
|
546
|
+
type: "string",
|
|
547
|
+
title: "PhoneNumber",
|
|
548
|
+
description: "a phone number with at least 7 digits",
|
|
549
|
+
format: "phone"
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
test("Url has format uri", () => {
|
|
556
|
+
const doc = specialJsonSchemaDocument(S.Url)
|
|
557
|
+
expect(doc).toStrictEqual({
|
|
558
|
+
dialect: "draft-2020-12",
|
|
559
|
+
schema: { "$ref": "#/$defs/Url" },
|
|
560
|
+
definitions: {
|
|
561
|
+
Url: {
|
|
562
|
+
type: "string",
|
|
563
|
+
title: "Url",
|
|
564
|
+
format: "uri"
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
describe("generateGuards", () => {
|
|
572
|
+
const StateSchema = S.TaggedUnion([
|
|
573
|
+
S.TaggedStruct("Active", { since: S.String }),
|
|
574
|
+
S.TaggedStruct("Inactive", { reason: S.String }),
|
|
575
|
+
S.TaggedStruct("Pending", { eta: S.Finite })
|
|
576
|
+
])
|
|
577
|
+
|
|
578
|
+
type State = S.Schema.Type<typeof StateSchema>
|
|
579
|
+
type Entity = { readonly state: State; readonly name: string }
|
|
580
|
+
|
|
581
|
+
const { isActive, isAnyOf, isInactive, isPending } = StateSchema.generateGuards("state")
|
|
582
|
+
|
|
583
|
+
test("isActive narrows to Active member", () => {
|
|
584
|
+
const entity: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
|
|
585
|
+
expect(isActive(entity)).toBe(true)
|
|
586
|
+
if (isActive(entity)) {
|
|
587
|
+
expectTypeOf(entity.state).toEqualTypeOf<{ readonly _tag: "Active"; readonly since: string }>()
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
test("isActive returns false for non-Active", () => {
|
|
592
|
+
const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" }
|
|
593
|
+
expect(isActive(entity)).toBe(false)
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
test("isInactive narrows to Inactive member", () => {
|
|
597
|
+
const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" }
|
|
598
|
+
expect(isInactive(entity)).toBe(true)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
test("isPending narrows to Pending member", () => {
|
|
602
|
+
const entity: Entity = { state: { _tag: "Pending", eta: 42 }, name: "foo" }
|
|
603
|
+
expect(isPending(entity)).toBe(true)
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
test("isAnyOf narrows to union of specified members", () => {
|
|
607
|
+
const isActiveOrPending = isAnyOf(["Active", "Pending"])
|
|
608
|
+
const active: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
|
|
609
|
+
const pending: Entity = { state: { _tag: "Pending", eta: 5 }, name: "bar" }
|
|
610
|
+
const inactive: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "baz" }
|
|
611
|
+
|
|
612
|
+
expect(isActiveOrPending(active)).toBe(true)
|
|
613
|
+
expect(isActiveOrPending(pending)).toBe(true)
|
|
614
|
+
expect(isActiveOrPending(inactive)).toBe(false)
|
|
615
|
+
|
|
616
|
+
if (isActiveOrPending(active)) {
|
|
617
|
+
expectTypeOf(active.state).toEqualTypeOf<
|
|
618
|
+
{ readonly _tag: "Active"; readonly since: string } | { readonly _tag: "Pending"; readonly eta: number }
|
|
619
|
+
>()
|
|
620
|
+
}
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
test("guards use schema-based validation (built-in guards)", () => {
|
|
624
|
+
expect(StateSchema.guards.Active({ _tag: "Active" })).toBe(false)
|
|
625
|
+
expect(StateSchema.guards.Active({ _tag: "Active", since: "2024-01-01" })).toBe(true)
|
|
626
|
+
})
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
describe("generateGuardsFor", () => {
|
|
630
|
+
const StateSchema = S.TaggedUnion([
|
|
631
|
+
S.TaggedStruct("Active", { since: S.String }),
|
|
632
|
+
S.TaggedStruct("Inactive", { reason: S.String }),
|
|
633
|
+
S.TaggedStruct("Pending", { eta: S.Finite })
|
|
634
|
+
])
|
|
635
|
+
|
|
636
|
+
type State = S.Schema.Type<typeof StateSchema>
|
|
637
|
+
type Entity = { readonly state: State; readonly name: string }
|
|
638
|
+
|
|
639
|
+
const { isActive, isAnyOf } = StateSchema.generateGuardsFor<Entity>()("state")
|
|
640
|
+
|
|
641
|
+
test("isActive narrows to Active member", () => {
|
|
642
|
+
const entity: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
|
|
643
|
+
expect(isActive(entity)).toBe(true)
|
|
644
|
+
if (isActive(entity)) {
|
|
645
|
+
expectTypeOf(entity.state).toEqualTypeOf<{ readonly _tag: "Active"; readonly since: string }>()
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
test("isActive returns false for non-Active", () => {
|
|
650
|
+
const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" }
|
|
651
|
+
expect(isActive(entity)).toBe(false)
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
test("isAnyOf narrows to union of specified members", () => {
|
|
655
|
+
const isActiveOrPending = isAnyOf(["Active", "Pending"])
|
|
656
|
+
const active: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" }
|
|
657
|
+
const inactive: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "baz" }
|
|
658
|
+
|
|
659
|
+
expect(isActiveOrPending(active)).toBe(true)
|
|
660
|
+
expect(isActiveOrPending(inactive)).toBe(false)
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
test("ExtendTaggedUnion also exposes generateGuardsFor", () => {
|
|
664
|
+
const union = S.Union([
|
|
665
|
+
S.TaggedStruct("X", { x: S.String }),
|
|
666
|
+
S.TaggedStruct("Y", { y: S.Finite })
|
|
667
|
+
])
|
|
668
|
+
const extended = S.ExtendTaggedUnion(union)
|
|
669
|
+
type Obj = { readonly field: S.Schema.Type<typeof extended> }
|
|
670
|
+
const { isX, isY } = extended.generateGuardsFor<Obj>()("field")
|
|
671
|
+
|
|
672
|
+
expect(isX({ field: { _tag: "X", x: "hi" } })).toBe(true)
|
|
673
|
+
expect(isX({ field: { _tag: "Y", y: 1 } })).toBe(false)
|
|
674
|
+
expect(isY({ field: { _tag: "Y", y: 1 } })).toBe(true)
|
|
675
|
+
})
|
|
676
|
+
})
|