Skip to content

Commit c352ad7

Browse files
authored
allow to Unset field of the message and composite (#332)
* allow to Unset field of the message and composite * add one more test
1 parent 8c09e82 commit c352ad7

File tree

4 files changed

+275
-8
lines changed

4 files changed

+275
-8
lines changed

field/composite.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"math"
88
"reflect"
99
"strconv"
10+
"strings"
1011
"sync"
1112

1213
"github.com/moov-io/iso8583/encoding"
@@ -687,3 +688,54 @@ func orderedKeys(kvs map[string]Field, sorter sort.StringSlice) []string {
687688
sorter(keys)
688689
return keys
689690
}
691+
692+
// UnsetSubfield marks the subfield with the given ID as not set and replaces it
693+
// with a new zero-valued field. This effectively removes the subfield's value and
694+
// excludes it from operations like Pack() or Marshal().
695+
func (m *Composite) UnsetSubfield(id string) {
696+
m.mu.Lock()
697+
defer m.mu.Unlock()
698+
699+
// unset the field
700+
delete(m.setSubfields, id)
701+
702+
// we should re-create the subfield to reset its value (and its subfields)
703+
m.subfields[id] = CreateSubfield(m.Spec().Subfields[id])
704+
}
705+
706+
// UnsetSubfields marks multiple subfields identified by their paths as not set and
707+
// replaces them with new zero-valued fields. Each path should be in the format
708+
// "a.b.c". This effectively removes the subfields' values and excludes them from
709+
// operations like Pack() or Marshal().
710+
func (m *Composite) UnsetSubfields(idPaths ...string) error {
711+
for _, idPath := range idPaths {
712+
if idPath == "" {
713+
continue
714+
}
715+
716+
id, path, _ := strings.Cut(idPath, ".")
717+
718+
if _, ok := m.setSubfields[id]; ok {
719+
if len(path) == 0 {
720+
m.UnsetSubfield(id)
721+
continue
722+
}
723+
724+
f := m.subfields[id]
725+
if f == nil {
726+
return fmt.Errorf("subfield %s does not exist", id)
727+
}
728+
729+
composite, ok := f.(*Composite)
730+
if !ok {
731+
return fmt.Errorf("field %s is not a composite field and its subfields %s cannot be unset", id, path)
732+
}
733+
734+
if err := composite.UnsetSubfields(path); err != nil {
735+
return fmt.Errorf("failed to unset %s in composite field %s: %w", path, id, err)
736+
}
737+
}
738+
}
739+
740+
return nil
741+
}

field/composite_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,11 @@ var (
281281
Enc: encoding.Binary,
282282
Pref: prefix.BerTLV,
283283
}),
284+
"9F02": NewHex(&Spec{
285+
Description: "Amount, Authorized (Numeric)",
286+
Enc: encoding.Binary,
287+
Pref: prefix.BerTLV,
288+
}),
284289
},
285290
}),
286291
},
@@ -348,6 +353,7 @@ type ConstructedTLVTestData struct {
348353

349354
type SubConstructedTLVTestData struct {
350355
F9F45 *Hex
356+
F9F02 *Hex
351357
}
352358

353359
func TestCompositeField_Marshal(t *testing.T) {
@@ -481,6 +487,90 @@ func TestCompositeField_Unmarshal(t *testing.T) {
481487
})
482488
}
483489

