@nmnmcc/ability 1.0.0 → 2.0.0

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 (127) hide show
  1. package/README.md +143 -87
  2. package/dist/Authorizer.d.ts +43 -47
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +5 -290
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +164 -332
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +75 -890
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Decision.d.ts +13 -13
  11. package/dist/Decision.d.ts.map +1 -1
  12. package/dist/Decision.js.map +1 -1
  13. package/dist/Domain.d.ts +10 -21
  14. package/dist/Domain.d.ts.map +1 -1
  15. package/dist/Domain.js +4 -62
  16. package/dist/Domain.js.map +1 -1
  17. package/dist/HttpApi.d.ts +31 -7
  18. package/dist/HttpApi.d.ts.map +1 -1
  19. package/dist/HttpApi.js +28 -11
  20. package/dist/HttpApi.js.map +1 -1
  21. package/dist/Limits.d.ts +114 -0
  22. package/dist/Limits.d.ts.map +1 -0
  23. package/dist/Limits.js +132 -0
  24. package/dist/Limits.js.map +1 -0
  25. package/dist/Policy.d.ts +274 -272
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +71 -1180
  28. package/dist/Policy.js.map +1 -1
  29. package/dist/Resource.d.ts +8 -181
  30. package/dist/Resource.d.ts.map +1 -1
  31. package/dist/Resource.js +8 -807
  32. package/dist/Resource.js.map +1 -1
  33. package/dist/index.d.ts +3 -4
  34. package/dist/index.js +4 -5
  35. package/dist/internal/algebra.js +267 -0
  36. package/dist/internal/algebra.js.map +1 -0
  37. package/dist/internal/analysis.js +245 -0
  38. package/dist/internal/analysis.js.map +1 -0
  39. package/dist/internal/authorizer.d.ts +30 -0
  40. package/dist/internal/authorizer.d.ts.map +1 -0
  41. package/dist/internal/authorizer.js +221 -0
  42. package/dist/internal/authorizer.js.map +1 -0
  43. package/dist/internal/bind.js +286 -0
  44. package/dist/internal/bind.js.map +1 -0
  45. package/dist/internal/catalog.d.ts +56 -0
  46. package/dist/internal/catalog.d.ts.map +1 -0
  47. package/dist/internal/catalog.js +292 -0
  48. package/dist/internal/catalog.js.map +1 -0
  49. package/dist/internal/decide.js +166 -0
  50. package/dist/internal/decide.js.map +1 -0
  51. package/dist/internal/domain.js +32 -0
  52. package/dist/internal/domain.js.map +1 -0
  53. package/dist/internal/json.js +134 -0
  54. package/dist/internal/json.js.map +1 -0
  55. package/dist/internal/limits.d.ts +14 -0
  56. package/dist/internal/limits.d.ts.map +1 -0
  57. package/dist/internal/limits.js +43 -0
  58. package/dist/internal/limits.js.map +1 -0
  59. package/dist/internal/nodes.d.ts +121 -0
  60. package/dist/internal/nodes.d.ts.map +1 -0
  61. package/dist/internal/nodes.js +277 -0
  62. package/dist/internal/nodes.js.map +1 -0
  63. package/dist/internal/patterns.js +110 -0
  64. package/dist/internal/patterns.js.map +1 -0
  65. package/dist/internal/policy-json.js +255 -0
  66. package/dist/internal/policy-json.js.map +1 -0
  67. package/dist/internal/policy-nodes.d.ts +61 -0
  68. package/dist/internal/policy-nodes.d.ts.map +1 -0
  69. package/dist/internal/policy-nodes.js +146 -0
  70. package/dist/internal/policy-nodes.js.map +1 -0
  71. package/dist/internal/policy.d.ts +42 -0
  72. package/dist/internal/policy.d.ts.map +1 -0
  73. package/dist/internal/policy.js +124 -0
  74. package/dist/internal/policy.js.map +1 -0
  75. package/dist/internal/read.js +39 -0
  76. package/dist/internal/read.js.map +1 -0
  77. package/dist/internal/resource.js +115 -0
  78. package/dist/internal/resource.js.map +1 -0
  79. package/dist/internal/rule.d.ts +26 -0
  80. package/dist/internal/rule.d.ts.map +1 -0
  81. package/dist/internal/rule.js +93 -0
  82. package/dist/internal/rule.js.map +1 -0
  83. package/dist/internal/search.d.ts +22 -0
  84. package/dist/internal/search.d.ts.map +1 -0
  85. package/dist/internal/search.js +274 -0
  86. package/dist/internal/search.js.map +1 -0
  87. package/dist/internal/suggest.js +46 -0
  88. package/dist/internal/suggest.js.map +1 -0
  89. package/package.json +4 -5
  90. package/src/Authorizer.ts +69 -585
  91. package/src/Condition.ts +339 -2095
  92. package/src/Decision.ts +6 -5
  93. package/src/Domain.ts +13 -70
  94. package/src/HttpApi.ts +75 -13
  95. package/src/Limits.ts +132 -0
  96. package/src/Policy.ts +386 -2772
  97. package/src/Resource.ts +14 -1555
  98. package/src/index.ts +3 -9
  99. package/src/internal/algebra.ts +482 -0
  100. package/src/internal/analysis.ts +478 -0
  101. package/src/internal/authorizer.ts +418 -0
  102. package/src/internal/bind.ts +596 -0
  103. package/src/internal/catalog.ts +534 -0
  104. package/src/internal/decide.ts +402 -0
  105. package/src/internal/domain.ts +66 -0
  106. package/src/internal/json.ts +260 -0
  107. package/src/internal/limits.ts +78 -0
  108. package/src/internal/nodes.ts +404 -0
  109. package/src/internal/patterns.ts +211 -0
  110. package/src/internal/policy-json.ts +484 -0
  111. package/src/internal/policy-nodes.ts +207 -0
  112. package/src/internal/policy.ts +264 -0
  113. package/src/internal/read.ts +55 -0
  114. package/src/internal/resource.ts +180 -0
  115. package/src/internal/rule.ts +181 -0
  116. package/src/internal/search.ts +541 -0
  117. package/src/internal/suggest.ts +104 -0
  118. package/dist/Principal.d.ts +0 -35
  119. package/dist/Principal.d.ts.map +0 -1
  120. package/dist/Principal.js +0 -37
  121. package/dist/Principal.js.map +0 -1
  122. package/dist/Rule.d.ts +0 -275
  123. package/dist/Rule.d.ts.map +0 -1
  124. package/dist/Rule.js +0 -257
  125. package/dist/Rule.js.map +0 -1
  126. package/src/Principal.ts +0 -41
  127. package/src/Rule.ts +0 -683
package/src/Condition.ts CHANGED
@@ -31,20 +31,24 @@ import type * as Cause from "effect/Cause"
31
31
  import type * as Chunk from "effect/Chunk"
32
32
  import type * as DateTime from "effect/DateTime"
33
33
  import type * as Duration from "effect/Duration"
34
+ import type * as Exit from "effect/Exit"
35
+ import type * as HashMap from "effect/HashMap"
36
+ import type * as HashSet from "effect/HashSet"
37
+ import type * as Option from "effect/Option"
34
38
  import type * as Redacted from "effect/Redacted"
35
- import type * as Result from "effect/Result"
36
39
  import type * as Context from "effect/Context"
37
- import * as Data from "effect/Data"
38
40
  import * as Effect from "effect/Effect"
39
- import * as Option from "effect/Option"
40
41
  import { dual } from "effect/Function"
41
- import * as HashMap from "effect/HashMap"
42
- import * as HashSet from "effect/HashSet"
43
- import * as MutableHashMap from "effect/MutableHashMap"
44
- import { hasProperty, isObject } from "effect/Predicate"
42
+ import { hasProperty } from "effect/Predicate"
45
43
  import * as Schema from "effect/Schema"
46
44
  import * as SchemaIssue from "effect/SchemaIssue"
47
45
  import * as SchemaTransformation from "effect/SchemaTransformation"
46
+ import * as Result from "effect/Result"
47
+ import * as Algebra from "./internal/algebra.ts"
48
+ import * as Catalog from "./internal/catalog.ts"
49
+ import * as Json from "./internal/json.ts"
50
+ import * as Nodes from "./internal/nodes.ts"
51
+ import * as Search from "./internal/search.ts"
48
52
  import * as Resource from "./Resource.ts"
49
53
 
50
54
  /**
@@ -54,7 +58,7 @@ import * as Resource from "./Resource.ts"
54
58
  * @category models
55
59
  * @since 0.3.0
56
60
  */
57
- export type Scalar = string | number | boolean | null
61
+ export type Scalar = Nodes.Scalar
58
62
 
59
63
  /**
60
64
  * A fixed value in a condition.
@@ -63,10 +67,7 @@ export type Scalar = string | number | boolean | null
63
67
  * @category models
64
68
  * @since 0.3.0
65
69
  */
66
- export interface Literal {
67
- readonly _tag: "Literal"
68
- readonly value: Scalar
69
- }
70
+ export type Literal = Nodes.Literal
70
71
 
71
72
  /**
72
73
  * A field of the principal. {@link partial} puts the value of the field in its
@@ -81,18 +82,13 @@ export interface Literal {
81
82
  * @category models
82
83
  * @since 0.3.0
83
84
  */
84
- export interface PrincipalRef {
85
- readonly _tag: "Principal"
86
- readonly path: string
87
- }
85
+ export type PrincipalRef = Nodes.PrincipalRef
88
86
 
89
87
  /**
90
88
  * A field of the principal with a known value type `A`, from a principal of
91
- * the type `P`. The type fields are only in the types. A reference with no
92
- * type does not have them, so it can not stand for a field of any type. A
93
- * test with the reference reads a principal of the type `P`, so it can not go
94
- * into a rule of a domain with another principal. With no `P`, it is `never`,
95
- * and a test with the reference fits no rule.
89
+ * the type `P`. The type fields are only in the types. A test with the
90
+ * reference reads a principal of the type `P`, so it can not go into a rule
91
+ * of a domain with another principal.
96
92
  *
97
93
  * @stability stable
98
94
  * @category models
@@ -115,7 +111,7 @@ export type Operand = Literal | PrincipalRef
115
111
  * @category models
116
112
  * @since 0.3.0
117
113
  */
118
- export type CompareOperator = "eq" | "ne" | "lt" | "lte" | "gt" | "gte"
114
+ export type CompareOperator = Nodes.CompareOperator
119
115
 
120
116
  /**
121
117
  * Always true.
@@ -124,9 +120,7 @@ export type CompareOperator = "eq" | "ne" | "lt" | "lte" | "gt" | "gte"
124
120
  * @category models
125
121
  * @since 0.3.0
126
122
  */
127
- export interface True {
128
- readonly _tag: "True"
129
- }
123
+ export type True = Nodes.True
130
124
 
131
125
  /**
132
126
  * Always false.
@@ -135,9 +129,7 @@ export interface True {
135
129
  * @category models
136
130
  * @since 0.3.0
137
131
  */
138
- export interface False {
139
- readonly _tag: "False"
140
- }
132
+ export type False = Nodes.False
141
133
 
142
134
  /**
143
135
  * Compares a field with a value. A missing field has the value `null`. `lt`,
@@ -148,12 +140,7 @@ export interface False {
148
140
  * @category models
149
141
  * @since 0.3.0
150
142
  */
151
- export interface Compare {
152
- readonly _tag: "Compare"
153
- readonly op: CompareOperator
154
- readonly field: string
155
- readonly value: Operand
156
- }
143
+ export type Compare = Nodes.Compare
157
144
 
158
145
  /**
159
146
  * True when a field is equal to one of the values.
@@ -162,11 +149,7 @@ export interface Compare {
162
149
  * @category models
163
150
  * @since 0.3.0
164
151
  */
165
- export interface In {
166
- readonly _tag: "In"
167
- readonly field: string
168
- readonly values: ReadonlyArray<Operand>
169
- }
152
+ export type In = Nodes.In
170
153
 
171
154
  /**
172
155
  * True when a field is not `null` and not missing.
@@ -175,10 +158,7 @@ export interface In {
175
158
  * @category models
176
159
  * @since 0.3.0
177
160
  */
178
- export interface Exists {
179
- readonly _tag: "Exists"
180
- readonly field: string
181
- }
161
+ export type Exists = Nodes.Exists
182
162
 
183
163
  /**
184
164
  * True when the path of the resource starts with the prefix. A resource with
@@ -188,40 +168,28 @@ export interface Exists {
188
168
  * @category models
189
169
  * @since 0.3.0
190
170
  */
191
- export interface Under {
192
- readonly _tag: "Under"
193
- readonly prefix: ReadonlyArray<Operand>
194
- }
171
+ export type Under = Nodes.Under
195
172
 
196
173
  /**
197
174
  * @stability stable
198
175
  * @category models
199
176
  * @since 0.3.0
200
177
  */
201
- export interface And {
202
- readonly _tag: "And"
203
- readonly conditions: ReadonlyArray<Ast>
204
- }
178
+ export type And = Nodes.And
205
179
 
206
180
  /**
207
181
  * @stability stable
208
182
  * @category models
209
183
  * @since 0.3.0
210
184
  */
211
- export interface Or {
212
- readonly _tag: "Or"
213
- readonly conditions: ReadonlyArray<Ast>
214
- }
185
+ export type Or = Nodes.Or
215
186
 
216
187
  /**
217
188
  * @stability stable
218
189
  * @category models
219
190
  * @since 0.3.0
220
191
  */
221
- export interface Not {
222
- readonly _tag: "Not"
223
- readonly condition: Ast
224
- }
192
+ export type Not = Nodes.Not
225
193
 
226
194
  /**
227
195
  * A test that runs an `Effect`. It can not become JSON or a query.
@@ -230,168 +198,104 @@ export interface Not {
230
198
  * @category models
231
199
  * @since 0.3.0
232
200
  */
233
- export interface Predicate {
234
- readonly _tag: "Predicate"
235
- readonly name: string
236
- // A method, so the typed `run` of `PredicateCondition`, with inputs of the
237
- // type `never`, is a `run` of this node too.
238
- run(value: any, principal: any): Effect.Effect<boolean, unknown, unknown>
239
- }
201
+ export type Predicate = Nodes.Predicate
240
202
 
241
203
  /**
242
- * The nodes of a condition, without types.
204
+ * The nodes of a condition, without types. Each node is a class, and only the
205
+ * builder, the functions of this module and the Schema make one. So a plain
206
+ * object, or a spread of a node, is not a node.
243
207
  *
244
208
  * @stability stable
245
209
  * @category models
246
210
  * @since 0.3.0
247
211
  */
248
- export type Ast =
249
- True | False | Compare | In | Exists | Under | And | Or | Not | Predicate
212
+ export type Ast = Nodes.Node
250
213
 
251
214
  /**
252
- * The type marker for a condition with a {@link Predicate}.
215
+ * The mark of a condition with a predicate. `E` and `R` are the error type
216
+ * and the service type of its predicates.
253
217
  *
254
218
  * @stability stable
255
219
  * @category models
256
- * @since 0.3.0
257
- */
258
- export type Opaque = "Opaque"
259
-
260
- /**
261
- * @stability stable
262
- * @category models
263
- * @since 0.3.0
264
- */
265
- export interface Variance<E, R, O> {
266
- readonly E: () => E
267
- readonly R: () => R
268
- readonly O: () => O
269
- }
270
-
271
- /**
272
- * What the predicates of a condition read: a resource value of the type `V`
273
- * and a principal of the type `P`. It is only in the types.
274
- *
275
- * @stability stable
276
- * @category models
277
- * @since 1.0.0
220
+ * @since 2.0.0
278
221
  */
