Skip to content

Commit

Permalink
allow to Unset field of the message and composite (#332)
Browse files Browse the repository at this point in the history
* allow to Unset field of the message and composite

* add one more test
  • Loading branch information
alovak authored Oct 28, 2024
1 parent 8c09e82 commit c352ad7
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 8 deletions.
52 changes: 52 additions & 0 deletions field/composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"math"
"reflect"
"strconv"
"strings"
"sync"

"github.com/moov-io/iso8583/encoding"
Expand Down Expand Up @@ -687,3 +688,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
}
90 changes: 90 additions & 0 deletions field/composite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,11 @@ var (
Enc: encoding.Binary,
Pref: prefix.BerTLV,
}),
"9F02": NewHex(&Spec{
Description: "Amount, Authorized (Numeric)",
Enc: encoding.Binary,
Pref: prefix.BerTLV,
}),
},
}),
},
Expand Down Expand Up @@ -348,6 +353,7 @@ type ConstructedTLVTestData struct {

type SubConstructedTLVTestData struct {
F9F45 *Hex
F9F02 *Hex
}

func TestCompositeField_Marshal(t *testing.T) {
Expand Down Expand Up @@ -481,6 +487,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{
Expand Down
58 changes: 58 additions & 0 deletions message.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"reflect"
"sort"
"strconv"
"strings"
"sync"

iso8583errors "github.com/moov-io/iso8583/errors"
Expand Down Expand Up @@ -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
}
83 changes: 75 additions & 8 deletions message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func TestMessage(t *testing.T) {
require.Equal(t, wantMsg, rawMsg)
})

t.Run("Clone, set zero values and reset fields", func(t *testing.T) {
t.Run("Clone, set zero values", func(t *testing.T) {
type TestISOF3Data struct {
F1 *field.String
F2 *field.String
Expand All @@ -395,11 +395,11 @@ func TestMessage(t *testing.T) {
})
require.NoError(t, err)

// clone the message and reset some fields
// clone the message and reset some values
clone, err := message.Clone()
require.NoError(t, err)

// reset the fields
// reset the values
// first, check that the fields are set
data := &ISO87Data{}
require.NoError(t, clone.Unmarshal(data))
Expand All @@ -424,11 +424,6 @@ func TestMessage(t *testing.T) {
data = &ISO87Data{}
require.NoError(t, clone.Unmarshal(data))

// check that fields are set
require.NotNil(t, data.F2)
require.NotNil(t, data.F3)
require.NotNil(t, data.F3.F2)

// check the zero values
require.Equal(t, "", data.F2.Value())
require.Equal(t, "", data.F3.F2.Value())
Expand All @@ -440,6 +435,78 @@ func TestMessage(t *testing.T) {
require.Equal(t, "100", data.F4.Value())
})

t.Run("Unset doesn't return error for fields that are not set", func(t *testing.T) {
message := NewMessage(spec)
err := message.UnsetFields("2", "3", "4")
require.NoError(t, err)
})

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) {
Expand Down

0 comments on commit c352ad7

Please sign in to comment.