@nmnmcc/ability 0.3.0 → 1.0.1

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 (66) hide show
  1. package/README.md +199 -31
  2. package/dist/Authorizer.d.ts +64 -39
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +291 -6
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +444 -65
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +1007 -25
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Domain.d.ts +27 -17
  11. package/dist/Domain.d.ts.map +1 -1
  12. package/dist/Domain.js +52 -10
  13. package/dist/Domain.js.map +1 -1
  14. package/dist/HttpApi.d.ts +84 -40
  15. package/dist/HttpApi.d.ts.map +1 -1
  16. package/dist/HttpApi.js +103 -13
  17. package/dist/HttpApi.js.map +1 -1
  18. package/dist/Policy.d.ts +314 -56
  19. package/dist/Policy.d.ts.map +1 -1
  20. package/dist/Policy.js +1215 -38
  21. package/dist/Policy.js.map +1 -1
  22. package/dist/Principal.js +2 -2
  23. package/dist/Principal.js.map +1 -1
  24. package/dist/Resource.d.ts +171 -20
  25. package/dist/Resource.d.ts.map +1 -1
  26. package/dist/Resource.js +788 -15
  27. package/dist/Resource.js.map +1 -1
  28. package/dist/Rule.d.ts +115 -29
  29. package/dist/Rule.d.ts.map +1 -1
  30. package/dist/Rule.js +219 -12
  31. package/dist/Rule.js.map +1 -1
  32. package/dist/index.js +4 -4
  33. package/package.json +2 -2
  34. package/src/Authorizer.ts +625 -61
  35. package/src/Condition.ts +2403 -112
  36. package/src/Domain.ts +80 -23
  37. package/src/HttpApi.ts +225 -25
  38. package/src/Policy.ts +2836 -96
  39. package/src/Principal.ts +2 -2
  40. package/src/Resource.ts +1544 -29
  41. package/src/Rule.ts +463 -45
  42. package/dist/internal/authorizer.d.ts +0 -28
  43. package/dist/internal/authorizer.d.ts.map +0 -1
  44. package/dist/internal/authorizer.js +0 -153
  45. package/dist/internal/authorizer.js.map +0 -1
  46. package/dist/internal/condition.d.ts +0 -11
  47. package/dist/internal/condition.d.ts.map +0 -1
  48. package/dist/internal/condition.js +0 -393
  49. package/dist/internal/condition.js.map +0 -1
  50. package/dist/internal/domain.js +0 -34
  51. package/dist/internal/domain.js.map +0 -1
  52. package/dist/internal/httpApi.js +0 -41
  53. package/dist/internal/httpApi.js.map +0 -1
  54. package/dist/internal/policy.js +0 -524
  55. package/dist/internal/policy.js.map +0 -1
  56. package/dist/internal/resource.js +0 -66
  57. package/dist/internal/resource.js.map +0 -1
  58. package/dist/internal/rule.js +0 -131
  59. package/dist/internal/rule.js.map +0 -1
  60. package/src/internal/authorizer.ts +0 -271
  61. package/src/internal/condition.ts +0 -607
  62. package/src/internal/domain.ts +0 -56
  63. package/src/internal/httpApi.ts +0 -84
  64. package/src/internal/policy.ts +0 -815
  65. package/src/internal/resource.ts +0 -98
  66. package/src/internal/rule.ts +0 -203
package/src/Condition.ts CHANGED
@@ -21,10 +21,31 @@
21
21
  * @stability stable
22
22
  * @since 0.3.0
23
23
  */
24
- import type * as Effect from "effect/Effect"
25
- import type * as Schema from "effect/Schema"
26
- import * as internal from "./internal/condition.ts"
27
- import type * as Resource from "./Resource.ts"
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"
28
49
 
29
50
  /**
30
51
  * A value that a condition can compare.
@@ -66,13 +87,21 @@ export interface PrincipalRef {
66
87
  }
67
88
 
68
89
  /**
69
- * A field of the principal with a known value type.
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.
70
96
  *
71
97
  * @stability stable
72
98
  * @category models
73
99
  * @since 0.3.0
74
100
  */
