diff --git a/exp_test.go b/exp_test.go new file mode 100644 index 0000000..4e88c61 --- /dev/null +++ b/exp_test.go @@ -0,0 +1,206 @@ +package iso8583_test + +import ( + "testing" + + "github.com/moov-io/iso8583" + "github.com/moov-io/iso8583/field" + "github.com/moov-io/iso8583/specs" + "github.com/stretchr/testify/require" +) + +func TestStructWithTypes(t *testing.T) { + t.Run("pack", func(t *testing.T) { + panInt := 4242424242424242 + panStr := "4242424242424242" + + tests := []struct { + name string + input interface{} + expectedPackedString string + }{ + // Tests for string type + { + name: "struct with string type and value set", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber string `index:"2"` + }{ + MTI: "0110", + PrimaryAccountNumber: panStr, + }, + expectedPackedString: "011040000000000000000000000000000000164242424242424242", + }, + { + name: "struct with string type and no value", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber string `index:"2"` + }{ + MTI: "0110", + }, + expectedPackedString: "011000000000000000000000000000000000", + }, + { + name: "struct with string type, no value and keepzero tag - length prefix is set to 0 and no value is following", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber string `index:"2,keepzero"` + }{ + MTI: "0110", + }, + expectedPackedString: "01104000000000000000000000000000000000", + }, + + // Tests for *string type + { + name: "struct with *string type and value set", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber *string `index:"2"` + }{ + MTI: "0110", + PrimaryAccountNumber: &panStr, + }, + expectedPackedString: "011040000000000000000000000000000000164242424242424242", + }, + { + name: "struct with *string type and no value", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber *string `index:"2"` + }{ + MTI: "0110", + }, + expectedPackedString: "011000000000000000000000000000000000", + }, + { + name: "struct with *string type, no value and keepzero tag", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber *string `index:"2,keepzero"` + }{ + MTI: "0110", + }, + expectedPackedString: "011000000000000000000000000000000000", + }, + + // Tests for int type + { + name: "struct with int type and value set", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber int `index:"2"` + }{ + MTI: "0110", + PrimaryAccountNumber: panInt, + }, + expectedPackedString: "011040000000000000000000000000000000164242424242424242", + }, + { + name: "struct with int type and no value", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber int `index:"2"` + }{ + MTI: "0110", + }, + expectedPackedString: "011000000000000000000000000000000000", + }, + { + name: "struct with int type, no value and keepzero tag", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber int `index:"2,keepzero"` + }{ + MTI: "0110", + }, + expectedPackedString: "011040000000000000000000000000000000010", + }, + + // Tests for *int type + { + name: "struct with *int type and value set", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber *int `index:"2"` + }{ + MTI: "0110", + PrimaryAccountNumber: &panInt, + }, + expectedPackedString: "011040000000000000000000000000000000164242424242424242", + }, + { + name: "struct with *int type and no value", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber *int `index:"2"` + }{ + MTI: "0110", + }, + expectedPackedString: "011000000000000000000000000000000000", + }, + { + name: "struct with *int type, no value and keepzero tag", + input: struct { + MTI string `index:"0"` + PrimaryAccountNumber *int `index:"2,keepzero"` + }{ + MTI: "0110", + }, + expectedPackedString: "011000000000000000000000000000000000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := iso8583.NewMessage(specs.Spec87ASCII) + err := message.Marshal(tt.input) + require.NoError(t, err) + + packed, err := message.Pack() + require.NoError(t, err) + + require.Equal(t, tt.expectedPackedString, string(packed)) + }) + } + }) + + t.Run("unpack", func(t *testing.T) { + type authRequest struct { + MTI string `index:"0"` + PrimaryAccountNumber string `index:"2"` + } + + packed := []byte("011040000000000000000000000000000000164242424242424242") + + message := iso8583.NewMessage(specs.Spec87ASCII) + err := message.Unpack(packed) + require.NoError(t, err) + + data := authRequest{} + err = message.Unmarshal(&data) + require.NoError(t, err) + require.Equal(t, "0110", data.MTI) + require.Equal(t, "4242424242424242", data.PrimaryAccountNumber) + }) + + t.Run("unpack2", func(t *testing.T) { + type authRequest struct { + MTI *string `index:"0"` + PrimaryAccountNumber *field.String `index:"2"` + } + + packed := []byte("011040000000000000000000000000000000164242424242424242") + + message := iso8583.NewMessage(specs.Spec87ASCII) + err := message.Unpack(packed) + require.NoError(t, err) + + data := authRequest{} + err = message.Unmarshal(&data) + require.NoError(t, err) + require.Equal(t, "0110", *data.MTI) + require.Equal(t, "4242424242424242", data.PrimaryAccountNumber.Value()) + }) +} diff --git a/field/composite.go b/field/composite.go index 2edeea5..d3fc9b3 100644 --- a/field/composite.go +++ b/field/composite.go @@ -623,12 +623,17 @@ var fieldNameTagRe = regexp.MustCompile(`^F.+$`) // field name. If it does not match F.+ pattern, it checks value of `index` // tag. If empty string, then index/tag was not found for the field. func getFieldIndexOrTag(field reflect.StructField) (string, error) { - dataFieldName := field.Name - - if fieldIndex := field.Tag.Get("index"); fieldIndex != "" { - return fieldIndex, nil + var fieldIndex string + // keep the order of tags for now, when index tag is deprecated we can + // change the order + for _, tag := range []string{"index", "iso8583"} { + if fieldIndex = field.Tag.Get(tag); fieldIndex != "" { + return fieldIndex, nil + } } + dataFieldName := field.Name + if len(dataFieldName) > 0 && fieldNameTagRe.MatchString(dataFieldName) { return dataFieldName[1:], nil } diff --git a/field/composite_test.go b/field/composite_test.go index 874273a..f789adb 100644 --- a/field/composite_test.go +++ b/field/composite_test.go @@ -595,7 +595,7 @@ func TestCompositePacking(t *testing.T) { }) require.Error(t, err) - require.EqualError(t, err, "failed to set data from field 1: data does not match required *String type") + require.Contains(t, err.Error(), "failed to set data from field 1: data does not match required *String") }) t.Run("Pack returns error on failure of subfield packing", func(t *testing.T) { @@ -745,7 +745,7 @@ func TestCompositePacking(t *testing.T) { err = composite.Unmarshal(data) require.Error(t, err) - require.EqualError(t, err, "failed to get data from field 1: data does not match required *String type") + require.Contains(t, err.Error(), "failed to get data from field 1: data does not match required *String") }) t.Run("Unpack returns an error on failure of subfield to unpack bytes", func(t *testing.T) { diff --git a/field/numeric.go b/field/numeric.go index 18e4193..4e11992 100644 --- a/field/numeric.go +++ b/field/numeric.go @@ -16,7 +16,6 @@ var _ json.Unmarshaler = (*Numeric)(nil) type Numeric struct { value int spec *Spec - data *Numeric } func NewNumeric(spec *Spec) *Numeric { @@ -54,9 +53,6 @@ func (f *Numeric) SetBytes(b []byte) error { f.value = val } - if f.data != nil { - *(f.data) = *f - } return nil } @@ -143,19 +139,45 @@ func (f *Numeric) Unmarshal(v interface{}) error { } func (f *Numeric) SetData(data interface{}) error { - if data == nil { - return nil - } + switch v := data.(type) { + case *Numeric: + if v == nil { + return nil + } + f.value = v.value + case int: + f.value = v + case *int: + if v == nil { + f.value = 0 + return nil + } + f.value = *v + case string: + if v == "" { + f.value = 0 + return nil + } + val, err := strconv.Atoi(v) + if err != nil { + return utils.NewSafeError(err, "failed to convert sting value into number") + } + f.value = val + case *string: + if v == nil { + f.value = 0 + return nil + } - num, ok := data.(*Numeric) - if !ok { - return fmt.Errorf("data does not match required *Numeric type") + val, err := strconv.Atoi(*v) + if err != nil { + return utils.NewSafeError(err, "failed to convert sting value into number") + } + f.value = val + default: + return fmt.Errorf("data does not match require *Numeric or supported numeric types (int, *int, string, *string)") } - f.data = num - if num.value != 0 { - f.value = num.value - } return nil } diff --git a/field/numeric_test.go b/field/numeric_test.go index c2ac8f8..05e236d 100644 --- a/field/numeric_test.go +++ b/field/numeric_test.go @@ -42,17 +42,8 @@ func TestNumericField(t *testing.T) { require.NoError(t, err) require.Equal(t, " 9876", string(packed)) - numeric = NewNumeric(spec) - data := NewNumericValue(0) - numeric.SetData(data) - length, err = numeric.Unpack([]byte(" 9876")) - require.NoError(t, err) - require.Equal(t, 10, length) - require.Equal(t, 9876, data.Value()) - numeric = NewNumeric(spec) numeric.SetValue(9876) - require.Equal(t, 9876, numeric.Value()) } @@ -151,18 +142,9 @@ func TestNumericFieldZeroLeftPaddedZero(t *testing.T) { } func TestNumericSetBytesSetsDataOntoDataStruct(t *testing.T) { - numeric := NewNumeric(&Spec{ - Length: 1, - Description: "Field", - Enc: encoding.ASCII, - Pref: prefix.ASCII.Fixed, - }) - data := &Numeric{} - err := numeric.SetData(data) - require.NoError(t, err) - err = numeric.SetBytes([]byte("9")) + err := data.SetBytes([]byte("9")) require.NoError(t, err) require.Equal(t, 9, data.Value()) diff --git a/field/string.go b/field/string.go index 94a1a8a..598712d 100644 --- a/field/string.go +++ b/field/string.go @@ -2,8 +2,9 @@ package field import ( "encoding/json" - "errors" "fmt" + "reflect" + "strconv" "github.com/moov-io/iso8583/utils" ) @@ -15,7 +16,6 @@ var _ json.Unmarshaler = (*String)(nil) type String struct { value string spec *Spec - data *String } func NewString(spec *Spec) *String { @@ -40,9 +40,6 @@ func (f *String) SetSpec(spec *Spec) { func (f *String) SetBytes(b []byte) error { f.value = string(b) - if f.data != nil { - *(f.data) = *f - } return nil } @@ -114,34 +111,63 @@ func (f *String) Unpack(data []byte) (int, error) { } func (f *String) Unmarshal(v interface{}) error { - if v == nil { - return nil - } - - str, ok := v.(*String) - if !ok { - return errors.New("data does not match required *String type") + switch val := v.(type) { + case reflect.Value: + switch val.Kind() { + case reflect.String: + val.SetString(f.value) + case reflect.Int: + i, err := strconv.Atoi(f.value) + if err != nil { + return fmt.Errorf("failed to convert string to int: %w", err) + } + val.SetInt(int64(i)) + default: + return fmt.Errorf("data does not match required reflect.Value type") + } + case *string: + *val = f.value + case *int: + i, err := strconv.Atoi(f.value) + if err != nil { + return fmt.Errorf("failed to convert string to int: %w", err) + } + *val = i + case *String: + val.value = f.value + default: + return fmt.Errorf("data does not match required *String or *string type") } - str.value = f.value - return nil } func (f *String) SetData(data interface{}) error { - if data == nil { - return nil + switch v := data.(type) { + case *String: + if v == nil { + return nil + } + f.value = v.value + case string: + if v == "" { + return nil + } + f.value = v + case *string: + if v == nil { + f.value = "" + return nil + } + f.value = *v + case int: + f.value = strconv.FormatInt(int64(v), 10) + case *int: + f.value = strconv.FormatInt(int64(*v), 10) + default: + return fmt.Errorf("data does not match required *String or (string, *string, int, *int) type") } - str, ok := data.(*String) - if !ok { - return fmt.Errorf("data does not match required *String type") - } - - f.data = str - if str.value != "" { - f.value = str.value - } return nil } diff --git a/field/string_test.go b/field/string_test.go index f2537d3..d0b995b 100644 --- a/field/string_test.go +++ b/field/string_test.go @@ -43,24 +43,8 @@ func TestStringField(t *testing.T) { require.Equal(t, " hello", string(packed)) str = NewString(spec) - data := NewStringValue("") - str.SetData(data) - length, err = str.Unpack([]byte(" olleh")) - require.NoError(t, err) - require.Equal(t, 10, length) - require.Equal(t, "olleh", data.Value()) - - str = NewString(spec) - data = &String{} - str.SetData(data) - err = str.SetBytes([]byte("hello")) - require.NoError(t, err) - require.Equal(t, "hello", data.Value()) - - str = NewString(spec) - str.SetValue("hello") - require.Equal(t, "hello", data.Value()) + require.Equal(t, "hello", str.Value()) } func TestStringNil(t *testing.T) { diff --git a/field_tag.go b/field_tag.go new file mode 100644 index 0000000..4601352 --- /dev/null +++ b/field_tag.go @@ -0,0 +1,93 @@ +package iso8583 + +import ( + "reflect" + "strconv" + "strings" +) + +type FieldTag struct { + Id int // is -1 if index is not a number + Index string + + // KeepZero tells the marshaler to use zero value and set bitmap bit to + // 1 for this field. Default behavior is to omit the field from the + // message if it's zero value. + KeepZero bool +} + +func NewFieldTag(field reflect.StructField) FieldTag { + // value of the key "index" in the tag + var value string + + // keep the order of tags for now, when index tag is deprecated we can + // change the order + for _, tag := range []string{"index", "iso8583"} { + if value = field.Tag.Get(tag); value != "" { + break + } + } + + // format of the value is "id[,keep_zero_value]" + // id is the id of the field + // let's parse it + if value != "" { + index, keepZero := parseValue(value) + + id, err := strconv.Atoi(index) + if err != nil { + id = -1 + } + + return FieldTag{ + Id: id, + Index: index, + KeepZero: keepZero, + } + } + + dataFieldName := field.Name + if len(dataFieldName) > 0 && fieldNameIndexRe.MatchString(dataFieldName) { + indexStr := dataFieldName[1:] + fieldIndex, err := strconv.Atoi(indexStr) + if err != nil { + return FieldTag{ + Id: -1, + Index: indexStr, + } + } + + return FieldTag{ + Id: fieldIndex, + Index: indexStr, + } + } + + return FieldTag{ + Id: -1, + } +} + +func parseValue(value string) (index string, keepZero bool) { + if value == "" { + return + } + + // split the value by comma + values := strings.Split(value, ",") + + // the first value is the index + index = values[0] + + // if there is only one value, return + if len(values) == 1 { + return + } + + // if the second value is "keep_zero_value", set the flag + if values[1] == "keepzero" { + keepZero = true + } + + return +} diff --git a/message.go b/message.go index 51c7a47..59bb884 100644 --- a/message.go +++ b/message.go @@ -354,34 +354,41 @@ func (m *Message) Marshal(v interface{}) error { // iterate over struct fields for i := 0; i < dataStruct.NumField(); i++ { - fieldIndex, err := getFieldIndex(dataStruct.Type().Field(i)) - if err != nil { - return fmt.Errorf("getting field %d index: %w", i, err) - } + fieldTag := NewFieldTag(dataStruct.Type().Field(i)) - // skip field without index - if fieldIndex < 0 { + // skip field without index or if index in tag is not defined + if fieldTag.Id < 0 { continue } - messageField := m.GetField(fieldIndex) + messageField := m.GetField(fieldTag.Id) // if struct field we are usgin to populate value expects to // set index of the field that is not described by spec if messageField == nil { - return fmt.Errorf("no message field defined by spec with index: %d", fieldIndex) + return fmt.Errorf("no message field defined by spec with index: %d", fieldTag.Id) } dataField := dataStruct.Field(i) - if dataField.IsNil() { + + // for pointer fields we need to check if they are nil + // and if they are we need to skip them, to not set bitmap bit + // and not to set value to the field + if dataField.Kind() == reflect.Pointer && dataField.IsZero() { continue } - err = messageField.Marshal(dataField.Interface()) + // for non pointer fields we need to check if they are zero + // and we want to skip them (as specified in the field tag) + if dataField.IsZero() && !fieldTag.KeepZero { + continue + } + + err := messageField.Marshal(dataField.Interface()) if err != nil { - return fmt.Errorf("failed to set value to field %d: %w", fieldIndex, err) + return fmt.Errorf("failed to set value to field %d: %w", fieldTag.Id, err) } - m.fieldsMap[fieldIndex] = struct{}{} + m.fieldsMap[fieldTag.Id] = struct{}{} } return nil @@ -426,11 +433,19 @@ func (m *Message) Unmarshal(v interface{}) error { } dataField := dataStruct.Field(i) - if dataField.IsNil() { - dataField.Set(reflect.New(dataField.Type().Elem())) - } - err = messageField.Unmarshal(dataField.Interface()) + // if field is pointer we will pass pointer to the field + if dataField.Kind() == reflect.Ptr { + if dataField.IsZero() { + fmt.Println("is zero") + dataField.Set(reflect.New(dataField.Type().Elem())) + } + + err = messageField.Unmarshal(dataField.Interface()) + } else { + // if field is not pointer we will pass the field as reflect.Value + err = messageField.Unmarshal(dataField) + } if err != nil { return fmt.Errorf("failed to get value from field %d: %w", fieldIndex, err) }