effect-app 4.0.0-beta.9 → 4.0.0-beta.91

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.
Files changed (142) hide show
  1. package/CHANGELOG.md +370 -0
  2. package/dist/Config/SecretURL.js +2 -2
  3. package/dist/Config.d.ts +7 -0
  4. package/dist/Config.d.ts.map +1 -0
  5. package/dist/Config.js +6 -0
  6. package/dist/ConfigProvider.d.ts +39 -0
  7. package/dist/ConfigProvider.d.ts.map +1 -0
  8. package/dist/ConfigProvider.js +42 -0
  9. package/dist/Context.d.ts +40 -0
  10. package/dist/Context.d.ts.map +1 -0
  11. package/dist/Context.js +66 -0
  12. package/dist/Effect.d.ts +8 -7
  13. package/dist/Effect.d.ts.map +1 -1
  14. package/dist/Effect.js +3 -2
  15. package/dist/Layer.d.ts +5 -4
  16. package/dist/Layer.d.ts.map +1 -1
  17. package/dist/Layer.js +1 -1
  18. package/dist/Operations.d.ts +68 -31
  19. package/dist/Operations.d.ts.map +1 -1
  20. package/dist/Pure.d.ts +2 -2
  21. package/dist/Pure.d.ts.map +1 -1
  22. package/dist/Pure.js +13 -13
  23. package/dist/Schema/Class.d.ts +60 -5
  24. package/dist/Schema/Class.d.ts.map +1 -1
  25. package/dist/Schema/Class.js +103 -12
  26. package/dist/Schema/SpecialJsonSchema.d.ts +21 -0
  27. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  28. package/dist/Schema/SpecialJsonSchema.js +59 -0
  29. package/dist/Schema/SpecialOpenApi.d.ts +32 -0
  30. package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
  31. package/dist/Schema/SpecialOpenApi.js +123 -0
  32. package/dist/Schema/brand.d.ts +8 -5
  33. package/dist/Schema/brand.d.ts.map +1 -1
  34. package/dist/Schema/brand.js +1 -1
  35. package/dist/Schema/email.d.ts.map +1 -1
  36. package/dist/Schema/email.js +9 -4
  37. package/dist/Schema/ext.d.ts +103 -46
  38. package/dist/Schema/ext.d.ts.map +1 -1
  39. package/dist/Schema/ext.js +110 -51
  40. package/dist/Schema/moreStrings.d.ts +19 -7
  41. package/dist/Schema/moreStrings.d.ts.map +1 -1
  42. package/dist/Schema/moreStrings.js +14 -9
  43. package/dist/Schema/numbers.d.ts +11 -11
  44. package/dist/Schema/numbers.d.ts.map +1 -1
  45. package/dist/Schema/numbers.js +10 -9
  46. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  47. package/dist/Schema/phoneNumber.js +8 -3
  48. package/dist/Schema/strings.d.ts +4 -4
  49. package/dist/Schema/strings.d.ts.map +1 -1
  50. package/dist/Schema.d.ts +23 -56
  51. package/dist/Schema.d.ts.map +1 -1
  52. package/dist/Schema.js +44 -65
  53. package/dist/client/apiClientFactory.d.ts +11 -28
  54. package/dist/client/apiClientFactory.d.ts.map +1 -1
  55. package/dist/client/apiClientFactory.js +17 -18
  56. package/dist/client/clientFor.d.ts +6 -5
  57. package/dist/client/clientFor.d.ts.map +1 -1
  58. package/dist/client/errors.d.ts +18 -9
  59. package/dist/client/errors.d.ts.map +1 -1
  60. package/dist/client/errors.js +35 -10
  61. package/dist/client/makeClient.d.ts +21 -16
  62. package/dist/client/makeClient.d.ts.map +1 -1
  63. package/dist/client/makeClient.js +32 -23
  64. package/dist/http/Request.d.ts.map +1 -1
  65. package/dist/http/Request.js +5 -5
  66. package/dist/ids.d.ts +3 -3
  67. package/dist/ids.d.ts.map +1 -1
  68. package/dist/ids.js +3 -2
  69. package/dist/index.d.ts +3 -8
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +4 -9
  72. package/dist/middleware.d.ts +2 -2
  73. package/dist/middleware.d.ts.map +1 -1
  74. package/dist/middleware.js +3 -3
  75. package/dist/rpc/MiddlewareMaker.d.ts +4 -3
  76. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  77. package/dist/rpc/MiddlewareMaker.js +7 -6
  78. package/dist/rpc/RpcContextMap.d.ts +2 -2
  79. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  80. package/dist/rpc/RpcContextMap.js +4 -4
  81. package/dist/rpc/RpcMiddleware.d.ts +4 -3
  82. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  83. package/dist/rpc/RpcMiddleware.js +1 -1
  84. package/dist/utils/gen.d.ts +1 -1
  85. package/dist/utils/gen.d.ts.map +1 -1
  86. package/dist/utils/logger.d.ts +2 -2
  87. package/dist/utils/logger.d.ts.map +1 -1
  88. package/dist/utils/logger.js +3 -3
  89. package/dist/utils.d.ts +18 -0
  90. package/dist/utils.d.ts.map +1 -1
  91. package/dist/utils.js +24 -5
  92. package/package.json +29 -17
  93. package/src/Config/SecretURL.ts +1 -1
  94. package/src/Config.ts +14 -0
  95. package/src/ConfigProvider.ts +48 -0
  96. package/src/{ServiceMap.ts → Context.ts} +57 -64
  97. package/src/Effect.ts +11 -9
  98. package/src/Layer.ts +5 -4
  99. package/src/Pure.ts +17 -18
  100. package/src/Schema/Class.ts +168 -20
  101. package/src/Schema/SpecialJsonSchema.ts +69 -0
  102. package/src/Schema/SpecialOpenApi.ts +130 -0
  103. package/src/Schema/brand.ts +13 -7
  104. package/src/Schema/email.ts +10 -2
  105. package/src/Schema/ext.ts +185 -82
  106. package/src/Schema/moreStrings.ts +21 -11
  107. package/src/Schema/numbers.ts +9 -8
  108. package/src/Schema/phoneNumber.ts +8 -1
  109. package/src/Schema.ts +80 -104
  110. package/src/client/apiClientFactory.ts +30 -34
  111. package/src/client/clientFor.ts +6 -1
  112. package/src/client/errors.ts +46 -12
  113. package/src/client/makeClient.ts +122 -62
  114. package/src/http/Request.ts +7 -4
  115. package/src/ids.ts +3 -2
  116. package/src/index.ts +3 -11
  117. package/src/middleware.ts +2 -2
  118. package/src/rpc/MiddlewareMaker.ts +8 -7
  119. package/src/rpc/RpcContextMap.ts +6 -5
  120. package/src/rpc/RpcMiddleware.ts +5 -4
  121. package/src/utils/gen.ts +1 -1
  122. package/src/utils/logger.ts +2 -2
  123. package/src/utils.ts +26 -4
  124. package/test/dist/moreStrings.test.d.ts.map +1 -0
  125. package/test/dist/rpc.test.d.ts.map +1 -1
  126. package/test/dist/secretURL.test.d.ts.map +1 -0
  127. package/test/dist/special.test.d.ts.map +1 -0
  128. package/test/moreStrings.test.ts +17 -0
  129. package/test/rpc.test.ts +28 -6
  130. package/test/schema.test.ts +397 -4
  131. package/test/secretURL.test.ts +157 -0
  132. package/test/special.test.ts +732 -0
  133. package/test/utils.test.ts +2 -2
  134. package/tsconfig.base.json +0 -1
  135. package/tsconfig.json +0 -1
  136. package/dist/ServiceMap.d.ts +0 -44
  137. package/dist/ServiceMap.d.ts.map +0 -1
  138. package/dist/ServiceMap.js +0 -91
  139. package/dist/Struct.d.ts +0 -44
  140. package/dist/Struct.d.ts.map +0 -1
  141. package/dist/Struct.js +0 -29
  142. package/src/Struct.ts +0 -54
