@rethinkhealth/hl7v2-utils 0.2.20 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,12 +2,180 @@
2
2
 
3
3
  A utility package for working with HL7v2 messages, most commonly used within the `@rethinkhealth/hl7v2` ecosystem — a unist-inspired collection of plugins and utilities designed to parse, transform, validate, and manipulate HL7v2 messages.
4
4
 
5
+ This package provides core utilities including:
6
+ - Diagnostic reporting system for linters, validators, and transformers
7
+ - Standard HL7v2 delimiters
8
+ - AST node utility functions
9
+
5
10
  ## Installation
6
11
 
7
12
  ```bash
8
13
  npm install @rethinkhealth/hl7v2-utils
9
14
  ```
10
15
 
16
+ ## API
17
+
18
+ ### `report(file, rule, options?)`
19
+
20
+ Report a diagnostic to a VFile. This function is the standard way to report issues in the HL7v2 ecosystem.
21
+
22
+ #### Parameters
23
+
24
+ - `file` (`VFile | null | undefined`) - The VFile to report to
25
+ - `rule` (`Diagnostic`) - The diagnostic rule definition
26
+ - `options` (`ReportOptions`, optional)
27
+ - `node` (`Node`, optional) - The AST node related to the diagnostic
28
+ - `context` (`Record<string, unknown>`, optional) - Context data passed to the diagnostic's message function
29
+
30
+ #### Example
31
+
32
+ ```typescript
33
+ import { report } from '@rethinkhealth/hl7v2-utils';
34
+ import type { Diagnostic } from '@rethinkhealth/hl7v2-utils';
35
+ import { VFile } from 'vfile';
36
+
37
+ // Define a diagnostic rule
38
+ const requiredFieldRule: Diagnostic = {
39
+ type: 'lint',
40
+ namespace: 'field',
41
+ code: 'required',
42
+ title: 'Required Field Missing',
43
+ description: 'A required field is missing from the segment.',
44
+ severity: 'error',
45
+ message: (ctx) => `Field '${ctx.fieldPath}' is required`,
46
+ helpUrl: 'https://example.com/docs/required-field'
47
+ };
48
+
49
+ // Report the diagnostic
50
+ const file = new VFile();
51
+ report(file, requiredFieldRule, {
52
+ context: { fieldPath: 'PID-5' },
53
+ node: segmentNode
54
+ });
55
+ ```
56
+
57
+ ### `Diagnostic`
58
+
59
+ Type definition for diagnostic rules. Diagnostics are used to report issues about HL7v2 messages.
60
+
61
+ #### Properties
62
+
63
+ - `type` (`string`) - Tool/plugin category (e.g., "validator", "lint", "annotator", "transformer", "parser")
64
+ - `namespace` (`string`) - Domain/concern (e.g., "order", "field", "segment", "conformance")
65
+ - `code` (`string`) - Specific issue code (e.g., "transition", "required", "acceptance")
66
+ - `title` (`string`) - Human-readable title
67
+ - `description` (`string`) - Full description of the rule
68
+ - `severity` (`"error" | "warning" | "info" | null | undefined`) - Default severity level
69
+ - `message` (`(context: Record<string, unknown>) => string`) - Message formatter function
70
+ - `helpUrl` (`string`, optional) - URL to documentation
71
+
72
+ #### Rule ID Format
73
+
74
+ The rule ID is automatically constructed as `type:namespace:code` (e.g., `lint:field:required`).
75
+
76
+ #### Example
77
+
78
+ ```typescript
79
+ const invalidTransitionRule: Diagnostic = {
80
+ type: 'validator',
81
+ namespace: 'order',
82
+ code: 'transition',
83
+ title: 'Invalid Segment Transition',
84
+ description: 'A segment arrived that is not allowed in this position.',
85
+ severity: 'error',
86
+ message: (ctx) => {
87
+ const expected = Array.isArray(ctx.expected)
88
+ ? ctx.expected.join(', ')
89
+ : ctx.expected;
90
+ return `Expected ${expected}, got '${ctx.actual}'`;
91
+ },
92
+ helpUrl: 'https://example.com/docs/segment-order'
93
+ };
94
+ ```
95
+
96
+ ### `DEFAULT_DELIMITERS`
97
+
98
+ Standard HL7v2 delimiters as defined in the specification.
99
+
100
+ ```typescript
101
+ export const DEFAULT_DELIMITERS = {
102
+ field: "|",
103
+ component: "^",
104
+ repetition: "~",
105
+ subcomponent: "&",
106
+ escape: "\\",
107
+ segment: "\r",
108
+ };
109
+ ```
110
+
111
+ #### Example
112
+
113
+ ```typescript
114
+ import { DEFAULT_DELIMITERS } from '@rethinkhealth/hl7v2-utils';
115
+
116
+ const message = segments.join(DEFAULT_DELIMITERS.segment);
117
+ ```
118
+
119
+ ### `isEmptyNode(node)`
120
+
121
+ Check if an AST node is semantically empty. This is useful for validation and transformation logic.
122
+
123
+ #### Parameters
124
+
125
+ - `node` (`Nodes | null | undefined`) - The AST node to check
126
+
127
+ #### Returns
128
+
129
+ `boolean` - `true` if the node is empty, `false` otherwise
130
+
131
+ #### Example
132
+
133
+ ```typescript
134
+ import { isEmptyNode } from '@rethinkhealth/hl7v2-utils';
135
+
136
+ if (isEmptyNode(field)) {
137
+ report(file, requiredFieldRule, {
138
+ context: { fieldPath: 'PID-5' },
139
+ node: field
140
+ });
141
+ }
142
+ ```
143
+
144
+ ## Usage
145
+
146
+ ### Creating a Custom Linter
147
+
148
+ ```typescript
149
+ import { visit } from 'unist-util-visit';
150
+ import { report } from '@rethinkhealth/hl7v2-utils';
151
+ import type { Diagnostic } from '@rethinkhealth/hl7v2-utils';
152
+ import type { Root } from '@rethinkhealth/hl7v2-ast';
153
+ import type { VFile } from 'vfile';
154
+
155
+ const noEmptySegmentRule: Diagnostic = {
156
+ type: 'lint',
157
+ namespace: 'segment',
158
+ code: 'no-empty',
159
+ title: 'Empty Segment',
160
+ description: 'Segments should not be empty.',
161
+ severity: 'warning',
162
+ message: (ctx) => `Segment '${ctx.segmentId}' is empty`
163
+ };
164
+
165
+ function lintNoEmptySegment() {
166
+ return (tree: Root, file: VFile) => {
167
+ visit(tree, 'segment', (node) => {
168
+ if (isEmptyNode(node)) {
169
+ report(file, noEmptySegmentRule, {
170
+ node,
171
+ context: { segmentId: node.segmentId }
172
+ });
173
+ }
174
+ });
175
+ };
176
+ }
177
+ ```
178
+
11
179
  ## Contributing
