@nmnmcc/ability 1.0.1 → 2.0.1
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 +112 -69
- package/dist/Authorizer.d.ts +43 -47
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js +5 -290
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +164 -332
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +75 -890
- package/dist/Condition.js.map +1 -1
- package/dist/Decision.d.ts +13 -13
- package/dist/Decision.d.ts.map +1 -1
- package/dist/Decision.js.map +1 -1
- package/dist/Domain.d.ts +10 -21
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +4 -62
- package/dist/Domain.js.map +1 -1
- package/dist/HttpApi.d.ts +31 -7
- package/dist/HttpApi.d.ts.map +1 -1
- package/dist/HttpApi.js +25 -8
- package/dist/HttpApi.js.map +1 -1
- package/dist/Limits.d.ts +114 -0
- package/dist/Limits.d.ts.map +1 -0
- package/dist/Limits.js +132 -0
- package/dist/Limits.js.map +1 -0
- package/dist/Policy.d.ts +276 -269
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +73 -1177
- package/dist/Policy.js.map +1 -1
- package/dist/Resource.d.ts +4 -177
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +4 -803
- package/dist/Resource.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -5
- package/dist/internal/algebra.js +267 -0
- package/dist/internal/algebra.js.map +1 -0
- package/dist/internal/analysis.js +245 -0
- package/dist/internal/analysis.js.map +1 -0
- package/dist/internal/authorizer.d.ts +30 -0
- package/dist/internal/authorizer.d.ts.map +1 -0
- package/dist/internal/authorizer.js +221 -0
- package/dist/internal/authorizer.js.map +1 -0
- package/dist/internal/bind.js +311 -0
- package/dist/internal/bind.js.map +1 -0
- package/dist/internal/catalog.d.ts +56 -0
- package/dist/internal/catalog.d.ts.map +1 -0
- package/dist/internal/catalog.js +292 -0
- package/dist/internal/catalog.js.map +1 -0
- package/dist/internal/decide.js +166 -0
- package/dist/internal/decide.js.map +1 -0
- package/dist/internal/domain.js +32 -0
- package/dist/internal/domain.js.map +1 -0
- package/dist/internal/json.js +134 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/limits.d.ts +14 -0
- package/dist/internal/limits.d.ts.map +1 -0
- package/dist/internal/limits.js +43 -0
- package/dist/internal/limits.js.map +1 -0
- package/dist/internal/nodes.d.ts +121 -0
- package/dist/internal/nodes.d.ts.map +1 -0
- package/dist/internal/nodes.js +277 -0
- package/dist/internal/nodes.js.map +1 -0
- package/dist/internal/patterns.js +110 -0
- package/dist/internal/patterns.js.map +1 -0
- package/dist/internal/policy-json.js +265 -0
- package/dist/internal/policy-json.js.map +1 -0
- package/dist/internal/policy-nodes.d.ts +61 -0
- package/dist/internal/policy-nodes.d.ts.map +1 -0
- package/dist/internal/policy-nodes.js +146 -0
- package/dist/internal/policy-nodes.js.map +1 -0
- package/dist/internal/policy.d.ts +42 -0
- package/dist/internal/policy.d.ts.map +1 -0
- package/dist/internal/policy.js +124 -0
- package/dist/internal/policy.js.map +1 -0
- package/dist/internal/read.js +39 -0
- package/dist/internal/read.js.map +1 -0
- package/dist/internal/resource.js +115 -0
- package/dist/internal/resource.js.map +1 -0
- package/dist/internal/rule.d.ts +26 -0
- package/dist/internal/rule.d.ts.map +1 -0
- package/dist/internal/rule.js +103 -0
- package/dist/internal/rule.js.map +1 -0
- package/dist/internal/search.d.ts +22 -0
- package/dist/internal/search.d.ts.map +1 -0
- package/dist/internal/search.js +274 -0
- package/dist/internal/search.js.map +1 -0
- package/dist/internal/suggest.js +46 -0
- package/dist/internal/suggest.js.map +1 -0
- package/package.json +3 -4
- package/src/Authorizer.ts +67 -583
- package/src/Condition.ts +339 -2095
- package/src/Decision.ts +6 -5
- package/src/Domain.ts +12 -69
- package/src/HttpApi.ts +72 -10
- package/src/Limits.ts +132 -0
- package/src/Policy.ts +391 -2772
- package/src/Resource.ts +7 -1548
- package/src/index.ts +3 -9
- package/src/internal/algebra.ts +482 -0
- package/src/internal/analysis.ts +478 -0
- package/src/internal/authorizer.ts +418 -0
- package/src/internal/bind.ts +648 -0
- package/src/internal/catalog.ts +534 -0
- package/src/internal/decide.ts +402 -0
- package/src/internal/domain.ts +66 -0
- package/src/internal/json.ts +260 -0
- package/src/internal/limits.ts +78 -0
- package/src/internal/nodes.ts +404 -0
- package/src/internal/patterns.ts +211 -0
- package/src/internal/policy-json.ts +487 -0
- package/src/internal/policy-nodes.ts +207 -0
- package/src/internal/policy.ts +264 -0
- package/src/internal/read.ts +55 -0
- package/src/internal/resource.ts +180 -0
- package/src/internal/rule.ts +196 -0
- package/src/internal/search.ts +541 -0
- package/src/internal/suggest.ts +104 -0
- package/dist/Principal.d.ts +0 -35
- package/dist/Principal.d.ts.map +0 -1
- package/dist/Principal.js +0 -37
- package/dist/Principal.js.map +0 -1
- package/dist/Rule.d.ts +0 -275
- package/dist/Rule.d.ts.map +0 -1
- package/dist/Rule.js +0 -257
- package/dist/Rule.js.map +0 -1
- package/src/Principal.ts +0 -41
- package/src/Rule.ts +0 -683
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The nodes of a condition. Each node is a class, and only its constructor
|
|
3
|
+
* makes one, also when a condition comes from JSON. The constructor checks the
|
|
4
|
+
* data of the node and works out three facts once:
|
|
5
|
+
* - `size`: the number of nodes, with a shared child counted for each place;
|
|
6
|
+
* - `depth`: the number of levels;
|
|
7
|
+
* - `opaque`: the node has a predicate.
|
|
8
|
+
*
|
|
9
|
+
* The constructor stops a node that goes past `conditionNodes` or
|
|
10
|
+
* `conditionDepth`. So a walk over any condition visits at most
|
|
11
|
+
* `conditionNodes` nodes and goes at most `conditionDepth` levels down, and it
|
|
12
|
+
* needs no memo.
|
|
13
|
+
*
|
|
14
|
+
* The facts are private fields, so `Equal` and `Hash` do not read them: two
|
|
15
|
+
* nodes with the same data are equal.
|
|
16
|
+
*/
|
|
17
|
+
// A predicate gives an Effect with an error type and a service type that the
|
|
18
|
+
// nodes do not know, so they are `unknown` here.
|
|
19
|
+
// @effect-diagnostics anyUnknownInErrorContext:off
|
|
20
|
+
import * as Arr from "effect/Array"
|
|
21
|
+
import * as Data from "effect/Data"
|
|
22
|
+
import type * as Effect from "effect/Effect"
|
|
23
|
+
import { hasProperty } from "effect/Predicate"
|
|
24
|
+
import * as Limits from "./limits.ts"
|
|
25
|
+
|
|
26
|
+
/** A value that a condition can compare. */
|
|
27
|
+
export type Scalar = string | number | boolean | null
|
|
28
|
+
|
|
29
|
+
export type CompareOperator = "eq" | "ne" | "lt" | "lte" | "gt" | "gte"
|
|
30
|
+
|
|
31
|
+
/** A fixed value. */
|
|
32
|
+
export interface Literal {
|
|
33
|
+
readonly _tag: "Literal"
|
|
34
|
+
readonly value: Scalar
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A field of the principal. */
|
|
38
|
+
export interface PrincipalRef {
|
|
39
|
+
readonly _tag: "Principal"
|
|
40
|
+
readonly path: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type Operand = Literal | PrincipalRef
|
|
44
|
+
|
|
45
|
+
const operands = Data.taggedEnum<Operand>()
|
|
46
|
+
|
|
47
|
+
export const literal = (value: Scalar): Literal => operands.Literal({ value })
|
|
48
|
+
|
|
49
|
+
export const principalRef = (path: string): PrincipalRef =>
|
|
50
|
+
operands.Principal({ path })
|
|
51
|
+
|
|
52
|
+
const compareOperators: ReadonlyArray<string> = [
|
|
53
|
+
"eq",
|
|
54
|
+
"ne",
|
|
55
|
+
"lt",
|
|
56
|
+
"lte",
|
|
57
|
+
"gt",
|
|
58
|
+
"gte",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
/** Tests if a value is a scalar. Any number is one, also `NaN`. */
|
|
62
|
+
export const isScalar = (value: unknown): value is Scalar =>
|
|
63
|
+
value === null ||
|
|
64
|
+
typeof value === "string" ||
|
|
65
|
+
typeof value === "number" ||
|
|
66
|
+
typeof value === "boolean"
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Checks a field path: a string with at most `fieldPathParts` parts. A longer
|
|
70
|
+
* path is never a field of a resource or of the principal.
|
|
71
|
+
*/
|
|
72
|
+
const checkPath = (path: unknown): string => {
|
|
73
|
+
if (typeof path !== "string") {
|
|
74
|
+
throw new Error("A field path must be a string")
|
|
75
|
+
}
|
|
76
|
+
const parts = path.split(".", Limits.fieldPathParts + 1).length
|
|
77
|
+
if (parts > Limits.fieldPathParts) {
|
|
78
|
+
throw Limits.exceeded(
|
|
79
|
+
"A field path",
|
|
80
|
+
"fieldPathParts",
|
|
81
|
+
"parts",
|
|
82
|
+
parts,
|
|
83
|
+
Limits.fieldPathParts,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
return path
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Checks an operand, and gives a new one with only its data. */
|
|
90
|
+
const checkOperand = (operand: unknown): Operand => {
|
|
91
|
+
if (hasProperty(operand, "_tag")) {
|
|
92
|
+
if (
|
|
93
|
+
operand._tag === "Literal" &&
|
|
94
|
+
hasProperty(operand, "value") &&
|
|
95
|
+
isScalar(operand.value)
|
|
96
|
+
) {
|
|
97
|
+
return literal(operand.value)
|
|
98
|
+
}
|
|
99
|
+
if (operand._tag === "Principal" && hasProperty(operand, "path")) {
|
|
100
|
+
return principalRef(checkPath(operand.path))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw new Error(
|
|
104
|
+
"An operand must be a fixed scalar value or a field of the principal",
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const checkOperands = (values: unknown): ReadonlyArray<Operand> => {
|
|
109
|
+
if (!Array.isArray(values)) {
|
|
110
|
+
throw new Error("A list of operands must be an array")
|
|
111
|
+
}
|
|
112
|
+
return Object.freeze(Arr.map(Array.from(values), checkOperand))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface Facts {
|
|
116
|
+
readonly size: number
|
|
117
|
+
readonly depth: number
|
|
118
|
+
readonly opaque: boolean
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const leaf: Facts = { size: 1, depth: 1, opaque: false }
|
|
122
|
+
|
|
123
|
+
/** The facts of a node with children. It stops a node past the limits. */
|
|
124
|
+
const factsOf = (children: ReadonlyArray<Node>): Facts => {
|
|
125
|
+
const size = Arr.reduce(children, 1, (total, child) => total + child.size)
|
|
126
|
+
if (size > Limits.conditionNodes) {
|
|
127
|
+
throw Limits.exceeded(
|
|
128
|
+
"A condition",
|
|
129
|
+
"conditionNodes",
|
|
130
|
+
"nodes",
|
|
131
|
+
size,
|
|
132
|
+
Limits.conditionNodes,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
const depth =
|
|
136
|
+
1 + Arr.reduce(children, 0, (top, child) => Math.max(top, child.depth))
|
|
137
|
+
if (depth > Limits.conditionDepth) {
|
|
138
|
+
throw Limits.exceeded(
|
|
139
|
+
"A condition",
|
|
140
|
+
"conditionDepth",
|
|
141
|
+
"levels",
|
|
142
|
+
depth,
|
|
143
|
+
Limits.conditionDepth,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
size,
|
|
148
|
+
depth,
|
|
149
|
+
opaque: Arr.some(children, (child) => child.opaque),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The base of every node. It keeps the facts. */
|
|
154
|
+
abstract class Base extends Data.Class {
|
|
155
|
+
/**
|
|
156
|
+
* The JSON form of the node, for `JSON.stringify`. A number that JSON can
|
|
157
|
+
* not hold, as `Infinity`, becomes an object that no decoder takes, in
|
|
158
|
+
* place of `null`, which would change the test.
|
|
159
|
+
*/
|
|
160
|
+
toJSON(): unknown {
|
|
161
|
+
return toJson(this as unknown as Node)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
readonly #facts: Facts
|
|
165
|
+
|
|
166
|
+
constructor(props: object, facts: Facts) {
|
|
167
|
+
super(props as never)
|
|
168
|
+
this.#facts = facts
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The number of nodes, with a shared child counted for each place. */
|
|
172
|
+
get size(): number {
|
|
173
|
+
return this.#facts.size
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The number of levels. */
|
|
177
|
+
get depth(): number {
|
|
178
|
+
return this.#facts.depth
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The node has a predicate. */
|
|
182
|
+
get opaque(): boolean {
|
|
183
|
+
return this.#facts.opaque
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Always true. */
|
|
188
|
+
export class True extends Base {
|
|
189
|
+
readonly _tag = "True"
|
|
190
|
+
constructor() {
|
|
191
|
+
super({}, leaf)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Always false. */
|
|
196
|
+
export class False extends Base {
|
|
197
|
+
readonly _tag = "False"
|
|
198
|
+
constructor() {
|
|
199
|
+
super({}, leaf)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Compares a field with a value. */
|
|
204
|
+
export class Compare extends Base {
|
|
205
|
+
readonly _tag = "Compare"
|
|
206
|
+
declare readonly op: CompareOperator
|
|
207
|
+
declare readonly field: string
|
|
208
|
+
declare readonly value: Operand
|
|
209
|
+
constructor(props: {
|
|
210
|
+
readonly op: CompareOperator
|
|
211
|
+
readonly field: string
|
|
212
|
+
readonly value: Operand
|
|
213
|
+
}) {
|
|
214
|
+
if (!compareOperators.includes(props.op)) {
|
|
215
|
+
throw new Error(`"${String(props.op)}" is not a compare operator`)
|
|
216
|
+
}
|
|
217
|
+
super(
|
|
218
|
+
{
|
|
219
|
+
op: props.op,
|
|
220
|
+
field: checkPath(props.field),
|
|
221
|
+
value: checkOperand(props.value),
|
|
222
|
+
},
|
|
223
|
+
leaf,
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** True when a field is equal to one of the values. */
|
|
229
|
+
export class In extends Base {
|
|
230
|
+
readonly _tag = "In"
|
|
231
|
+
declare readonly field: string
|
|
232
|
+
declare readonly values: ReadonlyArray<Operand>
|
|
233
|
+
constructor(props: {
|
|
234
|
+
readonly field: string
|
|
235
|
+
readonly values: ReadonlyArray<Operand>
|
|
236
|
+
}) {
|
|
237
|
+
super(
|
|
238
|
+
{ field: checkPath(props.field), values: checkOperands(props.values) },
|
|
239
|
+
leaf,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** True when a field is not `null` and not missing. */
|
|
245
|
+
export class Exists extends Base {
|
|
246
|
+
readonly _tag = "Exists"
|
|
247
|
+
declare readonly field: string
|
|
248
|
+
constructor(props: { readonly field: string }) {
|
|
249
|
+
super({ field: checkPath(props.field) }, leaf)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** True when the path of the resource starts with the prefix. */
|
|
254
|
+
export class Under extends Base {
|
|
255
|
+
readonly _tag = "Under"
|
|
256
|
+
declare readonly prefix: ReadonlyArray<Operand>
|
|
257
|
+
constructor(props: { readonly prefix: ReadonlyArray<Operand> }) {
|
|
258
|
+
super({ prefix: checkOperands(props.prefix) }, leaf)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const checkChildren = (conditions: unknown): ReadonlyArray<Node> => {
|
|
263
|
+
if (!Array.isArray(conditions)) {
|
|
264
|
+
throw new Error("The children of a node must be an array")
|
|
265
|
+
}
|
|
266
|
+
const children = Array.from(conditions as ReadonlyArray<unknown>)
|
|
267
|
+
if (!Arr.every(children, isNode)) {
|
|
268
|
+
throw notNode()
|
|
269
|
+
}
|
|
270
|
+
return Object.freeze(children)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export class And extends Base {
|
|
274
|
+
readonly _tag = "And"
|
|
275
|
+
declare readonly conditions: ReadonlyArray<Node>
|
|
276
|
+
constructor(conditions: ReadonlyArray<Node>) {
|
|
277
|
+
const children = checkChildren(conditions)
|
|
278
|
+
super({ conditions: children }, factsOf(children))
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export class Or extends Base {
|
|
283
|
+
readonly _tag = "Or"
|
|
284
|
+
declare readonly conditions: ReadonlyArray<Node>
|
|
285
|
+
constructor(conditions: ReadonlyArray<Node>) {
|
|
286
|
+
const children = checkChildren(conditions)
|
|
287
|
+
super({ conditions: children }, factsOf(children))
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export class Not extends Base {
|
|
292
|
+
readonly _tag = "Not"
|
|
293
|
+
declare readonly condition: Node
|
|
294
|
+
constructor(condition: Node) {
|
|
295
|
+
if (!isNode(condition)) {
|
|
296
|
+
throw notNode()
|
|
297
|
+
}
|
|
298
|
+
super({ condition }, factsOf([condition]))
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The test of a predicate, with no types. */
|
|
303
|
+
export type Run = (
|
|
304
|
+
value: unknown,
|
|
305
|
+
principal: unknown,
|
|
306
|
+
) => Effect.Effect<boolean, unknown, unknown>
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* A test that runs an `Effect`. Its test takes inputs of the type `never`, so
|
|
310
|
+
* no code can call it with a value of the wrong type: `Condition.evaluate`
|
|
311
|
+
* checks the input. Use {@link runOf} inside the library.
|
|
312
|
+
*/
|
|
313
|
+
export class Predicate extends Base {
|
|
314
|
+
readonly _tag = "Predicate"
|
|
315
|
+
declare readonly name: string
|
|
316
|
+
declare readonly run: (
|
|
317
|
+
value: never,
|
|
318
|
+
principal: never,
|
|
319
|
+
) => Effect.Effect<boolean, unknown, unknown>
|
|
320
|
+
constructor(props: { readonly name: string; readonly run: Run }) {
|
|
321
|
+
if (typeof props.name !== "string" || typeof props.run !== "function") {
|
|
322
|
+
throw new Error("A predicate needs a name and a test")
|
|
323
|
+
}
|
|
324
|
+
super(
|
|
325
|
+
{ name: props.name, run: props.run },
|
|
326
|
+
{ size: 1, depth: 1, opaque: true },
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Gives the test of a predicate, with no types. */
|
|
332
|
+
export const runOf = (node: Predicate): Run => node.run as Run
|
|
333
|
+
|
|
334
|
+
/** A node of a condition. */
|
|
335
|
+
export type Node =
|
|
336
|
+
True | False | Compare | In | Exists | Under | And | Or | Not | Predicate
|
|
337
|
+
|
|
338
|
+
const operandJson = (operand: Operand): unknown =>
|
|
339
|
+
operand._tag === "Literal"
|
|
340
|
+
? {
|
|
341
|
+
_tag: "Literal",
|
|
342
|
+
// JSON has no `Infinity`, `-Infinity` or `NaN`. Such a number
|
|
343
|
+
// becomes an object that the decoder does not take.
|
|
344
|
+
value:
|
|
345
|
+
typeof operand.value === "number" && !Number.isFinite(operand.value)
|
|
346
|
+
? { nonFinite: String(operand.value) }
|
|
347
|
+
: operand.value,
|
|
348
|
+
}
|
|
349
|
+
: { _tag: "Principal", path: operand.path }
|
|
350
|
+
|
|
351
|
+
/** Gives the JSON form of a condition with no predicate. */
|
|
352
|
+
export const toJson = (node: Node): unknown => {
|
|
353
|
+
switch (node._tag) {
|
|
354
|
+
case "True":
|
|
355
|
+
case "False":
|
|
356
|
+
return { _tag: node._tag }
|
|
357
|
+
case "Compare":
|
|
358
|
+
return {
|
|
359
|
+
_tag: "Compare",
|
|
360
|
+
op: node.op,
|
|
361
|
+
field: node.field,
|
|
362
|
+
value: operandJson(node.value),
|
|
363
|
+
}
|
|
364
|
+
case "In":
|
|
365
|
+
return {
|
|
366
|
+
_tag: "In",
|
|
367
|
+
field: node.field,
|
|
368
|
+
values: Arr.map(node.values, operandJson),
|
|
369
|
+
}
|
|
370
|
+
case "Exists":
|
|
371
|
+
return { _tag: "Exists", field: node.field }
|
|
372
|
+
case "Under":
|
|
373
|
+
return { _tag: "Under", prefix: Arr.map(node.prefix, operandJson) }
|
|
374
|
+
case "And":
|
|
375
|
+
case "Or":
|
|
376
|
+
return { _tag: node._tag, conditions: Arr.map(node.conditions, toJson) }
|
|
377
|
+
case "Not":
|
|
378
|
+
return { _tag: "Not", condition: toJson(node.condition) }
|
|
379
|
+
case "Predicate":
|
|
380
|
+
throw new Error(
|
|
381
|
+
`The predicate "${node.name}" can not become JSON. A condition with a predicate has no JSON form.`,
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Tests if a value is a node. Only a constructor of a node makes one. */
|
|
387
|
+
export const isNode = (u: unknown): u is Node => u instanceof Base
|
|
388
|
+
|
|
389
|
+
const notNode = (): Error =>
|
|
390
|
+
new Error(
|
|
391
|
+
"A condition must be made with a builder or a function of Condition. A plain object is not a condition",
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
/** Gives a value as a node, or throws when it is not a node. */
|
|
395
|
+
export const toNode = (u: unknown): Node => {
|
|
396
|
+
if (!isNode(u)) {
|
|
397
|
+
throw notNode()
|
|
398
|
+
}
|
|
399
|
+
return u
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export const always: Node = new True()
|
|
403
|
+
|
|
404
|
+
export const never: Node = new False()
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field patterns, as in `settings.*`, `secret*` or `**.secret`. A pattern is
|
|
3
|
+
* a list of parts with dots between them:
|
|
4
|
+
* - A name with no star matches that name.
|
|
5
|
+
* - A part with stars matches a name, and each star matches any text with no
|
|
6
|
+
* dot, as `secret*` matches `secretKey`.
|
|
7
|
+
* - `**` matches any number of names, also none. It must be a whole part.
|
|
8
|
+
*
|
|
9
|
+
* A pattern that matches a field matches each field below it too. So `pay`
|
|
10
|
+
* matches `pay.amount`, as `pay.**` does.
|
|
11
|
+
*
|
|
12
|
+
* The match goes one name at a time, and it keeps the set of places in the
|
|
13
|
+
* pattern that it may be at. It does not go back, so the time is at most the
|
|
14
|
+
* number of names times the number of parts.
|
|
15
|
+
*/
|
|
16
|
+
import * as Arr from "effect/Array"
|
|
17
|
+
import * as Result from "effect/Result"
|
|
18
|
+
import * as Catalog from "./catalog.ts"
|
|
19
|
+
import * as Limits from "./limits.ts"
|
|
20
|
+
|
|
21
|
+
/** One part of a pattern. */
|
|
22
|
+
export type Part =
|
|
23
|
+
| { readonly _tag: "Name"; readonly name: string }
|
|
24
|
+
/** The texts between the stars of the part. */
|
|
25
|
+
| { readonly _tag: "Glob"; readonly pieces: ReadonlyArray<string> }
|
|
26
|
+
| { readonly _tag: "Globstar" }
|
|
27
|
+
|
|
28
|
+
/** A pattern, as a list of parts. Two `**` parts are never next to each other. */
|
|
29
|
+
export type Pattern = ReadonlyArray<Part>
|
|
30
|
+
|
|
31
|
+
const globstar: Part = { _tag: "Globstar" }
|
|
32
|
+
|
|
33
|
+
const partOf = (text: string): Part =>
|
|
34
|
+
text === "**"
|
|
35
|
+
? globstar
|
|
36
|
+
: text.includes("*")
|
|
37
|
+
? { _tag: "Glob", pieces: text.split("*") }
|
|
38
|
+
: { _tag: "Name", name: text }
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reads a pattern. It fails with the problem when the pattern has `**`
|
|
42
|
+
* inside a part, or has more parts than `patternParts`.
|
|
43
|
+
*/
|
|
44
|
+
export const parse = (text: string): Result.Result<Pattern, string> => {
|
|
45
|
+
const texts = text.split(".", Limits.patternParts + 1)
|
|
46
|
+
if (texts.length > Limits.patternParts) {
|
|
47
|
+
return Result.fail(
|
|
48
|
+
`The field pattern "${text}" has more than ${String(Limits.patternParts)} parts. The most is ${String(Limits.patternParts)}.`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
if (Arr.some(texts, (part) => part !== "**" && part.includes("**"))) {
|
|
52
|
+
return Result.fail(
|
|
53
|
+
`The field pattern "${text}" has "**" inside a part. "**" must be a whole part`,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
// `a.**.**` matches what `a.**` matches.
|
|
57
|
+
return Result.succeed(
|
|
58
|
+
Arr.dedupeAdjacentWith(
|
|
59
|
+
Arr.map(texts, partOf),
|
|
60
|
+
(left, right) => left._tag === "Globstar" && right._tag === "Globstar",
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Reads a pattern of a bound rule. The binding of the rule took the pattern,
|
|
67
|
+
* so a problem here is a defect.
|
|
68
|
+
*/
|
|
69
|
+
export const parseBound = (text: string): Pattern =>
|
|
70
|
+
Result.getOrThrowWith(parse(text), (problem) => new Error(problem))
|
|
71
|
+
|
|
72
|
+
/** Tests if a name matches the pieces of a part with stars. */
|
|
73
|
+
const globMatches = (pieces: ReadonlyArray<string>, name: string): boolean => {
|
|
74
|
+
const first = pieces[0]!
|
|
75
|
+
const last = pieces[pieces.length - 1]!
|
|
76
|
+
const middle = pieces.slice(1, -1)
|
|
77
|
+
if (
|
|
78
|
+
name.length < first.length + last.length ||
|
|
79
|
+
!name.startsWith(first) ||
|
|
80
|
+
!name.endsWith(last)
|
|
81
|
+
) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
// Each piece in the middle is found as early as it can be. An earlier
|
|
85
|
+
// place never leaves less room for the pieces after it.
|
|
86
|
+
const end = name.length - last.length
|
|
87
|
+
const place = Arr.reduce(middle, first.length as number, (at, piece) => {
|
|
88
|
+
if (at < 0) {
|
|
89
|
+
return at
|
|
90
|
+
}
|
|
91
|
+
const found = name.indexOf(piece, at)
|
|
92
|
+
return found < 0 || found + piece.length > end ? -1 : found + piece.length
|
|
93
|
+
})
|
|
94
|
+
return place >= 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Tests if a part matches one segment of a path. A part with stars matches
|
|
99
|
+
* any key of a record. A name matches a key of a record when the Schema of
|
|
100
|
+
* the keys takes it.
|
|
101
|
+
*/
|
|
102
|
+
const partFits = (part: Part, segment: Catalog.Segment): boolean => {
|
|
103
|
+
switch (part._tag) {
|
|
104
|
+
case "Globstar":
|
|
105
|
+
return true
|
|
106
|
+
case "Glob":
|
|
107
|
+
return typeof segment !== "string" || globMatches(part.pieces, segment)
|
|
108
|
+
case "Name":
|
|
109
|
+
return typeof segment === "string"
|
|
110
|
+
? part.name === segment
|
|
111
|
+
: Catalog.keyFits(segment, part.name)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The places of a match: the parts that it may take next. */
|
|
116
|
+
type States = ReadonlyArray<number>
|
|
117
|
+
|
|
118
|
+
/** Adds the place after each `**`, since a `**` may match no names. */
|
|
119
|
+
const close = (pattern: Pattern, states: States): States =>
|
|
120
|
+
Arr.dedupe(
|
|
121
|
+
Arr.flatMap(states, (state) =>
|
|
122
|
+
pattern[state]?._tag === "Globstar" ? [state, state + 1] : [state],
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const start = (pattern: Pattern): States => close(pattern, [0])
|
|
127
|
+
|
|
128
|
+
/** Gives the places after one more segment. A `**` that takes it stays. */
|
|
129
|
+
const step = (
|
|
130
|
+
pattern: Pattern,
|
|
131
|
+
states: States,
|
|
132
|
+
segment: Catalog.Segment,
|
|
133
|
+
): States =>
|
|
134
|
+
close(
|
|
135
|
+
pattern,
|
|
136
|
+
Arr.flatMap(states, (state) => {
|
|
137
|
+
const part = pattern[state]
|
|
138
|
+
return part === undefined
|
|
139
|
+
? []
|
|
140
|
+
: part._tag === "Globstar"
|
|
141
|
+
? [state]
|
|
142
|
+
: partFits(part, segment)
|
|
143
|
+
? [state + 1]
|
|
144
|
+
: []
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const isDone = (pattern: Pattern, states: States): boolean =>
|
|
149
|
+
states.includes(pattern.length)
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Tests if a pattern matches a field, or a field above it. The field is a
|
|
153
|
+
* list of names.
|
|
154
|
+
*/
|
|
155
|
+
export const matches = (
|
|
156
|
+
pattern: Pattern,
|
|
157
|
+
names: ReadonlyArray<string>,
|
|
158
|
+
): boolean => {
|
|
159
|
+
const found = Arr.reduce(
|
|
160
|
+
names,
|
|
161
|
+
{ states: start(pattern), done: isDone(pattern, start(pattern)) },
|
|
162
|
+
(before, name) => {
|
|
163
|
+
if (before.done || before.states.length === 0) {
|
|
164
|
+
return before
|
|
165
|
+
}
|
|
166
|
+
const states = step(pattern, before.states, name)
|
|
167
|
+
return { states, done: isDone(pattern, states) }
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
return found.done
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Tests if a pattern may match a field below a field, with more names after
|
|
175
|
+
* the names of the field.
|
|
176
|
+
*/
|
|
177
|
+
export const matchesBelow = (
|
|
178
|
+
pattern: Pattern,
|
|
179
|
+
names: ReadonlyArray<string>,
|
|
180
|
+
): boolean =>
|
|
181
|
+
Arr.some(
|
|
182
|
+
Arr.reduce(names, start(pattern), (states, name) =>
|
|
183
|
+
step(pattern, states, name),
|
|
184
|
+
),
|
|
185
|
+
(state) => state < pattern.length,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
/** Tests if a pattern matches some field of a catalog. */
|
|
189
|
+
export const fitsCatalog = (
|
|
190
|
+
pattern: Pattern,
|
|
191
|
+
catalog: Catalog.Catalog,
|
|
192
|
+
): boolean => {
|
|
193
|
+
const below = (branch: Catalog.Branch, states: States): boolean =>
|
|
194
|
+
states.length > 0 &&
|
|
195
|
+
(Arr.some(branch.children, ([name, child]) => visit(child, name, states)) ||
|
|
196
|
+
(branch.key !== undefined &&
|
|
197
|
+
branch.keyBranch !== undefined &&
|
|
198
|
+
visit(branch.keyBranch, branch.key, states)))
|
|
199
|
+
const visit = (
|
|
200
|
+
branch: Catalog.Branch,
|
|
201
|
+
segment: Catalog.Segment,
|
|
202
|
+
before: States,
|
|
203
|
+
): boolean => {
|
|
204
|
+
const states = step(pattern, before, segment)
|
|
205
|
+
return (
|
|
206
|
+
(branch.field !== undefined && isDone(pattern, states)) ||
|
|
207
|
+
below(branch, states)
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
return below(catalog.root, start(pattern))
|
|
211
|
+
}
|