@nmnmcc/ability 2.0.2 → 2.0.4

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 (44) hide show
  1. package/README.md +20 -14
  2. package/dist/Condition.d.ts +17 -7
  3. package/dist/Condition.d.ts.map +1 -1
  4. package/dist/Condition.js +2 -1
  5. package/dist/Condition.js.map +1 -1
  6. package/dist/Limits.d.ts +3 -1
  7. package/dist/Limits.d.ts.map +1 -1
  8. package/dist/Limits.js +3 -1
  9. package/dist/Limits.js.map +1 -1
  10. package/dist/Policy.d.ts +2 -6
  11. package/dist/Policy.d.ts.map +1 -1
  12. package/dist/Policy.js.map +1 -1
  13. package/dist/internal/algebra.js +23 -13
  14. package/dist/internal/algebra.js.map +1 -1
  15. package/dist/internal/analysis.js +4 -2
  16. package/dist/internal/analysis.js.map +1 -1
  17. package/dist/internal/bind.js +28 -19
  18. package/dist/internal/bind.js.map +1 -1
  19. package/dist/internal/catalog.d.ts +9 -0
  20. package/dist/internal/catalog.d.ts.map +1 -1
  21. package/dist/internal/catalog.js +37 -22
  22. package/dist/internal/catalog.js.map +1 -1
  23. package/dist/internal/decide.js +2 -1
  24. package/dist/internal/decide.js.map +1 -1
  25. package/dist/internal/place.js +19 -0
  26. package/dist/internal/place.js.map +1 -0
  27. package/dist/internal/policy-json.js +39 -39
  28. package/dist/internal/policy-json.js.map +1 -1
  29. package/dist/internal/rule.d.ts +20 -22
  30. package/dist/internal/rule.d.ts.map +1 -1
  31. package/dist/internal/rule.js +32 -4
  32. package/dist/internal/rule.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/Condition.ts +46 -25
  35. package/src/Limits.ts +3 -1
  36. package/src/Policy.ts +1 -7
  37. package/src/internal/algebra.ts +30 -13
  38. package/src/internal/analysis.ts +5 -0
  39. package/src/internal/bind.ts +66 -27
  40. package/src/internal/catalog.ts +78 -30
  41. package/src/internal/decide.ts +3 -0
  42. package/src/internal/place.ts +32 -0
  43. package/src/internal/policy-json.ts +49 -29
  44. package/src/internal/rule.ts +55 -11
@@ -12,6 +12,7 @@ import type * as Domain from "../Domain.ts"
12
12
  import * as Rule from "./rule.ts"
13
13
  import * as Json from "./json.ts"
14
14
  import * as Limits from "./limits.ts"
15
+ import * as Place from "./place.ts"
15
16
  import * as PolicyNodes from "./policy-nodes.ts"
16
17
 
17
18
  type Node = PolicyNodes.Node
@@ -213,10 +214,13 @@ const spendValue = (
213
214
  )
214
215
  }
215
216
 
216
- /** A rule in the input, with its place, to decode after the tree. */
217
+ /**
218
+ * A rule in the input, with its place, to decode after the tree. The place
219
+ * shares the path of its parent: see `Place`.
220
+ */
217
221
  interface RuleInput {
218
222
  readonly input: unknown
219
- readonly path: ReadonlyArray<PropertyKey>
223
+ readonly place: Place.Place
220
224
  }
221
225
 
222
226
  /**
@@ -227,20 +231,20 @@ interface RuleInput {
227
231
  type Plan =
228
232
  | {
229
233
  readonly _tag: "Rules"
230
- readonly path: ReadonlyArray<PropertyKey>
234
+ readonly place: Place.Place | undefined
231
235
  readonly size: number
232
236
  readonly algorithm: PolicyNodes.Algorithm
233
237
  readonly rules: ReadonlyArray<number>
234
238
  }
235
239
  | {
236
240
  readonly _tag: "All" | "Any" | "FirstOf"
237
- readonly path: ReadonlyArray<PropertyKey>
241
+ readonly place: Place.Place | undefined
238
242
  readonly size: number
239
243
  readonly policies: ReadonlyArray<Plan>
240
244
  }
241
245
  | {
242
246
  readonly _tag: "Ceiling"
243
- readonly path: ReadonlyArray<PropertyKey>
247
+ readonly place: Place.Place | undefined
244
248
  readonly size: number
245
249
  readonly cap: Plan
246
250
  readonly policy: Plan
@@ -252,11 +256,13 @@ type Plan =
252
256
  * the value, so its size counts in the budget.
253
257
  */
