@nmnmcc/ability 1.0.0 → 2.0.0

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.
Files changed (127) hide show
  1. package/README.md +143 -87
  2. package/dist/Authorizer.d.ts +43 -47
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +5 -290
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +164 -332
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +75 -890
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Decision.d.ts +13 -13
  11. package/dist/Decision.d.ts.map +1 -1
  12. package/dist/Decision.js.map +1 -1
  13. package/dist/Domain.d.ts +10 -21
  14. package/dist/Domain.d.ts.map +1 -1
  15. package/dist/Domain.js +4 -62
  16. package/dist/Domain.js.map +1 -1
  17. package/dist/HttpApi.d.ts +31 -7
  18. package/dist/HttpApi.d.ts.map +1 -1
  19. package/dist/HttpApi.js +28 -11
  20. package/dist/HttpApi.js.map +1 -1
  21. package/dist/Limits.d.ts +114 -0
  22. package/dist/Limits.d.ts.map +1 -0
  23. package/dist/Limits.js +132 -0
  24. package/dist/Limits.js.map +1 -0
  25. package/dist/Policy.d.ts +274 -272
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +71 -1180
  28. package/dist/Policy.js.map +1 -1
  29. package/dist/Resource.d.ts +8 -181
  30. package/dist/Resource.d.ts.map +1 -1
  31. package/dist/Resource.js +8 -807
  32. package/dist/Resource.js.map +1 -1
  33. package/dist/index.d.ts +3 -4
  34. package/dist/index.js +4 -5
  35. package/dist/internal/algebra.js +267 -0
  36. package/dist/internal/algebra.js.map +1 -0
  37. package/dist/internal/analysis.js +245 -0
  38. package/dist/internal/analysis.js.map +1 -0
  39. package/dist/internal/authorizer.d.ts +30 -0
  40. package/dist/internal/authorizer.d.ts.map +1 -0
  41. package/dist/internal/authorizer.js +221 -0
  42. package/dist/internal/authorizer.js.map +1 -0
  43. package/dist/internal/bind.js +286 -0
  44. package/dist/internal/bind.js.map +1 -0
  45. package/dist/internal/catalog.d.ts +56 -0
  46. package/dist/internal/catalog.d.ts.map +1 -0
  47. package/dist/internal/catalog.js +292 -0
  48. package/dist/internal/catalog.js.map +1 -0
  49. package/dist/internal/decide.js +166 -0
  50. package/dist/internal/decide.js.map +1 -0
  51. package/dist/internal/domain.js +32 -0
  52. package/dist/internal/domain.js.map +1 -0
  53. package/dist/internal/json.js +134 -0
  54. package/dist/internal/json.js.map +1 -0
  55. package/dist/internal/limits.d.ts +14 -0
  56. package/dist/internal/limits.d.ts.map +1 -0
  57. package/dist/internal/limits.js +43 -0
  58. package/dist/internal/limits.js.map +1 -0
  59. package/dist/internal/nodes.d.ts +121 -0
  60. package/dist/internal/nodes.d.ts.map +1 -0
  61. package/dist/internal/nodes.js +277 -0
  62. package/dist/internal/nodes.js.map +1 -0
  63. package/dist/internal/patterns.js +110 -0
  64. package/dist/internal/patterns.js.map +1 -0
  65. package/dist/internal/policy-json.js +255 -0
  66. package/dist/internal/policy-json.js.map +1 -0
  67. package/dist/internal/policy-nodes.d.ts +61 -0
  68. package/dist/internal/policy-nodes.d.ts.map +1 -0
  69. package/dist/internal/policy-nodes.js +146 -0
  70. package/dist/internal/policy-nodes.js.map +1 -0
  71. package/dist/internal/policy.d.ts +42 -0
  72. package/dist/internal/policy.d.ts.map +1 -0
  73. package/dist/internal/policy.js +124 -0
  74. package/dist/internal/policy.js.map +1 -0
  75. package/dist/internal/read.js +39 -0
  76. package/dist/internal/read.js.map +1 -0
  77. package/dist/internal/resource.js +115 -0
  78. package/dist/internal/resource.js.map +1 -0
  79. package/dist/internal/rule.d.ts +26 -0
  80. package/dist/internal/rule.d.ts.map +1 -0
  81. package/dist/internal/rule.js +93 -0
  82. package/dist/internal/rule.js.map +1 -0
  83. package/dist/internal/search.d.ts +22 -0
  84. package/dist/internal/search.d.ts.map +1 -0
  85. package/dist/internal/search.js +274 -0
  86. package/dist/internal/search.js.map +1 -0
  87. package/dist/internal/suggest.js +46 -0
  88. package/dist/internal/suggest.js.map +1 -0
  89. package/package.json +4 -5
  90. package/src/Authorizer.ts +69 -585
  91. package/src/Condition.ts +339 -2095
  92. package/src/Decision.ts +6 -5
  93. package/src/Domain.ts +13 -70
  94. package/src/HttpApi.ts +75 -13
  95. package/src/Limits.ts +132 -0
  96. package/src/Policy.ts +386 -2772
  97. package/src/Resource.ts +14 -1555
  98. package/src/index.ts +3 -9
  99. package/src/internal/algebra.ts +482 -0
  100. package/src/internal/analysis.ts +478 -0
  101. package/src/internal/authorizer.ts +418 -0
  102. package/src/internal/bind.ts +596 -0
  103. package/src/internal/catalog.ts +534 -0
  104. package/src/internal/decide.ts +402 -0
  105. package/src/internal/domain.ts +66 -0
  106. package/src/internal/json.ts +260 -0
  107. package/src/internal/limits.ts +78 -0
  108. package/src/internal/nodes.ts +404 -0
  109. package/src/internal/patterns.ts +211 -0
  110. package/src/internal/policy-json.ts +484 -0
  111. package/src/internal/policy-nodes.ts +207 -0
  112. package/src/internal/policy.ts +264 -0
  113. package/src/internal/read.ts +55 -0
  114. package/src/internal/resource.ts +180 -0
  115. package/src/internal/rule.ts +181 -0
  116. package/src/internal/search.ts +541 -0
  117. package/src/internal/suggest.ts +104 -0
  118. package/dist/Principal.d.ts +0 -35
  119. package/dist/Principal.d.ts.map +0 -1
  120. package/dist/Principal.js +0 -37
  121. package/dist/Principal.js.map +0 -1
  122. package/dist/Rule.d.ts +0 -275
  123. package/dist/Rule.d.ts.map +0 -1
  124. package/dist/Rule.js +0 -257
  125. package/dist/Rule.js.map +0 -1
  126. package/src/Principal.ts +0 -41
  127. 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
+ }