@nmnmcc/ability 2.0.5 → 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.
Files changed (56) hide show
  1. package/README.md +6 -5
  2. package/dist/Condition.d.ts +6 -6
  3. package/dist/Condition.d.ts.map +1 -1
  4. package/dist/Condition.js.map +1 -1
  5. package/dist/Domain.d.ts +1 -1
  6. package/dist/Policy.d.ts +2 -2
  7. package/dist/Policy.d.ts.map +1 -1
  8. package/dist/Policy.js.map +1 -1
  9. package/dist/internal/algebra.js +128 -79
  10. package/dist/internal/algebra.js.map +1 -1
  11. package/dist/internal/analysis.js +1 -1
  12. package/dist/internal/analysis.js.map +1 -1
  13. package/dist/internal/authorizer.d.ts.map +1 -1
  14. package/dist/internal/authorizer.js +38 -14
  15. package/dist/internal/authorizer.js.map +1 -1
  16. package/dist/internal/bind.js +59 -43
  17. package/dist/internal/bind.js.map +1 -1
  18. package/dist/internal/catalog.d.ts +6 -0
  19. package/dist/internal/catalog.d.ts.map +1 -1
  20. package/dist/internal/catalog.js +17 -14
  21. package/dist/internal/catalog.js.map +1 -1
  22. package/dist/internal/decide.js +1 -1
  23. package/dist/internal/domain.js +1 -1
  24. package/dist/internal/json.js +2 -10
  25. package/dist/internal/json.js.map +1 -1
  26. package/dist/internal/nodes.d.ts +11 -1
  27. package/dist/internal/nodes.d.ts.map +1 -1
  28. package/dist/internal/nodes.js +51 -11
  29. package/dist/internal/nodes.js.map +1 -1
  30. package/dist/internal/patterns.js +1 -1
  31. package/dist/internal/patterns.js.map +1 -1
  32. package/dist/internal/policy-json.js +2 -8
  33. package/dist/internal/policy-json.js.map +1 -1
  34. package/dist/internal/policy-nodes.d.ts +4 -1
  35. package/dist/internal/policy-nodes.d.ts.map +1 -1
  36. package/dist/internal/policy-nodes.js +14 -5
  37. package/dist/internal/policy-nodes.js.map +1 -1
  38. package/dist/internal/search.js +4 -4
  39. package/dist/internal/search.js.map +1 -1
  40. package/dist/internal/trampoline.js +75 -0
  41. package/dist/internal/trampoline.js.map +1 -0
  42. package/package.json +1 -1
  43. package/src/Condition.ts +5 -2
  44. package/src/Policy.ts +1 -2
  45. package/src/internal/algebra.ts +262 -143
  46. package/src/internal/analysis.ts +3 -1
  47. package/src/internal/authorizer.ts +59 -14
  48. package/src/internal/bind.ts +149 -71
  49. package/src/internal/catalog.ts +54 -50
  50. package/src/internal/json.ts +2 -11
  51. package/src/internal/nodes.ts +58 -24
  52. package/src/internal/patterns.ts +7 -3
  53. package/src/internal/policy-json.ts +2 -9
  54. package/src/internal/policy-nodes.ts +16 -7
  55. package/src/internal/search.ts +13 -10
  56. package/src/internal/trampoline.ts +147 -0
