@nmnmcc/ability 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -14
- package/dist/Authorizer.d.ts +64 -39
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js +291 -6
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +444 -65
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +1007 -25
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +27 -17
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +52 -10
- package/dist/Domain.js.map +1 -1
- package/dist/HttpApi.d.ts +84 -40
- package/dist/HttpApi.d.ts.map +1 -1
- package/dist/HttpApi.js +101 -11
- package/dist/HttpApi.js.map +1 -1
- package/dist/Policy.d.ts +311 -53
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +1212 -35
- package/dist/Policy.js.map +1 -1
- package/dist/Resource.d.ts +167 -16
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +784 -11
- package/dist/Resource.js.map +1 -1
- package/dist/Rule.d.ts +113 -27
- package/dist/Rule.d.ts.map +1 -1
- package/dist/Rule.js +217 -10
- package/dist/Rule.js.map +1 -1
- package/dist/index.js +4 -4
- package/package.json +1 -1
- package/src/Authorizer.ts +623 -59
- package/src/Condition.ts +2403 -112
- package/src/Domain.ts +79 -22
- package/src/HttpApi.ts +223 -23
- package/src/Policy.ts +2833 -93
- package/src/Resource.ts +1537 -22
- package/src/Rule.ts +461 -43
- package/dist/internal/authorizer.d.ts +0 -28
- package/dist/internal/authorizer.d.ts.map +0 -1
- package/dist/internal/authorizer.js +0 -153
- package/dist/internal/authorizer.js.map +0 -1
- package/dist/internal/condition.d.ts +0 -11
- package/dist/internal/condition.d.ts.map +0 -1
- package/dist/internal/condition.js +0 -393
- package/dist/internal/condition.js.map +0 -1
- package/dist/internal/domain.js +0 -34
- package/dist/internal/domain.js.map +0 -1
- package/dist/internal/httpApi.js +0 -41
- package/dist/internal/httpApi.js.map +0 -1
- package/dist/internal/policy.js +0 -524
- package/dist/internal/policy.js.map +0 -1
- package/dist/internal/resource.js +0 -66
- package/dist/internal/resource.js.map +0 -1
- package/dist/internal/rule.js +0 -131
- package/dist/internal/rule.js.map +0 -1
- package/src/internal/authorizer.ts +0 -271
- package/src/internal/condition.ts +0 -607
- package/src/internal/domain.ts +0 -56
- package/src/internal/httpApi.ts +0 -84
- package/src/internal/policy.ts +0 -815
- package/src/internal/resource.ts +0 -98
- 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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 & {
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
253
|
-
|
|
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> = [
|
|
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
|
|
515
|
+
: NonNullable<T[K]> extends Function
|
|
312
516
|
? never
|
|
313
|
-
: NonNullable<T[K]> extends
|
|
314
|
-
?
|
|
315
|
-
|
|
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.
|
|
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: <
|
|
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:
|
|
386
|
-
) =>
|
|
667
|
+
value: V,
|
|
668
|
+
) => Test<Value, PrincipalOf<V, P>>
|
|
387
669
|
/** The field is not equal to the value. */
|
|
388
|
-
readonly ne: <
|
|
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:
|
|
391
|
-
) =>
|
|
392
|
-
readonly lt: <
|
|
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:
|
|
395
|
-
) =>
|
|
396
|
-
readonly lte: <
|
|
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:
|
|
399
|
-
) =>
|
|
400
|
-
readonly gt: <
|
|
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:
|
|
403
|
-
) =>
|
|
404
|
-
readonly gte: <
|
|
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:
|
|
407
|
-
) =>
|
|
703
|
+
value: V,
|
|
704
|
+
) => Test<Value, PrincipalOf<V, P>>
|
|
408
705
|
/** The field is equal to one of the values. */
|
|
409
|
-
readonly in: <
|
|
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:
|
|
412
|
-
) =>
|
|
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) =>
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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<
|
|
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<
|
|
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
|
|
454
|
-
|
|
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 =
|
|
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 =
|
|
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<
|
|
484
|
-
|
|
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<
|
|
496
|
-
|
|
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>> =
|
|
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
|
-
*
|
|
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:
|
|
536
|
-
): <E, R, O
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
} =
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
(
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
<E, R, O
|
|
571
|
-
|
|
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
|
-
):
|
|
577
|
-
|
|
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
|
|
621
|
-
|
|
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
|
-
*
|
|
625
|
-
*
|
|
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
|
-
|
|
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
|
-
} =
|
|
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
|
|
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<
|
|
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
|
+
})
|