diff --git a/field/composite.go b/field/composite.go index d9a05e9b..5485c19e 100644 --- a/field/composite.go +++ b/field/composite.go @@ -6,12 +6,15 @@ import ( "fmt" "reflect" "regexp" + "strconv" "github.com/moov-io/iso8583/padding" "github.com/moov-io/iso8583/sort" "github.com/moov-io/iso8583/utils" ) +const compositeBitmapKey = "0" + var _ Field = (*Composite)(nil) var _ json.Marshaler = (*Composite)(nil) var _ json.Unmarshaler = (*Composite)(nil) @@ -58,6 +61,8 @@ var _ json.Unmarshaler = (*Composite)(nil) type Composite struct { spec *Spec + bitmap *Bitmap + orderedSpecFieldTags []string // stores all fields according to the spec @@ -115,7 +120,10 @@ func (f *Composite) SetSpec(spec *Spec) { panic(err) } f.spec = spec - f.orderedSpecFieldTags = orderedKeys(spec.Subfields, spec.Tag.Sort) + + if spec.Tag != nil && spec.Tag.Sort != nil { + f.orderedSpecFieldTags = orderedKeys(spec.Subfields, spec.Tag.Sort) + } } func (f *Composite) Unmarshal(v interface{}) error { @@ -290,6 +298,18 @@ func (f *Composite) Bytes() ([]byte, error) { return f.pack() } +// Bitmap returns an instantiation of Bitmap using the BitmapSpec from the spec attribute. +func (f *Composite) Bitmap() *Bitmap { + if f.bitmap == nil { + if field, ok := f.subfields[compositeBitmapKey]; ok { + f.bitmap = field.(*Bitmap) + } + + } + + return f.bitmap +} + // String iterates over the receiver's subfields, packs them and converts the // result to a string. The result does not incorporate the encoded aggregate // length of the subfields in the prefix. @@ -342,6 +362,53 @@ func (f *Composite) UnmarshalJSON(b []byte) error { } func (f *Composite) pack() ([]byte, error) { + if f.Bitmap() != nil { + return f.packSubfieldsByBitmap() + } + + return f.packSubfieldsByTag() +} + +func (f *Composite) packSubfieldsByBitmap() ([]byte, error) { + packed := []byte{} + f.Bitmap().Reset() + + ids, err := f.packableFieldIDs() + if err != nil { + return nil, fmt.Errorf("failed to pack message: %w", err) + } + + for _, id := range ids { + // index 0 is for bitmap + // regular field number startd from index 1 + idInt, err := strconv.Atoi(id) + if err != nil { + return nil, fmt.Errorf("failed to pack message: %w", err) + } + + if idInt < 1 { + continue + } + f.Bitmap().Set(idInt) + } + + // pack fields + for _, i := range ids { + field, ok := f.subfields[i] + if !ok { + return nil, fmt.Errorf("failed to pack field %v: no specification found", i) + } + packedField, err := field.Pack() + if err != nil { + return nil, fmt.Errorf("failed to pack field %v (%s): %w", i, field.Spec().Description, err) + } + packed = append(packed, packedField...) + } + + return packed, nil +} + +func (f *Composite) packSubfieldsByTag() ([]byte, error) { packed := []byte{} for _, tag := range f.orderedSpecFieldTags { field, ok := f.subfields[tag] @@ -377,6 +444,9 @@ func (f *Composite) pack() ([]byte, error) { } func (f *Composite) unpack(data []byte, isVariableLength bool) (int, error) { + if f.Bitmap() != nil { + return f.unpackSubfieldsByBitmap(data) + } if f.spec.Tag.Enc != nil { return f.unpackSubfieldsByTag(data) } @@ -442,21 +512,63 @@ func (f *Composite) unpackSubfieldsByTag(data []byte) (int, error) { return offset, nil } +func (f *Composite) unpackSubfieldsByBitmap(data []byte) (int, error) { + var off int + + // reset fields that were set + f.setSubfields = map[string]struct{}{} + + // unpack Bitmap + read, err := f.Bitmap().Unpack(data[off:]) + if err != nil { + return 0, fmt.Errorf("failed to unpack bitmap: %w", err) + } + + off += read + + for i := 1; i <= f.Bitmap().Len(); i++ { + if f.Bitmap().IsSet(i) { + iStr := fmt.Sprintf("%v", i) + + fl, ok := f.subfields[iStr] + if !ok { + return 0, fmt.Errorf("failed to unpack field %v: no specification found", iStr) + } + + read, err = fl.Unpack(data[off:]) + if err != nil { + return 0, fmt.Errorf("failed to unpack field %v (%s): %w", iStr, fl.Spec().Description, err) + } + + f.setSubfields[iStr] = struct{}{} + + off += read + } + } + + return off, nil +} + func validateCompositeSpec(spec *Spec) error { - if spec.Tag == nil || spec.Tag.Sort == nil { - return fmt.Errorf("Composite spec requires a Tag.Sort function to be defined") + var bm *Bitmap + if value, ok := spec.Subfields[compositeBitmapKey]; ok { + bm = value.(*Bitmap) } - if spec.Pad != nil && spec.Pad != padding.None { - return fmt.Errorf("Composite spec only supports nil or None spec padding values") + + if (spec.Tag == nil || spec.Tag.Sort == nil) && bm == nil { + return fmt.Errorf("Composite spec requires a Tag.Sort function or a BitmapSpec to be defined") } - if spec.Enc != nil { - return fmt.Errorf("Composite spec only supports a nil Enc value") + if spec.Tag != nil && spec.Tag.Sort != nil && bm != nil { + return fmt.Errorf("Composite spec doesn't support Tag.Sort function when a BitmapSpec is defined") } if spec.Tag != nil && spec.Tag.Enc == nil && spec.Tag.Length > 0 { return fmt.Errorf("Composite spec requires a Tag.Enc to be defined if Tag.Length > 0") } - if spec.Tag.Sort == nil { - return fmt.Errorf("Composite spec requires a Tag.Sort function to be defined") + if spec.Pad != nil && spec.Pad != padding.None { + return fmt.Errorf("Composite spec only supports nil or None spec padding values") + } + if spec.Enc != nil { + return fmt.Errorf("Composite spec only supports a nil Enc value") } return nil } @@ -488,3 +600,21 @@ func getFieldIndexOrTag(field reflect.StructField) (string, error) { return "", nil } + +func (f *Composite) packableFieldIDs() ([]string, error) { + // Index 0 represent bitmap which is always populated. + populatedFieldIDs := []string{compositeBitmapKey} + + for id := range f.setSubfields { + // represents the bitmap + if id == compositeBitmapKey { + continue + } + + populatedFieldIDs = append(populatedFieldIDs, id) + } + + sort.StringsByInt(populatedFieldIDs) + + return populatedFieldIDs, nil +} diff --git a/field/composite_test.go b/field/composite_test.go index d2c9cba8..44e7076c 100644 --- a/field/composite_test.go +++ b/field/composite_test.go @@ -979,8 +979,8 @@ func TestCompositePanicsOnSpecValidationFailures(t *testing.T) { spec *Spec }{ { - desc: "panics on nil Tag being defined in spec", - err: "Composite spec requires a Tag.Sort function to be defined", + desc: "panics on nil Tag and no Bitmap being defined in spec", + err: "Composite spec requires a Tag.Sort function or a BitmapSpec to be defined", spec: &Spec{ Length: 6, Pref: prefix.ASCII.Fixed, @@ -989,8 +989,8 @@ func TestCompositePanicsOnSpecValidationFailures(t *testing.T) { }, }, { - desc: "panics on nil Tag.Sort being defined in spec", - err: "Composite spec requires a Tag.Sort function to be defined", + desc: "panics on nil Tag and no Bitmap being defined in spec", + err: "Composite spec requires a Tag.Sort function or a BitmapSpec to be defined", spec: &Spec{ Length: 6, Pref: prefix.ASCII.Fixed,