@nmnmcc/ability 2.0.6 → 2.0.7

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.
@@ -2,9 +2,10 @@
2
2
  * The work on condition nodes: smart constructors, simplification, the step
3
3
  * that puts the principal in, tests on a value, and folds.
4
4
  *
5
- * The constructors of the nodes keep each condition small and low, so each
6
- * walk here is a plain walk with no memo. A shared child is walked again for
7
- * each place, and the limits of the nodes keep that work small.
5
+ * The constructors of the nodes keep each condition small, so each walk here
6
+ * has no memo. A shared child is walked again for each place, and the limits
7
+ * of the nodes keep that work small. A walk takes the steps of a trampoline,
8
+ * so a deep condition does not fill the stack: see `Trampoline`.
8
9
  */
9
10
  // A predicate gives an Effect with an error type and a service type that this
10
11
  // code does not know, so they are `unknown` here.
@@ -16,6 +17,7 @@ import * as Option from "effect/Option"
16
17
  import type * as Resource from "../Resource.ts"
17
18
  import * as Nodes from "./nodes.ts"
18
19
  import { readPath } from "./read.ts"
20
+ import * as Trampoline from "./trampoline.ts"
19
21
 
20
22
  type Node = Nodes.Node
21
23
 
@@ -252,20 +254,41 @@ export const not = (child: Node): Node => {
252
254
  }
253
255
  }
254
256
 
257
+ type Step<A> = Trampoline.Step<A>
258
+
259
+ /** The children of a node, each walked by `go` as a step of `R`. */
260
+ const eachChild = <A>(
261
+ R: Trampoline.Runner,
262
+ children: ReadonlyArray<Node>,
263
+ go: (child: Node) => Step<A>,
264
+ ): Step<ReadonlyArray<A>> =>
265
+ R.forEach(children, (child) => R.suspend(() => go(child)))
266
+
255
267
  /** Makes a condition simple, from its leaves up. */
256
- export const simplify = (node: Node): Node => {
257
- switch (node._tag) {
258
- case "And":
259
- case "Or":
260
- return junction(node._tag, Arr.map(node.conditions, simplify))
261
- case "Not":
262
- return node.condition.pipe(simplify, not)
263
- case "In":
264
- return node.values.length === 0 ? never : node
265
- default:
266
- return node
267
- }
268
- }
268
+ export const simplify = (node: Node): Node =>
269
+ Trampoline.run(node.depth, (R) => {
270
+ const go = (current: Node): Step<Node> => {
271
+ switch (current._tag) {
272
+ case "And":
273
+ case "Or": {
274
+ const tag = current._tag
275
+ return R.map(eachChild(R, current.conditions, go), (children) =>
276
+ junction(tag, children),
277
+ )
278
+ }
279
+ case "Not":
280
+ return R.map(
281
+ R.suspend(() => go(current.condition)),
282
+ not,
283
+ )
284
+ case "In":
285
+ return R.succeed(current.values.length === 0 ? never : current)
286
+ default:
287
+ return R.succeed(current)
288
+ }
289
+ }
290
+ return go(node)
291
+ })
269
292
 
270
293
  // Tests on a value
271
294
 
@@ -359,69 +382,99 @@ const testUnder = (
359
382
  })
360
383
  }
361
384
 
