@glion/util-visit 0.15.0 → 0.15.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 (2) hide show
  1. package/README.md +50 -171
  2. package/package.json +17 -9
package/README.md CHANGED
@@ -1,32 +1,14 @@
1
1
  # @glion/util-visit
2
2
 
3
- ## Introduction
3
+ Visitor for traversing HL7v2 AST trees with ancestor context, depth, and HL7v2-aware sequence numbers.
4
4
 
5
- This package provides a lightweight, type-safe visitor pattern for traversing [HL7v2 AST][hl7v2-ast] trees. Built on top of the battle-tested [unist-util-visit-parents][], it adds HL7v2-specific context while delegating core traversal to a proven implementation.
5
+ ## What it does
6
6
 
7
- ### What is this?
8
-
9
- `hl7v2-util-visit` enables you to walk through any HL7v2 AST tree — from the root message down to individual subcomponents — with full context about where you are in the hierarchy. The visitor pattern:
10
-
11
- - **Works from any starting node** (Root, Segment, Field, Component, etc.)
12
- - **Tracks ancestors** from traversal root to current node
13
- - **Provides visit info** (index, sequence, depth, metadata)
14
- - **Efficient O(n) traversal** — Pre-computed index map for O(1) lookups
15
- - **Battle-tested core** — Delegates traversal to `unist-util-visit-parents`
16
-
17
- ### When should I use this?
18
-
19
- Use `hl7v2-util-visit` when you need to:
20
-
21
- - Validate HL7v2 message structure
22
- - Transform or annotate AST nodes
23
- - Extract specific fields with context
24
- - Analyze message patterns across the tree
25
- - Implement custom processing rules that need parent/ancestor awareness
7
+ Walks an HL7v2 AST from any starting node — Root, Segment, Field, Component, or Subcomponent — calling a visitor function for each matching node. Each call receives the node, its ancestors (root-to-parent), and a `VisitInfo` record with index, HL7v2 sequence number, depth, and extracted metadata. Supports filtering by type string, property object, or predicate, and exposes `SKIP`/`EXIT` control actions. Delegates core traversal to `unist-util-visit-parents` and pre-computes sibling indices for O(1) lookups.
26
8
 
27
9
  ## Install
28
10
 
29
- ```sh
11
+ ```bash
30
12
  npm install @glion/util-visit
31
13
  ```
32
14
 
@@ -42,8 +24,6 @@ const message = parse("MSH|^~\\&|...\rPID|...");
42
24
  visit(message, "segment", (node, ancestors, info) => {
43
25
  console.log(`Segment: ${info.metadata?.header} at depth ${info.depth}`);
44
26
  });
45
- // => Segment: MSH at depth 2
46
- // => Segment: PID at depth 2
47
27
 
48
28
  // Find fields with parent context
49
29
  visit(message, "field", (node, ancestors, info) => {
@@ -54,7 +34,7 @@ visit(message, "field", (node, ancestors, info) => {
54
34
  // Skip processing of sensitive segments
55
35
  visit(message, (node, ancestors, info) => {
56
36
  if (node.type === "segment" && info.metadata?.header === "NTE") {
57
- return SKIP; // Skip NTE segment children
37
+ return SKIP;
58
38
  }
59
39
  });
60
40
  ```
@@ -69,37 +49,37 @@ Visit nodes in an HL7v2 AST tree.
69
49
 
70
50
  #### Parameters
71
51
 
72
- - **`tree`** (`Nodes`) — Tree to traverse (can be any node type, not just Root)
73
- - **`test`** (`string | Partial<Nodes> | Test`) — Optional filter:
74
- - `string`: Match nodes by type (e.g., `'segment'`)
75
- - `Partial<Nodes>`: Match nodes with matching properties (e.g., `{ name: 'PATIENT_GROUP' }`)
76
- - `Test`: Custom function `(node, ancestors) => boolean`
77
- - **`visitor`** (`Visitor`) — Function called for each matching node
52
+ - `tree` (`Nodes`) — Tree to traverse. Can be any node type, not just `Root`.
53
+ - `test` (`string | Partial<Nodes> | Test`, optional) — Filter:
54
+ - `string` — Match nodes by type (e.g., `"segment"`).
55
+ - `Partial<Nodes>` — Match nodes with matching properties (e.g., `{ name: "PATIENT_GROUP" }`).
56
+ - `Test` — Custom function `(node, ancestors) => boolean`.
57
+ - `visitor` (`Visitor`) — Function called for each matching node.
78
58
 