@@ -0,0 +1,69 @@
1
+ /**
2
+ * SpecialJsonSchema — A variant of Schema.toJsonSchemaDocument that
3
+ * post-processes the output (e.g. flattens simple allOf).
4
+ */
5
+ import { type JsonSchema, type Schema, SchemaRepresentation } from "effect"
6
+
7
+ /**
8
+ * Converts a schema to a JSON Schema Document (draft-2020-12), with
9
+ * post-processing that flattens simple allOf entries.
10
+ */
11
+ export function specialJsonSchemaDocument(
12
+ schema: Schema.Top,
13
+ options?: Schema.ToJsonSchemaOptions
14
+ ): JsonSchema.Document<"draft-2020-12"> {
15
+ const doc = SchemaRepresentation.fromAST(schema.ast)
16
+ const jd = SchemaRepresentation.toJsonSchemaDocument(doc, options)
17
+ const processedDefs: JsonSchema.Definitions = {}
18
+ for (const [key, def] of Object.entries(jd.definitions)) {
19
+ processedDefs[key] = postProcessJsonSchema(def)
20
+ }
21
+ return {
22
+ dialect: "draft-2020-12",
23
+ schema: postProcessJsonSchema(jd.schema),
24
+ definitions: processedDefs
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Flattens `allOf` entries into the parent when the parent already has a
30
+ * `type` and every `allOf` entry is a plain constraint object (no `$ref`,
31
+ * no `type`). Merged properties from `allOf` entries win on conflict.
32
+ */
33
+ export function flattenSimpleAllOf(obj: unknown): unknown {
34
+ if (obj === null || typeof obj !== "object") return obj
35
+
36
+ if (globalThis.Array.isArray(obj)) {
37
+ return obj.map(flattenSimpleAllOf)
38
+ }
39
+
40
+ const record = obj as Record<string, unknown>
41
+ const result: Record<string, unknown> = {}
42
+ for (const [key, value] of Object.entries(record)) {
43
+ result[key] = flattenSimpleAllOf(value)
44
+ }
45
+
46
+ if (result["type"] && globalThis.Array.isArray(result["allOf"])) {
47
+ const allOf = result["allOf"] as Array<Record<string, unknown>>
48
+ const canFlatten = allOf.every((entry) =>
49
+ typeof entry === "object" && entry !== null && !("$ref" in entry) && !("type" in entry)
50
+ )
51
+ if (canFlatten) {
52
+ const { allOf: _, ...rest } = result
53
+ let merged: Record<string, unknown> = { ...rest }
54
+ for (const entry of allOf) {
55
+ merged = { ...merged, ...entry }
56
+ }
57
+ return merged
58
+ }
59
+ }
60
+
61
+ return result
62
+ }
63
+
64
+ /**
65
+ * Applies JSON Schema post-processing: flattens simple allOf.
66
+ */
67
+ export function postProcessJsonSchema(obj: JsonSchema.JsonSchema): JsonSchema.JsonSchema {
68
+ return flattenSimpleAllOf(obj) as JsonSchema.JsonSchema
69
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * SpecialOpenApi — Deduplicates `components/schemas` entries in an OpenAPI spec
3
+ * and applies JSON Schema post-processing (null removal, allOf flattening).
4
+ *
5
+ * When `OpenApi.fromApi` generates the spec, different AST nodes sharing the
6
+ * same identifier can produce duplicate entries (e.g. "X" and "X1") in
7
+ * `components.schemas`. This module provides a transform function that
8
+ * collapses those duplicates, rewrites all `$ref` pointers accordingly,
9
+ * and post-processes schemas for better codegen compatibility.
10
+ *
11
+ * Usage with the OpenApi `Transform` annotation:
12
+ *
13
+ * ```ts
14
+ * import { OpenApi } from "effect/unstable"
15
+ * import { deduplicateOpenApiSchemas } from "./SpecialOpenApi.js"
16
+ *
17
+ * const api = HttpApi.make("myApi")
18
+ * .pipe(HttpApi.annotateContext(OpenApi.annotations({ transform: deduplicateOpenApiSchemas })))
19
+ * ```
20
+ */
21
+
22
+ import { postProcessJsonSchema } from "./SpecialJsonSchema.js"
23
+
24
+ /**
25
+ * Deduplicates `components.schemas` entries in an OpenAPI spec.
26
+ *
27
+ * Entries sharing the same base identifier (e.g. "X" and "X1") whose JSON
28
+ * representations are identical are collapsed into a single canonical entry,
29
+ * and all `$ref` pointers throughout the spec are rewritten to point to
30
+ * the canonical key.
31
+ *
32
+ * Designed to be used as the `transform` option in `OpenApi.annotations`.
33
+ */
34
+ export function deduplicateOpenApiSchemas(
35
+ spec: Record<string, any>
36
+ ): Record<string, any> {
37
+ const components = spec["components"] as Record<string, any> | undefined
38
+ if (!components) return spec
39
+ const schemas = components["schemas"] as Record<string, any> | undefined
40
+ if (!schemas) return spec
41
+
42
+ const keys = Object.keys(schemas)
43
+ if (keys.length === 0) return spec
44
+
45
+ // Group keys by base identifier (strip trailing digits)
46
+ const groups = new Map<string, Array<{ key: string; fingerprint: string }>>()
47
+ for (const key of keys) {
48
+ const base = getBaseIdentifier(key)
49
+ const fingerprint = JSON.stringify(schemas[key])
50
+ const group = groups.get(base)
51
+ if (group === undefined) {
52
+ groups.set(base, [{ key, fingerprint }])
53
+ } else {
54
+ group.push({ key, fingerprint })
55
+ }
56
+ }
57
+
58
+ // Build remapping from duplicate keys to canonical keys
59
+ const remapping = new Map<string, string>()
60
+ for (const [, group] of groups) {
61
+ if (group.length <= 1) continue
62
+ const seen = new Map<string, string>() // fingerprint -> canonical key
63
+ for (const entry of group) {
64
+ const canonical = seen.get(entry.fingerprint)
65
+ if (canonical !== undefined) {
66
+ remapping.set(entry.key, canonical)
67
+ } else {
68
+ seen.set(entry.fingerprint, entry.key)
69
+ }
70
+ }
71
+ }
72
+
73
+ if (remapping.size === 0) return postProcessJsonSchema(spec) as Record<string, any>
74
+
75
+ // Build new schemas object without duplicates
76
+ const newSchemas: Record<string, any> = {}
77
+ for (const key of keys) {
78
+ if (!remapping.has(key)) {
79
+ newSchemas[key] = schemas[key]
80
+ }
81
+ }
82
+
83
+ // Deep clone the spec, replace schemas, and rewrite all $ref pointers
84
+ const newSpec = structuredClone(spec)
85
+ newSpec["components"]["schemas"] = newSchemas
86
+ rewriteRefs(newSpec, remapping)
87
+
88
+ return postProcessJsonSchema(newSpec) as Record<string, any>
89
+ }
90
+
91
+ /**
92
+ * Extracts the base identifier from a schema key by stripping trailing
93
+ * digits appended by the gen() function.
94
+ * E.g. "X1" -> "X", "X" -> "X", "MyType2" -> "MyType"
95
+ */
96
+ function getBaseIdentifier(key: string): string {
97
+ const match = key.match(/^(.+?)(\d+)$/)
98
+ return match ? match[1]! : key
99
+ }
100
+
101
+ /**
102
+ * Recursively rewrites `$ref` values in a JSON object tree.
103
+ * Mutates the object in-place (caller should pass a deep clone).
104
+ */
105
+ function rewriteRefs(obj: any, remapping: Map<string, string>): void {
106
+ if (obj === null || typeof obj !== "object") return
107
+
108
+ if (Array.isArray(obj)) {
109
+ for (const item of obj) {
110
+ rewriteRefs(item, remapping)
111
+ }
112
+ return
113
+ }
114
+
115
+ if (typeof obj.$ref === "string") {
116
+ // OpenAPI refs look like "#/components/schemas/X1"
117
+ const prefix = "#/components/schemas/"
118
+ if (obj.$ref.startsWith(prefix)) {
119
+ const refKey = obj.$ref.slice(prefix.length)
120
+ const canonical = remapping.get(refKey)
121
+ if (canonical !== undefined) {
122
+ obj.$ref = prefix + canonical
123
+ }
124
+ }
125
+ }
126
+
127
+ for (const value of Object.values(obj)) {
128
+ rewriteRefs(value, remapping)
129
+ }
130
+ }
@@ -2,7 +2,6 @@
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-return */
3
3
  import type { Option } from "effect"
4
4
  import * as B from "effect/Brand"
5
- import type * as Brand from "effect/Brand"
6
5
  import type * as Result from "effect/Result"
7
6
  import * as S from "effect/Schema"
8
7
 
@@ -21,7 +20,7 @@ export interface Constructor<in out A extends B.Brand<any>> {
21
20
  * Constructs a branded type from a value of type `A`, returning `Result.succeed`
22
21
  * if the provided `A` is valid, `Result.fail` otherwise.
23
22
  */
24
- result(args: Unbranded<A>): Result.Result<A, Brand.BrandError>
23
+ result(args: Unbranded<A>): Result.Result<A, B.BrandError>
25
24
  /**
26
25
  * Attempts to refine the provided value of type `A`, returning `true` if
27
26
  * the provided `A` is valid, `false` otherwise.
@@ -29,19 +28,26 @@ export interface Constructor<in out A extends B.Brand<any>> {
29
28
  is(a: Unbranded<A>): a is Unbranded<A> & A
30
29
  }
31
30
 
32
- export const fromBrand = <C extends Brand.Brand<string>>(
31
+ type BrandAnnotations<C extends B.Brand<any>> =
32
+ & S.Annotations.Filter
33
+ & (
34
+ C extends string ? { readonly toArbitrary?: S.Annotations.ToArbitrary.Declaration<C, readonly []> }
35
+ : {}
36
+ )
37
+
38
+ export const fromBrand = <C extends B.Brand<any>>(
33
39
  constructor: Constructor<C>,
34
- options?: S.Annotations.Filter
40
+ options?: BrandAnnotations<C>
35
41
  ) =>
36
- <Self extends S.Top>(self: Self): S.brand<Self["~rebuild.out"], Brand.Brand.Keys<C>> => {
42
+ <Self extends S.Top>(self: Self): S.brand<Self["Rebuild"], B.Brand.Keys<C>> => {
37
43
  const branded = S.fromBrand(options?.identifier ?? "Brand", constructor as any)(self as any)
38
44
  return options ? (branded as any).pipe(S.annotate(options)) : branded as any
39
45
  }
40
46
 
41
- export type Brands<P> = P extends B.Brand<any> ? Brand.Brand.Brands<P>
47
+ export type Brands<P> = P extends B.Brand<any> ? B.Brand.Brands<P>
42
48
  : never
43
49
 
44
- export type Unbranded<P> = P extends B.Brand<any> ? Brand.Brand.Unbranded<P> : P
50
+ export type Unbranded<P> = P extends B.Brand<any> ? B.Brand.Unbranded<P> : P
45
51
 
46
52
  export const nominal: <A extends B.Brand<any>>() => Constructor<A> = <
47
53
  A extends B.Brand<any>
@@ -12,11 +12,19 @@ export type Email = string & EmailBrand
12
12
  export const Email = S
13
13
  .String
14
14
  .pipe(
15
+ S.annotate({
16
+ title: "Email",
17
+ description: "an email according to RFC 5322",
18
+ format: "email"
19
+ }),
20
+ S.check(S.isMinLength(3), /* a@b */ S.isMaxLength(998)),
15
21
  S.refine(isValidEmail as Refinement<string, Email>, {
16
22
  identifier: "Email",
17
23
  title: "Email",
18
24
  description: "an email according to RFC 5322",
19
- jsonSchema: { format: "email", minLength: 3, /* a@b */ maxLength: 998 },
20
- arbitrary: () => (fc: any) => fc.emailAddress().map((_: any) => _ as Email)
25
+ jsonSchema: { format: "email", minLength: 3, maxLength: 998 }
26
+ }),
27
+ S.annotate({
28
+ toArbitrary: () => (fc) => fc.emailAddress().map((_) => _ as Email)
21
29
  })
22
30
  )
package/src/Schema/ext.ts CHANGED
@@ -1,56 +1,94 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-return */
3
- import { Effect, Option, pipe, Schema, type SchemaAST, SchemaGetter, SchemaIssue, SchemaParser, SchemaTransformation, type ServiceMap } from "effect"
3
+ import { Effect, Option, pipe, type SchemaAST, SchemaIssue, SchemaTransformation } from "effect"
4
4
  import * as S from "effect/Schema"
5
+ import { isDateValid } from "effect/Schema"
5
6
  import { type NonEmptyReadonlyArray } from "../Array.js"
7
+ import * as Context from "../Context.js"
6
8
  import { extendM, typedKeysOf } from "../utils.js"
7
9
  import { type AST } from "./schema.js"
8
10
 
9
- // TODO: v4 migration withConstructorDefault signature changed, propertySignature removed
10
- // Constraint relaxed from `Self extends S.Top & S.WithoutConstructorDefault` to `Self extends S.Top`
11
- // because `.pipe()` widens the schema type to `Top` which doesn't satisfy `WithoutConstructorDefault`.
12
- // The narrowing assertions below are safe — we're asserting "this schema hasn't had a default applied yet".
13
- export const withDefaultConstructor = <A>(
14
- makeDefault: () => NoInfer<A>
15
- ) =>
16
- <Self extends S.Top>(self: Self): S.withConstructorDefault<Self & S.WithoutConstructorDefault> => {
17
- type Narrowed = Self & S.WithoutConstructorDefault
18
- return S.withConstructorDefault<Narrowed>(
19
- () => Option.some(makeDefault() as Narrowed["~type.make.in"])
20
- )(self as Narrowed)
21
- }
11
+ type ProvidedCodec<Self extends S.Top, R> = S.Codec<
12
+ Self["Type"],
13
+ Self["Encoded"],
14
+ Exclude<Self["DecodingServices"], R>,
15
+ Exclude<Self["EncodingServices"], R>
16
+ >
22
17
 
23
18
  // TODO: v4 migration - Date is no longer by default encoded to string.
24
- const DateFromString = Schema.Date.pipe(
25
- Schema.encodeTo(Schema.String, {
26
- decode: SchemaGetter.Date(),
27
- encode: SchemaGetter.transform((_) => _.toISOString())
28
- })
29
- )
19
+
20
+ const DateString = S.String.annotate({
21
+ identifier: "Date",
22
+ description: "a string in ISO 8601 format that will be decoded as a Date",
23
+ format: "date-time"
24
+ })
25
+
26
+ /**
27
+ * Schema type for {@link DateFromString}.
28
+ *
29
+ * @category Schemas
30
+ * @since 4.0.0
31
+ */
32
+ export interface DateFromString extends S.decodeTo<S.Date, S.String> {}
33
+
34
+ /**
35
+ * A transformation schema that parses an ISO 8601 string into a `Date`.
36
+ *
37
+ * Decoding:
38
+ * - A `string` is decoded as a `Date`.
39
+ *
40
+ * Encoding:
41
+ * - A `Date` is encoded as a `string`.
42
+ *
43
+ * @since 4.0.0
44
+ */
45
+ export const DateFromString: DateFromString = DateString.pipe(S.decodeTo(S.Date, SchemaTransformation.dateFromString))
30
46
 
31
47
  /**
32
48
  * Like the default Schema `Date` but from String with `withDefault` => now
33
49
  */
34
50
  export const Date = Object.assign(DateFromString, {
35
- withDefault: DateFromString.pipe(withDefaultConstructor(() => new global.Date()))
51
+ withDefault: DateFromString.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date()))),
52
+ withDecodingDefaultType: DateFromString.pipe(S.withDecodingDefaultType(Effect.sync(() => new global.Date())))
53
+ })
54
+
55
+ /**
56
+ * Like the default Schema `DateValid` but from String with `withDefault` => now
57
+ */
58
+ export const DateValid = Object.assign(Date.check(isDateValid()), {
59
+ withDefault: DateFromString.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date()))),
60
+ withDecodingDefaultType: DateFromString.pipe(S.withDecodingDefaultType(Effect.sync(() => new global.Date())))
36
61
  })