@@ -0,0 +1,75 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Arr from "effect/Array";
3
+ import * as Option from "effect/Option";
4
+ import * as Cause from "effect/Cause";
5
+ import * as Exit from "effect/Exit";
6
+ // src/internal/trampoline.ts
7
+ /**
8
+ * Walks that do not fill the stack.
9
+ *
10
+ * A walk is written one time, with the steps of a {@link Runner}, and a
11
+ * runner runs it:
12
+ * - {@link direct} runs each step at once, as a plain call does. It is fast,
13
+ * and it is for an input with few levels.
14
+ * - {@link loop} puts each step into the run loop of an Effect. A step gives
15
+ * the next step in place of calling it, and the loop runs one step after
16
+ * the other, so the stack does not grow with the levels of the input. This
17
+ * is a trampoline.
18
+ *
19
+ * {@link run} takes `direct` for an input with at most `directDepth` levels,
20
+ * and `loop` for a deeper one. A walk makes each call for a child through
21
+ * `suspend`, so the loop makes the step of the child only when it runs it.
22
+ */
23
+ const value = (step) => step;
24
+ const step = (value) => value;
25
+ /** Runs each step at once, on the stack. */
26
+ const direct = {
27
+ succeed: step,
28
+ suspend: (make) => make(),
29
+ map: (from, f) => step(f(value(from))),
30
+ flatMap: (from, f) => f(value(from)),
31
+ forEach: (items, f) => step(Arr.map(items, (item, index) => value(f(item, index)))),
32
+ some: (items, f) => step(Arr.some(items, (item) => value(f(item)))),
33
+ every: (items, f) => step(Arr.every(items, (item) => value(f(item))))
34
+ };
35
+ const effect = (step) => step;
36
+ const fromEffect = (effect) => effect;
37
+ /** Runs each step in the run loop of an Effect, so a deep walk does not fill the stack. */
38
+ const loop = {
39
+ succeed: (value) => fromEffect(Effect.succeed(value)),
40
+ suspend: (make) => fromEffect(Effect.suspend(() => effect(make()))),
41
+ map: (from, f) => fromEffect(Effect.map(effect(from), f)),
42
+ flatMap: (from, f) => fromEffect(Effect.flatMap(effect(from), (value) => effect(f(value)))),
43
+ forEach: (items, f) => fromEffect(Effect.forEach(items, (item, index) => effect(f(item, index)))),
44
+ some: (items, f) => fromEffect(Effect.map(Effect.findFirst(items, (item) => effect(f(item))), Option.isSome)),
45
+ every: (items, f) => fromEffect(Effect.map(Effect.findFirst(items, (item) => Effect.map(effect(f(item)), (value) => !value)), Option.isNone))
46
+ };
47
+ /**
48
+ * Gives the runner for an input with `depth` levels. The walks of a condition
49
+ * or a policy tree know the depth of the input from its node. A walk of an
50
+ * input with an unknown depth, as a decoder, takes `loop`.
51
+ */
52
+ const runnerFor = (depth) => depth <= 200 ? direct : loop;
53
+ /**
54
+ * Runs a walk at once, with the runner for `depth` levels, and gives its
55
+ * value. An error that a step throws comes out as it was, also from `loop`.
56
+ */
57
+ const run = (depth, walk) => runWith(runnerFor(depth), walk);
58
+ /** Runs a walk at once with a runner, as {@link run} does. */
59
+ const runWith = (runner, walk) => {
60
+ if (runner === direct) return value(walk(direct));
61
+ return Exit.match(Effect.runSyncExit(effect(walk(loop))), {
62
+ onSuccess: (result) => result,
63
+ onFailure: (cause) => {
64
+ throw Cause.squash(cause);
65
+ }
66
+ });
67
+ };
68
+ /**
69
+ * Gives the Effect of a step of `loop`, so a walk can go on in an Effect
70
+ * that runs a predicate.
71
+ */
72
+ const toEffect = (step) => effect(step);
73
+ export { direct, loop, run, runWith, runnerFor, toEffect };
74
+
75
+ //# sourceMappingURL=trampoline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trampoline.js","names":[],"sources":["../../src/internal/trampoline.ts"],"sourcesContent":["/**\n * Walks that do not fill the stack.\n *\n * A walk is written one time, with the steps of a {@link Runner}, and a\n * runner runs it:\n * - {@link direct} runs each step at once, as a plain call does. It is fast,\n * and it is for an input with few levels.\n * - {@link loop} puts each step into the run loop of an Effect. A step gives\n * the next step in place of calling it, and the loop runs one step after\n * the other, so the stack does not grow with the levels of the input. This\n * is a trampoline.\n *\n * {@link run} takes `direct` for an input with at most `directDepth` levels,\n * and `loop` for a deeper one. A walk makes each call for a child through\n * `suspend`, so the loop makes the step of the child only when it runs it.\n */\nimport * as Arr from \"effect/Array\"\nimport * as Cause from \"effect/Cause\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Option from \"effect/Option\"\n\n/**\n * A step of a walk that gives an `A`. A runner of steps knows what a step\n * is: the value itself for `direct`, and an Effect for `loop`. The type is\n * only in the types.\n */\nexport interface Step<A> {\n\treadonly \"~Step\": (_: never) => A\n}\n\n/** The steps that a walk may take. */\nexport interface Runner {\n\treadonly succeed: <A>(value: A) => Step<A>\n\t/** The step that `step` makes, when the runner runs it. */\n\treadonly suspend: <A>(step: () => Step<A>) => Step<A>\n\treadonly map: <A, B>(step: Step<A>, f: (value: A) => B) => Step<B>\n\treadonly flatMap: <A, B>(step: Step<A>, f: (value: A) => Step<B>) => Step<B>\n\t/** Runs a step for each item, in order, and gives their values. */\n\treadonly forEach: <A, B>(\n\t\titems: ReadonlyArray<A>,\n\t\tf: (item: A, index: number) => Step<B>,\n\t) => Step<ReadonlyArray<B>>\n\t/** Runs a step for each item, in order, up to the first `true`. */\n\treadonly some: <A>(\n\t\titems: ReadonlyArray<A>,\n\t\tf: (item: A) => Step<boolean>,\n\t) => Step<boolean>\n\t/** Runs a step for each item, in order, up to the first `false`. */\n\treadonly every: <A>(\n\t\titems: ReadonlyArray<A>,\n\t\tf: (item: A) => Step<boolean>,\n\t) => Step<boolean>\n}\n\n// A step of `direct` is its value. The casts below only change the type.\nconst value = <A>(step: Step<A>): A => step as unknown as A\nconst step = <A>(value: A): Step<A> => value as unknown as Step<A>\n\n/** Runs each step at once, on the stack. */\nexport const direct: Runner = {\n\tsucceed: step,\n\tsuspend: (make) => make(),\n\tmap: (from, f) => step(f(value(from))),\n\tflatMap: (from, f) => f(value(from)),\n\tforEach: (items, f) =>\n\t\tstep(Arr.map(items, (item, index) => value(f(item, index)))),\n\tsome: (items, f) => step(Arr.some(items, (item) => value(f(item)))),\n\tevery: (items, f) => step(Arr.every(items, (item) => value(f(item)))),\n}\n\n// A step of `loop` is an Effect with no error and no services. A failure of a\n// walk is a thrown error, which `run` gives back as it was.\ntype Loop<A> = Effect.Effect<A>\nconst effect = <A>(step: Step<A>): Loop<A> => step as unknown as Loop<A>\nconst fromEffect = <A>(effect: Loop<A>): Step<A> => effect as unknown as Step<A>\n\n/** Runs each step in the run loop of an Effect, so a deep walk does not fill the stack. */\nexport const loop: Runner = {\n\tsucceed: (value) => fromEffect(Effect.succeed(value)),\n\tsuspend: (make) => fromEffect(Effect.suspend(() => effect(make()))),\n\tmap: (from, f) => fromEffect(Effect.map(effect(from), f)),\n\tflatMap: (from, f) =>\n\t\tfromEffect(Effect.flatMap(effect(from), (value) => effect(f(value)))),\n\tforEach: (items, f) =>\n\t\tfromEffect(Effect.forEach(items, (item, index) => effect(f(item, index)))),\n\tsome: (items, f) =>\n\t\tfromEffect(\n\t\t\tEffect.map(\n\t\t\t\tEffect.findFirst(items, (item) => effect(f(item))),\n\t\t\t\tOption.isSome,\n\t\t\t),\n\t\t),\n\tevery: (items, f) =>\n\t\tfromEffect(\n\t\t\tEffect.map(\n\t\t\t\tEffect.findFirst(items, (item) =>\n\t\t\t\t\tEffect.map(effect(f(item)), (value) => !value),\n\t\t\t\t),\n\t\t\t\tOption.isNone,\n\t\t\t),\n\t\t),\n}\n\n/**\n * The most levels of an input that `run` walks with `direct`. A level takes\n * a few calls of the walk and of the runner on the stack, so this keeps the\n * stack far from full, and most inputs take the fast way.\n */\nexport const directDepth = 200\n\n/**\n * Gives the runner for an input with `depth` levels. The walks of a condition\n * or a policy tree know the depth of the input from its node. A walk of an\n * input with an unknown depth, as a decoder, takes `loop`.\n */\nexport const runnerFor = (depth: number): Runner =>\n\tdepth <= directDepth ? direct : loop\n\n/**\n * Runs a walk at once, with the runner for `depth` levels, and gives its\n * value. An error that a step throws comes out as it was, also from `loop`.\n */\nexport const run = <A>(depth: number, walk: (runner: Runner) => Step<A>): A =>\n\trunWith(runnerFor(depth), walk)\n\n/** Runs a walk at once with a runner, as {@link run} does. */\nexport const runWith = <A>(\n\trunner: Runner,\n\twalk: (runner: Runner) => Step<A>,\n): A => {\n\tif (runner === direct) {\n\t\treturn value(walk(direct))\n\t}\n\treturn Exit.match(Effect.runSyncExit(effect(walk(loop))), {\n\t\tonSuccess: (result) => result,\n\t\tonFailure: (cause) => {\n\t\t\tthrow Cause.squash(cause)\n\t\t},\n\t})\n}\n\n/**\n * Gives the Effect of a step of `loop`, so a walk can go on in an Effect\n * that runs a predicate.\n */\nexport const toEffect = <A>(step: Step<A>): Effect.Effect<A> => effect(step)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAwDA,MAAM,SAAY,SAAqB;AACvC,MAAM,QAAW,UAAsB;;AAGvC,MAAa,SAAiB;CAC7B,SAAS;CACT,UAAU,SAAS,KAAK;CACxB,MAAM,MAAM,MAAM,KAAK,EAAE,MAAM,IAAI,CAAC,CAAC;CACrC,UAAU,MAAM,MAAM,EAAE,MAAM,IAAI,CAAC;CACnC,UAAU,OAAO,MAChB,KAAK,IAAI,IAAI,QAAQ,MAAM,UAAU,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC;CAC5D,OAAO,OAAO,MAAM,KAAK,IAAI,KAAK,QAAQ,SAAS,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;CAClE,QAAQ,OAAO,MAAM,KAAK,IAAI,MAAM,QAAQ,SAAS,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;AACrE;AAKA,MAAM,UAAa,SAA2B;AAC9C,MAAM,cAAiB,WAA6B;;AAGpD,MAAa,OAAe;CAC3B,UAAU,UAAU,WAAW,OAAO,QAAQ,KAAK,CAAC;CACpD,UAAU,SAAS,WAAW,OAAO,cAAc,OAAO,KAAK,CAAC,CAAC,CAAC;CAClE,MAAM,MAAM,MAAM,WAAW,OAAO,IAAI,OAAO,IAAI,GAAG,CAAC,CAAC;CACxD,UAAU,MAAM,MACf,WAAW,OAAO,QAAQ,OAAO,IAAI,IAAI,UAAU,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;CACrE,UAAU,OAAO,MAChB,WAAW,OAAO,QAAQ,QAAQ,MAAM,UAAU,OAAO,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC;CAC1E,OAAO,OAAO,MACb,WACC,OAAO,IACN,OAAO,UAAU,QAAQ,SAAS,OAAO,EAAE,IAAI,CAAC,CAAC,GACjD,OAAO,MACR,CACD;CACD,QAAQ,OAAO,MACd,WACC,OAAO,IACN,OAAO,UAAU,QAAQ,SACxB,OAAO,IAAI,OAAO,EAAE,IAAI,CAAC,IAAI,UAAU,CAAC,KAAK,CAC9C,GACA,OAAO,MACR,CACD;AACF;;;;;;AAcA,MAAa,aAAa,UACzB,SAAA,MAAuB,SAAS;;;;;AAMjC,MAAa,OAAU,OAAe,SACrC,QAAQ,UAAU,KAAK,GAAG,IAAI;;AAG/B,MAAa,WACZ,QACA,SACO;CACP,IAAI,WAAW,QACd,OAAO,MAAM,KAAK,MAAM,CAAC;CAE1B,OAAO,KAAK,MAAM,OAAO,YAAY,OAAO,KAAK,IAAI,CAAC,CAAC,GAAG;EACzD,YAAY,WAAW;EACvB,YAAY,UAAU;GACrB,MAAM,MAAM,OAAO,KAAK;EACzB;CACD,CAAC;AACF;;;;;AAMA,MAAa,YAAe,SAAoC,OAAO,IAAI"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nmnmcc/ability",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Typed, composable authorization policies for Effect programs.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/Condition.ts CHANGED
@@ -412,7 +412,8 @@ type ObjectPaths<
412
412
  > = [O] extends [never]
