@nmnmcc/ability 2.0.5 → 2.0.6
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/dist/Condition.d.ts +4 -4
- 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 +28 -6
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +37 -14
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +7 -8
- 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 +12 -9
- 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 +4 -1
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +17 -7
- 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 +13 -4
- 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/package.json +1 -1
- package/src/Condition.ts +5 -2
- package/src/Policy.ts +1 -2
- package/src/internal/algebra.ts +44 -8
- package/src/internal/authorizer.ts +56 -14
- package/src/internal/bind.ts +18 -14
- package/src/internal/catalog.ts +40 -27
- package/src/internal/json.ts +2 -11
- package/src/internal/nodes.ts +21 -11
- package/src/internal/patterns.ts +7 -3
- package/src/internal/policy-json.ts +2 -9
- package/src/internal/policy-nodes.ts +14 -6
- package/src/internal/search.ts +13 -10
|
@@ -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,36 @@ class PrincipalKey implements Equal.Equal {
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* tells them apart. So two principals with one key are equal, and the cache
|
|
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, in the order that a
|
|
137
|
+
* loop sees them. `Equal` does not see this order, but a loader may read it,
|
|
138
|
+
* as `Object.values` does. So two keys need the same order too. A scalar has
|
|
139
|
+
* no parts, and `Equal` compares it.
|
|
133
140
|
*/
|
|
141
|
+
const orderOf = (value: unknown): string => {
|
|
142
|
+
if (typeof value !== "object" || value === null) {
|
|
143
|
+
return ""
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
return `[${value.map(orderOf).join(",")}]`
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
value instanceof Map ||
|
|
150
|
+
value instanceof Set ||
|
|
151
|
+
HashMap.isHashMap(value) ||
|
|
152
|
+
HashSet.isHashSet(value) ||
|
|
153
|
+
Chunk.isChunk(value)
|
|
154
|
+
) {
|
|
155
|
+
return `<${Array.from(value as Iterable<unknown>, orderOf).join(",")}>`
|
|
156
|
+
}
|
|
157
|
+
return `{${Object.keys(value)
|
|
158
|
+
.map(
|
|
159
|
+
(name) =>
|
|
160
|
+
`${JSON.stringify(name)}:${orderOf((value as Record<string, unknown>)[name])}`,
|
|
161
|
+
)
|
|
162
|
+
.join(",")}}`
|
|
163
|
+
}
|
|
164
|
+
|
|
134
165
|
/**
|
|
135
166
|
* Gives the JSON codec of a Schema, or `undefined` when the Schema has no JSON
|
|
136
167
|
* form, as a struct with a symbol key has none.
|
|
@@ -148,6 +179,17 @@ const jsonCodecOf = (
|
|
|
148
179
|
}
|
|
149
180
|
}
|
|
150
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Makes the cache key of a principal from its JSON form. The key keeps the
|
|
184
|
+
* principal that the Schema decodes from that form, so the loader sees only
|
|
185
|
+
* the data that the key shows. A principal gets no key when it does not
|
|
186
|
+
* encode, or when the decoded value is not `Equal` to it. Two classes, or a
|
|
187
|
+
* string and a number, may have one JSON form, as in a union, and `Equal`
|
|
188
|
+
* tells them apart. A Schema may make data when it decodes, as the order of
|
|
189
|
+
* the fields of a record, so the key keeps that order too. So two
|
|
190
|
+
* principals with one key are equal, and the cache can not give one the
|
|
191
|
+
* policy of the other.
|
|
192
|
+
*/
|
|
151
193
|
const jsonKey = (
|
|
152
194
|
schema: Schema.Top,
|
|
153
195
|
): ((principal: object) => PrincipalKey | undefined) => {
|
|
@@ -170,7 +212,7 @@ const jsonKey = (
|
|
|
170
212
|
onFailure: () => undefined,
|
|
171
213
|
onSuccess: (decoded) =>
|
|
172
214
|
Equal.equals(decoded, principal)
|
|
173
|
-
? new PrincipalKey(decoded as object, text,
|
|
215
|
+
? new PrincipalKey(decoded as object, text, orderOf(decoded))
|
|
174
216
|
: undefined,
|
|
175
217
|
})
|
|
176
218
|
},
|
|
@@ -236,7 +278,7 @@ export const makeLayer = (
|
|
|
236
278
|
const text = custom(principal)
|
|
237
279
|
return text === undefined
|
|
238
280
|
? undefined
|
|
239
|
-
: new PrincipalKey(canonical?.principal ?? principal, text,
|
|
281
|
+
: new PrincipalKey(canonical?.principal ?? principal, text, undefined)
|
|
240
282
|
}
|
|
241
283
|
return Layer.effect(
|
|
242
284
|
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
|
|
|
@@ -590,9 +592,10 @@ export const ruleIssues = (
|
|
|
590
592
|
memo: Memo = makeMemo(),
|
|
591
593
|
): ReadonlyArray<Issue> => {
|
|
592
594
|
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
|
-
//
|
|
595
|
+
// Each kind is checked one time, also when the list names it again. The
|
|
596
|
+
// names come from a policy, so a native `Set` finds the copies:
|
|
597
|
+
// `Arr.dedupe` puts the names in groups by the hash of Effect, which has
|
|
598
|
+
// no random seed, and very many names with one hash make one long group.
|
|
596
599
|
const names = Array.from(
|
|
597
600
|
new Set(Arr.filter(rule.resources, (kind) => kind !== "*")),
|
|
598
601
|
)
|
|
@@ -616,8 +619,9 @@ export const ruleIssues = (
|
|
|
616
619
|
? Arr.filter(known, (descriptor) => hasAction(descriptor, rule.actions))
|
|
617
620
|
: applicable,
|
|
618
621
|
}
|
|
619
|
-
|
|
620
|
-
|
|
622
|
+
// A native `Set` finds copies about ten times as fast as `Arr.dedupe`.
|
|
623
|
+
const actions = Array.from(
|
|
624
|
+
new Set(Arr.flatMap(known, (descriptor) => descriptor.actions)),
|
|
621
625
|
)
|
|
622
626
|
return [
|
|
623
627
|
...Arr.map(
|
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
|
|
|
@@ -478,9 +490,10 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
478
490
|
const structProperties = Arr.map(structs, propertiesOf)
|
|
479
491
|
const classProperties = Arr.map(classes, propertiesOf)
|
|
480
492
|
const children = Arr.map(namesOf(structs), (name) => {
|
|
481
|
-
const properties = Arr.flatMap(structProperties, (named) =>
|
|
482
|
-
|
|
483
|
-
|
|
493
|
+
const properties = Arr.flatMap(structProperties, (named) => {
|
|
494
|
+
const property = named.get(name)
|
|
495
|
+
return property === undefined ? [] : [property]
|
|
496
|
+
})
|
|
484
497
|
const below: Place = {
|
|
485
498
|
path: [...place.path, name],
|
|
486
499
|
schemas: Arr.flatMap(properties, (property) =>
|
|
@@ -500,7 +513,7 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
500
513
|
plain:
|
|
501
514
|
place.plain &&
|
|
502
515
|
!valueKinds.object &&
|
|
503
|
-
Arr.every(classProperties, (named) =>
|
|
516
|
+
Arr.every(classProperties, (named) => named.has(name)),
|
|
504
517
|
}
|
|
505
518
|
return [name, branchOf(below, count)] as const
|
|
506
519
|
})
|
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
|
})
|
package/src/internal/nodes.ts
CHANGED
|
@@ -21,12 +21,31 @@ import * as Arr from "effect/Array"
|
|
|
21
21
|
import * as Data from "effect/Data"
|
|
22
22
|
import type * as Effect from "effect/Effect"
|
|
23
23
|
import { hasProperty } from "effect/Predicate"
|
|
24
|
+
import * as Schema from "effect/Schema"
|
|
24
25
|
import * as Limits from "./limits.ts"
|
|
25
26
|
|
|
26
27
|
/** A value that a condition can compare. */
|
|
27
28
|
export type Scalar = string | number | boolean | null
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
/** The operators of a `Compare` test. The decoders read this list too. */
|
|
31
|
+
export const CompareOperator = Schema.Literals([
|
|
32
|
+
"eq",
|
|
33
|
+
"ne",
|
|
34
|
+
"lt",
|
|
35
|
+
"lte",
|
|
36
|
+
"gt",
|
|
37
|
+
"gte",
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
export type CompareOperator = typeof CompareOperator.Type
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tests if a value is an operator. It reads the list of the Schema with
|
|
44
|
+
* `includes`: for a short list, this is about twice as fast as
|
|
45
|
+
* `Schema.is`, and a decoder tests each node of a large input.
|
|
46
|
+
*/
|
|
47
|
+
export const isCompareOperator = (u: unknown): u is CompareOperator =>
|
|
48
|
+
(CompareOperator.literals as ReadonlyArray<unknown>).includes(u)
|
|
30
49
|
|
|
31
50
|
/** A fixed value. */
|
|
32
51
|
export interface Literal {
|
|
@@ -49,15 +68,6 @@ export const literal = (value: Scalar): Literal => operands.Literal({ value })
|
|
|
49
68
|
export const principalRef = (path: string): PrincipalRef =>
|
|
50
69
|
operands.Principal({ path })
|
|
51
70
|
|
|
52
|
-
const compareOperators: ReadonlyArray<string> = [
|
|
53
|
-
"eq",
|
|
54
|
-
"ne",
|
|
55
|
-
"lt",
|
|
56
|
-
"lte",
|
|
57
|
-
"gt",
|
|
58
|
-
"gte",
|
|
59
|
-
]
|
|
60
|
-
|
|
61
71
|
/** Tests if a value is a scalar. Any number is one, also `NaN`. */
|
|
62
72
|
export const isScalar = (value: unknown): value is Scalar =>
|
|
63
73
|
value === null ||
|
|
@@ -211,7 +221,7 @@ export class Compare extends Base {
|
|
|
211
221
|
readonly field: string
|
|
212
222
|
readonly value: Operand
|
|
213
223
|
}) {
|
|
214
|
-
if (!
|
|
224
|
+
if (!isCompareOperator(props.op)) {
|
|
215
225
|
throw new Error(`"${String(props.op)}" is not a compare operator`)
|
|
216
226
|
}
|
|
217
227
|
super(
|
package/src/internal/patterns.ts
CHANGED
|
@@ -117,9 +117,13 @@ type States = ReadonlyArray<number>
|
|
|
117
117
|
|
|
118
118
|
/** Adds the place after each `**`, since a `**` may match no names. */
|
|
119
119
|
const close = (pattern: Pattern, states: States): States =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
// A native `Set` keeps the first copy of each place, in order. It is
|
|
121
|
+
// faster than `Arr.dedupe`, and this is on the path of each match.
|
|
122
|
+
Array.from(
|
|
123
|
+
new Set(
|
|
124
|
+
Arr.flatMap(states, (state) =>
|
|
125
|
+
pattern[state]?._tag === "Globstar" ? [state, state + 1] : [state],
|
|
126
|
+
),
|
|
123
127
|
),
|
|
124
128
|
)
|
|
125
129
|
|
|
@@ -136,13 +136,6 @@ export const encode = (
|
|
|
136
136
|
return toJson(node, (rule) => forms.get(rule))
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
-
const algorithms: ReadonlyArray<string> = [
|
|
140
|
-
"denyOverrides",
|
|
141
|
-
"permitOverrides",
|
|
142
|
-
"firstApplicable",
|
|
143
|
-
"lastApplicable",
|
|
144
|
-
]
|
|
145
|
-
|
|
146
139
|
/** Reads a list of strings. */
|
|
147
140
|
const readStrings = (
|
|
148
141
|
input: object,
|
|
@@ -374,7 +367,7 @@ const readTree = (
|
|
|
374
367
|
switch (tag) {
|
|
375
368
|
case "Rules": {
|
|
376
369
|
const algorithm = Json.readString(input, "algorithm", path)
|
|
377
|
-
if (!
|
|
370
|
+
if (!PolicyNodes.isAlgorithm(algorithm)) {
|
|
378
371
|
throw Json.problem(
|
|
379
372
|
[...path, "algorithm"],
|
|
380
373
|
`"${algorithm}" is not a combining algorithm`,
|
|
@@ -392,7 +385,7 @@ const readTree = (
|
|
|
392
385
|
_tag: "Rules",
|
|
393
386
|
place,
|
|
394
387
|
size: 1 + found.length,
|
|
395
|
-
algorithm
|
|
388
|
+
algorithm,
|
|
396
389
|
rules: found,
|
|
397
390
|
})
|
|
398
391
|
}
|
|
@@ -11,19 +11,27 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as Arr from "effect/Array"
|
|
13
13
|
import * as Data from "effect/Data"
|
|
14
|
+
import * as Schema from "effect/Schema"
|
|
14
15
|
import * as Rule from "./rule.ts"
|
|
15
16
|
import * as Limits from "./limits.ts"
|
|
16
17
|
import * as Nodes from "./nodes.ts"
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const algorithms: ReadonlyArray<string> = [
|
|
19
|
+
/** The combining algorithms of a list of rules. The decoder reads this list too. */
|
|
20
|
+
export const Algorithm = Schema.Literals([
|
|
22
21
|
"denyOverrides",
|
|
23
22
|
"permitOverrides",
|
|
24
23
|
"firstApplicable",
|
|
25
24
|
"lastApplicable",
|
|
26
|
-
]
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
export type Algorithm = typeof Algorithm.Type
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tests if a value is a combining algorithm. It reads the list of the Schema
|
|
31
|
+
* with `includes`, as `Nodes.isCompareOperator` does.
|
|
32
|
+
*/
|
|
33
|
+
export const isAlgorithm = (u: unknown): u is Algorithm =>
|
|
34
|
+
(Algorithm.literals as ReadonlyArray<unknown>).includes(u)
|
|
27
35
|
|
|
28
36
|
interface Facts {
|
|
29
37
|
readonly size: number
|
|
@@ -103,7 +111,7 @@ export class Rules extends Base {
|
|
|
103
111
|
readonly algorithm: Algorithm
|
|
104
112
|
readonly rules: ReadonlyArray<Rule.Rule>
|
|
105
113
|
}) {
|
|
106
|
-
if (!
|
|
114
|
+
if (!isAlgorithm(props.algorithm)) {
|
|
107
115
|
throw new Error(
|
|
108
116
|
`"${String(props.algorithm)}" is not a combining algorithm`,
|
|
109
117
|
)
|
package/src/internal/search.ts
CHANGED
|
@@ -196,12 +196,13 @@ interface Search {
|
|
|
196
196
|
*/
|
|
197
197
|
readonly members: Map<Nodes.In, Set<string>>
|
|
198
198
|
/**
|
|
199
|
-
* The
|
|
200
|
-
*
|
|
201
|
-
* fields, so its answer is the same. The set only grows, so the steps
|
|
202
|
-
* search may share it. It is a native `Set`, as in
|
|
199
|
+
* The keys of the conditions that the search found to be unsatisfiable:
|
|
200
|
+
* see `Algebra.keyOf`. A condition with the same structure has the same
|
|
201
|
+
* free fields, so its answer is the same. The set only grows, so the steps
|
|
202
|
+
* of a search may share it. It is a native `Set`, as in
|
|
203
|
+
* {@link dedupeCells}.
|
|
203
204
|
*/
|
|
204
|
-
readonly failed: Set<string>
|
|
205
|
+
readonly failed: Set<string | Node>
|
|
205
206
|
readonly budget: number
|
|
206
207
|
}
|
|
207
208
|
|
|
@@ -364,10 +365,12 @@ const searchNode = (
|
|
|
364
365
|
if (node._tag === "False") {
|
|
365
366
|
return { assignment: Option.none(), state }
|
|
366
367
|
}
|
|
367
|
-
// The
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
// The key reads at most a small part of the node: a node with very many
|
|
369
|
+
// nodes, or with very many values in its `In` tests, is its own key. A
|
|
370
|
+
// step reads the nodes but not the values, which `membersOf` keeps, so the
|
|
371
|
+
// budget counts the size.
|
|
372
|
+
const key = Algebra.keyOf(node)
|
|
373
|
+
if (search.failed.has(key)) {
|
|
371
374
|
return { assignment: Option.none(), state }
|
|
372
375
|
}
|
|
373
376
|
if (state.steps + node.size > search.budget) {
|
|
@@ -400,7 +403,7 @@ const searchNode = (
|
|
|
400
403
|
},
|
|
401
404
|
)
|
|
402
405
|
if (Option.isNone(result.assignment)) {
|
|
403
|
-
search.failed.add(
|
|
406
|
+
search.failed.add(key)
|
|
404
407
|
}
|
|
405
408
|
return result
|
|
406
409
|
}
|