37
62
 
38
63
  /**
39
64
  * Like the default Schema `Boolean` but with `withDefault` => false
40
65
  */
41
66
  export const Boolean = Object.assign(S.Boolean, {
42
- withDefault: S.Boolean.pipe(withDefaultConstructor(() => false))
67
+ withDefault: S.Boolean.pipe(S.withConstructorDefault(Effect.succeed(false))),
68
+ withDecodingDefaultType: S.Boolean.pipe(S.withDecodingDefaultType(Effect.succeed(false)))
43
69
  })
44
70
 
45
71
  /**
72
+ * You probably want to use `Finite` instead of this.
46
73
  * Like the default Schema `Number` but with `withDefault` => 0
47
74
  */
48
- export const Number = Object.assign(S.Number, { withDefault: S.Number.pipe(withDefaultConstructor(() => 0)) })
75
+ export const Number = Object.assign(S.Number, {
76
+ withDefault: S.Number.pipe(S.withConstructorDefault(Effect.succeed(0))),
77
+ withDecodingDefaultType: S.Number.pipe(S.withDecodingDefaultType(Effect.succeed(0)))
78
+ })
79
+
80
+ /**
81
+ * Like the default Schema `Finite` but with `withDefault` => 0
82
+ */
83
+ export const Finite = Object.assign(S.Finite, {
84
+ withDefault: S.Finite.pipe(S.withConstructorDefault(Effect.succeed(0))),
85
+ withDecodingDefaultType: S.Finite.pipe(S.withDecodingDefaultType(Effect.succeed(0)))
86
+ })
49
87
 
