Skip to content

Commit

Permalink
make default packer/unpacker private; update comments
Browse files Browse the repository at this point in the history
  • Loading branch information
alovak committed Jun 10, 2024
1 parent 77c1c9c commit 186410f
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 28 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.
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
32 changes: 20 additions & 12 deletions field/packer_unpacker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,50 @@ package field

import "fmt"

type DefaultPacker struct{}
type defaultPacker struct{}

func (p DefaultPacker) Pack(data []byte, spec *Spec) ([]byte, error) {
// 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 {
data = spec.Pad.Pad(data, spec.Length)
value = spec.Pad.Pad(value, spec.Length)
}

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

packedLength, err := spec.Pref.EncodeLength(spec.Length, len(data))
// 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(packedLength, packed...), nil
return append(lengthPrefix, encodedValue...), nil
}

type DefaultUnpacker struct{}
type defaultUnpacker struct{}

func (u DefaultUnpacker) Unpack(data []byte, spec *Spec) ([]byte, int, error) {
dataLen, prefBytes, err := spec.Pref.DecodeLength(spec.Length, data)
// 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)
}

raw, read, err := spec.Enc.Decode(data[prefBytes:], dataLen)
// 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 {
raw = spec.Pad.Unpad(raw)
value = spec.Pad.Unpad(value)
}

return raw, read + prefBytes, nil
return value, read + prefBytes, nil
}
36 changes: 23 additions & 13 deletions field/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ type TagSpec struct {

// Spec defines the structure of a field.
type Spec struct {
// Length defines the maximum length of field (bytes, characters,
// digits or hex digits), for both fixed and variable lengths.
// You should use appropriate field types corresponding to the
// Length defines the maximum length of the field value (bytes,
// characters, digits or hex digits), for both fixed and variable
// lengths. You should use appropriate field types corresponding to the
// length of the field you're defining, e.g. Numeric, String, Binary
// etc. For Hex fields, the length is defined in terms of the number
// of bytes, while the value of the field is hex string.
// etc. For Hex fields, the length is defined in terms of the number of
// bytes, while the value of the field is hex string.
Length int
// Tag sets the tag specification. Only applicable to composite field
// types.
Expand Down Expand Up @@ -77,35 +77,45 @@ type Spec struct {
// will be disregarded, and the size of the bitmap will not change when
// the first bit is set.
DisableAutoExpand bool
// 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 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 packes the field value according to its spec. Default is
// defaultPacker.
Packer Packer
// Unpacker is the unpacker used to unpack the field. Default is DefaultUnpacker.
// Unpacker unpackes the field value according to its spec. Default is
// defaultUnpacker.
Unpacker Unpacker
}

// Packer is the interface that wraps the Pack method.
type Packer interface {
// Pack packs the data (field value) according to the spec and returns
// the packed data.
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 unpacks the packed data according to the spec and returns the
// unpacked data (field value) and the number of bytes read.
Unpack(data []byte, spec *Spec) ([]byte, int, error)
}

// PackerFunc is a function type that implements the Packer interface.
type PackerFunc func(data []byte, spec *Spec) ([]byte, error)

// Pack packs the data (field value) according to the spec.
func (f PackerFunc) Pack(data []byte, spec *Spec) ([]byte, error) {
return f(data, spec)
}

// UnpackerFunc is a function type that implements the Unpacker interface.
type UnpackerFunc func(data []byte, spec *Spec) ([]byte, int, error)

// Unpack unpacks the packed data according to the spec and returns the
// unpacked data (field value) and the number of bytes read.
func (f UnpackerFunc) Unpack(data []byte, spec *Spec) ([]byte, int, error) {
return f(data, spec)
}
Expand All @@ -121,14 +131,14 @@ func NewSpec(length int, desc string, enc encoding.Encoder, pref prefix.Prefixer

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

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

0 comments on commit 186410f

Please sign in to comment.