12
180
 
13
181
  We welcome contributions! Please see our [Contributing Guide][github-contributing] for more details.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  /** biome-ignore-all lint/performance/noBarrelFile: fine */
2
+ export { report } from "./report";
3
+ export type { Diagnostic } from "./types";
2
4
  export { DEFAULT_DELIMITERS, isEmptyNode } from "./utils";
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,35 @@
1
+ // src/report.ts
2
+ function report(file, rule, options) {
3
+ if (!file) {
4
+ return;
5
+ }
6
+ const ruleId = `${rule.type}:${rule.namespace}:${rule.code}`;
7
+ const context = options?.context ?? {};
8
+ const message = rule.message(context);
9
+ const vfileMessage = file.message(message, options?.node);
10
+ vfileMessage.ruleId = ruleId;
11
+ vfileMessage.url = rule.helpUrl;
12
+ vfileMessage.note = rule.description;
13
+ vfileMessage.source = rule.namespace;
14
+ switch (rule.severity) {
15
+ case "error":
16
+ vfileMessage.fatal = true;
17
+ break;
18
+ case "warning":
19
+ vfileMessage.fatal = false;
20
+ break;
21
+ case "info":
22
+ vfileMessage.fatal = null;
23
+ break;
24
+ case null:
25
+ vfileMessage.fatal = null;
26
+ break;
27
+ default:
28
+ vfileMessage.fatal = void 0;
29
+ break;
30
+ }
31
+ }
32
+
1
33
  // src/utils.ts