50
88
  /**
51
- * Like the default Schema `Literal` but with `withDefault` => literals[0]
89
+ * Like the default Schema `Literals` but with `withDefault` => literals[0]
52
90
  */
53
- export const Literal = <Literals extends NonEmptyReadonlyArray<AST.LiteralValue>>(...literals: Literals) =>
91
+ export const Literals = <const Literals extends NonEmptyReadonlyArray<AST.LiteralValue>>(literals: Literals) =>
54
92
  pipe(
55
93
  S.Literals(literals),
56
94
  (s) =>
@@ -58,79 +96,140 @@ export const Literal = <Literals extends NonEmptyReadonlyArray<AST.LiteralValue>
58
96
  changeDefault: <A extends Literals[number]>(a: A) => {
59
97
  return Object.assign(S.Literals(literals), {
60
98
  Default: a,
61
- withDefault: s.pipe(withDefaultConstructor(() => a))
99
+ withDefault: s.pipe(S.withConstructorDefault(Effect.succeed(a))),
100
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(a)))
62
101
  }) // todo: copy annotations from original?
63
102
  },
64
103
  Default: literals[0] as typeof literals[0],
65
- withDefault: s.pipe(withDefaultConstructor(() => literals[0]))
104
+ withDefault: s.pipe(S.withConstructorDefault(Effect.succeed(literals[0]))),
105
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(literals[0])))
66
106
  })