413
413
  ? never
414
414
  : // A record is one field to a rule or a request, as `permittedFields`
415
- // lists it.
415
+ // lists it. So is a struct at the last part that a path may have, since
416
+ // a path does not go below it.
416
417
  | (Parents extends true
417
418
  ? K
418
419
  : Arrays extends true
@@ -420,7 +421,9 @@ type ObjectPaths<
420
421
  ? K
421
422
  : number extends keyof O
422
423
  ? K
423
- : never
424
+ : [Depth] extends [0]
425
+ ? K
426
+ : never
424
427
  : never)
425
428
  | `${K}.${FieldPathInternal<O, PreviousDepth[Depth], Arrays, Parents>}`
426
429
 
package/src/Policy.ts CHANGED
@@ -53,8 +53,7 @@ import type * as Resource from "./Resource.ts"
53
53
  * @category models
54
54
  * @since 0.3.0
55
55
  */
56
- export type Algorithm =
57
- "denyOverrides" | "permitOverrides" | "firstApplicable" | "lastApplicable"
56
+ export type Algorithm = PolicyNodes.Algorithm
58
57
 
59
58
  /**
60
59
  * A list of rules with a combining algorithm.
@@ -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
 
@@ -60,15 +62,46 @@ const shapeOf = (node: Node): unknown => {
60
62
  * with one text give the same answer for every value. It reads all of the
61
63
  * condition, so a search counts its size in the budget.
62
64
  */