279
- export interface Reads<V, P> {
280
- readonly value: V
281
- readonly principal: P
222
+ export interface Opaque<out E = never, out R = never> {
223
+ readonly "~Opaque": { readonly E: E; readonly R: R }
282
224
  }
283
225
 
284
226
  /**
285
- * The type fields of a condition. They are only in the types.
227
+ * The error type of the predicates of a mark `X`. A mark that may be
228
+ * anything, as `unknown`, may have any error.
286
229
  *
287
230
  * @stability stable
288
- * @category models
289
- * @since 1.0.0
231
+ * @category utility types
232
+ * @since 2.0.0
290
233
  */
291
- export interface Marks<E, R, O, I> {
292
- readonly "~Condition": Variance<E, R, O>
293
- /** `I` is what the predicates read. See {@link Reads}. */
294
- readonly "~Reads": (reads: I) => void
295
- }
234
+ export type ErrorOf<X> = unknown extends X
235
+ ? unknown
236
+ : X extends Opaque<infer E, infer _R>
237
+ ? E
238
+ : never
296
239
 
297
240
  /**
298
- * An `And` node with the types of its children.
241
+ * The service type of the predicates of a mark `X`. A mark that may be
242
+ * anything, as `unknown`, may need any service.
299
243
  *
300
244
  * @stability stable
301
- * @category models
302
- * @since 1.0.0
245
+ * @category utility types
246
+ * @since 2.0.0
303
247
  */
304
- export interface AndCondition<E, R, O, I> extends Marks<E, R, O, I> {
305
- readonly _tag: "And"
306
- readonly conditions: ReadonlyArray<Condition<E, R, O, I>>
307
- }
248
+ export type ServicesOf<X> = unknown extends X
249
+ ? unknown
250
+ : X extends Opaque<infer _E, infer R>
251
+ ? R
252
+ : never
308
253
 
309
254
  /**
310
- * An `Or` node with the types of its children.
255
+ * The mark `X` with no services.
311
256
  *
312
257
  * @stability stable
313
- * @category models
314
- * @since 1.0.0
258
+ * @category utility types
259
+ * @since 2.0.0
315
260
  */
316
- export interface OrCondition<E, R, O, I> extends Marks<E, R, O, I> {
317
- readonly _tag: "Or"
318
- readonly conditions: ReadonlyArray<Condition<E, R, O, I>>
319
- }
261
+ export type Provided<X> = unknown extends X
262
+ ? Opaque<unknown>
263
+ : X extends Opaque<infer E, infer _R>
264
+ ? Opaque<E>
265
+ : never
320
266
 
321
267
  /**
322
- * A `Not` node with the types of its child.
268
+ * What the predicates of a condition read: a resource value of the type `V`
269
+ * and a principal of the type `P`. It is only in the types.
323
270
  *
324
271
  * @stability stable
325
272
  * @category models
326
273
  * @since 1.0.0
327
274
  */
328
- export interface NotCondition<E, R, O, I> extends Marks<E, R, O, I> {
329
- readonly _tag: "Not"
330
- readonly condition: Condition<E, R, O, I>
275
+ export interface Reads<V, P> {
276
+ readonly value: V
277
+ readonly principal: P
331
278
  }
332
279
 
333
280
  /**
334
- * A predicate node with its types. `"~Predicate"` is only in the types. It has
335
- * the type `O`, so a plain condition, with `O` as `never`, can not be a
336
- * predicate. The builder makes predicates with `O` as {@link Opaque}.
281
+ * A condition. `I` is what its tests read: see {@link Reads}. `X` is
282
+ * {@link Opaque} when the condition has a predicate, and `never` when it is
283
+ * plain data. A plain condition can become JSON or a query.
337
284
  *
338
- * @stability stable
339
- * @category models
340
- * @since 1.0.0
341
- */
342
- export interface PredicateCondition<E, R, O, I> extends Marks<E, R, O, I> {
343
- readonly _tag: "Predicate"
344
- readonly name: string
345
- /**
346
- * The test. Its inputs have the type `never`, so it can not be called with
347
- * a value that it does not read. Use {@link evaluate}, which checks the
348
- * input against `I`.
349
- */
350
- readonly run: PredicateRun<E, R, I>
351
- readonly "~Predicate": O
352
- }
353
-
354
- /**
355
- * The test of a predicate node. The type field `"~Reads"` is only in the
356
- * types. It ties the test to what it reads, so the test of one predicate can
357
- * not go into a node that says it reads other values.
285
+ * The two type fields are only in the types. `I` goes into a function as an
286
+ * input, so a condition that reads a `Post` does not fit a rule for a
287
+ * `Comment`. `X` comes out of a function, so a plain condition fits where a
288
+ * condition with predicates may go.
358
289
  *
359
290
  * @stability stable
360
291
  * @category models
361
- * @since 1.0.0
292
+ * @since 0.3.0
362
293
  */
363
- export type PredicateRun<E, R, I> = ((
364
- value: never,
365
- principal: never,
366
- ) => Effect.Effect<boolean, E, R>) & {
367
- // Required, so a plain function, with no such field, is not the test of a
368
- // predicate. Only the builder makes one.
294
+ export type Condition<I = unknown, X = never> = Ast & {
369
295
  readonly "~Reads": (reads: I) => void
296
+ readonly "~Opaque": () => X
370
297
  }
371
298
 
372
- /**
373
- * A condition. `E` and `R` are the error type and the service type of its
374
- * predicates. `O` is {@link Opaque} when the condition has a predicate, and
375
- * `never` when it is plain data. `I` is what its predicates read. See
376
- * {@link Reads}.
377
- *
378
- * The type fields are only in the types. A plain node made by hand does not
379
- * have them, so it is not a condition. Use the builder. Each child of a node
380
- * is a condition with the same types, so a node can not hide the types of a
381
- * predicate inside it. A predicate is a condition only when `O` is
382
- * {@link Opaque}.
383
- *
384
- * @stability stable
385
- * @category models
386
- * @since 0.3.0
387
- */
388
- export type Condition<E = never, R = never, O = never, I = unknown> =
389
- | ((True | False | Compare | In | Exists | Under) & Marks<E, R, O, I>)
390
- | AndCondition<E, R, O, I>
391
- | OrCondition<E, R, O, I>
392
- | NotCondition<E, R, O, I>
393
- | PredicateCondition<E, R, O, I>
394
-
395
299
  /**
396
300
  * Any condition.
397
301
  *
@@ -399,37 +303,11 @@ export type Condition<E = never, R = never, O = never, I = unknown> =
399
303
  * @category models
400
304
  * @since 0.3.0
401
305
  */
402
- export type Any = Condition<any, any, any, any>
403
-
404
- /**
405
- * The error type of a condition.
406
- *
407
- * @stability stable
408
- * @category utility types
409
- * @since 0.3.0
410
- */
411
- export type Error<C> = C extends {
412
- readonly "~Condition"?: Variance<infer E, infer _R, infer _O>
413
- }
414
- ? E
415
- : never
416
-
417
- /**
418
- * The service type of a condition.
419
- *
420
- * @stability stable
421
- * @category utility types
422
- * @since 0.3.0
423
- */
424
- export type Services<C> = C extends {
425
- readonly "~Condition"?: Variance<infer _E, infer R, infer _O>
426
- }
427
- ? R
428
- : never
306
+ export type Any = Condition<any, any>
429
307
 
430
308
  /**
431
- * What the predicates of a condition read. For a union of conditions, it is
432
- * what all of them read.
309
+ * What the tests of a condition read. For a union of conditions, it is what
310
+ * all of them read.
433
311
  *
434
312
  * @stability stable
435
313
  * @category utility types
@@ -442,41 +320,14 @@ export type ReadsOf<C> = [C] extends [
442
320
  : unknown
443
321
 
444
322
  /**
445
- * `unknown` when a condition is a condition of its own type fields: each of
446
- * its children has them too. A node made with a spread may keep the type
447
- * fields of another node, and then it is `never`, so the call stops.
448
- *
449
- * @stability stable
450
- * @category utility types
451
- * @since 1.0.0
452
- */
453
- export type Checked<C> = [NotChecked<C>] extends [never] ? unknown : never
454
-
455
- /**
456
- * The members of a union of conditions that do not fit their own type fields.
457
- * Each member is checked alone, so a good member does not hide a bad one.
458
- */
459
- type NotChecked<C> =
460
- C extends Condition<Error<C>, Services<C>, OpaqueOf<C>, ReadsOf<C>>
461
- ? never
462
- : C
463
-
464
- /** Each condition, checked with {@link Checked}. */
465
- type Consistent<Cs extends ReadonlyArray<Any>> = {
466
- readonly [K in keyof Cs]: Cs[K] & Checked<Cs[K]>
467
- }
468
-
469
- /**
470
- * {@link Opaque} when a condition has a predicate.
323
+ * The mark of a condition: {@link Opaque} when it has a predicate.
471
324
  *
472
325
  * @stability stable
473
326
  * @category utility types
474
327
  * @since 0.3.0
475
328
  */
476
- export type OpaqueOf<C> = C extends {
477
- readonly "~Condition"?: Variance<infer _E, infer _R, infer O>
478
- }
479
- ? O
329
+ export type OpaqueOf<C> = C extends { readonly "~Opaque": () => infer X }
330
+ ? X
480
331
  : never
481
332
 
482
333
  type Leaf = string | number | boolean | bigint | symbol | null | undefined
@@ -501,28 +352,77 @@ type Whole =
501
352
  | Chunk.Chunk<unknown>
502
353
  | Cause.Cause<unknown>
503
354
  | Result.Result<unknown, unknown>
355
+ | Exit.Exit<unknown, unknown>
504
356
  | File
505
357
  | FormData
506
358
  | RegExp
359
+ | URLSearchParams
507
360
  type PreviousDepth = [never, 0, 1, 2, 3]
508
- type FieldPathInternal<T, Depth extends number, Arrays extends boolean> = [
509
- Depth,
510
- ] extends [never]
361
+ type FieldPathInternal<
362
+ T,
363
+ Depth extends number,
364
+ Arrays extends boolean,
365
+ Parents extends boolean = false,
366
+ > = [Depth] extends [never]
511
367
  ? never
512
368
  : {
513
- [K in keyof T & string]-?: NonNullable<T[K]> extends Leaf | Whole
514
- ? K
515
- : NonNullable<T[K]> extends Function
516
- ? never
517
- : NonNullable<T[K]> extends ReadonlyArray<unknown>
518
- ? Arrays extends true
519
- ? K
520
- : never
521
- : NonNullable<T[K]> extends object
522
- ? `${K}.${FieldPathInternal<NonNullable<T[K]>, PreviousDepth[Depth], Arrays>}`
523
- : never
369
+ [K in keyof T & string]-?: PathsOf<
370
+ K,
371
+ NonNullable<T[K]>,
372
+ Depth,
373
+ Arrays,
374
+ Parents
375
+ >
524
376
  }[keyof T & string]
525
377
 
378
+ /**
379
+ * The paths of one field `K` with the values `V`. Each kind of value in `V`
380
+ * gives its own paths, so a field that holds a string or a struct is a field,
381
+ * and the fields of the struct are fields too. A function is no field.
382
+ */
383
+ type PathsOf<
384
+ K extends string,
385
+ V,
386
+ Depth extends number,
387
+ Arrays extends boolean,
388
+ Parents extends boolean,
389
+ > =
390
+ // A field that is only ever `null` or missing is a field too.
391
+ | ([V] extends [never] ? K : never)
392
+ | ([Extract<V, Leaf | Whole>] extends [never] ? never : K)
393
+ | ([Extract<V, ReadonlyArray<unknown>>] extends [never]
394
+ ? never
395
+ : Arrays extends true
396
+ ? K
397
+ : never)
398
+ | ObjectPaths<
399
+ K,
400
+ Exclude<V, Leaf | Whole | Function | ReadonlyArray<unknown>>,
401
+ Depth,
402
+ Arrays,
403
+ Parents
404
+ >
405
+
406
+ /** The paths of a field `K` that holds the objects `O`, and of the fields below. */
407
+ type ObjectPaths<
408
+ K extends string,
409
+ O,
410
+ Depth extends number,
411
+ Arrays extends boolean,
412
+ Parents extends boolean,
413
+ > = [O] extends [never]
414
+ ? never
415
+ : // A record is one field to a rule or a request, as `permittedFields`
416
+ // lists it.
417
+ | (Parents extends true
418
+ ? K
419
+ : Arrays extends true
420
+ ? string extends keyof O
421
+ ? K
422
+ : never
423
+ : never)
424
+ | `${K}.${FieldPathInternal<O, PreviousDepth[Depth], Arrays, Parents>}`
425
+
526
426
  /**
527
427
  * The paths of the fields of `T` that hold one value, with dots for nested
528
428
  * fields. Arrays are left out. A map, a set, a date, a URL, a byte array, an
@@ -553,6 +453,19 @@ export type FieldSelector<T> = [T] extends [object]
553
453
  ? FieldPathInternal<T, 4, true>
554
454
  : never
555
455
 
456
+ /**
457
+ * The names that the `fields` of a rule may have: the paths of
458
+ * {@link FieldSelector}, and the paths of the structs above them. A name of a
459
+ * struct covers the fields below it.
460
+ *
461
+ * @stability stable
462
+ * @category utility types
463
+ * @since 2.0.0
464
+ */
465
+ export type FieldName<T> = [T] extends [object]
466
+ ? FieldPathInternal<T, 4, true, true>
467
+ : never
468
+
556
469
  /**
557
470
  * The value type that fits every field in `F`. For one field, it is the type
558
471
  * of the field. For a union of fields, it is the values that each of them can
@@ -629,12 +542,7 @@ type OrderedPath<T> = {
629
542
  * @category models
630
543
  * @since 1.0.0
631
544
  */
632
- export type Test<Value, P = unknown> = Condition<
633
- never,
634
- never,
635
- never,
636
- Reads<Value, P>
637
- >
545
+ export type Test<Value, P = unknown> = Condition<Reads<Value, P>>
638
546
 
639
547
  /**
640
548
  * The principal type that a test reads: the principal of each field of the
@@ -726,24 +634,14 @@ export interface Builder<Data, Value, P> {
726
634
  field: F,
727
635
  ) => Ref<PathValue<P, F>, P>
728
636
  readonly and: <const Cs extends ReadonlyArray<Any>>(
729
- ...conditions: Consistent<Cs>
730
- ) => Condition<
731
- Error<Cs[number]>,
732
- Services<Cs[number]>,
733
- OpaqueOf<Cs[number]>,
734
- ReadsOf<Cs[number]>
735
- >
637
+ ...conditions: Cs
638
+ ) => Condition<ReadsOf<Cs[number]>, OpaqueOf<Cs[number]>>
736
639
  readonly or: <const Cs extends ReadonlyArray<Any>>(
737
- ...conditions: Consistent<Cs>
738
- ) => Condition<
739
- Error<Cs[number]>,
740
- Services<Cs[number]>,
741
- OpaqueOf<Cs[number]>,
742
- ReadsOf<Cs[number]>
743
- >
640
+ ...conditions: Cs
641
+ ) => Condition<ReadsOf<Cs[number]>, OpaqueOf<Cs[number]>>
744
642
  readonly not: <C extends Any>(
745
- condition: C & Checked<C>,
746
- ) => Condition<Error<C>, Services<C>, OpaqueOf<C>, ReadsOf<C>>
643
+ condition: C,
644
+ ) => Condition<ReadsOf<C>, OpaqueOf<C>>
747
645
  /**
748
646
  * A test that runs any code. It may give a boolean or an `Effect`. A
749
647
  * condition with a predicate can not become JSON or a query.
@@ -754,45 +652,31 @@ export interface Builder<Data, Value, P> {
754
652
  value: Value,
755
653
  principal: P,
756
654
  ) => boolean | Effect.Effect<boolean, E, R>,
757
- ) => Condition<E, R, Opaque, Reads<Value, P>>
655
+ ) => Condition<Reads<Value, P>, Opaque<E, R>>
758
656
  }
759
657
 
760
- const nodes = Data.taggedEnum<Ast>()
761
- const operands = Data.taggedEnum<Operand>()
762
-
763
- const literal = (value: Scalar): Literal => operands.Literal({ value })
764
-
765
658
  const isOperand = (u: unknown): u is Operand =>
766
659
  hasProperty(u, "_tag") && (u._tag === "Literal" || u._tag === "Principal")
767
660
 
768
661
  const toOperand = (value: unknown): Operand =>
769
- isOperand(value) ? value : literal(value as Scalar)
662
+ isOperand(value) ? value : Nodes.literal(value as Scalar)
770
663
 
771
664
  const compare =
772
665
  (op: CompareOperator) =>
773
- (field: string, value: unknown): Compare =>
774
- nodes.Compare({ op, field, value: toOperand(value) })
666
+ (field: string, value: unknown): Ast =>
667
+ new Nodes.Compare({ op, field, value: toOperand(value) })
775
668
 
776
669
  // A list of conditions goes in as an array, not as arguments: a call may take
777
670
  // only so many arguments, and a decoded condition may have more children.
778
- // The list is copied, so a later change to the array of a caller does not
779
- // change the condition.
671
+ // The constructor copies the list, so a later change to the array of a caller
672
+ // does not change the condition.
780
673
  const andOf = (conditions: ReadonlyArray<Ast>): Ast =>
781
- nodes.And({ conditions: Arr.copy(conditions) })
674
+ new Nodes.And(conditions as ReadonlyArray<Nodes.Node>)
782
675
 
783
676
  const orOf = (conditions: ReadonlyArray<Ast>): Ast =>
784
- nodes.Or({ conditions: Arr.copy(conditions) })
785
-
786
- const makeAnd = (...conditions: ReadonlyArray<Ast>): Ast => andOf(conditions)
677
+ new Nodes.Or(conditions as ReadonlyArray<Nodes.Node>)
787
678
 
788
- const makeOr = (...conditions: ReadonlyArray<Ast>): Ast => orOf(conditions)
789
-
790
- const makeNot = (condition: Ast): Ast => nodes.Not({ condition })
791
-
792
- const makePredicate = (
793
- name: string,
794
- run: (value: unknown, principal: unknown) => Effect.Effect<boolean, any, any>,
795
- ): Ast => nodes.Predicate({ name, run })
679
+ const makeNot = (condition: Ast): Ast => new Nodes.Not(condition as Nodes.Node)
796
680
 
797
681
  const sharedBuilder: Builder<any, any, any> = {
798
682
  eq: compare("eq"),
@@ -802,13 +686,13 @@ const sharedBuilder: Builder<any, any, any> = {
802
686
  gt: compare("gt"),
803
687
  gte: compare("gte"),
804
688
  in: (field: string, values: ReadonlyArray<unknown>) =>
805
- nodes.In({ field, values: values.map(toOperand) }),
806
- exists: (field: string) => nodes.Exists({ field }),
689
+ new Nodes.In({ field, values: Arr.map(Array.from(values), toOperand) }),
690
+ exists: (field: string) => new Nodes.Exists({ field }),
807
691
  under: (...prefix: ReadonlyArray<unknown>) =>
808
- nodes.Under({ prefix: prefix.map(toOperand) }),
809
- principal: (path: string) => operands.Principal({ path }),
810
- and: makeAnd,
811
- or: makeOr,
692
+ new Nodes.Under({ prefix: Arr.map(prefix, toOperand) }),
693
+ principal: (path: string) => Nodes.principalRef(path),
694
+ and: (...conditions: ReadonlyArray<Ast>) => andOf(conditions),
695
+ or: (...conditions: ReadonlyArray<Ast>) => orOf(conditions),
812
696
  not: makeNot,
813
697
  predicate: (
814
698
  name: string,
@@ -817,12 +701,14 @@ const sharedBuilder: Builder<any, any, any> = {
817
701
  principal: unknown,
818
702
  ) => boolean | Effect.Effect<boolean, unknown, unknown>,
819
703
  ) =>
820
- makePredicate(name, (value, principal) =>
821
- Effect.suspend(() => {
822
- const result = test(value, principal)
823
- return Effect.isEffect(result) ? result : Effect.succeed(result)
824
- }),
825
- ),
704
+ new Nodes.Predicate({
705
+ name,
706
+ run: (value, principal) =>
707
+ Effect.suspend(() => {
708
+ const result = test(value, principal)
709
+ return Effect.isEffect(result) ? result : Effect.succeed(result)
710
+ }),
711
+ }),
826
712
  } as never
827
713
 
828
714
  /**
@@ -843,7 +729,7 @@ export const builder = <Data, Value, P>(): Builder<Data, Value, P> =>
843
729
  * @category constructors
844
730
  * @since 0.3.0
845
731
  */
846
- export const always: Condition = nodes.True() as Condition
732
+ export const always: Condition = Nodes.always as never
847
733
 
848
734
  /**
849
735
  * Always false.
@@ -852,7 +738,7 @@ export const always: Condition = nodes.True() as Condition
852
738
  * @category constructors
853
739
  * @since 0.3.0
854
740
  */
855
- export const never: Condition = nodes.False() as Condition
741
+ export const never: Condition = Nodes.never as never
856
742
 
857
743
  /**
858
744
  * True when all the conditions are true. With no conditions, it is true.
@@ -862,13 +748,10 @@ export const never: Condition = nodes.False() as Condition
862
748
  * @since 0.3.0
863
749
  */
864
750
  export const and: <const Cs extends ReadonlyArray<Any>>(
865
- ...conditions: Consistent<Cs>
866
- ) => Condition<
867
- Error<Cs[number]>,
868
- Services<Cs[number]>,
869
- OpaqueOf<Cs[number]>,
870
- ReadsOf<Cs[number]>
871
- > = makeAnd as never
751
+ ...conditions: Cs
752
+ ) => Condition<ReadsOf<Cs[number]>, OpaqueOf<Cs[number]>> = ((
753
+ ...conditions: ReadonlyArray<Ast>
754
+ ) => andOf(conditions)) as never
872
755
 
873
756
  /**
874
757
  * The same as {@link and}, for a list of conditions of one type. Use it for a
@@ -879,8 +762,8 @@ export const and: <const Cs extends ReadonlyArray<Any>>(
879
762
  * @since 1.0.0
880
763
  */
881
764
  export const andAll: <C extends Any>(
882
- conditions: ReadonlyArray<C & Checked<C>>,
883
- ) => Condition<Error<C>, Services<C>, OpaqueOf<C>, ReadsOf<C>> = andOf as never
765
+ conditions: ReadonlyArray<C>,
766
+ ) => Condition<ReadsOf<C>, OpaqueOf<C>> = andOf as never
884
767
 
885
768
  /**
886
769
  * The same as {@link or}, for a list of conditions of one type. Use it for a
@@ -891,8 +774,8 @@ export const andAll: <C extends Any>(
891
774
  * @since 1.0.0
892
775
  */
893
776
  export const orAll: <C extends Any>(
894
- conditions: ReadonlyArray<C & Checked<C>>,
895
- ) => Condition<Error<C>, Services<C>, OpaqueOf<C>, ReadsOf<C>> = orOf as never
777
+ conditions: ReadonlyArray<C>,
778
+ ) => Condition<ReadsOf<C>, OpaqueOf<C>> = orOf as never
896
779
 