79
59
  #### Returns
80
60
 
81
- `void`
61
+ `void`.
82
62
 
83
- #### Important: Test vs Visitor Functions
63
+ #### Important: test vs visitor functions
84
64
 
85
65
  **If you pass a function as the second argument, it is always treated as a Visitor, never as a Test.**
86
66
 
87
67
  ```typescript
88
- // WRONG - testFn will be treated as a visitor, not a test
68
+ // WRONG — testFn will be treated as a visitor, not a test
89
69
  visit(ast, (node) => node.type === 'segment', ...); // Missing visitor!
90
70
 
91
- // CORRECT - Explicit 3-argument form
71
+ // CORRECT — Explicit 3-argument form
92
72
  visit(ast, (node) => node.type === 'segment', (node, ancestors, info) => {
93
73
  console.log('Visiting segment');
94
74
  });
95
75
 
96
- // CORRECT - Use string or object for simple tests
76
+ // CORRECT — Use string or object for simple tests
97
77
  visit(ast, 'segment', (node, ancestors, info) => {
98
78
  console.log('Visiting segment');
99
79
  });
100
80
  ```
101
81
 
102
- #### Visitor Function
82
+ #### Visitor function
103
83
 
104
84
  ```typescript
105
85
  type Visitor<T extends Nodes = Nodes> = (
@@ -111,17 +91,17 @@ type Visitor<T extends Nodes = Nodes> = (
111
91
 
112
92
  The visitor receives:
113
93
 
114
- - **`node`** — Current AST node
115
- - **`ancestors`** — Array of ancestor nodes from root to parent (not including current node)
116
- - **`info`** — Visit information with index, sequence, depth, and metadata
94
+ - `node` — Current AST node.
95
+ - `ancestors` — Array of ancestor nodes from root to parent (not including current node).
96
+ - `info` — Visit information with index, sequence, depth, and metadata.
117
97
 
118
98
  The visitor can return:
119
99
 
120
- - `undefined` or `void` — Continue traversal normally
121
- - `SKIP` — Skip children of current node
122
- - `EXIT` — Stop traversal immediately
100
+ - `undefined` or `void` — Continue traversal normally.
101
+ - `SKIP` — Skip children of current node.
102
+ - `EXIT` — Stop traversal immediately.
123
103
 
124
- ### VisitInfo
104
+ ### `VisitInfo`
125
105
 
126
106
  ```typescript