67
107
  )
68
108
 
69
109
  /**
70
110
  * Like the default Schema `Array` but with `withDefault` => []
71
111
  */
72
- export function Array<Value extends S.Top>(value: Value) {
112
+ export function Array<ValueSchema extends S.Top>(value: ValueSchema) {
73
113
  return pipe(
74
114
  S.Array(value),
75
- (s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => [])) })
115
+ (s) =>
116
+ Object.assign(s, {
117
+ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => []))),
118
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => [])))
119
+ })
76
120
  )
77
121
  }
78
122
 
79
123
  /**
80
- * Like the default Schema `Map` but with `withDefault` => []
124
+ * An annotated `S.Array` of unique items that decodes to a `ReadonlySet`.
81
125
  */
82
- function Map_<Key extends S.Top, Value extends S.Top>(input: { key: Key; value: Value }) {
83
- return pipe(
84
- S.ReadonlyMap(input.key, input.value),
85
- (s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => new global.Map())) })
126
+ export const ReadonlySetFromArray = <ValueSchema extends S.Top>(value: ValueSchema) => {
127
+ const from = S
128
+ .Array(value)
129
+ .annotate({ expected: "an array of unique items that will be decoded as a ReadonlySet" })
130
+ const to = S.instanceOf(Set) as S.instanceOf<ReadonlySet<S.Schema.Type<ValueSchema>>>
131
+ const schema = from.pipe(
132
+ S.decodeTo(
133
+ to,
134
+ SchemaTransformation.transform({
135
+ decode: (arr) => new Set(arr) as ReadonlySet<S.Schema.Type<ValueSchema>>,
136
+ encode: (set) => [...set]
137
+ })
138
+ )
86
139
  )
140
+ return schema
87
141
  }