897
780
  /**
898
781
  * True when one of the conditions is true. With no conditions, it is false.
@@ -902,13 +785,10 @@ export const orAll: <C extends Any>(
902
785
  * @since 0.3.0
903
786
  */
904
787
  export const or: <const Cs extends ReadonlyArray<Any>>(
905
- ...conditions: Consistent<Cs>
906
- ) => Condition<
907
- Error<Cs[number]>,
908
- Services<Cs[number]>,
909
- OpaqueOf<Cs[number]>,
910
- ReadsOf<Cs[number]>
911
- > = makeOr as never
788
+ ...conditions: Cs
789
+ ) => Condition<ReadsOf<Cs[number]>, OpaqueOf<Cs[number]>> = ((
790
+ ...conditions: ReadonlyArray<Ast>
791
+ ) => orOf(conditions)) as never
912
792
 
913
793
  /**
914
794
  * @stability stable
@@ -916,9 +796,8 @@ export const or: <const Cs extends ReadonlyArray<Any>>(
916
796
  * @since 0.3.0
917
797
  */
918
798
  export const not: <C extends Any>(
919
- condition: C & Checked<C>,
920
- ) => Condition<Error<C>, Services<C>, OpaqueOf<C>, ReadsOf<C>> =
921
- makeNot as never
799
+ condition: C,
800
+ ) => Condition<ReadsOf<C>, OpaqueOf<C>> = makeNot as never
922
801
 
923
802
  /**
924
803
  * The data that a condition needs for a test.
@@ -939,206 +818,8 @@ export interface Input {
939
818
  readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
940
819
  }
941
820
 
942
- /**
943
- * Gets a field of an object by a path with dots. A missing field, or a field
944
- * under a value that is not an object, gives `null`.
945
- */
946
- /**
947
- * Tests if an object has a field. A field may be the object's own, or come
948
- * from its class, as a getter does: `Schema.is` reads such a field too. A
949
- * name that every object has from `Object.prototype`, such as `constructor`
950
- * or `toString`, is a field only when the object gives it another value, as
951
- * its own or from its class.
952
- */
953
- const hasField = (value: object, key: string): boolean => {
954
- if (Object.hasOwn(value, key)) {
955
- return true
956
- }
957
- // `__proto__` reads the prototype of every object, so only an own one is a
958
- // field.
959
- if (key === "__proto__" || !(key in value)) {
960
- return false
961
- }
962
- return (
963
- !(key in Object.prototype) ||
964
- (value as Record<string, unknown>)[key] !==
965
- (Object.prototype as Record<string, unknown>)[key]
966
- )
967
- }
968
-
969
- const getField = (value: unknown, path: string): unknown =>
970
- Option.match(
971
- Arr.reduce(path.split("."), Option.some(value), (cursor, key) =>
972
- Option.flatMap(cursor, (current) =>
973
- isObject(current) && hasField(current, key)
974
- ? Option.some((current as Record<string, unknown>)[key])
975
- : Option.none(),
976
- ),
977
- ),
978
- {
979
- onNone: () => null,
980
- onSome: (field) => (field === undefined ? null : field),
981
- },
982
- )
983
-
984
- // Any number is a scalar, also `Infinity` and `NaN`: a principal field may
985
- // hold them, and a test compares them as a fixed value of a condition does.
986
- const isScalar = (value: unknown): value is Scalar =>
987
- typeof value === "string" ||
988
- typeof value === "boolean" ||
989
- typeof value === "number"
990
-
991
- /**
992
- * Gives the value of an operand. A principal field that is `null`, missing, or
993
- * not a {@link Scalar} gives `undefined`. No field of a resource is
994
- * `undefined`, so a test with it is never true. This keeps the check closed: a
995
- * principal with no id never matches a resource with no owner. It also keeps
996
- * other values, such as a `bigint`, out of the residual condition.
997
- */
998
- const resolve = (operand: Operand, principal: unknown): unknown => {
999
- if (operand._tag === "Literal") {
1000
- return operand.value
1001
- }
1002
- const value = getField(principal, operand.path)
1003
- return isScalar(value) ? value : undefined
1004
- }
1005
-
1006
- const testCompare = (
1007
- op: CompareOperator,
1008
- left: unknown,
1009
- right: unknown,
1010
- ): boolean => {
1011
- if (right === undefined) {
1012
- return false
1013
- }
1014
- switch (op) {
1015
- case "eq":
1016
- return left === right
1017
- case "ne":
1018
- return left !== right
1019
- default: {
1020
- const ordered =
1021
- (typeof left === "number" && typeof right === "number") ||
1022
- (typeof left === "string" && typeof right === "string")
1023
- if (!ordered) {
1024
- return false
1025
- }
1026
- const a = left as number | string
1027
- const b = right as number | string
1028
- switch (op) {
1029
- case "lt":
1030
- return a < b
1031
- case "lte":
1032
- return a <= b
1033
- case "gt":
1034
- return a > b
1035
- case "gte":
1036
- return a >= b
1037
- }
1038
- }
1039
- }
1040
- }
1041
-
1042
- /**
1043
- * Gives the values of a resource path. It gives `undefined` when the resource
1044
- * has no path.
1045
- */
1046
- const pathValues = (
1047
- value: unknown,
1048
- path: ReadonlyArray<Resource.PathSegment> | undefined,
1049
- ): ReadonlyArray<unknown> | undefined =>
1050
- path?.map((segment) =>
1051
- typeof segment === "string" ? segment : getField(value, segment.field),
1052
- )
1053
-
1054
- const testUnder = (prefix: ReadonlyArray<Operand>, input: Input): boolean => {
1055
- const values = pathValues(input.value, input.path)
1056
- if (values === undefined || prefix.length > values.length) {
1057
- return false
1058
- }
1059
- // An operand with no value never matches, also a hole in a path that gives
1060
- // `undefined` too.
1061
- return prefix.every((operand, index) => {
1062
- const value = resolve(operand, input.principal)
1063
- return value !== undefined && value === values[index]
1064
- })
1065
- }
1066
-
1067
- /**
1068
- * True when a condition has a {@link Predicate}.
1069
- *
1070
- * @stability stable
1071
- * @category predicates
1072
- * @since 1.0.0
1073
- */
1074
- export const hasPredicate = (condition: Ast): boolean =>
1075
- hasPredicateIn(condition, new Map())
1076
-
1077
- // One node may be a child of many nodes. Each walk below keeps its work on a
1078
- // node in a memo that the walk makes for itself, so a condition with shared
1079
- // children takes time for its nodes, not for its paths. No memo lives longer
1080
- // than one call.
1081
-
1082
- /**
1083
- * Gives the answer for a key from the memo, or works it out and keeps it.
1084
- *
1085
- * A memo keyed by a node is a native `Map`, because it must find a node by
1086
- * the node itself. An Effect `HashMap` finds a key by `Equal`, and the nodes
1087
- * are Effect data, so it would hash the whole structure of a node, and a
1088
- * shared child again for each path to it.
1089
- */
1090
- const remember = <K, A>(memo: Map<K, A>, key: K, work: () => A): A => {
1091
- // A result may be `undefined`, as the result of a fold may be, so the test
1092
- // asks for the key.
1093
- if (memo.has(key)) {
1094
- return memo.get(key) as A
1095
- }
1096
- const result = work()
1097
- memo.set(key, result)
1098
- return result
1099
- }
1100
-
1101
- const hasPredicateIn = (ast: Ast, memo: Map<Ast, boolean>): boolean =>
1102
- remember(memo, ast, () => {
1103
- switch (ast._tag) {
1104
- case "Predicate":
1105
- return true
1106
- case "And":
1107
- case "Or":
1108
- return Arr.some(ast.conditions, (child) => hasPredicateIn(child, memo))
1109
- case "Not":
1110
- return hasPredicateIn(ast.condition, memo)
1111
- default:
1112
- return false
1113
- }
1114
- })
1115
-
1116
- const provideAst = (
1117
- ast: Ast,
1118
- context: Context.Context<never>,
1119
- done: Map<Ast, Ast> = new Map(),
1120
- predicates: Map<Ast, boolean> = new Map(),
1121
- ): Ast =>
1122
- !hasPredicateIn(ast, predicates)
1123
- ? ast
1124
- : remember(done, ast, () => {
1125
- const provide = (child: Ast) =>
1126
- provideAst(child, context, done, predicates)
1127
- switch (ast._tag) {
1128
- case "Predicate":
1129
- return makePredicate(ast.name, (value, principal) =>
1130
- Effect.provideContext(ast.run(value, principal), context),
1131
- )
1132
- case "And":
1133
- return andOf(Arr.map(ast.conditions, provide))
1134
- case "Or":
1135
- return orOf(Arr.map(ast.conditions, provide))
1136
- case "Not":
1137
- return makeNot(provide(ast.condition))
1138
- default:
1139
- return ast
1140
- }
1141
- })
821
+ /** Gives a condition as a node. A value that no constructor made is a defect. */
822
+ const nodeOf = (condition: Ast): Nodes.Node => Nodes.toNode(condition)
1142
823
 
