@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.
- package/README.md +20 -14
- package/dist/Condition.d.ts +17 -7
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +2 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Limits.d.ts +3 -1
- package/dist/Limits.d.ts.map +1 -1
- package/dist/Limits.js +3 -1
- package/dist/Limits.js.map +1 -1
- package/dist/Policy.d.ts +2 -6
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js.map +1 -1
- package/dist/internal/algebra.js +23 -13
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/analysis.js +4 -2
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/bind.js +28 -19
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +9 -0
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +37 -22
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +2 -1
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/place.js +19 -0
- package/dist/internal/place.js.map +1 -0
- package/dist/internal/policy-json.js +39 -39
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/rule.d.ts +20 -22
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +32 -4
- package/dist/internal/rule.js.map +1 -1
- package/package.json +1 -1
- package/src/Condition.ts +46 -25
- package/src/Limits.ts +3 -1
- package/src/Policy.ts +1 -7
- package/src/internal/algebra.ts +30 -13
- package/src/internal/analysis.ts +5 -0
- package/src/internal/bind.ts +66 -27
- package/src/internal/catalog.ts +78 -30
- package/src/internal/decide.ts +3 -0
- package/src/internal/place.ts +32 -0
- package/src/internal/policy-json.ts +49 -29
- 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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
259
|
+
rule: RuleInput,
|
|
256
260
|
domain: Domain.Any,
|
|
257
261
|
budget: Json.Budget,
|
|
258
|
-
): Effect.Effect<unknown, Json.Problem> =>
|
|
259
|
-
|
|
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
|
-
|
|
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,
|
|
354
|
-
readTree(
|
|
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({
|
|
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
|
-
|
|
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,
|
|
404
|
+
(value, index) => child(value, "policies", index),
|
|
384
405
|
)
|
|
385
|
-
return sized({
|
|
406
|
+
return sized(path, {
|
|
386
407
|
_tag: tag,
|
|
387
|
-
|
|
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),
|
|
394
|
-
const policy = child(Json.read(input, "policy", path),
|
|
395
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(() =>
|
|
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
|
)
|
package/src/internal/rule.ts
CHANGED
|
@@ -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
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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, (
|
|
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
|