75
- export type Ref<A> = PrincipalRef & { readonly "~Ref"?: () => A }
101
+ export type Ref<A, P = never> = PrincipalRef & {
102
+ readonly "~Ref": () => A
103
+ readonly "~Principal": (principal: P) => void
104
+ }
76
105
 
77
106
  /**
78
107
  * @stability stable
@@ -204,10 +233,9 @@ export interface Not {
204
233
  export interface Predicate {
205
234
  readonly _tag: "Predicate"
206
235
  readonly name: string
207
- readonly run: (
208
- value: any,
209
- principal: any,
210
- ) => Effect.Effect<boolean, unknown, unknown>
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>
211
239
  }
212
240
 
213
241
  /**
@@ -240,18 +268,129 @@ export interface Variance<E, R, O> {
240
268
  readonly O: () => O
241
269
  }
242
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
+
243
372
  /**
244
373
  * A condition. `E` and `R` are the error type and the service type of its
245
374
  * predicates. `O` is {@link Opaque} when the condition has a predicate, and
246
- * `never` when it is plain data.
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}.
247
383
  *
248
384
  * @stability stable
249
385
  * @category models
250
386
  * @since 0.3.0
251
387
  */
252
- export type Condition<E = never, R = never, O = never> = Ast & {
253
- readonly "~Condition"?: Variance<E, R, O>
254
- }
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>
255
394
 
256
395
  /**
257
396
  * Any condition.
@@ -260,7 +399,7 @@ export type Condition<E = never, R = never, O = never> = Ast & {
260
399
  * @category models
261
400
  * @since 0.3.0
262
401
  */
263
- export type Any = Condition<any, any, any>
402
+ export type Any = Condition<any, any, any, any>
264
403
 
265
404
  /**
266
405
  * The error type of a condition.
@@ -288,6 +427,45 @@ export type Services<C> = C extends {
288
427
  ? R
289
428
  : never
290
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
+
291
469
  /**
292
470
  * {@link Opaque} when a condition has a predicate.
293
471
  *
@@ -302,22 +480,54 @@ export type OpaqueOf<C> = C extends {
302
480
  : never
303
481
 
304
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
305
507
  type PreviousDepth = [never, 0, 1, 2, 3]
306
- type FieldPathInternal<T, Depth extends number> = [Depth] extends [never]
508
+ type FieldPathInternal<T, Depth extends number, Arrays extends boolean> = [
509
+ Depth,
510
+ ] extends [never]
307
511
  ? never
308
512
  : {
309
- [K in keyof T & string]-?: NonNullable<T[K]> extends Leaf
513
+ [K in keyof T & string]-?: NonNullable<T[K]> extends Leaf | Whole
310
514
  ? K
311
- : NonNullable<T[K]> extends ReadonlyArray<unknown> | Function
515
+ : NonNullable<T[K]> extends Function
312
516
  ? never
313
- : NonNullable<T[K]> extends object
314
- ? `${K}.${FieldPathInternal<NonNullable<T[K]>, PreviousDepth[Depth]>}`
315
- : 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
316
524
  }[keyof T & string]
317
525
 
318
526
  /**
319
527
  * The paths of the fields of `T` that hold one value, with dots for nested
320
- * fields. Arrays are left out. The type goes down four levels, so type
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
321
531
  * checking stays fast. For a union of types, only the common fields are
322
532
  * paths.
323
533
  *
@@ -326,7 +536,42 @@ type FieldPathInternal<T, Depth extends number> = [Depth] extends [never]
326
536
  * @since 0.3.0
327
537
  */
328
538
  export type FieldPath<T> = [T] extends [object]
329
- ? FieldPathInternal<T, 4>
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
330
575
  : never
331
576
 
332
577
  type Normalize<A> = Exclude<A, undefined> | (undefined extends A ? null : never)
@@ -362,7 +607,11 @@ export type PathValue<
362
607
  * @category models
363
608
  * @since 0.3.0
364
609
  */
365
- export type OperandOf<A> = Extract<A, Scalar> | Ref<A>
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>]
366
615
 
