diff --git a/exp_test.go b/exp_test.go index 4c750ca..f6d45b1 100644 --- a/exp_test.go +++ b/exp_test.go @@ -13,6 +13,7 @@ func TestStructWithTypes(t *testing.T) { MTI string `index:"0"` PrimaryAccountNumber string `index:"2"` ProcessingCode int `index:"3"` + TransactionAmount int `index:"4,keepzero"` // we will set message field value to 0 } t.Run("pack", func(t *testing.T) { @@ -29,6 +30,6 @@ func TestStructWithTypes(t *testing.T) { packed, err := message.Pack() require.NoError(t, err) - require.Equal(t, "011060000000000000000000000000000000164242424242424242200000", string(packed)) + require.Equal(t, "011070000000000000000000000000000000164242424242424242200000000000000000", string(packed)) }) } 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/numeric.go b/field/numeric.go index dcf5d30..3cf04d7 100644 --- a/field/numeric.go +++ b/field/numeric.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "reflect" + "math" "strconv" "github.com/moov-io/iso8583/utils" @@ -17,7 +17,6 @@ var _ json.Unmarshaler = (*Numeric)(nil) type Numeric struct { value int spec *Spec - data *Numeric } func NewNumeric(spec *Spec) *Numeric { @@ -55,9 +54,6 @@ func (f *Numeric) SetBytes(b []byte) error { f.value = val } - if f.data != nil { - *(f.data) = *f - } return nil } @@ -144,44 +140,32 @@ func (f *Numeric) Unmarshal(v interface{}) error { } func (f *Numeric) SetData(data interface{}) error { - if v, ok := data.(reflect.Value); ok { - switch v.Kind() { - case reflect.Int: - f.value = int(v.Int()) - case reflect.String: - val, err := strconv.Atoi(v.String()) - if err != nil { - return utils.NewSafeError(err, "failed to convert into number") - } - f.value = val - default: - return fmt.Errorf("data does not match required *Numeric type") + switch v := data.(type) { + case *Numeric: + if v == nil { + return nil } - return nil - } - - // if v.Kind() == reflect.Int { - // } - // if v, ok := data.(reflect.Value); ok { - // if v.CanSet() { - // v.Set(reflect.ValueOf(f.value)) - // return nil - // } - // } - - if data == nil { - return nil - } - - num, ok := data.(*Numeric) - if !ok { - return fmt.Errorf("data does not match required *Numeric type") + f.value = v.value + case int: + f.value = v + case int32: + if v >= math.MinInt32 && v <= math.MaxInt32 { + f.value = int(v) + } else { + return fmt.Errorf("int32 value out of range for int") + } + case int64: + if v >= math.MinInt64 && v <= math.MaxInt64 { + f.value = int(v) + } else { + return fmt.Errorf("int64 value out of range for int") + } + case []byte: + return f.SetBytes(v) + default: + return fmt.Errorf("data does not match require *Numeric or supported numeric types (int, int32, int64)") } - 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 4715440..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()) } diff --git a/field/string.go b/field/string.go index f68501b..29489e0 100644 --- a/field/string.go +++ b/field/string.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "reflect" + "strconv" "github.com/moov-io/iso8583/utils" ) @@ -16,7 +16,6 @@ var _ json.Unmarshaler = (*String)(nil) type String struct { value string spec *Spec - data *String } func NewString(spec *Spec) *String { @@ -41,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 } @@ -130,33 +126,27 @@ func (f *String) Unmarshal(v interface{}) error { } func (f *String) SetData(data interface{}) error { - if v, ok := data.(reflect.Value); ok { - switch v.Kind() { - case reflect.String: - f.value = v.String() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - f.value = fmt.Sprintf("%d", v.Int()) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - f.value = fmt.Sprintf("%d", v.Uint()) - default: - return fmt.Errorf("unsupported type %v", v.Kind()) + switch v := data.(type) { + case *String: + if v == nil { + return nil } - return nil - } - - if data == nil { - return nil - } - - str, ok := data.(*String) - if !ok { - return fmt.Errorf("data does not match required *String type") + f.value = v.value + case string: + if v == "" { + return nil + } + f.value = v + case int: + f.value = strconv.FormatInt(int64(v), 10) + case int32: + f.value = strconv.FormatInt(int64(v), 10) + case int64: + f.value = strconv.FormatInt(v, 10) + default: + return fmt.Errorf("data does not match required *String or 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 cfc2d36..7cd406c 100644 --- a/message.go +++ b/message.go @@ -354,36 +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) + // 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()) + 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