63
- export const structureText = (node: Node): string =>
64
- JSON.stringify(shapeOf(node))
65
+ const structureText = (node: Node): string => JSON.stringify(shapeOf(node))
65
66
 
66
67
  /**
67
- * The most nodes of a condition that a smart constructor reads for its key.
68
- * A larger child is its own key, so the work of a constructor stays small.
68
+ * The work of the text of the structure of a condition: each node, and each
69
+ * value of an `In` test or an `Under` test. A node counts as one in `size`,
70
+ * also an `In` test with very many values. This walk reads the nodes only,
71
+ * so it takes `size` steps.
72
+ */
73
+ const weightOf = (node: Node): number => {
74
+ switch (node._tag) {
75
+ case "In":
76
+ return 1 + node.values.length
77
+ case "Under":
78
+ return 1 + node.prefix.length
79
+ case "And":
80
+ case "Or":
81
+ return Arr.reduce(
82
+ node.conditions,
83
+ 1,
84
+ (total, child) => total + weightOf(child),
85
+ )
86
+ case "Not":
87
+ return 1 + weightOf(node.condition)
88
+ default:
89
+ return 1
90
+ }
91
+ }
92
+
93
+ /**
94
+ * The most nodes of a condition that a smart constructor reads for its key,
95
+ * and the most children of a child that it puts into a node. A larger child
96
+ * is its own key, and it stays one child, so the work of a constructor stays
97
+ * small. A deep chain of `And` nodes, for one, does not copy all its leaves
98
+ * at each level.
69
99
  */
