Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow setting custom field packer and unpacker #310

Merged
merged 5 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 11 additions & 28 deletions field/binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"github.com/moov-io/iso8583/utils"
)

var _ Field = (*Binary)(nil)
var _ json.Marshaler = (*Binary)(nil)
var _ json.Unmarshaler = (*Binary)(nil)
var (
_ Field = (*Binary)(nil)
_ json.Marshaler = (*Binary)(nil)
_ json.Unmarshaler = (*Binary)(nil)
)

type Binary struct {
value []byte
Expand Down Expand Up @@ -72,43 +74,24 @@ func (f *Binary) SetValue(v []byte) {
func (f *Binary) Pack() ([]byte, error) {
data := f.value

if f.spec.Pad != nil {
data = f.spec.Pad.Pad(data, f.spec.Length)
}

packed, err := f.spec.Enc.Encode(data)
if err != nil {
return nil, fmt.Errorf("failed to encode content: %w", err)
}

packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(data))
if err != nil {
return nil, fmt.Errorf("failed to encode length: %w", err)
}
packer := f.spec.getPacker()

return append(packedLength, packed...), nil
return packer.Pack(data, f.spec)
}

func (f *Binary) Unpack(data []byte) (int, error) {
dataLen, prefBytes, err := f.spec.Pref.DecodeLength(f.spec.Length, data)
if err != nil {
return 0, fmt.Errorf("failed to decode length: %w", err)
}
unpacker := f.spec.getUnpacker()

raw, read, err := f.spec.Enc.Decode(data[prefBytes:], dataLen)
raw, bytesRead, err := unpacker.Unpack(data, f.spec)
if err != nil {
return 0, fmt.Errorf("failed to decode content: %w", err)
}

if f.spec.Pad != nil {
raw = f.spec.Pad.Unpad(raw)
return 0, err
}

if err := f.SetBytes(raw); err != nil {
return 0, fmt.Errorf("failed to set bytes: %w", err)
}

return read + prefBytes, nil
return bytesRead, nil
}

// Deprecated. Use Marshal instead
Expand Down
8 changes: 5 additions & 3 deletions field/composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import (
"github.com/moov-io/iso8583/utils"
)

var _ Field = (*Composite)(nil)
var _ json.Marshaler = (*Composite)(nil)
var _ json.Unmarshaler = (*Composite)(nil)
var (
_ Field = (*Composite)(nil)
_ json.Marshaler = (*Composite)(nil)
_ json.Unmarshaler = (*Composite)(nil)
)

// Composite is a wrapper object designed to hold ISO8583 TLVs, subfields and
// subelements. Because Composite handles both of these usecases generically,
Expand Down
39 changes: 11 additions & 28 deletions field/numeric.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
"github.com/moov-io/iso8583/utils"
)

var _ Field = (*Numeric)(nil)
var _ json.Marshaler = (*Numeric)(nil)
var _ json.Unmarshaler = (*Numeric)(nil)
var (
_ Field = (*Numeric)(nil)
_ json.Marshaler = (*Numeric)(nil)
_ json.Unmarshaler = (*Numeric)(nil)
)

