diff --git a/charset.go b/charset.go index 2615949..5f49f7a 100644 --- a/charset.go +++ b/charset.go @@ -14,20 +14,24 @@ 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. +// ValueMapper converts field-content bytes to a new representation. +// It is used at read time (DecodeString) to decode stored bytes, and at +// write time (MapAllValues) to transform content before storage. +// +// The mapper receives post-unescape bytes and must return the transformed bytes. +// When used with DecodeString, returning an error aborts the decode. +// When used with MapAllValues, returning an error aborts the transform. // // Callers read MSH-18 to determine the source charset, then select an -// appropriate ValueDecoder before calling DecodeString. When decode is nil, +// appropriate ValueMapper 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 +// Note: Unescape runs before the ValueMapper, so the mapper 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) +// through verbatim by Unescape; a sophisticated ValueMapper may parse them, +// but a simple byte-level mapper will treat them as-is. +type ValueMapper 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 @@ -35,7 +39,7 @@ type ValueDecoder func(data []byte) ([]byte, error) // // DecodeString is promoted to Field, Repetition, Component, and Subcomponent // via their embedded Value. -func (v Value) DecodeString(decode ValueDecoder) (string, error) { +func (v Value) DecodeString(decode ValueMapper) (string, error) { unescaped := Unescape(v.raw, v.delims) if decode == nil { return string(unescaped), nil diff --git a/charset_test.go b/charset_test.go index 52cb4db..a87e2f0 100644 --- a/charset_test.go +++ b/charset_test.go @@ -19,7 +19,7 @@ import ( "testing" ) -// latin1ToUTF8 is a test-only ValueDecoder that converts Latin-1 (ISO 8859-1) +// latin1ToUTF8 is a test-only ValueMapper 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) { @@ -55,7 +55,7 @@ func TestValueDecodeString(t *testing.T) { tests := []struct { name string raw []byte - decode ValueDecoder + decode ValueMapper want string wantErr bool }{ @@ -142,7 +142,7 @@ func TestFieldDecodeString(t *testing.T) { tests := []struct { name string raw []byte - decode ValueDecoder + decode ValueMapper want string wantErr bool }{ @@ -205,7 +205,7 @@ func TestRepetitionDecodeString(t *testing.T) { tests := []struct { name string raw []byte - decode ValueDecoder + decode ValueMapper want string wantErr bool }{ @@ -262,7 +262,7 @@ func TestComponentDecodeString(t *testing.T) { tests := []struct { name string raw []byte - decode ValueDecoder + decode ValueMapper want string wantErr bool }{ @@ -319,7 +319,7 @@ func TestSubcomponentDecodeString(t *testing.T) { tests := []struct { name string raw []byte - decode ValueDecoder + decode ValueMapper want string wantErr bool }{ @@ -370,7 +370,7 @@ func TestSubcomponentDecodeString(t *testing.T) { } // TestDecodeStringUnescapeFirst verifies that Unescape runs before the -// ValueDecoder — a field containing \F\ (the escape sequence for the field +// ValueMapper — 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() diff --git a/doc.go b/doc.go index a11eac1..0dbd3e1 100644 --- a/doc.go +++ b/doc.go @@ -55,7 +55,7 @@ // # 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 +// MSH-18) can be decoded with DecodeString. A ValueMapper is a // func([]byte) ([]byte, error) that converts post-unescape bytes to UTF-8. // DecodeString is available on Value, Field, Repetition, Component, and // Subcomponent: @@ -69,9 +69,9 @@ // 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 +// extra allocation. Unescape always runs before the mapper. 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 +// sophisticated ValueMapper may parse them, but a simple byte-level mapper // will treat them as-is. // // # Hierarchical Traversal @@ -127,11 +127,16 @@ // 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, -// and Copy copies src to dst while preserving the source. +// 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: // diff --git a/transform.go b/transform.go index 2e41bd9..513f2d4 100644 --- a/transform.go +++ b/transform.go @@ -93,6 +93,17 @@ type copyChange struct { func (copyChange) applyChange() {} +type mapAllValuesChange struct{ mapper ValueMapper } + +func (mapAllValuesChange) applyChange() {} + +type mapValueChange struct { + location string + mapper ValueMapper +} + +func (mapValueChange) applyChange() {} + // Replace returns a Change that sets the value at location to the given string. // The value is plain text and will be escaped for the output delimiter set. func Replace(location, value string) Change { return replaceChange{location, value} } @@ -144,6 +155,23 @@ func InsertSegmentBefore(beforeType string, beforeIndex int, newType string) Cha // at the end of the message. This change never returns an error. func AppendSegment(newType string) Change { return appendSegmentChange{newType} } +// MapAllValues returns a Change that applies mapper to every leaf value in the +// message, including empty and null ("") leaves. MSH-1 and MSH-2 +// (delimiter definition fields) are never modified. +// +// The mapper receives unescaped field-content bytes; its output is escaped +// before storage. If the mapper returns an error for any value, Transform +// returns that error and the message is not modified. +func MapAllValues(mapper ValueMapper) Change { return mapAllValuesChange{mapper} } + +// MapValue returns a Change that applies mapper to the value at the given +// terser-style location. The mapper receives unescaped bytes at that location +// (nil if the location is absent); its output is escaped before storage. +// If the mapper returns an error, Transform returns that error. +func MapValue(location string, mapper ValueMapper) Change { + return mapValueChange{location, mapper} +} + // Transform applies the given changes to the message and returns a new Message. // The original message is not modified. The output uses the same delimiters as the source. func (m *Message) Transform(changes ...Change) (*Message, error) { @@ -681,6 +709,21 @@ func applyOneChange(w *workBuf, delims Delimiters, c Change) error { case appendSegmentChange: w.insertBefore(len(w.segs), []byte(ch.newType)) + + case mapAllValuesChange: + return applyMapAllValues(w, delims, ch.mapper) + + case mapValueChange: + loc, err := validateLocation(ch.location) + if err != nil { + return err + } + raw := readValueAtLocation(w, delims, loc) + newRaw, err := mapLeafValue(raw, delims, ch.mapper) + if err != nil { + return err + } + applyValueAtLocation(w, delims, loc, newRaw) } return nil } @@ -905,6 +948,132 @@ func reencodeData(data []byte, src, dst Delimiters) []byte { return out } +// applyMapAllValues applies mapper to every leaf (subcomponent-level) value in +// the message. MSH-1 and MSH-2 are never touched. The mapper receives +// unescaped bytes and its result is re-escaped before storage. +func applyMapAllValues(w *workBuf, delims Delimiters, mapper ValueMapper) error { + for segIdx := range w.segs { + segRaw := w.segBytes(segIdx) + isMSH := len(segRaw) >= 3 && segRaw[0] == 'M' && segRaw[1] == 'S' && segRaw[2] == 'H' + + // MSH fields start at 3 (skip MSH-1 and MSH-2). + firstField := 1 + if isMSH { + firstField = 3 + } + + for fieldNum := firstField; ; fieldNum++ { + // Re-read segment bytes on each iteration because replaceField may + // reallocate w.data and shift segment offsets. + segRaw = w.segBytes(segIdx) + raw := readFieldBytes(segRaw, delims, fieldNum) + if raw == nil { + break + } + + newRaw, err := mapFieldValues(raw, delims, mapper) + if err != nil { + return err + } + if !bytes.Equal(raw, newRaw) { + w.replaceField(segIdx, delims, fieldNum, newRaw) + } + } + } + return nil +} + +// mapFieldValues applies mapper to every leaf within a field's raw bytes. +// The field may contain repetitions, each of which may contain components, etc. +// Returns raw unchanged (no allocation) if the mapper is a no-op for all leaves. +func mapFieldValues(raw []byte, delims Delimiters, mapper ValueMapper) ([]byte, error) { + n := countDelimited(raw, delims.Repetition) + if n == 1 { + return mapRepValues(raw, delims, mapper) + } + return mapDelimited(raw, delims.Repetition, func(piece []byte) ([]byte, error) { + return mapRepValues(piece, delims, mapper) + }) +} + +// mapRepValues applies mapper to every leaf within a single repetition's bytes. +// Returns raw unchanged (no allocation) if the mapper is a no-op for all leaves. +func mapRepValues(raw []byte, delims Delimiters, mapper ValueMapper) ([]byte, error) { + n := countDelimited(raw, delims.Component) + if n == 1 { + return mapComponentValues(raw, delims, mapper) + } + return mapDelimited(raw, delims.Component, func(piece []byte) ([]byte, error) { + return mapComponentValues(piece, delims, mapper) + }) +} + +// mapComponentValues applies mapper to every leaf within a single component's bytes. +// Returns raw unchanged (no allocation) if the mapper is a no-op for all leaves. +func mapComponentValues(raw []byte, delims Delimiters, mapper ValueMapper) ([]byte, error) { + n := countDelimited(raw, delims.SubComponent) + if n == 1 { + return mapLeafValue(raw, delims, mapper) + } + return mapDelimited(raw, delims.SubComponent, func(piece []byte) ([]byte, error) { + return mapLeafValue(piece, delims, mapper) + }) +} + +// mapDelimited applies fn to each delimited piece of raw. +// Uses lazy allocation: returns raw unchanged if fn is a no-op for every piece. +// When a change is detected at index i, all prior unchanged pieces are copied +// into the output before appending the changed piece and any remainder. +func mapDelimited(raw []byte, sep byte, fn func([]byte) ([]byte, error)) ([]byte, error) { + n := countDelimited(raw, sep) + var out []byte + + for i := range n { + piece := nthSlice(raw, sep, i) + mapped, err := fn(piece) + if err != nil { + return nil, err + } + + if out == nil && bytes.Equal(piece, mapped) { + continue // unchanged so far — no allocation + } + + if out == nil { + // First change at index i: retroactively copy pieces 0..i-1, + // then add the separator that precedes this changed piece. + out = make([]byte, 0, len(raw)) + for j := range i { + if j > 0 { + out = append(out, sep) + } + out = append(out, nthSlice(raw, sep, j)...) + } + if i > 0 { + out = append(out, sep) + } + } else { + out = append(out, sep) + } + out = append(out, mapped...) + } + + if out == nil { + return raw, nil // nothing changed + } + return out, nil +} + +// mapLeafValue unescapes raw, applies mapper, and re-escapes the result. +func mapLeafValue(raw []byte, delims Delimiters, mapper ValueMapper) ([]byte, error) { + unescaped := Unescape(raw, delims) + mapped, err := mapper(unescaped) + if err != nil { + return nil, err + } + return Escape(mapped, delims), nil +} + // appendMaybeEscaped appends a literal byte to out, escaping it if it // matches a destination delimiter character. func appendMaybeEscaped(out []byte, b byte, dst Delimiters) []byte { diff --git a/transform_test.go b/transform_test.go index e6acbdc..49ffc2f 100644 --- a/transform_test.go +++ b/transform_test.go @@ -15,6 +15,7 @@ package hl7 import ( + "bytes" "errors" "os" "path/filepath" @@ -1573,3 +1574,399 @@ func BenchmarkAppendSegment(b *testing.B) { _, _ = msg.Transform(AppendSegment("ZPD")) } } + +// --- MapAllValues tests --- + +func TestMapAllValuesIdentity(t *testing.T) { + raw := "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||123||Doe^John||19800101" + msg := parseTestMessage(t, raw) + + identity := func(b []byte) ([]byte, error) { return b, nil } + + result, err := msg.Transform(MapAllValues(identity)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + + // Delimiters unchanged. + if result.Delimiters() != msg.Delimiters() { + t.Errorf("delimiters changed: got %+v, want %+v", result.Delimiters(), msg.Delimiters()) + } + // All sampled values unchanged. + for _, loc := range []string{"PID-3", "PID-5.1", "PID-5.2", "PID-7", "MSH-9.1", "MSH-9.2"} { + got := result.Get(loc).String() + want := msg.Get(loc).String() + if got != want { + t.Errorf("identity: %s = %q, want %q", loc, got, want) + } + } +} + +func TestMapAllValuesUppercase(t *testing.T) { + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||123||doe^john") + + upper := func(b []byte) ([]byte, error) { return bytes.ToUpper(b), nil } + + result, err := msg.Transform(MapAllValues(upper)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + + // PID-5 components uppercased. + if got := result.Get("PID-5.1").String(); got != "DOE" { + t.Errorf("PID-5.1 = %q, want %q", got, "DOE") + } + if got := result.Get("PID-5.2").String(); got != "JOHN" { + t.Errorf("PID-5.2 = %q, want %q", got, "JOHN") + } + // Component separator preserved (not uppercased to something else). + if result.Delimiters().Component != '^' { + t.Errorf("component separator changed: %c", result.Delimiters().Component) + } + // PID-3 uppercased. + if got := result.Get("PID-3").String(); got != "123" { + t.Errorf("PID-3 = %q, want %q", got, "123") // digits: already "uppercase" + } + // MSH-9 components uppercased. + if got := result.Get("MSH-9.1").String(); got != "ADT" { + t.Errorf("MSH-9.1 = %q, want %q", got, "ADT") + } +} + +func TestMapAllValuesNull(t *testing.T) { + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||\"\"|") + + callCount := 0 + capture := func(b []byte) ([]byte, error) { + callCount++ + return b, nil // pass through + } + + result, err := msg.Transform(MapAllValues(capture)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + // Mapper should have been called, including for the null field (PID-3 = ""). + if callCount == 0 { + t.Error("mapper was never called") + } + // PID-3 was null ("") — after identity mapper it should still be null. + pid3 := result.Segments()[1].Field(3) + if !pid3.IsNull() { + t.Errorf("PID-3 should still be null, got %q", pid3.String()) + } +} + +func TestMapAllValuesNullReplaced(t *testing.T) { + // Mapper returns non-empty for a null ("") leaf — value should be stored. + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||\"\"") + + replaceNull := func(b []byte) ([]byte, error) { + if bytes.Equal(b, []byte(`""`)) { + return []byte("REPLACED"), nil + } + return b, nil + } + + result, err := msg.Transform(MapAllValues(replaceNull)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + if got := result.Get("PID-3").String(); got != "REPLACED" { + t.Errorf("PID-3 = %q, want %q", got, "REPLACED") + } +} + +func TestMapAllValuesEmpty(t *testing.T) { + // Mapper IS called with empty bytes; it can return a value. + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1|||5678") + + replaceEmpty := func(b []byte) ([]byte, error) { + if len(b) == 0 { + return []byte("FILLED"), nil + } + return b, nil + } + + result, err := msg.Transform(MapAllValues(replaceEmpty)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + // PID-3 was empty — mapper returns "FILLED". + if got := result.Get("PID-3").String(); got != "FILLED" { + t.Errorf("PID-3 = %q, want %q", got, "FILLED") + } + // PID-4 was "5678" — mapper returns it unchanged. + if got := result.Get("PID-4").String(); got != "5678" { + t.Errorf("PID-4 = %q, want %q", got, "5678") + } +} + +func TestMapAllValuesMSHDelimFields(t *testing.T) { + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||123") + + upper := func(b []byte) ([]byte, error) { return bytes.ToUpper(b), nil } + + result, err := msg.Transform(MapAllValues(upper)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + + // MSH-1 and MSH-2 must never be modified. + d := result.Delimiters() + if d.Field != '|' { + t.Errorf("MSH-1 (field sep) changed: %q", d.Field) + } + if got := result.Segments()[0].Field(2).Bytes(); string(got) != "^~\\&" { + t.Errorf("MSH-2 changed: %q", got) + } +} + +func TestMapAllValuesSubcomponents(t *testing.T) { + // Field a&b — mapper applied to each subcomponent ("a" and "b") independently. + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||a&b") + + + upper := func(b []byte) ([]byte, error) { return bytes.ToUpper(b), nil } + + result, err := msg.Transform(MapAllValues(upper)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + + // Each subcomponent is independently uppercased; & separator is preserved. + if got := result.Get("PID-3.1.1").String(); got != "A" { + t.Errorf("PID-3.1.1 = %q, want %q", got, "A") + } + if got := result.Get("PID-3.1.2").String(); got != "B" { + t.Errorf("PID-3.1.2 = %q, want %q", got, "B") + } +} + +func TestMapAllValuesSubcomponentsPartialChange(t *testing.T) { + // Only the SECOND subcomponent changes: "A&b" → expected "A&B". + // Exercises the lazy-init separator logic when i > 0 is the first change. + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||A&b") + + upper := func(b []byte) ([]byte, error) { return bytes.ToUpper(b), nil } + + result, err := msg.Transform(MapAllValues(upper)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + + if got := result.Get("PID-3.1.1").String(); got != "A" { + t.Errorf("PID-3.1.1 = %q, want %q", got, "A") + } + if got := result.Get("PID-3.1.2").String(); got != "B" { + t.Errorf("PID-3.1.2 = %q, want %q", got, "B") + } +} + +func TestMapAllValuesRepetitions(t *testing.T) { + // Field val1~val2 — mapper applied to each repetition independently. + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||rep1~rep2") + + upper := func(b []byte) ([]byte, error) { return bytes.ToUpper(b), nil } + + result, err := msg.Transform(MapAllValues(upper)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + + if got := result.Get("PID-3[0]").String(); got != "REP1" { + t.Errorf("PID-3[0] = %q, want %q", got, "REP1") + } + if got := result.Get("PID-3[1]").String(); got != "REP2" { + t.Errorf("PID-3[1] = %q, want %q", got, "REP2") + } +} + +func TestMapAllValuesEscapeRoundTrip(t *testing.T) { + // PID-3 contains \F\ (escaped field separator). + // Mapper receives unescaped "|"; output is re-escaped before storage. + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||before\\F\\after") + + identity := func(b []byte) ([]byte, error) { return b, nil } + + result, err := msg.Transform(MapAllValues(identity)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + + // String() should return the unescaped value unchanged. + if got := result.Get("PID-3").String(); got != "before|after" { + t.Errorf("PID-3 = %q, want %q", got, "before|after") + } + // Raw bytes should contain the re-escaped sequence. + if raw := result.Get("PID-3").Bytes(); !bytes.Contains(raw, []byte(`\F\`)) { + t.Errorf("PID-3 raw %q should contain \\F\\", raw) + } +} + +func TestMapAllValuesError(t *testing.T) { + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||123") + + sentinel := errors.New("mapper error") + errMapper := func(b []byte) ([]byte, error) { + if len(b) > 0 { + return nil, sentinel + } + return b, nil + } + + _, err := msg.Transform(MapAllValues(errMapper)) + if !errors.Is(err, sentinel) { + t.Errorf("expected sentinel error, got %v", err) + } +} + +func TestMapAllValuesMixedChanges(t *testing.T) { + // MapAllValues and Replace in the same Transform call. + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||123||doe^john") + + upper := func(b []byte) ([]byte, error) { return bytes.ToUpper(b), nil } + + result, err := msg.Transform( + MapAllValues(upper), + Replace("PID-5.2", "Override"), + ) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + + // MapAllValues ran first, uppercasing everything; then Replace overwrote PID-5.2. + if got := result.Get("PID-5.1").String(); got != "DOE" { + t.Errorf("PID-5.1 = %q, want %q", got, "DOE") + } + if got := result.Get("PID-5.2").String(); got != "Override" { + t.Errorf("PID-5.2 = %q, want %q", got, "Override") + } +} + +func BenchmarkMapAllValues(b *testing.B) { + data, err := os.ReadFile(filepath.Join("testdata", "oru_r01.hl7")) + if err != nil { + b.Fatalf("failed to read test file: %v", err) + } + + msg, err := ParseMessage(data) + if err != nil { + b.Fatalf("ParseMessage failed: %v", err) + } + + identity := func(b []byte) ([]byte, error) { return b, nil } + + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + _, _ = msg.Transform(MapAllValues(identity)) + } +} + +// --- MapValue tests --- + +func TestMapValue(t *testing.T) { + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||123||Doe^John") + + upper := func(b []byte) ([]byte, error) { return bytes.ToUpper(b), nil } + + result, err := msg.Transform(MapValue("PID-5.1", upper)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + + // Only PID-5.1 is modified. + if got := result.Get("PID-5.1").String(); got != "DOE" { + t.Errorf("PID-5.1 = %q, want %q", got, "DOE") + } + // PID-5.2 unchanged. + if got := result.Get("PID-5.2").String(); got != "John" { + t.Errorf("PID-5.2 = %q, want %q", got, "John") + } + // PID-3 unchanged. + if got := result.Get("PID-3").String(); got != "123" { + t.Errorf("PID-3 = %q, want %q", got, "123") + } +} + +func TestMapValueAbsentLocation(t *testing.T) { + // PID-19 doesn't exist; mapper is called with nil; if it returns + // non-nil, a new field should be created. + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||123") + + fill := func(b []byte) ([]byte, error) { + if b == nil || len(b) == 0 { + return []byte("CREATED"), nil + } + return b, nil + } + + result, err := msg.Transform(MapValue("PID-19", fill)) + if err != nil { + t.Fatalf("Transform failed: %v", err) + } + + if got := result.Get("PID-19").String(); got != "CREATED" { + t.Errorf("PID-19 = %q, want %q", got, "CREATED") + } +} + +func TestMapValueInvalidLocation(t *testing.T) { + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1") + + identity := func(b []byte) ([]byte, error) { return b, nil } + + _, err := msg.Transform(MapValue("invalid", identity)) + if !errors.Is(err, ErrInvalidLocation) { + t.Errorf("expected ErrInvalidLocation, got %v", err) + } +} + +func TestMapValueError(t *testing.T) { + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||123") + + sentinel := errors.New("map error") + errMapper := func(b []byte) ([]byte, error) { return nil, sentinel } + + _, err := msg.Transform(MapValue("PID-3", errMapper)) + if !errors.Is(err, sentinel) { + t.Errorf("expected sentinel error, got %v", err) + } +} + +func TestMapValueMSHDelimReject(t *testing.T) { + msg := parseTestMessage(t, "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1") + + identity := func(b []byte) ([]byte, error) { return b, nil } + + _, err := msg.Transform(MapValue("MSH-1", identity)) + if !errors.Is(err, ErrMSHDelimiterField) { + t.Errorf("expected ErrMSHDelimiterField for MSH-1, got %v", err) + } + + _, err = msg.Transform(MapValue("MSH-2", identity)) + if !errors.Is(err, ErrMSHDelimiterField) { + t.Errorf("expected ErrMSHDelimiterField for MSH-2, got %v", err) + } +} + +func BenchmarkMapValue(b *testing.B) { + data, err := os.ReadFile(filepath.Join("testdata", "oru_r01.hl7")) + if err != nil { + b.Fatalf("failed to read test file: %v", err) + } + + msg, err := ParseMessage(data) + if err != nil { + b.Fatalf("ParseMessage failed: %v", err) + } + + upper := func(b []byte) ([]byte, error) { return bytes.ToUpper(b), nil } + + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + _, _ = msg.Transform(MapValue("PID-5.1", upper)) + } +}