70
100
  const keyNodes = 64
71
101
 
102
+ /** The most work of a key, with the values of its tests: see {@link weightOf}. */
103
+ const keyWeight = 256
104
+
72
105
  /**
73
106
  * The key of a child in a smart constructor: the text of its structure, or
74
107
  * the node itself when it is large. The keys go into a native `Set`, not a
@@ -77,8 +110,10 @@ const keyNodes = 64
77
110
  * set would read all of them. A native `Set` finds a node by the node itself,
78
111
  * and it hashes a string with the random seed of the engine.
79
112
  */
80
- const keyOf = (node: Node): string | Node =>
81
- node.size > keyNodes ? node : structureText(node)
113
+ export const keyOf = (node: Node): string | Node =>
114
+ node.size > keyNodes || weightOf(node) > keyWeight
115
+ ? node
116
+ : structureText(node)
82
117
 
83
118
  /**
84
119
  * Keeps the first node of each structure, in order. A large node is a copy
@@ -145,7 +180,8 @@ const shortCircuit = (
145
180
  /**
146
181
  * Makes an `And` or an `Or` of simple children, and makes it simple. It looks
147
182
  * one level down only:
148
- * - it puts the children of a child of the same kind into the node;
183
+ * - it puts the children of a child of the same kind into the node, when
184
+ * the child has at most `keyNodes` of them;
149
185
  * - it stops at a `False` in an `And`, or at a `True` in an `Or`;
150
186
  * - it removes `True` from an `And`, `False` from an `Or`, copies, and the
151
187
  * children that other children make useless.
@@ -158,7 +194,9 @@ const junction = (tag: "And" | "Or", children: ReadonlyArray<Node>): Node => {
158
194
  const constant = tag === "And" ? "False" : "True"
159
195
  const neutral = tag === "And" ? "True" : "False"
160
196
  const flat = Arr.flatMap(children, (child) =>
161
- child._tag === tag && !child.opaque ? child.conditions : [child],
197
+ child._tag === tag && !child.opaque && child.conditions.length <= keyNodes
198
+ ? child.conditions
199
+ : [child],
162
200
  )
163
201
  return Option.match(
164
202
  Arr.findFirstIndex(flat, (child) => child._tag === constant),
@@ -216,20 +254,41 @@ export const not = (child: Node): Node => {
216
254
  }
217
255
  }
218
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
+
219
267
  /** Makes a condition simple, from its leaves up. */