2
34
  var DEFAULT_DELIMITERS = {
3
35
  field: "|",
@@ -27,6 +59,7 @@ function isEmptyNode(node) {
27
59
  }
28
60
  export {
29
61
  DEFAULT_DELIMITERS,
30
- isEmptyNode
62
+ isEmptyNode,
63
+ report
31
64
  };
32
65
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils.ts"],"sourcesContent":["import type { Nodes } from \"@rethinkhealth/hl7v2-ast\";\n\n// -------------\n// Delimiters\n// -------------\n\nexport const DEFAULT_DELIMITERS = {\n field: \"|\",\n component: \"^\",\n repetition: \"~\",\n subcomponent: \"&\",\n escape: \"\\\\\",\n segment: \"\\r\",\n};\n\n// -------------\n// General\n// -------------\n\n/**\n * Utility: check if a node is semantically empty\n */\nexport function isEmptyNode(node: Nodes | null | undefined): boolean {\n if (!node) {\n return true;\n }\n\n // If node has a \"value\" property (Subcomponent, maybe Component)\n if (\"value\" in node) {\n return !node.value || node.value.trim() === \"\";\n }\n\n // If node has children (Field, Component, Repetition, Segment, Root, etc.)\n if (\"children\" in node) {\n if (!node.children || node.children.length === 0) {\n return true;\n }\n\n // If node has more than one child, then it is considered non-empty\n if (node.children.length > 1) {\n return false;\n }\n\n // If node has only one child, then it is considered empty if the child is also empty\n return isEmptyNode(node.children[0]);\n }\n\n // Fallback: consider unknown node as non-empty\n return false;\n}\n"],"mappings":";AAMO,IAAM,qBAAqB;AAAA,EAChC,OAAO;AAAA,EACP,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,SAAS;AACX;AASO,SAAS,YAAY,MAAyC;AACnE,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AAGA,MAAI,WAAW,MAAM;AACnB,WAAO,CAAC,KAAK,SAAS,KAAK,MAAM,KAAK,MAAM;AAAA,EAC9C;AAGA,MAAI,cAAc,MAAM;AACtB,QAAI,CAAC,KAAK,YAAY,KAAK,SAAS,WAAW,GAAG;AAChD,aAAO;AAAA,IACT;AAGA,QAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,aAAO;AAAA,IACT;AAGA,WAAO,YAAY,KAAK,SAAS,CAAC,CAAC;AAAA,EACrC;AAGA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/report.ts","../src/utils.ts"],"sourcesContent":["import type { Node } from \"@rethinkhealth/hl7v2-ast\";\nimport type { VFile } from \"vfile\";\nimport type { Diagnostic } from \"./types\";\n\nexport type ReportOptions = {\n node?: Node;\n context?: Record<string, unknown>;\n};\n\n/**\n * Report a diagnostic to a vfile.\n *\n * Calls the diagnostic's message function with context and wires to vfile.\n *\n * @param file - The VFile to report to\n * @param rule - The diagnostic rule definition\n * @param options - Position and context data\n */\nexport function report(\n file: VFile | null | undefined,\n rule: Diagnostic,\n options?: ReportOptions\n): void {\n if (!file) {\n return;\n }\n\n // Construct the rule ID from type, namespace, and code\n const ruleId = `${rule.type}:${rule.namespace}:${rule.code}`;\n\n const context = options?.context ?? {};\n const message = rule.message(context);\n\n // Create the vfile message\n const vfileMessage = file.message(message, options?.node);\n\n // Set the ruleId explicitly\n vfileMessage.ruleId = ruleId;\n\n // Set the help URL if provided\n vfileMessage.url = rule.helpUrl;\n\n // Set the description\n vfileMessage.note = rule.description;\n\n // Set source to the namespace (middle part of ruleId)\n vfileMessage.source = rule.namespace;\n\n // Map severity to fatal flag\n switch (rule.severity) {\n case \"error\":\n vfileMessage.fatal = true;\n break;\n case \"warning\":\n vfileMessage.fatal = false;\n break;\n case \"info\":\n vfileMessage.fatal = null;\n break;\n case null:\n vfileMessage.fatal = null;\n break;\n default:\n vfileMessage.fatal = undefined;\n break;\n }\n}\n","import type { Nodes } from \"@rethinkhealth/hl7v2-ast\";\n\n// -------------\n// Delimiters\n// -------------\n\nexport const DEFAULT_DELIMITERS = {\n field: \"|\",\n component: \"^\",\n repetition: \"~\",\n subcomponent: \"&\",\n escape: \"\\\\\",\n segment: \"\\r\",\n};\n\n// -------------\n// General\n// -------------\n\n/**\n * Utility: check if a node is semantically empty\n */\nexport function isEmptyNode(node: Nodes | null | undefined): boolean {\n if (!node) {\n return true;\n }\n\n // If node has a \"value\" property (Subcomponent, maybe Component)\n if (\"value\" in node) {\n return !node.value || node.value.trim() === \"\";\n }\n\n // If node has children (Field, Component, Repetition, Segment, Root, etc.)\n if (\"children\" in node) {\n if (!node.children || node.children.length === 0) {\n return true;\n }\n\n // If node has more than one child, then it is considered non-empty\n if (node.children.length > 1) {\n return false;\n }\n\n // If node has only one child, then it is considered empty if the child is also empty\n return isEmptyNode(node.children[0]);\n }\n\n // Fallback: consider unknown node as non-empty\n return false;\n}\n"],"mappings":";AAkBO,SAAS,OACd,MACA,MACA,SACM;AACN,MAAI,CAAC,MAAM;AACT;AAAA,EACF;AAGA,QAAM,SAAS,GAAG,KAAK,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,IAAI;AAE1D,QAAM,UAAU,SAAS,WAAW,CAAC;AACrC,QAAM,UAAU,KAAK,QAAQ,OAAO;AAGpC,QAAM,eAAe,KAAK,QAAQ,SAAS,SAAS,IAAI;AAGxD,eAAa,SAAS;AAGtB,eAAa,MAAM,KAAK;AAGxB,eAAa,OAAO,KAAK;AAGzB,eAAa,SAAS,KAAK;AAG3B,UAAQ,KAAK,UAAU;AAAA,IACrB,KAAK;AACH,mBAAa,QAAQ;AACrB;AAAA,IACF,KAAK;AACH,mBAAa,QAAQ;AACrB;AAAA,IACF,KAAK;AACH,mBAAa,QAAQ;AACrB;AAAA,IACF,KAAK;AACH,mBAAa,QAAQ;AACrB;AAAA,IACF;AACE,mBAAa,QAAQ;AACrB;AAAA,EACJ;AACF;;;AC5DO,IAAM,qBAAqB;AAAA,EAChC,OAAO;AAAA,EACP,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,SAAS;AACX;AASO,SAAS,YAAY,MAAyC;AACnE,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AAGA,MAAI,WAAW,MAAM;AACnB,WAAO,CAAC,KAAK,SAAS,KAAK,MAAM,KAAK,MAAM;AAAA,EAC9C;AAGA,MAAI,cAAc,MAAM;AACtB,QAAI,CAAC,KAAK,YAAY,KAAK,SAAS,WAAW,GAAG;AAChD,aAAO;AAAA,IACT;AAGA,QAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,aAAO;AAAA,IACT;AAGA,WAAO,YAAY,KAAK,SAAS,CAAC,CAAC;AAAA,EACrC;AAGA,SAAO;AACT;","names":[]}
@@ -0,0 +1,18 @@
1
+ import type { Node } from "@rethinkhealth/hl7v2-ast";
2
+ import type { VFile } from "vfile";
3
+ import type { Diagnostic } from "./types";
4
+ export type ReportOptions = {
5
+ node?: Node;
6
+ context?: Record<string, unknown>;
7
+ };
8
+ /**
9
+ * Report a diagnostic to a vfile.
10
+ *
11
+ * Calls the diagnostic's message function with context and wires to vfile.
12
+ *
13
+ * @param file - The VFile to report to
14
+ * @param rule - The diagnostic rule definition
15
+ * @param options - Position and context data
16
+ */
17
+ export declare function report(file: VFile | null | undefined, rule: Diagnostic, options?: ReportOptions): void;
18
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CACpB,IAAI,EAAE,KAAK,GAAG,IAAI,GAAG,SAAS,EAC9B,IAAI,EAAE,UAAU,EAChB,OAAO,CAAC,EAAE,aAAa,GACtB,IAAI,CA4CN"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Diagnostic definition.
3
+ *
4
+ * A diagnostic is a message about a problem in the HL7v2 message. It is used to report issues to the user.
5
+ *
6
+ * Diagnostics are defined per-package with type, namespace, and code that compose the `ruleId`.
7
+ *
8
+ * Diagnostics are used in conjunction with the `report()` function to report issues to the user.
9
+ */
10
+ export type Diagnostic = Readonly<{
11
+ /**
12
+ * Tool/plugin category
13
+ */
14
+ type: "validator" | "lint" | "annotator" | "transformer" | "parser" | string;
15
+ /**
16
+ * Domain/concern (order, field, conformance, segment, group, datatype, etc.)
17
+ */
18
+ namespace: string;
19
+ /**
20
+ * Specific issue code (transition, required, acceptance, mismatch, etc.)
21
+ */
22
+ code: string;
23
+ /**
24
+ * Full description of the rule
25
+ */
26
+ description?: string;
27
+ /**
28
+ * Default severity
29
+ *
30
+ * @defaultValue 'info'
31
+ */
32
+ severity?: "error" | "warning" | "info" | null | undefined;
33
+ /**
34
+ * Message formatter function.
35
+ * Takes context and returns the formatted message.
36
+ * The function signature is self-documenting: it shows what context is required.
37
+ */
38
+ message: (context: Record<string, unknown>) => string;
39
+ /**
40
+ * Optional URL to documentation
41
+ */
42
+ helpUrl?: string;
43
+ }>;
44
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC;;OAEG;IACH,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,aAAa,GAAG,QAAQ,GAAG,MAAM,CAAC;IAE7E;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAE3D;;;;OAIG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC;IAEtD;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rethinkhealth/hl7v2-utils",
3
3
  "description": "hl7v2 utilities",
4
- "version": "0.2.20",
4
+ "version": "0.2.21",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Melek Somai",
@@ -22,10 +22,11 @@
22
22
  "tsup": "8.5.0",
23
23
  "typescript": "^5.8.3",
24
24
  "unist-builder": "^4.0.0",
25
+ "vfile": "^6.0.3",
25
26
  "vitest": "^3.2.4",
26
- "@rethinkhealth/tsconfig": "0.0.1",
27
- "@rethinkhealth/hl7v2-ast": "0.2.20",
28
- "@rethinkhealth/testing": "0.0.1"
27
+ "@rethinkhealth/hl7v2-ast": "0.2.21",
28
+ "@rethinkhealth/testing": "0.0.1",
29
+ "@rethinkhealth/tsconfig": "0.0.1"
29
30
  },
30
31
  "repository": "rethinkhealth/hl7v2.git",
31
32
  "homepage": "https://www.rethinkhealth.io/hl7v2/docs",