@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
@@ -21,12 +21,31 @@ import * as Arr from "effect/Array"
21
21
  import * as Data from "effect/Data"
22
22
  import type * as Effect from "effect/Effect"
23
23
  import { hasProperty } from "effect/Predicate"
24
+ import * as Schema from "effect/Schema"
24
25
  import * as Limits from "./limits.ts"
25
26
 
26
27
  /** A value that a condition can compare. */
27
28
  export type Scalar = string | number | boolean | null
28
29
 
29
- export type CompareOperator = "eq" | "ne" | "lt" | "lte" | "gt" | "gte"
30
+ /** The operators of a `Compare` test. The decoders read this list too. */
31
+ export const CompareOperator = Schema.Literals([
32
+ "eq",
33
+ "ne",
34
+ "lt",
35
+ "lte",
36
+ "gt",
37
+ "gte",
38
+ ])
39
+
40
+ export type CompareOperator = typeof CompareOperator.Type
41
+
42
+ /**
43
+ * Tests if a value is an operator. It reads the list of the Schema with
44
+ * `includes`: for a short list, this is about twice as fast as
45
+ * `Schema.is`, and a decoder tests each node of a large input.
46
+ */
47
+ export const isCompareOperator = (u: unknown): u is CompareOperator =>
48
+ (CompareOperator.literals as ReadonlyArray<unknown>).includes(u)
30
49
 
31
50
  /** A fixed value. */
