@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 +21 -0
- package/README.md +498 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/dist/traversal.d.ts +16 -0
- package/dist/types.d.ts +48 -0
- package/dist/utils.d.ts +16 -0
- package/package.json +54 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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;
|
package/dist/types.d.ts
ADDED
|
@@ -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;
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
}
|