@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.
- package/README.md +52 -29
- package/dist/Authorizer.d.ts +21 -12
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +121 -19
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +32 -21
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +9 -0
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +5 -0
- package/dist/Domain.js.map +1 -1
- package/dist/Limits.d.ts +44 -13
- package/dist/Limits.d.ts.map +1 -1
- package/dist/Limits.js +52 -22
- package/dist/Limits.js.map +1 -1
- package/dist/Policy.d.ts +102 -18
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +7 -6
- package/dist/Policy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internal/algebra.js +2 -2
- package/dist/internal/analysis.js +81 -75
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +11 -7
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +86 -64
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +33 -10
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +170 -77
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +27 -31
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/domain.js +19 -5
- package/dist/internal/domain.js.map +1 -1
- package/dist/internal/json.js +120 -76
- package/dist/internal/json.js.map +1 -1
- package/dist/internal/limits.d.ts +28 -0
- package/dist/internal/limits.d.ts.map +1 -1
- package/dist/internal/limits.js +75 -16
- package/dist/internal/limits.js.map +1 -1
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +53 -44
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/patterns.js +50 -15
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-json.js +169 -127
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +16 -13
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/policy.d.ts.map +1 -1
- package/dist/internal/policy.js +38 -22
- package/dist/internal/policy.js.map +1 -1
- package/dist/internal/resource.js +1 -1
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +6 -5
- package/dist/internal/rule.js.map +1 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +106 -92
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/trampoline.js +19 -5
- package/dist/internal/trampoline.js.map +1 -1
- package/package.json +1 -1
- package/src/Authorizer.ts +30 -20
- package/src/Condition.ts +317 -86
- package/src/Domain.ts +9 -0
- package/src/Limits.ts +69 -29
- package/src/Policy.ts +143 -21
- package/src/internal/analysis.ts +154 -124
- package/src/internal/authorizer.ts +29 -14
- package/src/internal/bind.ts +193 -136
- package/src/internal/catalog.ts +320 -118
- package/src/internal/decide.ts +61 -50
- package/src/internal/domain.ts +39 -3
- package/src/internal/json.ts +212 -117
- package/src/internal/limits.ts +101 -31
- package/src/internal/nodes.ts +65 -47
- package/src/internal/patterns.ts +79 -25
- package/src/internal/policy-json.ts +266 -197
- package/src/internal/policy-nodes.ts +31 -23
- package/src/internal/policy.ts +63 -23
- package/src/internal/rule.ts +16 -3
- package/src/internal/search.ts +186 -136
- 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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
+
at: Place.Place | undefined,
|
|
144
156
|
budget: Json.Budget,
|
|
145
157
|
): ReadonlyArray<string> =>
|
|
146
|
-
Arr.map(Json.readList(input, name,
|
|
158
|
+
Arr.map(Json.readList(input, name, at, budget), (value, index) => {
|
|
147
159
|
if (typeof value !== "string") {
|
|
148
|
-
throw Json.
|
|
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
|
-
|
|
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,
|
|
183
|
+
Json.read(input, name, at)
|
|
169
184
|
return undefined
|
|
170
185
|
}
|
|
171
186
|
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
value: unknown
|
|
175
|
-
|
|
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
|
|
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",
|
|
263
|
-
const value = Json.read(input, "meta",
|
|
278
|
+
: readOptional(input, "meta", place, () => {
|
|
279
|
+
const value = Json.read(input, "meta", place)
|
|
264
280
|
if (domain.meta === undefined) {
|
|
265
|
-
throw Json.
|
|
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,
|
|
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:
|
|
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.
|
|
310
|
+
throw Json.problemAt(place, "A rule must be an object")
|
|
299
311
|
}
|
|
300
|
-
Json.spend(budget, 1,
|
|
301
|
-
const tag = Json.read(input, "_tag",
|
|
312
|
+
Json.spend(budget, 1, place)
|
|
313
|
+
const tag = Json.read(input, "_tag", place)
|
|
302
314
|
if (tag !== "Allow" && tag !== "Deny") {
|
|
303
|
-
throw Json.
|
|
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",
|
|
306
|
-
Json.decodeAt(
|
|
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",
|
|
309
|
-
readStrings(input, "fields",
|
|
327
|
+
const fields = readOptional(input, "fields", place, () =>
|
|
328
|
+
readStrings(input, "fields", place, budget),
|
|
310
329
|
)
|
|
311
|
-
const reason = readOptional(input, "reason",
|
|
312
|
-
Json.readString(input, "reason",
|
|
330
|
+
const reason = readOptional(input, "reason", place, () =>
|
|
331
|
+
Json.readString(input, "reason", place),
|
|
313
332
|
)
|
|
314
|
-
const id = readOptional(input, "id",
|
|
315
|
-
Json.readString(input, "id",
|
|
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",
|
|
322
|
-
resources: readStrings(input, "resources",
|
|
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.
|
|
336
|
-
*
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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 = (
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 = (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
})
|