362
- /** Tests a simple condition with no predicate on a value. */
363
- const testSync = (node: Node, input: Input): boolean => {
364
- switch (node._tag) {
365
- case "True":
366
- return true
367
- case "False":
368
- return false
369
- case "Compare":
370
- return testCompare(
371
- node.op,
372
- getField(input.value, node.field),
373
- resolve(node.value, input.principal),
374
- )
375
- case "In": {
376
- const field = getField(input.value, node.field)
377
- return node.values.some(
378
- (operand) => resolve(operand, input.principal) === field,
379
- )
385
+ /**
386
+ * The test of a simple condition with no predicate on a value, as a walk.
387
+ * The children of `And` and `Or` run in order, and they stop at the first
388
+ * child that gives the answer.
389
+ */
390
+ const testWith = (input: Input) => (R: Trampoline.Runner) => {
391
+ const go = (node: Node): Step<boolean> => {
392
+ switch (node._tag) {
393
+ case "True":
394
+ return R.succeed(true)
395
+ case "False":
396
+ return R.succeed(false)
397
+ case "Compare":
398
+ return R.succeed(
399
+ testCompare(
400
+ node.op,
401
+ getField(input.value, node.field),
402
+ resolve(node.value, input.principal),
403
+ ),
404
+ )
405
+ case "In": {
406
+ const field = getField(input.value, node.field)
407
+ return R.succeed(
408
+ node.values.some(
409
+ (operand) => resolve(operand, input.principal) === field,
410
+ ),
411
+ )
412
+ }
413
+ case "Exists":
414
+ return R.succeed(getField(input.value, node.field) !== null)
415
+ case "Under":
416
+ return R.succeed(testUnder(node.prefix, input))
417
+ case "And":
418
+ return R.every(node.conditions, (child) => R.suspend(() => go(child)))
419
+ case "Or":
420
+ return R.some(node.conditions, (child) => R.suspend(() => go(child)))
421
+ case "Not":
422
+ return R.map(
423
+ R.suspend(() => go(node.condition)),
424
+ (value) => !value,
425
+ )
426
+ case "Predicate":
427
+ // The types stop this. A predicate needs `evaluate`.
428
+ throw new Error(
429
+ `The predicate "${node.name}" needs an Effect. Use Condition.evaluate.`,
430
+ )
380
431
  }
381
- case "Exists":
382
- return getField(input.value, node.field) !== null
383
- case "Under":
384
- return testUnder(node.prefix, input)
385
- case "And":
386
- return Arr.every(node.conditions, (child) => testSync(child, input))
387
- case "Or":
388
- return Arr.some(node.conditions, (child) => testSync(child, input))
389
- case "Not":
390
- return !testSync(node.condition, input)
391
- case "Predicate":
392
- // The types stop this. A predicate needs `evaluate`.
393
- throw new Error(
394
- `The predicate "${node.name}" needs an Effect. Use Condition.evaluate.`,
395
- )
396
432
  }
433
+ return go
397
434
  }
398
435
 
436
+ /** Tests a simple condition with no predicate on a value. */
437
+ const testSync = (node: Node, input: Input): boolean =>
438
+ Trampoline.run(node.depth, (R) => testWith(input)(R)(node))
439
+
399
440
  /**
400
441
  * Tests a simple condition on a value. The children of `And` and `Or` run in
401
- * order, and they stop at the first child that gives the answer.
442
+ * order, and they stop at the first child that gives the answer. Each call
443
+ * for a child is a step of the Effect, so a deep condition does not fill the
444
+ * stack. A part with no predicate runs as a walk in the same loop.
402
445
  */
403
446
  const test = (
404
447
  node: Node,
405
448
  input: Input,
406
449
  ): Effect.Effect<boolean, unknown, unknown> => {
407
450
  if (!node.opaque) {
408
- return Effect.sync(() => testSync(node, input))
451
+ return node.depth <= Trampoline.directDepth
452
+ ? Effect.sync(() => testSync(node, input))
453
+ : node.pipe(testWith(input)(Trampoline.loop), Trampoline.toEffect)
409
454
  }
410
455
  switch (node._tag) {
411
456
  case "Predicate":
412
457
  return Nodes.runOf(node)(input.value, input.principal)
413
458
  case "Not":
414
- return Effect.map(test(node.condition, input), (value) => !value)
459
+ return Effect.map(
460
+ Effect.suspend(() => test(node.condition, input)),
461
+ (value) => !value,
462
+ )
415
463
  case "And":
416
464
  return Effect.map(
417
465
  Effect.findFirst(node.conditions, (child) =>
418
- Effect.map(test(child, input), (value) => !value),
466
+ Effect.map(
467
+ Effect.suspend(() => test(child, input)),
468
+ (value) => !value,
469
+ ),
419
470
  ),
420
471
  Option.isNone,
421
472
  )
422
473
  case "Or":
423
474
  return Effect.map(
424
- Effect.findFirst(node.conditions, (child) => test(child, input)),
475
+ Effect.findFirst(node.conditions, (child) =>
476
+ Effect.suspend(() => test(child, input)),
477
+ ),
425
478
  Option.isSome,
426
479
  )
427
480
  default:
@@ -501,96 +554,126 @@ export interface PartialInput {
501
554
  * resource. The result is simple. It has no principal fields and no `Under`
502
555
  * nodes. A predicate keeps the principal.
503
556
  */
504
- export const partial = (node: Node, input: PartialInput): Node => {
505
- const bind = (current: Node): Node => {
506
- switch (current._tag) {
507
- case "Compare": {
508
- const value = bindOperand(current.value, input.principal)
509
- return value === undefined
510
- ? never
511
- : value === current.value
512
- ? current
513
- : new Nodes.Compare({ op: current.op, field: current.field, value })
514
- }
515
- case "In": {
516
- const values = Arr.flatMap(current.values, (operand) =>
517
- Option.toArray(
518
- Option.fromUndefinedOr(bindOperand(operand, input.principal)),
519
- ),
520
- )
521
- return values.length === 0
522
- ? never
523
- : new Nodes.In({ field: current.field, values })
557
+ export const partial = (node: Node, input: PartialInput): Node =>
558
+ Trampoline.run(node.depth, (R) => {
559
+ const bind = (current: Node): Step<Node> => {
560
+ switch (current._tag) {
561
+ case "Compare": {
562
+ const value = bindOperand(current.value, input.principal)
563
+ return R.succeed(
564
+ value === undefined
565
+ ? never
566
+ : value === current.value
567
+ ? current
568
+ : new Nodes.Compare({
569
+ op: current.op,
570
+ field: current.field,
571
+ value,
572
+ }),
573
+ )
574
+ }
575
+ case "In": {
576
+ const values = Arr.flatMap(current.values, (operand) =>
577
+ Option.toArray(
578
+ Option.fromUndefinedOr(bindOperand(operand, input.principal)),
579
+ ),
580
+ )
581
+ return R.succeed(
582
+ values.length === 0
583
+ ? never
584
+ : new Nodes.In({ field: current.field, values }),
585
+ )
586
+ }
587
+ case "Under":
588
+ return R.succeed(
589
+ expandUnder(current.prefix, input.principal, input.path),
590
+ )
591
+ case "And":
592
+ return R.map(eachChild(R, current.conditions, bind), and)
593
+ case "Or":
594
+ return R.map(eachChild(R, current.conditions, bind), or)
595
+ case "Not":
596
+ return R.map(
597
+ R.suspend(() => bind(current.condition)),
598
+ not,
599
+ )
600
+ case "Predicate": {
601
+ const run = Nodes.runOf(current)
602
+ return R.succeed(
603
+ new Nodes.Predicate({
604
+ name: current.name,
605
+ run: (value) => run(value, input.principal),
606
+ }),
607
+ )
608
+ }
609
+ default:
610
+ return R.succeed(current)
524
611
  }
525
- case "Under":
526
- return expandUnder(current.prefix, input.principal, input.path)
527
- case "And":
528
- return and(Arr.map(current.conditions, bind))
529
- case "Or":
530
- return or(Arr.map(current.conditions, bind))
531
- case "Not":
532
- return current.condition.pipe(bind, not)
533
- case "Predicate": {
534
- const run = Nodes.runOf(current)
535
- return new Nodes.Predicate({
536
- name: current.name,
537
- run: (value) => run(value, input.principal),
538
- })
539
- }
540
- default:
541
- return current
542
612
  }
543
- }
544
- return bind(node)
545
- }
613
+ return bind(node)
614
+ })
546
615
 
547
616
  /** Gives the services in `context` to every predicate of a condition. */
548
- export const provide = (node: Node, context: Context.Context<never>): Node => {
549
- if (!node.opaque) {
550
- return node
551
- }
552
- switch (node._tag) {
553
- case "Predicate": {
554
- const run = Nodes.runOf(node)
555
- return new Nodes.Predicate({
556
- name: node.name,
557
- run: (value, principal) =>
558
- Effect.provideContext(run(value, principal), context),
559
- })
617
+ export const provide = (node: Node, context: Context.Context<never>): Node =>
618
+ Trampoline.run(node.depth, (R) => {
619
+ const go = (current: Node): Step<Node> => {
620
+ if (!current.opaque) {
621
+ return R.succeed(current)
622
+ }
623
+ switch (current._tag) {
624
+ case "Predicate": {
625
+ const run = Nodes.runOf(current)
626
+ return R.succeed(
627
+ new Nodes.Predicate({
628
+ name: current.name,
629
+ run: (value, principal) =>
630
+ Effect.provideContext(run(value, principal), context),
631
+ }),
632
+ )
633
+ }
634
+ case "And":
635
+ return R.map(
636
+ eachChild(R, current.conditions, go),
637
+ (children) => new Nodes.And(children),
638
+ )
639
+ case "Or":
640
+ return R.map(
641
+ eachChild(R, current.conditions, go),
642
+ (children) => new Nodes.Or(children),
643
+ )
644
+ case "Not":
645
+ return R.map(
646
+ R.suspend(() => go(current.condition)),
647
+ (child) => new Nodes.Not(child),
648
+ )
649
+ default:
650
+ return R.succeed(current)
651
+ }
560
652
  }
561
- case "And":
562
- return new Nodes.And(
563
- Arr.map(node.conditions, (child) => provide(child, context)),
564
- )
565
- case "Or":
566
- return new Nodes.Or(
567
- Arr.map(node.conditions, (child) => provide(child, context)),
568
- )
569
- case "Not":
570
- return new Nodes.Not(provide(node.condition, context))
571
- default:
572
- return node
573
- }
574
- }
653
+ return go(node)
654
+ })
575
655
 
576
656
  /** The fields that a condition tests, in the order that they first come. */
577
657
  export const fields = (node: Node): ReadonlyArray<string> => {
578
- const all = (current: Node): ReadonlyArray<string> => {
579
- switch (current._tag) {
580
- case "Compare":
581
- case "In":
582
- case "Exists":
583
- return [current.field]
584
- case "And":
585
- case "Or":
586
- return Arr.flatMap(current.conditions, all)
587
- case "Not":
588
- return all(current.condition)
589
- default:
590
- return []
658
+ const all = Trampoline.run(node.depth, (R) => {
659
+ const go = (current: Node): Step<ReadonlyArray<string>> => {
660
+ switch (current._tag) {
661
+ case "Compare":
662
+ case "In":
663
+ case "Exists":
664
+ return R.succeed([current.field])
665
+ case "And":
666
+ case "Or":
667
+ return R.map(eachChild(R, current.conditions, go), Arr.flatten)
668
+ case "Not":
669
+ return R.suspend(() => go(current.condition))
670
+ default:
671
+ return R.succeed([])
672
+ }
591
673
  }
592
- }
674
+ return go(node)
675
+ })
593
676
  // A native `Set`, as in `dedupe`: a field of a condition that is not bound
594
677
  // to a domain may be any string.
595
- return Array.from(new Set(all(node)))
678
+ return Array.from(new Set(all))
596
679
  }
@@ -51,6 +51,8 @@ interface Build {
51
51
  }
52
52
 
53
53
  const limit = (node: Nodes.Node): Ast => {
54
+ // A residual grows by its nodes, as the conditions of the rules come
55
+ // together. Its values come from the policy, which has a limit of them.
54
56
  if (node.size > Limits.residualNodes) {
55
57
  throw Limits.exceeded(
56
58
  "A residual condition",
@@ -296,7 +298,7 @@ const scopeOf = (
296
298
  const issues = Bind.ruleIssues(
297
299
  domain,
298
300
  { actions: [request.action], resources: [request.kind], when: ast },
299
- ["within"],
301
+ () => ["within"],
300
302
  )
301
303
  if (issues.length > 0) {
302
304
  throw new globalThis.Error(
@@ -133,14 +133,17 @@ class PrincipalKey implements Equal.Equal {
133
133
 
134
134
  /**
135
135
  * The text of the order of the parts of a value: the names of the fields of
136
- * each object, and the items of each list, set and map, in the order that a
137
- * loop sees them. `Equal` does not see this order, but a loader may read it,
138
- * as `Object.values` does. So two keys need the same order too. A scalar has
139
- * no parts, and `Equal` compares it.
136
+ * each object, and the items of each list, set and map, with each scalar, in
137
+ * the order that a loop sees them. `Equal` does not see the order of the
138
+ * fields of an object or of the items of a map, but a loader may read it, as
139
+ * `Object.values` does. So two keys need the same order too. A scalar is in
140
+ * the text with its type, so the items of a set or a map show their order.
140
141
  */
141
142
  const orderOf = (value: unknown): string => {
142
143
  if (typeof value !== "object" || value === null) {
143
- return ""
144
+ return typeof value === "string"
145
+ ? JSON.stringify(value)
146
+ : `${typeof value}:${String(value)}`
144
147
  }
145
148
  if (Array.isArray(value)) {
146
149
  return `[${value.map(orderOf).join(",")}]`