254
258
  const decodeMeta = (
255
- { input, path }: RuleInput,
259
+ rule: RuleInput,
256
260
  domain: Domain.Any,
257
261
  budget: Json.Budget,
258
- ): Effect.Effect<unknown, Json.Problem> =>
259
- Effect.flatMap(
262
+ ): Effect.Effect<unknown, Json.Problem> => {
263
+ const { input } = rule
264
+ const path = Place.pathOf(rule.place)
265
+ return Effect.flatMap(
260
266
  attempt(() =>
261
267
  typeof input !== "object" || input === null
262
268
  ? undefined
@@ -285,13 +291,16 @@ const decodeMeta = (
285
291
  }),
286
292
  ),
287
293
  )
294
+ }
288
295
 
289
296
  const decodeRule = (
290
- { input, path }: RuleInput,
297
+ rule: RuleInput,
291
298
  meta: unknown,
292
299
  budget: Json.Budget,
293
300
  domain: Domain.Any,
294
301
  ): Rule.Rule => {
302
+ const { input } = rule
303
+ const path = Place.pathOf(rule.place)
295
304
  if (typeof input !== "object" || input === null) {
296
305
  throw Json.problem(path, "A rule must be an object")
297
306
  }
@@ -330,11 +339,13 @@ const decodeRule = (
330
339
  /**
331
340
  * Reads the shape of a tree, and puts each rule in `rules`. The rules are
332
341
  * decoded after the walk: a condition has levels of its own, and a decode
333
- * inside the walk would go down the levels of both.
342
+ * inside the walk would go down the levels of both. `path` is the path of the
343
+ * node for a problem here, and `place` keeps it for later.
334
344
  */
335
345
  const readTree = (
336
346
  input: unknown,
337
347
  path: ReadonlyArray<PropertyKey>,
348
+ place: Place.Place | undefined,
338
349
  depth: number,
339
350
  budget: Json.Budget,
340
351
  // The rules of one decode. The list belongs to that decode.
@@ -350,8 +361,15 @@ const readTree = (
350
361
  throw Json.problem(path, "A node of a policy must be an object")
351
362
  }
352
363
  Json.spend(budget, 1, path)
353
- const child = (value: unknown, at: ReadonlyArray<PropertyKey>) =>
354
- readTree(value, at, depth + 1, budget, rules)
364
+ const child = (value: unknown, ...parts: ReadonlyArray<PropertyKey>) =>
365
+ readTree(
366
+ value,
367
+ [...path, ...parts],
368
+ Place.at(place, ...parts),
369
+ depth + 1,
370
+ budget,
371
+ rules,
372
+ )
355
373
  const tag = Json.read(input, "_tag", path)
356
374
  switch (tag) {
357
375
  case "Rules": {
@@ -365,11 +383,14 @@ const readTree = (
365
383
  const found = Arr.map(
366
384
  Json.readList(input, "rules", path, budget),
367
385
  (rule, index) =>
368
- rules.push({ input: rule, path: [...path, "rules", index] }) - 1,
386
+ rules.push({
387
+ input: rule,
388
+ place: Place.at(place, "rules", index),
389
+ }) - 1,
369
390
  )
370
- return sized({
391
+ return sized(path, {
371
392
  _tag: "Rules",
372
- path,
393
+ place,
373
394
  size: 1 + found.length,
374
395
  algorithm: algorithm as PolicyNodes.Algorithm,
375
396
  rules: found,
@@ -380,24 +401,21 @@ const readTree = (
380
401
  case "FirstOf": {
381
402
  const policies = Arr.map(
382
403
  Json.readList(input, "policies", path, budget),
383
- (value, index) => child(value, [...path, "policies", index]),
404
+ (value, index) => child(value, "policies", index),
384
405
  )
385
- return sized({
406
+ return sized(path, {
386
407
  _tag: tag,
387
- path,
408
+ place,
388
409
  size: Arr.reduce(policies, 1, (total, plan) => total + plan.size),
389
410
  policies,
390
411
  })
391
412
  }
392
413
  case "Ceiling": {
393
- const cap = child(Json.read(input, "cap", path), [...path, "cap"])
394
- const policy = child(Json.read(input, "policy", path), [
395
- ...path,
396
- "policy",
397
- ])
398
- return sized({
414
+ const cap = child(Json.read(input, "cap", path), "cap")
415
+ const policy = child(Json.read(input, "policy", path), "policy")
416
+ return sized(path, {
399
417
  _tag: "Ceiling",
400
- path,
418
+ place,
401
419
  size: 1 + cap.size + policy.size,
402
420
  cap,
403
421
  policy,
@@ -409,10 +427,10 @@ const readTree = (
409
427
  }
410
428
 
411
429
  /** Stops a plan past `policyNodes`, with the message of the nodes. */
412
- const sized = (plan: Plan): Plan => {
430
+ const sized = (path: ReadonlyArray<PropertyKey>, plan: Plan): Plan => {
413
431
  if (plan.size > Limits.policyNodes) {
414
432
  throw Json.problem(
415
- plan.path,
433
+ path,
416
434
  `A policy tree has ${String(plan.size)} nodes. The most is ${String(Limits.policyNodes)}.`,
417
435
  )
418
436
  }
@@ -443,7 +461,7 @@ const build = (plan: Plan, rules: ReadonlyArray<Rule.Rule>): Node => {
443
461
  } catch (error) {
444
462
  // A node past a limit is a problem at its place.
445
463
  if (error instanceof Limits.LimitExceeded) {
446
- throw Json.problem(plan.path, error.message)
464
+ throw Json.problem(Place.pathOf(plan.place), error.message)
447
465
  }
448
466
  throw error
449
467
  }
@@ -474,7 +492,9 @@ export const decode = (
474
492
  const budget: Json.Budget = { used: 0 }
475
493
  // The rules of this decode. The list belongs to this decode.
476
494
  const inputs: Array<RuleInput> = []
477
- const plan = yield* attempt(() => readTree(input, [], 1, budget, inputs))
495
+ const plan = yield* attempt(() =>
496
+ readTree(input, [], undefined, 1, budget, inputs),
497
+ )
478
498
  const metas = yield* Effect.forEach(inputs, (rule) =>
479
499
  decodeMeta(rule, domain, budget),
480
500
  )
@@ -7,8 +7,11 @@
7
7
  */
8
8
  import * as Arr from "effect/Array"
9
9
  import * as Data from "effect/Data"
10
+ import * as MutableHashMap from "effect/MutableHashMap"
11
+ import * as Option from "effect/Option"
10
12
  import * as Condition from "../Condition.ts"
11
13
  import type * as Domain from "../Domain.ts"
14
+ import type * as Policy from "../Policy.ts"
12
15
  import type * as Resource from "../Resource.ts"
13
16
  import * as Catalog from "./catalog.ts"
14
17
  import * as Nodes from "./nodes.ts"
@@ -33,17 +36,42 @@ export interface Shape {
33
36
  readonly domain?: Domain.Any
34
37
  }
35
38
 
36
- /** The class of every rule value. */
37
- export class Rule extends Data.Class<Shape> {
38
- *[Symbol.iterator](): Generator<this, this, unknown> {
39
+ /**
40
+ * The class of every rule value. The types are those of `Policy.Rule`: `D` is
41
+ * the domain, `I` is what the tests of the condition read, and `X` is the
42
+ * mark of the condition. With no types, as in the nodes of a policy, a rule
43
+ * fits no domain, its condition fits no other rule, and it may have a
44
+ * predicate.
45
+ */
46
+ export class Rule<D = never, I = never, X = unknown> extends Data.Class<
47
+ Policy.RuleShape<I, X>
48
+ > {
49
+ /** Only in the types. */
50
+ declare readonly "~Opaque": () => X
51
+ /** Only in the types. */
52
+ declare readonly "~Domain": (domain: D) => D
53
+ // A private field makes the type of a rule its own, as for a policy: a
54
+ // spread of a rule, or an object made by hand, is not a rule. `Equal` does
55
+ // not read it. It comes last, so its `;` stays: the generator method below
56
+ // needs one after a field. Node.js takes the `declare` fields out of the
57
+ // code, and a `;` after one of them would go with it.
58
+ readonly #rule = true;
59
+
60
+ *[Symbol.iterator](): Generator<Rule<D, I, X>, Rule<D, I, X>, unknown> {
39
61
  yield this
40
62
  return this
41
63
  }
64
+
65
+ /** Tests if a value is a rule, by its private field. */
66
+ static is(u: unknown): u is Rule {
67
+ return typeof u === "object" && u !== null && #rule in u
68
+ }
42
69
  }
43
70
 
44
71
  /** Makes a rule from its data. It does not check the data against a domain. */
45
72
  export const make = (data: Shape): Rule =>
46
73
  // A missing option is left out, so two rules with the same data are equal.
74
+ // The data has no types here, as the code below works with no types.
47
75
  new Rule({
48
76
  _tag: data._tag,
49
77
  actions: [...data.actions],
@@ -54,7 +82,7 @@ export const make = (data: Shape): Rule =>
54
82
  ...(data.id === undefined ? {} : { id: data.id }),
55
83
  ...(data.meta === undefined ? {} : { meta: data.meta }),
56
84
  ...(data.domain === undefined ? {} : { domain: data.domain }),
57
- })
85
+ } as never)
58
86
 
59
87
  /**
60
88
  * Makes a rule from its data, as `Policy.rule` does. A condition with a
@@ -71,7 +99,7 @@ export const makePlain = (data: Shape): Rule => {
71
99
  }
72
100
 
73
101
  /** Tests if a value is a rule. */
74
- export const isRule = (u: unknown): u is Rule => u instanceof Rule
102
+ export const isRule = (u: unknown): u is Rule => Rule.is(u)
75
103
 
76
104
  const toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>
77
105
  Array.isArray(value) ? value : [value as A]
@@ -132,6 +160,25 @@ export const matchesField = (pattern: string, field: string): boolean =>
132
160
  export const matchesInside = (pattern: string, field: string): boolean =>
133
161
  Patterns.matchesBelow(Patterns.parseBound(pattern), field.split("."))
134
162
 
163
+ /**
164
+ * Makes the test "a field pattern matches some field of the catalog" for one
165
+ * walk, such as one decision. A policy may name one pattern very many times,
166
+ * so the test keeps each answer by the text of the pattern, and the memo goes
167
+ * away when the walk ends. The binding puts a limit on the work of the
168
+ * patterns of a policy on each kind, so one walk does at most that work.
169
+ */
170
+ export const fitsOf = (
171
+ catalog: Catalog.Catalog,
172
+ ): ((pattern: string) => boolean) => {
173
+ const memo = MutableHashMap.empty<string, boolean>()
174
+ return (pattern) =>
175
+ Option.getOrElse(MutableHashMap.get(memo, pattern), () => {
176
+ const found = Patterns.fitsCatalog(Patterns.parseBound(pattern), catalog)
177
+ MutableHashMap.set(memo, pattern, found)
178
+ return found
179
+ })
180
+ }
181
+
135
182
  /**
136
183
  * Tests the parts of a rule that do not need the resource value: the action,
137
184
  * the kind and the field.
@@ -152,6 +199,8 @@ export const matchesTarget = (
152
199
  readonly kind: string
153
200
  readonly field?: string | undefined
154
201
  readonly descriptor?: Resource.Descriptor | undefined
202
+ /** The test of {@link fitsOf} for the catalog of the descriptor. */
203
+ readonly fits?: ((pattern: string) => boolean) | undefined
155
204
  },
156
205
  ): boolean => {
157
206
  if (!rule.actions.includes("*") && !rule.actions.includes(target.action)) {
@@ -173,12 +222,7 @@ export const matchesTarget = (
173
222
  return (
174
223
  rule._tag === "Allow" &&
175
224
  (descriptor === undefined ||
176
- Arr.some(rule.fields, (pattern) =>
177
- Patterns.fitsCatalog(
178
- Patterns.parseBound(pattern),
179
- descriptor.catalog,
180
- ),
181
- ))
225
+ Arr.some(rule.fields, target.fits ?? fitsOf(descriptor.catalog)))
182
226
  )
183
227
  }
184
228
  const field = target.field