1143
824
  /**
1144
825
  * Gives the services in `context` to every predicate of a condition, so the
@@ -1151,98 +832,17 @@ const provideAst = (
1151
832
  export const provideContext: {
1152
833
  <R>(
1153
834
  context: Context.Context<R>,
1154
- ): <E, O, I>(condition: Condition<E, R, O, I>) => Condition<E, never, O, I>
1155
- <E, R, O, I>(
1156
- condition: Condition<E, R, O, I>,
1157
- context: Context.Context<R>,
1158
- ): Condition<E, never, O, I>
1159
- } = dual(2, provideAst) as never
1160
-
1161
- const evaluateSyncAst = (ast: Ast, input: Input): boolean =>
1162
- evaluateSyncNode(ast, input, new Map())
1163
-
1164
- const evaluateSyncNode = (
1165
- ast: Ast,
1166
- input: Input,
1167
- done: Map<Ast, boolean>,
1168
- ): boolean => remember(done, ast, () => evaluateSyncOnce(ast, input, done))
1169
-
1170
- const evaluateSyncOnce = (
1171
- ast: Ast,
1172
- input: Input,
1173
- done: Map<Ast, boolean>,
1174
- ): boolean => {
1175
- switch (ast._tag) {
1176
- case "True":
1177
- return true
1178
- case "False":
1179
- return false
1180
- case "Compare":
1181
- return testCompare(
1182
- ast.op,
1183
- getField(input.value, ast.field),
1184
- resolve(ast.value, input.principal),
1185
- )
1186
- case "In": {
1187
- const field = getField(input.value, ast.field)
1188
- return ast.values.some(
1189
- (operand) => resolve(operand, input.principal) === field,
1190
- )
1191
- }
1192
- case "Exists":
1193
- return getField(input.value, ast.field) !== null
1194
- case "Under":
1195
- return testUnder(ast.prefix, input)
1196
- case "And":
1197
- return Arr.every(ast.conditions, (child) =>
1198
- evaluateSyncNode(child, input, done),
1199
- )
1200
- case "Or":
1201
- return Arr.some(ast.conditions, (child) =>
1202
- evaluateSyncNode(child, input, done),
1203
- )
1204
- case "Not":
1205
- return !evaluateSyncNode(ast.condition, input, done)
1206
- case "Predicate":
1207
- // The types stop this. A predicate needs `evaluate`.
1208
- throw new globalThis.Error(
1209
- `The predicate "${ast.name}" needs an Effect. Use Condition.evaluate.`,
1210
- )
1211
- }
1212
- }
1213
-
1214
- const evaluateAst = (
1215
- ast: Ast,
1216
- input: Input,
1217
- predicates: Map<Ast, boolean> = new Map(),
1218
- ): Effect.Effect<boolean, unknown, unknown> => {
1219
- if (!hasPredicateIn(ast, predicates)) {
1220
- return Effect.sync(() => evaluateSyncAst(ast, input))
1221
- }
1222
- const evaluate = (child: Ast) => evaluateAst(child, input, predicates)
1223
- switch (ast._tag) {
1224
- case "Predicate":
1225
- return ast.run(input.value, input.principal)
1226
- case "Not":
1227
- return Effect.map(evaluate(ast.condition), (value) => !value)
1228
- case "And":
1229
- // The children run in order, until one is false.
1230
- return Effect.map(
1231
- Effect.findFirst(ast.conditions, (child) =>
1232
- Effect.map(evaluate(child), (value) => !value),
1233
- ),
1234
- Option.isNone,
1235
- )
1236
- case "Or":
1237
- // The children run in order, until one is true.
1238
- return Effect.map(
1239
- Effect.findFirst(ast.conditions, evaluate),
1240
- Option.isSome,
1241
- )
1242
- default:
1243
- return Effect.sync(() => evaluateSyncAst(ast, input))
1244
- }
1245
- }
835
+ ): <I, X>(
836
+ condition: Condition<I, X> &
837
+ ([ServicesOf<X>] extends [R] ? unknown : never),
838
+ ) => Condition<I, Provided<X>>
839
+ <I, X>(
840
+ condition: Condition<I, X>,
841
+ context: Context.Context<ServicesOf<X>>,
842
+ ): Condition<I, Provided<X>>
843
+ } = dual(2, (condition: Ast, context: Context.Context<never>): Ast =>
844
+ Algebra.provide(nodeOf(condition), context),
845
+ ) as never
1246
846
 
1247
847
  /**
1248
848
  * The input of {@link evaluate} that the predicates of a condition need: a
@@ -1255,12 +855,6 @@ type ReadsInput<I> =
1255
855
  : { readonly principal: P })
1256
856
  : unknown
1257
857
 
1258
- /**
1259
- * `unknown` when `In` gives what `I` reads, and `never` when it does not. For
1260
- * a union, each member must give it, so the test is not split over the union.
1261
- */
1262
- type ReadsFrom<I, In> = [In] extends [ReadsInput<I>] ? unknown : never
1263
-
1264
858
  /**
1265
859
  * Tests a condition on a value. A predicate may fail with its error. When the
1266
860
  * condition has a predicate, the input must have the value and the principal
@@ -1270,17 +864,14 @@ type ReadsFrom<I, In> = [In] extends [ReadsInput<I>] ? unknown : never
1270
864
  * @category evaluation
1271
865
  * @since 0.3.0
1272
866
  */
1273
- export const evaluate: {
1274
- <In extends Input>(
1275
- input: In,
1276
- ): <E, R, O, I>(
1277
- condition: Condition<E, R, O, I> & ReadsFrom<I, In>,
1278
- ) => Effect.Effect<boolean, E, R>
1279
- <E, R, O, I, In extends Input>(
1280
- condition: Condition<E, R, O, I>,
1281
- input: In & ReadsInput<I>,
1282
- ): Effect.Effect<boolean, E, R>
1283
- } = dual(2, evaluateAst) as never
867
+ export const evaluate: <I, X, In extends Input>(
868
+ condition: Condition<I, X>,
869
+ input: In & ReadsInput<I>,
870
+ ) => Effect.Effect<boolean, ErrorOf<X>, ServicesOf<X>> = ((
871
+ condition: Ast,
872
+ input: Input,
873
+ ): Effect.Effect<boolean, unknown, unknown> =>
874
+ Effect.suspend(() => Algebra.evaluate(nodeOf(condition), input))) as never
1284
875
 
1285
876
  /**
1286
877
  * Tests a condition with no predicate on a value, at once.
@@ -1289,394 +880,23 @@ export const evaluate: {
1289
880
  * @category evaluation
1290
881
  * @since 0.3.0
1291
882
  */
1292
- export const evaluateSync: {
1293
- // The input must have the value that the condition reads, as in
1294
- // `evaluate`. A residual of one kind can not test a value of another kind.
1295
- <In extends Input>(
1296
- input: In,
1297
- ): <I>(
1298
- condition: Condition<never, never, never, I> & ReadsFrom<I, In>,
1299
- ) => boolean
1300
- <I, In extends Input>(
1301
- condition: Condition<never, never, never, I>,
1302
- input: In & ReadsInput<I>,
1303
- ): boolean
1304
- } = dual(2, evaluateSyncAst)
1305
-
1306
- /**
1307
- * The memo of one simplification. One node may be a child of many nodes, so
1308
- * the work on each node is done one time. The memo lives only as long as the
1309
- * work that made it.
1310
- */
1311
- interface Memo {
1312
- /** The simple form of each node. */
1313
- readonly simple: Map<Ast, Ast>
1314
- readonly predicates: Map<Ast, boolean>
1315
- /** A number for each node. Two nodes with one structure get one number. */
1316
- readonly ids: Map<Ast, number>
1317
- readonly structures: MutableHashMap.MutableHashMap<string, number>
1318
- }
1319
-
1320
- const makeMemo = (): Memo => ({
1321
- simple: new Map(),
1322
- predicates: new Map(),
1323
- ids: new Map(),
1324
- structures: MutableHashMap.empty(),
1325
- })
1326
-
1327
- const literalText = (value: Scalar): string =>
1328
- // JSON gives `null` for `Infinity`, `-Infinity` and `NaN`, so a number is
1329
- // given as its own text.
1330
- typeof value === "number"
1331
- ? `n${Object.is(value, -0) ? "-0" : String(value)}`
1332
- : `${typeof value}:${JSON.stringify(value)}`
1333
-
1334
- const operandText = (operand: Operand): string =>
1335
- operand._tag === "Literal"
1336
- ? `L${literalText(operand.value)}`
1337
- : `R${JSON.stringify(operand.path)}`
1338
-
1339
- const structureText = (ast: Ast, memo: Memo): string | undefined => {
1340
- const id = (child: Ast) => structureId(child, memo)
1341
- switch (ast._tag) {
1342
- case "True":
1343
- case "False":
1344
- return ast._tag
1345
- case "Compare":
1346
- return `C${ast.op}:${JSON.stringify(ast.field)}:${operandText(ast.value)}`
1347
- case "In":
1348
- return `I${JSON.stringify(ast.field)}:${Arr.map(ast.values, operandText).join(",")}`
1349
- case "Exists":
1350
- return `E${JSON.stringify(ast.field)}`
1351
- case "Under":
1352
- return `U${Arr.map(ast.prefix, operandText).join(",")}`
1353
- case "And":
1354
- case "Or":
1355
- return `${ast._tag}:${Arr.map(ast.conditions, id).join(",")}`
1356
- case "Not":
1357
- return `N${String(id(ast.condition))}`
1358
- case "Predicate":
1359
- // A predicate is equal only to itself.
1360
- return undefined
1361
- }
1362
- }
1363
-
1364
- /**
1365
- * Gives the number of the structure of a node. Two nodes with the same
1366
- * structure get the same number, so a set of numbers finds an equal node at
1367
- * once. The hash of Effect data would walk a shared child again for each path
1368
- * to it. A new number is the count of the numbered nodes, so it is new.
1369
- */
1370
- const structureId = (ast: Ast, memo: Memo): number =>
1371
- remember(memo.ids, ast, () => {
1372
- const text = structureText(ast, memo)
1373
- const fresh = memo.ids.size
1374
- if (text === undefined) {
1375
- return fresh
1376
- }
1377
- return Option.getOrElse(MutableHashMap.get(memo.structures, text), () => {
1378
- MutableHashMap.set(memo.structures, text, fresh)
1379
- return fresh
1380
- })
1381
- })
1382
-
1383
- const dedupe = (
1384
- conditions: ReadonlyArray<Ast>,
1385
- memo: Memo,
1386
- ): ReadonlyArray<Ast> => {
1387
- const ids = Arr.map(conditions, (condition) => structureId(condition, memo))
1388
- // The first place of each number. The entries go in from the end, so the
1389
- // first place is the one that stays.
1390
- const first = HashMap.fromIterable(
1391
- Arr.reverse(Arr.map(ids, (id, index) => [id, index] as const)),
1392
- )
1393
- return Arr.filter(conditions, (_, index) =>
1394
- Option.contains(HashMap.get(first, ids[index]!), index),
1395
- )
1396
- }
1397
-
1398
- /**
1399
- * Removes the children that the other children make useless. In an `Or`,
1400
- * `a OR (a AND b)` is `a`. In an `And`, `a AND (a OR b)` is `a`. The
1401
- * children are already without copies.
1402
- */
1403
- const absorb = (
1404
- inner: "And" | "Or",
1405
- conditions: ReadonlyArray<Ast>,
1406
- memo: Memo,
1407
- ): ReadonlyArray<Ast> => {
1408
- const all = HashSet.fromIterable(
1409
- Arr.map(conditions, (condition) => structureId(condition, memo)),
1410
- )
1411
- return Arr.filter(
1412
- conditions,
1413
- (condition) =>
1414
- condition._tag !== inner ||
1415
- !Arr.some(condition.conditions, (part) =>
1416
- HashSet.has(all, structureId(part, memo)),
1417
- ),
1418
- )
1419
- }
1420
-
1421
- /**
1422
- * Removes children that do not change the result. A predicate may fail, and it
1423
- * may give another answer when it runs again. So when a child has a predicate,
1424
- * every child stays, and each one runs in its place.
1425
- */
1426
- const reduce = (
1427
- inner: "And" | "Or",
1428
- conditions: ReadonlyArray<Ast>,
1429
- memo: Memo,
1430
- ): ReadonlyArray<Ast> =>
1431
- Arr.some(conditions, (condition) =>
1432
- hasPredicateIn(condition, memo.predicates),
1433
- )
1434
- ? conditions
1435
- : absorb(inner, dedupe(conditions, memo), memo)
1436
-
1437
- /**
1438
- * Puts the children of a child of the same kind into the node. A child with a
1439
- * predicate stays one child: its children all stay anyway, and one shared
1440
- * child would come again for each path to it, so the node could grow without
1441
- * end.
1442
- */
1443
- const flatten = (
1444
- tag: "And" | "Or",
1445
- conditions: ReadonlyArray<Ast>,
1446
- memo: Memo,
1447
- ): ReadonlyArray<Ast> =>
1448
- Arr.flatMap(conditions, (child) => {
1449
- const simple = simplifyIn(child, memo)
1450
- return simple._tag === tag && !hasPredicateIn(simple, memo.predicates)
1451
- ? simple.conditions
1452
- : [simple]
1453
- })
1454
-
1455
- /**
1456
- * Gives the result of an `And` with a `False` child, or of an `Or` with a
1457
- * `True` child. The children are tested in order, so a predicate before the
1458
- * constant still runs, and it may fail. When there is such a predicate, the
1459
- * children before the constant stay, so the failure is not lost. The other
1460
- * children stay too, because they decide if the predicate runs.
1461
- */
1462
- const shortCircuit = (
1463
- tag: "And" | "Or",
1464
- children: ReadonlyArray<Ast>,
1465
- index: number,
1466
- memo: Memo,
1467
- ): Ast => {
1468
- const constant = tag === "And" ? never : always
1469
- const neutral = tag === "And" ? "True" : "False"
1470
- const before = Arr.filter(
1471
- Arr.take(children, index),
1472
- (child) => child._tag !== neutral,
1473
- )
1474
- if (!Arr.some(before, (child) => hasPredicateIn(child, memo.predicates))) {
1475
- return constant
1476
- }
1477
- return tag === "And"
1478
- ? andOf([...before, constant])
1479
- : orOf([...before, constant])
1480
- }
1481
-
1482
- const simplifyIn = (ast: Ast, memo: Memo): Ast =>
1483
- remember(memo.simple, ast, () => {
1484
- const result = simplifyNode(ast, memo)
1485
- // The simple form of a simple node is the node itself.
1486
- memo.simple.set(result, result)
1487
- return result
1488
- })
1489
-
1490
- const simplifyJunction = (
1491
- tag: "And" | "Or",
1492
- conditions: ReadonlyArray<Ast>,
1493
- memo: Memo,
1494
- ): Ast => {
1495
- const constant = tag === "And" ? "False" : "True"
1496
- const neutral = tag === "And" ? "True" : "False"
1497
- const flat = flatten(tag, conditions, memo)
1498
- return Option.match(
1499
- Arr.findFirstIndex(flat, (child) => child._tag === constant),
1500
- {
1501
- onSome: (stop) => shortCircuit(tag, flat, stop, memo),
1502
- onNone: () => {
1503
- const unique = reduce(
1504
- tag === "And" ? "Or" : "And",
1505
- Arr.filter(flat, (child) => child._tag !== neutral),
1506
- memo,
1507
- )
1508
- return unique.length === 0
1509
- ? tag === "And"
1510
- ? always
1511
- : never
1512
- : unique.length === 1
1513
- ? unique[0]!
1514
- : tag === "And"
1515
- ? andOf(unique)
1516
- : orOf(unique)
1517
- },
1518
- },
1519
- )
1520
- }
1521
-
1522
- const simplifyNode = (ast: Ast, memo: Memo): Ast => {
1523
- switch (ast._tag) {
1524
- case "And":
1525
- case "Or":
1526
- return simplifyJunction(ast._tag, ast.conditions, memo)
1527
- case "Not": {
1528
- const child = simplifyIn(ast.condition, memo)
1529
- switch (child._tag) {
1530
- case "True":
1531
- return never
1532
- case "False":
1533
- return always
1534
- case "Not":
1535
- return child.condition
1536
- case "Compare":
1537
- // A principal field with no value makes `eq` and `ne` both false,
1538
- // so only a fixed value lets `not eq` become `ne`.
1539
- if (
1540
- child.value._tag === "Literal" &&
1541
- (child.op === "eq" || child.op === "ne")
1542
- ) {
1543
- return nodes.Compare({
1544
- op: child.op === "eq" ? "ne" : "eq",
1545
- field: child.field,
1546
- value: child.value,
1547
- })
1548
- }
1549
- return makeNot(child)
1550
- default:
1551
- return makeNot(child)
1552
- }
1553
- }
1554
- case "In":
1555
- return ast.values.length === 0 ? never : ast
1556
- default:
1557
- return ast
1558
- }
1559
- }
1560
-
1561
- const simplifyAst = (ast: Ast): Ast => simplifyIn(ast, makeMemo())
883
+ export const evaluateSync: <I, In extends Input>(
884
+ condition: Condition<I>,
885
+ input: In & ReadsInput<I>,
886
+ ) => boolean = (condition: Ast, input: Input): boolean =>
887
+ Algebra.evaluateSync(nodeOf(condition), input)
1562
888
 
