@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.
Files changed (87) hide show
  1. package/README.md +52 -29
  2. package/dist/Authorizer.d.ts +21 -12
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js.map +1 -1
  5. package/dist/Condition.d.ts +121 -19
  6. package/dist/Condition.d.ts.map +1 -1
  7. package/dist/Condition.js +32 -21
  8. package/dist/Condition.js.map +1 -1
  9. package/dist/Domain.d.ts +9 -0
  10. package/dist/Domain.d.ts.map +1 -1
  11. package/dist/Domain.js +5 -0
  12. package/dist/Domain.js.map +1 -1
  13. package/dist/Limits.d.ts +44 -13
  14. package/dist/Limits.d.ts.map +1 -1
  15. package/dist/Limits.js +52 -22
  16. package/dist/Limits.js.map +1 -1
  17. package/dist/Policy.d.ts +102 -18
  18. package/dist/Policy.d.ts.map +1 -1
  19. package/dist/Policy.js +7 -6
  20. package/dist/Policy.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/internal/algebra.js +2 -2
  23. package/dist/internal/analysis.js +81 -75
  24. package/dist/internal/analysis.js.map +1 -1
  25. package/dist/internal/authorizer.d.ts.map +1 -1
  26. package/dist/internal/authorizer.js +11 -7
  27. package/dist/internal/authorizer.js.map +1 -1
  28. package/dist/internal/bind.js +86 -64
  29. package/dist/internal/bind.js.map +1 -1
  30. package/dist/internal/catalog.d.ts +33 -10
  31. package/dist/internal/catalog.d.ts.map +1 -1
  32. package/dist/internal/catalog.js +170 -77
  33. package/dist/internal/catalog.js.map +1 -1
  34. package/dist/internal/decide.js +27 -31
  35. package/dist/internal/decide.js.map +1 -1
  36. package/dist/internal/domain.js +19 -5
  37. package/dist/internal/domain.js.map +1 -1
  38. package/dist/internal/json.js +120 -76
  39. package/dist/internal/json.js.map +1 -1
  40. package/dist/internal/limits.d.ts +28 -0
  41. package/dist/internal/limits.d.ts.map +1 -1
  42. package/dist/internal/limits.js +75 -16
  43. package/dist/internal/limits.js.map +1 -1
  44. package/dist/internal/nodes.d.ts.map +1 -1
  45. package/dist/internal/nodes.js +53 -44
  46. package/dist/internal/nodes.js.map +1 -1
  47. package/dist/internal/patterns.js +50 -15
  48. package/dist/internal/patterns.js.map +1 -1
  49. package/dist/internal/policy-json.js +169 -127
  50. package/dist/internal/policy-json.js.map +1 -1
  51. package/dist/internal/policy-nodes.d.ts.map +1 -1
  52. package/dist/internal/policy-nodes.js +16 -13
  53. package/dist/internal/policy-nodes.js.map +1 -1
  54. package/dist/internal/policy.d.ts.map +1 -1
  55. package/dist/internal/policy.js +38 -22
  56. package/dist/internal/policy.js.map +1 -1
  57. package/dist/internal/resource.js +1 -1
  58. package/dist/internal/rule.d.ts.map +1 -1
  59. package/dist/internal/rule.js +6 -5
  60. package/dist/internal/rule.js.map +1 -1
  61. package/dist/internal/search.d.ts.map +1 -1
  62. package/dist/internal/search.js +106 -92
  63. package/dist/internal/search.js.map +1 -1
  64. package/dist/internal/trampoline.js +19 -5
  65. package/dist/internal/trampoline.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/Authorizer.ts +30 -20
  68. package/src/Condition.ts +317 -86
  69. package/src/Domain.ts +9 -0
  70. package/src/Limits.ts +69 -29
  71. package/src/Policy.ts +143 -21
  72. package/src/internal/analysis.ts +154 -124
  73. package/src/internal/authorizer.ts +29 -14
  74. package/src/internal/bind.ts +193 -136
  75. package/src/internal/catalog.ts +320 -118
  76. package/src/internal/decide.ts +61 -50
  77. package/src/internal/domain.ts +39 -3
  78. package/src/internal/json.ts +212 -117
  79. package/src/internal/limits.ts +101 -31
  80. package/src/internal/nodes.ts +65 -47
  81. package/src/internal/patterns.ts +79 -25
  82. package/src/internal/policy-json.ts +266 -197
  83. package/src/internal/policy-nodes.ts +31 -23
  84. package/src/internal/policy.ts +63 -23
  85. package/src/internal/rule.ts +16 -3
  86. package/src/internal/search.ts +186 -136
  87. package/src/internal/trampoline.ts +51 -4
