@nmnmcc/ability 2.0.1 → 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 +25 -17
- package/dist/Condition.d.ts +26 -16
- 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 +6 -8
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +4 -2
- 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/authorizer.js +13 -7
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +53 -27
- 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 +64 -50
- package/src/Limits.ts +3 -1
- package/src/Policy.ts +5 -9
- package/src/internal/algebra.ts +30 -13
- package/src/internal/analysis.ts +5 -0
- package/src/internal/authorizer.ts +17 -7
- package/src/internal/bind.ts +85 -23
- 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
package/src/internal/bind.ts
CHANGED
|
@@ -16,6 +16,7 @@ import * as Catalog from "./catalog.ts"
|
|
|
16
16
|
import * as Limits from "./limits.ts"
|
|
17
17
|
import * as Nodes from "./nodes.ts"
|
|
18
18
|
import * as Patterns from "./patterns.ts"
|
|
19
|
+
import * as Place from "./place.ts"
|
|
19
20
|
import { withSuggestion } from "./suggest.ts"
|
|
20
21
|
|
|
21
22
|
/** One problem of a rule or a policy, with its path. */
|
|
@@ -44,12 +45,15 @@ interface Memo {
|
|
|
44
45
|
readonly fits: MutableHashMap.MutableHashMap<string, boolean>
|
|
45
46
|
/** The count of the issues of conditions so far. It belongs to one binding. */
|
|
46
47
|
readonly issues: { count: number; noted: boolean }
|
|
48
|
+
/** The steps of the checks of patterns so far. It belongs to one binding. */
|
|
49
|
+
readonly work: { steps: number; noted: boolean }
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
const makeMemo = (): Memo => ({
|
|
50
53
|
suggestions: MutableHashMap.empty(),
|
|
51
54
|
fits: MutableHashMap.empty(),
|
|
52
55
|
issues: { count: 0, noted: false },
|
|
56
|
+
work: { steps: 0, noted: false },
|
|
53
57
|
})
|
|
54
58
|
|
|
55
59
|
/**
|
|
@@ -173,6 +177,36 @@ const fieldIssue = (
|
|
|
173
177
|
]
|
|
174
178
|
}
|
|
175
179
|
|
|
180
|
+
/**
|
|
181
|
+
* The issues of a field that a condition reads: a field that is not a field
|
|
182
|
+
* of every resource of the rule, or a field that is not plain data in one of
|
|
183
|
+
* them. A read of a value that is not plain data, as a getter of a class, is
|
|
184
|
+
* a defect in a decision. A residual may leave the read out, so the residual
|
|
185
|
+
* would say "yes" where the decision fails. So the binding does not take
|
|
186
|
+
* such a read: see `Catalog.Field.plain`.
|
|
187
|
+
*/
|
|
188
|
+
const readIssues = (
|
|
189
|
+
context: Context,
|
|
190
|
+
field: string,
|
|
191
|
+
at: ReadonlyArray<PropertyKey>,
|
|
192
|
+
): ReadonlyArray<Issue> => {
|
|
193
|
+
const exists = fieldIssue(context, field, at)
|
|
194
|
+
if (exists.length > 0) {
|
|
195
|
+
return exists
|
|
196
|
+
}
|
|
197
|
+
return Arr.some(
|
|
198
|
+
context.named,
|
|
199
|
+
(descriptor) => !(Catalog.lookup(descriptor.catalog, field)?.plain ?? true),
|
|
200
|
+
)
|
|
201
|
+
? [
|
|
202
|
+
{
|
|
203
|
+
path: at,
|
|
204
|
+
issue: `A condition may not read the field "${field}". A value above it may be an object that does not have it as a data field, as a class with a getter, a date or a list. Use a predicate for it.`,
|
|
205
|
+
},
|
|
206
|
+
]
|
|
207
|
+
: []
|
|
208
|
+
}
|
|
209
|
+
|
|
176
210
|
/**
|
|
177
211
|
* The issues of a field pattern: a pattern that does not parse, or that
|
|
178
212
|
* matches no field of the resources of the rule, as a pattern with a mistake
|
|
@@ -186,24 +220,49 @@ const patternIssues = (
|
|
|
186
220
|
): ReadonlyArray<Issue> =>
|
|
187
221
|
Result.match(Patterns.parse(pattern), {
|
|
188
222
|
onFailure: (issue) => [{ path: at, issue }],
|
|
189
|
-
onSuccess: (parsed) =>
|
|
190
|
-
// A rule with no known resource has an issue for that already.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
223
|
+
onSuccess: (parsed) => {
|
|
224
|
+
// A rule with no known resource has an issue for that already. The
|
|
225
|
+
// pattern is checked on every kind, not only up to the first that it
|
|
226
|
+
// fits: a decision on a kind does this check, so the limit of the
|
|
227
|
+
// work below is a limit of the work of a decision too.
|
|
228
|
+
const fits =
|
|
229
|
+
known.length === 0 ||
|
|
230
|
+
Arr.reduce(known, false, (before, descriptor) => {
|
|
231
|
+
const key = `${descriptor.kind}\u0000${pattern}`
|
|
232
|
+
return (
|
|
233
|
+
Option.getOrElse(MutableHashMap.get(memo.fits, key), () => {
|
|
234
|
+
// Each check of a pattern reads the fields of a catalog. The
|
|
235
|
+
// work of one binding has a limit, so very many patterns stop
|
|
236
|
+
// with one issue, and the policy does not load.
|
|
237
|
+
memo.work.steps +=
|
|
238
|
+
descriptor.catalog.fields.length * Math.max(parsed.length, 1)
|
|
239
|
+
if (memo.work.steps > Limits.searchSteps) {
|
|
240
|
+
return true
|
|
241
|
+
}
|
|
242
|
+
const found = Patterns.fitsCatalog(parsed, descriptor.catalog)
|
|
243
|
+
MutableHashMap.set(memo.fits, key, found)
|
|
244
|
+
return found
|
|
245
|
+
}) || before
|
|
246
|
+
)
|
|
198
247
|
})
|
|
199
|
-
|
|
248
|
+
if (memo.work.steps > Limits.searchSteps && !memo.work.noted) {
|
|
249
|
+
memo.work.noted = true
|
|
250
|
+
return [
|
|
251
|
+
{
|
|
252
|
+
path: at,
|
|
253
|
+
issue: `The field patterns of the policy take more than ${String(Limits.searchSteps)} steps to check. The most is ${String(Limits.searchSteps)}.`,
|
|
254
|
+
},
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
return fits
|
|
200
258
|
? []
|
|
201
259
|
: [
|
|
202
260
|
{
|
|
203
261
|
path: at,
|
|
204
262
|
issue: `The field pattern "${pattern}" matches no field of the resources of the rule`,
|
|
205
263
|
},
|
|
206
|
-
]
|
|
264
|
+
]
|
|
265
|
+
},
|
|
207
266
|
})
|
|
208
267
|
|
|
209
268
|
/** The fields of the resources that a test of the rule may read. */
|
|
@@ -448,7 +507,7 @@ const conditionIssues = (
|
|
|
448
507
|
const here = (issues: ReadonlyArray<Issue>) => under(context.memo, at, issues)
|
|
449
508
|
switch (node._tag) {
|
|
450
509
|
case "Compare": {
|
|
451
|
-
const exists =
|
|
510
|
+
const exists = readIssues(context, node.field, ["field"])
|
|
452
511
|
return here(
|
|
453
512
|
exists.length > 0
|
|
454
513
|
? exists
|
|
@@ -462,7 +521,7 @@ const conditionIssues = (
|
|
|
462
521
|
)
|
|
463
522
|
}
|
|
464
523
|
case "In": {
|
|
465
|
-
const exists =
|
|
524
|
+
const exists = readIssues(context, node.field, ["field"])
|
|
466
525
|
return here(
|
|
467
526
|
exists.length > 0
|
|
468
527
|
? exists
|
|
@@ -475,7 +534,7 @@ const conditionIssues = (
|
|
|
475
534
|
)
|
|
476
535
|
}
|
|
477
536
|
case "Exists":
|
|
478
|
-
return here(
|
|
537
|
+
return here(readIssues(context, node.field, ["field"]))
|
|
479
538
|
case "Under":
|
|
480
539
|
return here(underIssues(context, node.prefix, []))
|
|
481
540
|
case "And":
|
|
@@ -607,15 +666,18 @@ export const treeIssues = (
|
|
|
607
666
|
// The rules come out of the tree first, and each one is checked after.
|
|
608
667
|
// A check of a condition goes down its levels, so a check inside the walk
|
|
609
668
|
// of the tree would go down the levels of both.
|
|
669
|
+
// The place of each rule shares the path of its parent, and the full path
|
|
670
|
+
// is made only when its rule is checked. A deep tree with very many rules
|
|
671
|
+
// would need very much memory for a full copy of the path of each rule.
|
|
610
672
|
const rulesOf = (
|
|
611
673
|
node: TreeNode,
|
|
612
|
-
|
|
674
|
+
place: Place.Place | undefined,
|
|
613
675
|
level: number,
|
|
614
|
-
): ReadonlyArray<Issue | readonly [RuleData,
|
|
676
|
+
): ReadonlyArray<Issue | readonly [RuleData, Place.Place]> => {
|
|
615
677
|
if (level > Limits.policyDepth) {
|
|
616
678
|
return [
|
|
617
679
|
{
|
|
618
|
-
path,
|
|
680
|
+
path: Place.pathOf(place),
|
|
619
681
|
issue: `A policy tree has more than ${String(Limits.policyDepth)} levels. The most is ${String(Limits.policyDepth)}.`,
|
|
620
682
|
},
|
|
621
683
|
]
|
|
@@ -624,25 +686,25 @@ export const treeIssues = (
|
|
|
624
686
|
case "Rules":
|
|
625
687
|
return Arr.map(
|
|
626
688
|
node.rules,
|
|
627
|
-
(rule, index) => [rule,
|
|
689
|
+
(rule, index) => [rule, Place.at(place, "rules", index)] as const,
|
|
628
690
|
)
|
|
629
691
|
case "All":
|
|
630
692
|
case "Any":
|
|
631
693
|
case "FirstOf":
|
|
632
694
|
return Arr.flatMap(node.policies, (child, index) =>
|
|
633
|
-
rulesOf(child,
|
|
695
|
+
rulesOf(child, Place.at(place, "policies", index), level + 1),
|
|
634
696
|
)
|
|
635
697
|
case "Ceiling":
|
|
636
698
|
return [
|
|
637
|
-
...rulesOf(node.cap,
|
|
638
|
-
...rulesOf(node.policy,
|
|
699
|
+
...rulesOf(node.cap, Place.at(place, "cap"), level + 1),
|
|
700
|
+
...rulesOf(node.policy, Place.at(place, "policy"), level + 1),
|
|
639
701
|
]
|
|
640
702
|
}
|
|
641
703
|
}
|
|
642
704
|
const memo = makeMemo()
|
|
643
|
-
return Arr.flatMap(rulesOf(root,
|
|
705
|
+
return Arr.flatMap(rulesOf(root, undefined, 1), (found) =>
|
|
644
706
|
Array.isArray(found)
|
|
645
|
-
? ruleIssues(domain, found[0], found[1], memo)
|
|
707
|
+
? ruleIssues(domain, found[0], Place.pathOf(found[1]), memo)
|
|
646
708
|
: [found as Issue],
|
|
647
709
|
)
|
|
648
710
|
}
|
package/src/internal/catalog.ts
CHANGED
|
@@ -129,6 +129,15 @@ export interface Field {
|
|
|
129
129
|
readonly listed: boolean
|
|
130
130
|
/** The field may hold parts that the Schema does not name, as a record may. */
|
|
131
131
|
readonly open: boolean
|
|
132
|
+
/**
|
|
133
|
+
* A read of the field finds only plain data. It is `false` when a value
|
|
134
|
+
* above the field may be an object that does not have it as a data field:
|
|
135
|
+
* a class without the field, which may have a getter of that name, a
|
|
136
|
+
* value such as a date or a list, or any object, as of `Schema.Unknown`.
|
|
137
|
+
* A decode or a `make` copies a struct or a record into a plain object,
|
|
138
|
+
* so a struct without the field gives `null` for it.
|
|
139
|
+
*/
|
|
140
|
+
readonly plain: boolean
|
|
132
141
|
}
|
|
133
142
|
|
|
134
143
|
/** A place in the tree of a catalog, with the places below it. */
|
|
@@ -185,8 +194,8 @@ const classStruct = (
|
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
/**
|
|
188
|
-
* Gives the Schemas that a value at one place may fit: it opens unions
|
|
189
|
-
* `Schema.suspend
|
|
197
|
+
* Gives the Schemas that a value at one place may fit: it opens unions and
|
|
198
|
+
* `Schema.suspend`. A class stays a class: see {@link structOf}. The open suspend nodes are in a native `Set`,
|
|
190
199
|
* because the walk must find a node by the node itself: `Equal` compares
|
|
191
200
|
* Schema nodes by structure, and a recursive Schema has no end. A suspend
|
|
192
201
|
* node that gives itself again, with no step on the path, gives nothing.
|
|
@@ -202,15 +211,19 @@ const alternatives = (
|
|
|
202
211
|
return open.has(ast)
|
|
203
212
|
? []
|
|
204
213
|
: alternatives(ast.thunk(), new Set([...open, ast]))
|
|
205
|
-
case "Declaration": {
|
|
206
|
-
const inner = classStruct(ast)
|
|
207
|
-
return inner === undefined ? [ast] : alternatives(inner, open)
|
|
208
|
-
}
|
|
209
214
|
default:
|
|
210
215
|
return [ast]
|
|
211
216
|
}
|
|
212
217
|
}
|
|
213
218
|
|
|
219
|
+
/** The struct of a Schema: a struct, or the struct of a class. */
|
|
220
|
+
const structOf = (ast: SchemaAST.AST): SchemaAST.Objects | undefined =>
|
|
221
|
+
ast._tag === "Objects"
|
|
222
|
+
? ast
|
|
223
|
+
: ast._tag === "Declaration"
|
|
224
|
+
? classStruct(ast)
|
|
225
|
+
: undefined
|
|
226
|
+
|
|
214
227
|
const hasCheck = (
|
|
215
228
|
checks: ReadonlyArray<SchemaAST.Check<never>>,
|
|
216
229
|
id: string,
|
|
@@ -223,8 +236,10 @@ const hasCheck = (
|
|
|
223
236
|
)
|
|
224
237
|
|
|
225
238
|
/** A struct with named fields, or with the keys of a record. */
|
|
226
|
-
const isStruct = (
|
|
227
|
-
ast.
|
|
239
|
+
const isStruct = (
|
|
240
|
+
ast: SchemaAST.Objects | undefined,
|
|
241
|
+
): ast is SchemaAST.Objects =>
|
|
242
|
+
ast !== undefined &&
|
|
228
243
|
(ast.propertySignatures.length > 0 || ast.indexSignatures.length > 0)
|
|
229
244
|
|
|
230
245
|
/** The kinds of the values of a Schema that is not a struct. */
|
|
@@ -326,6 +341,8 @@ interface Place {
|
|
|
326
341
|
readonly missing: boolean
|
|
327
342
|
/** The value may be anything, as an extra field of a struct may be. */
|
|
328
343
|
readonly top: boolean
|
|
344
|
+
/** A read here finds only plain data: see {@link Field.plain}. */
|
|
345
|
+
readonly plain: boolean
|
|
329
346
|
}
|
|
330
347
|
|
|
331
348
|
const textOf = (path: ReadonlyArray<Segment>): string =>
|
|
@@ -333,16 +350,27 @@ const textOf = (path: ReadonlyArray<Segment>): string =>
|
|
|
333
350
|
typeof segment === "string" ? segment : "*",
|
|
334
351
|
).join(".")
|
|
335
352
|
|
|
336
|
-
/**
|
|
337
|
-
|
|
353
|
+
/**
|
|
354
|
+
* The name of a property as a part of a path. A key of an object is a string,
|
|
355
|
+
* so a number key, as from `Schema.Record(Schema.Literal(1), ...)`, has the
|
|
356
|
+
* name `"1"`. A symbol key is not a field.
|
|
357
|
+
*/
|
|
358
|
+
const nameOf = (signature: SchemaAST.PropertySignature): string | undefined =>
|
|
359
|
+
typeof signature.name === "symbol" ? undefined : String(signature.name)
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The properties of a struct by name. A walk makes it one time for each
|
|
363
|
+
* struct of a place, so a struct with very many fields does not take a
|
|
364
|
+
* search for each name.
|
|
365
|
+
*/
|
|
366
|
+
const propertiesOf = (
|
|
338
367
|
struct: SchemaAST.Objects,
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
),
|
|
368
|
+
): HashMap.HashMap<string, SchemaAST.PropertySignature> =>
|
|
369
|
+
HashMap.fromIterable(
|
|
370
|
+
Arr.flatMap(struct.propertySignatures, (signature) => {
|
|
371
|
+
const name = nameOf(signature)
|
|
372
|
+
return name === undefined ? [] : [[name, signature] as const]
|
|
373
|
+
}),
|
|
346
374
|
)
|
|
347
375
|
|
|
348
376
|
/** The names of the fields of the structs, in the order that they first come. */
|
|
@@ -352,8 +380,8 @@ const namesOf = (
|
|
|
352
380
|
Arr.dedupe(
|
|
353
381
|
Arr.flatMap(structs, (struct) =>
|
|
354
382
|
Arr.flatMap(struct.propertySignatures, (signature) => {
|
|
355
|
-
const name = signature
|
|
356
|
-
if (
|
|
383
|
+
const name = nameOf(signature)
|
|
384
|
+
if (name === undefined) {
|
|
357
385
|
return []
|
|
358
386
|
}
|
|
359
387
|
// A dot in a path is the step into a nested field. A field name with
|
|
@@ -377,10 +405,22 @@ const namesOf = (
|
|
|
377
405
|
* very many fields before the walk is done.
|
|
378
406
|
*/
|
|
379
407
|
const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
380
|
-
const structs = Arr.filter(place.schemas, isStruct)
|
|
381
|
-
|
|
408
|
+
const structs = Arr.filter(Arr.map(place.schemas, structOf), isStruct)
|
|
409
|
+
// The structs of classes. A value of a class is not copied into a plain
|
|
410
|
+
// object, so it keeps its getters and methods.
|
|
411
|
+
const classes = Arr.filter(
|
|
412
|
+
Arr.map(
|
|
413
|
+
Arr.filter(place.schemas, (schema) => schema._tag === "Declaration"),
|
|
414
|
+
structOf,
|
|
415
|
+
),
|
|
416
|
+
isStruct,
|
|
417
|
+
)
|
|
418
|
+
const values = Arr.filter(
|
|
419
|
+
place.schemas,
|
|
420
|
+
(schema) => !isStruct(structOf(schema)),
|
|
421
|
+
)
|
|
382
422
|
const valueKinds = Arr.reduce(values, noKinds, (kinds, schema) =>
|
|
383
|
-
joinKinds(kinds, kindsOf(schema)),
|
|
423
|
+
joinKinds(kinds, kindsOf(structOf(schema) ?? schema)),
|
|
384
424
|
)
|
|
385
425
|
const kinds = place.top
|
|
386
426
|
? anyKinds
|
|
@@ -412,6 +452,7 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
412
452
|
structs.length > 0 ||
|
|
413
453
|
valueKinds.object ||
|
|
414
454
|
place.top,
|
|
455
|
+
plain: place.plain,
|
|
415
456
|
}
|
|
416
457
|
if (field !== undefined) {
|
|
417
458
|
count.size += 1
|
|
@@ -434,11 +475,12 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
434
475
|
keyBranch: undefined,
|
|
435
476
|
}
|
|
436
477
|
}
|
|
478
|
+
const structProperties = Arr.map(structs, propertiesOf)
|
|
479
|
+
const classProperties = Arr.map(classes, propertiesOf)
|
|
437
480
|
const children = Arr.map(namesOf(structs), (name) => {
|
|
438
|
-
const properties = Arr.flatMap(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
})
|
|
481
|
+
const properties = Arr.flatMap(structProperties, (named) =>
|
|
482
|
+
Option.toArray(HashMap.get(named, name)),
|
|
483
|
+
)
|
|
442
484
|
const below: Place = {
|
|
443
485
|
path: [...place.path, name],
|
|
444
486
|
schemas: Arr.flatMap(properties, (property) =>
|
|
@@ -454,10 +496,11 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
454
496
|
// with any value: only a decode takes extra fields away. A value
|
|
455
497
|
// that may be any object, as one of `Schema.Unknown` may, may have
|
|
456
498
|
// the name with any value too.
|
|
457
|
-
top:
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
499
|
+
top: place.top || valueKinds.object || properties.length < structs.length,
|
|
500
|
+
plain:
|
|
501
|
+
place.plain &&
|
|
502
|
+
!valueKinds.object &&
|
|
503
|
+
Arr.every(classProperties, (named) => HashMap.has(named, name)),
|
|
461
504
|
}
|
|
462
505
|
return [name, branchOf(below, count)] as const
|
|
463
506
|
})
|
|
@@ -479,6 +522,10 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
479
522
|
// A record may not have a given key.
|
|
480
523
|
missing: true,
|
|
481
524
|
top: place.top || valueKinds.object || records.length < structs.length,
|
|
525
|
+
plain:
|
|
526
|
+
place.plain &&
|
|
527
|
+
!valueKinds.object &&
|
|
528
|
+
Arr.every(classes, (struct) => struct.indexSignatures.length > 0),
|
|
482
529
|
},
|
|
483
530
|
count,
|
|
484
531
|
)
|
|
@@ -502,6 +549,7 @@ export const catalogOf = (ast: SchemaAST.AST): Catalog => {
|
|
|
502
549
|
schemas: alternatives(ast, new Set()),
|
|
503
550
|
missing: false,
|
|
504
551
|
top: false,
|
|
552
|
+
plain: true,
|
|
505
553
|
},
|
|
506
554
|
{ size: 0 },
|
|
507
555
|
)
|
package/src/internal/decide.ts
CHANGED
|
@@ -37,6 +37,8 @@ interface DecideContext {
|
|
|
37
37
|
readonly input: Condition.Input
|
|
38
38
|
/** The resource of the kind, for the fields that a rule may grant. */
|
|
39
39
|
readonly descriptor: Resource.Descriptor
|
|
40
|
+
/** The test of `RuleData.fitsOf` for the catalog, for this decision. */
|
|
41
|
+
readonly fits: (pattern: string) => boolean
|
|
40
42
|
/** The rules that were tested, for {@link explain}. */
|
|
41
43
|
readonly trace: Ref.Ref<Chunk.Chunk<Step>> | undefined
|
|
42
44
|
}
|
|
@@ -282,6 +284,7 @@ const makeContext = (
|
|
|
282
284
|
path: descriptor.path,
|
|
283
285
|
},
|
|
284
286
|
descriptor,
|
|
287
|
+
fits: RuleData.fitsOf(descriptor.catalog),
|
|
285
288
|
trace,
|
|
286
289
|
}
|
|
287
290
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The place of a part of an input, as a path that shares the path of its
|
|
3
|
+
* parent. A deep tree with very many rules would need very much memory for a
|
|
4
|
+
* full copy of the path of each rule, so a place keeps only its own parts and
|
|
5
|
+
* its parent. The full path is made only for a problem, in one walk up.
|
|
6
|
+
*/
|
|
7
|
+
import * as Arr from "effect/Array"
|
|
8
|
+
import * as Option from "effect/Option"
|
|
9
|
+
|
|
10
|
+
/** A place: its own parts of the path, after the path of its parent. */
|
|
11
|
+
export interface Place {
|
|
12
|
+
readonly parent: Place | undefined
|
|
13
|
+
readonly parts: ReadonlyArray<PropertyKey>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The place below `parent` with more parts. */
|
|
17
|
+
export const at = (
|
|
18
|
+
parent: Place | undefined,
|
|
19
|
+
...parts: ReadonlyArray<PropertyKey>
|
|
20
|
+
): Place => ({ parent, parts })
|
|
21
|
+
|
|
22
|
+
/** The full path of a place. The root, with no place, has the empty path. */
|
|
23
|
+
export const pathOf = (place: Place | undefined): ReadonlyArray<PropertyKey> =>
|
|
24
|
+
Arr.flatten(
|
|
25
|
+
Arr.reverse(
|
|
26
|
+
Arr.unfold(place, (current) =>
|
|
27
|
+
current === undefined
|
|
28
|
+
? Option.none()
|
|
29
|
+
: Option.some([current.parts, current.parent] as const),
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
)
|
|
@@ -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
|
)
|