@nmnmcc/ability 2.0.5 → 2.0.7
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 +6 -5
- package/dist/Condition.d.ts +6 -6
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +1 -1
- package/dist/Policy.d.ts +2 -2
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js.map +1 -1
- package/dist/internal/algebra.js +128 -79
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/analysis.js +1 -1
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +38 -14
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +59 -43
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +6 -0
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +17 -14
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +1 -1
- package/dist/internal/domain.js +1 -1
- package/dist/internal/json.js +2 -10
- package/dist/internal/json.js.map +1 -1
- package/dist/internal/nodes.d.ts +11 -1
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +51 -11
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/patterns.js +1 -1
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-json.js +2 -8
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts +4 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +14 -5
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/search.js +4 -4
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/trampoline.js +75 -0
- package/dist/internal/trampoline.js.map +1 -0
- package/package.json +1 -1
- package/src/Condition.ts +5 -2
- package/src/Policy.ts +1 -2
- package/src/internal/algebra.ts +262 -143
- package/src/internal/analysis.ts +3 -1
- package/src/internal/authorizer.ts +59 -14
- package/src/internal/bind.ts +149 -71
- package/src/internal/catalog.ts +54 -50
- package/src/internal/json.ts +2 -11
- package/src/internal/nodes.ts +58 -24
- package/src/internal/patterns.ts +7 -3
- package/src/internal/policy-json.ts +2 -9
- package/src/internal/policy-nodes.ts +16 -7
- package/src/internal/search.ts +13 -10
- package/src/internal/trampoline.ts +147 -0
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
// language service check for `unknown` in an Effect is off here.
|
|
8
8
|
// @effect-diagnostics anyUnknownInErrorContext:off
|
|
9
9
|
import * as Cache from "effect/Cache"
|
|
10
|
+
import * as Chunk from "effect/Chunk"
|
|
10
11
|
import * as Context from "effect/Context"
|
|
11
12
|
import type * as Duration from "effect/Duration"
|
|
12
13
|
import * as Effect from "effect/Effect"
|
|
13
14
|
import * as Equal from "effect/Equal"
|
|
14
15
|
import * as Hash from "effect/Hash"
|
|
16
|
+
import * as HashMap from "effect/HashMap"
|
|
17
|
+
import * as HashSet from "effect/HashSet"
|
|
15
18
|
import * as Layer from "effect/Layer"
|
|
16
19
|
import * as Result from "effect/Result"
|
|
17
20
|
import * as Schema from "effect/Schema"
|
|
@@ -103,18 +106,24 @@ type AnyTarget = {
|
|
|
103
106
|
class PrincipalKey implements Equal.Equal {
|
|
104
107
|
readonly principal: object
|
|
105
108
|
readonly text: string
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
/**
|
|
110
|
+
* The order of the parts of the decoded principal, from {@link orderOf},
|
|
111
|
+
* when the key is the JSON form. The decoded principals must be equal
|
|
112
|
+
* too, with their parts in the same order. `undefined` for a key from
|
|
113
|
+
* `cache.key`.
|
|
114
|
+
*/
|
|
115
|
+
readonly order: string | undefined
|
|
116
|
+
constructor(principal: object, text: string, order: string | undefined) {
|
|
109
117
|
this.principal = principal
|
|
110
118
|
this.text = text
|
|
111
|
-
this.
|
|
119
|
+
this.order = order
|
|
112
120
|
}
|
|
113
121
|
[Equal.symbol](that: unknown): boolean {
|
|
114
122
|
return (
|
|
115
123
|
that instanceof PrincipalKey &&
|
|
116
124
|
that.text === this.text &&
|
|
117
|
-
|
|
125
|
+
that.order === this.order &&
|
|
126
|
+
(this.order === undefined || Equal.equals(that.principal, this.principal))
|
|
118
127
|
)
|
|
119
128
|
}
|
|
120
129
|
[Hash.symbol](): number {
|
|
@@ -123,14 +132,39 @@ class PrincipalKey implements Equal.Equal {
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* the
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* can not give one the policy of the other.
|
|
135
|
+
* The text of the order of the parts of a value: the names of the fields of
|
|
136
|
+
* each object, and the items of each list, set and map, with each scalar, in
|
|
137
|
+
* the order that a loop sees them. `Equal` does not see the order of the
|
|
138
|
+
* fields of an object or of the items of a map, but a loader may read it, as
|
|
139
|
+
* `Object.values` does. So two keys need the same order too. A scalar is in
|
|
140
|
+
* the text with its type, so the items of a set or a map show their order.
|
|
133
141
|
*/
|
|
142
|
+
const orderOf = (value: unknown): string => {
|
|
143
|
+
if (typeof value !== "object" || value === null) {
|
|
144
|
+
return typeof value === "string"
|
|
145
|
+
? JSON.stringify(value)
|
|
146
|
+
: `${typeof value}:${String(value)}`
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
return `[${value.map(orderOf).join(",")}]`
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
value instanceof Map ||
|
|
153
|
+
value instanceof Set ||
|
|
154
|
+
HashMap.isHashMap(value) ||
|
|
155
|
+
HashSet.isHashSet(value) ||
|
|
156
|
+
Chunk.isChunk(value)
|
|
157
|
+
) {
|
|
158
|
+
return `<${Array.from(value as Iterable<unknown>, orderOf).join(",")}>`
|
|
159
|
+
}
|
|
160
|
+
return `{${Object.keys(value)
|
|
161
|
+
.map(
|
|
162
|
+
(name) =>
|
|
163
|
+
`${JSON.stringify(name)}:${orderOf((value as Record<string, unknown>)[name])}`,
|
|
164
|
+
)
|
|
165
|
+
.join(",")}}`
|
|
166
|
+
}
|
|
167
|
+
|
|
134
168
|
/**
|
|
135
169
|
* Gives the JSON codec of a Schema, or `undefined` when the Schema has no JSON
|
|
136
170
|
* form, as a struct with a symbol key has none.
|
|
@@ -148,6 +182,17 @@ const jsonCodecOf = (
|
|
|
148
182
|
}
|
|
149
183
|
}
|
|
150
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Makes the cache key of a principal from its JSON form. The key keeps the
|
|
187
|
+
* principal that the Schema decodes from that form, so the loader sees only
|
|
188
|
+
* the data that the key shows. A principal gets no key when it does not
|
|
189
|
+
* encode, or when the decoded value is not `Equal` to it. Two classes, or a
|
|
190
|
+
* string and a number, may have one JSON form, as in a union, and `Equal`
|
|
191
|
+
* tells them apart. A Schema may make data when it decodes, as the order of
|
|
192
|
+
* the fields of a record, so the key keeps that order too. So two
|
|
193
|
+
* principals with one key are equal, and the cache can not give one the
|
|
194
|
+
* policy of the other.
|
|
195
|
+
*/
|
|
151
196
|
const jsonKey = (
|
|
152
197
|
schema: Schema.Top,
|
|
153
198
|
): ((principal: object) => PrincipalKey | undefined) => {
|
|
@@ -170,7 +215,7 @@ const jsonKey = (
|
|
|
170
215
|
onFailure: () => undefined,
|
|
171
216
|
onSuccess: (decoded) =>
|
|
172
217
|
Equal.equals(decoded, principal)
|
|
173
|
-
? new PrincipalKey(decoded as object, text,
|
|
218
|
+
? new PrincipalKey(decoded as object, text, orderOf(decoded))
|
|
174
219
|
: undefined,
|
|
175
220
|
})
|
|
176
221
|
},
|
|
@@ -236,7 +281,7 @@ export const makeLayer = (
|
|
|
236
281
|
const text = custom(principal)
|
|
237
282
|
return text === undefined
|
|
238
283
|
? undefined
|
|
239
|
-
: new PrincipalKey(canonical?.principal ?? principal, text,
|
|
284
|
+
: new PrincipalKey(canonical?.principal ?? principal, text, undefined)
|
|
240
285
|
}
|
|
241
286
|
return Layer.effect(
|
|
242
287
|
service,
|
package/src/internal/bind.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as Arr from "effect/Array"
|
|
9
9
|
import * as HashMap from "effect/HashMap"
|
|
10
|
-
import * as MutableHashMap from "effect/MutableHashMap"
|
|
11
10
|
import * as Option from "effect/Option"
|
|
12
11
|
import * as Result from "effect/Result"
|
|
13
12
|
import type * as Domain from "../Domain.ts"
|
|
@@ -39,8 +38,11 @@ export interface RuleData {
|
|
|
39
38
|
* time. It goes away when the binding ends.
|
|
40
39
|
*/
|
|
41
40
|
interface Memo {
|
|
42
|
-
/**
|
|
43
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The suggestion for each name, by the place where it was looked for. The
|
|
43
|
+
* names come from a policy, so the map is native, as `fits` is.
|
|
44
|
+
*/
|
|
45
|
+
readonly suggestions: Map<string, string>
|
|
44
46
|
/**
|
|
45
47
|
* If a pattern matches a field of a kind, by the kind and the pattern. The
|
|
46
48
|
* patterns come from a policy, so the map is native: the hash of a string
|
|
@@ -55,7 +57,7 @@ interface Memo {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
const makeMemo = (): Memo => ({
|
|
58
|
-
suggestions:
|
|
60
|
+
suggestions: new Map(),
|
|
59
61
|
fits: new Map(),
|
|
60
62
|
issues: { count: 0, noted: false },
|
|
61
63
|
work: { steps: 0, noted: false },
|
|
@@ -77,15 +79,15 @@ const suggestIn = (
|
|
|
77
79
|
names: () => ReadonlyArray<string>,
|
|
78
80
|
): string => {
|
|
79
81
|
const key = `${scope}\u0000${name}`
|
|
80
|
-
const known =
|
|
81
|
-
if (
|
|
82
|
-
return known
|
|
82
|
+
const known = memo.suggestions.get(key)
|
|
83
|
+
if (known !== undefined) {
|
|
84
|
+
return known
|
|
83
85
|
}
|
|
84
|
-
if (
|
|
86
|
+
if (memo.suggestions.size >= maxSuggestions) {
|
|
85
87
|
return message
|
|
86
88
|
}
|
|
87
89
|
const made = withSuggestion(message, name, names())
|
|
88
|
-
|
|
90
|
+
memo.suggestions.set(key, made)
|
|
89
91
|
return made
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -250,16 +252,8 @@ const patternIssues = (
|
|
|
250
252
|
}) || before
|
|
251
253
|
)
|
|
252
254
|
})
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return [
|
|
256
|
-
{
|
|
257
|
-
path: at,
|
|
258
|
-
issue: `The field patterns of the policy take more than ${String(Limits.searchSteps)} steps to check. The most is ${String(Limits.searchSteps)}.`,
|
|
259
|
-
},
|
|
260
|
-
]
|
|
261
|
-
}
|
|
262
|
-
return fits
|
|
255
|
+
// Past the limit of the work, `ruleIssues` gives one issue for it.
|
|
256
|
+
return fits || memo.work.steps > Limits.searchSteps
|
|
263
257
|
? []
|
|
264
258
|
: [
|
|
265
259
|
{
|
|
@@ -312,15 +306,45 @@ const principalField = (
|
|
|
312
306
|
const isIssue = (value: Catalog.Field | Issue): value is Issue =>
|
|
313
307
|
"issue" in value
|
|
314
308
|
|
|
315
|
-
/**
|
|
309
|
+
/**
|
|
310
|
+
* The fields that a test reads, with one field for each distinct kind of
|
|
311
|
+
* value. A rule for very many kinds may have one field type in all of them,
|
|
312
|
+
* so each value of the test is checked one time for each distinct kind, not
|
|
313
|
+
* one time for each resource. The kinds are found by their text in a native
|
|
314
|
+
* `Map`, which belongs to this call.
|
|
315
|
+
*/
|
|
316
|
+
const distinctFields = (
|
|
317
|
+
context: Context,
|
|
318
|
+
field: string,
|
|
319
|
+
): ReadonlyArray<Catalog.Field> =>
|
|
320
|
+
Array.from(
|
|
321
|
+
new Map(
|
|
322
|
+
Arr.map(
|
|
323
|
+
fieldsAt(context, field),
|
|
324
|
+
(found) => [JSON.stringify(found.kinds), found] as const,
|
|
325
|
+
),
|
|
326
|
+
).values(),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Counts the work of the checks of the values of a test, as the work of the
|
|
331
|
+
* field patterns counts. Past the limit, the values are not checked, and
|
|
332
|
+
* `ruleIssues` gives one issue for the work.
|
|
333
|
+
*/
|
|
334
|
+
const spendChecks = (memo: Memo, values: number, fields: number): boolean => {
|
|
335
|
+
memo.work.steps += values * Math.max(fields, 1)
|
|
336
|
+
return memo.work.steps <= Limits.searchSteps
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The issues of one operand of a test of a field, with its distinct fields. */
|
|
316
340
|
const operandIssues = (
|
|
317
341
|
context: Context,
|
|
318
342
|
field: string,
|
|
343
|
+
fields: ReadonlyArray<Catalog.Field>,
|
|
319
344
|
ordered: boolean,
|
|
320
345
|
operand: Operand,
|
|
321
346
|
at: ReadonlyArray<PropertyKey>,
|
|
322
347
|
): ReadonlyArray<Issue> => {
|
|
323
|
-
const fields = fieldsAt(context, field)
|
|
324
348
|
if (operand._tag === "Literal") {
|
|
325
349
|
const value = operand.value
|
|
326
350
|
if (ordered && typeof value !== "string" && typeof value !== "number") {
|
|
@@ -518,8 +542,17 @@ const conditionIssues = (
|
|
|
518
542
|
}
|
|
519
543
|
seen.add(node)
|
|
520
544
|
const here = (issues: ReadonlyArray<Issue>) => under(context.memo, at, issues)
|
|
545
|
+
// A test looks its field up in each resource of the rule. A rule for very
|
|
546
|
+
// many kinds with very many tests is very much work, so it counts. Past
|
|
547
|
+
// the limit, `ruleIssues` gives one issue for it, and the policy does not
|
|
548
|
+
// load.
|
|
549
|
+
const room = (values: number) =>
|
|
550
|
+
spendChecks(context.memo, 1 + values, context.named.length)
|
|
521
551
|
switch (node._tag) {
|
|
522
552
|
case "Compare": {
|
|
553
|
+
if (!room(1)) {
|
|
554
|
+
return []
|
|
555
|
+
}
|
|
523
556
|
const exists = readIssues(context, node.field, ["field"])
|
|
524
557
|
return here(
|
|
525
558
|
exists.length > 0
|
|
@@ -527,6 +560,7 @@ const conditionIssues = (
|
|
|
527
560
|
: operandIssues(
|
|
528
561
|
context,
|
|
529
562
|
node.field,
|
|
563
|
+
distinctFields(context, node.field),
|
|
530
564
|
node.op !== "eq" && node.op !== "ne",
|
|
531
565
|
node.value,
|
|
532
566
|
["value"],
|
|
@@ -534,20 +568,28 @@ const conditionIssues = (
|
|
|
534
568
|
)
|
|
535
569
|
}
|
|
536
570
|
case "In": {
|
|
571
|
+
if (!room(0)) {
|
|
572
|
+
return []
|
|
573
|
+
}
|
|
537
574
|
const exists = readIssues(context, node.field, ["field"])
|
|
575
|
+
if (exists.length > 0) {
|
|
576
|
+
return here(exists)
|
|
577
|
+
}
|
|
578
|
+
const fields = distinctFields(context, node.field)
|
|
579
|
+
if (!spendChecks(context.memo, node.values.length, fields.length)) {
|
|
580
|
+
return []
|
|
581
|
+
}
|
|
538
582
|
return here(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
]),
|
|
546
|
-
),
|
|
583
|
+
Arr.flatMap(node.values, (operand, index) =>
|
|
584
|
+
operandIssues(context, node.field, fields, false, operand, [
|
|
585
|
+
"values",
|
|
586
|
+
index,
|
|
587
|
+
]),
|
|
588
|
+
),
|
|
547
589
|
)
|
|
548
590
|
}
|
|
549
591
|
case "Exists":
|
|
550
|
-
return here(readIssues(context, node.field, ["field"]))
|
|
592
|
+
return room(0) ? here(readIssues(context, node.field, ["field"])) : []
|
|
551
593
|
case "Under":
|
|
552
594
|
return here(underIssues(context, node.prefix, []))
|
|
553
595
|
case "And":
|
|
@@ -580,19 +622,23 @@ const hasAction = (
|
|
|
580
622
|
Arr.some(actions, (action) => descriptor.actions.includes(action))
|
|
581
623
|
|
|
582
624
|
/**
|
|
583
|
-
* Checks one rule against a domain. It gives one issue for each problem
|
|
584
|
-
*
|
|
625
|
+
* Checks one rule against a domain. It gives one issue for each problem, up
|
|
626
|
+
* to the most issues of the binding. `at` gives the path of the rule, and it
|
|
627
|
+
* is made only for an issue that the binding keeps: a rule with very many
|
|
628
|
+
* problems deep in a tree does not make a full path for each one.
|
|
585
629
|
*/
|
|
586
630
|
export const ruleIssues = (
|
|
587
631
|
domain: Domain.Any,
|
|
588
632
|
rule: RuleData,
|
|
589
|
-
at:
|
|
633
|
+
at: LazyPath,
|
|
590
634
|
memo: Memo = makeMemo(),
|
|
591
635
|
): ReadonlyArray<Issue> => {
|
|
636
|
+
const here = (issues: ReadonlyArray<Issue>) => under(memo, at, issues)
|
|
592
637
|
const all = rule.resources.includes("*")
|
|
593
|
-
// Each kind is checked one time, also when the list names it again.
|
|
594
|
-
// native `Set` finds the copies
|
|
595
|
-
//
|
|
638
|
+
// Each kind is checked one time, also when the list names it again. The
|
|
639
|
+
// names come from a policy, so a native `Set` finds the copies:
|
|
640
|
+
// `Arr.dedupe` puts the names in groups by the hash of Effect, which has
|
|
641
|
+
// no random seed, and very many names with one hash make one long group.
|
|
596
642
|
const names = Array.from(
|
|
597
643
|
new Set(Arr.filter(rule.resources, (kind) => kind !== "*")),
|
|
598
644
|
)
|
|
@@ -616,49 +662,81 @@ export const ruleIssues = (
|
|
|
616
662
|
? Arr.filter(known, (descriptor) => hasAction(descriptor, rule.actions))
|
|
617
663
|
: applicable,
|
|
618
664
|
}
|
|
619
|
-
|
|
620
|
-
|
|
665
|
+
// A native `Set` finds copies about ten times as fast as `Arr.dedupe`.
|
|
666
|
+
const actions = Array.from(
|
|
667
|
+
new Set(Arr.flatMap(known, (descriptor) => descriptor.actions)),
|
|
621
668
|
)
|
|
669
|
+
const fieldIssues = Arr.flatMap(rule.fields ?? [], (field, index) =>
|
|
670
|
+
here(
|
|
671
|
+
field.includes("*")
|
|
672
|
+
? patternIssues(memo, known, field, ["fields", index])
|
|
673
|
+
: fieldIssue(context, field, ["fields", index]),
|
|
674
|
+
),
|
|
675
|
+
)
|
|
676
|
+
const whenIssues =
|
|
677
|
+
rule.when === undefined
|
|
678
|
+
? []
|
|
679
|
+
: Nodes.isNode(rule.when)
|
|
680
|
+
? conditionIssues(
|
|
681
|
+
context,
|
|
682
|
+
rule.when,
|
|
683
|
+
() => [...at(), "when"],
|
|
684
|
+
new Set(),
|
|
685
|
+
)
|
|
686
|
+
: here([notWhole(["when"])])
|
|
687
|
+
// The work is known only after every check of the rule. Past the limit, a
|
|
688
|
+
// check gives no issue, so this issue must come, or the policy would load
|
|
689
|
+
// with checks left out.
|
|
690
|
+
const work = memo.work.steps > Limits.searchSteps && !memo.work.noted
|
|
691
|
+
memo.work.noted ||= work
|
|
622
692
|
return [
|
|
623
|
-
...Arr.
|
|
693
|
+
...Arr.flatMap(
|
|
624
694
|
Arr.filter(names, (kind) => !HashMap.has(domain.resources, kind)),
|
|
625
|
-
(kind
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
695
|
+
(kind: string) =>
|
|
696
|
+
here([
|
|
697
|
+
{
|
|
698
|
+
path: ["resources"],
|
|
699
|
+
issue: suggestIn(
|
|
700
|
+
memo,
|
|
701
|
+
`There is no resource with the kind "${kind}"`,
|
|
702
|
+
kind,
|
|
703
|
+
"~kinds",
|
|
704
|
+
() => kinds,
|
|
705
|
+
),
|
|
706
|
+
},
|
|
707
|
+
]),
|
|
635
708
|
),
|
|
636
|
-
...Arr.
|
|
709
|
+
...Arr.flatMap(
|
|
637
710
|
Arr.filter(
|
|
638
711
|
rule.actions,
|
|
639
712
|
(action) => action !== "*" && !actions.includes(action),
|
|
640
713
|
),
|
|
641
|
-
(action
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
? patternIssues(memo, known, field, [...at, "fields", index])
|
|
655
|
-
: fieldIssue(context, field, [...at, "fields", index]),
|
|
714
|
+
(action: string) =>
|
|
715
|
+
here([
|
|
716
|
+
{
|
|
717
|
+
path: ["actions"],
|
|
718
|
+
issue: suggestIn(
|
|
719
|
+
memo,
|
|
720
|
+
`The action "${action}" is not an action of these resources`,
|
|
721
|
+
action,
|
|
722
|
+
`~actions\u0000${names.join(",")}`,
|
|
723
|
+
() => actions,
|
|
724
|
+
),
|
|
725
|
+
},
|
|
726
|
+
]),
|
|
656
727
|
),
|
|
657
|
-
...
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
728
|
+
...fieldIssues,
|
|
729
|
+
...whenIssues,
|
|
730
|
+
// The issue of the work is not one of the most issues of the binding,
|
|
731
|
+
// so a policy past the limit always says why.
|
|
732
|
+
...(work
|
|
733
|
+
? [
|
|
734
|
+
{
|
|
735
|
+
path: at(),
|
|
736
|
+
issue: `The policy takes more than ${String(Limits.searchSteps)} steps to check: its field patterns, and the fields and values of its tests on each resource. The most is ${String(Limits.searchSteps)}.`,
|
|
737
|
+
},
|
|
738
|
+
]
|
|
739
|
+
: []),
|
|
662
740
|
]
|
|
663
741
|
}
|
|
664
742
|
|
|
@@ -721,7 +799,7 @@ export const treeIssues = (
|
|
|
721
799
|
const memo = makeMemo()
|
|
722
800
|
return Arr.flatMap(rulesOf(root, undefined, 1), (found) =>
|
|
723
801
|
Array.isArray(found)
|
|
724
|
-
? ruleIssues(domain, found[0], Place.pathOf(found[1]), memo)
|
|
802
|
+
? ruleIssues(domain, found[0], () => Place.pathOf(found[1]), memo)
|
|
725
803
|
: [found as Issue],
|
|
726
804
|
)
|
|
727
805
|
}
|
package/src/internal/catalog.ts
CHANGED
|
@@ -69,7 +69,9 @@ export const joinKinds = (left: Kinds, right: Kinds): Kinds => ({
|
|
|
69
69
|
? left.number
|
|
70
70
|
: right.number,
|
|
71
71
|
object: left.object || right.object,
|
|
72
|
-
|
|
72
|
+
// A native `Set` finds copies about ten times as fast as `Arr.dedupe`. It
|
|
73
|
+
// takes `0` and `-0` as one, as `includes` does when a test reads them.
|
|
74
|
+
literals: Array.from(new Set([...left.literals, ...right.literals])),
|
|
73
75
|
})
|
|
74
76
|
|
|
75
77
|
/** Tests if a number fits the numbers of a field. */
|
|
@@ -146,6 +148,12 @@ export interface Branch {
|
|
|
146
148
|
readonly field: Field | undefined
|
|
147
149
|
/** The places below, by name, in the order of the Schema. */
|
|
148
150
|
readonly children: ReadonlyArray<readonly [string, Branch]>
|
|
151
|
+
/**
|
|
152
|
+
* The places below, by name. A lookup gets each name from `split`, as a
|
|
153
|
+
* new string, and a `HashMap` finds a new string about 20% faster than a
|
|
154
|
+
* native `Map`. The names come from the Schema, so a name from a policy
|
|
155
|
+
* can not make a long list of names with one hash here.
|
|
156
|
+
*/
|
|
149
157
|
readonly names: HashMap.HashMap<string, Branch>
|
|
150
158
|
/** The place for the keys of a record, when the value may be a record. */
|
|
151
159
|
readonly key: Key | undefined
|
|
@@ -361,12 +369,12 @@ const nameOf = (signature: SchemaAST.PropertySignature): string | undefined =>
|
|
|
361
369
|
/**
|
|
362
370
|
* The properties of a struct by name. A walk makes it one time for each
|
|
363
371
|
* struct of a place, so a struct with very many fields does not take a
|
|
364
|
-
* search for each name.
|
|
372
|
+
* search for each name. It is a native `Map`, as `Branch.names` is.
|
|
365
373
|
*/
|
|
366
374
|
const propertiesOf = (
|
|
367
375
|
struct: SchemaAST.Objects,
|
|
368
|
-
):
|
|
369
|
-
|
|
376
|
+
): ReadonlyMap<string, SchemaAST.PropertySignature> =>
|
|
377
|
+
new Map(
|
|
370
378
|
Arr.flatMap(struct.propertySignatures, (signature) => {
|
|
371
379
|
const name = nameOf(signature)
|
|
372
380
|
return name === undefined ? [] : [[name, signature] as const]
|
|
@@ -377,25 +385,29 @@ const propertiesOf = (
|
|
|
377
385
|
const namesOf = (
|
|
378
386
|
structs: ReadonlyArray<SchemaAST.Objects>,
|
|
379
387
|
): ReadonlyArray<string> =>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
388
|
+
// A native `Set` keeps the first copy of each name, in order, about ten
|
|
389
|
+
// times as fast as `Arr.dedupe`.
|
|
390
|
+
Array.from(
|
|
391
|
+
new Set(
|
|
392
|
+
Arr.flatMap(structs, (struct) =>
|
|
393
|
+
Arr.flatMap(struct.propertySignatures, (signature) => {
|
|
394
|
+
const name = nameOf(signature)
|
|
395
|
+
if (name === undefined) {
|
|
396
|
+
return []
|
|
397
|
+
}
|
|
398
|
+
// A dot in a path is the step into a nested field. A field name with
|
|
399
|
+
// a dot would make two fields have one path, so it is a defect.
|
|
400
|
+
if (name.includes(".")) {
|
|
401
|
+
throw new Error(`A field name may not have a dot: "${name}"`)
|
|
402
|
+
}
|
|
403
|
+
// An empty name would make an empty part in a path, such as
|
|
404
|
+
// `.secret`.
|
|
405
|
+
if (name === "") {
|
|
406
|
+
throw new Error("A field name may not be empty")
|
|
407
|
+
}
|
|
408
|
+
return [name]
|
|
409
|
+
}),
|
|
410
|
+
),
|
|
399
411
|
),
|
|
400
412
|
)
|
|
401
413
|
|
|
@@ -406,15 +418,6 @@ const namesOf = (
|
|
|
406
418
|
*/
|
|
407
419
|
const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
408
420
|
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
421
|
const values = Arr.filter(
|
|
419
422
|
place.schemas,
|
|
420
423
|
(schema) => !isStruct(structOf(schema)),
|
|
@@ -476,11 +479,18 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
476
479
|
}
|
|
477
480
|
}
|
|
478
481
|
const structProperties = Arr.map(structs, propertiesOf)
|
|
479
|
-
const classProperties = Arr.map(classes, propertiesOf)
|
|
480
482
|
const children = Arr.map(namesOf(structs), (name) => {
|
|
481
|
-
const properties = Arr.flatMap(structProperties, (named) =>
|
|
482
|
-
|
|
483
|
-
|
|
483
|
+
const properties = Arr.flatMap(structProperties, (named) => {
|
|
484
|
+
const property = named.get(name)
|
|
485
|
+
return property === undefined ? [] : [property]
|
|
486
|
+
})
|
|
487
|
+
// A struct without the name may still have it as an extra field, with
|
|
488
|
+
// any value: only a decode or a `make` takes extra fields away, and a
|
|
489
|
+
// decision may get a value made by hand. A value that may be any
|
|
490
|
+
// object, as one of `Schema.Unknown` may, may have the name with any
|
|
491
|
+
// value too.
|
|
492
|
+
const top =
|
|
493
|
+
place.top || valueKinds.object || properties.length < structs.length
|
|
484
494
|
const below: Place = {
|
|
485
495
|
path: [...place.path, name],
|
|
486
496
|
schemas: Arr.flatMap(properties, (property) =>
|
|
@@ -492,15 +502,10 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
492
502
|
place.missing ||
|
|
493
503
|
values.length > 0 ||
|
|
494
504
|
Arr.some(properties, (property) => SchemaAST.isOptional(property.type)),
|
|
495
|
-
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
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)),
|
|
505
|
+
top,
|
|
506
|
+
// A value that may be anything may be a list, or have a getter, so
|
|
507
|
+
// a read below it may not find plain data.
|
|
508
|
+
plain: place.plain && !top,
|
|
504
509
|
}
|
|
505
510
|
return [name, branchOf(below, count)] as const
|
|
506
511
|
})
|
|
@@ -509,6 +514,8 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
509
514
|
if (signatures.length === 0) {
|
|
510
515
|
return { field, children, names, key: undefined, keyBranch: undefined }
|
|
511
516
|
}
|
|
517
|
+
const keyTop =
|
|
518
|
+
place.top || valueKinds.object || records.length < structs.length
|
|
512
519
|
const key: Key = {
|
|
513
520
|
_tag: "Key",
|
|
514
521
|
parameters: Arr.map(signatures, (signature) => signature.parameter),
|
|
@@ -521,11 +528,8 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
521
528
|
),
|
|
522
529
|
// A record may not have a given key.
|
|
523
530
|
missing: true,
|
|
524
|
-
top:
|
|
525
|
-
plain:
|
|
526
|
-
place.plain &&
|
|
527
|
-
!valueKinds.object &&
|
|
528
|
-
Arr.every(classes, (struct) => struct.indexSignatures.length > 0),
|
|
531
|
+
top: keyTop,
|
|
532
|
+
plain: place.plain && !keyTop,
|
|
529
533
|
},
|
|
530
534
|
count,
|
|
531
535
|
)
|
package/src/internal/json.ts
CHANGED
|
@@ -143,15 +143,6 @@ const decodeOperands = (
|
|
|
143
143
|
decodeOperand(operand, [...path, name, index]),
|
|
144
144
|
)
|
|
145
145
|
|
|
146
|
-
const compareOperators: ReadonlyArray<string> = [
|
|
147
|
-
"eq",
|
|
148
|
-
"ne",
|
|
149
|
-
"lt",
|
|
150
|
-
"lte",
|
|
151
|
-
"gt",
|
|
152
|
-
"gte",
|
|
153
|
-
]
|
|
154
|
-
|
|
155
146
|
const decodeNode = (
|
|
156
147
|
input: unknown,
|
|
157
148
|
path: ReadonlyArray<PropertyKey>,
|
|
@@ -199,11 +190,11 @@ const makeNode = (
|
|
|
199
190
|
return Nodes.never
|
|
200
191
|
case "Compare": {
|
|
201
192
|
const op = readString(input, "op", path)
|
|
202
|
-
if (!
|
|
193
|
+
if (!Nodes.isCompareOperator(op)) {
|
|
203
194
|
throw problem([...path, "op"], `"${op}" is not a compare operator`)
|
|
204
195
|
}
|
|
205
196
|
return new Nodes.Compare({
|
|
206
|
-
op
|
|
197
|
+
op,
|
|
207
198
|
field: readString(input, "field", path),
|
|
208
199
|
value: decodeOperand(read(input, "value", path), [...path, "value"]),
|
|
209
200
|
})
|