367
616
  type OrderedPath<T> = {
368
617
  [F in FieldPath<T>]: Exclude<PathValue<T, F>, null> extends number | string
@@ -370,6 +619,36 @@ type OrderedPath<T> = {
370
619
  : never
371
620
  }[FieldPath<T>]
372
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
+
373
652
  /**
374
653
  * Makes conditions for a resource with the data type `Data` and the value type
375
654
  * `Value`, and a principal with the type `P`.
@@ -380,55 +659,91 @@ type OrderedPath<T> = {
380
659
  */
381
660
  export interface Builder<Data, Value, P> {
382
661
  /** The field is equal to the value. */
383
- readonly eq: <const F extends FieldPath<Data>>(
662
+ readonly eq: <
663
+ const F extends FieldPath<Data>,
664
+ const V extends OperandOf<FieldValue<Data, F>, any>,
665
+ >(
384
666
  field: F,
385
- value: OperandOf<PathValue<Data, F>>,
386
- ) => Condition
667
+ value: V,
668
+ ) => Test<Value, PrincipalOf<V, P>>
387
669
  /** The field is not equal to the value. */
388
- readonly ne: <const F extends FieldPath<Data>>(
670
+ readonly ne: <
671
+ const F extends FieldPath<Data>,
672
+ const V extends OperandOf<FieldValue<Data, F>, any>,
673
+ >(
389
674
  field: F,
390
- value: OperandOf<PathValue<Data, F>>,
391
- ) => Condition
392
- readonly lt: <const F extends OrderedPath<Data>>(
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
+ >(
393
681
  field: F,
394
- value: OperandOf<Exclude<PathValue<Data, F>, null>>,
395
- ) => Condition
396
- readonly lte: <const F extends OrderedPath<Data>>(
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
+ >(
397
688
  field: F,
398
- value: OperandOf<Exclude<PathValue<Data, F>, null>>,
399
- ) => Condition
400
- readonly gt: <const F extends OrderedPath<Data>>(
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
+ >(
401
695
  field: F,
402
- value: OperandOf<Exclude<PathValue<Data, F>, null>>,
403
- ) => Condition
404
- readonly gte: <const F extends OrderedPath<Data>>(
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
+ >(
405
702
  field: F,
406
- value: OperandOf<Exclude<PathValue<Data, F>, null>>,
407
- ) => Condition
703
+ value: V,
704
+ ) => Test<Value, PrincipalOf<V, P>>
408
705
  /** The field is equal to one of the values. */
409
- readonly in: <const F extends FieldPath<Data>>(
706
+ readonly in: <
707
+ const F extends FieldPath<Data>,
708
+ const Vs extends ReadonlyArray<OperandOf<FieldValue<Data, F>, any>>,
709
+ >(
410
710
  field: F,
411
- values: ReadonlyArray<OperandOf<PathValue<Data, F>>>,
412
- ) => Condition
711
+ values: Vs,
712
+ ) => Test<Value, PrincipalOf<Vs[number], P>>
413
713
  /** The field has a value that is not `null`. */
414
- readonly exists: <const F extends FieldPath<Data>>(field: F) => Condition
714
+ readonly exists: <const F extends FieldPath<Data>>(field: F) => Test<Value>
415
715
  /** The path of the resource starts with these parts. */
416
- readonly under: (
417
- ...prefix: ReadonlyArray<string | null | Ref<string | null>>
418
- ) => Condition
419
- /** A field of the principal, to use as a value. */
420
- readonly principal: <const F extends FieldPath<P>>(
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>>(
421
726
  field: F,
422
- ) => Ref<PathValue<P, F>>
727
+ ) => Ref<PathValue<P, F>, P>
423
728
  readonly and: <const Cs extends ReadonlyArray<Any>>(
424
- ...conditions: Cs
425
- ) => Condition<Error<Cs[number]>, Services<Cs[number]>, OpaqueOf<Cs[number]>>
729
+ ...conditions: Consistent<Cs>
730
+ ) => Condition<
731
+ Error<Cs[number]>,
732
+ Services<Cs[number]>,
733
+ OpaqueOf<Cs[number]>,
734
+ ReadsOf<Cs[number]>
735
+ >
426
736
  readonly or: <const Cs extends ReadonlyArray<Any>>(
427
- ...conditions: Cs
428
- ) => Condition<Error<Cs[number]>, Services<Cs[number]>, OpaqueOf<Cs[number]>>
737
+ ...conditions: Consistent<Cs>
738
+ ) => Condition<
739
+ Error<Cs[number]>,
740
+ Services<Cs[number]>,
741
+ OpaqueOf<Cs[number]>,
742
+ ReadsOf<Cs[number]>
743
+ >
429
744
  readonly not: <C extends Any>(
430
- condition: C,
431
- ) => Condition<Error<C>, Services<C>, OpaqueOf<C>>
745
+ condition: C & Checked<C>,
746
+ ) => Condition<Error<C>, Services<C>, OpaqueOf<C>, ReadsOf<C>>
432
747
  /**
433
748
  * A test that runs any code. It may give a boolean or an `Effect`. A
434
749
  * condition with a predicate can not become JSON or a query.
@@ -439,9 +754,77 @@ export interface Builder<Data, Value, P> {
439
754
  value: Value,
440
755
  principal: P,
441
756
  ) => boolean | Effect.Effect<boolean, E, R>,
442
- ) => Condition<E, R, Opaque>
757
+ ) => Condition<E, R, Opaque, Reads<Value, P>>
443
758
  }
444
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
+
445
828
  /**
446
829
  * Makes a builder. Rules give you a builder with the right types, so you do not
447
830
  * often need this.
@@ -450,8 +833,8 @@ export interface Builder<Data, Value, P> {
450
833
  * @category constructors
451
834
  * @since 0.3.0
452
835
  */
453
- export const builder: <Data, Value, P>() => Builder<Data, Value, P> =
454
- internal.builder
836
+ export const builder = <Data, Value, P>(): Builder<Data, Value, P> =>
837
+ sharedBuilder
455
838
 
456
839
  /**
457
840
  * Always true.
@@ -460,7 +843,7 @@ export const builder: <Data, Value, P>() => Builder<Data, Value, P> =
460
843
  * @category constructors
461
844
  * @since 0.3.0
462
845
  */
463
- export const always: Condition = internal.always
846
+ export const always: Condition = nodes.True() as Condition
464
847
 
465
848
  /**
466
849
  * Always false.
@@ -469,7 +852,7 @@ export const always: Condition = internal.always
469
852
  * @category constructors
470
853
  * @since 0.3.0
471
854
  */
472
- export const never: Condition = internal.never
855
+ export const never: Condition = nodes.False() as Condition
473
856
 
474
857
  /**
475
858
  * True when all the conditions are true. With no conditions, it is true.
@@ -479,9 +862,37 @@ export const never: Condition = internal.never
479
862
  * @since 0.3.0
480
863
  */
481
864
  export const and: <const Cs extends ReadonlyArray<Any>>(
482
- ...conditions: Cs
483
- ) => Condition<Error<Cs[number]>, Services<Cs[number]>, OpaqueOf<Cs[number]>> =
484
- internal.and as never
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
485
896
 
486
897
  /**
487
898
  * True when one of the conditions is true. With no conditions, it is false.
@@ -491,9 +902,13 @@ export const and: <const Cs extends ReadonlyArray<Any>>(
491
902
  * @since 0.3.0
492
903
  */
493
904
  export const or: <const Cs extends ReadonlyArray<Any>>(
494
- ...conditions: Cs
495
- ) => Condition<Error<Cs[number]>, Services<Cs[number]>, OpaqueOf<Cs[number]>> =
496
- internal.or as never
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
497
912
 
498
913
  /**
499
914
  * @stability stable
@@ -501,8 +916,9 @@ export const or: <const Cs extends ReadonlyArray<Any>>(
501
916
  * @since 0.3.0
502
917
  */
503
918
  export const not: <C extends Any>(
504
- condition: C,
505
- ) => Condition<Error<C>, Services<C>, OpaqueOf<C>> = internal.not as never
919
+ condition: C & Checked<C>,
920
+ ) => Condition<Error<C>, Services<C>, OpaqueOf<C>, ReadsOf<C>> =
921
+ makeNot as never
506
922
 
507
923
  /**
508
924
  * The data that a condition needs for a test.
@@ -524,21 +940,347 @@ export interface Input {
524
940
  }
525
941
 
526
942
  /**
527
- * Tests a condition on a value. A predicate may fail with its error.
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.
528
1268
  *
529
1269
  * @stability stable
530
1270
  * @category evaluation
531
1271
  * @since 0.3.0
532
1272
  */
533
1273
  export const evaluate: {
534
- (
535
- input: Input,
536
- ): <E, R, O>(condition: Condition<E, R, O>) => Effect.Effect<boolean, E, R>
537
- <E, R, O>(
538
- condition: Condition<E, R, O>,
539
- input: Input,
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>,
540
1282
  ): Effect.Effect<boolean, E, R>
541
- } = internal.evaluate as never
1283
+ } = dual(2, evaluateAst) as never
542
1284
 
543
1285
  /**
544
1286
  * Tests a condition with no predicate on a value, at once.
@@ -548,9 +1290,416 @@ export const evaluate: {
548
1290
  * @since 0.3.0
549
1291
  */
550
1292
  export const evaluateSync: {
551
- (input: Input): (condition: Condition) => boolean
552
- (condition: Condition, input: Input): boolean
553
- } = internal.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
554
1703
 
555
1704
  /**
556
1705
  * Puts the principal into a condition and expands `Under` with the path of the
@@ -563,31 +1712,25 @@ export const evaluateSync: {
563
1712
  * @since 0.3.0
564
1713
  */
565
1714
  export const partial: {
566
- (input: {
567
- readonly principal?: object | undefined
568
- readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
569
- }): <E, R, O>(condition: Condition<E, R, O>) => Condition<E, R, O>
570
- <E, R, O>(
571
- condition: Condition<E, R, O>,
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,
572
1728
  input: {
573
1729
  readonly principal?: object | undefined
574
1730
  readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
575
1731
  },
576
- ): Condition<E, R, O>
577
- } = internal.partial as never
578
-
579
- /**
580
- * Makes a condition smaller without a change to its meaning. It removes `True`
581
- * and `False` from `And` and `Or`, joins nested `And` and `Or` nodes, and
582
- * removes double `Not` nodes.
583
- *
584
- * @stability stable
585
- * @category evaluation
586
- * @since 0.3.0
587
- */
588
- export const simplify: <E, R, O>(
589
- condition: Condition<E, R, O>,
590
- ) => Condition<E, R, O> = internal.simplify as never
1732
+ ): Ast => simplifyAst(bind(ast, input.principal, input.path)),
1733
+ )
591
1734
 