127
107
  interface VisitInfo {
@@ -139,28 +119,27 @@ interface VisitInfo {
139
119
  }
140
120
  ```
141
121
 
142
- **Important**: `index` and `sequence` represent the node's **position in the tree**, not its position among filtered results. For example:
122
+ `index` and `sequence` represent the node's **position in the tree**, not its position among filtered results:
143
123
 
144
124
  ```typescript
145
125
  // Structure: MSH segment with fields at positions 1, 2, 3, 4
146
126
  // Filter matches only field 3
147
-
148
127
  visit(
149
128
  ast,
150
129
  (n) => n.type === "field" && hasContent(n),
151
130
  (node, ancestors, info) => {
152
- console.log(info.sequence); // => 3 (position in segment, not "1st match")
131
+ console.log(info.sequence); // 3 (position in segment, not "1st match")
153
132
  }
154
133
  );
155
134
  ```
156
135
 
157
136
  This is correct because HL7v2 paths like `PID.3` refer to tree positions, not filtered positions.
158
137
 
159
- ### Automatic Metadata Extraction
138
+ ### Automatic metadata extraction
160
139
 
161
- The `metadata` field is populated automatically with common metadata:
140
+ The `metadata` field is populated automatically:
162
141
 
163
- | Node Type | Metadata Key | Description |
142
+ | Node type | Metadata key | Description |
164
143
  | --------- | ------------ | ------------------------------------------- |
165
144
  | `segment` | `header` | Segment identifier (e.g., `"MSH"`, `"PID"`) |
166
145
  | `group` | `name` | Group name (e.g., `"PATIENT_GROUP"`) |
@@ -185,7 +164,7 @@ import type {
185
164
 
186
165
  ## Examples
187
166
 
188
- ### Filter by Node Type
167
+ ### Filter by node type
189
168
 
190
169
  ```typescript
191
170
  visit(ast, "segment", (node, ancestors, info) => {
@@ -193,7 +172,7 @@ visit(ast, "segment", (node, ancestors, info) => {
193
172
  });
194
173
  ```
195
174
 
196
- ### Filter by Properties
175
+ ### Filter by properties
197
176
 
198
177
  ```typescript
199
178
  visit(ast, { name: "PATIENT_GROUP" }, (node, ancestors, info) => {
@@ -201,7 +180,7 @@ visit(ast, { name: "PATIENT_GROUP" }, (node, ancestors, info) => {
201
180
  });
202
181
  ```
203
182
 
204
- ### Custom Test Function
183
+ ### Custom test function
205
184
 
206
185
  ```typescript
207
186
  // Visit fields in MSH segment only
@@ -217,21 +196,17 @@ visit(
217
196
  );
218
197
  ```
219
198
 
220
- ### Access Parent and Ancestors
199
+ ### Access parent and ancestors
221
200
 
222
201
  ```typescript
223
202
  visit(ast, "component", (node, ancestors, info) => {
224
- // Get immediate parent
225
203
  const parent = ancestors.at(-1);
226
-
227
- // Find closest segment ancestor
228
204
  const segment = ancestors.findLast((n) => n.type === "segment");
229
-
230
205
  console.log(`Component at depth ${info.depth}`);
231
206
  });
232
207
  ```
233
208
 
234
- ### Control Flow: Skip Children
209
+ ### Control flow: skip children
235
210
 
236
211
  ```typescript
237
212
  visit(ast, (node, ancestors, info) => {
@@ -241,13 +216,13 @@ visit(ast, (node, ancestors, info) => {
241
216
  });
242
217
  ```
243
218
 
244
- ### Control Flow: Exit Early
219
+ ### Control flow: exit early
245
220
 
246
221
  ```typescript
247
- import { EXIT } from '@glion/util-visit';
222
+ import { EXIT } from "@glion/util-visit";
248
223
 
249
224
  let found = false;
250
- visit(ast, 'field', (node, ancestors, info) => {
225
+ visit(ast, "field", (node, ancestors, info) => {
251
226
  if (/* some condition */) {
252
227
  found = true;
253
228
  return EXIT; // Stop traversal completely
@@ -255,7 +230,7 @@ visit(ast, 'field', (node, ancestors, info) => {
255
230
  });
256
231
  ```
257
232
 
258
- ### Start from Any Node
233
+ ### Start from any node
259
234
 
260
235
  ```typescript
261
236
  import { s, f, c } from "@glion/builder";
@@ -269,14 +244,13 @@ visit(segment, "field", (node, ancestors, info) => {
269
244
  });
270
245
  ```
271
246
 
272
- ### Track Nesting Levels
247
+ ### Track nesting levels
273
248
 
274
249
  ```typescript
275
250
  visit(ast, (node, ancestors, info) => {
276
251
  const indent = " ".repeat(info.depth - 1);
277
252
  console.log(`${indent}${node.type} [${info.sequence}]`);
278
253
  });
279
- // Output:
280
254
  // root [1]
281
255
  // segment [1]
282
256
  // segment-header [0]
@@ -285,11 +259,10 @@ visit(ast, (node, ancestors, info) => {
285
259
  // component [1]
286
260
  ```
287
261
 
288
- ### Group Hierarchy Navigation
262
+ ### Group hierarchy navigation
289
263
 
290
264
  ```typescript
291
265
  visit(ast, "segment", (node, ancestors, info) => {
292
- // Get all parent groups
293
266
  const groups = ancestors
294
267
  .filter((n) => n.type === "group")
295
268
  .map((n) => (n as any).name)
@@ -297,12 +270,10 @@ visit(ast, "segment", (node, ancestors, info) => {
297
270
 
298
271
  console.log(`${info.metadata?.header} is in groups: ${groups.join(" > ")}`);
299
272
  });
300
- // => PID is in groups: PATIENT_GROUP
273
+ // PID is in groups: PATIENT_GROUP
301
274
  ```
302
275
 
303
- ## Real-World Use Cases
304
-
305
- ### Validate Required Fields
276
+ ### Validate required fields
306
277
 
307
278
  ```typescript
308
279
  function validateRequiredFields(ast: Root): string[] {
@@ -312,12 +283,10 @@ function validateRequiredFields(ast: Root): string[] {
312
283
  const segment = node as Segment;
313
284
  const header = info.metadata?.header;
314
285
 
315
- // MSH segment must have at least 12 fields
316
286
  if (header === "MSH" && segment.children.length < 12) {
317
- errors.push(`MSH segment missing required fields`);
287
+ errors.push("MSH segment missing required fields");
318
288
  }
319
289
 
320
- // PID segment must have patient ID (PID.3)
321
290
  if (header === "PID") {
322
291
  const patientId = segment.children[3];
323
292
  if (!patientId || patientId.children.length === 0) {
@@ -330,7 +299,7 @@ function validateRequiredFields(ast: Root): string[] {
330
299
  }
331
300
  ```
332
301
 
333
- ### Extract Specific Data with Context
302
+ ### Extract data with context
334
303
 
335
304
  ```typescript
336
305
  interface PatientName {
@@ -350,7 +319,6 @@ function extractPatientNames(ast: Root): PatientName[] {
350
319
  if (nameField?.children[0]?.children[0]) {
351
320
  const nameComponent = nameField.children[0].children[0];
352
321
  const name = (nameComponent.children[0] as Subcomponent)?.value || "";
353
-
354
322
  const groupAncestor = ancestors.find((n) => n.type === "group");
355
323
 
356
324
  names.push({
@@ -365,46 +333,7 @@ function extractPatientNames(ast: Root): PatientName[] {
365
333
  }
366
334
  ```
367
335
 
368
- ### Message Structure Analysis
369
-
370
- ```typescript
371
- interface MessageStructure {
372
- segmentCount: number;
373
- groupCount: number;
374
- maxDepth: number;
375
- segmentTypes: Record<string, number>;
376
- }
377
-
378
- function analyzeStructure(ast: Root): MessageStructure {
379
- const structure: MessageStructure = {
380
- segmentCount: 0,
381
- groupCount: 0,
382
- maxDepth: 0,
383
- segmentTypes: {},
384
- };
385
-
386
- visit(ast, (node, ancestors, info) => {
387
- structure.maxDepth = Math.max(structure.maxDepth, info.depth);
388
-
389
- if (node.type === "segment") {
390
- structure.segmentCount++;
391
- const header = info.metadata?.header as string;
392
- if (header) {
393
- structure.segmentTypes[header] =
394
- (structure.segmentTypes[header] || 0) + 1;
395
- }
396
- }
397
-
398
- if (node.type === "group") {
399
- structure.groupCount++;
400
- }
401
- });
402
-
403
- return structure;
404
- }
405
- ```
406
-
407
- ### Find First Match and Exit
336
+ ### Find first match and exit
408
337
 
409
338
  ```typescript
410
339
  function findFirstObservation(ast: Root, targetCode: string): string | null {
@@ -420,7 +349,7 @@ function findFirstObservation(ast: Root, targetCode: string): string | null {
420
349
  if (code === targetCode) {
421
350
  const valueField = segment.children[5];
422
351
  result = valueField?.children[0]?.children[0]?.children[0]?.value || null;
423
- return EXIT; // Stop traversal
352
+ return EXIT;
424
353
  }
425
354
  });
426
355
 
@@ -428,59 +357,9 @@ function findFirstObservation(ast: Root, targetCode: string): string | null {
428
357
  }
429
358
  ```
430
359
 
431
- ## Architecture
432
-
433
- This library is a thin wrapper around [unist-util-visit-parents][] that adds HL7v2-specific features:
434
-
435
- 1. **Core Traversal**: Delegates to `unist-util-visit-parents` (battle-tested with 50M+ weekly downloads)
436
- 2. **Index Optimization**: Pre-computes node indices for O(1) lookups (avoids O(n²) `indexOf` calls)
437
- 3. **HL7v2 Context**: Adds `VisitInfo` with sequence numbers, depth tracking, and metadata extraction
438
- 4. **Domain Conventions**: Implements HL7v2-specific indexing (segment-header sequence = 0, fields = 1,2,3...)
439
-
440
- This design gives us:
441
-
442
- - ✅ Proven traversal logic (EXIT/SKIP handling, edge cases)
443
- - ✅ Mutation support (inherited from unist)
444
- - ✅ Reverse traversal support (pass `reverse: true` to underlying library)
445
- - ✅ Small codebase (~120 lines vs previous ~180 lines)
446
- - ✅ Ecosystem compatibility (works with other unist utilities)
447
-
448
- ## Performance Characteristics
449
-
450
- - **O(n) traversal** — Single pass through all nodes
451
- - **O(n) index pre-computation** — One-time upfront cost
452
- - **O(1) index lookups** — Avoids O(n²) `indexOf()` anti-pattern
453
- - **O(d) ancestor construction** where d = depth (typically < 10 for HL7v2)
454
- - **Minimal allocations** — Metadata extracted once per node
455
-
456
- ## Types
457
-
458
- ```typescript
459
- export type {
460
- VisitInfo, // { index, sequence, depth, metadata }
461
- Visitor, // (node, ancestors, info) => VisitorResult
462
- VisitorResult, // void | false | 'skip' | ActionTuple
463
- Test, // string | Partial | predicate | null
464
- Predicate, // (node, ancestors) => boolean
465
- } from "@glion/util-visit";
466
- ```
467
-
468
- ## Contributing
469
-
470
- We welcome contributions! Please see our [Contributing Guide][github-contributing] for more details.
471
-
472
- ## Code of Conduct
473
-
474
- To ensure a welcoming and positive environment, we have a [Code of Conduct][github-code-of-conduct] that all contributors and participants are expected to adhere to.
475
-
476
- ## License
477
-
478
- Copyright 2025 Rethink Health, SUARL. All rights reserved.
360
+ ## Part of Glion
479
361
 
480
- This program is licensed to you under the terms of the [MIT License](https://opensource.org/licenses/MIT). See the [LICENSE][github-license] file for details.
362
+ `@glion/util-visit` is part of **[Glion]**, the application framework for HL7v2. See the [Glion README] for the full package catalog and architecture.
481
363
 
482
- [github-code-of-conduct]: https://github.com/rethinkhealth/glion/blob/main/CODE_OF_CONDUCT.md
483
- [github-license]: https://github.com/rethinkhealth/glion/blob/main/LICENSE
484
- [github-contributing]: https://github.com/rethinkhealth/glion/blob/main/CONTRIBUTING.md
485
- [hl7v2-ast]: https://github.com/rethinkhealth/glion/tree/main/packages/hl7v2-ast
486
- [unist-util-visit-parents]: https://github.com/syntax-tree/unist-util-visit-parents
364
+ [Glion]: https://github.com/rethinkhealth/glion#readme
365
+ [Glion README]: https://github.com/rethinkhealth/glion#readme
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glion/util-visit",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "An AST visitor for HL7v2 messages",
5
5
  "keywords": [
6
6
  "health",
@@ -16,7 +16,11 @@
16
16
  "name": "Melek Somai",
17
17
  "email": "melek@rethinkhealth.io"
18
18
  },
19
- "repository": "rethinkhealth/glion.git",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/rethinkhealth/glion.git",
22
+ "directory": "packages/util-visit"
23
+ },
20
24
  "files": [
21
25
  "dist"
22
26
  ],
@@ -35,15 +39,17 @@
35
39
  "devDependencies": {
36
40
  "@types/node": "^25.5.0",
37
41
  "@vitest/coverage-v8": "4.1.2",
42
+ "publint": "0.3.18",
38
43
  "tsdown": "0.21.7",
39
44
  "typescript": "^5.9.3",
40
45
  "vitest": "4.1.2",
41
- "@glion/ast": "0.15.0",
42
- "@glion/builder": "0.15.0",
43
- "@glion/config": "0.15.0",
44
- "@glion/testing": "0.15.0",
45
- "@glion/parser": "0.15.0",
46
- "@glion/tsconfig": "0.15.0"
46
+ "@glion/builder": "0.15.2",
47
+ "@glion/check-readme": "0.15.2",
48
+ "@glion/config": "0.15.2",
49
+ "@glion/parser": "0.15.2",
50
+ "@glion/ast": "0.15.2",
51
+ "@glion/testing": "0.15.2",
52
+ "@glion/tsconfig": "0.15.2"
47
53
  },
48
54
  "engines": {
49
55
  "node": ">=18"
@@ -52,8 +58,10 @@
52
58
  "scripts": {
53
59
  "build": "tsdown && tsc --emitDeclarationOnly",
54
60
  "check-types": "tsc --noEmit",
61
+ "check:readme": "glion-check-readme",
55
62
  "test": "vitest run",
56
63
  "test:coverage": "vitest run --coverage",
57
- "test:watch": "vitest"
64
+ "test:watch": "vitest",
65
+ "publint": "publint --strict"
58
66
  }
59
67
  }