88
142
 
89
- export { Map_ as Map }
143
+ /**
144
+ * An annotated `S.Array` of key-value tuples that decodes to a `ReadonlyMap`.
145
+ */
146
+ export const ReadonlyMapFromArray = <KeySchema extends S.Top, ValueSchema extends S.Top>(pair: {
147
+ readonly key: KeySchema
148
+ readonly value: ValueSchema
149
+ }) => {
150
+ const from = S
151
+ .Array(S.Tuple([pair.key, pair.value]))
152
+ .annotate({ expected: "an array of key-value tuples that will be decoded as a ReadonlyMap" })
153
+ const to = S.instanceOf(Map) as S.instanceOf<
154
+ ReadonlyMap<S.Schema.Type<KeySchema>, S.Schema.Type<ValueSchema>>
155
+ >
156
+ const schema = from.pipe(
157
+ S.decodeTo(
158
+ to,
159
+ SchemaTransformation.transform({
160
+ decode: (
161
+ arr
162
+ ) => new Map(arr) as ReadonlyMap<S.Schema.Type<KeySchema>, S.Schema.Type<ValueSchema>>,
163
+ encode: (
164
+ map
165
+ ) => [...map.entries()] as any // fu
166
+ })
167
+ )
168
+ )
169
+ return schema
170
+ }
90
171
 