32
51
  export interface Literal {
@@ -49,15 +68,6 @@ export const literal = (value: Scalar): Literal => operands.Literal({ value })
49
68
  export const principalRef = (path: string): PrincipalRef =>
50
69
  operands.Principal({ path })
51
70
 
52
- const compareOperators: ReadonlyArray<string> = [
53
- "eq",
54
- "ne",
55
- "lt",
56
- "lte",
57
- "gt",
58
- "gte",
59
- ]
60
-
61
71
  /** Tests if a value is a scalar. Any number is one, also `NaN`. */
62
72
  export const isScalar = (value: unknown): value is Scalar =>
63
73
  value === null ||
@@ -114,24 +124,39 @@ const checkOperands = (values: unknown): ReadonlyArray<Operand> => {
114
124
 
115
125
  interface Facts {
116
126
  readonly size: number
127
+ readonly weight: number
117
128
  readonly depth: number
118
129
  readonly opaque: boolean
119
130
  }
120
131
 
121
- const leaf: Facts = { size: 1, depth: 1, opaque: false }
132
+ const leaf: Facts = { size: 1, weight: 1, depth: 1, opaque: false }
122
133
 
123
- /** The facts of a node with children. It stops a node past the limits. */
124
- const factsOf = (children: ReadonlyArray<Node>): Facts => {
125
- const size = Arr.reduce(children, 1, (total, child) => total + child.size)
126
- if (size > Limits.conditionNodes) {
134
+ /**
135
+ * The facts of a test with values, as `In` and `Under`. Each value counts in
136
+ * the weight, since a walk that reads the test reads its values too. It stops
137
+ * a test past the limits.
138
+ */
139
+ const valuesFacts = (values: ReadonlyArray<Operand>): Facts =>
140
+ checkWeight({ ...leaf, weight: 1 + values.length })
141
+
142
+ const checkWeight = (facts: Facts): Facts => {
143
+ if (facts.weight > Limits.conditionNodes) {
127
144
  throw Limits.exceeded(
128
145
  "A condition",
129
146
  "conditionNodes",
130
- "nodes",
131
- size,
147
+ "nodes and values",
148
+ facts.weight,
132
149
  Limits.conditionNodes,
133
150
  )
134
151
  }
152
+ return facts
153
+ }
154
+
155
+ /** The facts of a node with children. It stops a node past the limits. */
156
+ const factsOf = (children: ReadonlyArray<Node>): Facts => {
157
+ const size = Arr.reduce(children, 1, (total, child) => total + child.size)
158
+ const weight = Arr.reduce(children, 1, (total, child) => total + child.weight)
159
+ checkWeight({ ...leaf, weight })
135
160
  const depth =
136
161
  1 + Arr.reduce(children, 0, (top, child) => Math.max(top, child.depth))
137
162
  if (depth > Limits.conditionDepth) {
@@ -145,6 +170,7 @@ const factsOf = (children: ReadonlyArray<Node>): Facts => {
145
170
  }
146
171
  return {
147
172
  size,
173
+ weight,
148
174
  depth,
149
175
  opaque: Arr.some(children, (child) => child.opaque),
150
176
  }
@@ -173,6 +199,15 @@ abstract class Base extends Data.Class {
173
199
  return this.#facts.size
174
200
  }
175
201
 
202
+ /**
203
+ * The number of nodes and of the values of their `In` and `Under` tests,
204
+ * with a shared child counted for each place. A walk that reads the tests
205
+ * does this much work, so the limits of the nodes count it.
206
+ */
207
+ get weight(): number {
208
+ return this.#facts.weight
209
+ }
210
+
176
211
  /** The number of levels. */
177
212
  get depth(): number {
178
213
  return this.#facts.depth
@@ -211,7 +246,7 @@ export class Compare extends Base {
211
246
  readonly field: string
212
247
  readonly value: Operand
213
248
  }) {
214
- if (!compareOperators.includes(props.op)) {
249
+ if (!isCompareOperator(props.op)) {
215
250
  throw new Error(`"${String(props.op)}" is not a compare operator`)
216
251
  }
217
252
  super(
@@ -234,10 +269,8 @@ export class In extends Base {
234
269
  readonly field: string
235
270
  readonly values: ReadonlyArray<Operand>
236
271
  }) {
237
- super(
238
- { field: checkPath(props.field), values: checkOperands(props.values) },
239
- leaf,
240
- )
272
+ const values = checkOperands(props.values)
273
+ super({ field: checkPath(props.field), values }, valuesFacts(values))
241
274
  }
242
275
  }
243
276
 
@@ -255,7 +288,8 @@ export class Under extends Base {
255
288
  readonly _tag = "Under"
256
289
  declare readonly prefix: ReadonlyArray<Operand>
257
290
  constructor(props: { readonly prefix: ReadonlyArray<Operand> }) {
258
- super({ prefix: checkOperands(props.prefix) }, leaf)
291
+ const prefix = checkOperands(props.prefix)
292
+ super({ prefix }, valuesFacts(prefix))
259
293
  }
260
294
  }
261
295
 
@@ -323,7 +357,7 @@ export class Predicate extends Base {
323
357
  }
324
358
  super(
325
359
  { name: props.name, run: props.run },
326
- { size: 1, depth: 1, opaque: true },
360
+ { size: 1, weight: 1, depth: 1, opaque: true },
327
361
  )
328
362
  }
329
363
  }
@@ -117,9 +117,13 @@ type States = ReadonlyArray<number>
117
117
 
118
118
  /** Adds the place after each `**`, since a `**` may match no names. */
119
119
  const close = (pattern: Pattern, states: States): States =>
120
- Arr.dedupe(
121
- Arr.flatMap(states, (state) =>
122
- pattern[state]?._tag === "Globstar" ? [state, state + 1] : [state],
120
+ // A native `Set` keeps the first copy of each place, in order. It is
121
+ // faster than `Arr.dedupe`, and this is on the path of each match.
122
+ Array.from(
123
+ new Set(
124
+ Arr.flatMap(states, (state) =>
125
+ pattern[state]?._tag === "Globstar" ? [state, state + 1] : [state],
126
+ ),
123
127
  ),
124
128
  )
125
129
 
@@ -136,13 +136,6 @@ export const encode = (
136
136
  return toJson(node, (rule) => forms.get(rule))
137
137
  })
138
138
 
139
- const algorithms: ReadonlyArray<string> = [
140
- "denyOverrides",
141
- "permitOverrides",
142
- "firstApplicable",
143
- "lastApplicable",
144
- ]
145
-
146
139
  /** Reads a list of strings. */
147
140
  const readStrings = (
148
141
  input: object,
@@ -374,7 +367,7 @@ const readTree = (
374
367
  switch (tag) {
375
368
  case "Rules": {
376
369
  const algorithm = Json.readString(input, "algorithm", path)
377
- if (!algorithms.includes(algorithm)) {
370
+ if (!PolicyNodes.isAlgorithm(algorithm)) {
378
371
  throw Json.problem(
379
372
  [...path, "algorithm"],
380
373
  `"${algorithm}" is not a combining algorithm`,
@@ -392,7 +385,7 @@ const readTree = (
392
385
  _tag: "Rules",
393
386
  place,
394
387
  size: 1 + found.length,
395
- algorithm: algorithm as PolicyNodes.Algorithm,
388
+ algorithm,
396
389
  rules: found,
397
390
  })
398
391
  }
@@ -11,19 +11,27 @@
11
11
  */
12
12
  import * as Arr from "effect/Array"
13
13
  import * as Data from "effect/Data"
14
+ import * as Schema from "effect/Schema"
14
15
  import * as Rule from "./rule.ts"
15
16
  import * as Limits from "./limits.ts"
16
17
  import * as Nodes from "./nodes.ts"
17
18
 
18
- export type Algorithm =
19
- "denyOverrides" | "permitOverrides" | "firstApplicable" | "lastApplicable"
20
-
21
- const algorithms: ReadonlyArray<string> = [
19
+ /** The combining algorithms of a list of rules. The decoder reads this list too. */
20
+ export const Algorithm = Schema.Literals([
22
21
  "denyOverrides",
23
22
  "permitOverrides",
24
23
  "firstApplicable",
25
24
  "lastApplicable",
26
- ]
25
+ ])
26
+
27
+ export type Algorithm = typeof Algorithm.Type
28
+
29
+ /**
30
+ * Tests if a value is a combining algorithm. It reads the list of the Schema
31
+ * with `includes`, as `Nodes.isCompareOperator` does.
32
+ */
33
+ export const isAlgorithm = (u: unknown): u is Algorithm =>
34
+ (Algorithm.literals as ReadonlyArray<unknown>).includes(u)
27
35
 
28
36
  interface Facts {
29
37
  readonly size: number
@@ -103,7 +111,7 @@ export class Rules extends Base {
103
111
  readonly algorithm: Algorithm
104
112
  readonly rules: ReadonlyArray<Rule.Rule>
105
113
  }) {
106
- if (!algorithms.includes(props.algorithm)) {
114
+ if (!isAlgorithm(props.algorithm)) {
107
115
  throw new Error(
108
116
  `"${String(props.algorithm)}" is not a combining algorithm`,
109
117
  )
@@ -122,7 +130,8 @@ export class Rules extends Base {
122
130
  size: Arr.reduce(
123
131
  conditions,
124
132
  1,
125
- (total, when) => total + 1 + (when?.size ?? 0),
133
+ // A condition counts its nodes and the values of its tests.
134
+ (total, when) => total + 1 + (when?.weight ?? 0),
126
135
  ),
127
136
  depth: 1,
128
137
  opaque: Arr.some(conditions, (when) => when?.opaque ?? false),
@@ -196,12 +196,13 @@ interface Search {
196
196
  */
197
197
  readonly members: Map<Nodes.In, Set<string>>
198
198
  /**
199
- * The texts of the structure of the conditions that the search found to be
200
- * unsatisfiable. A condition with the same structure has the same free
201
- * fields, so its answer is the same. The set only grows, so the steps of a
202
- * search may share it. It is a native `Set`, as in {@link dedupeCells}.
199
+ * The keys of the conditions that the search found to be unsatisfiable:
200
+ * see `Algebra.keyOf`. A condition with the same structure has the same
201
+ * free fields, so its answer is the same. The set only grows, so the steps
202
+ * of a search may share it. It is a native `Set`, as in
203
+ * {@link dedupeCells}.
203
204
  */
204
- readonly failed: Set<string>
205
+ readonly failed: Set<string | Node>
205
206
  readonly budget: number
206
207
  }
207
208
 
@@ -364,10 +365,12 @@ const searchNode = (
364
365
  if (node._tag === "False") {
365
366
  return { assignment: Option.none(), state }
366
367
  }
367
- // The text reads all of the node. The step before counted the size of a
368
- // larger node, so the budget counts this work too.
369
- const text = Algebra.structureText(node)
370
- if (search.failed.has(text)) {
368
+ // The key reads at most a small part of the node: a node with very many
369
+ // nodes, or with very many values in its `In` tests, is its own key. A
370
+ // step reads the nodes but not the values, which `membersOf` keeps, so the
371
+ // budget counts the size.
372
+ const key = Algebra.keyOf(node)
373
+ if (search.failed.has(key)) {
371
374
  return { assignment: Option.none(), state }
372
375
  }
373
376
  if (state.steps + node.size > search.budget) {
@@ -400,7 +403,7 @@ const searchNode = (
400
403
  },
401
404
  )
402
405
  if (Option.isNone(result.assignment)) {
403
- search.failed.add(text)
406
+ search.failed.add(key)
404
407
  }
405
408
  return result
406
409
  }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Walks that do not fill the stack.
3
+ *
4
+ * A walk is written one time, with the steps of a {@link Runner}, and a
5
+ * runner runs it:
6
+ * - {@link direct} runs each step at once, as a plain call does. It is fast,
7
+ * and it is for an input with few levels.
8
+ * - {@link loop} puts each step into the run loop of an Effect. A step gives
9
+ * the next step in place of calling it, and the loop runs one step after
10
+ * the other, so the stack does not grow with the levels of the input. This
11
+ * is a trampoline.
12
+ *
13
+ * {@link run} takes `direct` for an input with at most `directDepth` levels,
14
+ * and `loop` for a deeper one. A walk makes each call for a child through
15
+ * `suspend`, so the loop makes the step of the child only when it runs it.
16
+ */
17
+ import * as Arr from "effect/Array"
18
+ import * as Cause from "effect/Cause"
19
+ import * as Effect from "effect/Effect"
20
+ import * as Exit from "effect/Exit"
21
+ import * as Option from "effect/Option"
22
+
23
+ /**
24
+ * A step of a walk that gives an `A`. A runner of steps knows what a step
25
+ * is: the value itself for `direct`, and an Effect for `loop`. The type is
26
+ * only in the types.
27
+ */
28
+ export interface Step<A> {
29
+ readonly "~Step": (_: never) => A
30
+ }
31
+
32
+ /** The steps that a walk may take. */
33
+ export interface Runner {
34
+ readonly succeed: <A>(value: A) => Step<A>
35
+ /** The step that `step` makes, when the runner runs it. */
36
+ readonly suspend: <A>(step: () => Step<A>) => Step<A>
37
+ readonly map: <A, B>(step: Step<A>, f: (value: A) => B) => Step<B>
38
+ readonly flatMap: <A, B>(step: Step<A>, f: (value: A) => Step<B>) => Step<B>
39
+ /** Runs a step for each item, in order, and gives their values. */
40
+ readonly forEach: <A, B>(
41
+ items: ReadonlyArray<A>,
42
+ f: (item: A, index: number) => Step<B>,
43
+ ) => Step<ReadonlyArray<B>>
44
+ /** Runs a step for each item, in order, up to the first `true`. */
45
+ readonly some: <A>(
46
+ items: ReadonlyArray<A>,
47
+ f: (item: A) => Step<boolean>,
48
+ ) => Step<boolean>
49
+ /** Runs a step for each item, in order, up to the first `false`. */
50
+ readonly every: <A>(
51
+ items: ReadonlyArray<A>,
52
+ f: (item: A) => Step<boolean>,
53
+ ) => Step<boolean>
54
+ }
55
+
56
+ // A step of `direct` is its value. The casts below only change the type.
57
+ const value = <A>(step: Step<A>): A => step as unknown as A
58
+ const step = <A>(value: A): Step<A> => value as unknown as Step<A>
59
+
60
+ /** Runs each step at once, on the stack. */
61
+ export const direct: Runner = {
62
+ succeed: step,
63
+ suspend: (make) => make(),
64
+ map: (from, f) => step(f(value(from))),
65
+ flatMap: (from, f) => f(value(from)),
66
+ forEach: (items, f) =>
67
+ step(Arr.map(items, (item, index) => value(f(item, index)))),
68
+ some: (items, f) => step(Arr.some(items, (item) => value(f(item)))),
69
+ every: (items, f) => step(Arr.every(items, (item) => value(f(item)))),
70
+ }
71
+
72
+ // A step of `loop` is an Effect with no error and no services. A failure of a
73
+ // walk is a thrown error, which `run` gives back as it was.
74
+ type Loop<A> = Effect.Effect<A>
75
+ const effect = <A>(step: Step<A>): Loop<A> => step as unknown as Loop<A>
76
+ const fromEffect = <A>(effect: Loop<A>): Step<A> => effect as unknown as Step<A>
77
+
78
+ /** Runs each step in the run loop of an Effect, so a deep walk does not fill the stack. */
79
+ export const loop: Runner = {
80
+ succeed: (value) => fromEffect(Effect.succeed(value)),
81
+ suspend: (make) => fromEffect(Effect.suspend(() => effect(make()))),
82
+ map: (from, f) => fromEffect(Effect.map(effect(from), f)),
83
+ flatMap: (from, f) =>
84
+ fromEffect(Effect.flatMap(effect(from), (value) => effect(f(value)))),
85
+ forEach: (items, f) =>
86
+ fromEffect(Effect.forEach(items, (item, index) => effect(f(item, index)))),
87
+ some: (items, f) =>
88
+ fromEffect(
89
+ Effect.map(
90
+ Effect.findFirst(items, (item) => effect(f(item))),
91
+ Option.isSome,
92
+ ),
93
+ ),
94
+ every: (items, f) =>
95
+ fromEffect(
96
+ Effect.map(
97
+ Effect.findFirst(items, (item) =>
98
+ Effect.map(effect(f(item)), (value) => !value),
99
+ ),
100
+ Option.isNone,
101
+ ),
102
+ ),
103
+ }
104
+
105
+ /**
106
+ * The most levels of an input that `run` walks with `direct`. A level takes
107
+ * a few calls of the walk and of the runner on the stack, so this keeps the
108
+ * stack far from full, and most inputs take the fast way.
109
+ */
110
+ export const directDepth = 200
111
+
112
+ /**
113
+ * Gives the runner for an input with `depth` levels. The walks of a condition
114
+ * or a policy tree know the depth of the input from its node. A walk of an
115
+ * input with an unknown depth, as a decoder, takes `loop`.
116
+ */
117
+ export const runnerFor = (depth: number): Runner =>
118
+ depth <= directDepth ? direct : loop
119
+
120
+ /**
121
+ * Runs a walk at once, with the runner for `depth` levels, and gives its
122
+ * value. An error that a step throws comes out as it was, also from `loop`.
123
+ */
124
+ export const run = <A>(depth: number, walk: (runner: Runner) => Step<A>): A =>
125
+ runWith(runnerFor(depth), walk)
126
+
127
+ /** Runs a walk at once with a runner, as {@link run} does. */
128
+ export const runWith = <A>(
129
+ runner: Runner,
130
+ walk: (runner: Runner) => Step<A>,
131
+ ): A => {
132
+ if (runner === direct) {
133
+ return value(walk(direct))
134
+ }
135
+ return Exit.match(Effect.runSyncExit(effect(walk(loop))), {
136
+ onSuccess: (result) => result,
137
+ onFailure: (cause) => {
138
+ throw Cause.squash(cause)
139
+ },
140
+ })
141
+ }
142
+
143
+ /**
144
+ * Gives the Effect of a step of `loop`, so a walk can go on in an Effect
145
+ * that runs a predicate.
146
+ */
147
+ export const toEffect = <A>(step: Step<A>): Effect.Effect<A> => effect(step)