Skip to content

Commit 20a9f54

Browse files
committed
decode: support decoding when field level errors are present
1 parent 0cb603c commit 20a9f54

24 files changed

+314
-143
lines changed

coder.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ type RegistryLookup = map[string]any
1010

1111
type Registry interface {
1212
Version() string
13-
ControlSegment() RegistryLookup
14-
Segment() RegistryLookup
15-
Trigger() RegistryLookup
16-
DataType() RegistryLookup
13+
ControlSegment(string) (any, bool)
14+
Segment(string) (any, bool)
15+
Trigger(string) (any, bool)
16+
DataType(string) (any, bool)
1717
}
1818

1919
const tagName = "hl7"

coder_test.go

+109
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package hl7
33
import (
44
"bytes"
55
"encoding/json"
6+
"errors"
67
"flag"
78
"os"
89
"path/filepath"
@@ -56,6 +57,114 @@ OBR|2|XYZ||||||||||||||903^Blacky|||||||||||||||||||||||||||||||||||||||||||`)
5657
}
5758
_ = data
5859
}
60+
61+
// DecodeInlineError test that a single field error will not prevent
62+
// decoding the remainder of the message.
63+
func TestDecodeInlineError(t *testing.T) {
64+
// The date of birth is incorrect in this PID. The day of month is "92" rather then "02".
65+
// 92 is an invalid day of month, so it will error. However, only this segment will error, and the other valid segment fields
66+
// will still be accessable.
67+
var raw = []byte(`MSH|^~\&|DX_LAB|Hematology|WPX||20070305170957||ORL^O34^ORL_O34|2|P|2.5||||||8859/1|||
68+
PID|1||PID1992299||Smith^John||19561192000000|M||Caucasian||||||||||||Caucasian|||||||||||||||||
69+
NTE|1||testing the system comments here|||
70+
NTE|2||more comments here|||
71+
OBR|1|ABC||||||||||||||1234^Acron^Smith~5678^Beta^Zeta|||||||||||||||||||||||||||||||||||||||||||
72+
OBR|2|XYZ||||||||||||||903^Blacky|||||||||||||||||||||||||||||||||||||||||||`)
73+
74+
d := NewDecoder(v25.Registry, nil)
75+
v, err := d.DecodeList(raw)
76+
if err != nil {
77+
t.Fatal(err)
78+
}
79+
const (
80+
wantFirst = "John"
81+
wantLast = "Smith"
82+
wantError = `line 2, PID.DateTimeOfBirth: time.Time.7: field "19561192000000" : parsing time "19561192000000": day out of range`
83+
)
84+
var gotError, gotFirst, gotLast string
85+
for _, item := range v {
86+
if se, ok := item.(SegmentError); ok {
87+
gotError = errors.Join(se.ErrorList...).Error()
88+
if pid, ok := se.Segment.(*v25.PID); ok {
89+
if len(pid.PatientName) > 0 {
90+
p := pid.PatientName[0]
91+
gotFirst = p.GivenName
92+
gotLast = p.FamilyName
93+
}
94+
}
95+
}
96+
}
97+
ck := func(name, g, w string) {
98+
if g != w {
99+
t.Errorf("for %s, got=<<%s>> want=<<%s>>", name, g, w)
100+
}
101+
}
102+
ck("error", gotError, wantError)
103+
ck("first", gotFirst, wantFirst)
104+
ck("last", gotLast, wantLast)
105+
106+
gr, err := d.DecodeGroup(v)
107+
var grErrText string
108+
if err != nil {
109+
grErrText = err.Error()
110+
}
111+
ck("group-error", grErrText, wantError)
112+
m, ok := gr.(v25.ORL_O34)
113+
if !ok {
114+
t.Fatal("incorrect message type")
115+
}
116+
117+
p := m.Response.Patient.PID.PatientName[0]
118+
gotFirst = p.GivenName
119+
gotLast = p.FamilyName
120+
ck("first", gotFirst, wantFirst)
121+
ck("last", gotLast, wantLast)
122+
}
123+
124+
// Test decoding only the header.
125+
func TestDecodeHeader(t *testing.T) {
126+
var raw = []byte(`MSH|^~\&|DX_LAB|Hematology|WPX||20070305170957|XYZ|ORL^O34^ORL_O34|2|P|2.5||||||8859/1|||
127+
PID|1||PID1992299||Smith^John||19561192000000|M||Caucasian||||||||||||Caucasian|||||||||||||||||
128+
NTE|1||testing the system comments here|||
129+
NTE|2||more comments here|||
130+
OBR|1|ABC||||||||||||||1234^Acron^Smith~5678^Beta^Zeta|||||||||||||||||||||||||||||||||||||||||||
131+
OBR|2|XYZ||||||||||||||903^Blacky|||||||||||||||||||||||||||||||||||||||||||`)
132+
133+
d := NewDecoder(v25.Registry, &DecodeOption{
134+
HeaderOnly: true,
135+
})
136+
v, err := d.DecodeList(raw)
137+
if err != nil {
138+
t.Fatal(err)
139+
}
140+
if len(v) != 1 {
141+
t.Fatalf("expected 1 segment, got %d segments", len(v))
142+
}
143+
el := v[0]
144+
m, ok := el.(*v25.MSH)
145+
if !ok {
146+
t.Fatalf("expected MSG, got %T", el)
147+
}
148+
ck := func(name, g, w string) {
149+
if g != w {
150+
t.Errorf("for %s, got=<<%s>> want=<<%s>>", name, g, w)
151+
}
152+
}
153+
ck("security", m.Security, "XYZ")
154+
gr, err := d.DecodeGroup(v)
155+
mgr, ok := gr.(v25.ORL_O34)
156+
if !ok {
157+
t.Fatal("incorrect message type")
158+
}
159+
if err != nil {
160+
t.Error(err)
161+
}
162+
ck("group-security", mgr.MSH.Security, "XYZ")
163+
if mgr.Response != nil {
164+
t.Error("expected nil Response")
165+
}
166+
}
167+
59168
func TestGroup(t *testing.T) {
60169
flag.Parse()
61170
var raw = []byte(`MSH|^~\&|DX_LAB|Hematology|WPX||20070305170957||ORL^O34^ORL_O34|2|P|2.5||||||8859/1|||

decode.go

+29-9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Decoder struct {
2929
// Decode options for the HL7 decoder.
3030
type DecodeOption struct {
3131
ErrorZSegment bool // Error on an unknown Zxx segment when true.
32+
HeaderOnly bool // Only decode first segment, usually the header.
3233
}
3334

3435
// Create a new Decoder. A registry must be provided. Option is optional.
@@ -57,19 +58,29 @@ func (d *Decoder) Decode(data []byte) (any, error) {
5758
}
5859

5960
// Group a list of elements into trigger groupings.
61+
// A value and error may be present at the same time.
6062
func (d *Decoder) DecodeGroup(list []any) (any, error) {
6163
return group(list, d.registry)
6264
}
6365

6466
// Varies should be implemented on a segment that knows how to
6567
// decode a child VARIES data type.
6668
type Varies interface {
67-
ChildVaries(dtReg map[string]any) (reflect.Value, error)
69+
ChildVaries(reg func(string) (any, bool)) (reflect.Value, error)
6870
}
6971

7072
type variesFunc func() (reflect.Value, error)
7173

72-
var variesType = reflect.TypeOf((*Varies)(nil)).Elem()
74+
var variesType = reflect.TypeFor[Varies]()
75+
76+
// SgmentError may be returned as part of the DecodeList result.
77+
// This allows a single segment to be decoded poorly with error, while
78+
// still decoding the rest of the message.
79+
// This will not be returned as an error.
80+
type SegmentError struct {
81+
ErrorList []error
82+
Segment any
83+
}
7384

7485
// Decode returns a list of segments without any grouping applied.
7586
func (d *Decoder) DecodeList(data []byte) ([]any, error) {
@@ -93,7 +104,6 @@ func (d *Decoder) DecodeList(data []byte) ([]any, error) {
93104
ret := []any{}
94105

95106
ld := &lineDecoder{}
96-
segmentRegistry := d.registry.Segment()
97107
for index, line := range lines {
98108
lineNumber := index + 1
99109
if len(line) == 0 {
@@ -106,7 +116,7 @@ func (d *Decoder) DecodeList(data []byte) ([]any, error) {
106116
return nil, fmt.Errorf("line %d: missing segment type", lineNumber)
107117
}
108118

109-
seg, ok := segmentRegistry[segTypeName]
119+
seg, ok := d.registry.Segment(segTypeName)
110120
if !ok {
111121
isZ := len(segTypeName) > 0 && segTypeName[0] == 'Z'
112122
if isZ && !d.opt.ErrorZSegment {
@@ -214,12 +224,12 @@ func (d *Decoder) DecodeList(data []byte) ([]any, error) {
214224

215225
var vfc variesFunc
216226
if rvv.Type().Implements(variesType) {
217-
dtReg := d.registry.DataType()
218227
vfc = func() (reflect.Value, error) {
219-
return rvv.Interface().(Varies).ChildVaries(dtReg)
228+
return rvv.Interface().(Varies).ChildVaries(d.registry.DataType)
220229
}
221230
}
222231

232+
var segmentErrorList []error
223233
for i, f := range ff {
224234
if i >= len(parts) {
225235
break
@@ -233,11 +243,21 @@ func (d *Decoder) DecodeList(data []byte) ([]any, error) {
233243
}
234244
err := ld.decodeSegmentList(p, f.tag, f.field, vfc)
235245
if err != nil {
236-
return ret, fmt.Errorf("line %d, %s.%s: %w", lineNumber, SegmentName, f.name, err)
246+
err = fmt.Errorf("line %d, %s.%s: %w", lineNumber, SegmentName, f.name, err)
247+
segmentErrorList = append(segmentErrorList, err)
237248
}
238249
}
239-
240-
ret = append(ret, rv.Interface())
250+
if segmentErrorList != nil {
251+
ret = append(ret, SegmentError{
252+
ErrorList: segmentErrorList,
253+
Segment: rv.Interface(),
254+
})
255+
} else {
256+
ret = append(ret, rv.Interface())
257+
}
258+
if d.opt.HeaderOnly {
259+
return ret, nil
260+
}
241261
}
242262
return ret, nil
243263
}

decodeGroup.go

+32-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package hl7
22

33
import (
4+
"errors"
45
"fmt"
56
"reflect"
67
)
@@ -11,22 +12,21 @@ type messageStructure interface {
1112

1213
func newWalker(list []any, registry Registry) (*walker, error) {
1314
if len(list) == 0 {
14-
return nil, fmt.Errorf("List is empty")
15+
return nil, fmt.Errorf("list is empty")
1516
}
1617

1718
root := list[0]
1819
ms, ok := root.(messageStructure)
1920
if !ok {
20-
return nil, fmt.Errorf("First message must implment MessageStructure, %T does not", root)
21+
return nil, fmt.Errorf("first message must implment MessageStructure, %T does not", root)
2122
}
2223
code := ms.MessageStructureID()
2324
if len(code) == 0 {
24-
return nil, fmt.Errorf("Message structure code empty, malformed message: %#v", root)
25+
return nil, fmt.Errorf("message structure code empty, malformed message: %#v", root)
2526
}
26-
tr := registry.Trigger()
27-
vex, ok := tr[code]
27+
vex, ok := registry.Trigger(code)
2828
if !ok {
29-
return nil, fmt.Errorf("Message structure code not found %q", code)
29+
return nil, fmt.Errorf("message structure code not found %q", code)
3030
}
3131
tp := reflect.TypeOf(vex)
3232

@@ -63,7 +63,7 @@ func (w *walker) process(list []any) (any, error) {
6363
}
6464

6565
if len(w.list) == 0 {
66-
return nil, fmt.Errorf("Missing list item. This should not happen.")
66+
return nil, fmt.Errorf("missing list item,his should not happen")
6767
}
6868
rootSI := w.list[0]
6969
if !rootSI.ActiveValue.IsValid() {
@@ -76,11 +76,22 @@ func (w *walker) process(list []any) (any, error) {
7676

7777
// group a list of segments into hierarchical groups with a single root element.
7878
func group(list []any, registry Registry) (any, error) {
79+
var segErrs []error
80+
for i, item := range list {
81+
if se, ok := item.(SegmentError); ok {
82+
segErrs = append(segErrs, se.ErrorList...)
83+
list[i] = se.Segment
84+
}
85+
}
7986
w, err := newWalker(list, registry)
8087
if err != nil {
8188
return nil, err
8289
}
83-
return w.process(list)
90+
gr, err := w.process(list)
91+
if err != nil {
92+
segErrs = append(segErrs, err)
93+
}
94+
return gr, errors.Join(segErrs...)
8495
}
8596

8697
type linkType int
@@ -107,6 +118,17 @@ func (lt linkType) String() string {
107118
}
108119
}
109120

121+
func present(rv reflect.Value) bool {
122+
if !rv.IsValid() {
123+
return false
124+
}
125+
if rv.IsZero() {
126+
return false
127+
}
128+
129+
return true
130+
}
131+
110132
type structItem struct {
111133
Index int
112134
Parent *structItem
@@ -118,28 +140,9 @@ type structItem struct {
118140
}
119141

120142
func (si *structItem) present() bool {
121-
rv := si.ActiveValue
122-
if !rv.IsValid() {
123-
return false
124-
}
125-
if rv.IsZero() {
126-
return false
127-
}
128-
129-
return true
130-
// return present(si.ActiveValue)
131-
// return si.ActiveSet
143+
return present(si.ActiveValue)
132144
}
133-
func present(rv reflect.Value) bool {
134-
if !rv.IsValid() {
135-
return false
136-
}
137-
if rv.IsZero() {
138-
return false
139-
}
140145

141-
return true
142-
}
143146
func (si *structItem) set(rv reflect.Value) {
144147
si.ActiveValue = rv
145148
}
@@ -181,8 +184,7 @@ func (w *walker) digest(line int, v any) error {
181184
return w.found(line, i, si, v, rv, rt)
182185
}
183186

184-
gr := w.registry.ControlSegment()
185-
_, isControl := gr[rt.Name()]
187+
_, isControl := w.registry.ControlSegment(rt.Name())
186188
if isControl {
187189
// TODO: handle batch and control segments.
188190
return nil

h210/registry.go

+12-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)