From 852111085a8e6f973b20d52b29d0f3904431ec45 Mon Sep 17 00:00:00 2001 From: Pavel Gabriel Date: Sat, 26 Oct 2024 17:02:37 +0200 Subject: [PATCH] allow to Unset field of the message and composite --- field/composite.go | 52 ++++++++++++++++++++++++ field/composite_test.go | 90 +++++++++++++++++++++++++++++++++++++++++ message.go | 58 ++++++++++++++++++++++++++ message_test.go | 67 ++++++++++++++++++++++++++++++ 4 files changed, 267 insertions(+) diff --git a/field/composite.go b/field/composite.go index 639e414..da73971 100644 --- a/field/composite.go +++ b/field/composite.go @@ -7,6 +7,7 @@ import ( "math" "reflect" "strconv" + "strings" "sync" "github.com/moov-io/iso8583/encoding" @@ -681,3 +682,54 @@ func orderedKeys(kvs map[string]Field, sorter sort.StringSlice) []string { sorter(keys) return keys } + +// UnsetSubfield marks the subfield with the given ID as not set and replaces it +// with a new zero-valued field. This effectively removes the subfield's value and +// excludes it from operations like Pack() or Marshal(). +func (m *Composite) UnsetSubfield(id string) { + m.mu.Lock() + defer m.mu.Unlock() + + // unset the field + delete(m.setSubfields, id) + + // we should re-create the subfield to reset its value (and its subfields) + m.subfields[id] = CreateSubfield(m.Spec().Subfields[id]) +} + +// UnsetSubfields marks multiple subfields identified by their paths as not set and +// replaces them with new zero-valued fields. Each path should be in the format +// "a.b.c". This effectively removes the subfields' values and excludes them from +// operations like Pack() or Marshal(). +func (m *Composite) UnsetSubfields(idPaths ...string) error { + for _, idPath := range idPaths { + if idPath == "" { + continue + } + + id, path, _ := strings.Cut(idPath, ".") + + if _, ok := m.setSubfields[id]; ok { + if len(path) == 0 { + m.UnsetSubfield(id) + continue + } + + f := m.subfields[id] + if f == nil { + return fmt.Errorf("subfield %s does not exist", id) + } + + composite, ok := f.(*Composite) + if !ok { + return fmt.Errorf("field %s is not a composite field and its subfields %s cannot be unset", id, path) + } + + if err := composite.UnsetSubfields(path); err != nil { + return fmt.Errorf("failed to unset %s in composite field %s: %w", path, id, err) + } + } + } + + return nil +} diff --git a/field/composite_test.go b/field/composite_test.go index 7651b88..ac7a026 100644 --- a/field/composite_test.go +++ b/field/composite_test.go @@ -281,6 +281,11 @@ var ( Enc: encoding.Binary, Pref: prefix.BerTLV, }), + "9F02": NewHex(&Spec{ + Description: "Amount, Authorized (Numeric)", + Enc: encoding.Binary, + Pref: prefix.BerTLV, + }), }, }), }, @@ -348,6 +353,7 @@ type ConstructedTLVTestData struct { type SubConstructedTLVTestData struct { F9F45 *Hex + F9F02 *Hex } func TestCompositeField_Marshal(t *testing.T) { @@ -468,6 +474,90 @@ func TestCompositeField_Unmarshal(t *testing.T) { }) } +func TestCompositeField_Unset(t *testing.T) { + t.Run("Unset creates new empty field when it deletes it", func(t *testing.T) { + composite := NewComposite(constructedBERTLVTestSpec) + err := composite.Marshal(&ConstructedTLVTestData{ + F82: NewHexValue("017F"), + F9F36: NewHexValue("027F"), + F9F3B: &SubConstructedTLVTestData{ + F9F45: NewHexValue("047F"), + F9F02: NewHexValue("057F"), + }, + }) + require.NoError(t, err) + + data := &ConstructedTLVTestData{} + require.NoError(t, composite.Unmarshal(data)) + + // all fields are set + require.Equal(t, "017F", data.F82.Value()) + require.Equal(t, "027F", data.F9F36.Value()) + require.Equal(t, "047F", data.F9F3B.F9F45.Value()) + require.Equal(t, "057F", data.F9F3B.F9F02.Value()) + + // if we delete subfield F9F3B and then set only one field of it, + // the other field should be nil (not set) + require.NoError(t, composite.UnsetSubfields("9F3B")) + + data = &ConstructedTLVTestData{} + require.NoError(t, composite.Unmarshal(data)) + + require.Equal(t, "017F", data.F82.Value()) + require.Equal(t, "027F", data.F9F36.Value()) + require.Nil(t, data.F9F3B) // F9F3B should be nil as it was unset / deleted + + // if we set only one field of subfield F9F3B, the other field should be nil (not set) + err = composite.Marshal(&ConstructedTLVTestData{ + F9F3B: &SubConstructedTLVTestData{ + F9F45: NewHexValue("047F"), + }, + }) + require.NoError(t, err) + + data = &ConstructedTLVTestData{} + require.NoError(t, composite.Unmarshal(data)) + + require.Equal(t, "017F", data.F82.Value()) + require.Equal(t, "027F", data.F9F36.Value()) + require.Equal(t, "047F", data.F9F3B.F9F45.Value()) + require.Nil(t, data.F9F3B.F9F02) // F9F02 should be nil as it was not set + }) + + t.Run("Unset sets all fields of composite field to nil", func(t *testing.T) { + composite := NewComposite(constructedBERTLVTestSpec) + err := composite.Marshal(&ConstructedTLVTestData{ + F82: NewHexValue("017F"), + F9F36: NewHexValue("027F"), + F9F3B: &SubConstructedTLVTestData{ + F9F45: NewHexValue("047F"), + }, + }) + require.NoError(t, err) + + data := &ConstructedTLVTestData{} + err = composite.Unmarshal(data) + require.NoError(t, err) + + // all fields are set + require.Equal(t, "017F", data.F82.Value()) + require.Equal(t, "027F", data.F9F36.Value()) + require.Equal(t, "047F", data.F9F3B.F9F45.Value()) + + // unset the composite fields + err = composite.UnsetSubfields("82", "9F3B.9F45") + require.NoError(t, err) + + data = &ConstructedTLVTestData{} + err = composite.Unmarshal(data) + require.NoError(t, err) + + require.Nil(t, data.F82) + require.Equal(t, "027F", data.F9F36.Value()) + require.Nil(t, data.F9F3B.F9F45) + }) +} + func TestTLVPacking(t *testing.T) { t.Run("Pack correctly serializes data to bytes (general tlv)", func(t *testing.T) { data := &TLVTestData{ diff --git a/message.go b/message.go index 1a6640a..0b416aa 100644 --- a/message.go +++ b/message.go @@ -7,6 +7,7 @@ import ( "reflect" "sort" "strconv" + "strings" "sync" iso8583errors "github.com/moov-io/iso8583/errors" @@ -516,3 +517,60 @@ func (m *Message) Unmarshal(v interface{}) error { return nil } + +// UnsetField marks the field with the given ID as not set and replaces it with +// a new zero-valued field. This effectively removes the field's value and excludes +// it from operations like Pack() or Marshal(). +func (m *Message) UnsetField(id int) { + m.mu.Lock() + defer m.mu.Unlock() + + if _, ok := m.fieldsMap[id]; ok { + delete(m.fieldsMap, id) + // re-create the field to reset its value (and subfields if it's a composite field) + if fieldSpec, ok := m.GetSpec().Fields[id]; ok { + m.fields[id] = createMessageField(fieldSpec) + } + } +} + +// UnsetFields marks multiple fields identified by their paths as not set and +// replaces them with new zero-valued fields. Each path should be in the format +// "a.b.c". This effectively removes the fields' values and excludes them from +// operations like Pack() or Marshal(). +func (m *Message) UnsetFields(idPaths ...string) error { + for _, idPath := range idPaths { + if idPath == "" { + continue + } + + id, path, _ := strings.Cut(idPath, ".") + idx, err := strconv.Atoi(id) + if err != nil { + return fmt.Errorf("conversion of %s to int failed: %w", id, err) + } + + if _, ok := m.fieldsMap[idx]; ok { + if len(path) == 0 { + m.UnsetField(idx) + continue + } + + f := m.fields[idx] + if f == nil { + return fmt.Errorf("field %d does not exist", idx) + } + + composite, ok := f.(*field.Composite) + if !ok { + return fmt.Errorf("field %d is not a composite field and its subfields %s cannot be unset", idx, path) + } + + if err := composite.UnsetSubfields(path); err != nil { + return fmt.Errorf("failed to unset %s in composite field %d: %w", path, idx, err) + } + } + } + + return nil +} diff --git a/message_test.go b/message_test.go index 09fd185..1ae124a 100644 --- a/message_test.go +++ b/message_test.go @@ -367,6 +367,73 @@ func TestMessage(t *testing.T) { wantMsg := []byte("01007000000000000000164242424242424242123456000000000100") require.Equal(t, wantMsg, rawMsg) }) + + t.Run("Unset unsets fields", func(t *testing.T) { + type TestISOF3Data struct { + F1 *field.String + F2 *field.String + F3 *field.String + } + + type ISO87Data struct { + F0 *field.String + F2 *field.String + F3 *TestISOF3Data + F4 *field.String + } + + messageCode := "0100" + message := NewMessage(spec) + err := message.Marshal(&ISO87Data{ + F0: field.NewStringValue(messageCode), + F2: field.NewStringValue("4242424242424242"), + F3: &TestISOF3Data{ + F1: field.NewStringValue("12"), + F2: field.NewStringValue("34"), + F3: field.NewStringValue("56"), + }, + F4: field.NewStringValue("100"), + }) + require.NoError(t, err) + + // unset fields + err = message.UnsetFields("2", "3.3") + require.NoError(t, err) + + data := &ISO87Data{} + err = message.Unmarshal(data) + require.NoError(t, err) + + require.Nil(t, data.F2) + require.Nil(t, data.F3.F3) + + // unset field 3 + err = message.UnsetFields("3") + require.NoError(t, err) + + data = &ISO87Data{} + err = message.Unmarshal(data) + require.NoError(t, err) + + require.Nil(t, data.F3) + + // let's set the field 3.3 again + // only subfield 3 should be set in the field 3, the rest should be unset + err = message.Marshal(&ISO87Data{ + F3: &TestISOF3Data{ + F3: field.NewStringValue("56"), + }, + }) + require.NoError(t, err) + + data = &ISO87Data{} + err = message.Unmarshal(data) + require.NoError(t, err) + + require.Nil(t, data.F3.F1) + require.Nil(t, data.F3.F2) + require.Equal(t, "56", data.F3.F3.Value()) + }) } func TestPackUnpack(t *testing.T) {