diff --git a/AGENTS.md b/AGENTS.md index 2d059e9..85ae910 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,8 @@ hl7/ field.go # Field and Repetition value types segment.go # Segment type with MSH special-case handling message.go # Message type, ParseMessage(), segment iterators - accessor.go # Terser-style location parser, Location type, Get()/GetBytes() + accessor.go # Terser-style location parser, Location type, Value type, Get() + charset.go # ValueDecoder type; DecodeString on Field/Repetition/Component/Subcomponent/Value reader.go # io.Reader wrapper with MLLP and raw mode support writer.go # io.Writer wrapper with MLLP and raw mode support ack.go # ACK message generation (AckCode, AckOption, Message.Ack, WithErrors) @@ -153,7 +154,11 @@ Schema types are defined in `schema.go`: ### Location Type -`Location` in `accessor.go` represents a specific position in an HL7 message hierarchy. `ParseLocation` parses terser-style strings (e.g., `"PID-3[1].4.2"`) into a `Location`. `Location.String()` implements `fmt.Stringer` and produces the inverse terser representation. Both are used by the accessor (`Get`/`GetBytes`), transform, and builder subsystems. +`Location` in `accessor.go` represents a specific position in an HL7 message hierarchy. `ParseLocation` parses terser-style strings (e.g., `"PID-3[1].4.2"`) into a `Location`. `Location.String()` implements `fmt.Stringer` and produces the inverse terser representation. Both are used by the accessor (`Get`), transform, and builder subsystems. + +`Value` in `accessor.go` is the return type of `Get()`. It is a lightweight value type (`raw []byte` + `delims Delimiters`) with `String()`, `Bytes()`, `IsEmpty()`, `IsNull()`, and `HasValue()` — the same interface as `Field`, `Repetition`, `Component`, and `Subcomponent`. A zero `Value` (nil raw bytes) is returned for invalid or not-found locations. + +`ValueDecoder` in `charset.go` is a `func([]byte) ([]byte, error)` that converts post-unescape bytes to a target encoding (typically UTF-8). `DecodeString(ValueDecoder)` is defined on `Value`, `Field`, `Repetition`, `Component`, and `Subcomponent`. When the decoder is nil, `DecodeString` is equivalent to `String()` with no extra allocation. ## HL7v2 Specification Decisions diff --git a/README.md b/README.md index ecfe42d..72fb038 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ Zero external dependencies. Requires Go 1.23+. ```go msg, _ := hl7.ParseMessage(rawBytes) -fmt.Println(msg.Get("MSH-9.1")) // "ADT" -fmt.Println(msg.Get("PID-5.1")) // "Smith" +fmt.Println(msg.Get("MSH-9.1").String()) // "ADT" +fmt.Println(msg.Get("PID-5.1").String()) // "Smith" ``` ## When to use this library @@ -42,27 +42,30 @@ if err != nil { ### Terser-style access -The `Get` method accepts location strings in the format `SEG-Field.Component.SubComponent`: +The `Get` method accepts location strings in the format `SEG-Field.Component.SubComponent` +and returns a `Value` — a lightweight value type holding the raw bytes and delimiters: ```go -msg.Get("MSH-9") // "ADT^A01" (full field, unescaped) -msg.Get("MSH-9.1") // "ADT" (first component) -msg.Get("MSH-9.2") // "A01" (second component) -msg.Get("PID-5.1") // "Smith" (family name) -msg.Get("PID-3.1") // "12345" (patient ID) -msg.Get("PID-3.1.1") // "12345" (first subcomponent) +msg.Get("MSH-9").String() // "ADT^A01" (full field, unescaped) +msg.Get("MSH-9.1").String() // "ADT" (first component) +msg.Get("MSH-9.2").String() // "A01" (second component) +msg.Get("PID-5.1").String() // "Smith" (family name) +msg.Get("PID-3.1").String() // "12345" (patient ID) +msg.Get("PID-3.1.1").String() // "12345" (first subcomponent) +msg.Get("PID-3.1").Bytes() // raw bytes without unescaping ``` Segment occurrence and repetition indices are supported: ```go -msg.Get("OBX(0)-5") // first OBX, observation value -msg.Get("OBX(1)-5") // second OBX, observation value -msg.Get("PID-3[0].1") // first repetition of PID-3, component 1 -msg.Get("PID-3[1].1") // second repetition of PID-3, component 1 +msg.Get("OBX(0)-5").String() // first OBX, observation value +msg.Get("OBX(1)-5").String() // second OBX, observation value +msg.Get("PID-3[0].1").String() // first repetition of PID-3, component 1 +msg.Get("PID-3[1].1").String() // second repetition of PID-3, component 1 ``` -Missing values return an empty string — no error checking needed for chained reads. +Missing values return a zero `Value` — `String()` returns `""` and `Bytes()` returns `nil`. +No error checking is needed for chained reads. ### Location parsing @@ -102,6 +105,34 @@ f.IsNull() // true if field is the HL7 null value "" f.HasValue() // true if neither empty nor null ``` +`Value` (returned by `Get`) has the same `IsEmpty`, `IsNull`, and `HasValue` methods. + +### Character set decoding + +For messages that declare a non-UTF-8 encoding in MSH-18, use `DecodeString` with a +`ValueDecoder` to convert bytes after unescaping. `DecodeString` is available on `Value`, +`Field`, `Repetition`, `Component`, and `Subcomponent`: + +```go +// A ValueDecoder is func([]byte) ([]byte, error) — wrap e.g. golang.org/x/text. +var decode hl7.ValueDecoder +switch msg.Get("MSH-18").String() { +case "8859/1": + decode = latin1ToUTF8 // caller-provided +} + +// Terser-style: decode a specific field value. +name, err := msg.Get("PID-5.1").DecodeString(decode) + +// Hierarchical: decode a component. +family, err := seg.Field(5).Rep(0).Component(1).DecodeString(decode) +``` + +When `decode` is `nil`, `DecodeString` is equivalent to `String()` with no extra allocation. +Unescape always runs before the decoder, so the decoder receives resolved bytes. The `\C..\` +and `\M..\` charset escape sequences are passed through verbatim; a sophisticated decoder +may interpret them, but a simple byte-level decoder will treat them as-is. + ## Transforming `Transform` applies changes to a message and returns a new `*Message`. The original is never modified. @@ -333,7 +364,7 @@ schema.Segments["PID"] = &hl7.SegmentDef{ ```go schema.Checks = []hl7.MessageCheckFunc{ func(msg *hl7.Message) []hl7.Issue { - if msg.Get("MSH-9.1") == "ORU" && msg.Get("OBX-1") == "" { + if msg.Get("MSH-9.1").String() == "ORU" && msg.Get("OBX-1").String() == "" { return []hl7.Issue{{ Severity: hl7.SeverityError, Location: "OBX", Code: "BUSINESS_RULE", @@ -468,7 +499,7 @@ os.WriteFile("schema.json", data, 0644) reader := hl7.NewReader(conn, hl7.WithMode(hl7.ModeMLLP)) err := reader.EachMessage(func(msg *hl7.Message) error { - msgType := msg.Get("MSH-9.1") + msgType := msg.Get("MSH-9.1").String() fmt.Println("received", msgType) return nil }) diff --git a/accessor.go b/accessor.go index 221da5f..9e67c6d 100644 --- a/accessor.go +++ b/accessor.go @@ -196,40 +196,71 @@ func ParseLocation(s string) (Location, error) { return loc, nil } -// Get retrieves the unescaped string value at the given terser-style location. +// Value holds the raw bytes at a terser-style location, returned by Get. +// It is a lightweight value type (raw []byte + Delimiters), consistent with +// Field, Repetition, Component, and Subcomponent. // -// Examples: -// -// msg.Get("MSH-9") // Message type field (full value) -// msg.Get("MSH-9.1") // Message code (e.g., "ADT") -// msg.Get("MSH-9.2") // Trigger event (e.g., "A01") -// msg.Get("PID-3.1") // Patient ID -// msg.Get("PID-5.1") // Family name -// msg.Get("OBX(0)-5") // First OBX segment, field 5 -// -// Returns an empty string if the location is invalid or the value is not present. -func (m *Message) Get(location string) string { - loc, err := ParseLocation(location) - if err != nil { - return "" - } - result := m.getByLocation(loc) - return result.String() +// A zero Value (nil raw bytes) is returned when the location is invalid or +// the addressed element is not present in the message. A zero Value is empty: +// IsEmpty() returns true, String() returns "", and Bytes() returns nil. +type Value struct { + raw []byte + delims Delimiters +} + +// String returns the unescaped string value. Returns an empty string for a +// zero (not-found) Value. +func (v Value) String() string { + return string(Unescape(v.raw, v.delims)) +} + +// Bytes returns the raw bytes without escape processing. Returns nil for a +// zero (not-found) Value. +func (v Value) Bytes() []byte { + return v.raw +} + +// IsEmpty returns true if the value was not present (nil raw bytes). +func (v Value) IsEmpty() bool { + return len(v.raw) == 0 +} + +// IsNull returns true if the value is the HL7 explicit null, represented by +// two double-quote characters (""). +func (v Value) IsNull() bool { + return len(v.raw) == 2 && v.raw[0] == '"' && v.raw[1] == '"' } -// GetBytes retrieves the raw bytes at the given terser-style location. -// Returns nil if the location is invalid or the value is not present. -func (m *Message) GetBytes(location string) []byte { +// HasValue returns true if the value is neither empty nor null. +func (v Value) HasValue() bool { + return !v.IsEmpty() && !v.IsNull() +} + +// Get retrieves the value at the given terser-style location. +// +// Returns a zero Value if the location string is invalid or the addressed +// element is not present — consistent with how Field(n), Rep(n), Component(n), +// and SubComponent(n) return zero values for out-of-range indices. +// +// Examples: +// +// msg.Get("MSH-9").String() // Message type field (full value, unescaped) +// msg.Get("MSH-9.1").String() // Message code (e.g., "ADT") +// msg.Get("MSH-9.2").String() // Trigger event (e.g., "A01") +// msg.Get("PID-3.1").String() // Patient ID +// msg.Get("PID-5.1").String() // Family name +// msg.Get("OBX(0)-5").String() // First OBX segment, field 5 +// msg.Get("PID-3.1").Bytes() // raw bytes without unescaping +func (m *Message) Get(location string) Value { loc, err := ParseLocation(location) if err != nil { - return nil + return Value{} } - result := m.getByLocation(loc) - return result.Bytes() + return m.getByLocation(loc) } // getByLocation navigates the message hierarchy to the specified location. -func (m *Message) getByLocation(loc Location) componentOrField { +func (m *Message) getByLocation(loc Location) Value { // Find the matching segment. matchIdx := 0 var seg *Segment @@ -243,51 +274,34 @@ func (m *Message) getByLocation(loc Location) componentOrField { } } if seg == nil { - return componentOrField{} + return Value{} } field := seg.Field(loc.Field) if field.IsEmpty() { - return componentOrField{} + return Value{} } rep := field.Rep(loc.Repetition) if rep.IsEmpty() { - return componentOrField{} + return Value{} } // If no component specified, return the repetition. if loc.Component == 0 { - return componentOrField{fieldBytes: rep.raw, delims: rep.delims} + return rep.Value } comp := rep.Component(loc.Component) if comp.IsEmpty() && loc.SubComponent == 0 { - return componentOrField{} + return Value{} } // If no subcomponent specified, return the component. if loc.SubComponent == 0 { - return componentOrField{fieldBytes: comp.raw, delims: comp.delims} + return comp.Value } sub := comp.SubComponent(loc.SubComponent) - return componentOrField{fieldBytes: sub.raw, delims: sub.delims} -} - -// componentOrField is a helper to unify the return type from getByLocation. -type componentOrField struct { - fieldBytes []byte - delims Delimiters -} - -func (c componentOrField) String() string { - if len(c.fieldBytes) == 0 { - return "" - } - return string(Unescape(c.fieldBytes, c.delims)) -} - -func (c componentOrField) Bytes() []byte { - return c.fieldBytes + return sub.Value } diff --git a/accessor_test.go b/accessor_test.go index b27d1bf..ee3a5e5 100644 --- a/accessor_test.go +++ b/accessor_test.go @@ -224,7 +224,7 @@ func TestMessageGet(t *testing.T) { {"PID-3.3.3", "ISO"}, {"PID-3[0].3.2", "OID"}, {"PID-3[1].3.1", ""}, - // Non-existent locations return empty string. + // Non-existent locations return empty string via Value.String(). {"ZZZ-1", ""}, {"PID-99", ""}, {"OBX(5)-1", ""}, @@ -235,9 +235,9 @@ func TestMessageGet(t *testing.T) { for _, tt := range tests { t.Run(tt.location, func(t *testing.T) { - got := msg.Get(tt.location) + got := msg.Get(tt.location).String() if got != tt.want { - t.Errorf("Get(%q) = %q, want %q", tt.location, got, tt.want) + t.Errorf("Get(%q).String() = %q, want %q", tt.location, got, tt.want) } }) } @@ -247,42 +247,69 @@ func TestMessageGetInvalidLocation(t *testing.T) { raw := []byte("MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1") msg, _ := ParseMessage(raw) - // Invalid location string should return empty string, not panic. - if got := msg.Get(""); got != "" { - t.Errorf("Get(\"\") = %q, want empty", got) + // Invalid location string should return zero Value (empty string), not panic. + if got := msg.Get("").String(); got != "" { + t.Errorf("Get(\"\").String() = %q, want empty", got) } - if got := msg.Get("garbage"); got != "" { - t.Errorf("Get(\"garbage\") = %q, want empty", got) + if got := msg.Get("garbage").String(); got != "" { + t.Errorf("Get(\"garbage\").String() = %q, want empty", got) } } -func TestMessageGetBytes(t *testing.T) { - raw := []byte("MSH|^~\\&|SEND|F|R|RF|20240101||ADT^A01|1|P|2.5.1") - msg, _ := ParseMessage(raw) - - got := msg.GetBytes("MSH-3") - if string(got) != "SEND" { - t.Errorf("GetBytes(MSH-3) = %q, want SEND", got) +func TestValueZeroValue(t *testing.T) { + var v Value + if !v.IsEmpty() { + t.Error("zero Value should be empty") } - - got = msg.GetBytes("ZZZ-1") - if got != nil { - t.Errorf("GetBytes(ZZZ-1) = %v, want nil", got) + if v.IsNull() { + t.Error("zero Value should not be null") + } + if v.HasValue() { + t.Error("zero Value should not have value") + } + if v.String() != "" { + t.Errorf("zero Value.String() = %q, want empty", v.String()) + } + if v.Bytes() != nil { + t.Errorf("zero Value.Bytes() = %v, want nil", v.Bytes()) } } -func TestGetBytesInvalidLocation(t *testing.T) { - raw := []byte("MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1") +func TestValueFromGet(t *testing.T) { + raw := []byte("MSH|^~\\&|SEND|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|||12345") msg, _ := ParseMessage(raw) - got := msg.GetBytes("invalid") - if got != nil { - t.Errorf("GetBytes(invalid) = %v, want nil", got) + // Present field: Bytes() returns raw bytes without unescaping. + v := msg.Get("MSH-3") + if string(v.Bytes()) != "SEND" { + t.Errorf("Get(MSH-3).Bytes() = %q, want SEND", v.Bytes()) + } + if v.String() != "SEND" { + t.Errorf("Get(MSH-3).String() = %q, want SEND", v.String()) + } + if v.IsEmpty() { + t.Error("Get(MSH-3).IsEmpty() should be false") + } + if v.IsNull() { + t.Error("Get(MSH-3).IsNull() should be false") + } + if !v.HasValue() { + t.Error("Get(MSH-3).HasValue() should be true") + } + + // Not-found location: Bytes() returns nil. + vMissing := msg.Get("ZZZ-1") + if vMissing.Bytes() != nil { + t.Errorf("Get(ZZZ-1).Bytes() = %v, want nil", vMissing.Bytes()) + } + if !vMissing.IsEmpty() { + t.Error("Get(ZZZ-1).IsEmpty() should be true") } - got = msg.GetBytes("") - if got != nil { - t.Errorf("GetBytes(\"\") = %v, want nil", got) + // Invalid location: also returns zero Value. + vInvalid := msg.Get("invalid") + if vInvalid.Bytes() != nil { + t.Errorf("Get(invalid).Bytes() = %v, want nil", vInvalid.Bytes()) } } @@ -322,9 +349,9 @@ func TestGetVeryHighSegmentIndex(t *testing.T) { if err != nil { t.Fatalf("ParseMessage: %v", err) } - // OBX(999) does not exist — should return empty string, no panic. - if got := msg.Get("OBX(999)-5"); got != "" { - t.Errorf("Get(OBX(999)-5) = %q, want empty", got) + // OBX(999) does not exist — should return zero Value, no panic. + if got := msg.Get("OBX(999)-5").String(); got != "" { + t.Errorf("Get(OBX(999)-5).String() = %q, want empty", got) } } @@ -335,9 +362,9 @@ func TestGetSegmentWithNoFields(t *testing.T) { if err != nil { t.Fatalf("ParseMessage: %v", err) } - // ZZZ has no fields at all — accessing ZZZ-1 should return empty, no panic. - if got := msg.Get("ZZZ-1"); got != "" { - t.Errorf("Get(ZZZ-1) = %q, want empty", got) + // ZZZ has no fields at all — accessing ZZZ-1 should return zero Value, no panic. + if got := msg.Get("ZZZ-1").String(); got != "" { + t.Errorf("Get(ZZZ-1).String() = %q, want empty", got) } } @@ -345,9 +372,9 @@ func TestGetSubcomponentEmpty(t *testing.T) { raw := []byte("MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||123") msg, _ := ParseMessage(raw) - // Accessing a subcomponent that doesn't exist returns empty string. - if got := msg.Get("PID-3.1.99"); got != "" { - t.Errorf("Get(PID-3.1.99) = %q, want empty", got) + // Accessing a subcomponent that doesn't exist returns zero Value. + if got := msg.Get("PID-3.1.99").String(); got != "" { + t.Errorf("Get(PID-3.1.99).String() = %q, want empty", got) } } diff --git a/ack_test.go b/ack_test.go index 9db8eab..a80345f 100644 --- a/ack_test.go +++ b/ack_test.go @@ -53,7 +53,7 @@ func TestAck(t *testing.T) { {"MSH-12", "2.5.1"}, } for _, tt := range tests { - if got := ackMsg.Get(tt.loc); got != tt.want { + if got := ackMsg.Get(tt.loc).String(); got != tt.want { t.Errorf("Get(%q) = %q, want %q", tt.loc, got, tt.want) } } @@ -93,7 +93,7 @@ func TestAck(t *testing.T) { t.Fatalf("ParseMessage(ack): %v", err) } - if got := ackMsg.Get("MSH-9.1"); got != "ACK" { + if got := ackMsg.Get("MSH-9.1").String(); got != "ACK" { t.Errorf("MSH-9.1 = %q, want ACK", got) } @@ -133,11 +133,11 @@ func TestAck(t *testing.T) { t.Fatalf("ParseMessage(ack): %v", err) } - if got := ackMsg.Get("MSH-9.1"); got != "ACK" { + if got := ackMsg.Get("MSH-9.1").String(); got != "ACK" { t.Errorf("MSH-9.1 = %q, want ACK", got) } // No trigger event, so MSH-9.2 and MSH-9.3 should be empty. - if got := ackMsg.Get("MSH-9.2"); got != "" { + if got := ackMsg.Get("MSH-9.2").String(); got != "" { t.Errorf("MSH-9.2 = %q, want empty", got) } }) @@ -164,10 +164,10 @@ func TestAck(t *testing.T) { if d.Component != '@' { t.Errorf("Component delimiter = %c, want @", d.Component) } - if got := ackMsg.Get("MSH-9.1"); got != "ACK" { + if got := ackMsg.Get("MSH-9.1").String(); got != "ACK" { t.Errorf("MSH-9.1 = %q, want ACK", got) } - if got := ackMsg.Get("MSH-9.2"); got != "A01" { + if got := ackMsg.Get("MSH-9.2").String(); got != "A01" { t.Errorf("MSH-9.2 = %q, want A01", got) } }) @@ -242,7 +242,7 @@ func TestAck(t *testing.T) { if err != nil { t.Fatalf("ParseMessage(ack): %v", err) } - if got := ackMsg.Get("MSH-9.2"); got != "R01" { + if got := ackMsg.Get("MSH-9.2").String(); got != "R01" { t.Errorf("MSH-9.2 = %q, want R01", got) } }) diff --git a/batch_test.go b/batch_test.go index 3e5c746..3854134 100644 --- a/batch_test.go +++ b/batch_test.go @@ -36,10 +36,10 @@ func TestParseBatch(t *testing.T) { if len(batch.Messages) != 2 { t.Fatalf("expected 2 messages, got %d", len(batch.Messages)) } - if got := batch.Messages[0].Get("MSH-10"); got != "1" { + if got := batch.Messages[0].Get("MSH-10").String(); got != "1" { t.Errorf("msg1 MSH-10 = %q, want 1", got) } - if got := batch.Messages[1].Get("MSH-10"); got != "2" { + if got := batch.Messages[1].Get("MSH-10").String(); got != "2" { t.Errorf("msg2 MSH-10 = %q, want 2", got) } }) @@ -133,10 +133,10 @@ func TestParseFile(t *testing.T) { if len(batch.Messages) != 2 { t.Fatalf("expected 2 messages in batch, got %d", len(batch.Messages)) } - if got := batch.Messages[0].Get("PID-5.1"); got != "Alpha" { + if got := batch.Messages[0].Get("PID-5.1").String(); got != "Alpha" { t.Errorf("msg1 PID-5.1 = %q, want Alpha", got) } - if got := batch.Messages[1].Get("PID-5.1"); got != "Beta" { + if got := batch.Messages[1].Get("PID-5.1").String(); got != "Beta" { t.Errorf("msg2 PID-5.1 = %q, want Beta", got) } }) diff --git a/builder_test.go b/builder_test.go index 46b8332..b313c00 100644 --- a/builder_test.go +++ b/builder_test.go @@ -50,13 +50,13 @@ func TestBuilderBasic(t *testing.T) { t.Fatalf("Build: %v", err) } - if got := msg.Get("MSH-9.1"); got != "ADT" { + if got := msg.Get("MSH-9.1").String(); got != "ADT" { t.Errorf("MSH-9.1 = %q, want %q", got, "ADT") } - if got := msg.Get("MSH-9.2"); got != "A01" { + if got := msg.Get("MSH-9.2").String(); got != "A01" { t.Errorf("MSH-9.2 = %q, want %q", got, "A01") } - if got := msg.Get("MSH-10"); got != "CTRL123" { + if got := msg.Get("MSH-10").String(); got != "CTRL123" { t.Errorf("MSH-10 = %q, want %q", got, "CTRL123") } } @@ -84,19 +84,19 @@ func TestBuilderMultipleSegments(t *testing.T) { t.Errorf("segment count = %d, want 4", count) } - if got := msg.Get("PID-3.1"); got != "12345" { + if got := msg.Get("PID-3.1").String(); got != "12345" { t.Errorf("PID-3.1 = %q, want %q", got, "12345") } - if got := msg.Get("PID-5.1"); got != "Doe" { + if got := msg.Get("PID-5.1").String(); got != "Doe" { t.Errorf("PID-5.1 = %q, want %q", got, "Doe") } - if got := msg.Get("PID-5.2"); got != "John" { + if got := msg.Get("PID-5.2").String(); got != "John" { t.Errorf("PID-5.2 = %q, want %q", got, "John") } - if got := msg.Get("PV1-2"); got != "I" { + if got := msg.Get("PV1-2").String(); got != "I" { t.Errorf("PV1-2 = %q, want %q", got, "I") } - if got := msg.Get("OBX-5"); got != "Normal" { + if got := msg.Get("OBX-5").String(); got != "Normal" { t.Errorf("OBX-5 = %q, want %q", got, "Normal") } } @@ -116,13 +116,13 @@ func TestBuilderComponentSubcomponent(t *testing.T) { t.Fatalf("Build: %v", err) } - if got := msg.Get("PID-3.1"); got != "12345" { + if got := msg.Get("PID-3.1").String(); got != "12345" { t.Errorf("PID-3.1 = %q, want %q", got, "12345") } - if got := msg.Get("PID-3.4.1"); got != "AUTH" { + if got := msg.Get("PID-3.4.1").String(); got != "AUTH" { t.Errorf("PID-3.4.1 = %q, want %q", got, "AUTH") } - if got := msg.Get("PID-3.4.2"); got != "SYSTEM" { + if got := msg.Get("PID-3.4.2").String(); got != "SYSTEM" { t.Errorf("PID-3.4.2 = %q, want %q", got, "SYSTEM") } } @@ -198,7 +198,7 @@ func TestBuilderEscaping(t *testing.T) { } // String() should round-trip through unescape. - if got := msg.Get("PID-3"); got != value { + if got := msg.Get("PID-3").String(); got != value { t.Errorf("PID-3 = %q, want %q", got, value) } @@ -275,7 +275,7 @@ func TestBuilderCustomDelimiters(t *testing.T) { if got := msh.Field(2).String(); got != "@!?%" { t.Errorf("MSH-2 = %q, want %q", got, "@!?%") } - if got := msg.Get("MSH-9.1"); got != "ADT" { + if got := msg.Get("MSH-9.1").String(); got != "ADT" { t.Errorf("MSH-9.1 = %q, want %q", got, "ADT") } } @@ -312,18 +312,18 @@ func TestBuilderBuildReusability(t *testing.T) { } // First message should be unchanged (ParseMessage copies). - if got := msg1.Get("PID-3"); got != "AAA" { + if got := msg1.Get("PID-3").String(); got != "AAA" { t.Errorf("msg1 PID-3 = %q, want %q", got, "AAA") } - if got := msg1.Get("PID-5.1"); got != "" { + if got := msg1.Get("PID-5.1").String(); got != "" { t.Errorf("msg1 PID-5.1 = %q, want empty", got) } // Second message should have the updated values. - if got := msg2.Get("PID-3"); got != "BBB" { + if got := msg2.Get("PID-3").String(); got != "BBB" { t.Errorf("msg2 PID-3 = %q, want %q", got, "BBB") } - if got := msg2.Get("PID-5.1"); got != "Doe" { + if got := msg2.Get("PID-5.1").String(); got != "Doe" { t.Errorf("msg2 PID-5.1 = %q, want %q", got, "Doe") } } @@ -372,7 +372,7 @@ func TestBuilderRoundTrip(t *testing.T) { {"OBX-5", "42"}, } for _, c := range checks { - if got := msg2.Get(c.loc); got != c.want { + if got := msg2.Get(c.loc).String(); got != c.want { t.Errorf("re-parsed %s = %q, want %q", c.loc, got, c.want) } } diff --git a/charset.go b/charset.go new file mode 100644 index 0000000..2615949 --- /dev/null +++ b/charset.go @@ -0,0 +1,48 @@ +// Copyright 2026 Joshua Jones +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hl7 + +// ValueDecoder converts post-unescape field value bytes to the desired +// encoding, typically UTF-8. The decoder is pre-configured for the +// source → target charset pair; it carries no charset parameter. +// +// Callers read MSH-18 to determine the source charset, then select an +// appropriate ValueDecoder before calling DecodeString. When decode is nil, +// DecodeString behaves identically to String() — no conversion is applied and +// no extra allocation is incurred beyond the Unescape fast path. +// +// Note: Unescape runs before the ValueDecoder, so the decoder receives +// resolved byte values. Charset escape sequences (\C..\ and \M..\) are passed +// through verbatim by Unescape; a sophisticated ValueDecoder may parse them, +// but a simple byte-level decoder will treat them as-is. +type ValueDecoder func(data []byte) ([]byte, error) + +// DecodeString returns the unescaped, charset-decoded string value of the +// Value. Unescape runs first; if decode is nil the unescaped bytes are cast +// to string with no further allocation. +// +// DecodeString is promoted to Field, Repetition, Component, and Subcomponent +// via their embedded Value. +func (v Value) DecodeString(decode ValueDecoder) (string, error) { + unescaped := Unescape(v.raw, v.delims) + if decode == nil { + return string(unescaped), nil + } + converted, err := decode(unescaped) + if err != nil { + return "", err + } + return string(converted), nil +} diff --git a/charset_test.go b/charset_test.go new file mode 100644 index 0000000..52cb4db --- /dev/null +++ b/charset_test.go @@ -0,0 +1,486 @@ +// Copyright 2026 Joshua Jones +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hl7 + +import ( + "errors" + "testing" +) + +// latin1ToUTF8 is a test-only ValueDecoder that converts Latin-1 (ISO 8859-1) +// bytes to UTF-8. Each byte in the range 0x80–0xFF is expanded to its UTF-8 +// two-byte sequence; bytes below 0x80 are ASCII and pass through unchanged. +func latin1ToUTF8(data []byte) ([]byte, error) { + out := make([]byte, 0, len(data)) + for _, b := range data { + if b < 0x80 { + out = append(out, b) + } else { + // Latin-1 0x80–0xFF maps to Unicode U+0080–U+00FF. + out = append(out, 0xC0|(b>>6), 0x80|(b&0x3F)) + } + } + return out, nil +} + +// errHighBit is returned by errorDecoder when a byte with the high bit set is seen. +var errHighBit = errors.New("high bit set") + +// errorDecoder rejects any byte with the high bit set. +func errorDecoder(data []byte) ([]byte, error) { + for _, b := range data { + if b&0x80 != 0 { + return nil, errHighBit + } + } + return data, nil +} + +// --- Value.DecodeString --- + +func TestValueDecodeString(t *testing.T) { + delims := DefaultDelimiters() + tests := []struct { + name string + raw []byte + decode ValueDecoder + want string + wantErr bool + }{ + { + name: "nil decoder passthrough ASCII", + raw: []byte("hello"), + decode: nil, + want: "hello", + }, + { + name: "nil decoder passthrough Latin-1", + raw: []byte{0xE9}, // é in Latin-1 + decode: nil, + want: string([]byte{0xE9}), + }, + { + name: "live decoder converts Latin-1", + raw: []byte{0xE9}, // é in Latin-1 + decode: latin1ToUTF8, + want: "é", // U+00E9 in UTF-8 = 0xC3 0xA9 + }, + { + name: "error decoder rejects high bit", + raw: []byte{0xE9}, + decode: errorDecoder, + wantErr: true, + }, + { + name: "empty raw nil decoder", + raw: nil, + decode: nil, + want: "", + }, + { + name: "empty raw live decoder", + raw: nil, + decode: latin1ToUTF8, + want: "", + }, + { + name: "null value (two double quotes) nil decoder", + raw: []byte(`""`), + decode: nil, + want: `""`, + }, + { + name: "escape sequence resolved before decode", + raw: []byte(`\E\`), // escape sequence for the escape char itself + decode: nil, + want: `\`, + }, + { + name: "mixed ASCII and Latin-1", + raw: []byte{'A', 0xE9, 'B'}, + decode: latin1ToUTF8, + want: "A\xC3\xA9B", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := Value{raw: tt.raw, delims: delims} + got, err := v.DecodeString(tt.decode) + if tt.wantErr { + if err == nil { + t.Errorf("DecodeString() expected error, got %q", got) + } + return + } + if err != nil { + t.Fatalf("DecodeString() unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("DecodeString() = %q, want %q", got, tt.want) + } + }) + } +} + +// --- Field.DecodeString --- + +func TestFieldDecodeString(t *testing.T) { + delims := DefaultDelimiters() + tests := []struct { + name string + raw []byte + decode ValueDecoder + want string + wantErr bool + }{ + { + name: "nil decoder ASCII", + raw: []byte("Smith"), + decode: nil, + want: "Smith", + }, + { + name: "live decoder Latin-1", + raw: []byte{0xE9}, + decode: latin1ToUTF8, + want: "é", + }, + { + name: "error decoder", + raw: []byte{0xE9}, + decode: errorDecoder, + wantErr: true, + }, + { + name: "empty field", + raw: nil, + decode: nil, + want: "", + }, + { + name: "null field", + raw: []byte(`""`), + decode: nil, + want: `""`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := Field{Value: Value{raw: tt.raw, delims: delims}} + got, err := f.DecodeString(tt.decode) + if tt.wantErr { + if err == nil { + t.Errorf("DecodeString() expected error, got %q", got) + } + return + } + if err != nil { + t.Fatalf("DecodeString() unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("DecodeString() = %q, want %q", got, tt.want) + } + }) + } +} + +// --- Repetition.DecodeString --- + +func TestRepetitionDecodeString(t *testing.T) { + delims := DefaultDelimiters() + tests := []struct { + name string + raw []byte + decode ValueDecoder + want string + wantErr bool + }{ + { + name: "nil decoder ASCII", + raw: []byte("rep1"), + decode: nil, + want: "rep1", + }, + { + name: "live decoder Latin-1", + raw: []byte{0xE9}, + decode: latin1ToUTF8, + want: "é", + }, + { + name: "error decoder", + raw: []byte{0xE9}, + decode: errorDecoder, + wantErr: true, + }, + { + name: "empty repetition", + raw: nil, + decode: nil, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Repetition{Value: Value{raw: tt.raw, delims: delims}} + got, err := r.DecodeString(tt.decode) + if tt.wantErr { + if err == nil { + t.Errorf("DecodeString() expected error, got %q", got) + } + return + } + if err != nil { + t.Fatalf("DecodeString() unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("DecodeString() = %q, want %q", got, tt.want) + } + }) + } +} + +// --- Component.DecodeString --- + +func TestComponentDecodeString(t *testing.T) { + delims := DefaultDelimiters() + tests := []struct { + name string + raw []byte + decode ValueDecoder + want string + wantErr bool + }{ + { + name: "nil decoder ASCII", + raw: []byte("comp1"), + decode: nil, + want: "comp1", + }, + { + name: "live decoder Latin-1", + raw: []byte{0xE9}, + decode: latin1ToUTF8, + want: "é", + }, + { + name: "error decoder", + raw: []byte{0xE9}, + decode: errorDecoder, + wantErr: true, + }, + { + name: "empty component", + raw: nil, + decode: nil, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := Component{Value: Value{raw: tt.raw, delims: delims}} + got, err := c.DecodeString(tt.decode) + if tt.wantErr { + if err == nil { + t.Errorf("DecodeString() expected error, got %q", got) + } + return + } + if err != nil { + t.Fatalf("DecodeString() unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("DecodeString() = %q, want %q", got, tt.want) + } + }) + } +} + +// --- Subcomponent.DecodeString --- + +func TestSubcomponentDecodeString(t *testing.T) { + delims := DefaultDelimiters() + tests := []struct { + name string + raw []byte + decode ValueDecoder + want string + wantErr bool + }{ + { + name: "nil decoder ASCII", + raw: []byte("sub1"), + decode: nil, + want: "sub1", + }, + { + name: "live decoder Latin-1", + raw: []byte{0xE9}, + decode: latin1ToUTF8, + want: "é", + }, + { + name: "error decoder", + raw: []byte{0xE9}, + decode: errorDecoder, + wantErr: true, + }, + { + name: "empty subcomponent", + raw: nil, + decode: nil, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc := Subcomponent{Value: Value{raw: tt.raw, delims: delims}} + got, err := sc.DecodeString(tt.decode) + if tt.wantErr { + if err == nil { + t.Errorf("DecodeString() expected error, got %q", got) + } + return + } + if err != nil { + t.Fatalf("DecodeString() unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("DecodeString() = %q, want %q", got, tt.want) + } + }) + } +} + +// TestDecodeStringUnescapeFirst verifies that Unescape runs before the +// ValueDecoder — a field containing \F\ (the escape sequence for the field +// separator) gives the decoder a literal | byte, not the raw escape bytes. +func TestDecodeStringUnescapeFirst(t *testing.T) { + delims := DefaultDelimiters() + + // \F\ is the HL7 escape sequence for the field separator character. + // After Unescape it becomes a literal '|' (0x7C), which is ASCII (< 0x80) + // and does NOT trigger errorDecoder. + raw := []byte(`\F\`) // three bytes: \, F, \ + + // errorDecoder rejects bytes with the high bit set but passes ASCII. + // If Unescape ran first, the decoder sees 0x7C ('|'), which is fine. + // If the decoder ran on raw bytes, it would see '\' (0x5C) — still ASCII — + // so let's use a field with a high-bit escape sequence to prove order. + + // \X00E9\ is the hex escape for 0xE9. After Unescape it becomes a single + // byte 0xE9 (high bit set). errorDecoder should reject it. + rawHex := []byte(`\XE9\`) + + // With Unescape first: decoder sees 0xE9 → error. + f := Field{Value: Value{raw: rawHex, delims: delims}} + _, err := f.DecodeString(errorDecoder) + if err == nil { + t.Errorf("expected errorDecoder to reject post-unescape 0xE9, but got no error") + } + + // With nil decoder: Unescape converts \XE9\ to 0xE9, String() returns that byte. + v := Value{raw: rawHex, delims: delims} + s, err := v.DecodeString(nil) + if err != nil { + t.Fatalf("nil decoder error: %v", err) + } + if s != string([]byte{0xE9}) { + t.Errorf("got %q, want single byte 0xE9", s) + } + + // Verify \F\ unescapes to | before decode. + fField := Field{Value: Value{raw: raw, delims: delims}} + got, err := fField.DecodeString(errorDecoder) + if err != nil { + t.Errorf("expected no error for \\F\\ (unescapes to ASCII '|'), got: %v", err) + } + if got != "|" { + t.Errorf("DecodeString(\\F\\) = %q, want %q", got, "|") + } +} + +// --- Benchmarks --- + +func BenchmarkValueDecodeString(b *testing.B) { + raw := []byte("Smith^John^M") + delims := DefaultDelimiters() + v := Value{raw: raw, delims: delims} + + b.Run("nil decoder", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = v.DecodeString(nil) + } + }) + + rawLatin1 := []byte{'S', 'm', 'i', 't', 'h', 0xE9} + vLatin1 := Value{raw: rawLatin1, delims: delims} + b.Run("live decoder", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = vLatin1.DecodeString(latin1ToUTF8) + } + }) +} + +func BenchmarkFieldDecodeString(b *testing.B) { + raw := []byte("Smith^John^M") + delims := DefaultDelimiters() + f := Field{Value: Value{raw: raw, delims: delims}} + + b.Run("nil decoder", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = f.DecodeString(nil) + } + }) + + rawLatin1 := []byte{'S', 'm', 'i', 't', 'h', 0xE9} + fLatin1 := Field{Value: Value{raw: rawLatin1, delims: delims}} + b.Run("live decoder", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = fLatin1.DecodeString(latin1ToUTF8) + } + }) +} + +func BenchmarkComponentDecodeString(b *testing.B) { + raw := []byte("Smith") + delims := DefaultDelimiters() + c := Component{Value: Value{raw: raw, delims: delims}} + + b.Run("nil decoder", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = c.DecodeString(nil) + } + }) + + rawLatin1 := []byte{'S', 'm', 'i', 't', 'h', 0xE9} + cLatin1 := Component{Value: Value{raw: rawLatin1, delims: delims}} + b.Run("live decoder", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = cLatin1.DecodeString(latin1ToUTF8) + } + }) +} diff --git a/component.go b/component.go index 48e08da..c80df6a 100644 --- a/component.go +++ b/component.go @@ -17,23 +17,7 @@ package hl7 // Subcomponent represents the lowest level of the HL7 message hierarchy. // It is a leaf node holding raw bytes from a single subcomponent value. type Subcomponent struct { - raw []byte - delims Delimiters -} - -// String returns the unescaped string value of this subcomponent. -func (sc Subcomponent) String() string { - return string(Unescape(sc.raw, sc.delims)) -} - -// Bytes returns the raw bytes without escape processing. -func (sc Subcomponent) Bytes() []byte { - return sc.raw -} - -// IsEmpty returns true if this subcomponent has no value. -func (sc Subcomponent) IsEmpty() bool { - return len(sc.raw) == 0 + Value } // Component represents a single component within a field repetition. @@ -43,38 +27,20 @@ func (sc Subcomponent) IsEmpty() bool { // Component is a lightweight value type that scans for subcomponent // boundaries on each access rather than caching parsed results. type Component struct { - raw []byte - delims Delimiters -} - -// String returns the unescaped string value of this component. -// This returns the full component value including any subcomponent separators -// resolved through escape processing. -func (c Component) String() string { - return string(Unescape(c.raw, c.delims)) -} - -// Bytes returns the raw bytes without escape processing. -func (c Component) Bytes() []byte { - return c.raw -} - -// IsEmpty returns true if this component has no value. -func (c Component) IsEmpty() bool { - return len(c.raw) == 0 + Value } // SubComponent returns the subcomponent at the given 1-based index. // Returns an empty Subcomponent if the index is out of range. func (c Component) SubComponent(index int) Subcomponent { if index < 1 { - return Subcomponent{delims: c.delims} + return Subcomponent{Value: Value{delims: c.delims}} } slice := nthSlice(c.raw, c.delims.SubComponent, index-1) if slice == nil { - return Subcomponent{delims: c.delims} + return Subcomponent{Value: Value{delims: c.delims}} } - return Subcomponent{raw: slice, delims: c.delims} + return Subcomponent{Value: Value{raw: slice, delims: c.delims}} } // SubComponentCount returns the number of subcomponents. diff --git a/component_test.go b/component_test.go index 2e1d64e..810345c 100644 --- a/component_test.go +++ b/component_test.go @@ -20,32 +20,32 @@ func TestSubcomponent(t *testing.T) { d := DefaultDelimiters() t.Run("String", func(t *testing.T) { - sc := Subcomponent{raw: []byte("hello"), delims: d} + sc := Subcomponent{Value: Value{raw: []byte("hello"), delims: d}} if got := sc.String(); got != "hello" { t.Errorf("String() = %q, want %q", got, "hello") } }) t.Run("Bytes", func(t *testing.T) { - sc := Subcomponent{raw: []byte("hello"), delims: d} + sc := Subcomponent{Value: Value{raw: []byte("hello"), delims: d}} if got := string(sc.Bytes()); got != "hello" { t.Errorf("Bytes() = %q, want %q", got, "hello") } }) t.Run("IsEmpty", func(t *testing.T) { - sc := Subcomponent{raw: []byte{}, delims: d} + sc := Subcomponent{Value: Value{raw: []byte{}, delims: d}} if !sc.IsEmpty() { t.Error("expected IsEmpty() = true") } - sc2 := Subcomponent{raw: []byte("x"), delims: d} + sc2 := Subcomponent{Value: Value{raw: []byte("x"), delims: d}} if sc2.IsEmpty() { t.Error("expected IsEmpty() = false") } }) t.Run("with escape", func(t *testing.T) { - sc := Subcomponent{raw: []byte(`hello\F\world`), delims: d} + sc := Subcomponent{Value: Value{raw: []byte(`hello\F\world`), delims: d}} if got := sc.String(); got != "hello|world" { t.Errorf("String() = %q, want %q", got, "hello|world") } @@ -56,7 +56,7 @@ func TestComponent(t *testing.T) { d := DefaultDelimiters() t.Run("no subcomponents", func(t *testing.T) { - c := Component{raw: []byte("simple"), delims: d} + c := Component{Value: Value{raw: []byte("simple"), delims: d}} if got := c.String(); got != "simple" { t.Errorf("String() = %q, want %q", got, "simple") } @@ -66,7 +66,7 @@ func TestComponent(t *testing.T) { }) t.Run("with subcomponents", func(t *testing.T) { - c := Component{raw: []byte("sub1&sub2&sub3"), delims: d} + c := Component{Value: Value{raw: []byte("sub1&sub2&sub3"), delims: d}} if c.SubComponentCount() != 3 { t.Errorf("SubComponentCount() = %d, want 3", c.SubComponentCount()) } @@ -82,7 +82,7 @@ func TestComponent(t *testing.T) { }) t.Run("out of range", func(t *testing.T) { - c := Component{raw: []byte("only"), delims: d} + c := Component{Value: Value{raw: []byte("only"), delims: d}} sub := c.SubComponent(5) if !sub.IsEmpty() { t.Error("expected out-of-range SubComponent to be empty") @@ -94,9 +94,95 @@ func TestComponent(t *testing.T) { }) t.Run("empty component", func(t *testing.T) { - c := Component{raw: []byte{}, delims: d} + c := Component{Value: Value{raw: []byte{}, delims: d}} if !c.IsEmpty() { t.Error("expected IsEmpty() = true") } }) } + +func TestComponentIsNull(t *testing.T) { + d := DefaultDelimiters() + + t.Run("null component", func(t *testing.T) { + c := Component{Value: Value{raw: []byte(`""`), delims: d}} + if !c.IsNull() { + t.Error("expected IsNull() = true for component with \"\"") + } + }) + + t.Run("non-null component", func(t *testing.T) { + c := Component{Value: Value{raw: []byte("value"), delims: d}} + if c.IsNull() { + t.Error("expected IsNull() = false for non-null component") + } + }) +} + +func TestComponentHasValue(t *testing.T) { + d := DefaultDelimiters() + + t.Run("has value", func(t *testing.T) { + c := Component{Value: Value{raw: []byte("hello"), delims: d}} + if !c.HasValue() { + t.Error("expected HasValue() = true") + } + }) + + t.Run("empty has no value", func(t *testing.T) { + c := Component{Value: Value{raw: []byte{}, delims: d}} + if c.HasValue() { + t.Error("expected HasValue() = false for empty component") + } + }) + + t.Run("null has no value", func(t *testing.T) { + c := Component{Value: Value{raw: []byte(`""`), delims: d}} + if c.HasValue() { + t.Error("expected HasValue() = false for null component") + } + }) +} + +func TestSubcomponentIsNull(t *testing.T) { + d := DefaultDelimiters() + + t.Run("null subcomponent", func(t *testing.T) { + sc := Subcomponent{Value: Value{raw: []byte(`""`), delims: d}} + if !sc.IsNull() { + t.Error("expected IsNull() = true for subcomponent with \"\"") + } + }) + + t.Run("non-null subcomponent", func(t *testing.T) { + sc := Subcomponent{Value: Value{raw: []byte("value"), delims: d}} + if sc.IsNull() { + t.Error("expected IsNull() = false for non-null subcomponent") + } + }) +} + +func TestSubcomponentHasValue(t *testing.T) { + d := DefaultDelimiters() + + t.Run("has value", func(t *testing.T) { + sc := Subcomponent{Value: Value{raw: []byte("hello"), delims: d}} + if !sc.HasValue() { + t.Error("expected HasValue() = true") + } + }) + + t.Run("empty has no value", func(t *testing.T) { + sc := Subcomponent{Value: Value{raw: []byte{}, delims: d}} + if sc.HasValue() { + t.Error("expected HasValue() = false for empty subcomponent") + } + }) + + t.Run("null has no value", func(t *testing.T) { + sc := Subcomponent{Value: Value{raw: []byte(`""`), delims: d}} + if sc.HasValue() { + t.Error("expected HasValue() = false for null subcomponent") + } + }) +} diff --git a/continuation_test.go b/continuation_test.go index 61f3518..b8d9738 100644 --- a/continuation_test.go +++ b/continuation_test.go @@ -162,25 +162,25 @@ func TestConcatenate_resultParseable(t *testing.T) { } // MSH-9.1 should be accessible. - msgType := result.Get("MSH-9.1") + msgType := result.Get("MSH-9.1").String() if msgType != "QRY" { t.Errorf("MSH-9.1 = %q, want QRY", msgType) } // First PID segment (index 0). - pid1ID := result.Get("PID(0)-3.1") + pid1ID := result.Get("PID(0)-3.1").String() if pid1ID != "12345" { t.Errorf("PID(0)-3.1 = %q, want 12345", pid1ID) } // Second PID segment (index 1). - pid2ID := result.Get("PID(1)-3.1") + pid2ID := result.Get("PID(1)-3.1").String() if pid2ID != "67890" { t.Errorf("PID(1)-3.1 = %q, want 67890", pid2ID) } // Second PID family name. - pid2Name := result.Get("PID(1)-5.1") + pid2Name := result.Get("PID(1)-5.1").String() if pid2Name != "Jones" { t.Errorf("PID(1)-5.1 = %q, want Jones", pid2Name) } @@ -229,7 +229,7 @@ func TestConcatenate_multiplePages(t *testing.T) { // Verify OBX sequence IDs are correct. for i, wantID := range []string{"1", "2", "3"} { loc := "OBX(" + string(rune('0'+i)) + ")-1" - got := abc.Get(loc) + got := abc.Get(loc).String() if got != wantID { t.Errorf("%s = %q, want %q", loc, got, wantID) } @@ -275,18 +275,18 @@ func TestConcatenate_nextHasADDSegments(t *testing.T) { } // OBX(0)-5: page1's observation value, unchanged. - if got := result.Get("OBX(0)-5"); got != "Page one content" { + if got := result.Get("OBX(0)-5").String(); got != "Page one content" { t.Errorf("OBX(0)-5 = %q, want %q", got, "Page one content") } // OBX(1)-5: page2's own field value, unchanged. // mergeADD strips "\rADD" but keeps the "|", so ADD field 1 becomes a // distinct next field of OBX rather than being concatenated onto OBX-5. - if got := result.Get("OBX(1)-5"); got != "Page two first part" { + if got := result.Get("OBX(1)-5").String(); got != "Page two first part" { t.Errorf("OBX(1)-5 = %q, want %q", got, "Page two first part") } // OBX(1)-6: ADD field 1 separated from the preceding segment by "|". - if got := result.Get("OBX(1)-6"); got != "Page two second part" { + if got := result.Get("OBX(1)-6").String(); got != "Page two second part" { t.Errorf("OBX(1)-6 = %q, want %q", got, "Page two second part") } } @@ -328,12 +328,12 @@ func TestConcatenate_secondPageStartsWithADD(t *testing.T) { } // OBX-5: page1's original field value, unchanged. - if got := result.Get("OBX(0)-5"); got != "First part" { + if got := result.Get("OBX(0)-5").String(); got != "First part" { t.Errorf("OBX(0)-5 = %q, want %q", got, "First part") } // OBX-6: ADD field 1 from page2. mergeADD appends fields (not within-field // concatenation), so ADD field 1 becomes the next field of OBX. - if got := result.Get("OBX(0)-6"); got != "Second part" { + if got := result.Get("OBX(0)-6").String(); got != "Second part" { t.Errorf("OBX(0)-6 = %q, want %q", got, "Second part") } } diff --git a/doc.go b/doc.go index 23c59b5..a11eac1 100644 --- a/doc.go +++ b/doc.go @@ -34,22 +34,45 @@ // if err != nil { // log.Fatal(err) // } -// fmt.Println(msg.Get("MSH-9.1")) // "ADT" -// fmt.Println(msg.Get("PID-5.1")) // Family name +// fmt.Println(msg.Get("MSH-9.1").String()) // "ADT" +// fmt.Println(msg.Get("PID-5.1").String()) // Family name // // # Terser-Style Access // -// Get and GetBytes retrieve values using terser-style location strings. +// 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") // Message code -// msg.Get("PID-3[1].4.2") // 2nd repetition of PID-3, component 4, subcomponent 2 -// msg.Get("OBX(1)-5") // 2nd OBX segment (0-based), field 5 -// msg.GetBytes("PID-3.1") // raw bytes without unescaping +// 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() // raw bytes without unescaping // -// Returns an empty string (or nil) if the location is invalid or the value -// is not present. +// 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 ValueDecoder is a +// func([]byte) ([]byte, error) that converts post-unescape bytes to UTF-8. +// 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 decoder. The \C..\ and +// \M..\ charset escape sequences are passed through verbatim by Unescape; a +// sophisticated ValueDecoder may parse them, but a simple byte-level decoder +// will treat them as-is. // // # Hierarchical Traversal // @@ -76,7 +99,7 @@ // // reader := hl7.NewReader(conn, hl7.WithMode(hl7.ModeMLLP)) // err := reader.EachMessage(func(msg *hl7.Message) error { -// msgType := msg.Get("MSH-9") +// msgType := msg.Get("MSH-9").String() // return nil // }) // @@ -238,8 +261,9 @@ // - Empty (||): No value provided; preserve existing data during updates. // - Null (|""|): Explicitly set to null; clear existing data. // -// Use Field.IsNull() and Field.IsEmpty() to distinguish these cases. -// Field.HasValue() returns true when neither empty nor null. +// 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 // diff --git a/examples/builder/main.go b/examples/builder/main.go index a0c9954..93616b9 100644 --- a/examples/builder/main.go +++ b/examples/builder/main.go @@ -60,10 +60,10 @@ func main() { } // Verify it round-trips through the parser. - fmt.Println("Message type:", msg.Get("MSH-9.1")+"^"+msg.Get("MSH-9.2")) - fmt.Println("Patient:", msg.Get("PID-5.1")+", "+msg.Get("PID-5.2")) - fmt.Println("Patient ID:", msg.Get("PID-3.1")) - fmt.Println("Patient class:", msg.Get("PV1-2")) + fmt.Println("Message type:", msg.Get("MSH-9.1").String()+"^"+msg.Get("MSH-9.2").String()) + fmt.Println("Patient:", msg.Get("PID-5.1").String()+", "+msg.Get("PID-5.2").String()) + fmt.Println("Patient ID:", msg.Get("PID-3.1").String()) + fmt.Println("Patient class:", msg.Get("PV1-2").String()) fmt.Println() // Write with MLLP framing. diff --git a/examples/full/main.go b/examples/full/main.go index a08f7aa..e94e964 100644 --- a/examples/full/main.go +++ b/examples/full/main.go @@ -32,8 +32,8 @@ func main() { if err != nil { log.Fatal("read:", err) } - fmt.Println("Read message:", msg.Get("MSH-9.1")+"^"+msg.Get("MSH-9.2")) - fmt.Println("Patient:", msg.Get("PID-5.1")+", "+msg.Get("PID-5.2")) + fmt.Println("Read message:", msg.Get("MSH-9.1").String()+"^"+msg.Get("MSH-9.2").String()) + fmt.Println("Patient:", msg.Get("PID-5.1").String()+", "+msg.Get("PID-5.2").String()) fmt.Println() // Define an inbound schema. @@ -87,10 +87,10 @@ func main() { if err != nil { log.Fatal("transform:", err) } - fmt.Println("\nTransformed MSH-3:", transformed.Get("MSH-3")) - fmt.Println("Transformed MSH-4:", transformed.Get("MSH-4")) - fmt.Println("Transformed PID-3:", transformed.Get("PID-3")) - fmt.Println("Transformed PID-4:", transformed.Get("PID-4")) + fmt.Println("\nTransformed MSH-3:", transformed.Get("MSH-3").String()) + fmt.Println("Transformed MSH-4:", transformed.Get("MSH-4").String()) + fmt.Println("Transformed PID-3:", transformed.Get("PID-3").String()) + fmt.Println("Transformed PID-4:", transformed.Get("PID-4").String()) fmt.Println() // Define an outbound schema (different expectations post-transform). diff --git a/field.go b/field.go index d234fcf..e9ab334 100644 --- a/field.go +++ b/field.go @@ -23,34 +23,7 @@ package hl7 // Field is a lightweight value type that scans for repetition // boundaries on each access rather than caching parsed results. type Field struct { - raw []byte - delims Delimiters -} - -// IsNull returns true if the field is explicitly null. -// Per HL7 v2, a null field is represented as two double-quote characters (""). -func (f Field) IsNull() bool { - return len(f.raw) == 2 && f.raw[0] == '"' && f.raw[1] == '"' -} - -// IsEmpty returns true if the field was omitted (nothing between delimiters). -func (f Field) IsEmpty() bool { - return len(f.raw) == 0 -} - -// HasValue returns true if the field has a non-null, non-empty value. -func (f Field) HasValue() bool { - return !f.IsEmpty() && !f.IsNull() -} - -// String returns the unescaped string value of the first repetition. -func (f Field) String() string { - return string(Unescape(f.raw, f.delims)) -} - -// Bytes returns the raw bytes of the field without escape processing. -func (f Field) Bytes() []byte { - return f.raw + Value } // RepetitionCount returns the number of repetitions. @@ -62,13 +35,13 @@ func (f Field) RepetitionCount() int { // Returns an empty Repetition if the index is out of range. func (f Field) Rep(index int) Repetition { if index < 0 { - return Repetition{delims: f.delims} + return Repetition{Value: Value{delims: f.delims}} } slice := nthSlice(f.raw, f.delims.Repetition, index) if slice == nil { - return Repetition{delims: f.delims} + return Repetition{Value: Value{delims: f.delims}} } - return Repetition{raw: slice, delims: f.delims} + return Repetition{Value: Value{raw: slice, delims: f.delims}} } // Repetition represents one repetition of a field. @@ -77,36 +50,20 @@ func (f Field) Rep(index int) Repetition { // Repetition is a lightweight value type that scans for component // boundaries on each access rather than caching parsed results. type Repetition struct { - raw []byte - delims Delimiters -} - -// String returns the unescaped string value of this repetition. -func (r Repetition) String() string { - return string(Unescape(r.raw, r.delims)) -} - -// Bytes returns the raw bytes without escape processing. -func (r Repetition) Bytes() []byte { - return r.raw -} - -// IsEmpty returns true if this repetition has no value. -func (r Repetition) IsEmpty() bool { - return len(r.raw) == 0 + Value } // Component returns the component at the given 1-based index. // Returns an empty Component if the index is out of range. func (r Repetition) Component(index int) Component { if index < 1 { - return Component{delims: r.delims} + return Component{Value: Value{delims: r.delims}} } slice := nthSlice(r.raw, r.delims.Component, index-1) if slice == nil { - return Component{delims: r.delims} + return Component{Value: Value{delims: r.delims}} } - return Component{raw: slice, delims: r.delims} + return Component{Value: Value{raw: slice, delims: r.delims}} } // ComponentCount returns the number of components. diff --git a/field_test.go b/field_test.go index edaa097..8b81785 100644 --- a/field_test.go +++ b/field_test.go @@ -20,7 +20,7 @@ func TestField(t *testing.T) { d := DefaultDelimiters() t.Run("simple value", func(t *testing.T) { - f := Field{raw: []byte("hello"), delims: d} + f := Field{Value: Value{raw: []byte("hello"), delims: d}} if got := f.String(); got != "hello" { t.Errorf("String() = %q, want %q", got, "hello") } @@ -36,7 +36,7 @@ func TestField(t *testing.T) { }) t.Run("empty field", func(t *testing.T) { - f := Field{raw: []byte{}, delims: d} + f := Field{Value: Value{raw: []byte{}, delims: d}} if !f.IsEmpty() { t.Error("expected IsEmpty() = true") } @@ -46,7 +46,7 @@ func TestField(t *testing.T) { }) t.Run("null field", func(t *testing.T) { - f := Field{raw: []byte(`""`), delims: d} + f := Field{Value: Value{raw: []byte(`""`), delims: d}} if !f.IsNull() { t.Error("expected IsNull() = true") } @@ -59,7 +59,7 @@ func TestField(t *testing.T) { }) t.Run("repetitions", func(t *testing.T) { - f := Field{raw: []byte("Smith~Jones~Williams"), delims: d} + f := Field{Value: Value{raw: []byte("Smith~Jones~Williams"), delims: d}} if f.RepetitionCount() != 3 { t.Errorf("RepetitionCount() = %d, want 3", f.RepetitionCount()) } @@ -75,7 +75,7 @@ func TestField(t *testing.T) { }) t.Run("rep out of range", func(t *testing.T) { - f := Field{raw: []byte("only"), delims: d} + f := Field{Value: Value{raw: []byte("only"), delims: d}} rep := f.Rep(5) if !rep.IsEmpty() { t.Error("expected out-of-range Rep to be empty") @@ -87,7 +87,7 @@ func TestField(t *testing.T) { }) t.Run("component shorthand", func(t *testing.T) { - f := Field{raw: []byte("Doe^John^M"), delims: d} + f := Field{Value: Value{raw: []byte("Doe^John^M"), delims: d}} if got := f.Rep(0).Component(1).String(); got != "Doe" { t.Errorf("Component(1) = %q, want %q", got, "Doe") } @@ -100,7 +100,7 @@ func TestField(t *testing.T) { }) t.Run("single rep single component", func(t *testing.T) { - f := Field{raw: []byte("value"), delims: d} + f := Field{Value: Value{raw: []byte("value"), delims: d}} if f.RepetitionCount() != 1 { t.Errorf("RepetitionCount() = %d, want 1", f.RepetitionCount()) } @@ -114,7 +114,7 @@ func TestRepetition(t *testing.T) { d := DefaultDelimiters() t.Run("components", func(t *testing.T) { - r := Repetition{raw: []byte("Doe^John^M"), delims: d} + r := Repetition{Value: Value{raw: []byte("Doe^John^M"), delims: d}} if r.ComponentCount() != 3 { t.Errorf("ComponentCount() = %d, want 3", r.ComponentCount()) } @@ -124,7 +124,7 @@ func TestRepetition(t *testing.T) { }) t.Run("with subcomponents", func(t *testing.T) { - r := Repetition{raw: []byte("comp1&sub1&sub2^comp2"), delims: d} + r := Repetition{Value: Value{raw: []byte("comp1&sub1&sub2^comp2"), delims: d}} if r.ComponentCount() != 2 { t.Errorf("ComponentCount() = %d, want 2", r.ComponentCount()) } @@ -141,17 +141,67 @@ func TestRepetition(t *testing.T) { }) t.Run("empty", func(t *testing.T) { - r := Repetition{raw: []byte{}, delims: d} + r := Repetition{Value: Value{raw: []byte{}, delims: d}} if !r.IsEmpty() { t.Error("expected IsEmpty() = true") } }) t.Run("out of range component", func(t *testing.T) { - r := Repetition{raw: []byte("only"), delims: d} + r := Repetition{Value: Value{raw: []byte("only"), delims: d}} comp := r.Component(10) if !comp.IsEmpty() { t.Error("expected out-of-range Component to be empty") } }) } + +func TestRepetitionIsNull(t *testing.T) { + d := DefaultDelimiters() + + t.Run("null repetition", func(t *testing.T) { + r := Repetition{Value: Value{raw: []byte(`""`), delims: d}} + if !r.IsNull() { + t.Error("expected IsNull() = true for repetition with \"\"") + } + }) + + t.Run("non-null repetition", func(t *testing.T) { + r := Repetition{Value: Value{raw: []byte("value"), delims: d}} + if r.IsNull() { + t.Error("expected IsNull() = false for non-null repetition") + } + }) + + t.Run("empty is not null", func(t *testing.T) { + r := Repetition{Value: Value{raw: []byte{}, delims: d}} + if r.IsNull() { + t.Error("expected IsNull() = false for empty repetition") + } + }) +} + +func TestRepetitionHasValue(t *testing.T) { + d := DefaultDelimiters() + + t.Run("has value", func(t *testing.T) { + r := Repetition{Value: Value{raw: []byte("hello"), delims: d}} + if !r.HasValue() { + t.Error("expected HasValue() = true for non-empty, non-null repetition") + } + }) + + t.Run("empty has no value", func(t *testing.T) { + r := Repetition{Value: Value{raw: []byte{}, delims: d}} + if r.HasValue() { + t.Error("expected HasValue() = false for empty repetition") + } + }) + + t.Run("null has no value", func(t *testing.T) { + r := Repetition{Value: Value{raw: []byte(`""`), delims: d}} + if r.HasValue() { + t.Error("expected HasValue() = false for null repetition") + } + }) +} diff --git a/message_test.go b/message_test.go index 3f84c01..57c5455 100644 --- a/message_test.go +++ b/message_test.go @@ -161,7 +161,7 @@ func TestGoldenADTA01(t *testing.T) { for _, tt := range tests { t.Run(tt.field, func(t *testing.T) { - got := msg.Get(tt.field) + got := msg.Get(tt.field).String() if got != tt.want { t.Errorf("Get(%q) = %q, want %q", tt.field, got, tt.want) } @@ -181,15 +181,15 @@ func TestGoldenORUR01(t *testing.T) { } // Check message type. - if got := msg.Get("MSH-9.1"); got != "ORU" { + if got := msg.Get("MSH-9.1").String(); got != "ORU" { t.Errorf("MSH-9.1 = %q, want %q", got, "ORU") } - if got := msg.Get("MSH-9.2"); got != "R01" { + if got := msg.Get("MSH-9.2").String(); got != "R01" { t.Errorf("MSH-9.2 = %q, want %q", got, "R01") } // Check patient. - if got := msg.Get("PID-5.1"); got != "Smith" { + if got := msg.Get("PID-5.1").String(); got != "Smith" { t.Errorf("PID-5.1 = %q, want %q", got, "Smith") } @@ -230,12 +230,12 @@ func TestGoldenEscapeSequences(t *testing.T) { } // PID-5.1 contains "Doe\T\Smith" which should unescape to "Doe&Smith". - if got := msg.Get("PID-5.1"); got != "Doe&Smith" { + if got := msg.Get("PID-5.1").String(); got != "Doe&Smith" { t.Errorf("PID-5.1 = %q, want %q", got, "Doe&Smith") } // PID-5.2 contains "John\S\Jane" which should unescape to "John^Jane". - if got := msg.Get("PID-5.2"); got != "John^Jane" { + if got := msg.Get("PID-5.2").String(); got != "John^Jane" { t.Errorf("PID-5.2 = %q, want %q", got, "John^Jane") } } @@ -393,7 +393,7 @@ func TestParseMessageADD_NoADD(t *testing.T) { if got := len(msg.Segments()); got != 2 { t.Errorf("len(Segments()) = %d, want 2", got) } - if got := msg.Get("PID-5.1"); got != "Doe" { + if got := msg.Get("PID-5.1").String(); got != "Doe" { t.Errorf("PID-5.1 = %q, want %q", got, "Doe") } } @@ -430,7 +430,7 @@ func TestParseMessageADD_MultipleSegmentsWithADD(t *testing.T) { } // PID should be unaffected. - if got := msg.Get("PID-5.1"); got != "Doe" { + if got := msg.Get("PID-5.1").String(); got != "Doe" { t.Errorf("PID-5.1 = %q, want %q", got, "Doe") } @@ -529,10 +529,10 @@ func TestParseMessageADD_AccessorWorks(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - if got := msg.Get("OBX-5"); got != "long_dat" { + if got := msg.Get("OBX-5").String(); got != "long_dat" { t.Errorf("Get(OBX-5) = %q, want %q", got, "long_dat") } - if got := msg.Get("OBX-6"); got != "a_continued" { + if got := msg.Get("OBX-6").String(); got != "a_continued" { t.Errorf("Get(OBX-6) = %q, want %q", got, "a_continued") } } @@ -767,11 +767,23 @@ func BenchmarkGetAccessor(b *testing.B) { } msg, _ := ParseMessage(data) - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = msg.Get("MSH-9.1") - _ = msg.Get("PID-5.1") - _ = msg.Get("OBX(0)-5") - } + // Value only: location resolution with no string conversion. + b.Run("Value", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = msg.Get("MSH-9.1") + _ = msg.Get("PID-5.1") + _ = msg.Get("OBX(0)-5") + } + }) + + // String: location resolution + unescape + string conversion. + b.Run("String", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = msg.Get("MSH-9.1").String() + _ = msg.Get("PID-5.1").String() + _ = msg.Get("OBX(0)-5").String() + } + }) } diff --git a/reader_test.go b/reader_test.go index 88f6432..b424236 100644 --- a/reader_test.go +++ b/reader_test.go @@ -41,10 +41,10 @@ func TestReaderMLLPSingle(t *testing.T) { if err != nil { t.Fatalf("ReadMessage() error: %v", err) } - if got := msg.Get("MSH-9.1"); got != "ADT" { + if got := msg.Get("MSH-9.1").String(); got != "ADT" { t.Errorf("MSH-9.1 = %q, want ADT", got) } - if got := msg.Get("PID-3.1"); got != "123" { + if got := msg.Get("PID-3.1").String(); got != "123" { t.Errorf("PID-3.1 = %q, want 123", got) } @@ -69,7 +69,7 @@ func TestReaderMLLPMultiple(t *testing.T) { if err != nil { t.Fatalf("ReadMessage() #1 error: %v", err) } - if got := m1.Get("MSH-10"); got != "1" { + if got := m1.Get("MSH-10").String(); got != "1" { t.Errorf("msg1 MSH-10 = %q, want 1", got) } @@ -77,7 +77,7 @@ func TestReaderMLLPMultiple(t *testing.T) { if err != nil { t.Fatalf("ReadMessage() #2 error: %v", err) } - if got := m2.Get("MSH-10"); got != "2" { + if got := m2.Get("MSH-10").String(); got != "2" { t.Errorf("msg2 MSH-10 = %q, want 2", got) } @@ -95,7 +95,7 @@ func TestReaderRawMode(t *testing.T) { if err != nil { t.Fatalf("ReadMessage() error: %v", err) } - if got := msg.Get("MSH-10"); got != "1" { + if got := msg.Get("MSH-10").String(); got != "1" { t.Errorf("MSH-10 = %q, want 1", got) } } @@ -109,7 +109,7 @@ func TestReaderAutoMLLP(t *testing.T) { if err != nil { t.Fatalf("ReadMessage() error: %v", err) } - if got := msg.Get("MSH-9.1"); got != "ADT" { + if got := msg.Get("MSH-9.1").String(); got != "ADT" { t.Errorf("MSH-9.1 = %q, want ADT", got) } } @@ -122,7 +122,7 @@ func TestReaderAutoRaw(t *testing.T) { if err != nil { t.Fatalf("ReadMessage() error: %v", err) } - if got := msg.Get("MSH-9.1"); got != "ADT" { + if got := msg.Get("MSH-9.1").String(); got != "ADT" { t.Errorf("MSH-9.1 = %q, want ADT", got) } } @@ -139,7 +139,7 @@ func TestReaderEachMessage(t *testing.T) { var ids []string err := reader.EachMessage(func(msg *Message) error { - ids = append(ids, msg.Get("MSH-10")) + ids = append(ids, msg.Get("MSH-10").String()) return nil }) if err != nil { @@ -204,7 +204,7 @@ func TestReaderWithBufferSize(t *testing.T) { if err != nil { t.Fatalf("ReadMessage() error: %v", err) } - if got := msg.Get("MSH-10"); got != "1" { + if got := msg.Get("MSH-10").String(); got != "1" { t.Errorf("MSH-10 = %q, want 1", got) } } @@ -226,7 +226,7 @@ func TestReaderRawMultiMessage(t *testing.T) { if err != nil { t.Fatalf("ReadMessage #1: %v", err) } - if got := m1.Get("MSH-10"); got != "1" { + if got := m1.Get("MSH-10").String(); got != "1" { t.Errorf("msg1 MSH-10 = %q, want 1", got) } @@ -234,7 +234,7 @@ func TestReaderRawMultiMessage(t *testing.T) { if err != nil { t.Fatalf("ReadMessage #2: %v", err) } - if got := m2.Get("MSH-10"); got != "2" { + if got := m2.Get("MSH-10").String(); got != "2" { t.Errorf("msg2 MSH-10 = %q, want 2", got) } @@ -252,7 +252,7 @@ func TestReaderRawLeadingWhitespace(t *testing.T) { if err != nil { t.Fatalf("ReadMessage: %v", err) } - if got := msg.Get("MSH-10"); got != "1" { + if got := msg.Get("MSH-10").String(); got != "1" { t.Errorf("MSH-10 = %q, want 1", got) } } @@ -275,7 +275,7 @@ func TestReaderAutoWhitespaceBeforeMLLP(t *testing.T) { if err != nil { t.Fatalf("ReadMessage: %v", err) } - if got := msg.Get("MSH-10"); got != "1" { + if got := msg.Get("MSH-10").String(); got != "1" { t.Errorf("MSH-10 = %q, want 1", got) } } @@ -298,7 +298,7 @@ func TestReaderMLLPWhitespaceBeforeStart(t *testing.T) { if err != nil { t.Fatalf("ReadMessage: %v", err) } - if got := msg.Get("MSH-10"); got != "1" { + if got := msg.Get("MSH-10").String(); got != "1" { t.Errorf("MSH-10 = %q, want 1", got) } } @@ -315,7 +315,7 @@ func TestReaderMLLPMissingTrailingCR(t *testing.T) { if err != nil { t.Fatalf("ReadMessage: %v", err) } - if got := msg.Get("MSH-10"); got != "1" { + if got := msg.Get("MSH-10").String(); got != "1" { t.Errorf("MSH-10 = %q, want 1", got) } } @@ -378,7 +378,7 @@ func TestReaderRawThreeMessages(t *testing.T) { if err != nil { t.Fatalf("ReadMessage #1: %v", err) } - if got := m1.Get("MSH-10"); got != "1" { + if got := m1.Get("MSH-10").String(); got != "1" { t.Errorf("msg1 MSH-10 = %q, want 1", got) } @@ -386,7 +386,7 @@ func TestReaderRawThreeMessages(t *testing.T) { if err != nil { t.Fatalf("ReadMessage #2: %v", err) } - if got := m2.Get("MSH-10"); got != "2" { + if got := m2.Get("MSH-10").String(); got != "2" { t.Errorf("msg2 MSH-10 = %q, want 2", got) } @@ -394,7 +394,7 @@ func TestReaderRawThreeMessages(t *testing.T) { if err != nil { t.Fatalf("ReadMessage #3: %v", err) } - if got := m3.Get("MSH-10"); got != "3" { + if got := m3.Get("MSH-10").String(); got != "3" { t.Errorf("msg3 MSH-10 = %q, want 3", got) } @@ -416,7 +416,7 @@ func TestReaderAutoRawMultipleMessages(t *testing.T) { if err != nil { t.Fatalf("ReadMessage #1: %v", err) } - if got := m1.Get("MSH-10"); got != "1" { + if got := m1.Get("MSH-10").String(); got != "1" { t.Errorf("msg1 MSH-10 = %q, want 1", got) } @@ -424,7 +424,7 @@ func TestReaderAutoRawMultipleMessages(t *testing.T) { if err != nil { t.Fatalf("ReadMessage #2: %v", err) } - if got := m2.Get("MSH-10"); got != "2" { + if got := m2.Get("MSH-10").String(); got != "2" { t.Errorf("msg2 MSH-10 = %q, want 2", got) } @@ -432,7 +432,7 @@ func TestReaderAutoRawMultipleMessages(t *testing.T) { if err != nil { t.Fatalf("ReadMessage #3: %v", err) } - if got := m3.Get("MSH-10"); got != "3" { + if got := m3.Get("MSH-10").String(); got != "3" { t.Errorf("msg3 MSH-10 = %q, want 3", got) } diff --git a/segment.go b/segment.go index 31b00af..4de495c 100644 --- a/segment.go +++ b/segment.go @@ -44,12 +44,12 @@ func (s *Segment) Type() string { // Returns an empty Field if the index is out of range. func (s *Segment) Field(index int) Field { if index < 0 || len(s.raw) < 3 { - return Field{delims: s.delims} + return Field{Value: Value{delims: s.delims}} } // Field 0: segment type. if index == 0 { - return Field{raw: s.raw[0:3], delims: s.delims} + return Field{Value: Value{raw: s.raw[0:3], delims: s.delims}} } if s.raw[0] == 'M' && s.raw[1] == 'S' && s.raw[2] == 'H' { @@ -65,14 +65,14 @@ func (s *Segment) mshField(index int) Field { // Field 1: field separator character. if index == 1 { if len(raw) < 4 { - return Field{delims: s.delims} + return Field{Value: Value{delims: s.delims}} } - return Field{raw: raw[3:4], delims: s.delims} + return Field{Value: Value{raw: raw[3:4], delims: s.delims}} } // Find end of MSH-2 (encoding characters — everything until next field separator). if len(raw) < 5 { - return Field{delims: s.delims} + return Field{Value: Value{delims: s.delims}} } raw = raw[4:] encEnd := bytes.IndexByte(raw, s.delims.Field) @@ -82,32 +82,32 @@ func (s *Segment) mshField(index int) Field { // Field 2: encoding characters. if index == 2 { - return Field{raw: raw[:encEnd], delims: s.delims} + return Field{Value: Value{raw: raw[:encEnd], delims: s.delims}} } // Field 3+: normal fields after encoding chars. if encEnd >= len(raw) { - return Field{delims: s.delims} + return Field{Value: Value{delims: s.delims}} } remaining := raw[encEnd+1:] slice := nthSlice(remaining, s.delims.Field, index-3) if slice == nil { - return Field{delims: s.delims} + return Field{Value: Value{delims: s.delims}} } - return Field{raw: slice, delims: s.delims} + return Field{Value: Value{raw: slice, delims: s.delims}} } // normalField handles all non-MSH segments. func (s *Segment) normalField(index int) Field { raw := s.raw if len(raw) < 4 || raw[3] != s.delims.Field { - return Field{delims: s.delims} + return Field{Value: Value{delims: s.delims}} } slice := nthSlice(raw[4:], s.delims.Field, index-1) if slice == nil { - return Field{delims: s.delims} + return Field{Value: Value{delims: s.delims}} } - return Field{raw: slice, delims: s.delims} + return Field{Value: Value{raw: slice, delims: s.delims}} } // FieldCount returns the number of fields, including the segment type at index 0. diff --git a/transform_test.go b/transform_test.go index 0245d7f..e6acbdc 100644 --- a/transform_test.go +++ b/transform_test.go @@ -38,11 +38,11 @@ func TestTransformReplace(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "Smith" { + if got := result.Get("PID-5.1").String(); got != "Smith" { t.Errorf("PID-5.1 = %q, want %q", got, "Smith") } // Other components should be preserved. - if got := result.Get("PID-5.2"); got != "John" { + if got := result.Get("PID-5.2").String(); got != "John" { t.Errorf("PID-5.2 = %q, want %q", got, "John") } } @@ -106,19 +106,19 @@ func TestTransformMove(t *testing.T) { } // Destination should have source value. - if got := result.Get("PID-5.1"); got != "123" { + if got := result.Get("PID-5.1").String(); got != "123" { t.Errorf("PID-5.1 (dst) = %q, want %q", got, "123") } // Source should be cleared. - if got := result.Get("PID-3.1"); got != "" { + if got := result.Get("PID-3.1").String(); got != "" { t.Errorf("PID-3.1 (src) = %q, want empty", got) } // Other components at source should be preserved. - if got := result.Get("PID-3.2"); got != "MRN" { + if got := result.Get("PID-3.2").String(); got != "MRN" { t.Errorf("PID-3.2 = %q, want %q", got, "MRN") } // Other components at destination should be preserved. - if got := result.Get("PID-5.2"); got != "John" { + if got := result.Get("PID-5.2").String(); got != "John" { t.Errorf("PID-5.2 = %q, want %q", got, "John") } } @@ -135,10 +135,10 @@ func TestTransformMultipleChanges(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "Smith" { + if got := result.Get("PID-5.1").String(); got != "Smith" { t.Errorf("PID-5.1 = %q, want %q", got, "Smith") } - if got := result.Get("PID-5.2"); got != "Jane" { + if got := result.Get("PID-5.2").String(); got != "Jane" { t.Errorf("PID-5.2 = %q, want %q", got, "Jane") } @@ -165,7 +165,7 @@ func TestTransformFieldLevelReplace(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "NewValue" { + if got := result.Get("PID-5.1").String(); got != "NewValue" { t.Errorf("PID-5.1 = %q, want %q", got, "NewValue") } } @@ -182,7 +182,7 @@ func TestTransformFieldLevelSupersedes(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "FullReplace" { + if got := result.Get("PID-5.1").String(); got != "FullReplace" { t.Errorf("PID-5.1 = %q, want %q", got, "FullReplace") } } @@ -201,7 +201,7 @@ func TestTransformOrderFieldThenComponent(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "Override" { + if got := result.Get("PID-5.1").String(); got != "Override" { t.Errorf("PID-5.1 = %q, want %q", got, "Override") } } @@ -219,10 +219,10 @@ func TestTransformOrderFieldThenTwoComponents(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "Smith" { + if got := result.Get("PID-5.1").String(); got != "Smith" { t.Errorf("PID-5.1 = %q, want %q", got, "Smith") } - if got := result.Get("PID-5.2"); got != "Jane" { + if got := result.Get("PID-5.2").String(); got != "Jane" { t.Errorf("PID-5.2 = %q, want %q", got, "Jane") } } @@ -239,10 +239,10 @@ func TestTransformOrderOmitThenComponent(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "" { + if got := result.Get("PID-5.1").String(); got != "" { t.Errorf("PID-5.1 = %q, want empty", got) } - if got := result.Get("PID-5.2"); got != "Jane" { + if got := result.Get("PID-5.2").String(); got != "Jane" { t.Errorf("PID-5.2 = %q, want %q", got, "Jane") } } @@ -259,10 +259,10 @@ func TestTransformOrderComponentThenSubcomponent(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-3.1.1"); got != "NewBase" { + if got := result.Get("PID-3.1.1").String(); got != "NewBase" { t.Errorf("PID-3.1.1 = %q, want %q", got, "NewBase") } - if got := result.Get("PID-3.1.2"); got != "SubOverride" { + if got := result.Get("PID-3.1.2").String(); got != "SubOverride" { t.Errorf("PID-3.1.2 = %q, want %q", got, "SubOverride") } } @@ -280,11 +280,11 @@ func TestTransformOrderReplaceThenMove(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-3.1"); got != "Smith" { + if got := result.Get("PID-3.1").String(); got != "Smith" { t.Errorf("PID-3.1 = %q, want %q", got, "Smith") } // Source should be cleared by the move. - if got := result.Get("PID-5.1"); got != "" { + if got := result.Get("PID-5.1").String(); got != "" { t.Errorf("PID-5.1 = %q, want empty", got) } } @@ -311,13 +311,13 @@ func TestTransformDelimiterConversion(t *testing.T) { t.Errorf("delimiters = %+v, want %+v", result.Delimiters(), newDelims) } - if got := result.Get("PID-5.1"); got != "Smith" { + if got := result.Get("PID-5.1").String(); got != "Smith" { t.Errorf("PID-5.1 = %q, want %q", got, "Smith") } - if got := result.Get("PID-5.2"); got != "John" { + if got := result.Get("PID-5.2").String(); got != "John" { t.Errorf("PID-5.2 = %q, want %q", got, "John") } - if got := result.Get("MSH-9.1"); got != "ADT" { + if got := result.Get("MSH-9.1").String(); got != "ADT" { t.Errorf("MSH-9.1 = %q, want %q", got, "ADT") } } @@ -341,10 +341,10 @@ func TestTransformDelimiterConversionNoChanges(t *testing.T) { if result.Delimiters() != newDelims { t.Errorf("delimiters = %+v, want %+v", result.Delimiters(), newDelims) } - if got := result.Get("PID-5.1"); got != "Doe" { + if got := result.Get("PID-5.1").String(); got != "Doe" { t.Errorf("PID-5.1 = %q, want %q", got, "Doe") } - if got := result.Get("MSH-9.1"); got != "ADT" { + if got := result.Get("MSH-9.1").String(); got != "ADT" { t.Errorf("MSH-9.1 = %q, want %q", got, "ADT") } } @@ -357,14 +357,14 @@ func TestTransformMSHReconstruction(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("MSH-10"); got != "NEWID" { + if got := result.Get("MSH-10").String(); got != "NEWID" { t.Errorf("MSH-10 = %q, want %q", got, "NEWID") } // Other MSH fields should be preserved. - if got := result.Get("MSH-3"); got != "S" { + if got := result.Get("MSH-3").String(); got != "S" { t.Errorf("MSH-3 = %q, want %q", got, "S") } - if got := result.Get("MSH-9.1"); got != "ADT" { + if got := result.Get("MSH-9.1").String(); got != "ADT" { t.Errorf("MSH-9.1 = %q, want %q", got, "ADT") } } @@ -378,11 +378,11 @@ func TestTransformExtendField(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "Doe" { + if got := result.Get("PID-5.1").String(); got != "Doe" { t.Errorf("PID-5.1 = %q, want %q", got, "Doe") } // Earlier fields should be preserved. - if got := result.Get("PID-3"); got != "123" { + if got := result.Get("PID-3").String(); got != "123" { t.Errorf("PID-3 = %q, want %q", got, "123") } } @@ -395,7 +395,7 @@ func TestTransformExtendNewSegment(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-3"); got != "12345" { + if got := result.Get("PID-3").String(); got != "12345" { t.Errorf("PID-3 = %q, want %q", got, "12345") } } @@ -410,7 +410,7 @@ func TestTransformMoveEmptySource(t *testing.T) { } // Destination gets the empty value. - if got := result.Get("PID-5.1"); got != "" { + if got := result.Get("PID-5.1").String(); got != "" { t.Errorf("PID-5.1 = %q, want empty", got) } } @@ -447,7 +447,7 @@ func TestTransformNoChanges(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "Doe" { + if got := result.Get("PID-5.1").String(); got != "Doe" { t.Errorf("PID-5.1 = %q, want %q", got, "Doe") } } @@ -461,7 +461,7 @@ func TestTransformOriginalUnchanged(t *testing.T) { } // Original should not be modified. - if got := msg.Get("PID-5.1"); got != "Doe" { + if got := msg.Get("PID-5.1").String(); got != "Doe" { t.Errorf("original PID-5.1 = %q, want %q", got, "Doe") } } @@ -476,10 +476,10 @@ func TestTransformEscapeSequencesInSource(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "Doe&Smith" { + if got := result.Get("PID-5.1").String(); got != "Doe&Smith" { t.Errorf("PID-5.1 = %q, want %q", got, "Doe&Smith") } - if got := result.Get("PID-5.2"); got != "Jane" { + if got := result.Get("PID-5.2").String(); got != "Jane" { t.Errorf("PID-5.2 = %q, want %q", got, "Jane") } } @@ -492,13 +492,13 @@ func TestTransformSubcomponentLevel(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-3.1.1"); got != "123" { + if got := result.Get("PID-3.1.1").String(); got != "123" { t.Errorf("PID-3.1.1 = %q, want %q", got, "123") } - if got := result.Get("PID-3.1.2"); got != "NEWMRN" { + if got := result.Get("PID-3.1.2").String(); got != "NEWMRN" { t.Errorf("PID-3.1.2 = %q, want %q", got, "NEWMRN") } - if got := result.Get("PID-3.1.3"); got != "AUTH" { + if got := result.Get("PID-3.1.3").String(); got != "AUTH" { t.Errorf("PID-3.1.3 = %q, want %q", got, "AUTH") } } @@ -524,7 +524,7 @@ func TestTransformReplaceWithDelimiterChars(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-5.1"); got != "Doe|Smith" { + if got := result.Get("PID-5.1").String(); got != "Doe|Smith" { t.Errorf("PID-5.1 = %q, want %q", got, "Doe|Smith") } } @@ -548,13 +548,13 @@ func TestTransformMoveWithDelimiterConversion(t *testing.T) { t.Fatalf("TransformWith failed: %v", err) } - if got := result.Get("PID-5.1"); got != "123" { + if got := result.Get("PID-5.1").String(); got != "123" { t.Errorf("PID-5.1 = %q, want %q", got, "123") } - if got := result.Get("PID-3"); got != "" { + if got := result.Get("PID-3").String(); got != "" { t.Errorf("PID-3 = %q, want empty", got) } - if got := result.Get("MSH-10"); got != "NEW_CONTROL_ID" { + if got := result.Get("MSH-10").String(); got != "NEW_CONTROL_ID" { t.Errorf("MSH-10 = %q, want %q", got, "NEW_CONTROL_ID") } } @@ -571,7 +571,7 @@ func TestTransformLastWriteWins(t *testing.T) { } // Last write wins — component-level, last change at same location. - if got := result.Get("PID-5.1"); got != "Second" { + if got := result.Get("PID-5.1").String(); got != "Second" { t.Errorf("PID-5.1 = %q, want %q", got, "Second") } } @@ -584,10 +584,10 @@ func TestTransformSecondSegmentOccurrence(t *testing.T) { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("OBX(0)-5"); got != "val1" { + if got := result.Get("OBX(0)-5").String(); got != "val1" { t.Errorf("OBX(0)-5 = %q, want %q", got, "val1") } - if got := result.Get("OBX(1)-5"); got != "newval" { + if got := result.Get("OBX(1)-5").String(); got != "newval" { t.Errorf("OBX(1)-5 = %q, want %q", got, "newval") } } @@ -601,11 +601,11 @@ func TestTransformMoveSourceClearsSource(t *testing.T) { } // Destination has the moved value. - if got := result.Get("PID-3"); got != "Doe^John" { + if got := result.Get("PID-3").String(); got != "Doe^John" { t.Errorf("PID-3 = %q, want %q", got, "Doe^John") } // Source should be cleared. - if got := result.Get("PID-5"); got != "" { + if got := result.Get("PID-5").String(); got != "" { t.Errorf("PID-5 = %q, want empty", got) } } @@ -634,7 +634,7 @@ func TestTransformDelimiterConversionResolvesEscapes(t *testing.T) { // The escaped source delimiters should appear as literal characters in the // output because |, ^, and & are not delimiters in the destination set. - if got := result.Get("OBX-5"); got != "value|with^delims&here" { + if got := result.Get("OBX-5").String(); got != "value|with^delims&here" { t.Errorf("OBX-5 = %q, want %q", got, "value|with^delims&here") } } @@ -660,12 +660,12 @@ func TestTransformDelimiterConversionReEscapes(t *testing.T) { } // The literal "|" collides with dst.Component, so it must be re-escaped. - if got := result.Get("OBX-5"); got != "before|after" { + if got := result.Get("OBX-5").String(); got != "before|after" { t.Errorf("OBX-5 = %q, want %q", got, "before|after") } // Verify the raw bytes contain the escape sequence, not a bare "|". - if raw := string(result.GetBytes("OBX-5")); raw != "before$S$after" { + if raw := string(result.Get("OBX-5").Bytes()); raw != "before$S$after" { t.Errorf("OBX-5 raw = %q, want %q", raw, "before$S$after") } } @@ -690,11 +690,11 @@ func TestTransformDelimiterConversionEscapesNewCollisions(t *testing.T) { t.Fatalf("TransformWith failed: %v", err) } - if got := result.Get("PID-5.1"); got != "Do#e" { + if got := result.Get("PID-5.1").String(); got != "Do#e" { t.Errorf("PID-5.1 = %q, want %q", got, "Do#e") } // Raw bytes should contain the escape sequence, not a bare "#". - if raw := string(result.GetBytes("PID-5.1")); raw != "Do$F$e" { + if raw := string(result.Get("PID-5.1").Bytes()); raw != "Do$F$e" { t.Errorf("PID-5.1 raw = %q, want %q", raw, "Do$F$e") } } @@ -880,7 +880,7 @@ func TestTransformMoveNonExistentSource(t *testing.T) { t.Fatalf("Transform: %v", err) } // Destination gets nil/empty since source doesn't exist. - if got := result.Get("PID-5.1"); got != "" { + if got := result.Get("PID-5.1").String(); got != "" { t.Errorf("PID-5.1 = %q, want empty", got) } } @@ -892,10 +892,10 @@ func TestTransformRepetitionLevel(t *testing.T) { if err != nil { t.Fatalf("Transform: %v", err) } - if got := result.Get("PID-3[0].1"); got != "123" { + if got := result.Get("PID-3[0].1").String(); got != "123" { t.Errorf("PID-3[0].1 = %q, want 123", got) } - if got := result.Get("PID-3[1].1"); got != "789" { + if got := result.Get("PID-3[1].1").String(); got != "789" { t.Errorf("PID-3[1].1 = %q, want 789", got) } } @@ -945,19 +945,19 @@ func TestTransformCopy(t *testing.T) { } // Destination should have source value. - if got := result.Get("PID-5.1"); got != "123" { + if got := result.Get("PID-5.1").String(); got != "123" { t.Errorf("PID-5.1 (dst) = %q, want %q", got, "123") } // Source should be preserved (unlike Move). - if got := result.Get("PID-3.1"); got != "123" { + if got := result.Get("PID-3.1").String(); got != "123" { t.Errorf("PID-3.1 (src) = %q, want %q", got, "123") } // Other components at source should be preserved. - if got := result.Get("PID-3.2"); got != "MRN" { + if got := result.Get("PID-3.2").String(); got != "MRN" { t.Errorf("PID-3.2 = %q, want %q", got, "MRN") } // Other components at destination should be preserved. - if got := result.Get("PID-5.2"); got != "John" { + if got := result.Get("PID-5.2").String(); got != "John" { t.Errorf("PID-5.2 = %q, want %q", got, "John") } } @@ -972,7 +972,7 @@ func TestTransformCopyEmptySource(t *testing.T) { } // Destination gets the empty value. - if got := result.Get("PID-5.1"); got != "" { + if got := result.Get("PID-5.1").String(); got != "" { t.Errorf("PID-5.1 = %q, want empty", got) } } @@ -986,7 +986,7 @@ func TestTransformCopyNonExistentSource(t *testing.T) { t.Fatalf("Transform: %v", err) } // Destination gets nil/empty since source doesn't exist. - if got := result.Get("PID-5.1"); got != "" { + if got := result.Get("PID-5.1").String(); got != "" { t.Errorf("PID-5.1 = %q, want empty", got) } } @@ -1000,11 +1000,11 @@ func TestTransformCopyFieldLevel(t *testing.T) { } // Destination has the copied value. - if got := result.Get("PID-3"); got != "Doe^John" { + if got := result.Get("PID-3").String(); got != "Doe^John" { t.Errorf("PID-3 = %q, want %q", got, "Doe^John") } // Source should be preserved. - if got := result.Get("PID-5"); got != "Doe^John" { + if got := result.Get("PID-5").String(); got != "Doe^John" { t.Errorf("PID-5 = %q, want %q", got, "Doe^John") } } @@ -1123,7 +1123,7 @@ func TestDeleteSegment(t *testing.T) { } } // Remaining NTE should be the second one. - if got := result.Get("NTE-3"); got != "Second" { + if got := result.Get("NTE-3").String(); got != "Second" { t.Errorf("NTE-3 = %q, want %q", got, "Second") } }) @@ -1145,7 +1145,7 @@ func TestDeleteSegment(t *testing.T) { } } // Remaining NTE should be the first one. - if got := result.Get("NTE-3"); got != "First" { + if got := result.Get("NTE-3").String(); got != "First" { t.Errorf("NTE-3 = %q, want %q", got, "First") } }) @@ -1207,10 +1207,10 @@ func TestDeleteAllSegments(t *testing.T) { if err != nil { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("PID-3"); got != "123" { + if got := result.Get("PID-3").String(); got != "123" { t.Errorf("PID-3 = %q, want %q", got, "123") } - if got := result.Get("OBX-5"); got != "42" { + if got := result.Get("OBX-5").String(); got != "42" { t.Errorf("OBX-5 = %q, want %q", got, "42") } }) @@ -1270,7 +1270,7 @@ func TestInsertSegmentAfter(t *testing.T) { if err != nil { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("ZPD-1"); got != "42" { + if got := result.Get("ZPD-1").String(); got != "42" { t.Errorf("ZPD-1 = %q, want %q", got, "42") } }) @@ -1395,7 +1395,7 @@ func TestAppendSegment(t *testing.T) { if err != nil { t.Fatalf("Transform failed: %v", err) } - if got := result.Get("ZPD-3"); got != "CustomValue" { + if got := result.Get("ZPD-3").String(); got != "CustomValue" { t.Errorf("ZPD-3 = %q, want %q", got, "CustomValue") } got := segTypes(result) @@ -1449,10 +1449,10 @@ func TestSegmentOpsWithFieldChanges(t *testing.T) { } } - if v := result.Get("ZPD-1"); v != "InsertedValue" { + if v := result.Get("ZPD-1").String(); v != "InsertedValue" { t.Errorf("ZPD-1 = %q, want %q", v, "InsertedValue") } - if v := result.Get("PID-3"); v != "456" { + if v := result.Get("PID-3").String(); v != "456" { t.Errorf("PID-3 = %q, want %q", v, "456") } } diff --git a/validate.go b/validate.go index 8d0d66e..7300d3c 100644 --- a/validate.go +++ b/validate.go @@ -152,10 +152,10 @@ func (v *validator) lookupMessageDef() *MessageDef { if v.schema.Messages == nil { return nil } - structID := v.msg.Get("MSH-9.3") + structID := v.msg.Get("MSH-9.3").String() if structID == "" { - code := v.msg.Get("MSH-9.1") - event := v.msg.Get("MSH-9.2") + code := v.msg.Get("MSH-9.1").String() + event := v.msg.Get("MSH-9.2").String() if code != "" && event != "" { structID = code + "_" + event } diff --git a/writer_test.go b/writer_test.go index 78072ca..d438193 100644 --- a/writer_test.go +++ b/writer_test.go @@ -103,7 +103,7 @@ func TestWriterMultipleMessages(t *testing.T) { if err != nil { t.Fatalf("ReadMessage #1: %v", err) } - if got := m1.Get("MSH-10"); got != "1" { + if got := m1.Get("MSH-10").String(); got != "1" { t.Errorf("msg1 MSH-10 = %q, want 1", got) } @@ -111,7 +111,7 @@ func TestWriterMultipleMessages(t *testing.T) { if err != nil { t.Fatalf("ReadMessage #2: %v", err) } - if got := m2.Get("MSH-10"); got != "2" { + if got := m2.Get("MSH-10").String(); got != "2" { t.Errorf("msg2 MSH-10 = %q, want 2", got) } } @@ -132,13 +132,13 @@ func TestWriterRoundTrip(t *testing.T) { t.Fatalf("ReadMessage: %v", err) } - if got := msg.Get("MSH-10"); got != "CTL123" { + if got := msg.Get("MSH-10").String(); got != "CTL123" { t.Errorf("MSH-10 = %q, want CTL123", got) } - if got := msg.Get("PID-5.1"); got != "Smith" { + if got := msg.Get("PID-5.1").String(); got != "Smith" { t.Errorf("PID-5.1 = %q, want Smith", got) } - if got := msg.Get("OBX-5"); got != "42" { + if got := msg.Get("OBX-5").String(); got != "42" { t.Errorf("OBX-5 = %q, want 42", got) } }