@@ -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 decode or a `make` copies a struct or a record into a plain object,
140
- * so a struct without the field gives `null` for it.
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
- /** A place in the tree of a catalog, with the places below it. */
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
- /** The field at this place. The root has none. */
148
- readonly field: Field | undefined
149
- /** The places below, by name, in the order of the Schema. */
150
- readonly children: ReadonlyArray<readonly [string, Branch]>
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, Branch>
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: Branch | undefined
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
- /** Every field, a field before the fields below it. */
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 Arr.flatMap(ast.types, (type) => alternatives(type, open))
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
- : alternatives(ast.thunk(), new Set([...open, ast]))
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 a walk of the catalog knows at one place. */
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
- * Makes the branch of a place, with the branches below it. `count` is the
416
- * number of fields so far. It belongs to one walk, and it stops a Schema with
417
- * very many fields before the walk is done.
418
- */
419
- const branchOf = (place: Place, count: { size: number }): Branch => {
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, branchOf(below, count)] as const
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
- const key: Key = {
520
- _tag: "Key",
521
- parameters: Arr.map(signatures, (signature) => signature.parameter),
522
- }
523
- const keyBranch = branchOf(
524
- {
525
- path: [...place.path, key],
526
- schemas: Arr.flatMap(signatures, (signature) =>
527
- alternatives(signature.type, new Set()),
528
- ),
529
- // A record may not have a given key.
530
- missing: true,
531
- top: keyTop,
532
- plain: place.plain && !keyTop,
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
- count,
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
- const fieldsOf = (branch: Branch): ReadonlyArray<Field> => [
540
- ...(branch.field === undefined ? [] : [branch.field]),
541
- ...Arr.flatMap(branch.children, ([, child]) => fieldsOf(child)),
542
- ...(branch.keyBranch === undefined ? [] : fieldsOf(branch.keyBranch)),
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. It throws when a field name has a dot or is
547
- * empty, and when the Schema has more fields than `catalogFields`.
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 root = branchOf(
551
- {
552
- path: [],
553
- schemas: alternatives(ast, new Set()),
554
- missing: false,
555
- top: false,
556
- plain: true,
557
- },
558
- { size: 0 },
559
- )
560
- return { fields: fieldsOf(root), root }
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
- /** Gives the field of a catalog at a path with dots, if the catalog has it. */
564
- export const lookup = (catalog: Catalog, path: string): Field | undefined => {
565
- const names = path.split(".", Limits.fieldPathParts + 1)
566
- if (names.length > Limits.fieldPathParts) {
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 branch = Arr.reduce(
762
+ const found = Arr.reduce(
570
763
  names,
571
- catalog.root as Branch | undefined,
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 named.value
773
+ return {
774
+ branch: catalog.branches[named.value]!,
775
+ path: [...current.path, name],
776
+ }
579
777
  }
580
- return current.key !== undefined && keyFits(current.key, name)
581
- ? current.keyBranch
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 branch?.field
785
+ return found === undefined
786
+ ? undefined
787
+ : fieldAt(found.branch, found.path, parts)
586
788
  }