@nmnmcc/ability 2.0.7 → 2.1.2

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 (87) hide show
  1. package/README.md +52 -29
  2. package/dist/Authorizer.d.ts +21 -12
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js.map +1 -1
  5. package/dist/Condition.d.ts +121 -19
  6. package/dist/Condition.d.ts.map +1 -1
  7. package/dist/Condition.js +32 -21
  8. package/dist/Condition.js.map +1 -1
  9. package/dist/Domain.d.ts +9 -0
  10. package/dist/Domain.d.ts.map +1 -1
  11. package/dist/Domain.js +5 -0
  12. package/dist/Domain.js.map +1 -1
  13. package/dist/Limits.d.ts +44 -13
  14. package/dist/Limits.d.ts.map +1 -1
  15. package/dist/Limits.js +52 -22
  16. package/dist/Limits.js.map +1 -1
  17. package/dist/Policy.d.ts +102 -18
  18. package/dist/Policy.d.ts.map +1 -1
  19. package/dist/Policy.js +7 -6
  20. package/dist/Policy.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/internal/algebra.js +2 -2
  23. package/dist/internal/analysis.js +81 -75
  24. package/dist/internal/analysis.js.map +1 -1
  25. package/dist/internal/authorizer.d.ts.map +1 -1
  26. package/dist/internal/authorizer.js +11 -7
  27. package/dist/internal/authorizer.js.map +1 -1
  28. package/dist/internal/bind.js +86 -64
  29. package/dist/internal/bind.js.map +1 -1
  30. package/dist/internal/catalog.d.ts +33 -10
  31. package/dist/internal/catalog.d.ts.map +1 -1
  32. package/dist/internal/catalog.js +170 -77
  33. package/dist/internal/catalog.js.map +1 -1
  34. package/dist/internal/decide.js +27 -31
  35. package/dist/internal/decide.js.map +1 -1
  36. package/dist/internal/domain.js +19 -5
  37. package/dist/internal/domain.js.map +1 -1
  38. package/dist/internal/json.js +120 -76
  39. package/dist/internal/json.js.map +1 -1
  40. package/dist/internal/limits.d.ts +28 -0
  41. package/dist/internal/limits.d.ts.map +1 -1
  42. package/dist/internal/limits.js +75 -16
  43. package/dist/internal/limits.js.map +1 -1
  44. package/dist/internal/nodes.d.ts.map +1 -1
  45. package/dist/internal/nodes.js +53 -44
  46. package/dist/internal/nodes.js.map +1 -1
  47. package/dist/internal/patterns.js +50 -15
  48. package/dist/internal/patterns.js.map +1 -1
  49. package/dist/internal/policy-json.js +169 -127
  50. package/dist/internal/policy-json.js.map +1 -1
  51. package/dist/internal/policy-nodes.d.ts.map +1 -1
  52. package/dist/internal/policy-nodes.js +16 -13
  53. package/dist/internal/policy-nodes.js.map +1 -1
  54. package/dist/internal/policy.d.ts.map +1 -1
  55. package/dist/internal/policy.js +38 -22
  56. package/dist/internal/policy.js.map +1 -1
  57. package/dist/internal/resource.js +1 -1
  58. package/dist/internal/rule.d.ts.map +1 -1
  59. package/dist/internal/rule.js +6 -5
  60. package/dist/internal/rule.js.map +1 -1
  61. package/dist/internal/search.d.ts.map +1 -1
  62. package/dist/internal/search.js +106 -92
  63. package/dist/internal/search.js.map +1 -1
  64. package/dist/internal/trampoline.js +19 -5
  65. package/dist/internal/trampoline.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/Authorizer.ts +30 -20
  68. package/src/Condition.ts +317 -86
  69. package/src/Domain.ts +9 -0
  70. package/src/Limits.ts +69 -29
  71. package/src/Policy.ts +143 -21
  72. package/src/internal/analysis.ts +154 -124
  73. package/src/internal/authorizer.ts +29 -14
  74. package/src/internal/bind.ts +193 -136
  75. package/src/internal/catalog.ts +320 -118
  76. package/src/internal/decide.ts +61 -50
  77. package/src/internal/domain.ts +39 -3
  78. package/src/internal/json.ts +212 -117
  79. package/src/internal/limits.ts +101 -31
  80. package/src/internal/nodes.ts +65 -47
  81. package/src/internal/patterns.ts +79 -25
  82. package/src/internal/policy-json.ts +266 -197
  83. package/src/internal/policy-nodes.ts +31 -23
  84. package/src/internal/policy.ts +63 -23
  85. package/src/internal/rule.ts +16 -3
  86. package/src/internal/search.ts +186 -136
  87. package/src/internal/trampoline.ts +51 -4