1563
889
  /**
1564
890
  * Makes a condition smaller without a change to its meaning. It removes `True`
1565
891
  * and `False` from `And` and `Or`, joins nested `And` and `Or` nodes, and
1566
- * removes double `Not` nodes.
892
+ * removes double `Not` nodes and copies of a child.
1567
893
  *
1568
894
  * @stability stable
1569
895
  * @category evaluation
1570
896
  * @since 0.3.0
1571
897
  */
1572
- export const simplify: <E, R, O, I>(
1573
- condition: Condition<E, R, O, I>,
1574
- ) => Condition<E, R, O, I> = simplifyAst as never
1575
-
1576
- /**
1577
- * Makes a {@link simplify} function that keeps its work for the parts it has
1578
- * seen. Use one for many calls on conditions that share parts, such as the
1579
- * steps of one residual condition. Do not keep it for long: it keeps every
1580
- * part that it has seen.
1581
- *
1582
- * @stability stable
1583
- * @category evaluation
1584
- * @since 1.0.0
1585
- */
1586
- export const simplifier = (): (<E, R, O, I>(
1587
- condition: Condition<E, R, O, I>,
1588
- ) => Condition<E, R, O, I>) => {
1589
- const memo = makeMemo()
1590
- return ((ast: Ast) => simplifyIn(ast, memo)) as never
1591
- }
1592
-
1593
- /**
1594
- * Puts the value of the principal into an operand. It gives `undefined` for a
1595
- * principal field with no value. See {@link resolve}.
1596
- */
1597
- const bindOperand = (
1598
- operand: Operand,
1599
- principal: unknown,
1600
- ): Literal | undefined => {
1601
- if (operand._tag === "Literal") {
1602
- return operand
1603
- }
1604
- const value = resolve(operand, principal)
1605
- return value === undefined ? undefined : literal(value as Scalar)
1606
- }
1607
-
1608
- const expandUnder = (
1609
- prefix: ReadonlyArray<Operand>,
1610
- principal: unknown,
1611
- path: ReadonlyArray<Resource.PathSegment> | undefined,
1612
- ): Ast => {
1613
- if (path === undefined || prefix.length > path.length) {
1614
- return never
1615
- }
1616
- const parts = Arr.map(prefix, (operand, index): Ast => {
1617
- const value = resolve(operand, principal)
1618
- const segment = path[index]!
1619
- return value === undefined
1620
- ? never
1621
- : typeof segment === "string"
1622
- ? segment === value
1623
- ? always
1624
- : never
1625
- : nodes.Compare({
1626
- op: "eq",
1627
- field: segment.field,
1628
- value: literal(value as Scalar),
1629
- })
1630
- })
1631
- return andOf(parts)
1632
- }
1633
-
1634
- const bind = (
1635
- ast: Ast,
1636
- principal: unknown,
1637
- path: ReadonlyArray<Resource.PathSegment> | undefined,
1638
- done: Map<Ast, Ast> = new Map(),
1639
- ): Ast => remember(done, ast, () => bindOnce(ast, principal, path, done))
1640
-
1641
- const bindOnce = (
1642
- ast: Ast,
1643
- principal: unknown,
1644
- path: ReadonlyArray<Resource.PathSegment> | undefined,
1645
- done: Map<Ast, Ast>,
1646
- ): Ast => {
1647
- switch (ast._tag) {
1648
- case "Compare": {
1649
- const value = bindOperand(ast.value, principal)
1650
- return value === undefined
1651
- ? never
1652
- : nodes.Compare({ op: ast.op, field: ast.field, value })
1653
- }
1654
- case "In":
1655
- return nodes.In({
1656
- field: ast.field,
1657
- values: ast.values.flatMap((operand) => {
1658
- const value = bindOperand(operand, principal)
1659
- return value === undefined ? [] : [value]
1660
- }),
1661
- })
1662
- case "Under":
1663
- return expandUnder(ast.prefix, principal, path)
1664
- case "And":
1665
- return andOf(
1666
- ast.conditions.map((child) => bind(child, principal, path, done)),
1667
- )
1668
- case "Or":
1669
- return orOf(
1670
- ast.conditions.map((child) => bind(child, principal, path, done)),
1671
- )
1672
- case "Not":
1673
- return makeNot(bind(ast.condition, principal, path, done))
1674
- case "Predicate":
1675
- return makePredicate(ast.name, (value) => ast.run(value, principal))
1676
- default:
1677
- return ast
1678
- }
1679
- }
898
+ export const simplify: <I, X>(condition: Condition<I, X>) => Condition<I, X> =
899
+ ((condition: Ast) => condition.pipe(nodeOf, Algebra.simplify)) as never
1680
900
 