220
- export const simplify = (node: Node): Node => {
221
- switch (node._tag) {
222
- case "And":
223
- case "Or":
224
- return junction(node._tag, Arr.map(node.conditions, simplify))
225
- case "Not":
226
- return node.condition.pipe(simplify, not)
227
- case "In":
228
- return node.values.length === 0 ? never : node
229
- default:
230
- return node
231
- }
232
- }
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
+ })
233
292
 
234
293
  // Tests on a value
235
294
 
@@ -323,69 +382,99 @@ const testUnder = (
323
382
  })
324
383
  }
325
384
 
326
- /** Tests a simple condition with no predicate on a value. */
327
- const testSync = (node: Node, input: Input): boolean => {
328
- switch (node._tag) {
329
- case "True":
330
- return true
331
- case "False":
332
- return false
333
- case "Compare":
334
- return testCompare(
335
- node.op,
336
- getField(input.value, node.field),
337
- resolve(node.value, input.principal),
338
- )
339
- case "In": {
340
- const field = getField(input.value, node.field)
341
- return node.values.some(
342
- (operand) => resolve(operand, input.principal) === field,
343
- )
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
+ )
344
431
  }
345
- case "Exists":
346
- return getField(input.value, node.field) !== null
347
- case "Under":
348
- return testUnder(node.prefix, input)
349
- case "And":
350
- return Arr.every(node.conditions, (child) => testSync(child, input))
351
- case "Or":
352
- return Arr.some(node.conditions, (child) => testSync(child, input))
353
- case "Not":
354
- return !testSync(node.condition, input)
355
- case "Predicate":
356
- // The types stop this. A predicate needs `evaluate`.
357
- throw new Error(
358
- `The predicate "${node.name}" needs an Effect. Use Condition.evaluate.`,
359
- )
360
432
  }
433
+ return go
361
434
  }
362
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
+
363
440
  /**
364
441
  * Tests a simple condition on a value. The children of `And` and `Or` run in
365
- * 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.
366
445
  */
367
446
  const test = (
368
447
  node: Node,
369
448
  input: Input,
370
449
  ): Effect.Effect<boolean, unknown, unknown> => {
371
450
  if (!node.opaque) {
372
- 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)
373
454
  }