592
1735
  /**
593
1736
  * The functions that change each node of a condition into another form, such
@@ -617,15 +1760,76 @@ export interface Interpreter<A> {
617
1760
  * @category errors
618
1761
  * @since 0.3.0
619
1762
  */
620
- export const Untranslatable: typeof internal.Untranslatable =
621
- internal.Untranslatable
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
+ }
622
1775
 
623
1776
  /**
624
- * @stability stable
625
- * @category errors
626
- * @since 0.3.0
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.
627
1779
  */
628
- export type Untranslatable = internal.Untranslatable
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
+ }
629
1833
 
630
1834
  /**
631
1835
  * Changes a condition into another form with an interpreter. Call
@@ -644,7 +1848,22 @@ export const fold: {
644
1848
  condition: Any,
645
1849
  interpreter: Interpreter<A>,
646
1850
  ): Effect.Effect<A, Untranslatable>
647
- } = internal.fold
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
+ )
648
1867
 
649
1868
  /**
650
1869
  * The fields that a condition uses.
@@ -653,7 +1872,758 @@ export const fold: {
653
1872
  * @category getters
654
1873
  * @since 0.3.0
655
1874
  */
656
- export const fields: (condition: Any) => ReadonlyArray<string> = internal.fields
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())
657
2627
 
658
2628
  /**
659
2629
  * The JSON form of a condition with no predicate.
@@ -695,6 +2665,171 @@ export interface EncodedNot {
695
2665
  readonly condition: Encoded
696
2666
  }
697
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
+
698
2833
  /**
699
2834
  * The Schema of a condition with no predicate. It checks the shape only. Use
700
2835
  * `Policy.schema` to check the field names against a domain too.
@@ -703,4 +2838,160 @@ export interface EncodedNot {
703
2838
  * @category schemas
704
2839
  * @since 0.3.0
705
2840
  */
706
- export const schema: Schema.Codec<Condition, Encoded> = internal.schema
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
+ })