91
172
  /**
92
- * Like the default Schema `ReadonlySet` but with `withDefault` => new Set()
173
+ * Like the default Schema `ReadonlySet` but from Array with `withDefault` => new Set()
93
174
  */
94
- export const ReadonlySet = <Value extends S.Top>(value: Value) =>
175
+ export const ReadonlySet = <ValueSchema extends S.Top>(value: ValueSchema) =>
95
176
  pipe(
96
- S.ReadonlySet(value),
97
- (s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => new Set<S.Schema.Type<Value>>())) })
177
+ ReadonlySetFromArray(value),
178
+ (s) =>
179
+ Object.assign(s, {
180
+ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new Set<S.Schema.Type<ValueSchema>>()))),
181
+ withDecodingDefaultType: s.pipe(
182
+ S.withDecodingDefaultType(Effect.sync(() => new Set<S.Schema.Type<ValueSchema>>()))
183
+ )
184
+ })
98
185
  )
99
186
 
100
187
  /**
101
- * Like the default Schema `ReadonlyMap` but with `withDefault` => new Map()
188
+ * Like the default Schema `ReadonlyMap` but from Array with `withDefault` => new Map()
102
189
  */
103
- export const ReadonlyMap = <K extends S.Top, V extends S.Top>(pair: {
104
- readonly key: K
105
- readonly value: V
190
+ export const ReadonlyMap = <KeySchema extends S.Top, ValueSchema extends S.Top>(pair: {
191
+ readonly key: KeySchema
192
+ readonly value: ValueSchema
106
193
  }) =>
107
194
  pipe(
108
- S.ReadonlyMap(pair.key, pair.value),
109
- (s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => new Map())) })
195
+ ReadonlyMapFromArray(pair),
196
+ (s) =>
197
+ Object.assign(s, {
198
+ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new Map()))),
199
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => new Map())))
200
+ })
110
201
  )
111
202
 
112
203
  /**
113
204
  * Like the default Schema `NullOr` but with `withDefault` => null
114
205
  */
115
- export const NullOr = <S extends S.Top>(self: S) =>
206
+ export const NullOr = <Schema extends S.Top>(self: Schema) =>
116
207
  pipe(
117
208
  S.NullOr(self),
118
- (s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => null)) })
209
+ (s) =>
210
+ Object.assign(s, {
211
+ withDefault: s.pipe(S.withConstructorDefault(Effect.succeed(null))),
212
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(null)))
213
+ })
119
214
  )
120
215
 
121
- export const defaultDate = (s: S.Top) => s.pipe(withDefaultConstructor(() => new global.Date()))
216
+ export const defaultDate = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
217
+ schema.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date())))
122
218
 
123
- export const defaultBool = (s: S.Top) => s.pipe(withDefaultConstructor(() => false))
219
+ export const defaultBool = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
220
+ schema.pipe(S.withConstructorDefault(Effect.succeed(false)))
124
221
 
125
- export const defaultNullable = (
126
- s: S.Top
127
- ) => s.pipe(withDefaultConstructor(() => null))
222
+ export const defaultNullable = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
223
+ schema.pipe(S.withConstructorDefault(Effect.succeed(null)))
128
224
 