@@ -14,6 +14,7 @@ import * as Json from "./json.ts"
14
14
  import * as Limits from "./limits.ts"
15
15
  import * as Place from "./place.ts"
16
16
  import * as PolicyNodes from "./policy-nodes.ts"
17
+ import * as Trampoline from "./trampoline.ts"
17
18
 
18
19
  type Node = PolicyNodes.Node
19
20
 
@@ -80,32 +81,43 @@ export const toJson = (node: Node, meta: MetaJson): unknown => {
80
81
  rule.when === undefined ? undefined : Json.toJson(rule.when as never),
81
82
  ]),
82
83
  )
83
- const tree = (current: Node): unknown => {
84
- switch (current._tag) {
85
- case "Rules":
86
- return {
87
- _tag: "Rules",
88
- algorithm: current.algorithm,
89
- rules: Arr.map(current.rules, (rule) =>
90
- ruleJson(rule, meta, (found) => whens.get(found)),
91
- ),
92
- }
93
- case "All":
94
- case "Any":
95
- case "FirstOf":
96
- return {
97
- _tag: current._tag,
98
- policies: Arr.map(current.policies, tree),
99
- }
100
- case "Ceiling":
101
- return {
102
- _tag: "Ceiling",
103
- cap: tree(current.cap),
104
- policy: tree(current.policy),
84
+ // The walk of the tree takes the steps of a trampoline, so a deep tree
85
+ // does not fill the stack.
86
+ return Trampoline.run(node.depth, (R) => {
87
+ const tree = (current: Node): Trampoline.Step<unknown> => {
88
+ switch (current._tag) {
89
+ case "Rules":
90
+ return R.succeed({
91
+ _tag: "Rules",
92
+ algorithm: current.algorithm,
93
+ rules: Arr.map(current.rules, (rule) =>
94
+ ruleJson(rule, meta, (found) => whens.get(found)),
95
+ ),
96
+ })
97
+ case "All":
98
+ case "Any":
99
+ case "FirstOf": {
100
+ const tag = current._tag
101
+ return R.map(
102
+ R.forEach(current.policies, (child) =>
103
+ R.suspend(() => tree(child)),
104
+ ),
105
+ (policies) => ({ _tag: tag, policies }),
106
+ )
105
107
  }
108
+ case "Ceiling":
109
+ return R.flatMap(
110
+ R.suspend(() => tree(current.cap)),
111
+ (cap) =>
112
+ R.map(
113
+ R.suspend(() => tree(current.policy)),
114
+ (policy) => ({ _tag: "Ceiling", cap, policy }),
115
+ ),
116
+ )
117
+ }
106
118
  }
107
- }
108
- return tree(node)
119
+ return tree(node)
120
+ })
109
121
  }
110
122
 
111
123
  /**
@@ -140,12 +152,15 @@ export const encode = (
140
152
  const readStrings = (
141
153
  input: object,
142
154
  name: string,
143
- path: ReadonlyArray<PropertyKey>,
155
+ at: Place.Place | undefined,
144
156
  budget: Json.Budget,
145
157
  ): ReadonlyArray<string> =>
146
- Arr.map(Json.readList(input, name, path, budget), (value, index) => {
158
+ Arr.map(Json.readList(input, name, at, budget), (value, index) => {
147
159
  if (typeof value !== "string") {
148
- throw Json.problem([...path, name, index], "The item must be a string")
160
+ throw Json.problemAt(
161
+ Place.at(at, name, index),
162
+ "The item must be a string",
163
+ )
149
164
  }
150
165
  return value
151
166
  })
@@ -158,55 +173,56 @@ const readStrings = (
158
173
  const readOptional = <A>(
159
174
  input: object,
160
175
  name: string,
161
- path: ReadonlyArray<PropertyKey>,
176
+ at: Place.Place | undefined,
162
177
  read: () => A,
163
178
  ): A | undefined => {
164
179
  if (Object.hasOwn(input, name)) {
165
180
  return read()
166
181
  }
167
182
  // A read of the field throws a problem when the field is not data.
168
- Json.read(input, name, path)
183
+ Json.read(input, name, at)
169
184
  return undefined
170
185
  }
171
186
 
172
- /** Counts the objects and items of a value, as the other parts count. */
173
- const spendValue = (
174
- value: unknown,
175
- path: ReadonlyArray<PropertyKey>,
176
- depth: number,
177
- budget: Json.Budget,
178
- ): void => {
179
- if (typeof value !== "object" || value === null) {
180
- return
181
- }
182
- if (depth > Limits.conditionDepth) {
183
- throw Json.problem(
184
- path,
185
- `The value has more than ${String(Limits.conditionDepth)} levels. The most is ${String(Limits.conditionDepth)}.`,
186
- )
187
- }
188
- // The length of an array counts before its items are read, so a sparse
189
- // array with a very large length stops at once.
190
- const list = Array.isArray(value)
191
- if (list) {
192
- Json.spend(budget, 1 + value.length, path)
193
- }
194
- const names = list
195
- ? Array.from({ length: value.length }, (_, index) => String(index))
196
- : Object.keys(value)
197
- if (!list) {
198
- Json.spend(budget, 1 + names.length, path)
199
- }
200
- Arr.forEach(names, (name) =>
201
- spendValue(
202
- Json.read(value, name, path),
203
- [...path, name],
204
- depth + 1,
205
- budget,
206
- ),
207
- )
187
+ /** A value of the input to count, with its place. */
188
+ interface Part {
189
+ readonly value: unknown
190
+ readonly at: Place.Place | undefined
208
191
  }
209
192
 
193
+ /**
194
+ * Counts the objects and items of a value, as the other parts count. It is a
195
+ * walk of the trampoline, so a deep value does not fill the stack.
196
+ */
197
+ const spendValue = (budget: Json.Budget): ((part: Part) => void) =>
198
+ Trampoline.adaptive<Part, void>((R, child) => (part, depth) => {
199
+ const { value, at } = part
200
+ if (typeof value !== "object" || value === null) {
201
+ return R.succeed(undefined)
202
+ }
203
+ // The length of an array counts before its items are read, so a sparse
204
+ // array with a very large length stops at once.
205
+ const list = Array.isArray(value)
206
+ if (list) {
207
+ Json.spend(budget, 1 + value.length, at)
208
+ }
209
+ const names = list
210
+ ? Array.from({ length: value.length }, (_, index) => String(index))
211
+ : Object.keys(value)
212
+ if (!list) {
213
+ Json.spend(budget, 1 + names.length, at)
214
+ }
215
+ return R.map(
216
+ R.forEach(names, (name) =>
217
+ child(
218
+ { value: Json.read(value, name, at), at: Place.at(at, name) },
219
+ depth + 1,
220
+ ),
221
+ ),
222
+ () => undefined,
223
+ )
224
+ })
225
+
210
226
  /**
211
227
  * A rule in the input, with its place, to decode after the tree. The place
212
228
  * shares the path of its parent: see `Place`.
@@ -253,21 +269,18 @@ const decodeMeta = (
253
269
  domain: Domain.Any,
254
270
  budget: Json.Budget,
255
271
  ): Effect.Effect<unknown, Json.Problem> => {
256
- const { input } = rule
257
- const path = Place.pathOf(rule.place)
272
+ const { input, place } = rule
273
+ const at = Place.at(place, "meta")
258
274
  return Effect.flatMap(
259
275
  attempt(() =>
260
276
  typeof input !== "object" || input === null
261
277
  ? undefined
262
- : readOptional(input, "meta", path, () => {
263
- const value = Json.read(input, "meta", path)
278
+ : readOptional(input, "meta", place, () => {
279
+ const value = Json.read(input, "meta", place)
264
280
  if (domain.meta === undefined) {
265
- throw Json.problem(
266
- [...path, "meta"],
267
- "The domain has no Schema for meta",
268
- )
281
+ throw Json.problemAt(at, "The domain has no Schema for meta")
269
282
  }
270
- spendValue(value, [...path, "meta"], 1, budget)
283
+ spendValue(budget)({ value, at })
271
284
  return { value, schema: domain.meta }
272
285
  }),
273
286
  ),
@@ -278,7 +291,7 @@ const decodeMeta = (
278
291
  Schema.decodeUnknownEffect(metaCodec(found.schema))(found.value),
279
292
  (error) =>
280
293
  new Json.Problem({
281
- path: [...path, "meta"],
294
+ path: Place.pathOf(at),
282
295
  message: error.message,
283
296
  issue: error.issue,
284
297
  }),
@@ -292,34 +305,40 @@ const decodeRule = (
292
305
  budget: Json.Budget,
293
306
  domain: Domain.Any,
294
307
  ): Rule.Rule => {
295
- const { input } = rule
296
- const path = Place.pathOf(rule.place)
308
+ const { input, place } = rule
297
309
  if (typeof input !== "object" || input === null) {
298
- throw Json.problem(path, "A rule must be an object")
310
+ throw Json.problemAt(place, "A rule must be an object")
299
311
  }
300
- Json.spend(budget, 1, path)
301
- const tag = Json.read(input, "_tag", path)
312
+ Json.spend(budget, 1, place)
313
+ const tag = Json.read(input, "_tag", place)
302
314
  if (tag !== "Allow" && tag !== "Deny") {
303
- throw Json.problem([...path, "_tag"], 'A rule must be "Allow" or "Deny"')
315
+ throw Json.problemAt(
316
+ Place.at(place, "_tag"),
317
+ 'A rule must be "Allow" or "Deny"',
318
+ )
304
319
  }
305
- const when = readOptional(input, "when", path, () =>
306
- Json.decodeAt(Json.read(input, "when", path), [...path, "when"], budget),
320
+ const when = readOptional(input, "when", place, () =>
321
+ Json.decodeAt(
322
+ Json.read(input, "when", place),
323
+ Place.at(place, "when"),
324
+ budget,
325
+ ),
307
326
  )
308
- const fields = readOptional(input, "fields", path, () =>
309
- readStrings(input, "fields", path, budget),
327
+ const fields = readOptional(input, "fields", place, () =>
328
+ readStrings(input, "fields", place, budget),
310
329
  )
311
- const reason = readOptional(input, "reason", path, () =>
312
- Json.readString(input, "reason", path),
330
+ const reason = readOptional(input, "reason", place, () =>
331
+ Json.readString(input, "reason", place),
313
332
  )
314
- const id = readOptional(input, "id", path, () =>
315
- Json.readString(input, "id", path),
333
+ const id = readOptional(input, "id", place, () =>
334
+ Json.readString(input, "id", place),
316
335
  )
317
336
  // Each rule gets the domain of the Schema, as a rule from the builder of
318
337
  // the domain has it.
319
338
  return Rule.make({
320
339
  _tag: tag,
321
- actions: readStrings(input, "actions", path, budget),
322
- resources: readStrings(input, "resources", path, budget),
340
+ actions: readStrings(input, "actions", place, budget),
341
+ resources: readStrings(input, "resources", place, budget),
323
342
  ...(when === undefined ? {} : { when: when as never }),
324
343
  ...(fields === undefined ? {} : { fields }),
325
344
  ...(reason === undefined ? {} : { reason }),
@@ -329,136 +348,186 @@ const decodeRule = (
329
348
  })
330
349
  }
331
350
 
351
+ /** A node of the input of a tree, with its place. */
352
+ interface TreePart {
353
+ readonly input: unknown
354
+ readonly at: Place.Place | undefined
355
+ }
356
+
332
357
  /**
333
358
  * Reads the shape of a tree, and puts each rule in `rules`. The rules are
334
359
  * decoded after the walk: a condition has levels of its own, and a decode
335
- * inside the walk would go down the levels of both. `path` is the path of the
336
- * node for a problem here, and `place` keeps it for later.
360
+ * inside the walk would go down the levels of both. The walk is a walk of
361
+ * the trampoline, so a deep tree does not fill the stack.
337
362
  */
338
363
  const readTree = (
339
- input: unknown,
340
- path: ReadonlyArray<PropertyKey>,
341
- place: Place.Place | undefined,
342
- depth: number,
343
364
  budget: Json.Budget,
344
365
  // The rules of one decode. The list belongs to that decode.
345
366
  rules: Array<RuleInput>,
346
- ): Plan => {
347
- if (depth > Limits.policyDepth) {
348
- throw Json.problem(
349
- path,
350
- `A policy tree has more than ${String(Limits.policyDepth)} levels. The most is ${String(Limits.policyDepth)}.`,
351
- )
352
- }
353
- if (typeof input !== "object" || input === null) {
354
- throw Json.problem(path, "A node of a policy must be an object")
355
- }
356
- Json.spend(budget, 1, path)
357
- const child = (value: unknown, ...parts: ReadonlyArray<PropertyKey>) =>
358
- readTree(
359
- value,
360
- [...path, ...parts],
361
- Place.at(place, ...parts),
362
- depth + 1,
363
- budget,
364
- rules,
365
- )
366
- const tag = Json.read(input, "_tag", path)
367
- switch (tag) {
368
- case "Rules": {
369
- const algorithm = Json.readString(input, "algorithm", path)
370
- if (!PolicyNodes.isAlgorithm(algorithm)) {
371
- throw Json.problem(
372
- [...path, "algorithm"],
373
- `"${algorithm}" is not a combining algorithm`,
374
- )
375
- }
376
- const found = Arr.map(
377
- Json.readList(input, "rules", path, budget),
378
- (rule, index) =>
379
- rules.push({
380
- input: rule,
381
- place: Place.at(place, "rules", index),
382
- }) - 1,
367
+ ): ((part: TreePart) => Plan) => {
368
+ // The nodes and rules of the tree that the walk has read. A tree with more
369
+ // of them than `policyNodes` does not load, so the walk stops at the first
370
+ // one past it, and it does not read the rest of a very large input. The
371
+ // count belongs to one decode.
372
+ const count = { nodes: 0 }
373
+ const counted = (more: number, at: Place.Place | undefined): void => {
374
+ count.nodes += more
375
+ const most = budget.limits.policyNodes
376
+ if (count.nodes > most) {
377
+ throw Json.problemAt(
378
+ at,
379
+ `A policy tree has more than ${String(most)} nodes. The most is ${String(most)}.`,
383
380
  )
384
- return sized(path, {
385
- _tag: "Rules",
386
- place,
387
- size: 1 + found.length,
388
- algorithm,
389
- rules: found,
390
- })
391
381
  }
392
- case "All":
393
- case "Any":
394
- case "FirstOf": {
395
- const policies = Arr.map(
396
- Json.readList(input, "policies", path, budget),
397
- (value, index) => child(value, "policies", index),
398
- )
399
- return sized(path, {
400
- _tag: tag,
401
- place,
402
- size: Arr.reduce(policies, 1, (total, plan) => total + plan.size),
403
- policies,
404
- })
382
+ }
383
+ return Trampoline.adaptive<TreePart, Plan>((R, child) => (part, depth) => {
384
+ const { input, at } = part
385
+ if (typeof input !== "object" || input === null) {
386
+ throw Json.problemAt(at, "A node of a policy must be an object")
405
387
  }
406
- case "Ceiling": {
407
- const cap = child(Json.read(input, "cap", path), "cap")
408
- const policy = child(Json.read(input, "policy", path), "policy")
409
- return sized(path, {
410
- _tag: "Ceiling",
411
- place,
412
- size: 1 + cap.size + policy.size,
413
- cap,
414
- policy,
415
- })
388
+ Json.spend(budget, 1, at)
389
+ counted(1, at)
390
+ const below = (value: unknown, ...parts: ReadonlyArray<PropertyKey>) =>
391
+ child({ input: value, at: Place.at(at, ...parts) }, depth + 1)
392
+ const tag = Json.read(input, "_tag", at)
393
+ switch (tag) {
394
+ case "Rules": {
395
+ const algorithm = Json.readString(input, "algorithm", at)
396
+ if (!PolicyNodes.isAlgorithm(algorithm)) {
397
+ throw Json.problemAt(
398
+ Place.at(at, "algorithm"),
399
+ `"${algorithm}" is not a combining algorithm`,
400
+ )
401
+ }
402
+ const list = Json.readList(input, "rules", at, budget)
403
+ counted(list.length, at)
404
+ const found = Arr.map(
405
+ list,
406
+ (rule, index) =>
407
+ rules.push({
408
+ input: rule,
409
+ place: Place.at(at, "rules", index),
410
+ }) - 1,
411
+ )
412
+ return R.succeed(
413
+ sized(budget, {
414
+ _tag: "Rules",
415
+ place: at,
416
+ size: 1 + found.length,
417
+ algorithm,
418
+ rules: found,
419
+ }),
420
+ )
421
+ }
422
+ case "All":
423
+ case "Any":
424
+ case "FirstOf":
425
+ return R.map(
426
+ R.forEach(
427
+ Json.readList(input, "policies", at, budget),
428
+ (value, index) => below(value, "policies", index),
429
+ ),
430
+ (policies) =>
431
+ sized(budget, {
432
+ _tag: tag,
433
+ place: at,
434
+ size: Arr.reduce(policies, 1, (total, plan) => total + plan.size),
435
+ policies,
436
+ }),
437
+ )
438
+ case "Ceiling": {
439
+ const cap = Json.read(input, "cap", at)
440
+ const policy = Json.read(input, "policy", at)
441
+ return R.flatMap(below(cap, "cap"), (capPlan) =>
442
+ R.map(below(policy, "policy"), (policyPlan) =>
443
+ sized(budget, {
444
+ _tag: "Ceiling",
445
+ place: at,
446
+ size: 1 + capPlan.size + policyPlan.size,
447
+ cap: capPlan,
448
+ policy: policyPlan,
449
+ }),
450
+ ),
451
+ )
452
+ }
453
+ default:
454
+ throw Json.problemAt(Place.at(at, "_tag"), "The node has no known tag")
416
455
  }
417
- default:
418
- throw Json.problem([...path, "_tag"], "The node has no known tag")
419
- }
456
+ })
420
457
  }
421
458
 
422
459
  /** Stops a plan past `policyNodes`, with the message of the nodes. */
423
- const sized = (path: ReadonlyArray<PropertyKey>, plan: Plan): Plan => {
424
- if (plan.size > Limits.policyNodes) {
425
- throw Json.problem(
426
- path,
427
- `A policy tree has ${String(plan.size)} nodes. The most is ${String(Limits.policyNodes)}.`,
460
+ const sized = (budget: Json.Budget, plan: Plan): Plan => {
461
+ const most = budget.limits.policyNodes
462
+ if (plan.size > most) {
463
+ throw Json.problemAt(
464
+ plan.place,
465
+ `A policy tree has ${String(plan.size)} nodes. The most is ${String(most)}.`,
428
466
  )
429
467
  }
430
468
  return plan
431
469
  }
432
470
 
433
- /** Makes the nodes of a plan, with the decoded rules. */
434
- const build = (plan: Plan, rules: ReadonlyArray<Rule.Rule>): Node => {
435
- try {
471
+ /**
472
+ * Makes the nodes of a plan, with the decoded rules. It is a walk of the
473
+ * trampoline, so a deep plan does not fill the stack.
474
+ */
475
+ const build = (
476
+ budget: Json.Budget,
477
+ rules: ReadonlyArray<Rule.Rule>,
478
+ ): ((plan: Plan) => Node) =>
479
+ Trampoline.adaptive<Plan, Node>((R, child) => (plan, depth) => {
480
+ // A node past a limit is a problem at its place. The constructor checks
481
+ // the ceilings, and the decoder checks the limits of the domain, with
482
+ // the nodes and values of the conditions.
483
+ const make = (made: () => Node): Node => {
484
+ const node = (() => {
485
+ try {
486
+ return made()
487
+ } catch (error) {
488
+ if (error instanceof Limits.LimitExceeded) {
489
+ throw Json.problemAt(plan.place, error.message)
490
+ }
491
+ throw error
492
+ }
493
+ })()
494
+ const most = budget.limits.policyNodes
495
+ if (node.size > most) {
496
+ throw Json.problemAt(
497
+ plan.place,
498
+ `A policy tree has ${String(node.size)} nodes. The most is ${String(most)}.`,
499
+ )
500
+ }
501
+ return node
502
+ }
436
503
  switch (plan._tag) {
437
504
  case "Rules":
438
- return new PolicyNodes.Rules({
439
- algorithm: plan.algorithm,
440
- rules: Arr.map(plan.rules, (index) => rules[index]!),
441
- })
505
+ return R.succeed(
506
+ make(
507
+ () =>
508
+ new PolicyNodes.Rules({
509
+ algorithm: plan.algorithm,
510
+ rules: Arr.map(plan.rules, (index) => rules[index]!),
511
+ }),
512
+ ),
513
+ )
442
514
  case "All":
443
515
  case "Any":
444
- case "FirstOf":
445
- return new PolicyNodes[plan._tag](
446
- Arr.map(plan.policies, (child) => build(child, rules)),
516
+ case "FirstOf": {
517
+ const tag = plan._tag
518
+ return R.map(
519
+ R.forEach(plan.policies, (inner) => child(inner, depth + 1)),
520
+ (policies) => make(() => new PolicyNodes[tag](policies)),
447
521
  )
522
+ }
448
523
  case "Ceiling":
449
- return new PolicyNodes.Ceiling({
450
- cap: build(plan.cap, rules),
451
- policy: build(plan.policy, rules),
452
- })
453
- }
454
- } catch (error) {
455
- // A node past a limit is a problem at its place.
456
- if (error instanceof Limits.LimitExceeded) {
457
- throw Json.problem(Place.pathOf(plan.place), error.message)
524
+ return R.flatMap(child(plan.cap, depth + 1), (cap) =>
525
+ R.map(child(plan.policy, depth + 1), (policy) =>
526
+ make(() => new PolicyNodes.Ceiling({ cap, policy })),
527
+ ),
528
+ )
458
529
  }
459
- throw error
460
- }
461
- }
530
+ })
462
531
 
463
532
  /** Runs a step that may throw a problem, and puts the problem in the error. */
464
533
  const attempt = <A>(step: () => A): Effect.Effect<A, Json.Problem> =>
@@ -482,11 +551,11 @@ export const decode = (
482
551
  domain: Domain.Any,
483
552
  ): Effect.Effect<Node, Json.Problem> =>
484
553
  Effect.gen(function* () {
485
- const budget: Json.Budget = { used: 0 }
554
+ const budget: Json.Budget = { used: 0, limits: domain.limits }
486
555
  // The rules of this decode. The list belongs to this decode.
487
556
  const inputs: Array<RuleInput> = []
488
557
  const plan = yield* attempt(() =>
489
- readTree(input, [], undefined, 1, budget, inputs),
558
+ readTree(budget, inputs)({ input, at: undefined }),
490
559
  )
491
560
  const metas = yield* Effect.forEach(inputs, (rule) =>
492
561
  decodeMeta(rule, domain, budget),
@@ -496,5 +565,5 @@ export const decode = (
496
565
  decodeRule(rule, metas[index], budget, domain),
497
566
  ),
498
567
  )
499
- return yield* attempt(() => build(plan, rules))
568
+ return yield* attempt(() => build(budget, rules)(plan))
500
569
  })
@@ -15,6 +15,7 @@ import * as Schema from "effect/Schema"
15
15
  import * as Rule from "./rule.ts"
16
16
  import * as Limits from "./limits.ts"
17
17
  import * as Nodes from "./nodes.ts"
18
+ import * as Trampoline from "./trampoline.ts"
18
19
 
19
20
  /** The combining algorithms of a list of rules. The decoder reads this list too. */
20
21
  export const Algorithm = Schema.Literals([
@@ -41,22 +42,15 @@ interface Facts {
41
42
 
42
43
  /** Checks the facts of a node against the limits. */
43
44
  const checked = (facts: Facts): Facts => {
44
- if (facts.size > Limits.policyNodes) {
45
+ // A node does not know its domain, so it checks the ceiling. A policy
46
+ // checks the limit of its domain.
47
+ if (facts.size > Limits.ceilings.policyNodes) {
45
48
  throw Limits.exceeded(
46
49
  "A policy tree",
47
50
  "policyNodes",
48
51
  "nodes",
49
52
  facts.size,
50
- Limits.policyNodes,
51
- )
52
- }
53
- if (facts.depth > Limits.policyDepth) {
54
- throw Limits.exceeded(
55
- "A policy tree",
56
- "policyDepth",
57
- "levels",
58
- facts.depth,
59
- Limits.policyDepth,
53
+ Limits.ceilings.policyNodes,
60
54
  )
61
55
  }
62
56
  return facts
@@ -202,15 +196,29 @@ export const isNode = (u: unknown): u is Node => u instanceof Base
202
196
  * into the condition of each rule takes the rules from this list first, so it
203
197
  * does not go down the levels of the tree and of a condition at once.
204
198
  */
205
- export const rulesOf = (node: Node): ReadonlyArray<Rule.Rule> => {
206
- switch (node._tag) {
207
- case "Rules":
208
- return node.rules
209
- case "All":
210
- case "Any":
211
- case "FirstOf":
212
- return Arr.flatMap(node.policies, rulesOf)
213
- case "Ceiling":
214
- return [...rulesOf(node.cap), ...rulesOf(node.policy)]
215
- }
216
- }
199
+ export const rulesOf = (root: Node): ReadonlyArray<Rule.Rule> =>
200
+ // The walk takes the steps of a trampoline, so a deep tree does not fill
201
+ // the stack.
202
+ Trampoline.run(root.depth, (R) => {
203
+ const go = (node: Node): Trampoline.Step<ReadonlyArray<Rule.Rule>> => {
204
+ switch (node._tag) {
205
+ case "Rules":
206
+ return R.succeed(node.rules)
207
+ case "All":
208
+ case "Any":
209
+ case "FirstOf":
210
+ return R.map(
211
+ R.forEach(node.policies, (child) => R.suspend(() => go(child))),
212
+ Arr.flatten,
213
+ )
214
+ case "Ceiling":
215
+ return R.map(
216
+ R.forEach([node.cap, node.policy], (child) =>
217
+ R.suspend(() => go(child)),
218
+ ),
219
+ Arr.flatten,
220
+ )
221
+ }
222
+ }
223
+ return go(root)
224
+ })