1681
901
  interface PartialInput {
1682
902
  readonly principal?: object | undefined
@@ -1695,9 +915,6 @@ type PrincipalInput<I> =
1695
915
  : { readonly principal: P }
1696
916
  : unknown
1697
917
 
1698
- /** `unknown` when `In` gives what `I` reads, and `never` when it does not. */
1699
- type PrincipalFrom<I, In> = [In] extends [PrincipalInput<I>] ? unknown : never
1700
-
1701
918
  /** What a condition reads after {@link partial}: the value, and no principal. */
1702
919
  type Bound<I> = I extends Reads<infer V, infer _P> ? Reads<V, unknown> : I
1703
920
 
@@ -1711,26 +928,11 @@ type Bound<I> = I extends Reads<infer V, infer _P> ? Reads<V, unknown> : I
1711
928
  * @category evaluation
1712
929
  * @since 0.3.0
1713
930
  */
1714
- export const partial: {
1715
- <In extends PartialInput>(
1716
- input: In,
1717
- ): <E, R, O, I>(
1718
- condition: Condition<E, R, O, I> & PrincipalFrom<I, In>,
1719
- ) => Condition<E, R, O, Bound<I>>
1720
- <E, R, O, I>(
1721
- condition: Condition<E, R, O, I>,
1722
- input: PartialInput & PrincipalInput<I>,
1723
- ): Condition<E, R, O, Bound<I>>
1724
- } = dual(
1725
- 2,
1726
- (
1727
- ast: Ast,
1728
- input: {
1729
- readonly principal?: object | undefined
1730
- readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
1731
- },
1732
- ): Ast => simplifyAst(bind(ast, input.principal, input.path)),
1733
- )
931
+ export const partial: <I, X>(
932
+ condition: Condition<I, X>,
933
+ input: PartialInput & PrincipalInput<I>,
934
+ ) => Condition<Bound<I>, X> = ((condition: Ast, input: PartialInput): Ast =>
935
+ Algebra.partial(nodeOf(condition), input)) as never
1734
936
 
1735
937
  /**
1736
938
  * The functions that change each node of a condition into another form, such
@@ -1773,39 +975,26 @@ const literalValue = (operand: Operand, field: string): Scalar => {
1773
975
  return operand.value
1774
976
  }
1775
977
 
1776
- /**
1777
- * Folds one node. The memo keeps the result of each node, so a node that is a
1778
- * child of many nodes is folded one time.
1779
- */
1780
- const foldAst = <A>(
1781
- ast: Ast,
1782
- interpreter: Interpreter<A>,
1783
- done: Map<Ast, A> = new Map(),
1784
- ): A => remember(done, ast, () => foldOnce(ast, interpreter, done))
1785
-
1786
- const foldOnce = <A>(
1787
- ast: Ast,
1788
- interpreter: Interpreter<A>,
1789
- done: Map<Ast, A>,
1790
- ): A => {
1791
- switch (ast._tag) {
978
+ /** Folds a condition. A shared child is folded again for each place. */
979
+ const foldNode = <A>(node: Nodes.Node, interpreter: Interpreter<A>): A => {
980
+ switch (node._tag) {
1792
981
  case "True":
1793
982
  return interpreter.true()
1794
983
  case "False":
1795
984
  return interpreter.false()
1796
985
  case "Compare":
1797
986
  return interpreter.compare(
1798
- ast.op,
1799
- ast.field,
1800
- literalValue(ast.value, ast.field),
987
+ node.op,
988
+ node.field,
989
+ literalValue(node.value, node.field),
1801
990
  )
1802
991
  case "In":
1803
992
  return interpreter.in(
1804
- ast.field,
1805
- ast.values.map((operand) => literalValue(operand, ast.field)),
993
+ node.field,
994
+ Arr.map(node.values, (operand) => literalValue(operand, node.field)),
1806
995
  )
1807
996
  case "Exists":
1808
- return interpreter.exists(ast.field)
997
+ return interpreter.exists(node.field)
1809
998
  case "Under":
1810
999
  throw Untranslatable.make({
1811
1000
  reason:
@@ -1813,21 +1002,21 @@ const foldOnce = <A>(
1813
1002
  })
1814
1003
  case "And":
1815
1004
  return interpreter.and(
1816
- Arr.map(ast.conditions, (child) => foldAst(child, interpreter, done)),
1005
+ Arr.map(node.conditions, (child) => foldNode(child, interpreter)),
1817
1006
  )
1818
1007
  case "Or":
1819
1008
  return interpreter.or(
1820
- Arr.map(ast.conditions, (child) => foldAst(child, interpreter, done)),
1009
+ Arr.map(node.conditions, (child) => foldNode(child, interpreter)),
1821
1010
  )
1822
1011
  case "Not":
1823
- return interpreter.not(foldAst(ast.condition, interpreter, done))
1012
+ return interpreter.not(foldNode(node.condition, interpreter))
1824
1013
  case "Predicate":
1825
1014
  if (interpreter.predicate === undefined) {
1826
1015
  throw Untranslatable.make({
1827
- reason: `The predicate "${ast.name}" can not become a query`,
1016
+ reason: `The predicate "${node.name}" can not become a query`,
1828
1017
  })
1829
1018
  }
1830
- return interpreter.predicate(ast.name)
1019
+ return interpreter.predicate(node.name)
1831
1020
  }
1832
1021
  }
1833
1022
 
@@ -1851,11 +1040,11 @@ export const fold: {
1851
1040
  } = dual(
1852
1041
  2,
1853
1042
  <A>(
1854
- ast: Ast,
1043
+ condition: Ast,
1855
1044
  interpreter: Interpreter<A>,
1856
1045
  ): Effect.Effect<A, Untranslatable> =>
1857
1046
  Effect.try({
1858
- try: () => foldAst(ast, interpreter),
1047
+ try: () => foldNode(nodeOf(condition), interpreter),
1859
1048
  catch: (cause) => {
1860
1049
  if (cause instanceof Untranslatable) {
1861
1050
  return cause
@@ -1866,49 +1055,14 @@ export const fold: {
1866
1055
  )
1867
1056
 
1868
1057
  /**
1869
- * The fields that a condition uses.
1058
+ * The fields that a condition uses, in the order that they first come.
1870
1059
  *
1871
1060
  * @stability stable
1872
1061
  * @category getters
1873
1062
  * @since 0.3.0
1874
1063
  */
1875
1064
  export const fields = (condition: Ast): ReadonlyArray<string> =>
1876
- fieldsIn(condition, new Map())
1877
-
1878
- /**
1879
- * The fields of a node, in the order that they first come. A `HashSet` would
1880
- * not keep that order, so it only says which fields came before.
1881
- */
1882
- /** Leaves out each string that came before, in linear time. */
1883
- const dedupeStrings = (values: ReadonlyArray<string>): ReadonlyArray<string> =>
1884
- Arr.flatten(
1885
- Arr.mapAccum(values, HashSet.empty<string>(), (seen, value) => [
1886
- HashSet.add(seen, value),
1887
- HashSet.has(seen, value) ? [] : [value],
1888
- ])[1],
1889
- )
1890
-
1891
- const fieldsIn = (
1892
- ast: Ast,
1893
- memo: Map<Ast, ReadonlyArray<string>>,
1894
- ): ReadonlyArray<string> =>
1895
- remember(memo, ast, () => {
1896
- switch (ast._tag) {
1897
- case "Compare":
1898
- case "In":
1899
- case "Exists":
1900
- return [ast.field]
1901
- case "And":
1902
- case "Or":
1903
- return dedupeStrings(
1904
- Arr.flatMap(ast.conditions, (child) => fieldsIn(child, memo)),
1905
- )
1906
- case "Not":
1907
- return fieldsIn(ast.condition, memo)
1908
- default:
1909
- return []
1910
- }
1911
- })
1065
+ condition.pipe(nodeOf, Algebra.fields)
1912
1066
 
1913
1067
  // Satisfiability
1914
1068
 
@@ -1921,29 +1075,20 @@ const fieldsIn = (
1921
1075
  * @category models
1922
1076
  * @since 1.0.0
1923
1077
  */
1924
- export interface Example {
1925
- readonly [field: string]: unknown
1926
- }
1078
+ export type Example = Search.Example
1927
1079
 
1928
1080
  /**
1929
1081
  * The answer of {@link satisfiable}:
1930
1082
  * - `Satisfiable`: some value makes the condition true. `example` is one.
1931
1083
  * - `Unsatisfiable`: no value makes the condition true.
1932
- * - `Unknown`: the search can not say. The condition has a predicate, the
1933
- * search went past its budget, or it set two fields that the Schema ties
1934
- * together, as below a union or a field that may be missing.
1084
+ * - `Unknown`: the search can not say. The condition has a predicate, or
1085
+ * the search went past its budget.
1935
1086
  *
1936
1087
  * @stability stable
1937
1088
  * @category models
1938
1089
  * @since 1.0.0
1939
1090
  */
1940
- export type Satisfiability = Data.TaggedEnum<{
1941
- Satisfiable: { readonly example: Example }
1942
- Unsatisfiable: {}
1943
- Unknown: { readonly reason: "predicate" | "budget" | "schema" }
1944
- }>
1945
-
1946
- const satisfiability = Data.taggedEnum<Satisfiability>()
1091
+ export type Satisfiability = Search.Satisfiability
1947
1092
 
1948
1093
  /**
1949
1094
  * The options of {@link satisfiable}.
@@ -1954,676 +1099,62 @@ const satisfiability = Data.taggedEnum<Satisfiability>()
1954
1099
  */
1955
1100
  export interface SatisfiableOptions {
1956
1101
  /**
1957
- * The Schema of the values. It gives the domain of each field. Without
1958
- * it, a field may have any value.
1102
+ * The Schema of the values. Its shape gives the kinds of values of each
1103
+ * field. Without it, a field may have any value.
1959
1104
  */
1960
1105
  readonly schema?: Schema.Top | undefined
1961
- /** The most nodes that the search may visit. The default is 1000000. */
1106
+ /** The most steps that the search may take. The default is 1000000. */
1962
1107
  readonly budget?: number | undefined
1963
1108
  }
1964
1109
 
1965
- /** Stands for a value with parts, such as a struct or a date. */
1966
- const opaque: object = Object.freeze({})
1967
-
1968
- /** Marks a field that a step of the search did not fix. */
1969
- const free: unique symbol = Symbol("free")
1970
-
1971
- /** The least number that is greater than `value`. */
1972
- const nextUp = (value: number): number => {
1973
- if (Number.isNaN(value) || value === Infinity) {
1974
- return value
1975
- }
1976
- if (value === 0) {
1977
- return Number.MIN_VALUE
1978
- }
1979
- const view = new DataView(new ArrayBuffer(8))
1980
- view.setFloat64(0, value)
1981
- const bits = view.getBigUint64(0)
1982
- view.setBigUint64(0, value > 0 ? bits + 1n : bits - 1n)
1983
- return view.getFloat64(0)
1984
- }
1985
-
1986
- /**
1987
- * Gives one number from each part of the number line that the constants
1988
- * make: each constant, and one number between each two neighbours when there
1989
- * is one. The ends of the domain are in the list too.
1990
- */
1991
- const numberCells = (
1992
- kind: "integer" | "finite" | "any",
1993
- constants: ReadonlyArray<number>,
1994
- ): ReadonlyArray<number> => {
1995
- const integer = kind === "integer"
1996
- const low = integer
1997
- ? Number.MIN_SAFE_INTEGER
1998
- : kind === "finite"
1999
- ? -Number.MAX_VALUE
2000
- : -Infinity
2001
- const high = integer
2002
- ? Number.MAX_SAFE_INTEGER
2003
- : kind === "finite"
2004
- ? Number.MAX_VALUE
2005
- : Infinity
2006
- const known: ReadonlyArray<number> = Arr.filter(
2007
- constants,
2008
- (value: number) => !Number.isNaN(value),
2009
- )
2010
- const points = Arr.filter(
2011
- Arr.flatMap(known, (value: number) =>
2012
- integer && Number.isFinite(value)
2013
- ? [Math.floor(value), Math.ceil(value)]
2014
- : [value],
2015
- ),
2016
- (value: number) => value >= low && value <= high,
2017
- )
2018
- // `0` and `-0` are one point: every test gives the same answer for both,
2019
- // and `===` calls them equal.
2020
- const ordered: Array<number> = [low, high, ...points].sort((a, b) => a - b)
2021
- const sorted = Arr.dedupeAdjacentWith(
2022
- ordered,
2023
- (a: number, b: number) => a === b,
2024
- )
2025
- const cells = Arr.flatMap(sorted, (value, index) => {
2026
- const after = sorted[index + 1]
2027
- const between = integer ? value + 1 : nextUp(value)
2028
- return after !== undefined && between < after ? [value, between] : [value]
2029
- })
2030
- return kind === "any" ? Arr.append(cells, Number.NaN) : cells
2031
- }
2032
-
2033
- /**
2034
- * Gives one string from each part that the constants make. Strings are in the
2035
- * order of their UTF-16 code units, as `<` sees them. The least string after
2036
- * `a` is `a + "\0"`, so there is a string between `a` and `b` exactly when
2037
- * `b` is not `a + "\0"`.
2038
- */
2039
- const stringCells = (
2040
- constants: ReadonlyArray<string>,
2041
- ): ReadonlyArray<string> => {
2042
- const ordered: Array<string> = [...constants].sort()
2043
- const sorted = Arr.dedupeAdjacentWith(
2044
- ordered,
2045
- (a: string, b: string) => a === b,
2046
- )
2047
- if (sorted.length === 0) {
2048
- return [""]
2049
- }
2050
- return [
2051
- ...(sorted[0] === "" ? [] : [""]),
2052
- ...Arr.flatMap(sorted, (value, index) => {
2053
- const next = `${value}\0`
2054
- return next === sorted[index + 1] ? [value] : [value, next]
2055
- }),
2056
- ]
2057
- }
2058
-
2059
- /**
2060
- * Gives one value from each cell of a field. Each test of a field compares it
2061
- * with constants, so the constants cut the domain into a few cells, and every
2062
- * test gives the same answer for all the values of one cell.
2063
- */
2064
- const cellsOf = (
2065
- domain: Resource.FieldDomain,
2066
- constants: ReadonlyArray<Scalar>,
2067
- ): ReadonlyArray<unknown> =>
2068
- dedupeCells([
2069
- ...(domain.null ? [null] : []),
2070
- ...(domain.object ? [opaque] : []),
2071
- ...(domain.boolean ? [true, false] : []),
2072
- ...(domain.string
2073
- ? stringCells(Arr.filter(constants, (value) => typeof value === "string"))
2074
- : []),
2075
- ...(domain.number === "none"
2076
- ? []
2077
- : numberCells(
2078
- domain.number,
2079
- Arr.filter(constants, (value) => typeof value === "number"),
2080
- )),
2081
- ...domain.literals,
2082
- ])
2083
-
2084
- /**
2085
- * A text for a cell. Two cells get one text when every test gives the same
2086
- * answer for them: `0` and `-0` are one, and `NaN` is one.
2087
- */
2088
- const cellText = (cell: unknown): string =>
2089
- cell === opaque
2090
- ? "opaque"
2091
- : typeof cell === "number"
2092
- ? `number:${Object.is(cell, -0) ? "0" : String(cell)}`
2093
- : `${typeof cell}:${String(cell)}`
2094
-
2095
- /**
2096
- * Keeps the first cell of each text, in order. A map from each text to its
2097
- * first place does it in one pass. A test of each pair would take a very long
2098
- * time for many constants.
2099
- */
2100
- const dedupeCells = (cells: ReadonlyArray<unknown>): ReadonlyArray<unknown> => {
2101
- const texts = Arr.map(cells, cellText)
2102
- const first = HashMap.fromIterable(
2103
- Arr.reverse(Arr.map(texts, (text, index) => [text, index] as const)),
2104
- )
2105
- return Arr.filter(cells, (_, index) =>
2106
- Option.contains(HashMap.get(first, texts[index]!), index),
2107
- )
2108
- }
2109
-
2110
- class OutOfBudget {}
2111
-
2112
- /** What one search keeps for all its steps. It lives only as long as the search. */
2113
- interface Search {
2114
- /** The memo of the simplification and of the numbers of structures. */
2115
- readonly memo: Memo
2116
- readonly cells: (field: string) => ReadonlyArray<unknown>
2117
- /**
2118
- * The texts of the values of each `In` test, so one test of a value does
2119
- * not read every value of the test. See `cellText`.
2120
- */
2121
- readonly members: Map<Ast, HashSet.HashSet<string>>
2122
- readonly budget: number
2123
- }
2124
-
2125
- /** What a search has done up to now. Each step gives a new one. */
2126
- interface SearchState {
2127
- readonly steps: number
2128
- /** The numbers of the conditions that the search found to be unsatisfiable. */
2129
- readonly failed: HashSet.HashSet<number>
2130
- }
2131
-
2132
- interface Found {
2133
- readonly assignment: Option.Option<ReadonlyArray<readonly [string, unknown]>>
2134
- readonly state: SearchState
2135
- }
2136
-
2137
- const literalOf = (operand: Operand): Scalar => {
2138
- if (operand._tag !== "Literal") {
2139
- throw new Error(
2140
- "Condition.satisfiable needs a condition with no principal fields. Call Condition.partial first.",
2141
- )
2142
- }
2143
- return operand.value
2144
- }
2145
-
2146
- /**
2147
- * The value of the field `name` after `field` gets `value`. A field below
2148
- * `field` is `null` when `value` is not a value with parts.
2149
- */
2150
- const fixedValue = (
2151
- name: string,
2152
- field: string,
2153
- value: unknown,
2154
- ): unknown | typeof free =>
2155
- name === field
2156
- ? value
2157
- : name.startsWith(`${field}.`)
2158
- ? value === opaque
2159
- ? free
2160
- : null
2161
- : free
2162
-
2163
- const testFixed = (
2164
- ast: Compare | In | Exists,
2165
- value: unknown,
2166
- search: Search,
2167
- ): boolean => {
2168
- switch (ast._tag) {
2169
- case "Compare":
2170
- return testCompare(ast.op, value, literalOf(ast.value))
2171
- case "In":
2172
- // `===` finds no `NaN`, so the set has none, and `NaN` is in no test.
2173
- return (
2174
- !Number.isNaN(value) &&
2175
- HashSet.has(
2176
- remember(search.members, ast, () =>
2177
- HashSet.fromIterable(
2178
- Arr.map(
2179
- Arr.filter(
2180
- Arr.map(ast.values, literalOf),
2181
- (literal) => !Number.isNaN(literal),
2182
- ),
2183
- cellText,
2184
- ),
2185
- ),
2186
- ),
2187
- cellText(value),
2188
- )
2189
- )
2190
- case "Exists":
2191
- return value !== null
2192
- }
2193
- }
2194
-
2195
- /**
2196
- * Puts one value into a field, and into the fields below it. The memo has a
2197
- * node for each node that the step visits, so its size is the work of the
2198
- * step.
2199
- */
2200
- const fix = (
2201
- ast: Ast,
2202
- field: string,
2203
- value: unknown,
2204
- search: Search,
2205
- done: Map<Ast, Ast>,
2206
- ): Ast =>
2207
- remember(done, ast, () => {
2208
- const fixChild = (child: Ast) => fix(child, field, value, search, done)
2209
- switch (ast._tag) {
2210
- case "Compare":
2211
- case "In":
2212
- case "Exists": {
2213
- const known = fixedValue(ast.field, field, value)
2214
- return known === free
2215
- ? ast
2216
- : testFixed(ast, known, search)
2217
- ? always
2218
- : never
2219
- }
2220
- case "And":
2221
- return andOf(Arr.map(ast.conditions, fixChild))
2222
- case "Or":
2223
- return orOf(Arr.map(ast.conditions, fixChild))
2224
- case "Not":
2225
- return makeNot(fixChild(ast.condition))
2226
- default:
2227
- return ast
2228
- }
2229
- })
2230
-
2231
- /**
2232
- * Counts the tests of each field. Each node is visited one time, so the work
2233
- * is the number of nodes, and the walk gives that work back for the budget.
2234
- * The counts and the seen nodes are a memo of this walk only.
2235
- */
2236
- const countFields = (
2237
- ast: Ast,
2238
- ): {
2239
- readonly counts: MutableHashMap.MutableHashMap<string, number>
2240
- readonly work: number
2241
- } => {
2242
- const counts = MutableHashMap.empty<string, number>()
2243
- // A native set: it must find a node by the node itself. See `remember`.
2244
- const seen = new Set<Ast>()
2245
- const visit = (node: Ast): void => {
2246
- if (seen.has(node)) {
2247
- return
2248
- }
2249
- seen.add(node)
2250
- switch (node._tag) {
2251
- case "Compare":
2252
- case "In":
2253
- case "Exists":
2254
- MutableHashMap.set(
2255
- counts,
2256
- node.field,
2257
- Option.getOrElse(MutableHashMap.get(counts, node.field), () => 0) + 1,
2258
- )
2259
- return
2260
- case "And":
2261
- case "Or":
2262
- return Arr.forEach(node.conditions, visit)
2263
- case "Not":
2264
- return visit(node.condition)
2265
- default:
2266
- return
2267
- }
2268
- }
2269
- visit(ast)
2270
- return { counts, work: seen.size }
2271
- }
2272
-
2273
- /**
2274
- * Picks the next field to split on. A field comes before the fields below it,
2275
- * so a value of a field can fix them. Then a field in more tests comes first,
2276
- * and then the first name in order, so the choice is the same each time.
2277
- */
2278
- const nextField = (
2279
- counts: MutableHashMap.MutableHashMap<string, number>,
2280
- ): string => {
2281
- const depth = (field: string) => field.split(".").length
2282
- const [first, ...rest] = Array.from(counts)
2283
- return Arr.reduce(rest, first!, (best, next) =>
2284
- depth(next[0]) < depth(best[0]) ||
2285
- (depth(next[0]) === depth(best[0]) &&
2286
- (next[1] > best[1] || (next[1] === best[1] && next[0] < best[0])))
2287
- ? next
2288
- : best,
2289
- )[0]
2290
- }
2291
-
2292
- /**
2293
- * Finds values of the fields that make the condition true. It splits on one
2294
- * field at a time, and tries one value from each cell of the field.
2295
- */
2296
- const searchAst = (
2297
- ast: Ast,
2298
- search: Search,
2299
- state: SearchState,
2300
- assignment: ReadonlyArray<readonly [string, unknown]>,
2301
- ): Found => {
2302
- const current = simplifyIn(ast, search.memo)
2303
- if (current._tag === "True") {
2304
- return { assignment: Option.some(assignment), state }
2305
- }
2306
- if (current._tag === "False") {
2307
- return { assignment: Option.none(), state }
2308
- }
2309
- // A condition with the same structure has the same free fields, so its
2310
- // answer is the same.
2311
- const id = structureId(current, search.memo)
2312
- if (HashSet.has(state.failed, id)) {
2313
- return { assignment: Option.none(), state }
2314
- }
2315
- // Counting the fields reads the whole condition, so the budget counts it.
2316
- const counted = countFields(current)
2317
- if (state.steps + counted.work > search.budget) {
2318
- throw new OutOfBudget()
2319
- }
2320
- const field = nextField(counted.counts)
2321
- const cells = search.cells(field)
2322
- // The cells are tried in order, until one gives values. A fold over the
2323
- // cells does no work after a found answer, and it does not grow the stack
2324
- // with the number of cells.
2325
- const result = Arr.reduce(
2326
- cells,
2327
- { assignment: Option.none(), state } as Found,
2328
- (before, cell): Found => {
2329
- if (Option.isSome(before.assignment)) {
2330
- return before
2331
- }
2332
- const done = new Map<Ast, Ast>()
2333
- const fixed = fix(current, field, cell, search, done)
2334
- const steps = before.state.steps + done.size
2335
- if (steps > search.budget) {
2336
- throw new OutOfBudget()
2337
- }
2338
- return searchAst(fixed, search, { ...before.state, steps }, [
2339
- ...assignment,
2340
- [field, cell],
2341
- ])
2342
- },
2343
- )
2344
- return Option.isSome(result.assignment)
2345
- ? result
2346
- : {
2347
- assignment: Option.none(),
2348
- state: {
2349
- ...result.state,
2350
- failed: HashSet.add(result.state.failed, id),
2351
- },
2352
- }
2353
- }
2354
-
2355
- /**
2356
- * Puts a value at a path in an object, and gives a new object. A value with
2357
- * parts (`opaque`) becomes an empty object, and it keeps an object that is
2358
- * there.
2359
- */
2360
- const setPath = (
2361
- root: Readonly<Record<string, unknown>>,
2362
- names: ReadonlyArray<string>,
2363
- value: unknown,
2364
- ): Readonly<Record<string, unknown>> => {
2365
- const [name, ...rest] = names
2366
- if (name === undefined) {
2367
- return root
2368
- }
2369
- // Only an own field counts, as a condition reads it. An inherited one,
2370
- // such as `__proto__`, is not a field of the example.
2371
- const inner = Object.hasOwn(root, name) ? root[name] : undefined
2372
- if (rest.length === 0) {
2373
- return value === opaque && isObject(inner)
2374
- ? root
2375
- : { ...root, [name]: value === opaque ? {} : value }
2376
- }
2377
- const base =
2378
- isObject(inner) && inner !== opaque
2379
- ? (inner as Readonly<Record<string, unknown>>)
2380
- : {}
2381
- return { ...root, [name]: setPath(base, rest, value) }
2382
- }
2383
-
2384
- const exampleOf = (
2385
- assignment: ReadonlyArray<readonly [string, unknown]>,
2386
- ): Example =>
2387
- Arr.reduce(
2388
- assignment,
2389
- {} as Readonly<Record<string, unknown>>,
2390
- (root, [field, value]) => setPath(root, field.split("."), value),
2391
- )
2392
-
2393
- /**
2394
- * The constants that the tests of each field use. A set keeps each constant
2395
- * one time, so a shared node does not make the list grow for each path to it.
2396
- */
2397
- type FieldConstants = HashMap.HashMap<string, HashSet.HashSet<Scalar>>
2398
-
2399
- const addConstants = (
2400
- left: FieldConstants,
2401
- right: FieldConstants,
2402
- ): FieldConstants =>
2403
- HashMap.reduce(right, left, (all, values, field) =>
2404
- HashMap.set(
2405
- all,
2406
- field,
2407
- HashSet.union(
2408
- Option.getOrElse(HashMap.get(all, field), HashSet.empty),
2409
- values,
2410
- ),
2411
- ),
2412
- )
2413
-
2414
- const constantsOf = (
2415
- ast: Ast,
2416
- memo: Map<Ast, FieldConstants> = new Map(),
2417
- ): FieldConstants =>
2418
- remember(memo, ast, (): FieldConstants => {
2419
- switch (ast._tag) {
2420
- case "Compare":
2421
- return HashMap.make([ast.field, HashSet.make(literalOf(ast.value))])
2422
- case "In":
2423
- return HashMap.make([
2424
- ast.field,
2425
- HashSet.fromIterable(Arr.map(ast.values, literalOf)),
2426
- ])
2427
- case "Exists":
2428
- return HashMap.make([ast.field, HashSet.empty<Scalar>()])
2429
- case "Under":
2430
- throw new Error(
2431
- "Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.",
2432
- )
2433
- case "And":
2434
- case "Or":
2435
- return Arr.reduce(ast.conditions, HashMap.empty(), (all, child) =>
2436
- addConstants(all, constantsOf(child, memo)),
2437
- )
2438
- case "Not":
2439
- return constantsOf(ast.condition, memo)
2440
- default:
2441
- return HashMap.empty()
2442
- }
2443
- })
2444
-
2445
- /**
2446
- * The most fields that the search of {@link satisfiable} takes. Past it, the
2447
- * answer is `Unknown` with the reason `"budget"`.
2448
- */
2449
- const maxSearchFields = 256
2450
-
2451
- /**
2452
- * The most parts of a field path that the search of {@link satisfiable}
2453
- * takes. Past it, the answer is `Unknown` with the reason `"budget"`.
2454
- */
2455
- const maxSearchParts = 256
2456
-
2457
- const satisfiableAst = (
2458
- ast: Ast,
2459
- options: SatisfiableOptions = {},
2460
- ): Satisfiability => {
2461
- if (hasPredicate(ast)) {
2462
- return satisfiability.Unknown({ reason: "predicate" })
2463
- }
2464
- const schema = options.schema
2465
- const tested = fields(ast)
2466
- // A Schema with no value has no value that the condition is true for.
2467
- if (schema !== undefined && Resource.hasNoValue(schema)) {
2468
- return satisfiability.Unsatisfiable()
2469
- }
2470
- // The search finds values for the fields that the condition tests. A value
2471
- // needs the other required fields too, and a check that the search can
2472
- // not see may stop every value of one of them. So a "yes" needs a Schema
2473
- // that surely has a value.
2474
- const complete = schema === undefined || Resource.hasValue(schema)
2475
- // The model of the values of a field is exact only for a plain scalar
2476
- // field. For another field, a value may have data that the Schema does not
2477
- // show, so the search can not say.
2478
- if (
2479
- schema !== undefined &&
2480
- !Arr.every(tested, (field) => Resource.isPlainField(schema, field))
2481
- ) {
2482
- return satisfiability.Unknown({ reason: "schema" })
2483
- }
2484
- // The search calls itself one time for each field that it fixes, and it
2485
- // makes an example with a step for each part of a path. So a condition
2486
- // with very many fields, or a very long path, could fill the stack.
2487
- if (
2488
- tested.length > maxSearchFields ||
2489
- Arr.some(
2490
- tested,
2491
- (field) => field.split(".", maxSearchParts + 1).length > maxSearchParts,
2492
- )
2493
- ) {
2494
- return satisfiability.Unknown({ reason: "budget" })
2495
- }
2496
- const constants = constantsOf(ast)
2497
- // The cells of each field. A field name is a string, so an Effect map
2498
- // finds it.
2499
- const cells = MutableHashMap.empty<string, ReadonlyArray<unknown>>()
2500
- const search: Search = {
2501
- memo: makeMemo(),
2502
- members: new Map(),
2503
- cells: (field) =>
2504
- Option.getOrElse(MutableHashMap.get(cells, field), () => {
2505
- // The cells come from the constants, so many constants are work
2506
- // that the budget must count before the cells are made.
2507
- const known = HashMap.get(constants, field)
2508
- if (
2509
- Option.isSome(known) &&
2510
- HashSet.size(known.value) > (options.budget ?? 1_000_000)
2511
- ) {
2512
- throw new OutOfBudget()
2513
- }
2514
- const found = cellsOf(
2515
- schema === undefined
2516
- ? Resource.anyDomain
2517
- : Resource.domainOf(schema, field),
2518
- Array.from(
2519
- Option.getOrElse(HashMap.get(constants, field), HashSet.empty),
2520
- ),
2521
- )
2522
- MutableHashMap.set(cells, field, found)
2523
- return found
2524
- }),
2525
- budget: options.budget ?? 1_000_000,
2526
- }
2527
- try {
2528
- const found = searchAst(
2529
- ast,
2530
- search,
2531
- { steps: 0, failed: HashSet.empty() },
2532
- [],
2533
- )
2534
- return Option.match(found.assignment, {
2535
- onNone: () => satisfiability.Unsatisfiable(),
2536
- onSome: (assignment) =>
2537
- complete
2538
- ? satisfiability.Satisfiable({ example: exampleOf(assignment) })
2539
- : satisfiability.Unknown({ reason: "schema" }),
2540
- })
2541
- } catch (error) {
2542
- if (error instanceof OutOfBudget) {
2543
- return satisfiability.Unknown({ reason: "budget" })
2544
- }
2545
- throw error
2546
- }
2547
- }
2548
-
2549
1110
  /**
2550
1111
  * Tests if some value makes a condition true, and gives such a value.
2551
1112
  *
2552
1113
  * Call {@link partial} first: the condition must have no principal fields and
2553
1114
  * no `Under` nodes. A condition with a predicate gives `Unknown`.
2554
1115
  *
2555
- * With `options.schema`, each field of the condition must be a plain scalar
2556
- * field of the Schema (see `Resource.isPlainField`). Else the answer is
2557
- * `Unknown` with the reason `"schema"`: for another field, a value may have
2558
- * data that the Schema does not show, so the model of its values is not
2559
- * exact.
1116
+ * Each test of a condition compares one field with constants, so the
1117
+ * constants cut the values of each field into a few cells. The search tries
1118
+ * one value from each cell, one field at a time. The values of a field come
1119
+ * from the shape of the Schema in `options.schema`: its literals, `null` for
1120
+ * a nullable or optional field, and finite or integer numbers.
2560
1121
  *
2561
- * The answer is exact. Each test of a condition compares one field with
2562
- * constants, so the constants cut the values of each field into a few cells.
2563
- * The search tries one value from each cell, one field at a time. The values
2564
- * of a field come from the Schema in `options.schema`: its literals, `null`
2565
- * for a nullable or optional field, and finite or integer numbers. A field
2566
- * with another Schema check is not a plain field, so the search never tries a
2567
- * value that such a check stops.
1122
+ * The search reads the shape of the Schema, not its other checks, such as a
1123
+ * minimum length. So `Unsatisfiable` holds for every value of the Schema. A
1124
+ * `Satisfiable` example fits the shape, but a check may still stop it.
2568
1125
  *
2569
1126
  * The problem is as hard as boolean satisfiability, so the search has a
2570
1127
  * budget. Past it, the answer is `Unknown`. A condition with more than 256
2571
- * fields, or with a field path of more than 256 parts, gives `Unknown` too.
1128
+ * fields gives `Unknown` too.
2572
1129
  *
2573
1130
  * @stability stable
2574
1131
  * @category evaluation
2575
1132
  * @since 1.0.0
2576
1133
  */
2577
- export const satisfiable: {
2578
- (options?: SatisfiableOptions): (condition: Any) => Satisfiability
2579
- (condition: Any, options?: SatisfiableOptions): Satisfiability
2580
- } = dual(
2581
- (args) => isObject(args[0]) && hasProperty(args[0], "_tag"),
2582
- satisfiableAst,
2583
- ) as never
2584
-
2585
- const operandJson = (operand: Operand): unknown =>
2586
- operand._tag === "Literal" &&
2587
- typeof operand.value === "number" &&
2588
- !Number.isFinite(operand.value)
2589
- ? { ...operand, value: { nonFinite: String(operand.value) } }
2590
- : operand
2591
-
2592
- const jsonIn = (ast: Ast, done: Map<Ast, unknown>): unknown =>
2593
- remember(done, ast, (): unknown => {
2594
- switch (ast._tag) {
2595
- case "Compare":
2596
- return { ...ast, value: operandJson(ast.value) }
2597
- case "In":
2598
- return { ...ast, values: Arr.map(ast.values, operandJson) }
2599
- case "Under":
2600
- return { ...ast, prefix: Arr.map(ast.prefix, operandJson) }
2601
- case "And":
2602
- case "Or":
2603
- return {
2604
- ...ast,
2605
- conditions: Arr.map(ast.conditions, (child) => jsonIn(child, done)),
2606
- }
2607
- case "Not":
2608
- return { ...ast, condition: jsonIn(ast.condition, done) }
2609
- default:
2610
- return ast
2611
- }
2612
- })
1134
+ export const satisfiable = (
1135
+ condition: Any,
1136
+ options: SatisfiableOptions = {},
1137
+ ): Satisfiability =>
1138
+ Search.satisfiable(
1139
+ nodeOf(condition),
1140
+ Search.kindsIn(
1141
+ options.schema === undefined
1142
+ ? undefined
1143
+ : Catalog.catalogOf(options.schema.ast),
1144
+ ),
1145
+ options.budget,
1146
+ )
2613
1147
 
2614
1148
  /**
2615
- * Gives a condition as a value that `JSON.stringify` keeps with no loss. JSON
2616
- * has no `Infinity`, `-Infinity` or `NaN`: it writes `null` in their place,
2617
- * and a test with `null` has another meaning. Such a number becomes an object
2618
- * that {@link schema} does not decode, so a policy with it fails to load and
2619
- * never gives a wrong answer.
1149
+ * The JSON form of an operand.
2620
1150
  *
2621
1151
  * @stability stable
2622
- * @category conversions
2623
- * @since 1.0.0
1152
+ * @category models
1153
+ * @since 2.0.0
2624
1154
  */
2625
- export const toJsonValue = (condition: Ast): unknown =>
2626
- jsonIn(condition, new Map())
1155
+ export type EncodedOperand =
1156
+ | { readonly _tag: "Literal"; readonly value: Scalar }
1157
+ | { readonly _tag: "Principal"; readonly path: string }
2627
1158
 
2628
1159
  /**
2629
1160
  * The JSON form of a condition with no predicate.
@@ -2633,49 +1164,26 @@ export const toJsonValue = (condition: Ast): unknown =>
2633
1164
  * @since 0.3.0
2634
1165
  */
2635
1166
  export type Encoded =
2636
- Exclude<Ast, Predicate | And | Or | Not> | EncodedAnd | EncodedOr | EncodedNot
2637
-
2638
- /**
2639
- * @stability stable
2640
- * @category models
2641
- * @since 0.3.0
2642
- */
2643
- export interface EncodedAnd {
2644
- readonly _tag: "And"
2645
- readonly conditions: ReadonlyArray<Encoded>
2646
- }
2647
-
2648
- /**
2649
- * @stability stable
2650
- * @category models
2651
- * @since 0.3.0
2652
- */
2653
- export interface EncodedOr {
2654
- readonly _tag: "Or"
2655
- readonly conditions: ReadonlyArray<Encoded>
2656
- }
2657
-
2658
- /**
2659
- * @stability stable
2660
- * @category models
2661
- * @since 0.3.0
2662
- */
2663
- export interface EncodedNot {
2664
- readonly _tag: "Not"
2665
- readonly condition: Encoded
2666
- }
2667
-
2668
- const ScalarSchema = Schema.Union([
2669
- Schema.String,
2670
- Schema.Finite,
2671
- Schema.Boolean,
2672
- Schema.Null,
2673
- ])
2674
-
2675
- const OperandSchema = Schema.Union([
2676
- Schema.TaggedStruct("Literal", { value: ScalarSchema }),
2677
- Schema.TaggedStruct("Principal", { path: Schema.String }),
2678
- ])
1167
+ | { readonly _tag: "True" }
1168
+ | { readonly _tag: "False" }
1169
+ | {
1170
+ readonly _tag: "Compare"
1171
+ readonly op: CompareOperator
1172
+ readonly field: string
1173
+ readonly value: EncodedOperand
1174
+ }
1175
+ | {
1176
+ readonly _tag: "In"
1177
+ readonly field: string
1178
+ readonly values: ReadonlyArray<EncodedOperand>
1179
+ }
1180
+ | { readonly _tag: "Exists"; readonly field: string }
1181
+ | { readonly _tag: "Under"; readonly prefix: ReadonlyArray<EncodedOperand> }
1182
+ | {
1183
+ readonly _tag: "And" | "Or"
1184
+ readonly conditions: ReadonlyArray<Encoded>
1185
+ }
1186
+ | { readonly _tag: "Not"; readonly condition: Encoded }
2679
1187
 
2680
1188
  /**
2681
1189
  * A condition with no predicate, that may read any values. It is the type of
@@ -2689,309 +1197,45 @@ const OperandSchema = Schema.Union([
2689
1197
  */
2690
1198
  // `any` for what it reads: the field is contravariant, so no other type both
2691
1199
  // takes every condition and fits every rule.
2692
- export type PlainCondition = Condition<never, never, never, any>
2693
-
2694
- const Recursive = Schema.suspend(
2695
- (): Schema.Codec<PlainCondition, Encoded> => shape,
2696
- )
2697
-
2698
- /**
2699
- * The most levels of objects and arrays in the JSON form of a condition. The
2700
- * Schema of a condition calls itself for each level, so a much deeper input
2701
- * could fill the stack before it fails.
2702
- */
2703
- const maxJsonDepth = 1000
2704
-
2705
- /**
2706
- * The most objects and arrays in the JSON form of an input, with a shared one
2707
- * counted for each place. The decoder writes out a shared one each time, so
2708
- * an input with few objects may give a very large tree.
2709
- */
2710
- const maxInputNodes = 1_000_000
2711
-
2712
- /** The size and the height of a value, in objects and arrays. */
2713
- interface Measure {
2714
- readonly size: number
2715
- readonly height: number
2716
- }
2717
-
2718
- /**
2719
- * Measures a value, or gives `undefined` when it is higher than `levels`. A
2720
- * shared object is measured one time. The walk stops at that height, so it
2721
- * does not fill the stack itself. The memo is a native `Map`, because it
2722
- * must find an object by the object itself.
2723
- */
2724
- const measure = (
2725
- input: unknown,
2726
- levels: number,
2727
- memo: Map<object, Measure | { readonly failed: number }>,
2728
- ): Measure | undefined => {
2729
- if (typeof input !== "object" || input === null) {
2730
- return { size: 0, height: 0 }
2731
- }
2732
- const cached = memo.get(input)
2733
- // A value that was too high for some levels is too high for fewer.
2734
- if (
2735
- cached !== undefined &&
2736
- ("failed" in cached ? levels <= cached.failed : true)
2737
- ) {
2738
- return "failed" in cached || cached.height > levels ? undefined : cached
2739
- }
2740
- // After the first child that is too high, the fold measures no more
2741
- // children. The walk has few frames for each level, so it does not fill
2742
- // the stack at the most levels.
2743
- // An input with a key that a loop over the keys does not see, or with a
2744
- // getter, may give the decoder more than the measure sees. Such an input
2745
- // is not plain data, so it fails.
2746
- const result =
2747
- levels === 0 || !isPlainData(input)
2748
- ? undefined
2749
- : Arr.reduce(
2750
- Object.values(input),
2751
- { size: 1, height: 1 } as Measure | undefined,
2752
- (total, child) => {
2753
- const measured =
2754
- total === undefined ? undefined : measure(child, levels - 1, memo)
2755
- return total === undefined || measured === undefined
2756
- ? undefined
2757
- : {
2758
- size: total.size + measured.size,
2759
- height: Math.max(total.height, 1 + measured.height),
2760
- }
2761
- },
2762
- )
2763
- memo.set(input, result ?? { failed: levels })
2764
- return result
2765
- }
2766
-
2767
- /**
2768
- * Tests if each own key of an object is a plain data key that a loop over the
2769
- * keys sees. The `length` of an array is the one key that is not seen. The
2770
- * decoder reads keys of the prototypes too, so a prototype other than the
2771
- * one of a plain object or an array may have only methods, as the prototype
2772
- * of a class has.
2773
- */
2774
- const isPlainData = (input: object): boolean =>
2775
- Arr.every(Reflect.ownKeys(input), (key) => {
2776
- const property = Object.getOwnPropertyDescriptor(input, key)
2777
- return (
2778
- property !== undefined &&
2779
- "value" in property &&
2780
- (property.enumerable === true ||
2781
- (Array.isArray(input) && key === "length"))
2782
- )
2783
- }) && hasOnlyMethods(Object.getPrototypeOf(input))
2784
-
2785
- /** Tests if the prototypes from `prototype` up have only methods. */
2786
- const hasOnlyMethods = (prototype: unknown): boolean =>
2787
- prototype === null ||
2788
- prototype === Object.prototype ||
2789
- prototype === Array.prototype ||
2790
- (typeof prototype === "object" &&
2791
- Arr.every(Reflect.ownKeys(prototype), (key) => {
2792
- const property = Object.getOwnPropertyDescriptor(prototype, key)
2793
- return (
2794
- typeof key === "symbol" ||
2795
- key === "constructor" ||
2796
- (property !== undefined &&
2797
- "value" in property &&
2798
- typeof property.value === "function")
2799
- )
2800
- }) &&
2801
- hasOnlyMethods(Object.getPrototypeOf(prototype)))
2802
-
2803
- /** Tests if a value is low and small enough to decode. */
2804
- const fitsLimits = (input: unknown): string | true => {
2805
- const measured = measure(input, maxJsonDepth, new Map())
2806
- return measured === undefined
2807
- ? `A condition may have at most ${String(maxJsonDepth)} levels of JSON`
2808
- : measured.size > maxInputNodes
2809
- ? `A condition may have at most ${String(maxInputNodes)} objects and arrays in JSON`
2810
- : true
2811
- }
2812
-
2813
- /** The shape of a condition, with no check of its depth. */
2814
- const shape: Schema.Codec<PlainCondition, Encoded> = Schema.Union([
2815
- Schema.TaggedStruct("True", {}),
2816
- Schema.TaggedStruct("False", {}),
2817
- Schema.TaggedStruct("Compare", {
2818
- op: Schema.Literals(["eq", "ne", "lt", "lte", "gt", "gte"]),
2819
- field: Schema.String,
2820
- value: OperandSchema,
2821
- }),
2822
- Schema.TaggedStruct("In", {
2823
- field: Schema.String,
2824
- values: Schema.Array(OperandSchema),
2825
- }),
2826
- Schema.TaggedStruct("Exists", { field: Schema.String }),
2827
- Schema.TaggedStruct("Under", { prefix: Schema.Array(OperandSchema) }),
2828
- Schema.TaggedStruct("And", { conditions: Schema.Array(Recursive) }),
2829
- Schema.TaggedStruct("Or", { conditions: Schema.Array(Recursive) }),
2830
- Schema.TaggedStruct("Not", { condition: Recursive }),
2831
- ]) as never
1200
+ export type PlainCondition = Condition<any>
2832
1201
 
2833
1202
  /**
2834
1203
  * The Schema of a condition with no predicate. It checks the shape only. Use
2835
1204
  * `Policy.schema` to check the field names against a domain too.
2836
1205
  *
1206
+ * The decoder takes at most 500 levels of conditions, and at most 1000000
1207
+ * objects and array items, with a shared object counted for each place. It
1208
+ * reads only the own data fields of the input.
1209
+ *
2837
1210
  * @stability stable
2838
1211
  * @category schemas
2839
1212
  * @since 0.3.0
2840
1213
  */
2841
1214
  export const schema: Schema.Codec<PlainCondition, Encoded> =
2842
- // The input is any value until the checks of `shape` run, so this guard
2843
- // says yes to every object.
2844
- Schema.declare(
2845
- (input): input is Encoded => typeof input === "object" && input !== null,
2846
- ).pipe(
2847
- Schema.check(Schema.makeFilter(fitsLimits)),
2848
- Schema.decodeTo(shape, SchemaTransformation.passthrough()),
2849
- // A condition may use one child in many places, and its JSON form
2850
- // writes it out each time. An encode runs this step first, so a small
2851
- // condition with a very large JSON form fails before it is written out.
1215
+ Schema.Unknown.pipe(
2852
1216
  Schema.decodeTo(
2853
- Schema.declare((input): input is PlainCondition =>
2854
- isPlainNode(input, new Map(), 0),
1217
+ Schema.declare(
1218
+ (input): input is PlainCondition =>
1219
+ Nodes.isNode(input) && !input.opaque,
2855
1220
  ),
2856
1221
  SchemaTransformation.transformEffect({
2857
- decode: (condition: PlainCondition) => Effect.succeed(condition),
2858
- encode: (condition: PlainCondition, options) =>
2859
- jsonSize(condition as Ast, new Map()) > maxJsonNodes
2860
- ? Effect.fail(
2861
- new SchemaIssue.InvalidValue(
2862
- {
2863
- message: `The JSON form of this condition would have more than ${String(maxJsonNodes)} nodes`,
2864
- },
2865
- condition,
2866
- options,
1222
+ decode: (input: unknown, options) =>
1223
+ Result.match(Json.decode(input), {
1224
+ onSuccess: (node) => Effect.succeed(node as never),
1225
+ onFailure: (problem) =>
1226
+ Effect.fail(
1227
+ new SchemaIssue.Pointer(
1228
+ problem.path,
1229
+ new SchemaIssue.InvalidValue(
1230
+ { message: problem.message },
1231
+ input,
1232
+ options,
1233
+ ),
2867
1234
  ),
2868
- )
2869
- : Effect.succeed(condition),
1235
+ ),
1236
+ }),
1237
+ encode: (condition: PlainCondition) =>
1238
+ Effect.sync(() => Json.toJson(condition as never)),
2870
1239
  }),
2871
1240
  ),
2872
- )
2873
-
2874
- const compareOperators = HashSet.make("eq", "ne", "lt", "lte", "gt", "gte")
2875
-
2876
- /**
2877
- * Tests if a value is an operand. With `json`, its number must be finite, as
2878
- * the JSON form holds only such numbers.
2879
- */
2880
- const isOperandValue = (operand: unknown, json: boolean): boolean =>
2881
- hasProperty(operand, "_tag") &&
2882
- (operand._tag === "Principal"
2883
- ? hasProperty(operand, "path") && typeof operand.path === "string"
2884
- : operand._tag === "Literal" &&
2885
- hasProperty(operand, "value") &&
2886
- (operand.value === null ||
2887
- typeof operand.value === "string" ||
2888
- typeof operand.value === "boolean" ||
2889
- (typeof operand.value === "number" &&
2890
- (!json || Number.isFinite(operand.value)))))
2891
-
2892
- const isOperandList = (values: unknown, json: boolean): boolean =>
2893
- Array.isArray(values) &&
2894
- Arr.every(values, (operand) => isOperandValue(operand, json))
2895
-
2896
- /**
2897
- * Tests if a value is a whole condition with no predicate, as the JSON form
2898
- * holds it. A shared node is tested one time. A node deeper than
2899
- * `maxJsonDepth` is not taken, so the walk does not fill the stack.
2900
- */
2901
- const isPlainNode = (
2902
- input: unknown,
2903
- memo: Map<unknown, boolean>,
2904
- depth: number,
2905
- // `true` for a tree in memory: it may have predicates and any number.
2906
- predicates = false,
2907
- ): boolean =>
2908
- depth <= maxJsonDepth &&
2909
- hasProperty(input, "_tag") &&
2910
- remember(memo, input, () => {
2911
- const child = (node: unknown) =>
2912
- isPlainNode(node, memo, depth + 1, predicates)
2913
- switch (input._tag) {
2914
- case "Predicate":
2915
- return (
2916
- predicates &&
2917
- hasProperty(input, "name") &&
2918
- typeof input.name === "string" &&
2919
- hasProperty(input, "run") &&
2920
- typeof input.run === "function"
2921
- )
2922
- case "True":
2923
- case "False":
2924
- return true
2925
- case "Compare":
2926
- return (
2927
- hasProperty(input, "op") &&
2928
- typeof input.op === "string" &&
2929
- HashSet.has(compareOperators, input.op) &&
2930
- hasProperty(input, "field") &&
2931
- typeof input.field === "string" &&
2932
- hasProperty(input, "value") &&
2933
- isOperandValue(input.value, !predicates)
2934
- )
2935
- case "In":
2936
- return (
2937
- hasProperty(input, "field") &&
2938
- typeof input.field === "string" &&
2939
- hasProperty(input, "values") &&
2940
- isOperandList(input.values, !predicates)
2941
- )
2942
- case "Exists":
2943
- return hasProperty(input, "field") && typeof input.field === "string"
2944
- case "Under":
2945
- return (
2946
- hasProperty(input, "prefix") &&
2947
- isOperandList(input.prefix, !predicates)
2948
- )
2949
- case "And":
2950
- case "Or":
2951
- return (
2952
- hasProperty(input, "conditions") &&
2953
- Array.isArray(input.conditions) &&
2954
- Arr.every(input.conditions, child)
2955
- )
2956
- case "Not":
2957
- return hasProperty(input, "condition") && child(input.condition)
2958
- default:
2959
- return false
2960
- }
2961
- })
2962
-
2963
- /**
2964
- * Tests if a value is a whole condition tree: each node has a known tag and
2965
- * the fields of that tag. A predicate node needs a name and a test. A tree
2966
- * deeper than 1000 levels is not taken.
2967
- *
2968
- * @stability stable
2969
- * @category guards
2970
- * @since 1.0.0
2971
- */
2972
- export const isAst = (u: unknown): u is Ast =>
2973
- isPlainNode(u, new Map(), 0, true)
2974
-
2975
- /** The most nodes of the JSON form of a condition. */
2976
- const maxJsonNodes = 100_000
2977
-
2978
- /**
2979
- * The number of nodes of the JSON form of a condition. A shared child counts
2980
- * one time for each place.
2981
- */
2982
- const jsonSize = (ast: Ast, memo: Map<Ast, number>): number =>
2983
- remember(memo, ast, () => {
2984
- switch (ast._tag) {
2985
- case "And":
2986
- case "Or":
2987
- return Arr.reduce(
2988
- ast.conditions,
2989
- 1,
2990
- (size, child) => size + jsonSize(child, memo),
2991
- )
2992
- case "Not":
2993
- return 1 + jsonSize(ast.condition, memo)
2994
- default:
2995
- return 1
2996
- }
2997
- })
1241
+ ) as never