Skip to content

Commit

Permalink
Allow setting custom field packer and unpacker (#310)
Browse files Browse the repository at this point in the history
* add Packer and Unpacker to the field spec
* add PackerFunc and UnpackerFunc to use them in the spec definition
  • Loading branch information
alovak authored Jun 11, 2024
1 parent ad2c1c5 commit e9c44b8
Show file tree
Hide file tree
Showing 12 changed files with 391 additions and 181 deletions.
105 changes: 105 additions & 0 deletions docs/howtos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Howtos

## Howto create custom packer and unpacker for a field

### Problem

The default behavior of the field packer and unpacker may not meet your requirements. For instance, you might need the length prefix to represent the length of the encoded data, not the field value. This is often necessary when using BCD or HEX encoding, where the field value's length differs from the encoded field value's length.

**Example Requirement:**

- Maximum length of the field: 9
- Field value encoding: BCD
- Length prefix: L (1 byte) representing the length of the encoded data
- Field value: "123"

### Default Behavior

Let's explore the default behavior of a Numeric field:

```go
f := field.NewNumeric(&field.Spec{
Length: 9, // The max length of the field is 9 digits
Description: "Amount",
Enc: encoding.BCD,
Pref: prefix.Binary.L,
})

f.SetValue(123)

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

require.Equal(t, []byte{0x03, 0x01, 0x23}, packed)
```

By default, the length prefix contains the field value's length, which is 3 digits, resulting in a length prefix of 0x03.

### Custom Packer and Unpacker

Let's create a custom packer and unpacker for the Numeric field to pack the field value as BCD and set the length prefix to the length of the encoded field value.

```go
f := field.NewNumeric(&field.Spec{
Length: 9, // max length of the field value (9 digits)
Description: "Amount",
Enc: encoding.BCD,
Pref: prefix.Binary.L,
// Define a custom packer to encode the length of the packed data
Packer: field.PackerFunc(func(value []byte, spec *field.Spec) ([]byte, error) {
if spec.Pad != nil {
value = spec.Pad.Pad(value, spec.Length)
}

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

// Encode the length of the packed data, not the length of the value
maxLength := spec.Length/2 + 1

// Encode the length of the encoded value
lengthPrefix, err := spec.Pref.EncodeLength(maxLength, len(encodedValue))
if err != nil {
return nil, fmt.Errorf("failed to encode length: %w", err)
}

return append(lengthPrefix, encodedValue...), nil
}),

// Define a custom unpacker to decode the length of the packed data
Unpacker: field.UnpackerFunc(func(packedFieldValue []byte, spec *field.Spec) ([]byte, int, error) {
maxEncodedValueLength := spec.Length/2 + 1

encodedValueLength, prefBytes, err := spec.Pref.DecodeLength(maxEncodedValueLength, packedFieldValue)
if err != nil {
return nil, 0, fmt.Errorf("failed to decode length: %w", err)
}

// for BCD encoding, the length of the packed data is twice the length of the encoded value
valueLength := encodedValueLength * 2

// Decode the packed data length
value, read, err := spec.Enc.Decode(packedFieldValue[prefBytes:], valueLength)
if err != nil {
return nil, 0, fmt.Errorf("failed to decode content: %w", err)
}

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

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

f.SetValue(123)

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

require.Equal(t, []byte{0x02, 0x01, 0x23}, packed)
```

Since 123 encoded in BCD is 0x01, 0x23, the length prefix is 0x02, indicating the length of the packed data is 2 bytes, not the field value's length which is 3 digits.
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
8 changes: 5 additions & 3 deletions field/hex.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 = (*Hex)(nil)
var _ json.Marshaler = (*Hex)(nil)
var _ json.Unmarshaler = (*Hex)(nil)
var (
_ Field = (*Hex)(nil)
_ json.Marshaler = (*Hex)(nil)
_ json.Unmarshaler = (*Hex)(nil)
)

// Hex field allows working with hex strings but under the hood it's a binary
// field. It's convenient to use when you need to work with hex strings, but
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
51 changes: 51 additions & 0 deletions field/packer_unpacker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package field

import "fmt"

type defaultPacker struct{}

// Pack packs the data according to the spec
func (p defaultPacker) Pack(value []byte, spec *Spec) ([]byte, error) {
// pad the value if needed
if spec.Pad != nil {
value = spec.Pad.Pad(value, spec.Length)
}

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

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

return append(lengthPrefix, encodedValue...), nil
}

type defaultUnpacker struct{}

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

// decode the value
value, read, err := spec.Enc.Decode(packedFieldValue[prefBytes:], valueLength)
if err != nil {
return nil, 0, fmt.Errorf("failed to decode content: %w", err)
}

// unpad the value if needed
if spec.Pad != nil {
value = spec.Pad.Unpad(value)
}

return value, read + prefBytes, nil
}
Loading

0 comments on commit e9c44b8

Please sign in to comment.