@nmnmcc/ability 0.2.0 → 1.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 (58) hide show
  1. package/README.md +462 -454
  2. package/dist/Authorizer.d.ts +241 -0
  3. package/dist/Authorizer.d.ts.map +1 -0
  4. package/dist/Authorizer.js +340 -0
  5. package/dist/Authorizer.js.map +1 -0
  6. package/dist/Condition.d.ts +915 -0
  7. package/dist/Condition.d.ts.map +1 -0
  8. package/dist/Condition.js +1123 -0
  9. package/dist/Condition.js.map +1 -0
  10. package/dist/Decision.d.ts +131 -0
  11. package/dist/Decision.d.ts.map +1 -0
  12. package/dist/Decision.js +83 -0
  13. package/dist/Decision.js.map +1 -0
  14. package/dist/Domain.d.ts +158 -0
  15. package/dist/Domain.d.ts.map +1 -0
  16. package/dist/Domain.js +76 -0
  17. package/dist/Domain.js.map +1 -0
  18. package/dist/HttpApi.d.ts +173 -0
  19. package/dist/HttpApi.d.ts.map +1 -0
  20. package/dist/HttpApi.js +185 -0
  21. package/dist/HttpApi.js.map +1 -0
  22. package/dist/Policy.d.ts +687 -0
  23. package/dist/Policy.d.ts.map +1 -0
  24. package/dist/Policy.js +1339 -0
  25. package/dist/Policy.js.map +1 -0
  26. package/dist/Principal.d.ts +35 -0
  27. package/dist/Principal.d.ts.map +1 -0
  28. package/dist/Principal.js +37 -0
  29. package/dist/Principal.js.map +1 -0
  30. package/dist/Resource.d.ts +364 -0
  31. package/dist/Resource.d.ts.map +1 -0
  32. package/dist/Resource.js +844 -0
  33. package/dist/Resource.js.map +1 -0
  34. package/dist/Rule.d.ts +275 -0
  35. package/dist/Rule.d.ts.map +1 -0
  36. package/dist/Rule.js +257 -0
  37. package/dist/Rule.js.map +1 -0
  38. package/dist/_virtual/_rolldown/runtime.js +1 -2
  39. package/dist/index.d.ts +9 -3
  40. package/dist/index.js +9 -3
  41. package/package.json +61 -72
  42. package/src/Authorizer.ts +894 -0
  43. package/src/Condition.ts +2997 -0
  44. package/src/Decision.ts +117 -0
  45. package/src/Domain.ts +246 -0
  46. package/src/HttpApi.ts +348 -0
  47. package/src/Policy.ts +3315 -0
  48. package/src/Principal.ts +41 -0
  49. package/src/Resource.ts +1805 -0
  50. package/src/Rule.ts +683 -0
  51. package/src/index.ts +57 -0
  52. package/dist/Ability.d.ts +0 -491
  53. package/dist/Ability.js +0 -207
  54. package/dist/AbilityExtra.d.ts +0 -79
  55. package/dist/AbilityExtra.js +0 -187
  56. package/dist/internal/ability.d.ts +0 -90
  57. package/dist/internal/ability.js +0 -550
  58. package/dist/internal/serializable.js +0 -22
