-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdoc.go
More file actions
314 lines (314 loc) · 12.4 KB
/
doc.go
File metadata and controls
314 lines (314 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
// Package hl7 provides a parser for HL7 version 2.x messages.
//
// The parser reads HL7v2 messages encoded in the ER7 (pipe-delimited) format
// and returns a structured representation. It has zero external dependencies,
// requires Go 1.23+, and is safe for concurrent use after parsing.
//
// # Message Structure
//
// HL7v2 messages have a hierarchical structure:
//
// Message → Segments → Fields → Repetitions → Components → Subcomponents
//
// Delimiter characters are extracted from each message's MSH segment header
// and are not assumed to be the standard set (|^~\&), although that is typical.
//
// # Parsing Strategy
//
// ParseMessage copies the input, extracts delimiters from the MSH header,
// merges any ADD continuation segments into their preceding segments per
// HL7v2.5.1 Section 2.5.2, and splits the message into segments by \r.
// ADD segments that immediately follow MSH are left as standalone segments.
// ADD segments that follow MSA, DSC, PID, QRD, QRF, URD, or URS are invalid
// per the spec and cause ParseMessage to return ErrInvalidADDContinuation.
// This is the only phase that allocates. All deeper access — fields,
// repetitions, components, and subcomponents — is performed by scanning the
// segment's raw bytes on each call. No parsed results are cached; the
// sub-message types (Field, Repetition, Component, Subcomponent) are
// lightweight value types holding only a []byte slice and a Delimiters struct.
//
// Escape processing is deferred until String() is called. A consumer that
// reads only MSH-9 (message type) never pays the cost of scanning OBX
// observation segments.
//
// # Quick Start
//
// msg, err := hl7.ParseMessage(rawBytes)
// if err != nil {
// log.Fatal(err)
// }
// fmt.Println(msg.Get("MSH-9.1").String()) // "ADT"
// fmt.Println(msg.Get("PID-5.1").String()) // Family name
//
// # Terser-Style Access
//
// Get retrieves values using terser-style location strings and returns a Value.
// ParseLocation parses the string into a Location struct, and
// Location.String implements the inverse.
//
// msg.Get("MSH-9.1").String() // Message code (unescaped string)
// msg.Get("PID-3[1].4.2").String() // 2nd repetition of PID-3, component 4, subcomponent 2
// msg.Get("OBX(1)-5").String() // 2nd OBX segment (0-based), field 5
// msg.Get("PID-3.1").Bytes() // unescaped bytes
// msg.Get("PID-3.1").Raw() // raw bytes without unescaping
//
// Get returns a zero Value when the location is invalid or the value is not
// present — consistent with how Field(n), Rep(n), and Component(n) return zero
// values for out-of-range indices rather than errors.
//
// # Character Set Decoding
//
// Field values in non-UTF-8 charsets (e.g. Latin-1 / ISO 8859-1 declared in
// MSH-18) can be decoded with DecodeString. A ValueMapper is a
// func(Value) ([]byte, error) that converts a Value to UTF-8 bytes; call
// v.Bytes() inside the mapper to get the post-unescape bytes to convert.
// DecodeString is available on Value, Field, Repetition, Component, and
// Subcomponent:
//
// switch msg.Get("MSH-18").String() {
// case "8859/1":
// decode = latin1ToUTF8 // caller-provided, wraps e.g. golang.org/x/text
// }
// name, err := msg.Get("PID-5.1").DecodeString(decode)
// // or on Field directly:
// family, err := seg.Field(5).Rep(0).Component(1).DecodeString(decode)
//
// When decode is nil, DecodeString behaves identically to String() with no
// extra allocation. Unescape always runs before the mapper. The \C..\ and
// \M..\ charset escape sequences are passed through verbatim by Unescape; a
// sophisticated ValueMapper may parse them, but a simple byte-level mapper
// will treat them as-is.
//
// # Hierarchical Traversal
//
// Segments returns a []Segment slice. Filter by type with seg.Type():
//
// for _, seg := range msg.Segments() {
// if seg.Type() == "OBX" {
// fmt.Println(seg.Field(5).String())
// }
// }
//
// Navigate the hierarchy using Field, Rep, Component, and SubComponent:
//
// name := seg.Field(5).Rep(0).Component(1).String()
//
// Index conventions: Field is 0-based (0 = segment type, 1+ = fields).
// Rep is 0-based. Component and SubComponent are 1-based. Out-of-range
// access returns zero values, enabling chained access without nil checks.
//
// Count methods are available at each level: FieldCount,
// RepetitionCount, ComponentCount, and SubComponentCount.
//
// # Stream Reading
//
// reader := hl7.NewReader(conn, hl7.WithMode(hl7.ModeMLLP))
// err := reader.EachMessage(func(msg *hl7.Message) error {
// msgType := msg.Get("MSH-9").String()
// return nil
// })
//
// Reader modes: ModeAuto (default, detects framing), ModeMLLP (strict MLLP),
// ModeRaw (MSH-boundary detection). ReadMessage returns one parsed message
// at a time; EachMessage iterates until EOF.
//
// WithMaxFrameSize caps the MLLP frame size; ReadMessage returns
// ErrFrameTooLarge if the frame body exceeds the limit. A limit of 0
// (the default) means no maximum.
//
// # Stream Writing
//
// w := hl7.NewWriter(conn, hl7.WithMLLP())
// w.WriteMessage(msg)
// w.Flush()
//
// Without WithMLLP, messages are written as raw bytes with a trailing \r.
// With WithMLLP, messages are wrapped in 0x0B...0x1C 0x0D framing.
//
// # Message Transformation
//
// Transform applies changes to a message and returns a new Message,
// leaving the original unmodified:
//
// updated, err := msg.Transform(
// hl7.Replace("PID-5.1", "Smith"),
// hl7.Null("PID-8"),
// hl7.Omit("PID-19"),
// hl7.Move("PID-4", "PID-3"),
// hl7.Copy("PID-4", "PID-3"),
// hl7.MapValue("PID-5.1", upperMapper),
// hl7.MapAllValues(latin1ToUTF8Mapper),
// )
//
// Field-level change constructors: Replace sets a value, Null sets the HL7
// null (""), Omit clears a field, Move copies src to dst then clears src,
// Copy copies src to dst while preserving the source, MapValue applies a
// ValueMapper to a single location, and MapAllValues applies a ValueMapper to
// every leaf in the message (except MSH-1 and MSH-2). The mapper receives
// unescaped bytes; its output is re-escaped before storage.
//
// Segment-level change constructors add, remove, or reorder entire segments:
//
// updated, err := msg.Transform(
// hl7.DeleteSegment("NTE", 0), // delete first NTE (no-op if absent)
// hl7.DeleteAllSegments("NTE"), // delete all NTE segments
// hl7.InsertSegmentAfter("PID", 0, "ZPD"), // insert ZPD after first PID
// hl7.InsertSegmentBefore("OBX", -1, "ZPD"), // insert ZPD before last OBX
// hl7.AppendSegment("ZPD"), // append ZPD at end of message
// )
//
// DeleteSegment and DeleteAllSegments are no-ops when the target is absent;
// both return ErrCannotDeleteMSH if given "MSH". InsertSegmentAfter and
// InsertSegmentBefore return ErrSegmentNotFound when the reference segment is
// absent. Segment-level and field-level changes compose freely in one call.
//
// TransformWith additionally re-encodes all delimiters to a new set:
//
// updated, err := msg.TransformWith(newDelims, changes...)
//
// # Message Building
//
// MessageBuilder constructs messages from scratch:
//
// b, err := hl7.NewMessageBuilder()
// if err != nil {
// log.Fatal(err)
// }
// b.Set("MSH-9.1", "ADT")
// b.Set("MSH-9.2", "A01")
// b.Set("PID-3.1", "12345")
// msg, err := b.Build()
//
// Use WithDelimiters to override the default delimiter set.
// SetNull sets a field to the HL7 null value. The builder is reusable
// after Build — subsequent Set calls modify its state.
//
// # ACK Generation
//
// Ack creates an acknowledgment message in response to a parsed message:
//
// ackBytes, err := msg.Ack(hl7.AA, "CTRL123")
//
// AckCode constants: AA, AE, AR (application level), CA, CE, CR (commit
// level). Options: WithText sets MSA-3 for error context, WithTimestamp
// overrides the default time.Now for MSH-7, and WithErrors adds ERR
// segments from validation issues. The ACK copies delimiters and swaps
// sender/receiver fields from the original MSH.
//
// WithErrors accepts a slice of Issue (from Validate) and produces one ERR
// segment per issue with error location (ERR-2), severity (ERR-4),
// application error code (ERR-5), and diagnostic information (ERR-7):
//
// result := msg.Validate(schema)
// if !result.Valid {
// ack, err := msg.Ack(hl7.AE, "ACK001", hl7.WithErrors(result.Issues))
// }
//
// # Validation
//
// Validate checks a parsed message against a user-provided Schema:
//
// result := msg.Validate(&hl7.Schema{
// Messages: map[string]*hl7.MessageDef{ ... },
// Segments: map[string]*hl7.SegmentDef{ ... },
// })
// if !result.Valid {
// for _, issue := range result.Issues {
// fmt.Println(issue)
// }
// }
//
// Schema has four optional maps (Messages, Segments, DataTypes, Tables) plus
// a Checks slice for message-level custom validators. Only populated maps
// trigger validation. Schema types support JSON/YAML/TOML struct tags.
//
// Validation runs in three phases: structure (segment presence, order,
// cardinality via MessageDef), content (field presence, length, format, table
// values via SegmentDef/DataTypeDef/TableDef, plus FieldCheckFunc and
// SegmentCheckFunc validators), and custom checks (MessageCheckFunc from
// schema.Checks for cross-segment business rules).
//
// # Multi-Page Response Assembly
//
// Concatenate reassembles multi-page query responses. When a server splits a
// large response across several messages using DSC (Continuation) segments,
// call Concatenate to merge them in order:
//
// assembled, err := page1.Concatenate(page2)
// if err != nil {
// log.Fatal(err) // ErrDelimiterMismatch if pages use different delimiters
// }
// assembled, err = assembled.Concatenate(page3)
//
// Concatenate strips the trailing DSC segment from the receiver and the leading
// MSH segment from the argument, then returns a new Message containing the
// remaining segments. The DSC-1 continuation pointer is not interpreted; the
// caller is responsible for retrieving subsequent pages.
//
// # Batch and File Parsing
//
// ParseBatch parses a BHS/BTS-wrapped group of messages:
//
// batch, err := hl7.ParseBatch(data)
// for _, msg := range batch.Messages { ... }
//
// ParseFile parses an FHS/FTS-wrapped file containing batches:
//
// file, err := hl7.ParseFile(data)
// for _, batch := range file.Batches { ... }
//
// Both are tolerant: header and trailer segments are optional.
//
// # Batch Building
//
// BatchBuilder constructs BHS/BTS-wrapped batch files from individual messages:
//
// bb := hl7.NewBatchBuilder()
// bb.SetHeader(3, "MYAPP").SetHeader(4, "MYFAC")
// bb.Add(msg1).Add(msg2)
// data, err := bb.Build()
//
// BHS-7 (date/time) defaults to time.Now() at Build time unless set via
// SetHeader(7, ...). BTS-1 (message count) is set automatically. Call
// Reset to clear accumulated messages while keeping header field values,
// then call Build again to produce a new batch with the same header.
// Use WithBatchDelimiters to override the default delimiter set.
//
// # Escape Sequences
//
// Escape replaces delimiter characters in data with HL7 escape sequences.
// Unescape resolves escape sequences back to literal values. Both have a
// zero-allocation fast path when no escaping is needed.
//
// Escape processing is deferred — Unescape runs only when String() is called,
// not during parsing. Bytes() returns raw data without escape processing.
//
// # Null vs Empty
//
// Per the HL7 specification, there is a distinction between empty and null fields:
//
// - Empty (||): No value provided; preserve existing data during updates.
// - Null (|""|): Explicitly set to null; clear existing data.
//
// Use IsNull() and IsEmpty() to distinguish these cases; HasValue() returns
// true when neither empty nor null. All four hierarchy types (Field,
// Repetition, Component, Subcomponent) as well as Value share these methods.
//
// # Error Handling
//
// Parse-time errors use sentinel errors (ErrMessageTooShort, ErrNoMSHSegment,
// ErrInvalidDelimiter, etc.) that can be checked with errors.Is. ParseError
// wraps a sentinel with position and context for detailed diagnostics.
//
// Access-time methods (Field, Rep, Component, SubComponent) return zero values
// for out-of-range indices rather than errors, enabling ergonomic chained access.
//
// Validate never returns an error — it returns a *ValidationResult containing
// a Valid bool and a slice of Issue structs with Severity, Location, Code, and
// Description.
//
// # Examples
//
// See the examples/ directory for runnable programs demonstrating end-to-end
// workflows including stream reading, validation, transformation, and writing.
package hl7