490+
func TestCompositeField_Unset(t *testing.T) {
491+
t.Run("Unset creates new empty field when it deletes it", func(t *testing.T) {
492+
composite := NewComposite(constructedBERTLVTestSpec)
493+
err := composite.Marshal(&ConstructedTLVTestData{
494+
F82: NewHexValue("017F"),
495+
F9F36: NewHexValue("027F"),
496+
F9F3B: &SubConstructedTLVTestData{
497+
F9F45: NewHexValue("047F"),
498+
F9F02: NewHexValue("057F"),
499+
},
500+
})
501+
require.NoError(t, err)
502+
503+
data := &ConstructedTLVTestData{}
504+
require.NoError(t, composite.Unmarshal(data))
505+
506+
// all fields are set
507+
require.Equal(t, "017F", data.F82.Value())
508+
require.Equal(t, "027F", data.F9F36.Value())
509+
require.Equal(t, "047F", data.F9F3B.F9F45.Value())
510+
require.Equal(t, "057F", data.F9F3B.F9F02.Value())
511+
512+
// if we delete subfield F9F3B and then set only one field of it,
513+
// the other field should be nil (not set)
514+
require.NoError(t, composite.UnsetSubfields("9F3B"))
515+
516+
data = &ConstructedTLVTestData{}
517+
require.NoError(t, composite.Unmarshal(data))
518+
519+
require.Equal(t, "017F", data.F82.Value())
520+
require.Equal(t, "027F", data.F9F36.Value())
521+
require.Nil(t, data.F9F3B) // F9F3B should be nil as it was unset / deleted
522+
523+
// if we set only one field of subfield F9F3B, the other field should be nil (not set)
524+
err = composite.Marshal(&ConstructedTLVTestData{
525+
F9F3B: &SubConstructedTLVTestData{
526+
F9F45: NewHexValue("047F"),
527+
},
528+
})
529+
require.NoError(t, err)
530+
531+
data = &ConstructedTLVTestData{}
532+
require.NoError(t, composite.Unmarshal(data))
533+
534+
require.Equal(t, "017F", data.F82.Value())
535+
require.Equal(t, "027F", data.F9F36.Value())
536+
require.Equal(t, "047F", data.F9F3B.F9F45.Value())
537+
require.Nil(t, data.F9F3B.F9F02) // F9F02 should be nil as it was not set
538+
})
539+
540+
t.Run("Unset sets all fields of composite field to nil", func(t *testing.T) {
541+
composite := NewComposite(constructedBERTLVTestSpec)
542+
err := composite.Marshal(&ConstructedTLVTestData{
543+
F82: NewHexValue("017F"),
544+
F9F36: NewHexValue("027F"),
545+
F9F3B: &SubConstructedTLVTestData{
546+
F9F45: NewHexValue("047F"),
547+
},
548+
})
549+
require.NoError(t, err)
550+
551+
data := &ConstructedTLVTestData{}
552+
err = composite.Unmarshal(data)
553+
require.NoError(t, err)
554+
555+
// all fields are set
556+
require.Equal(t, "017F", data.F82.Value())
557+
require.Equal(t, "027F", data.F9F36.Value())
558+
require.Equal(t, "047F", data.F9F3B.F9F45.Value())
559+
560+
// unset the composite fields
561+
err = composite.UnsetSubfields("82", "9F3B.9F45")
562+
require.NoError(t, err)
563+
564+
data = &ConstructedTLVTestData{}
565+
err = composite.Unmarshal(data)
566+
require.NoError(t, err)
567+
568+
require.Nil(t, data.F82)
569+
require.Equal(t, "027F", data.F9F36.Value())
570+
require.Nil(t, data.F9F3B.F9F45)
571+
})
572+
}
573+
484574
func TestTLVPacking(t *testing.T) {
485575
t.Run("Pack correctly serializes data to bytes (general tlv)", func(t *testing.T) {
486576
data := &TLVTestData{

message.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"reflect"
88
"sort"
99
"strconv"
10+
"strings"
1011
"sync"
1112

1213
iso8583errors "github.com/moov-io/iso8583/errors"
@@ -516,3 +517,60 @@ func (m *Message) Unmarshal(v interface{}) error {
516517

517518
return nil
518519
}
520+
521+
// UnsetField marks the field with the given ID as not set and replaces it with
522+
// a new zero-valued field. This effectively removes the field's value and excludes
523+
// it from operations like Pack() or Marshal().
524+
func (m *Message) UnsetField(id int) {
525+
m.mu.Lock()
526+
defer m.mu.Unlock()
527+
528+
if _, ok := m.fieldsMap[id]; ok {
529+
delete(m.fieldsMap, id)
530+
// re-create the field to reset its value (and subfields if it's a composite field)
531+
if fieldSpec, ok := m.GetSpec().Fields[id]; ok {
532+
m.fields[id] = createMessageField(fieldSpec)
533+
}
534+
}
535+
}
536+
537+
// UnsetFields marks multiple fields identified by their paths as not set and
538+
// replaces them with new zero-valued fields. Each path should be in the format
539+
// "a.b.c". This effectively removes the fields' values and excludes them from
540+
// operations like Pack() or Marshal().
541+
func (m *Message) UnsetFields(idPaths ...string) error {
542+
for _, idPath := range idPaths {
543+
if idPath == "" {
544+
continue
545+
}
546+
547+
id, path, _ := strings.Cut(idPath, ".")
548+
idx, err := strconv.Atoi(id)
549+
if err != nil {
550+
return fmt.Errorf("conversion of %s to int failed: %w", id, err)
551+
}
552+
553+
if _, ok := m.fieldsMap[idx]; ok {
554+
if len(path) == 0 {
555+
m.UnsetField(idx)
556+
continue
557+
}
558+
559+
f := m.fields[idx]
560+
if f == nil {
561+
return fmt.Errorf("field %d does not exist", idx)
562+
}
563+
564+
composite, ok := f.(*field.Composite)
565+
if !ok {
566+
return fmt.Errorf("field %d is not a composite field and its subfields %s cannot be unset", idx, path)
567+
}
568+
569+
if err := composite.UnsetSubfields(path); err != nil {
570+
return fmt.Errorf("failed to unset %s in composite field %d: %w", path, idx, err)
571+
}
572+
}
573+
}
574+
575+
return nil
576+
}

message_test.go

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ func TestMessage(t *testing.T) {
368368
require.Equal(t, wantMsg, rawMsg)
369369
})
370370

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

398-
// clone the message and reset some fields
398+
// clone the message and reset some values
399399
clone, err := message.Clone()
400400
require.NoError(t, err)
401401

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

427-
// check that fields are set
428-
require.NotNil(t, data.F2)
429-
require.NotNil(t, data.F3)
430-
require.NotNil(t, data.F3.F2)
431-
432427
// check the zero values
433428
require.Equal(t, "", data.F2.Value())
434429
require.Equal(t, "", data.F3.F2.Value())
@@ -440,6 +435,78 @@ func TestMessage(t *testing.T) {
440435
require.Equal(t, "100", data.F4.Value())
441436
})
442437

438+
t.Run("Unset doesn't return error for fields that are not set", func(t *testing.T) {
439+
message := NewMessage(spec)
440+
err := message.UnsetFields("2", "3", "4")
441+
require.NoError(t, err)
442+
})
443+
444+
t.Run("Unset unsets fields", func(t *testing.T) {
445+
type TestISOF3Data struct {
446+
F1 *field.String
447+
F2 *field.String
448+
F3 *field.String
449+
}
450+
451+
type ISO87Data struct {
452+
F0 *field.String
453+
F2 *field.String
454+
F3 *TestISOF3Data
455+
F4 *field.String
456+
}
457+
458+
messageCode := "0100"
459+
message := NewMessage(spec)
460+
err := message.Marshal(&ISO87Data{
461+
F0: field.NewStringValue(messageCode),
462+
F2: field.NewStringValue("4242424242424242"),
463+
F3: &TestISOF3Data{
464+
F1: field.NewStringValue("12"),
465+
F2: field.NewStringValue("34"),
466+
F3: field.NewStringValue("56"),
467+
},
468+
F4: field.NewStringValue("100"),
469+
})
470+
require.NoError(t, err)
471+
472+
// unset fields
473+
err = message.UnsetFields("2", "3.3")
474+
require.NoError(t, err)
475+
476+
data := &ISO87Data{}
477+
err = message.Unmarshal(data)
478+
require.NoError(t, err)
479+
480+
require.Nil(t, data.F2)
481+
require.Nil(t, data.F3.F3)
482+
483+
// unset field 3
484+
err = message.UnsetFields("3")
485+
require.NoError(t, err)
486+
487+
data = &ISO87Data{}
488+
err = message.Unmarshal(data)
489+
require.NoError(t, err)
490+
491+
require.Nil(t, data.F3)
492+
493+
// let's set the field 3.3 again
494+
// only subfield 3 should be set in the field 3, the rest should be unset
495+
err = message.Marshal(&ISO87Data{
496+
F3: &TestISOF3Data{
497+
F3: field.NewStringValue("56"),
498+
},
499+
})
500+
require.NoError(t, err)
501+
502+
data = &ISO87Data{}
503+
err = message.Unmarshal(data)
504+
require.NoError(t, err)
505+
506+
require.Nil(t, data.F3.F1)
507+
require.Nil(t, data.F3.F2)
508+
require.Equal(t, "56", data.F3.F3.Value())
509+
})
443510
}
444511

445512
func TestPackUnpack(t *testing.T) {

0 commit comments

Comments
 (0)