@@ -0,0 +1,2997 @@
1
+ /**
2
+ * A condition is a test on a resource value. A rule applies only when its
3
+ * condition is true.
4
+ *
5
+ * A condition is plain data: a tree of nodes. So you can:
6
+ * - test it on a value with {@link evaluate};
7
+ * - put the principal into it and make it smaller with {@link partial}, which
8
+ * gives a "residual" condition: the part that still depends on the
9
+ * resource;
10
+ * - change it into a database query with {@link fold};
11
+ * - keep it as JSON with {@link schema}.
12
+ *
13
+ * Make conditions with a {@link Builder}. A rule gives you a builder for its
14
+ * resource, so the field names and the value types are checked.
15
+ *
16
+ * {@link Builder.predicate} is the one way out of plain data. A predicate runs
17
+ * any `Effect`. Its error type and its service type go into the type of the
18
+ * check. A rule with a predicate can not become JSON, and the type system
19
+ * stops you when you try.
20
+ *
21
+ * @stability stable
22
+ * @since 0.3.0
23
+ */
24
+ // A predicate keeps its error type and service type in the public types. The
25
+ // code below works on the nodes with no types, where they are `unknown`, so
26
+ // the language service check for `unknown` in an Effect is off here.
27
+ // @effect-diagnostics anyUnknownInErrorContext:off
28
+ import * as Arr from "effect/Array"
29
+ import type * as BigDecimal from "effect/BigDecimal"
30
+ import type * as Cause from "effect/Cause"
31
+ import type * as Chunk from "effect/Chunk"
32
+ import type * as DateTime from "effect/DateTime"
33
+ import type * as Duration from "effect/Duration"
34
+ import type * as Redacted from "effect/Redacted"
35
+ import type * as Result from "effect/Result"
36
+ import type * as Context from "effect/Context"
37
+ import * as Data from "effect/Data"
38
+ import * as Effect from "effect/Effect"
39
+ import * as Option from "effect/Option"
40
+ 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"
45
+ import * as Schema from "effect/Schema"
46
+ import * as SchemaIssue from "effect/SchemaIssue"
47
+ import * as SchemaTransformation from "effect/SchemaTransformation"
48
+ import * as Resource from "./Resource.ts"
49
+
50
+ /**
51
+ * A value that a condition can compare.
52
+ *
53
+ * @stability stable
54
+ * @category models
55
+ * @since 0.3.0
56
+ */
57
+ export type Scalar = string | number | boolean | null
58
+
59
+ /**
60
+ * A fixed value in a condition.
61
+ *
62
+ * @stability stable
63
+ * @category models
64
+ * @since 0.3.0
65
+ */
66
+ export interface Literal {
67
+ readonly _tag: "Literal"
68
+ readonly value: Scalar
69
+ }
70
+
71
+ /**
72
+ * A field of the principal. {@link partial} puts the value of the field in its
73
+ * place.
74
+ *
75
+ * A principal field that is `null` or missing has no value. A `Compare`, `In`
76
+ * or `Under` test with it is always false, also with `ne`. So a principal with
77
+ * no id never matches a resource with no owner. `not` still turns false into
78
+ * true.
79
+ *
80
+ * @stability stable
81
+ * @category models
82
+ * @since 0.3.0
83
+ */
84
+ export interface PrincipalRef {
85
+ readonly _tag: "Principal"
86
+ readonly path: string
87
+ }
88
+
89
+ /**
90
+ * 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.
96
+ *
97
+ * @stability stable
98
+ * @category models
99
+ * @since 0.3.0
100
+ */
101
+ export type Ref<A, P = never> = PrincipalRef & {
102
+ readonly "~Ref": () => A
103
+ readonly "~Principal": (principal: P) => void
104
+ }
105
+
106
+ /**
107
+ * @stability stable
108
+ * @category models
109
+ * @since 0.3.0
110
+ */
111
+ export type Operand = Literal | PrincipalRef
112
+
113
+ /**
114
+ * @stability stable
115
+ * @category models
116
+ * @since 0.3.0
117
+ */
118
+ export type CompareOperator = "eq" | "ne" | "lt" | "lte" | "gt" | "gte"
119
+
120
+ /**
121
+ * Always true.
122
+ *
123
+ * @stability stable
124
+ * @category models
125
+ * @since 0.3.0
126
+ */
127
+ export interface True {
128
+ readonly _tag: "True"
129
+ }
130
+
131
+ /**
132
+ * Always false.
133
+ *
134
+ * @stability stable
135
+ * @category models
136
+ * @since 0.3.0
137
+ */
138
+ export interface False {
139
+ readonly _tag: "False"
140
+ }
141
+
142
+ /**
143
+ * Compares a field with a value. A missing field has the value `null`. `lt`,
144
+ * `lte`, `gt` and `gte` are false when the two sides are not both numbers or
145
+ * both strings.
146
+ *
147
+ * @stability stable
148
+ * @category models
149
+ * @since 0.3.0
150
+ */
151
+ export interface Compare {
152
+ readonly _tag: "Compare"
153
+ readonly op: CompareOperator
154
+ readonly field: string
155
+ readonly value: Operand
156
+ }
157
+
158
+ /**
159
+ * True when a field is equal to one of the values.
160
+ *
161
+ * @stability stable
162
+ * @category models
163
+ * @since 0.3.0
164
+ */
165
+ export interface In {
166
+ readonly _tag: "In"
167
+ readonly field: string
168
+ readonly values: ReadonlyArray<Operand>
169
+ }
170
+
171
+ /**
172
+ * True when a field is not `null` and not missing.
173
+ *
174
+ * @stability stable
175
+ * @category models
176
+ * @since 0.3.0
177
+ */
178
+ export interface Exists {
179
+ readonly _tag: "Exists"
180
+ readonly field: string
181
+ }
182
+
183
+ /**
184
+ * True when the path of the resource starts with the prefix. A resource with
185
+ * no path is never under a prefix.
186
+ *
187
+ * @stability stable
188
+ * @category models
189
+ * @since 0.3.0
190
+ */
191
+ export interface Under {
192
+ readonly _tag: "Under"
193
+ readonly prefix: ReadonlyArray<Operand>
194
+ }
195
+
196
+ /**
197
+ * @stability stable
198
+ * @category models
199
+ * @since 0.3.0
200
+ */
201
+ export interface And {
202
+ readonly _tag: "And"
203
+ readonly conditions: ReadonlyArray<Ast>
204
+ }
205
+
206
+ /**
207
+ * @stability stable
208
+ * @category models
209
+ * @since 0.3.0
210
+ */
211
+ export interface Or {
212
+ readonly _tag: "Or"
213
+ readonly conditions: ReadonlyArray<Ast>
214
+ }
215
+
216
+ /**
217
+ * @stability stable
218
+ * @category models
219
+ * @since 0.3.0
220
+ */
221
+ export interface Not {
222
+ readonly _tag: "Not"
223
+ readonly condition: Ast
224
+ }
225
+
226
+ /**
227
+ * A test that runs an `Effect`. It can not become JSON or a query.
228
+ *
229
+ * @stability stable
230
+ * @category models
231
+ * @since 0.3.0
232
+ */
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
+ }
240
+
241
+ /**
242
+ * The nodes of a condition, without types.
243
+ *
244
+ * @stability stable
245
+ * @category models
246
+ * @since 0.3.0
247
+ */
248
+ export type Ast =
249
+ True | False | Compare | In | Exists | Under | And | Or | Not | Predicate
250
+
251
+ /**
252
+ * The type marker for a condition with a {@link Predicate}.
253
+ *
254
+ * @stability stable
255
+ * @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
278
+ */
279
+ export interface Reads<V, P> {
280
+ readonly value: V
281
+ readonly principal: P
282
+ }
283
+
284
+ /**
285
+ * The type fields of a condition. They are only in the types.
286
+ *
287
+ * @stability stable
288
+ * @category models
289
+ * @since 1.0.0
290
+ */
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
+ }
296
+
297
+ /**
298
+ * An `And` node with the types of its children.
299
+ *
300
+ * @stability stable
301
+ * @category models
302
+ * @since 1.0.0
303
+ */
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
+ }
308
+
309
+ /**
310
+ * An `Or` node with the types of its children.
311
+ *
312
+ * @stability stable
313
+ * @category models
314
+ * @since 1.0.0
315
+ */
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
+ }
320
+
321
+ /**
322
+ * A `Not` node with the types of its child.
323
+ *
324
+ * @stability stable
325
+ * @category models
326
+ * @since 1.0.0
327
+ */
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>
331
+ }
332
+
333
+ /**
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}.
337
+ *
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.
358
+ *
359
+ * @stability stable
360
+ * @category models
361
+ * @since 1.0.0
362
+ */
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.
369
+ readonly "~Reads": (reads: I) => void
370
+ }
371
+
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
+ /**
396
+ * Any condition.
397
+ *
398
+ * @stability stable
399
+ * @category models
400
+ * @since 0.3.0
401
+ */
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
429
+
430
+ /**
431
+ * What the predicates of a condition read. For a union of conditions, it is
432
+ * what all of them read.
433
+ *
434
+ * @stability stable
435
+ * @category utility types
436
+ * @since 1.0.0
437
+ */
438
+ export type ReadsOf<C> = [C] extends [
439
+ { readonly "~Reads": (reads: infer I) => void },
440
+ ]
441
+ ? I
442
+ : unknown
443
+
444
+ /**
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.
471
+ *
472
+ * @stability stable
473
+ * @category utility types
474
+ * @since 0.3.0
475
+ */
476
+ export type OpaqueOf<C> = C extends {
477
+ readonly "~Condition"?: Variance<infer _E, infer _R, infer O>
478
+ }
479
+ ? O
480
+ : never
481
+
482
+ type Leaf = string | number | boolean | bigint | symbol | null | undefined
483
+ /**
484
+ * Values of a class that keeps its data inside. A path does not go into them,
485
+ * as in the field paths of a resource, so a field with such a value is one
486
+ * field. A condition can test only if it exists.
487
+ */
488
+ type Whole =
489
+ | ReadonlyMap<unknown, unknown>
490
+ | ReadonlySet<unknown>
491
+ | HashMap.HashMap<unknown, unknown>
492
+ | HashSet.HashSet<unknown>
493
+ | Date
494
+ | URL
495
+ | Uint8Array
496
+ | Option.Option<unknown>
497
+ | BigDecimal.BigDecimal
498
+ | Duration.Duration
499
+ | DateTime.DateTime
500
+ | Redacted.Redacted<unknown>
501
+ | Chunk.Chunk<unknown>
502
+ | Cause.Cause<unknown>
503
+ | Result.Result<unknown, unknown>
504
+ | File
505
+ | FormData
506
+ | RegExp
507
+ type PreviousDepth = [never, 0, 1, 2, 3]
508
+ type FieldPathInternal<T, Depth extends number, Arrays extends boolean> = [
509
+ Depth,
510
+ ] extends [never]
511
+ ? never
512
+ : {
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
524
+ }[keyof T & string]
525
+
526
+ /**
527
+ * The paths of the fields of `T` that hold one value, with dots for nested
528
+ * fields. Arrays are left out. A map, a set, a date, a URL, a byte array, an
529
+ * `Option` and the other value types of Effect, such as `BigDecimal` and
530
+ * `Duration`, are one field, and a path does not go into them. The type goes down four levels, so type
531
+ * checking stays fast. For a union of types, only the common fields are
532
+ * paths.
533
+ *
534
+ * @stability stable
535
+ * @category utility types
536
+ * @since 0.3.0
537
+ */
538
+ export type FieldPath<T> = [T] extends [object]
539
+ ? FieldPathInternal<T, 4, false>
540
+ : never
541
+
542
+ /**
543
+ * The paths of the fields of `T` that a rule or a request may name, as in
544
+ * `Rule.Options.fields`. They are the paths of {@link FieldPath}, and the
545
+ * paths of arrays too: a rule may allow or deny a whole array field, and
546
+ * `permittedFields` lists it.
547
+ *
548
+ * @stability stable
549
+ * @category utility types
550
+ * @since 1.0.0
551
+ */
552
+ export type FieldSelector<T> = [T] extends [object]
553
+ ? FieldPathInternal<T, 4, true>
554
+ : never
555
+
556
+ /**
557
+ * The value type that fits every field in `F`. For one field, it is the type
558
+ * of the field. For a union of fields, it is the values that each of them can
559
+ * hold, so a value of a union type can not test a field of another type. For
560
+ * a union of data types, as for a rule on two kinds, it is the values that
561
+ * the field can hold in each of them.
562
+ *
563
+ * @stability stable
564
+ * @category utility types
565
+ * @since 1.0.0
566
+ */
567
+ export type FieldValue<Data, F extends string> = (
568
+ Data extends unknown
569
+ ? F extends unknown
570
+ ? (value: PathValue<Data, F>) => void
571
+ : never
572
+ : never
573
+ ) extends (value: infer V) => void
574
+ ? V
575
+ : never
576
+
577
+ type Normalize<A> = Exclude<A, undefined> | (undefined extends A ? null : never)
578
+
579
+ /**
580
+ * The type of the field of `T` at `Path`. A field that may be missing may also
581
+ * be `null`.
582
+ *
583
+ * @stability stable
584
+ * @category utility types
585
+ * @since 0.3.0
586
+ */
587
+ export type PathValue<
588
+ T,
589
+ Path extends string,
590
+ > = Path extends `${infer Head}.${infer Rest}`
591
+ ? Head extends keyof T
592
+ ? undefined extends T[Head]
593
+ ? PathValue<NonNullable<T[Head]>, Rest> | null
594
+ : null extends T[Head]
595
+ ? PathValue<NonNullable<T[Head]>, Rest> | null
596
+ : PathValue<T[Head], Rest>
597
+ : never
598
+ : Path extends keyof T
599
+ ? Normalize<T[Path]>
600
+ : never
601
+
602
+ /**
603
+ * A value for a field of type `A`: a fixed value, or a field of the principal
604
+ * with the same type.
605
+ *
606
+ * @stability stable
607
+ * @category models
608
+ * @since 0.3.0
609
+ */
610
+ export type OperandOf<A, P = never> = Extract<A, Scalar> | Ref<A, P>
611
+
612
+ type ScalarPath<T> = {
613
+ [F in FieldPath<T>]: PathValue<T, F> extends Scalar ? F : never
614
+ }[FieldPath<T>]
615
+
616
+ type OrderedPath<T> = {
617
+ [F in FieldPath<T>]: Exclude<PathValue<T, F>, null> extends number | string
618
+ ? F
619
+ : never
620
+ }[FieldPath<T>]
621
+
622
+ /**
623
+ * A test made by a {@link Builder}. It runs no code, but it keeps the value
624
+ * type of its resource, so a test on the fields of one resource can not go
625
+ * into a rule for another resource. A test with a field of the principal
626
+ * keeps the principal type `P` too.
627
+ *
628
+ * @stability stable
629
+ * @category models
630
+ * @since 1.0.0
631
+ */
632
+ export type Test<Value, P = unknown> = Condition<
633
+ never,
634
+ never,
635
+ never,
636
+ Reads<Value, P>
637
+ >
638
+
639
+ /**
640
+ * The principal type that a test reads: the principal of each field of the
641
+ * principal in the values `V`, and `unknown` when there is none. A reference
642
+ * from another builder keeps the principal of that builder.
643
+ */
644
+ type PrincipalOf<V, _P> = [Extract<V, PrincipalRef>] extends [never]
645
+ ? unknown
646
+ : [Extract<V, PrincipalRef>] extends [
647
+ { readonly "~Principal": (principal: infer Q) => void },
648
+ ]
649
+ ? Q
650
+ : unknown
651
+
652
+ /**
653
+ * Makes conditions for a resource with the data type `Data` and the value type
654
+ * `Value`, and a principal with the type `P`.
655
+ *
656
+ * @stability stable
657
+ * @category models
658
+ * @since 0.3.0
659
+ */
660
+ export interface Builder<Data, Value, P> {
661
+ /** The field is equal to the value. */
662
+ readonly eq: <
663
+ const F extends FieldPath<Data>,
664
+ const V extends OperandOf<FieldValue<Data, F>, any>,
665
+ >(
666
+ field: F,
667
+ value: V,
668
+ ) => Test<Value, PrincipalOf<V, P>>
669
+ /** The field is not equal to the value. */
670
+ readonly ne: <
671
+ const F extends FieldPath<Data>,
672
+ const V extends OperandOf<FieldValue<Data, F>, any>,
673
+ >(
674
+ field: F,
675
+ value: V,
676
+ ) => Test<Value, PrincipalOf<V, P>>
677
+ readonly lt: <
678
+ const F extends OrderedPath<Data>,
679
+ const V extends OperandOf<Exclude<FieldValue<Data, F>, null>, any>,
680
+ >(
681
+ field: F,
682
+ value: V,
683
+ ) => Test<Value, PrincipalOf<V, P>>
684
+ readonly lte: <
685
+ const F extends OrderedPath<Data>,
686
+ const V extends OperandOf<Exclude<FieldValue<Data, F>, null>, any>,
687
+ >(
688
+ field: F,
689
+ value: V,
690
+ ) => Test<Value, PrincipalOf<V, P>>
691
+ readonly gt: <
692
+ const F extends OrderedPath<Data>,
693
+ const V extends OperandOf<Exclude<FieldValue<Data, F>, null>, any>,
694
+ >(
695
+ field: F,
696
+ value: V,
697
+ ) => Test<Value, PrincipalOf<V, P>>
698
+ readonly gte: <
699
+ const F extends OrderedPath<Data>,
700
+ const V extends OperandOf<Exclude<FieldValue<Data, F>, null>, any>,
701
+ >(
702
+ field: F,
703
+ value: V,
704
+ ) => Test<Value, PrincipalOf<V, P>>
705
+ /** The field is equal to one of the values. */
706
+ readonly in: <
707
+ const F extends FieldPath<Data>,
708
+ const Vs extends ReadonlyArray<OperandOf<FieldValue<Data, F>, any>>,
709
+ >(
710
+ field: F,
711
+ values: Vs,
712
+ ) => Test<Value, PrincipalOf<Vs[number], P>>
713
+ /** The field has a value that is not `null`. */
714
+ readonly exists: <const F extends FieldPath<Data>>(field: F) => Test<Value>
715
+ /** The path of the resource starts with these parts. */
716
+ readonly under: <
717
+ const Ps extends ReadonlyArray<string | null | Ref<string | null, any>>,
718
+ >(
719
+ ...prefix: Ps
720
+ ) => Test<Value, PrincipalOf<Ps[number], P>>
721
+ /**
722
+ * A field of the principal, to use as a value. The field must have a
723
+ * {@link Scalar} type.
724
+ */
725
+ readonly principal: <const F extends ScalarPath<P>>(
726
+ field: F,
727
+ ) => Ref<PathValue<P, F>, P>
728
+ 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
+ >
736
+ 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
+ >
744
+ readonly not: <C extends Any>(
745
+ condition: C & Checked<C>,
746
+ ) => Condition<Error<C>, Services<C>, OpaqueOf<C>, ReadsOf<C>>
747
+ /**
748
+ * A test that runs any code. It may give a boolean or an `Effect`. A
749
+ * condition with a predicate can not become JSON or a query.
750
+ */
751
+ readonly predicate: <E = never, R = never>(
752
+ name: string,
753
+ test: (
754
+ value: Value,
755
+ principal: P,
756
+ ) => boolean | Effect.Effect<boolean, E, R>,
757
+ ) => Condition<E, R, Opaque, Reads<Value, P>>
758
+ }
759
+
760
+ const nodes = Data.taggedEnum<Ast>()
761
+ const operands = Data.taggedEnum<Operand>()
762
+
763
+ const literal = (value: Scalar): Literal => operands.Literal({ value })
764
+
765
+ const isOperand = (u: unknown): u is Operand =>
766
+ hasProperty(u, "_tag") && (u._tag === "Literal" || u._tag === "Principal")
767
+
768
+ const toOperand = (value: unknown): Operand =>
769
+ isOperand(value) ? value : literal(value as Scalar)
770
+
771
+ const compare =
772
+ (op: CompareOperator) =>
773
+ (field: string, value: unknown): Compare =>
774
+ nodes.Compare({ op, field, value: toOperand(value) })
775
+
776
+ // A list of conditions goes in as an array, not as arguments: a call may take
777
+ // 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.
780
+ const andOf = (conditions: ReadonlyArray<Ast>): Ast =>
781
+ nodes.And({ conditions: Arr.copy(conditions) })
782
+
783
+ const orOf = (conditions: ReadonlyArray<Ast>): Ast =>
784
+ nodes.Or({ conditions: Arr.copy(conditions) })
785
+
786
+ const makeAnd = (...conditions: ReadonlyArray<Ast>): Ast => andOf(conditions)
787
+
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 })
796
+
797
+ const sharedBuilder: Builder<any, any, any> = {
798
+ eq: compare("eq"),
799
+ ne: compare("ne"),
800
+ lt: compare("lt"),
801
+ lte: compare("lte"),
802
+ gt: compare("gt"),
803
+ gte: compare("gte"),
804
+ in: (field: string, values: ReadonlyArray<unknown>) =>
805
+ nodes.In({ field, values: values.map(toOperand) }),
806
+ exists: (field: string) => nodes.Exists({ field }),
807
+ under: (...prefix: ReadonlyArray<unknown>) =>
808
+ nodes.Under({ prefix: prefix.map(toOperand) }),
809
+ principal: (path: string) => operands.Principal({ path }),
810
+ and: makeAnd,
811
+ or: makeOr,
812
+ not: makeNot,
813
+ predicate: (
814
+ name: string,
815
+ test: (
816
+ value: unknown,
817
+ principal: unknown,
818
+ ) => boolean | Effect.Effect<boolean, unknown, unknown>,
819
+ ) =>
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
+ ),
826
+ } as never
827
+
828
+ /**
829
+ * Makes a builder. Rules give you a builder with the right types, so you do not
830
+ * often need this.
831
+ *
832
+ * @stability stable
833
+ * @category constructors
834
+ * @since 0.3.0
835
+ */
836
+ export const builder = <Data, Value, P>(): Builder<Data, Value, P> =>
837
+ sharedBuilder
838
+
839
+ /**
840
+ * Always true.
841
+ *
842
+ * @stability stable
843
+ * @category constructors
844
+ * @since 0.3.0
845
+ */
846
+ export const always: Condition = nodes.True() as Condition
847
+
848
+ /**
849
+ * Always false.
850
+ *
851
+ * @stability stable
852
+ * @category constructors
853
+ * @since 0.3.0
854
+ */
855
+ export const never: Condition = nodes.False() as Condition
856
+
857
+ /**
858
+ * True when all the conditions are true. With no conditions, it is true.
859
+ *
860
+ * @stability stable
861
+ * @category combinators
862
+ * @since 0.3.0
863
+ */
864
+ 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
872
+
873
+ /**
874
+ * The same as {@link and}, for a list of conditions of one type. Use it for a
875
+ * long list: a function call may take only so many arguments.
876
+ *
877
+ * @stability stable
878
+ * @category combinators
879
+ * @since 1.0.0
880
+ */
881
+ 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
884
+
885
+ /**
886
+ * The same as {@link or}, for a list of conditions of one type. Use it for a
887
+ * long list: a function call may take only so many arguments.
888
+ *
889
+ * @stability stable
890
+ * @category combinators
891
+ * @since 1.0.0
892
+ */
893
+ 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
896
+
897
+ /**
898
+ * True when one of the conditions is true. With no conditions, it is false.
899
+ *
900
+ * @stability stable
901
+ * @category combinators
902
+ * @since 0.3.0
903
+ */
904
+ 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
912
+
913
+ /**
914
+ * @stability stable
915
+ * @category combinators
916
+ * @since 0.3.0
917
+ */
918
+ 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
922
+
923
+ /**
924
+ * The data that a condition needs for a test.
925
+ *
926
+ * @stability stable
927
+ * @category models
928
+ * @since 0.3.0
929
+ */
930
+ export interface Input {
931
+ /** The resource value. */
932
+ readonly value: object
933
+ /**
934
+ * The principal. Without it, a principal field has no value. See
935
+ * {@link PrincipalRef}.
936
+ */
937
+ readonly principal?: object | undefined
938
+ /** The path of the resource. Without it, `Under` is false. */
939
+ readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
940
+ }
941
+
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
+ })
1142
+
1143
+ /**
1144
+ * Gives the services in `context` to every predicate of a condition, so the
1145
+ * condition needs no services when it runs.
1146
+ *
1147
+ * @stability stable
1148
+ * @category services
1149
+ * @since 1.0.0
1150
+ */
1151
+ export const provideContext: {
1152
+ <R>(
1153
+ 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
+ }
1246
+
1247
+ /**
1248
+ * The input of {@link evaluate} that the predicates of a condition need: a
1249
+ * value and a principal of the types in `I`. See {@link Reads}.
1250
+ */
1251
+ type ReadsInput<I> =
1252
+ I extends Reads<infer V, infer P>
1253
+ ? { readonly value: V } & (unknown extends P
1254
+ ? unknown
1255
+ : { readonly principal: P })
1256
+ : unknown
1257
+
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
+ /**
1265
+ * Tests a condition on a value. A predicate may fail with its error. When the
1266
+ * condition has a predicate, the input must have the value and the principal
1267
+ * that the predicate reads.
1268
+ *
1269
+ * @stability stable
1270
+ * @category evaluation
1271
+ * @since 0.3.0
1272
+ */
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
1284
+
1285
+ /**
1286
+ * Tests a condition with no predicate on a value, at once.
1287
+ *
1288
+ * @stability stable
1289
+ * @category evaluation
1290
+ * @since 0.3.0
1291
+ */
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())
1562
+
1563
+ /**
1564
+ * Makes a condition smaller without a change to its meaning. It removes `True`
1565
+ * and `False` from `And` and `Or`, joins nested `And` and `Or` nodes, and
1566
+ * removes double `Not` nodes.
1567
+ *
1568
+ * @stability stable
1569
+ * @category evaluation
1570
+ * @since 0.3.0
1571
+ */
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
+ }
1680
+
1681
+ interface PartialInput {
1682
+ readonly principal?: object | undefined
1683
+ readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
1684
+ }
1685
+
1686
+ /**
1687
+ * The principal that {@link partial} must get for the predicates of a
1688
+ * condition. A predicate keeps the principal that it gets, so the principal
1689
+ * must have the type that the predicate reads.
1690
+ */
1691
+ type PrincipalInput<I> =
1692
+ I extends Reads<infer _V, infer P>
1693
+ ? unknown extends P
1694
+ ? unknown
1695
+ : { readonly principal: P }
1696
+ : unknown
1697
+
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
+ /** What a condition reads after {@link partial}: the value, and no principal. */
1702
+ type Bound<I> = I extends Reads<infer V, infer _P> ? Reads<V, unknown> : I
1703
+
1704
+ /**
1705
+ * Puts the principal into a condition and expands `Under` with the path of the
1706
+ * resource. The result is simplified. It has no principal fields and no
1707
+ * `Under` nodes. It is `True` or `False` when the answer does not depend on the
1708
+ * resource value.
1709
+ *
1710
+ * @stability stable
1711
+ * @category evaluation
1712
+ * @since 0.3.0
1713
+ */
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
+ )
1734
+
1735
+ /**
1736
+ * The functions that change each node of a condition into another form, such
1737
+ * as a database query.
1738
+ *
1739
+ * @stability stable
1740
+ * @category models
1741
+ * @since 0.3.0
1742
+ */
1743
+ export interface Interpreter<A> {
1744
+ readonly true: () => A
1745
+ readonly false: () => A
1746
+ readonly compare: (op: CompareOperator, field: string, value: Scalar) => A
1747
+ readonly in: (field: string, values: ReadonlyArray<Scalar>) => A
1748
+ readonly exists: (field: string) => A
1749
+ readonly and: (conditions: ReadonlyArray<A>) => A
1750
+ readonly or: (conditions: ReadonlyArray<A>) => A
1751
+ readonly not: (condition: A) => A
1752
+ /** Optional. Without it, a predicate makes `fold` fail. */
1753
+ readonly predicate?: (name: string) => A
1754
+ }
1755
+
1756
+ /**
1757
+ * The error when `fold` can not change a node.
1758
+ *
1759
+ * @stability stable
1760
+ * @category errors
1761
+ * @since 0.3.0
1762
+ */
1763
+ export class Untranslatable extends Schema.TaggedError<Untranslatable>(
1764
+ "@nmnmcc/ability/Condition/Untranslatable",
1765
+ )("Untranslatable", { reason: Schema.String }) {}
1766
+
1767
+ const literalValue = (operand: Operand, field: string): Scalar => {
1768
+ if (operand._tag !== "Literal") {
1769
+ throw Untranslatable.make({
1770
+ reason: `The field "${field}" uses a principal field. Call Condition.partial first.`,
1771
+ })
1772
+ }
1773
+ return operand.value
1774
+ }
1775
+
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) {
1792
+ case "True":
1793
+ return interpreter.true()
1794
+ case "False":
1795
+ return interpreter.false()
1796
+ case "Compare":
1797
+ return interpreter.compare(
1798
+ ast.op,
1799
+ ast.field,
1800
+ literalValue(ast.value, ast.field),
1801
+ )
1802
+ case "In":
1803
+ return interpreter.in(
1804
+ ast.field,
1805
+ ast.values.map((operand) => literalValue(operand, ast.field)),
1806
+ )
1807
+ case "Exists":
1808
+ return interpreter.exists(ast.field)
1809
+ case "Under":
1810
+ throw Untranslatable.make({
1811
+ reason:
1812
+ "An Under node needs the resource path. Call Condition.partial first.",
1813
+ })
1814
+ case "And":
1815
+ return interpreter.and(
1816
+ Arr.map(ast.conditions, (child) => foldAst(child, interpreter, done)),
1817
+ )
1818
+ case "Or":
1819
+ return interpreter.or(
1820
+ Arr.map(ast.conditions, (child) => foldAst(child, interpreter, done)),
1821
+ )
1822
+ case "Not":
1823
+ return interpreter.not(foldAst(ast.condition, interpreter, done))
1824
+ case "Predicate":
1825
+ if (interpreter.predicate === undefined) {
1826
+ throw Untranslatable.make({
1827
+ reason: `The predicate "${ast.name}" can not become a query`,
1828
+ })
1829
+ }
1830
+ return interpreter.predicate(ast.name)
1831
+ }
1832
+ }
1833
+
1834
+ /**
1835
+ * Changes a condition into another form with an interpreter. Call
1836
+ * {@link partial} first: `fold` fails on principal fields and on `Under`
1837
+ * nodes, and on predicates when the interpreter has no `predicate` function.
1838
+ *
1839
+ * @stability stable
1840
+ * @category converting
1841
+ * @since 0.3.0
1842
+ */
1843
+ export const fold: {
1844
+ <A>(
1845
+ interpreter: Interpreter<A>,
1846
+ ): (condition: Any) => Effect.Effect<A, Untranslatable>
1847
+ <A>(
1848
+ condition: Any,
1849
+ interpreter: Interpreter<A>,
1850
+ ): Effect.Effect<A, Untranslatable>
1851
+ } = dual(
1852
+ 2,
1853
+ <A>(
1854
+ ast: Ast,
1855
+ interpreter: Interpreter<A>,
1856
+ ): Effect.Effect<A, Untranslatable> =>
1857
+ Effect.try({
1858
+ try: () => foldAst(ast, interpreter),
1859
+ catch: (cause) => {
1860
+ if (cause instanceof Untranslatable) {
1861
+ return cause
1862
+ }
1863
+ throw cause
1864
+ },
1865
+ }),
1866
+ )
1867
+
1868
+ /**
1869
+ * The fields that a condition uses.
1870
+ *
1871
+ * @stability stable
1872
+ * @category getters
1873
+ * @since 0.3.0
1874
+ */
1875
+ 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
+ })
1912
+
1913
+ // Satisfiability
1914
+
1915
+ /**
1916
+ * Values of fields, as a nested object. A field that is not in it may have any
1917
+ * value. A value with parts that the search does not look into, such as a
1918
+ * date, is an empty object.
1919
+ *
1920
+ * @stability stable
1921
+ * @category models
1922
+ * @since 1.0.0
1923
+ */
1924
+ export interface Example {
1925
+ readonly [field: string]: unknown
1926
+ }
1927
+
1928
+ /**
1929
+ * The answer of {@link satisfiable}:
1930
+ * - `Satisfiable`: some value makes the condition true. `example` is one.
1931
+ * - `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.
1935
+ *
1936
+ * @stability stable
1937
+ * @category models
1938
+ * @since 1.0.0
1939
+ */
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>()
1947
+
1948
+ /**
1949
+ * The options of {@link satisfiable}.
1950
+ *
1951
+ * @stability stable
1952
+ * @category models
1953
+ * @since 1.0.0
1954
+ */
1955
+ export interface SatisfiableOptions {
1956
+ /**
1957
+ * The Schema of the values. It gives the domain of each field. Without
1958
+ * it, a field may have any value.
1959
+ */
1960
+ readonly schema?: Schema.Top | undefined
1961
+ /** The most nodes that the search may visit. The default is 1000000. */
1962
+ readonly budget?: number | undefined
1963
+ }
1964
+
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
+ /**
2550
+ * Tests if some value makes a condition true, and gives such a value.
2551
+ *
2552
+ * Call {@link partial} first: the condition must have no principal fields and
2553
+ * no `Under` nodes. A condition with a predicate gives `Unknown`.
2554
+ *
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.
2560
+ *
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.
2568
+ *
2569
+ * The problem is as hard as boolean satisfiability, so the search has a
2570
+ * 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.
2572
+ *
2573
+ * @stability stable
2574
+ * @category evaluation
2575
+ * @since 1.0.0
2576
+ */
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
+ })
2613
+
2614
+ /**
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.
2620
+ *
2621
+ * @stability stable
2622
+ * @category conversions
2623
+ * @since 1.0.0
2624
+ */
2625
+ export const toJsonValue = (condition: Ast): unknown =>
2626
+ jsonIn(condition, new Map())
2627
+
2628
+ /**
2629
+ * The JSON form of a condition with no predicate.
2630
+ *
2631
+ * @stability stable
2632
+ * @category models
2633
+ * @since 0.3.0
2634
+ */
2635
+ 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
+ ])
2679
+
2680
+ /**
2681
+ * A condition with no predicate, that may read any values. It is the type of
2682
+ * {@link schema}: a decoded condition reads nothing, so it fits any rule, and
2683
+ * any plain condition, such as one from a builder or a residual, may be
2684
+ * encoded.
2685
+ *
2686
+ * @stability stable
2687
+ * @category models
2688
+ * @since 1.0.0
2689
+ */
2690
+ // `any` for what it reads: the field is contravariant, so no other type both
2691
+ // 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
2832
+
2833
+ /**
2834
+ * The Schema of a condition with no predicate. It checks the shape only. Use
2835
+ * `Policy.schema` to check the field names against a domain too.
2836
+ *
2837
+ * @stability stable
2838
+ * @category schemas
2839
+ * @since 0.3.0
2840
+ */
2841
+ 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.
2852
+ Schema.decodeTo(
2853
+ Schema.declare((input): input is PlainCondition =>
2854
+ isPlainNode(input, new Map(), 0),
2855
+ ),
2856
+ 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,
2867
+ ),
2868
+ )
2869
+ : Effect.succeed(condition),
2870
+ }),
2871
+ ),
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
+ })