374
455
  switch (node._tag) {
375
456
  case "Predicate":
376
457
  return Nodes.runOf(node)(input.value, input.principal)
377
458
  case "Not":
378
- return Effect.map(test(node.condition, input), (value) => !value)
459
+ return Effect.map(
460
+ Effect.suspend(() => test(node.condition, input)),
461
+ (value) => !value,
462
+ )
379
463
  case "And":
380
464
  return Effect.map(
381
465
  Effect.findFirst(node.conditions, (child) =>
382
- Effect.map(test(child, input), (value) => !value),
466
+ Effect.map(
467
+ Effect.suspend(() => test(child, input)),
468
+ (value) => !value,
469
+ ),
383
470
  ),
384
471
  Option.isNone,
385
472
  )
386
473
  case "Or":
387
474
  return Effect.map(
388
- Effect.findFirst(node.conditions, (child) => test(child, input)),
475
+ Effect.findFirst(node.conditions, (child) =>
476
+ Effect.suspend(() => test(child, input)),
477
+ ),
389
478
  Option.isSome,
390
479
  )
391
480
  default:
@@ -465,96 +554,126 @@ export interface PartialInput {
465
554
  * resource. The result is simple. It has no principal fields and no `Under`
466
555
  * nodes. A predicate keeps the principal.
467
556
  */
468
- export const partial = (node: Node, input: PartialInput): Node => {
469
- const bind = (current: Node): Node => {
470
- switch (current._tag) {
471
- case "Compare": {
472
- const value = bindOperand(current.value, input.principal)
473
- return value === undefined
474
- ? never
475
- : value === current.value
476
- ? current
477
- : new Nodes.Compare({ op: current.op, field: current.field, value })
478
- }
479
- case "In": {
480
- const values = Arr.flatMap(current.values, (operand) =>
481
- Option.toArray(
482
- Option.fromUndefinedOr(bindOperand(operand, input.principal)),
483
- ),
484
- )
485
- return values.length === 0
486
- ? never
487
- : new Nodes.In({ field: current.field, values })
488
- }
489
- case "Under":
490
- return expandUnder(current.prefix, input.principal, input.path)
491
- case "And":
492
- return and(Arr.map(current.conditions, bind))
493
- case "Or":
494
- return or(Arr.map(current.conditions, bind))
495
- case "Not":
496
- return current.condition.pipe(bind, not)
497
- case "Predicate": {
498
- const run = Nodes.runOf(current)
499
- return new Nodes.Predicate({
500
- name: current.name,
501
- run: (value) => run(value, input.principal),
502
- })
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)
503
611
  }
504
- default:
505
- return current
506
612
  }
507
- }
508
- return bind(node)
509
- }
613
+ return bind(node)
614
+ })
510
615
 
511
616
  /** Gives the services in `context` to every predicate of a condition. */
512
- export const provide = (node: Node, context: Context.Context<never>): Node => {
513
- if (!node.opaque) {
514
- return node
515
- }
516
- switch (node._tag) {
517
- case "Predicate": {
518
- const run = Nodes.runOf(node)
519
- return new Nodes.Predicate({
520
- name: node.name,
521
- run: (value, principal) =>
522
- Effect.provideContext(run(value, principal), context),
523
- })
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
+ }
524
652
  }
525
- case "And":
526
- return new Nodes.And(
527
- Arr.map(node.conditions, (child) => provide(child, context)),
528
- )
529
- case "Or":
530
- return new Nodes.Or(
531
- Arr.map(node.conditions, (child) => provide(child, context)),
532
- )
533
- case "Not":
534
- return new Nodes.Not(provide(node.condition, context))
535
- default:
536
- return node
537
- }
538
- }
653
+ return go(node)
654
+ })
539
655
 
540
656
  /** The fields that a condition tests, in the order that they first come. */
541
657
  export const fields = (node: Node): ReadonlyArray<string> => {
542
- const all = (current: Node): ReadonlyArray<string> => {
543
- switch (current._tag) {
544
- case "Compare":
545
- case "In":
546
- case "Exists":
547
- return [current.field]
548
- case "And":
549
- case "Or":
550
- return Arr.flatMap(current.conditions, all)
551
- case "Not":
552
- return all(current.condition)
553
- default:
554
- 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
+ }
555
673
  }
556
- }
674
+ return go(node)
675
+ })
557
676
  // A native `Set`, as in `dedupe`: a field of a condition that is not bound
558
677
  // to a domain may be any string.
559
- return Array.from(new Set(all(node)))
678
+ return Array.from(new Set(all))
560
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(