moov-io / iso8583

A golang implementation to marshal and unmarshal iso8583 message.
https://moov.io
Apache License 2.0
304 stars 100 forks source link

Allow setting custom field packer and unpacker #310

Closed alovak closed 3 weeks ago

alovak commented 3 weeks ago

Currently, all Pack and Unpack methods of the fields (except Composite) do the same steps. These steps were extracted into the DefaultPacker.

We also introduced Packer and Unpacker interfaces, so you can create custom implementation to pack/unpack any field you have.

Here is an example of when such a change can be helpful.

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:

Default Behavior

Let's explore the default behavior of a 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)

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

Here, the length is expected to be 2 bytes since 123 encoded in BCD is 0x01, 0x23. By default, the length prefix will contain 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.

fc := field.NewNumeric(&field.Spec{
    Length:      5, // Indicates the max length of the encoded field value 9/2+1 = 5
    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(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)
        }

        // Encode the length of the packed data, not the value length
        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
    }),
    // Define a custom unpacker to 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)
        }

        // Decode the packed data length
        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)

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

In this case, the length is expected to be 2 bytes since 123 encoded in BCD is 0x01, 0x23. Thus, the length prefix is 0x02, indicating the length of the packed data is 2 bytes.