Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions accessor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ func TestParseLocationEdgeCases(t *testing.T) {
{"EmptyFieldPart", "PID-", true},
{"TrailingGarbageAfterRep", "PID-3[1]x", true},
{"TrailingGarbageAfterSegIndex", "OBX(1)x-5", true},
// ZeroComponent documents that ".0" is accepted and treated as
// "component not specified" (equivalent to omitting the component).
{"ZeroComponent", "PID-3.0", false},
}

for _, tt := range tests {
Expand All @@ -313,6 +316,31 @@ func TestParseLocationEdgeCases(t *testing.T) {
}
}

func TestGetVeryHighSegmentIndex(t *testing.T) {
raw := []byte("MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rOBX|1|NM|code||42")
msg, err := ParseMessage(raw)
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)
}
}

func TestGetSegmentWithNoFields(t *testing.T) {
// A segment consisting of only the type name with no field separator.
raw := []byte("MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rZZZ")
msg, err := ParseMessage(raw)
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)
}
}

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)
Expand Down
62 changes: 62 additions & 0 deletions ack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,68 @@ func TestAckWithErrors(t *testing.T) {
})
}

func TestAckNoMSH(t *testing.T) {
msg := &Message{}
_, err := msg.Ack(AA, "X")
if err != ErrNoMSHSegment {
t.Errorf("err = %v, want ErrNoMSHSegment", err)
}
}

