@rethinkhealth/hl7v2-util-visit 0.3.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Rethink Health
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,498 @@
1
+ # @rethinkhealth/hl7v2-util-visit
2
+
3
+ ## Introduction
4
+
5
+ This package provides a lightweight, type-safe visitor pattern for traversing [HL7v2 AST][hl7v2-ast] trees. It implements a pure functional approach inspired by [unist-util-visit-parents][] and [babel-traverse][], offering immutable path tracking with rich metadata.
6
+
7
+ ### What is this?
8
+
9
+ `hl7v2-visitor` 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 immutable paths** from traversal root to current node
13
+ - **Provides rich metadata** (segment headers, group names, delimiters)
14
+ - **Immutable path copies** — New path array created per level via spread operator
15
+ - **Minimal allocations** — Metadata extracted once per node, path arrays reused within same level
16
+
17
+ ### When should I use this?
18
+
19
+ Use `hl7v2-visitor` 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
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ npm install @rethinkhealth/hl7v2-util-visit
31
+ ```
32
+
33
+ ## Use
34
+
35
+ ```typescript
36
+ import { visit } from '@rethinkhealth/hl7v2-util-visit';
37
+ import { parse } from '@rethinkhealth/hl7v2-parser'; // hypothetical parser
38
+
39
+ const message = parse('MSH|^~\\&|...\rPID|...');
40
+
41
+ // Visit all segments
42
+ visit(message, 'segment', (node, path) => {
43
+ const entry = path.at(-1);
44
+ console.log(`Segment: ${entry?.data?.header} at level ${entry?.level}`);
45
+ });
46
+ // => Segment: MSH at level 2
47
+ // => Segment: PID at level 2
48
+
49
+ // Find fields with parent context
50
+ visit(message, 'field', (node, path) => {
51
+ const segment = path.find(e => e.type === 'segment');
52
+ console.log(`Field in ${segment?.data?.header}`);
53
+ });
54
+
55
+ // Skip processing of sensitive segments
56
+ visit(message, (node, path) => {
57
+ if (node.type === 'segment' && node.children[0]?.value === 'NTE') {
58
+ return 'skip'; // Skip NTE segment children
59
+ }
60
+ // Process other nodes
61
+ });
62
+ ```
63
+
64
+ ## API
65
+
66
+ ### `visit(tree, visitor)`
67
+
68
+ ### `visit(tree, test, visitor)`
69
+
70
+ Visit nodes in an HL7v2 AST tree.
71
+
72
+ #### Parameters
73
+
74
+ - **`tree`** (`Nodes`) — Tree to traverse (can be any node type, not just Root)
75
+ - **`test`** (`string | Partial<Nodes> | Test`) — Optional filter:
76
+ - `string`: Match nodes by type (e.g., `'segment'`)
77
+ - `Partial<Nodes>`: Match nodes with matching properties (e.g., `{ name: 'PATIENT_GROUP' }`)
78
+ - `Test`: Custom function `(node, path) => boolean`
79
+ - **`visitor`** (`Visitor`) — Function called for each matching node
80
+
81
+ #### Returns
82
+
83
+ `void`
84
+
85
+ #### Important: Test vs Visitor Functions
86
+
87
+ **If you pass a function as the second argument, it is always treated as a Visitor, never as a Test.**
88
+
89
+ ```typescript
90
+ // ❌ WRONG - testFn will be treated as a visitor, not a test
91
+ visit(ast, (node, path) => node.type === 'segment', ...); // Missing visitor!
92
+
93
+ // ✅ CORRECT - Explicit 3-argument form
94
+ visit(ast, (node, path) => node.type === 'segment', (node, path) => {
95
+ console.log('Visiting segment');
96
+ });
97
+
98
+ // ✅ CORRECT - Use string or object for simple tests
99
+ visit(ast, 'segment', (node, path) => {
100
+ console.log('Visiting segment');
101
+ });
102
+ ```
103
+
104
+ This design prevents runtime errors that could occur after side effects. Since Test and Visitor functions have the same signature but different return types, they cannot be distinguished at compile time.
105
+
106
+ #### Visitor Function
107
+
108
+ ```typescript
109
+ type Visitor = (node: Nodes, path: Path) => Action | void | undefined;
110
+ ```
111
+
112
+ The visitor receives:
113
+ - **`node`** — Current AST node
114
+ - **`path`** — Ordered array of `PathEntry` from traversal root to current node
115
+
116
+ The visitor can return:
117
+ - `undefined` or `void` — Continue traversal normally
118
+ - `'skip'` — Skip children of current node
119
+ - `'exit'` — Stop traversal immediately
120
+
121
+ ## Path Structure
122
+
123
+ Each entry in the `path` array has the following structure:
124
+
125
+ ```typescript
126
+ interface PathEntry {
127
+ type: string; // Node type: 'root', 'segment', 'field', 'component', etc.
128
+ level: number; // 1-based depth (root=1, children=2, ...)
129
+ index: number; // 1-based position within siblings
130
+ node: Nodes; // Reference to the actual AST node
131
+ data?: Record<string, unknown>; // Extensible metadata
132
+ }
133
+ ```
134
+
135
+ ### Automatic Metadata Extraction
136
+
137
+ The `data` field is populated automatically with common metadata:
138
+
139
+ | Node Type | Metadata Key | Description |
140
+ |-----------|--------------|-------------|
141
+ | `segment` | `header` | Segment identifier (e.g., `"MSH"`, `"PID"`) |
142
+ | `group` | `name` | Group name (e.g., `"PATIENT_GROUP"`) |
143
+
144
+ Example:
145
+
146
+ ```typescript
147
+ visit(ast, 'segment', (node, path) => {
148
+ const entry = path.at(-1);
149
+ console.log(entry?.data?.header); // "PID"
150
+ });
151
+ ```
152
+
153
+ ## Examples
154
+
155
+ ### Filter by Node Type
156
+
157
+ ```typescript
158
+ // Visit all segments
159
+ visit(ast, 'segment', (node, path) => {
160
+ console.log(`Found segment: ${node.children[0]?.value}`);
161
+ });
162
+ ```
163
+
164
+ ### Filter by Properties
165
+
166
+ ```typescript
167
+ // Visit specific group
168
+ visit(ast, { name: 'PATIENT_GROUP' }, (node, path) => {
169
+ console.log('Inside PATIENT_GROUP');
170
+ });
171
+ ```
172
+
173
+ ### Custom Test Function
174
+
175
+ ```typescript
176
+ // Visit fields in MSH segment only
177
+ visit(
178
+ ast,
179
+ (node, path) => {
180
+ const parent = path[path.length - 2];
181
+ return node.type === 'field' && parent?.data?.header === 'MSH';
182
+ },
183
+ (node, path) => {
184
+ console.log('MSH field found');
185
+ }
186
+ );
187
+ ```
188
+
189
+ ### Access Parent and Ancestors
190
+
191
+ ```typescript
192
+ visit(ast, 'component', (node, path) => {
193
+ // Get immediate parent
194
+ const parent = path[path.length - 2];
195
+
196
+ // Get all ancestors
197
+ const ancestors = path.slice(0, -1);
198
+
199
+ // Find closest segment
200
+ const segment = path.findLast(e => e.type === 'segment');
201
+ console.log(`Component in ${segment?.data?.header}`);
202
+ });
203
+ ```
204
+
205
+ ### Control Flow: Skip Children
206
+
207
+ ```typescript
208
+ visit(ast, (node, path) => {
209
+ if (node.type === 'segment' && node.children[0]?.value === 'OBX') {
210
+ return 'skip'; // Don't process OBX segment children
211
+ }
212
+ });
213
+ ```
214
+
215
+ ### Control Flow: Exit Early
216
+
217
+ ```typescript
218
+ let found = false;
219
+ visit(ast, 'field', (node, path) => {
220
+ if (/* some condition */) {
221
+ found = true;
222
+ return 'exit'; // Stop traversal completely
223
+ }
224
+ });
225
+ ```
226
+
227
+ ### Start from Any Node
228
+
229
+ ```typescript
230
+ import { s, f, c } from '@rethinkhealth/hl7v2-builder';
231
+
232
+ // Create a standalone segment
233
+ const segment = s('PID', f(c('value1')), f(c('value2')));
234
+
235
+ // Traverse from segment (not root)
236
+ visit(segment, 'field', (node, path) => {
237
+ // path[0] will be the segment, not a root node
238
+ console.log(`Field at index ${path.at(-1)?.index}`);
239
+ });
240
+ ```
241
+
242
+ ### Track Nesting Levels
243
+
244
+ ```typescript
245
+ visit(ast, (node, path) => {
246
+ const entry = path.at(-1);
247
+ const indent = ' '.repeat(entry!.level - 1);
248
+ console.log(`${indent}${entry!.type} [${entry!.index}]`);
249
+ });
250
+ // Output:
251
+ // root [1]
252
+ // segment [1]
253
+ // field [2]
254
+ // field-repetition [1]
255
+ // component [1]
256
+ ```
257
+
258
+ ### Group Hierarchy Navigation
259
+
260
+ ```typescript
261
+ visit(ast, 'segment', (node, path) => {
262
+ // Get all parent groups
263
+ const groups = path
264
+ .filter(e => e.type === 'group')
265
+ .map(e => e.data?.name)
266
+ .filter((name): name is string => typeof name === 'string');
267
+
268
+ const segmentHeader = path.at(-1)?.data?.header;
269
+ console.log(`${segmentHeader} is in groups: ${groups.join(' > ')}`);
270
+ });
271
+ // => PID is in groups: PATIENT_GROUP
272
+ ```
273
+
274
+ ## Real-World Use Cases
275
+
276
+ ### Validate Required Fields
277
+
278
+ ```typescript
279
+ import { visit } from '@rethinkhealth/hl7v2-util-visit';
280
+
281
+ function validateRequiredFields(ast: Root): string[] {
282
+ const errors: string[] = [];
283
+
284
+ visit(ast, 'segment', (node, path) => {
285
+ const segment = node as Segment;
286
+ const header = segment.children[0]?.value;
287
+
288
+ // MSH segment must have at least 12 fields
289
+ if (header === 'MSH' && segment.children.length < 12) {
290
+ errors.push(`MSH segment missing required fields (found ${segment.children.length - 1})`);
291
+ }
292
+
293
+ // PID segment must have patient ID (PID.3)
294
+ if (header === 'PID') {
295
+ const patientId = segment.children[3]; // Remember: 1-based, [0] is header
296
+ if (!patientId || patientId.children.length === 0) {
297
+ errors.push('PID segment missing required Patient ID (PID.3)');
298
+ }
299
+ }
300
+ });
301
+
302
+ return errors;
303
+ }
304
+ ```
305
+
306
+ ### Extract Specific Data with Context
307
+
308
+ ```typescript
309
+ // Extract all patient names with their segment location
310
+ interface PatientName {
311
+ name: string;
312
+ segmentIndex: number;
313
+ inGroup?: string;
314
+ }
315
+
316
+ function extractPatientNames(ast: Root): PatientName[] {
317
+ const names: PatientName[] = [];
318
+
319
+ visit(ast, 'segment', (node, path) => {
320
+ const segment = node as Segment;
321
+ const header = segment.children[0]?.value;
322
+
323
+ if (header === 'PID') {
324
+ // PID.5 is patient name
325
+ const nameField = segment.children[5];
326
+ if (nameField?.children[0]?.children[0]) {
327
+ const nameComponent = nameField.children[0].children[0];
328
+ const name = (nameComponent.children[0] as Subcomponent)?.value || '';
329
+
330
+ // Get segment's position in parent
331
+ const segmentEntry = path.at(-1);
332
+
333
+ // Check if inside a group
334
+ const groupEntry = path.find(e => e.type === 'group');
335
+
336
+ names.push({
337
+ name,
338
+ segmentIndex: segmentEntry?.index || 0,
339
+ inGroup: groupEntry?.data?.name as string | undefined,
340
+ });
341
+ }
342
+ }
343
+ });
344
+
345
+ return names;
346
+ }
347
+ ```
348
+
349
+ ### Message Structure Analysis
350
+
351
+ ```typescript
352
+ // Analyze message structure and generate a summary
353
+ interface MessageStructure {
354
+ segmentCount: number;
355
+ groupCount: number;
356
+ maxDepth: number;
357
+ segmentTypes: Record<string, number>;
358
+ }
359
+
360
+ function analyzeStructure(ast: Root): MessageStructure {
361
+ const structure: MessageStructure = {
362
+ segmentCount: 0,
363
+ groupCount: 0,
364
+ maxDepth: 0,
365
+ segmentTypes: {},
366
+ };
367
+
368
+ visit(ast, (node, path) => {
369
+ const entry = path.at(-1);
370
+
371
+ // Track max depth
372
+ if (entry && entry.level > structure.maxDepth) {
373
+ structure.maxDepth = entry.level;
374
+ }
375
+
376
+ // Count segments and types
377
+ if (node.type === 'segment') {
378
+ structure.segmentCount++;
379
+ const header = entry?.data?.header as string;
380
+ if (header) {
381
+ structure.segmentTypes[header] = (structure.segmentTypes[header] || 0) + 1;
382
+ }
383
+ }
384
+
385
+ // Count groups
386
+ if (node.type === 'group') {
387
+ structure.groupCount++;
388
+ }
389
+ });
390
+
391
+ return structure;
392
+ }
393
+ ```
394
+
395
+ ### Remove Sensitive Data
396
+
397
+ ```typescript
398
+ // Redact sensitive fields (like SSN in PID.19)
399
+ function redactSensitiveData(ast: Root): void {
400
+ visit(ast, 'segment', (node) => {
401
+ const segment = node as Segment;
402
+ const header = segment.children[0]?.value;
403
+
404
+ if (header === 'PID') {
405
+ // Redact SSN (PID.19)
406
+ const ssnField = segment.children[19];
407
+ if (ssnField?.children[0]?.children[0]?.children[0]) {
408
+ const subcomponent = ssnField.children[0].children[0].children[0];
409
+ if (subcomponent.type === 'subcomponent') {
410
+ subcomponent.value = '***-**-****';
411
+ }
412
+ }
413
+ }
414
+
415
+ // Continue traversal but don't skip - we might need to redact multiple segments
416
+ });
417
+ }
418
+ ```
419
+
420
+ ### Performance: Early Exit Optimization
421
+
422
+ ```typescript
423
+ // Find first occurrence of a specific observation and exit
424
+ function findFirstObservation(ast: Root, targetCode: string): string | null {
425
+ let result: string | null = null;
426
+
427
+ visit(ast, 'segment', (node) => {
428
+ const segment = node as Segment;
429
+ const header = segment.children[0]?.value;
430
+
431
+ if (header === 'OBX') {
432
+ // OBX.3 is observation identifier
433
+ const identifierField = segment.children[3];
434
+ const code = identifierField?.children[0]?.children[0]?.children[0]?.value;
435
+
436
+ if (code === targetCode) {
437
+ // OBX.5 is observation value
438
+ const valueField = segment.children[5];
439
+ result = valueField?.children[0]?.children[0]?.children[0]?.value || null;
440
+
441
+ // Exit immediately - we found what we need
442
+ return 'exit';
443
+ }
444
+ }
445
+ });
446
+
447
+ return result;
448
+ }
449
+ ```
450
+
451
+ ## Types
452
+
453
+ This package exports the following TypeScript types:
454
+
455
+ ```typescript
456
+ export type {
457
+ Action, // 'skip' | 'exit'
458
+ ChildProvider, // (node: Nodes) => Nodes[] | undefined
459
+ Path, // readonly PathEntry[]
460
+ PathEntry, // { type, level, index, node, data? }
461
+ Test, // (node: Nodes, path: Path) => boolean
462
+ Visitor, // (node: Nodes, path: Path) => Action | void | undefined
463
+ } from '@rethinkhealth/hl7v2-util-visit';
464
+ ```
465
+
466
+ ## Performance Characteristics
467
+
468
+ - **O(n) traversal** — Single pass through all nodes
469
+ - **O(d) path construction** where d = depth (typically < 10 for HL7v2 messages)
470
+ - **Zero defensive copying** — Paths reference the same array during visitor calls
471
+ - **Minimal allocations** — Metadata extracted once per node
472
+
473
+ ## Contributing
474
+
475
+ We welcome contributions! Please see our [Contributing Guide][github-contributing] for more details.
476
+
477
+ 1. Fork the repository
478
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
479
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
480
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
481
+ 5. Open a Pull Request
482
+
483
+ ## Code of Conduct
484
+
485
+ 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.
486
+
487
+ ## License
488
+
489
+ Copyright 2025 Rethink Health, SUARL. All rights reserved.
490
+
491
+ This program is licensed to you under the terms of the [MIT License](https://opensource.org/licenses/MIT). This program is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [LICENSE][github-license] file for details.
492
+
493
+ [github-code-of-conduct]: https://github.com/rethinkhealth/hl7v2/blob/main/CODE_OF_CONDUCT.md
494
+ [github-license]: https://github.com/rethinkhealth/hl7v2/blob/main/LICENSE
495
+ [github-contributing]: https://github.com/rethinkhealth/hl7v2/blob/main/CONTRIBUTING.md
496
+ [hl7v2-ast]: https://github.com/rethinkhealth/hl7v2/tree/main/packages/hl7v2-ast
497
+ [unist-util-visit-parents]: https://github.com/syntax-tree/unist-util-visit-parents
498
+ [babel-traverse]: https://github.com/babel/babel/tree/main/packages/babel-traverse
@@ -0,0 +1,10 @@
1
+ import type { Nodes } from "@rethinkhealth/hl7v2-ast";
2
+ import type { Action, Test, Visitor } from "./types";
3
+ export type { Action, ChildProvider, Path, PathEntry, Test, Visitor, } from "./types";
4
+ export declare const EXIT: Action;
5
+ export declare const SKIP: Action;
6
+ export declare function visit(tree: Nodes, visitor: Visitor): void;
7
+ export declare function visit<Type extends Nodes["type"]>(tree: Nodes, test: Type, visitor: Visitor<Extract<Nodes, {
8
+ type: Type;
9
+ }>>): void;
10
+ export declare function visit<T extends Nodes>(tree: Nodes, test: Test<T>, visitor: Visitor<T>): void;
package/dist/index.js ADDED
@@ -0,0 +1,117 @@
1
+ // src/traversal.ts
2
+ function createTraversal(childProvider) {
3
+ return function traverse(root, visitor) {
4
+ function visit2(node, path, index, level) {
5
+ const entry = {
6
+ type: node.type,
7
+ level,
8
+ index,
9
+ node,
10
+ // Extract common metadata if available
11
+ data: extractMetadata(node)
12
+ };
13
+ const currentPath = [...path, entry];
14
+ const action = visitor(node, currentPath);
15
+ if (action === "exit") {
16
+ return "exit";
17
+ }
18
+ if (action === "skip") {
19
+ return;
20
+ }
21
+ const children = childProvider(node);
22
+ if (children?.length) {
23
+ for (let i = 0; i < children.length; i++) {
24
+ const child = children[i];
25
+ if (!child) {
26
+ continue;
27
+ }
28
+ const result = visit2(child, currentPath, i + 1, level + 1);
29
+ if (result === "exit") {
30
+ return "exit";
31
+ }
32
+ }
33
+ }
34
+ return;
35
+ }
36
+ visit2(root, [], 1, 1);
37
+ };
38
+ }
39
+ function extractMetadata(node) {
40
+ switch (node.type) {
41
+ case "group": {
42
+ if (node.name !== void 0) {
43
+ return { name: node.name };
44
+ }
45
+ return;
46
+ }
47
+ case "segment": {
48
+ const header = node.children[0];
49
+ if (header?.type === "segment-header") {
50
+ return { header: header.value };
51
+ }
52
+ return;
53
+ }
54
+ default:
55
+ return;
56
+ }
57
+ }
58
+
59
+ // src/utils.ts
60
+ function createTest(test) {
61
+ if (test === null) {
62
+ return () => true;
63
+ }
64
+ if (typeof test === "string") {
65
+ return (node) => node.type === test;
66
+ }
67
+ if (typeof test === "function") {
68
+ return test;
69
+ }
70
+ return (node) => {
71
+ for (const key of Object.keys(test)) {
72
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
73
+ continue;
74
+ }
75
+ const testValue = test[key];
76
+ const nodeValue = node[key];
77
+ if (testValue === void 0) {
78
+ if (Object.hasOwn(node, key) && nodeValue !== void 0) {
79
+ return false;
80
+ }
81
+ } else if (nodeValue !== testValue) {
82
+ return false;
83
+ }
84
+ }
85
+ return true;
86
+ };
87
+ }
88
+
89
+ // src/index.ts
90
+ var EXIT = "exit";
91
+ var SKIP = "skip";
92
+ function visit(tree, arg2, arg3) {
93
+ let test = null;
94
+ let visitor;
95
+ if (arg3 === void 0) {
96
+ visitor = arg2;
97
+ } else {
98
+ test = arg2;
99
+ visitor = arg3;
100
+ }
101
+ const predicate = createTest(test);
102
+ const childProvider = (node) => "children" in node && Array.isArray(node.children) ? node.children : void 0;
103
+ const traverse = createTraversal(childProvider);
104
+ const wrappedVisitor = (node, path) => {
105
+ if (predicate(node, path)) {
106
+ return visitor(node, path);
107
+ }
108
+ return;
109
+ };
110
+ traverse(tree, wrappedVisitor);
111
+ }
112
+ export {
113
+ EXIT,
114
+ SKIP,
115
+ visit
116
+ };
117
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/traversal.ts","../src/utils.ts","../src/index.ts"],"sourcesContent":["import type { Nodes } from \"@rethinkhealth/hl7v2-ast\";\nimport type { ChildProvider, Path, PathEntry, Visitor } from \"./types\";\n\n/**\n * Creates a traversal function with the given child provider.\n * Pure functional approach - no classes, no mutation.\n *\n * Assumptions:\n * - Uses 1-based indexing for compatibility with HL7v2 field numbering convention\n * - Level 1 is the traversal root node (not necessarily a Root node type)\n * - Path arrays are immutable - new array created at each level via spread operator\n * - undefined/null children are safely skipped\n *\n * @param childProvider - Function to extract children from nodes\n * @returns Traversal function\n */\nexport function createTraversal(childProvider: ChildProvider) {\n /**\n * Traverse the tree starting from root.\n *\n * @param root - Starting node (can be any node type, not just Root)\n * @param visitor - Function called for each node\n */\n return function traverse(root: Nodes, visitor: Visitor): void {\n /**\n * Internal recursive function.\n * Uses immutable path construction (concat) like unist-visit-parents.\n *\n * @param node - Current node\n * @param path - Immutable path array from traversal root to parent\n * @param index - 1-based index within siblings (HL7v2 convention: MSH.1, MSH.2, etc.)\n * @param level - 1-based depth level (root=1, children=2, etc.)\n * @returns Control flow action\n */\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: fine\n function visit(\n node: Nodes,\n path: Path,\n index: number,\n level: number\n ): \"exit\" | \"skip\" | undefined {\n // Create path entry for current node\n const entry: PathEntry = {\n type: node.type,\n level,\n index,\n node,\n // Extract common metadata if available\n data: extractMetadata(node),\n };\n\n // Immutable path extension (like unist-visit-parents)\n const currentPath = [...path, entry];\n\n // Call visitor with current node and full path\n const action = visitor(node, currentPath);\n\n // Handle exit immediately\n if (action === \"exit\") {\n return \"exit\";\n }\n\n // Skip children if requested\n if (action === \"skip\") {\n return;\n }\n\n // Traverse children unless skipped\n const children = childProvider(node);\n if (children?.length) {\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n if (!child) {\n continue;\n }\n\n const result = visit(child, currentPath, i + 1, level + 1);\n\n if (result === \"exit\") {\n return \"exit\";\n }\n }\n }\n\n return;\n }\n\n // Start traversal with empty path, index 1, level 1\n visit(root, [], 1, 1);\n };\n}\n\n/**\n * Extract common metadata from node.\n * Extensible - can add more fields as needed.\n *\n * @param node - AST node\n * @returns Metadata object or undefined\n */\nfunction extractMetadata(node: Nodes): Record<string, unknown> | undefined {\n switch (node.type) {\n case \"group\": {\n if (node.name !== undefined) {\n return { name: node.name };\n }\n return;\n }\n\n case \"segment\": {\n const header = node.children[0];\n if (header?.type === \"segment-header\") {\n return { header: header.value };\n }\n return;\n }\n\n default:\n return;\n }\n}\n","import type { Nodes } from \"@rethinkhealth/hl7v2-ast\";\nimport type { Path, Test } from \"./types\";\n\n/**\n * Create test predicate from various input types.\n *\n * Assumptions:\n * - null test matches all nodes\n * - String test matches by node.type property\n * - Object test uses strict equality (===) for property matching\n * - Explicit undefined values in test object check for property absence\n * - Dangerous keys (__proto__, constructor, prototype) are filtered for security\n *\n * @param test - Filter criteria: null (all), string (type), object (properties), or function\n * @returns Predicate function that returns true if node matches test criteria\n */\nexport function createTest(\n test: Test<Nodes>\n): (node: Nodes, path: Path) => boolean {\n if (test === null) {\n return () => true;\n }\n if (typeof test === \"string\") {\n return (node) => node.type === test;\n }\n if (typeof test === \"function\") {\n return test;\n }\n // Object property matching\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Property matching requires checking multiple conditions\n return (node) => {\n for (const key of Object.keys(test)) {\n // Guard against prototype pollution\n if (key === \"__proto__\" || key === \"constructor\" || key === \"prototype\") {\n continue;\n }\n\n const testValue = test[key as keyof typeof test];\n // biome-ignore lint/suspicious/noExplicitAny: Need to access arbitrary properties on node\n const nodeValue = (node as any)[key];\n\n // If test has explicit undefined, check property doesn't exist or is undefined\n if (testValue === undefined) {\n if (Object.hasOwn(node, key) && nodeValue !== undefined) {\n return false;\n }\n } else if (nodeValue !== testValue) {\n // For non-undefined values, strict equality check\n return false;\n }\n }\n return true;\n };\n}\n","import type { Nodes } from \"@rethinkhealth/hl7v2-ast\";\nimport { createTraversal } from \"./traversal\";\nimport type { Action, Test, Visitor } from \"./types\";\nimport { createTest } from \"./utils\";\n\n// Export all types\nexport type {\n Action,\n ChildProvider,\n Path,\n PathEntry,\n Test,\n Visitor,\n} from \"./types\";\n\nexport const EXIT: Action = \"exit\" as const;\nexport const SKIP: Action = \"skip\" as const;\n\n// Overload signatures\nexport function visit(tree: Nodes, visitor: Visitor): void;\nexport function visit<Type extends Nodes[\"type\"]>(\n tree: Nodes,\n test: Type,\n visitor: Visitor<Extract<Nodes, { type: Type }>>\n): void;\nexport function visit<T extends Nodes>(\n tree: Nodes,\n test: Test<T>,\n visitor: Visitor<T>\n): void;\n\n/**\n * Visit nodes in an HL7 AST tree.\n * Pure functional implementation - no classes, no mutations.\n *\n * @param tree - The tree to traverse (can be any node type, not just Root)\n * @param test - Optional test to filter nodes (type string, partial match, or Test function)\n * @param visitor - Function called for each matching node\n *\n * @remarks\n * **Important**: If you pass a function as the second argument, it is always treated\n * as a Visitor, never as a Test. To use a Test function, you MUST provide both the\n * test and visitor parameters: `visit(tree, testFn, visitorFn)`.\n *\n * This design choice prevents runtime checks that could miss errors and cause side\n * effects before detection. Test and Visitor functions have the same signature but\n * different return types, making them indistinguishable without execution.\n *\n * Performance characteristics:\n * - O(n) time complexity - single depth-first traversal\n * - O(d) space for path where d = tree depth (typically <10 for HL7v2)\n * - Path arrays are created once per level via spread operator\n * - Metadata extracted lazily only when needed\n * - No defensive copying - paths reused during visitor execution\n */\nexport function visit<T extends Nodes>(\n tree: Nodes,\n arg2: Visitor<T> | Test<T>,\n arg3?: Visitor<T>\n): void {\n let test: Test<T> = null;\n let visitor: Visitor<T>;\n\n // Handle overloads - simple discrimination based on arg count\n if (arg3 === undefined) {\n // 2-argument form: visit(tree, visitor)\n // Function assumed to be Visitor (not Test)\n visitor = arg2 as Visitor<T>;\n } else {\n // 3-argument form: visit(tree, test, visitor)\n test = arg2 as Test<T>;\n visitor = arg3;\n }\n\n // Create test predicate\n const predicate = createTest(test as Test<Nodes>);\n\n // Create child provider\n const childProvider = (node: Nodes) =>\n \"children\" in node && Array.isArray(node.children)\n ? (node.children as Nodes[])\n : undefined;\n\n // Create traversal function\n const traverse = createTraversal(childProvider);\n\n // Wrap visitor to apply test\n const wrappedVisitor: Visitor = (node, path) => {\n if (predicate(node, path)) {\n return visitor(node as T, path);\n }\n return;\n };\n\n // Start traversal\n traverse(tree, wrappedVisitor);\n}\n"],"mappings":";AAgBO,SAAS,gBAAgB,eAA8B;AAO5D,SAAO,SAAS,SAAS,MAAa,SAAwB;AAY5D,aAASA,OACP,MACA,MACA,OACA,OAC6B;AAE7B,YAAM,QAAmB;AAAA,QACvB,MAAM,KAAK;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA;AAAA,QAEA,MAAM,gBAAgB,IAAI;AAAA,MAC5B;AAGA,YAAM,cAAc,CAAC,GAAG,MAAM,KAAK;AAGnC,YAAM,SAAS,QAAQ,MAAM,WAAW;AAGxC,UAAI,WAAW,QAAQ;AACrB,eAAO;AAAA,MACT;AAGA,UAAI,WAAW,QAAQ;AACrB;AAAA,MACF;AAGA,YAAM,WAAW,cAAc,IAAI;AACnC,UAAI,UAAU,QAAQ;AACpB,iBAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,gBAAM,QAAQ,SAAS,CAAC;AACxB,cAAI,CAAC,OAAO;AACV;AAAA,UACF;AAEA,gBAAM,SAASA,OAAM,OAAO,aAAa,IAAI,GAAG,QAAQ,CAAC;AAEzD,cAAI,WAAW,QAAQ;AACrB,mBAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF;AAEA;AAAA,IACF;AAGA,IAAAA,OAAM,MAAM,CAAC,GAAG,GAAG,CAAC;AAAA,EACtB;AACF;AASA,SAAS,gBAAgB,MAAkD;AACzE,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK,SAAS;AACZ,UAAI,KAAK,SAAS,QAAW;AAC3B,eAAO,EAAE,MAAM,KAAK,KAAK;AAAA,MAC3B;AACA;AAAA,IACF;AAAA,IAEA,KAAK,WAAW;AACd,YAAM,SAAS,KAAK,SAAS,CAAC;AAC9B,UAAI,QAAQ,SAAS,kBAAkB;AACrC,eAAO,EAAE,QAAQ,OAAO,MAAM;AAAA,MAChC;AACA;AAAA,IACF;AAAA,IAEA;AACE;AAAA,EACJ;AACF;;;ACvGO,SAAS,WACd,MACsC;AACtC,MAAI,SAAS,MAAM;AACjB,WAAO,MAAM;AAAA,EACf;AACA,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAO,CAAC,SAAS,KAAK,SAAS;AAAA,EACjC;AACA,MAAI,OAAO,SAAS,YAAY;AAC9B,WAAO;AAAA,EACT;AAGA,SAAO,CAAC,SAAS;AACf,eAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AAEnC,UAAI,QAAQ,eAAe,QAAQ,iBAAiB,QAAQ,aAAa;AACvE;AAAA,MACF;AAEA,YAAM,YAAY,KAAK,GAAwB;AAE/C,YAAM,YAAa,KAAa,GAAG;AAGnC,UAAI,cAAc,QAAW;AAC3B,YAAI,OAAO,OAAO,MAAM,GAAG,KAAK,cAAc,QAAW;AACvD,iBAAO;AAAA,QACT;AAAA,MACF,WAAW,cAAc,WAAW;AAElC,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;;;ACtCO,IAAM,OAAe;AACrB,IAAM,OAAe;AAuCrB,SAAS,MACd,MACA,MACA,MACM;AACN,MAAI,OAAgB;AACpB,MAAI;AAGJ,MAAI,SAAS,QAAW;AAGtB,cAAU;AAAA,EACZ,OAAO;AAEL,WAAO;AACP,cAAU;AAAA,EACZ;AAGA,QAAM,YAAY,WAAW,IAAmB;AAGhD,QAAM,gBAAgB,CAAC,SACrB,cAAc,QAAQ,MAAM,QAAQ,KAAK,QAAQ,IAC5C,KAAK,WACN;AAGN,QAAM,WAAW,gBAAgB,aAAa;AAG9C,QAAM,iBAA0B,CAAC,MAAM,SAAS;AAC9C,QAAI,UAAU,MAAM,IAAI,GAAG;AACzB,aAAO,QAAQ,MAAW,IAAI;AAAA,IAChC;AACA;AAAA,EACF;AAGA,WAAS,MAAM,cAAc;AAC/B;","names":["visit"]}
@@ -0,0 +1,16 @@
1
+ import type { Nodes } from "@rethinkhealth/hl7v2-ast";
2
+ import type { ChildProvider, Visitor } from "./types";
3
+ /**
4
+ * Creates a traversal function with the given child provider.
5
+ * Pure functional approach - no classes, no mutation.
6
+ *
7
+ * Assumptions:
8
+ * - Uses 1-based indexing for compatibility with HL7v2 field numbering convention
9
+ * - Level 1 is the traversal root node (not necessarily a Root node type)
10
+ * - Path arrays are immutable - new array created at each level via spread operator
11
+ * - undefined/null children are safely skipped
12
+ *
13
+ * @param childProvider - Function to extract children from nodes
14
+ * @returns Traversal function
15
+ */
16
+ export declare function createTraversal(childProvider: ChildProvider): (root: Nodes, visitor: Visitor) => void;
@@ -0,0 +1,48 @@
1
+ import type { Nodes } from "@rethinkhealth/hl7v2-ast";
2
+ /**
3
+ * A single entry in the traversal path.
4
+ * Simple, generic structure that works for any node type.
5
+ */
6
+ export type PathEntry = {
7
+ /** Type of the node (e.g., 'root', 'segment', 'field', 'component') */
8
+ type: string;
9
+ /** 1-based depth level in the tree (root = 1, its children = 2, etc.) */
10
+ level: number;
11
+ /** 1-based index within siblings at the same level */
12
+ index: number;
13
+ /** Reference to the actual node */
14
+ node: Nodes;
15
+ /** Extensible data object for custom metadata */
16
+ data?: Record<string, unknown>;
17
+ };
18
+ /**
19
+ * Ordered array of path entries from root to current node.
20
+ * Pure data structure - just an array.
21
+ */
22
+ export type Path = readonly PathEntry[];
23
+ /**
24
+ * Control flow actions for traversal.
25
+ */
26
+ export type Action = "skip" | "exit";
27
+ /**
28
+ * A function called for each node during traversal.
29
+ *
30
+ * @param node - Current node being visited
31
+ * @param path - Ordered array of PathEntry from traversal root to current node
32
+ * @returns Action to control traversal flow, or void/undefined to continue normally
33
+ */
34
+ export type Visitor<T extends Nodes = Nodes> = (node: T, path: Path) => Action | void | undefined;
35
+ /**
36
+ * Function that returns children of a node.
37
+ */
38
+ export type ChildProvider = (node: Nodes) => Nodes[] | undefined;
39
+ /**
40
+ * Filter criteria to determine which nodes to visit.
41
+ *
42
+ * Can be:
43
+ * - string: matches `node.type`
44
+ * - object: matches properties (partial match)
45
+ * - function: predicate returning boolean or type guard
46
+ * - null: matches everything
47
+ */
48
+ export type Test<T extends Nodes = Nodes> = string | Partial<T> | ((node: Nodes, path: Path) => node is T) | ((node: Nodes, path: Path) => boolean) | null;
@@ -0,0 +1,16 @@
1
+ import type { Nodes } from "@rethinkhealth/hl7v2-ast";
2
+ import type { Path, Test } from "./types";
3
+ /**
4
+ * Create test predicate from various input types.
5
+ *
6
+ * Assumptions:
7
+ * - null test matches all nodes
8
+ * - String test matches by node.type property
9
+ * - Object test uses strict equality (===) for property matching
10
+ * - Explicit undefined values in test object check for property absence
11
+ * - Dangerous keys (__proto__, constructor, prototype) are filtered for security
12
+ *
13
+ * @param test - Filter criteria: null (all), string (type), object (properties), or function
14
+ * @returns Predicate function that returns true if node matches test criteria
15
+ */
16
+ export declare function createTest(test: Test<Nodes>): (node: Nodes, path: Path) => boolean;
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@rethinkhealth/hl7v2-util-visit",
3
+ "description": "An AST visitor for HL7v2 messages",
4
+ "version": "0.3.4",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Melek Somai",
8
+ "email": "melek@rethinkhealth.io"
9
+ },
10
+ "type": "module",
11
+ "types": "./dist/index.d.ts",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "exports": {
16
+ ".": "./dist/index.js"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "24.10.0",
20
+ "@vitest/coverage-v8": "^4.0.5",
21
+ "tsup": "8.5.0",
22
+ "typescript": "^5.9.3",
23
+ "vitest": "^4.0.6",
24
+ "@rethinkhealth/hl7v2-ast": "0.3.4",
25
+ "@rethinkhealth/hl7v2-builder": "0.3.4",
26
+ "@rethinkhealth/testing": "0.0.2",
27
+ "@rethinkhealth/tsconfig": "0.0.1"
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "repository": "rethinkhealth/hl7v2.git",
33
+ "homepage": "https://www.rethinkhealth.io/hl7v2/docs",
34
+ "keywords": [
35
+ "health",
36
+ "healthcare",
37
+ "hl7",
38
+ "hl7v2",
39
+ "nodejs",
40
+ "typescript"
41
+ ],
42
+ "packageManager": "pnpm@10.14.0",
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "sideEffects": false,
47
+ "scripts": {
48
+ "build": "tsup && tsc --emitDeclarationOnly",
49
+ "check-types": "tsc --noEmit",
50
+ "test": "vitest run",
51
+ "test:coverage": "vitest run --coverage",
52
+ "test:watch": "vitest"
53
+ }
54
+ }