@nmnmcc/ability 2.0.7 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -29
- package/dist/Authorizer.d.ts +21 -12
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +121 -19
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +32 -21
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +9 -0
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +5 -0
- package/dist/Domain.js.map +1 -1
- package/dist/Limits.d.ts +44 -13
- package/dist/Limits.d.ts.map +1 -1
- package/dist/Limits.js +52 -22
- package/dist/Limits.js.map +1 -1
- package/dist/Policy.d.ts +102 -18
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +7 -6
- package/dist/Policy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internal/algebra.js +2 -2
- package/dist/internal/analysis.js +81 -75
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +11 -7
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +86 -64
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +33 -10
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +170 -77
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +27 -31
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/domain.js +19 -5
- package/dist/internal/domain.js.map +1 -1
- package/dist/internal/json.js +120 -76
- package/dist/internal/json.js.map +1 -1
- package/dist/internal/limits.d.ts +28 -0
- package/dist/internal/limits.d.ts.map +1 -1
- package/dist/internal/limits.js +75 -16
- package/dist/internal/limits.js.map +1 -1
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +53 -44
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/patterns.js +50 -15
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-json.js +169 -127
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +16 -13
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/policy.d.ts.map +1 -1
- package/dist/internal/policy.js +38 -22
- package/dist/internal/policy.js.map +1 -1
- package/dist/internal/resource.js +1 -1
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +6 -5
- package/dist/internal/rule.js.map +1 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +106 -92
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/trampoline.js +19 -5
- package/dist/internal/trampoline.js.map +1 -1
- package/package.json +1 -1
- package/src/Authorizer.ts +30 -20
- package/src/Condition.ts +317 -86
- package/src/Domain.ts +9 -0
- package/src/Limits.ts +69 -29
- package/src/Policy.ts +143 -21
- package/src/internal/analysis.ts +154 -124
- package/src/internal/authorizer.ts +29 -14
- package/src/internal/bind.ts +193 -136
- package/src/internal/catalog.ts +320 -118
- package/src/internal/decide.ts +61 -50
- package/src/internal/domain.ts +39 -3
- package/src/internal/json.ts +212 -117
- package/src/internal/limits.ts +101 -31
- package/src/internal/nodes.ts +65 -47
- package/src/internal/patterns.ts +79 -25
- package/src/internal/policy-json.ts +266 -197
- package/src/internal/policy-nodes.ts +31 -23
- package/src/internal/policy.ts +63 -23
- package/src/internal/rule.ts +16 -3
- package/src/internal/search.ts +186 -136
- package/src/internal/trampoline.ts +51 -4
package/src/internal/catalog.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { hasProperty } from "effect/Predicate"
|
|
|
18
18
|
import * as Schema from "effect/Schema"
|
|
19
19
|
import * as SchemaAST from "effect/SchemaAST"
|
|
20
20
|
import * as Limits from "./limits.ts"
|
|
21
|
+
import * as Trampoline from "./trampoline.ts"
|
|
21
22
|
|
|
22
23
|
/** The kinds of values that a field may hold. */
|
|
23
24
|
export interface Kinds {
|
|
@@ -136,34 +137,57 @@ export interface Field {
|
|
|
136
137
|
* above the field may be an object that does not have it as a data field:
|
|
137
138
|
* a class without the field, which may have a getter of that name, a
|
|
138
139
|
* value such as a date or a list, or any object, as of `Schema.Unknown`.
|
|
139
|
-
* A
|
|
140
|
-
*
|
|
140
|
+
* A struct of a union without the field makes it not plain too, since a
|
|
141
|
+
* value made by hand may have it with any value.
|
|
141
142
|
*/
|
|
142
143
|
readonly plain: boolean
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* A place in the graph of a catalog. A path of a field leads to a place, and
|
|
148
|
+
* many paths may lead to one place, as in a recursive Schema: `next` and
|
|
149
|
+
* `next.next` of a list of links are one place. A place keeps the ids of the
|
|
150
|
+
* places below it, so the graph has no loop of objects.
|
|
151
|
+
*/
|
|
146
152
|
export interface Branch {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
readonly id: number
|
|
154
|
+
/** The kinds of the values of the field at this place. */
|
|
155
|
+
readonly kinds: Kinds
|
|
156
|
+
/** The field holds a scalar, a record or a list: see `Field.listed`. */
|
|
157
|
+
readonly values: boolean
|
|
158
|
+
/** The field may hold a struct. */
|
|
159
|
+
readonly structs: boolean
|
|
160
|
+
readonly open: boolean
|
|
161
|
+
readonly plain: boolean
|
|
162
|
+
/** The ids of the places below, by name, in the order of the Schema. */
|
|
163
|
+
readonly children: ReadonlyArray<readonly [string, number]>
|
|
151
164
|
/**
|
|
152
165
|
* The places below, by name. A lookup gets each name from `split`, as a
|
|
153
166
|
* new string, and a `HashMap` finds a new string about 20% faster than a
|
|
154
167
|
* native `Map`. The names come from the Schema, so a name from a policy
|
|
155
168
|
* can not make a long list of names with one hash here.
|
|
156
169
|
*/
|
|
157
|
-
readonly names: HashMap.HashMap<string,
|
|
170
|
+
readonly names: HashMap.HashMap<string, number>
|
|
158
171
|
/** The place for the keys of a record, when the value may be a record. */
|
|
159
172
|
readonly key: Key | undefined
|
|
160
|
-
readonly keyBranch:
|
|
173
|
+
readonly keyBranch: number | undefined
|
|
161
174
|
}
|
|
162
175
|
|
|
163
176
|
/** The catalog of the fields of a Schema. */
|
|
164
177
|
export interface Catalog {
|
|
165
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* The fields that a list shows, a field before the fields below it. For a
|
|
180
|
+
* recursive Schema, the list goes into each recursive place one time on
|
|
181
|
+
* each path, so it is finite.
|
|
182
|
+
*/
|
|
166
183
|
readonly fields: ReadonlyArray<Field>
|
|
184
|
+
/** The places, by id. The root is the place with the id 0. */
|
|
185
|
+
readonly branches: ReadonlyArray<Branch>
|
|
186
|
+
/**
|
|
187
|
+
* The number of steps from a place to a place below it. A search of the
|
|
188
|
+
* catalog, as for a field pattern, reads each one, so this is its work.
|
|
189
|
+
*/
|
|
190
|
+
readonly edges: number
|
|
167
191
|
readonly root: Branch
|
|
168
192
|
}
|
|
169
193
|
|
|
@@ -211,18 +235,31 @@ const classStruct = (
|
|
|
211
235
|
const alternatives = (
|
|
212
236
|
ast: SchemaAST.AST,
|
|
213
237
|
open: ReadonlySet<SchemaAST.AST>,
|
|
214
|
-
): ReadonlyArray<SchemaAST.AST> => {
|
|
238
|
+
): ReadonlyArray<SchemaAST.AST> => alternativesOf({ ast, open })
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The walk of {@link alternatives}. A union may hold a union, very many
|
|
242
|
+
* levels down, so it is a walk of the trampoline for an input of an unknown
|
|
243
|
+
* depth.
|
|
244
|
+
*/
|
|
245
|
+
const alternativesOf = Trampoline.adaptive<
|
|
246
|
+
{ readonly ast: SchemaAST.AST; readonly open: ReadonlySet<SchemaAST.AST> },
|
|
247
|
+
ReadonlyArray<SchemaAST.AST>
|
|
248
|
+
>((R, child) => ({ ast, open }, depth) => {
|
|
215
249
|
switch (ast._tag) {
|
|
216
250
|
case "Union":
|
|
217
|
-
return
|
|
251
|
+
return R.map(
|
|
252
|
+
R.forEach(ast.types, (type) => child({ ast: type, open }, depth + 1)),
|
|
253
|
+
Arr.flatten,
|
|
254
|
+
)
|
|
218
255
|
case "Suspend":
|
|
219
256
|
return open.has(ast)
|
|
220
|
-
? []
|
|
221
|
-
:
|
|
257
|
+
? R.succeed([])
|
|
258
|
+
: child({ ast: ast.thunk(), open: new Set([...open, ast]) }, depth + 1)
|
|
222
259
|
default:
|
|
223
|
-
return [ast]
|
|
260
|
+
return R.succeed([ast])
|
|
224
261
|
}
|
|
225
|
-
}
|
|
262
|
+
})
|
|
226
263
|
|
|
227
264
|
/** The struct of a Schema: a struct, or the struct of a class. */
|
|
228
265
|
const structOf = (ast: SchemaAST.AST): SchemaAST.Objects | undefined =>
|
|
@@ -341,9 +378,8 @@ const declarationId = (ast: SchemaAST.Declaration): string | undefined => {
|
|
|
341
378
|
: undefined
|
|
342
379
|
}
|
|
343
380
|
|
|
344
|
-
/** The things that
|
|
381
|
+
/** The things that the walk of a catalog knows at one place. */
|
|
345
382
|
interface Place {
|
|
346
|
-
readonly path: ReadonlyArray<Segment>
|
|
347
383
|
readonly schemas: ReadonlyArray<SchemaAST.AST>
|
|
348
384
|
/** The value may be missing. */
|
|
349
385
|
readonly missing: boolean
|
|
@@ -411,12 +447,15 @@ const namesOf = (
|
|
|
411
447
|
),
|
|
412
448
|
)
|
|
413
449
|
|
|
414
|
-
/**
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
450
|
+
/** A place of the walk, and the places below it, before they have ids. */
|
|
451
|
+
interface Made {
|
|
452
|
+
readonly branch: Omit<Branch, "id" | "children" | "names" | "keyBranch">
|
|
453
|
+
readonly children: ReadonlyArray<readonly [string, Place]>
|
|
454
|
+
readonly keyPlace: Place | undefined
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Works out the facts of a place, and the places below it. */
|
|
458
|
+
const placeOf = (place: Place): Made => {
|
|
420
459
|
const structs = Arr.filter(Arr.map(place.schemas, structOf), isStruct)
|
|
421
460
|
const values = Arr.filter(
|
|
422
461
|
place.schemas,
|
|
@@ -435,49 +474,6 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
435
474
|
structs,
|
|
436
475
|
(struct) => struct.indexSignatures.length > 0,
|
|
437
476
|
)
|
|
438
|
-
// The last part that a path may have: the walk does not go below it.
|
|
439
|
-
const last = place.path.length === Limits.fieldPathParts
|
|
440
|
-
const field: Field | undefined =
|
|
441
|
-
place.path.length === 0
|
|
442
|
-
? undefined
|
|
443
|
-
: {
|
|
444
|
-
path: place.path,
|
|
445
|
-
text: textOf(place.path),
|
|
446
|
-
kinds,
|
|
447
|
-
listed:
|
|
448
|
-
values.length > 0 ||
|
|
449
|
-
records.length > 0 ||
|
|
450
|
-
(last && structs.length > 0),
|
|
451
|
-
// A field that is a scalar in one branch of a union and a struct
|
|
452
|
-
// in another holds parts too.
|
|
453
|
-
open:
|
|
454
|
-
records.length > 0 ||
|
|
455
|
-
structs.length > 0 ||
|
|
456
|
-
valueKinds.object ||
|
|
457
|
-
place.top,
|
|
458
|
-
plain: place.plain,
|
|
459
|
-
}
|
|
460
|
-
if (field !== undefined) {
|
|
461
|
-
count.size += 1
|
|
462
|
-
if (count.size > Limits.catalogFields) {
|
|
463
|
-
throw Limits.exceeded(
|
|
464
|
-
"A Schema",
|
|
465
|
-
"catalogFields",
|
|
466
|
-
"fields",
|
|
467
|
-
count.size,
|
|
468
|
-
Limits.catalogFields,
|
|
469
|
-
)
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (last) {
|
|
473
|
-
return {
|
|
474
|
-
field,
|
|
475
|
-
children: [],
|
|
476
|
-
names: HashMap.empty(),
|
|
477
|
-
key: undefined,
|
|
478
|
-
keyBranch: undefined,
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
477
|
const structProperties = Arr.map(structs, propertiesOf)
|
|
482
478
|
const children = Arr.map(namesOf(structs), (name) => {
|
|
483
479
|
const properties = Arr.flatMap(structProperties, (named) => {
|
|
@@ -492,7 +488,6 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
492
488
|
const top =
|
|
493
489
|
place.top || valueKinds.object || properties.length < structs.length
|
|
494
490
|
const below: Place = {
|
|
495
|
-
path: [...place.path, name],
|
|
496
491
|
schemas: Arr.flatMap(properties, (property) =>
|
|
497
492
|
alternatives(property.type, new Set()),
|
|
498
493
|
),
|
|
@@ -507,80 +502,287 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
|
507
502
|
// a read below it may not find plain data.
|
|
508
503
|
plain: place.plain && !top,
|
|
509
504
|
}
|
|
510
|
-
return [name,
|
|
505
|
+
return [name, below] as const
|
|
511
506
|
})
|
|
512
|
-
const names = HashMap.fromIterable(children)
|
|
513
507
|
const signatures = Arr.flatMap(records, (struct) => struct.indexSignatures)
|
|
514
|
-
if (signatures.length === 0) {
|
|
515
|
-
return { field, children, names, key: undefined, keyBranch: undefined }
|
|
516
|
-
}
|
|
517
508
|
const keyTop =
|
|
518
509
|
place.top || valueKinds.object || records.length < structs.length
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
510
|
+
return {
|
|
511
|
+
branch: {
|
|
512
|
+
kinds,
|
|
513
|
+
values: values.length > 0 || records.length > 0,
|
|
514
|
+
structs: structs.length > 0,
|
|
515
|
+
// A field that is a scalar in one branch of a union and a struct in
|
|
516
|
+
// another holds parts too.
|
|
517
|
+
open:
|
|
518
|
+
records.length > 0 ||
|
|
519
|
+
structs.length > 0 ||
|
|
520
|
+
valueKinds.object ||
|
|
521
|
+
place.top,
|
|
522
|
+
plain: place.plain,
|
|
523
|
+
key:
|
|
524
|
+
signatures.length === 0
|
|
525
|
+
? undefined
|
|
526
|
+
: {
|
|
527
|
+
_tag: "Key",
|
|
528
|
+
parameters: Arr.map(
|
|
529
|
+
signatures,
|
|
530
|
+
(signature) => signature.parameter,
|
|
531
|
+
),
|
|
532
|
+
},
|
|
533
533
|
},
|
|
534
|
-
|
|
534
|
+
children,
|
|
535
|
+
keyPlace:
|
|
536
|
+
signatures.length === 0
|
|
537
|
+
? undefined
|
|
538
|
+
: {
|
|
539
|
+
schemas: Arr.flatMap(signatures, (signature) =>
|
|
540
|
+
alternatives(signature.type, new Set()),
|
|
541
|
+
),
|
|
542
|
+
// A record may not have a given key.
|
|
543
|
+
missing: true,
|
|
544
|
+
top: keyTop,
|
|
545
|
+
plain: place.plain && !keyTop,
|
|
546
|
+
},
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Makes the places of a Schema. Each place comes one time: two paths with the
|
|
552
|
+
* same Schemas and the same marks lead to one place. The walk keeps a queue
|
|
553
|
+
* of the places that it has found, and it makes them in order, so a deep
|
|
554
|
+
* Schema does not fill the stack.
|
|
555
|
+
*/
|
|
556
|
+
const branchesOf = (root: Place): ReadonlyArray<Branch> => {
|
|
557
|
+
// The id of each Schema node, and of each place, for this walk. The ids
|
|
558
|
+
// are in native maps, because the walk must find a Schema node by the node
|
|
559
|
+
// itself: `Equal` compares Schema nodes by structure, and a recursive
|
|
560
|
+
// Schema has no end. The maps and the queue belong to this walk.
|
|
561
|
+
const astIds = new Map<SchemaAST.AST, number>()
|
|
562
|
+
const placeIds = new Map<string, number>()
|
|
563
|
+
const queue: Array<Place> = []
|
|
564
|
+
const idOf = (place: Place): number => {
|
|
565
|
+
const ids = Arr.map(place.schemas, (schema) => {
|
|
566
|
+
const known = astIds.get(schema)
|
|
567
|
+
if (known !== undefined) {
|
|
568
|
+
return known
|
|
569
|
+
}
|
|
570
|
+
astIds.set(schema, astIds.size)
|
|
571
|
+
return astIds.size - 1
|
|
572
|
+
})
|
|
573
|
+
const key = `${Array.from(new Set(ids))
|
|
574
|
+
.sort((a, b) => a - b)
|
|
575
|
+
.join(
|
|
576
|
+
",",
|
|
577
|
+
)}|${String(place.missing)}|${String(place.top)}|${String(place.plain)}`
|
|
578
|
+
const known = placeIds.get(key)
|
|
579
|
+
if (known !== undefined) {
|
|
580
|
+
return known
|
|
581
|
+
}
|
|
582
|
+
if (queue.length >= Limits.ceilings.catalogFields) {
|
|
583
|
+
throw Limits.exceeded(
|
|
584
|
+
"A Schema",
|
|
585
|
+
"catalogFields",
|
|
586
|
+
"places",
|
|
587
|
+
queue.length + 1,
|
|
588
|
+
Limits.ceilings.catalogFields,
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
placeIds.set(key, queue.length)
|
|
592
|
+
queue.push(place)
|
|
593
|
+
return queue.length - 1
|
|
594
|
+
}
|
|
595
|
+
idOf(root)
|
|
596
|
+
return Arr.unfold(0, (id) => {
|
|
597
|
+
const place = queue[id]
|
|
598
|
+
if (place === undefined) {
|
|
599
|
+
return Option.none()
|
|
600
|
+
}
|
|
601
|
+
const made = placeOf(place)
|
|
602
|
+
const children = Arr.map(
|
|
603
|
+
made.children,
|
|
604
|
+
([name, below]) => [name, idOf(below)] as const,
|
|
605
|
+
)
|
|
606
|
+
const branch: Branch = {
|
|
607
|
+
...made.branch,
|
|
608
|
+
id,
|
|
609
|
+
children,
|
|
610
|
+
names: HashMap.fromIterable(children),
|
|
611
|
+
keyBranch: made.keyPlace === undefined ? undefined : idOf(made.keyPlace),
|
|
612
|
+
}
|
|
613
|
+
return Option.some([branch, id + 1] as const)
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** The field at a place, for a path. */
|
|
618
|
+
const fieldAt = (
|
|
619
|
+
branch: Branch,
|
|
620
|
+
path: ReadonlyArray<Segment>,
|
|
621
|
+
parts: number,
|
|
622
|
+
): Field => ({
|
|
623
|
+
path,
|
|
624
|
+
text: textOf(path),
|
|
625
|
+
kinds: branch.kinds,
|
|
626
|
+
// A struct at the last part that a path may have is one field, since a
|
|
627
|
+
// path does not go below it.
|
|
628
|
+
listed: branch.values || (path.length === parts && branch.structs),
|
|
629
|
+
open: branch.open,
|
|
630
|
+
plain: branch.plain,
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* The fields that a list shows, a field before the fields below it, down to
|
|
635
|
+
* `parts` parts. A path goes into a place that is on it already one time
|
|
636
|
+
* only, so the list of a recursive Schema is finite. The walk keeps a stack
|
|
637
|
+
* of the places to visit, so a deep Schema does not fill the stack. The
|
|
638
|
+
* stack belongs to this walk.
|
|
639
|
+
*/
|
|
640
|
+
const listOf = (
|
|
641
|
+
branches: ReadonlyArray<Branch>,
|
|
642
|
+
parts: number,
|
|
643
|
+
): ReadonlyArray<Field> => {
|
|
644
|
+
interface Visit {
|
|
645
|
+
readonly branch: Branch
|
|
646
|
+
readonly path: ReadonlyArray<Segment>
|
|
647
|
+
/** The ids of the places on the path, as a set of Effect that shares its parts. */
|
|
648
|
+
readonly above: HashSet.HashSet<number>
|
|
649
|
+
}
|
|
650
|
+
const below = (visit: Visit): ReadonlyArray<Visit> => {
|
|
651
|
+
const { branch, path } = visit
|
|
652
|
+
if (
|
|
653
|
+
path.length === parts ||
|
|
654
|
+
(path.length > 0 && HashSet.has(visit.above, branch.id))
|
|
655
|
+
) {
|
|
656
|
+
return []
|
|
657
|
+
}
|
|
658
|
+
const above = HashSet.add(visit.above, branch.id)
|
|
659
|
+
const named = Arr.map(branch.children, ([name, id]) => ({
|
|
660
|
+
branch: branches[id]!,
|
|
661
|
+
path: [...path, name],
|
|
662
|
+
above,
|
|
663
|
+
}))
|
|
664
|
+
return branch.key === undefined || branch.keyBranch === undefined
|
|
665
|
+
? named
|
|
666
|
+
: [
|
|
667
|
+
...named,
|
|
668
|
+
{
|
|
669
|
+
branch: branches[branch.keyBranch]!,
|
|
670
|
+
path: [...path, branch.key],
|
|
671
|
+
above,
|
|
672
|
+
},
|
|
673
|
+
]
|
|
674
|
+
}
|
|
675
|
+
const stack: Array<Visit> = [
|
|
676
|
+
{ branch: branches[0]!, path: [], above: HashSet.empty() },
|
|
677
|
+
]
|
|
678
|
+
return Arr.flatten(
|
|
679
|
+
Arr.unfold(stack, (rest) => {
|
|
680
|
+
const visit = rest.pop()
|
|
681
|
+
if (visit === undefined) {
|
|
682
|
+
return Option.none()
|
|
683
|
+
}
|
|
684
|
+
// The visits go onto the stack one at a time: a spread into a call
|
|
685
|
+
// would pass one argument for each field, past the most arguments
|
|
686
|
+
// of a call.
|
|
687
|
+
Arr.forEach(Arr.reverse(below(visit)), (next) => {
|
|
688
|
+
rest.push(next)
|
|
689
|
+
})
|
|
690
|
+
const field =
|
|
691
|
+
visit.path.length === 0
|
|
692
|
+
? []
|
|
693
|
+
: [fieldAt(visit.branch, visit.path, parts)]
|
|
694
|
+
if (field.length > 0 && rest.length + 1 > Limits.ceilings.catalogFields) {
|
|
695
|
+
throw Limits.exceeded(
|
|
696
|
+
"A Schema",
|
|
697
|
+
"catalogFields",
|
|
698
|
+
"fields",
|
|
699
|
+
rest.length + 1,
|
|
700
|
+
Limits.ceilings.catalogFields,
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
return Option.some([field, rest] as const)
|
|
704
|
+
}),
|
|
535
705
|
)
|
|
536
|
-
return { field, children, names, key, keyBranch }
|
|
537
706
|
}
|
|
538
707
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
708
|
+
/**
|
|
709
|
+
* The fields that a list shows for paths of at most `parts` parts, as for a
|
|
710
|
+
* domain with that limit. See `Catalog.fields`.
|
|
711
|
+
*/
|
|
712
|
+
export const fieldsOf = (
|
|
713
|
+
catalog: Catalog,
|
|
714
|
+
parts: number,
|
|
715
|
+
): ReadonlyArray<Field> =>
|
|
716
|
+
parts === Limits.fieldPathParts
|
|
717
|
+
? catalog.fields
|
|
718
|
+
: listOf(catalog.branches, parts)
|
|
544
719
|
|
|
545
720
|
/**
|
|
546
|
-
* Makes the catalog of a Schema
|
|
547
|
-
*
|
|
721
|
+
* Makes the catalog of a Schema, with the list of its fields for the default
|
|
722
|
+
* limit of the parts of a path. It throws when a field name has a dot or is
|
|
723
|
+
* empty, and when the Schema has more places or fields than the ceiling of
|
|
724
|
+
* `catalogFields`. A domain checks the size of the list with its own limit.
|
|
548
725
|
*/
|
|
549
726
|
export const catalogOf = (ast: SchemaAST.AST): Catalog => {
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
727
|
+
const branches = branchesOf({
|
|
728
|
+
schemas: alternatives(ast, new Set()),
|
|
729
|
+
missing: false,
|
|
730
|
+
top: false,
|
|
731
|
+
plain: true,
|
|
732
|
+
})
|
|
733
|
+
return {
|
|
734
|
+
fields: listOf(branches, Limits.fieldPathParts),
|
|
735
|
+
branches,
|
|
736
|
+
edges: Arr.reduce(
|
|
737
|
+
branches,
|
|
738
|
+
0,
|
|
739
|
+
(total, branch) =>
|
|
740
|
+
total +
|
|
741
|
+
branch.children.length +
|
|
742
|
+
(branch.keyBranch === undefined ? 0 : 1),
|
|
743
|
+
),
|
|
744
|
+
root: branches[0]!,
|
|
745
|
+
}
|
|
561
746
|
}
|
|
562
747
|
|
|
563
|
-
/**
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
748
|
+
/**
|
|
749
|
+
* Gives the field of a catalog at a path with dots, if the catalog has it.
|
|
750
|
+
* A path has at most `parts` parts. The lookup follows one place for each
|
|
751
|
+
* part, in a loop.
|
|
752
|
+
*/
|
|
753
|
+
export const lookup = (
|
|
754
|
+
catalog: Catalog,
|
|
755
|
+
path: string,
|
|
756
|
+
parts: number = Limits.fieldPathParts,
|
|
757
|
+
): Field | undefined => {
|
|
758
|
+
const names = path.split(".", parts + 1)
|
|
759
|
+
if (names.length > parts) {
|
|
567
760
|
return undefined
|
|
568
761
|
}
|
|
569
|
-
const
|
|
762
|
+
const found = Arr.reduce(
|
|
570
763
|
names,
|
|
571
|
-
catalog.root
|
|
764
|
+
{ branch: catalog.root, path: [] } as
|
|
765
|
+
| { readonly branch: Branch; readonly path: ReadonlyArray<Segment> }
|
|
766
|
+
| undefined,
|
|
572
767
|
(current, name) => {
|
|
573
768
|
if (current === undefined) {
|
|
574
769
|
return undefined
|
|
575
770
|
}
|
|
576
|
-
const named = HashMap.get(current.names, name)
|
|
771
|
+
const named = HashMap.get(current.branch.names, name)
|
|
577
772
|
if (Option.isSome(named)) {
|
|
578
|
-
return
|
|
773
|
+
return {
|
|
774
|
+
branch: catalog.branches[named.value]!,
|
|
775
|
+
path: [...current.path, name],
|
|
776
|
+
}
|
|
579
777
|
}
|
|
580
|
-
|
|
581
|
-
|
|
778
|
+
const key = current.branch.key
|
|
779
|
+
const keyBranch = current.branch.keyBranch
|
|
780
|
+
return key !== undefined && keyBranch !== undefined && keyFits(key, name)
|
|
781
|
+
? { branch: catalog.branches[keyBranch]!, path: [...current.path, key] }
|
|
582
782
|
: undefined
|
|
583
783
|
},
|
|
584
784
|
)
|
|
585
|
-
return
|
|
785
|
+
return found === undefined
|
|
786
|
+
? undefined
|
|
787
|
+
: fieldAt(found.branch, found.path, parts)
|
|
586
788
|
}
|