func TestIsUpperAlpha(t *testing.T) {
tests := []struct {
input string
want bool
}{
{"", true}, // vacuously true — loop body never executes
{"AB", true},
{"PID", true},
{"pid", false},
{"Pid", false},
{"PI1", false},
{"ABCD", true}, // length not checked by isUpperAlpha itself
}

for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
if got := isUpperAlpha(tt.input); got != tt.want {
t.Errorf("isUpperAlpha(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}

func TestAppendERLBareSegmentBoundaries(t *testing.T) {
d := DefaultDelimiters()
tests := []struct {
location string
wantNil bool
want string
}{
{"A", true, ""}, // len < 2
{"AB", false, "AB^1"}, // len == 2, upper alpha
{"PID", false, "PID^1"}, // len == 3, upper alpha
{"ABCD", true, ""}, // len > 3
{"pid", true, ""}, // lowercase — isUpperAlpha fails
{"P!D", true, ""}, // non-alpha — isUpperAlpha fails
}

for _, tt := range tests {
t.Run(tt.location, func(t *testing.T) {
got := appendERL(nil, tt.location, d)
if tt.wantNil {
if len(got) != 0 {
t.Errorf("appendERL(nil, %q, d) = %q, want nil/empty", tt.location, got)
}
} else {
if string(got) != tt.want {
t.Errorf("appendERL(nil, %q, d) = %q, want %q", tt.location, got, tt.want)
}
}
})
}
}

func BenchmarkAckWithErrors(b *testing.B) {
raw := []byte("MSH|^~\\&|SEND_APP|SEND_FAC|RECV_APP|RECV_FAC|20240115||ADT^A01^ADT_A01|MSG001|P|2.5.1\rPID|||123||Smith^John")
msg, _ := ParseMessage(raw)
Expand Down
42 changes: 42 additions & 0 deletions batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,48 @@ func TestJoinLinesEdgeCases(t *testing.T) {
})
}

func TestParseBatchBodyEmpty(t *testing.T) {
// BHS and BTS present but no messages between them.
data := []byte("BHS|^~\\&|S|F|R|RF|20240101\rBTS|0")
batch, err := ParseBatch(data)
if err != nil {
t.Fatalf("ParseBatch: %v", err)
}
if batch.Header == nil {
t.Error("expected non-nil BHS header")
}
if batch.Trailer == nil {
t.Error("expected non-nil BTS trailer")
}
if len(batch.Messages) != 0 {
t.Errorf("expected 0 messages, got %d", len(batch.Messages))
}
}

func TestParseBatchNonMSHOnlyLines(t *testing.T) {
// No MSH, no BHS/BTS — only ZZZ segments.
data := []byte("ZZZ|some|data\rZZZ|more|data")
batch, err := ParseBatch(data)
if err != nil {
t.Fatalf("ParseBatch: %v", err)
}
if len(batch.Messages) != 0 {
t.Errorf("expected 0 messages, got %d", len(batch.Messages))
}
}

func TestParseFileBTSNoBatch(t *testing.T) {
// BTS before any BHS: flushBatch is a no-op since no current batch.
data := []byte("FHS|^~\\&|S|F|R|RF|20240101\rBTS|0\rFTS|1")
file, err := ParseFile(data)
if err != nil {
t.Fatalf("ParseFile: %v", err)
}
if len(file.Batches) != 0 {
t.Errorf("expected 0 batches, got %d", len(file.Batches))
}
}

func TestParseBatchCustomDelimiters(t *testing.T) {
data := []byte("BHS#@!$%SEND#FAC#REC#RFAC#20240101\rMSH#@!$%S#F#R#RF#20240101##ADT@A01#1#P#2.5.1\rPID#1##123\rBTS#1")
batch, err := ParseBatch(data)
Expand Down
35 changes: 35 additions & 0 deletions delimiters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ func TestExtractDelimiters(t *testing.T) {
input: []byte("MSH|\r~\\&|"),
wantErr: ErrInvalidDelimiter,
},
{
name: "LFRejected",
input: []byte("MSH|\n~\\&|"),
wantErr: ErrInvalidDelimiter,
},
{
// TAB as field separator: data[3] = '\t', then standard encoding chars.
name: "TabAccepted",
input: []byte("MSH\t^~\\&\trest"),
want: Delimiters{Field: '\t', Component: '^', Repetition: '~', Escape: '\\', SubComponent: '&'},
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -115,6 +126,14 @@ func TestDelimitersValidate(t *testing.T) {
if err := d.validate(); err != ErrDuplicateDelimiter {
t.Errorf("expected ErrDuplicateDelimiter, got %v", err)
}

t.Run("LFDelimiter", func(t *testing.T) {
d2 := DefaultDelimiters()
d2.Field = '\n'
if err := d2.validate(); err != ErrInvalidDelimiter {
t.Errorf("expected ErrInvalidDelimiter for LF field sep, got %v", err)
}
})
}

func TestNthSliceEdgeCases(t *testing.T) {
Expand Down Expand Up @@ -145,6 +164,22 @@ func TestNthSliceEdgeCases(t *testing.T) {
t.Errorf("got %q, want nil", got)
}
})

t.Run("DelimiterAtEnd", func(t *testing.T) {
data := []byte("hello|")
// n=0: the piece before the trailing delimiter.
got0 := nthSlice(data, '|', 0)
if string(got0) != "hello" {
t.Errorf("n=0: got %q, want %q", got0, "hello")
}
// n=1: the empty piece after the trailing delimiter — present but empty.
got1 := nthSlice(data, '|', 1)
if got1 == nil {
t.Error("n=1: got nil, want empty non-nil slice")
} else if len(got1) != 0 {
t.Errorf("n=1: got %q, want empty slice", got1)
}
})
}

func TestNthRangeEdgeCases(t *testing.T) {
Expand Down
21 changes: 18 additions & 3 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,24 @@
// hl7.Copy("PID-4", "PID-3"),
// )
//
// 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.
// 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.
//
// Segment-level change constructors add, remove, or reorder entire segments:
//
// updated, err := msg.Transform(
// hl7.DeleteSegment("NTE", 0), // delete first NTE (no-op if absent)
// hl7.DeleteAllSegments("NTE"), // delete all NTE segments
// hl7.InsertSegmentAfter("PID", 0, "ZPD"), // insert ZPD after first PID
// hl7.InsertSegmentBefore("OBX", -1, "ZPD"), // insert ZPD before last OBX
// hl7.AppendSegment("ZPD"), // append ZPD at end of message
// )
//
// DeleteSegment and DeleteAllSegments are no-ops when the target is absent;
// both return ErrCannotDeleteMSH if given "MSH". InsertSegmentAfter and
// InsertSegmentBefore return ErrSegmentNotFound when the reference segment is
// absent. Segment-level and field-level changes compose freely in one call.
//
// TransformWith additionally re-encodes all delimiters to a new set:
//
Expand Down
1 change: 1 addition & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var (
ErrBatchStructure = errors.New("hl7: invalid batch structure")
ErrMSHDelimiterField = errors.New("hl7: cannot modify MSH-1 or MSH-2 (delimiter fields)")
ErrDelimiterMismatch = errors.New("hl7: delimiter mismatch between messages")
ErrCannotDeleteMSH = errors.New("hl7: cannot delete MSH segment")
)

// ParseError provides detailed context about a parsing failure.
Expand Down
Loading
Loading