@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.
- package/README.md +6 -5
- package/dist/Condition.d.ts +6 -6
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +1 -1
- package/dist/Policy.d.ts +2 -2
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js.map +1 -1
- package/dist/internal/algebra.js +128 -79
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/analysis.js +1 -1
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +38 -14
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +59 -43
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +6 -0
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +17 -14
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +1 -1
- package/dist/internal/domain.js +1 -1
- package/dist/internal/json.js +2 -10
- package/dist/internal/json.js.map +1 -1
- package/dist/internal/nodes.d.ts +11 -1
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +51 -11
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/patterns.js +1 -1
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-json.js +2 -8
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts +4 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +14 -5
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/search.js +4 -4
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/trampoline.js +75 -0
- package/dist/internal/trampoline.js.map +1 -0
- package/package.json +1 -1
- package/src/Condition.ts +5 -2
- package/src/Policy.ts +1 -2
- package/src/internal/algebra.ts +262 -143
- package/src/internal/analysis.ts +3 -1
- package/src/internal/authorizer.ts +59 -14
- package/src/internal/bind.ts +149 -71
- package/src/internal/catalog.ts +54 -50
- package/src/internal/json.ts +2 -11
- package/src/internal/nodes.ts +58 -24
- package/src/internal/patterns.ts +7 -3
- package/src/internal/policy-json.ts +2 -9
- package/src/internal/policy-nodes.ts +16 -7
- package/src/internal/search.ts +13 -10
- 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
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
|
-
:
|
|
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.
|
package/src/internal/algebra.ts
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
64
|
-
JSON.stringify(shapeOf(node))
|
|
65
|
+
const structureText = (node: Node): string => JSON.stringify(shapeOf(node))
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
|
-
* The
|
|
68
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
/**
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
526
|
-
|
|
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 = (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
|
678
|
+
return Array.from(new Set(all))
|
|
560
679
|
}
|
package/src/internal/analysis.ts
CHANGED
|
@@ -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(
|