type Numeric struct {
value int64
Expand Down Expand Up @@ -84,44 +86,25 @@ func (f *Numeric) SetValue(v int64) {
func (f *Numeric) Pack() ([]byte, error) {
data := []byte(strconv.FormatInt(f.value, 10))

if f.spec.Pad != nil {
data = f.spec.Pad.Pad(data, f.spec.Length)
}

packed, err := f.spec.Enc.Encode(data)
if err != nil {
return nil, fmt.Errorf("failed to encode content: %w", err)
}

packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(data))
if err != nil {
return nil, fmt.Errorf("failed to encode length: %w", err)
}
packer := f.spec.getPacker()

return append(packedLength, packed...), nil
return packer.Pack(data, f.spec)
}

// returns number of bytes was read
func (f *Numeric) Unpack(data []byte) (int, error) {
dataLen, prefBytes, err := f.spec.Pref.DecodeLength(f.spec.Length, data)
if err != nil {
return 0, fmt.Errorf("failed to decode length: %w", err)
}
unpacker := f.spec.getUnpacker()

raw, read, err := f.spec.Enc.Decode(data[prefBytes:], dataLen)
raw, bytesRead, err := unpacker.Unpack(data, f.spec)
if err != nil {
return 0, fmt.Errorf("failed to decode content: %w", err)
}

if f.spec.Pad != nil {
raw = f.spec.Pad.Unpad(raw)
return 0, err
}

if err := f.SetBytes(raw); err != nil {
return 0, fmt.Errorf("failed to set bytes: %w", err)
}

return read + prefBytes, nil
return bytesRead, nil
}

// Deprecated. Use Marshal instead
Expand Down
43 changes: 43 additions & 0 deletions field/packer_unpacker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package field

import "fmt"

type DefaultPacker struct{}

func (p DefaultPacker) Pack(data []byte, spec *Spec) ([]byte, error) {
if spec.Pad != nil {
data = spec.Pad.Pad(data, spec.Length)
}

packed, err := spec.Enc.Encode(data)
if err != nil {
return nil, fmt.Errorf("failed to encode content: %w", err)
}

packedLength, err := spec.Pref.EncodeLength(spec.Length, len(data))
if err != nil {
return nil, fmt.Errorf("failed to encode length: %w", err)
}

return append(packedLength, packed...), nil
}

type DefaultUnpacker struct{}

func (u DefaultUnpacker) Unpack(data []byte, spec *Spec) ([]byte, int, error) {
dataLen, prefBytes, err := spec.Pref.DecodeLength(spec.Length, data)
if err != nil {
return nil, 0, fmt.Errorf("failed to decode length: %w", err)
}

raw, read, err := spec.Enc.Decode(data[prefBytes:], dataLen)
if err != nil {
return nil, 0, fmt.Errorf("failed to decode content: %w", err)
}

if spec.Pad != nil {
raw = spec.Pad.Unpad(raw)
}

return raw, read + prefBytes, nil
}
106 changes: 106 additions & 0 deletions field/packer_unpacker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package field_test

import (
"fmt"
"testing"

"github.com/moov-io/iso8583/encoding"
"github.com/moov-io/iso8583/field"
"github.com/moov-io/iso8583/prefix"
"github.com/stretchr/testify/require"
)

func TestCustomPackerAndUnpacker(t *testing.T) {
// let's say we have a requirements that the lentgh prefix should
// contain the length of the data we pass, not the length of the field
// value. This is the case when you use BCD or HEX encoding, where the
// length of the field value is not the same as the length of the
// encoded field value.

// Here is an example of such requirement:
// - the max length of the field is 9
// - the field value should be encoded using BCD encoding
// - the lenth prefix is L (1 byte) and should contain the length of the
// the data in the field we pass.
// - the field value is "123"

// let's see the default behavior of the Numeric field
fd := field.NewNumeric(&field.Spec{
Length: 9, // the max length of the field is 9 digits
Description: "Amount",
Enc: encoding.BCD,
Pref: prefix.Binary.L,
})

fd.SetValue(123)

packed, err := fd.Pack()
require.NoError(t, err)

// we expect the length to be 2 bytes, as 123 encoded in BCD is 0x01, 0x23
// by the default behavior, the length prefix will contain the length of the
// field value, which is 3 digits, so the length prefix as you can see is 0x03
require.Equal(t, []byte{0x03, 0x01, 0x23}, packed)

// now let's create a custom packer and unpacker for the Numeric field
// that will pack the field value as BCD and the length prefix as the length
// of the encoded field value.
fc := field.NewNumeric(&field.Spec{
Length: 5, // now, this field indicates the max length of the encoded field value 9/2+1 = 5
Description: "Amount",
Enc: encoding.BCD,
Pref: prefix.Binary.L,
// we define a custom packer here, which will encode the length of the packed data
Packer: field.PackerFunc(func(data []byte, spec *field.Spec) ([]byte, error) {
if spec.Pad != nil {
data = spec.Pad.Pad(data, spec.Length)
}

packed, err := spec.Enc.Encode(data)
if err != nil {
return nil, fmt.Errorf("failed to encode content: %w", err)
}

// here is where we encode the length of the packed data, not the length of the value
packedLength, err := spec.Pref.EncodeLength(spec.Length, len(packed))
if err != nil {
return nil, fmt.Errorf("failed to encode length: %w", err)
}

return append(packedLength, packed...), nil
}),
// we define a custom unpacker here, which will decode the length of the packed data
Unpacker: field.UnpackerFunc(func(data []byte, spec *field.Spec) ([]byte, int, error) {
dataLen, prefBytes, err := spec.Pref.DecodeLength(spec.Length, data)
if err != nil {
return nil, 0, fmt.Errorf("failed to decode length: %w", err)
}

// dataLen here is the length of the packed data, not the length of the value
// as we use BCD decoding, we have to multiply it by 2, as each BCD byte
// represents 2 digits. If the number of digits is even, it will be prepended
// with a 0. As the type of the field is Numeric, leading 0 will be removed
// so we will have exactly the number of digits we need.
raw, read, err := spec.Enc.Decode(data[prefBytes:], dataLen*2)
if err != nil {
return nil, 0, fmt.Errorf("failed to decode content: %w", err)
}

if spec.Pad != nil {
raw = spec.Pad.Unpad(raw)
}

return raw, read + prefBytes, nil
}),
})

fc.SetValue(123)

packed, err = fc.Pack()
require.NoError(t, err)

// we expect the length to be 2 bytes, as 123 encoded in BCD is 0x01, 0x23
// so, you can see that the length prefix is 0x02, as the length of the packed
// data is 2 bytes.
require.Equal(t, []byte{0x02, 0x01, 0x23}, packed)
}
42 changes: 42 additions & 0 deletions field/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,34 @@ type Spec struct {
// Bitmap defines a bitmap field that is used only by a composite field type.
// It defines the way that the composite will determine its subflieds existence.
Bitmap *Bitmap
// Packer is the packer used to pack the field. Default is DefaultPacker.
Packer Packer
// Unpacker is the unpacker used to unpack the field. Default is DefaultUnpacker.
Unpacker Unpacker
}

// Packer is the interface that wraps the Pack method.
type Packer interface {
Pack(data []byte, spec *Spec) ([]byte, error)
}

// Unpacker is the interface that wraps the Unpack method.
type Unpacker interface {
// Unpack unpacks the data according to the spec and returns the
// unpacked data and the number of bytes read.
Unpack(data []byte, spec *Spec) ([]byte, int, error)
}

type PackerFunc func(data []byte, spec *Spec) ([]byte, error)

func (f PackerFunc) Pack(data []byte, spec *Spec) ([]byte, error) {
return f(data, spec)
}

type UnpackerFunc func(data []byte, spec *Spec) ([]byte, int, error)

func (f UnpackerFunc) Unpack(data []byte, spec *Spec) ([]byte, int, error) {
return f(data, spec)
}

func NewSpec(length int, desc string, enc encoding.Encoder, pref prefix.Prefixer) *Spec {
Expand All @@ -91,6 +119,20 @@ func NewSpec(length int, desc string, enc encoding.Encoder, pref prefix.Prefixer
}
}

func (spec *Spec) getPacker() Packer {
if spec.Packer == nil {
return DefaultPacker{}
}
return spec.Packer
}

func (spec *Spec) getUnpacker() Unpacker {
if spec.Unpacker == nil {
return DefaultUnpacker{}
}
return spec.Unpacker
}

// Validate validates the spec.
func (s *Spec) Validate() error {
if s.Enc != nil {
Expand Down
Loading
Loading