129
- export const defaultArray = (s: S.Top) => s.pipe(withDefaultConstructor(() => []))
225
+ export const defaultArray = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
226
+ schema.pipe(S.withConstructorDefault(Effect.sync(() => [])))
130
227
 
131
- export const defaultMap = (s: S.Top) => s.pipe(withDefaultConstructor(() => new Map()))
228
+ export const defaultMap = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
229
+ schema.pipe(S.withConstructorDefault(Effect.sync(() => new Map())))
132
230
 
133
- export const defaultSet = (s: S.Top) => s.pipe(withDefaultConstructor(() => new Set()))
231
+ export const defaultSet = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
232
+ schema.pipe(S.withConstructorDefault(Effect.sync(() => new Set())))
134
233
 
135
234
  export const withDefaultMake = <Self extends S.Top>(s: Self) => {
136
235
  const a = Object.assign(S.decodeSync(s as any) as WithDefaults<Self>, s)
@@ -160,8 +259,11 @@ export type WithDefaults<Self extends S.Top> = (
160
259
  // : never
161
260
 
162
261
  export const inputDate = extendM(
163
- S.Union([S.DateValid, S.Date]),
164
- (s) => ({ withDefault: s.pipe(withDefaultConstructor(() => new globalThis.Date())) })
262
+ S.Union([S.DateValid, Date]),
263
+ (s) => ({
264
+ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new globalThis.Date()))),
265
+ withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => new globalThis.Date())))
266
+ })
165
267
  )
166
268
 
167
269
  export interface UnionBrand {}
@@ -211,7 +313,7 @@ export const transformTo = <To extends S.Top, From extends S.Top>(
211
313
  { message: "One way schema transformation, encoding is not allowed" }
212
314
  )
213
315
  )
214
- }) as any
316
+ })
215
317
  )
216
318
  )
217
319
 
@@ -228,7 +330,7 @@ export const transformToOrFail = <To extends S.Top, From extends S.Top, RD>(
228
330
  S.decodeTo(
229
331
  to,
230
332
  SchemaTransformation.transformOrFail({
231
- decode: decode as any,
333
+ decode,
232
334
  encode: (i: any) =>
233
335
  Effect.fail(
234
336
  new SchemaIssue.Forbidden(
@@ -236,33 +338,34 @@ export const transformToOrFail = <To extends S.Top, From extends S.Top, RD>(
236
338
  { message: "One way schema transformation, encoding is not allowed" }
237
339
  )
238
340
  )
239
- }) as any
341
+ })
240
342
  )
241
343
  )
242
344
 
243
- // TODO: v4 migration — S.declare API changed (no [self] + decode/encode pattern)
244
- // Need to find v4 equivalent for contextual schema wrapping
245
345
  export const provide = <Self extends S.Top, R>(
246
346
  self: Self,
247
- context: ServiceMap.ServiceMap<R>
248
- ): any => {
347
+ context: Context.Context<R>
348
+ ): ProvidedCodec<Self, R> => {
249
349
  const prov = Effect.provide(context)
250
- return S
251
- .declare((_u: unknown): _u is unknown => true) // placeholder — needs proper v4 declare
252
- .pipe(
253
- S.decodeTo(
254
- self,
255
- SchemaTransformation.transformOrFail({
256
- decode: (n: any) => prov(SchemaParser.decodeUnknownEffect(self)(n)),
257
- encode: (n: any) => prov(SchemaParser.encodeUnknownEffect(self)(n))
258
- }) as any
259
- ) as any
260
- )
261
- }
262
- // TODO: v4 migration — ServiceMap.pick and S.declare pattern removed
263
- export const contextFromServices = <Self extends S.Top, Tags extends readonly any[]>(
264
- _self: Self,
265
- ..._services: Tags
266
- ): any => {
267
- throw new Error("contextFromServices: not yet migrated to v4")
350
+ return self.pipe(
351
+ S.middlewareDecoding((effect) => prov(effect)),
352
+ S.middlewareEncoding((effect) => prov(effect))
353
+ ) as ProvidedCodec<Self, R>
268
354
  }
355
+ export const contextFromServices = <
356
+ Self extends S.Top,
357
+ Tags extends ReadonlyArray<Context.Key<any, any>>
358
+ >(
359
+ self: Self,
360
+ ...services: Tags
361
+ ): Effect.Effect<
362
+ ProvidedCodec<Self, Context.Service.Identifier<Tags[number]>>,
363
+ never,
364
+ Context.Service.Identifier<Tags[number]>
365
+ > =>
366
+ Effect.gen(function*() {
367
+ const context: Context.Context<Context.Service.Identifier<Tags[number]>> = Context.pick(...services)(
368
+ yield* Effect.context<Context.Service.Identifier<Tags[number]>>()
369
+ )
370
+ return provide(self, context)
371
+ })