moov-io / iso8583

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

How to place some data between length prefixer and the subfields? #305

Closed alovak closed 8 months ago

alovak commented 8 months ago

Let's say you have a case where you have to build the following field:

[LLL][CODE][TLVs]

The main question here is how to place CODE between the length prefix and the set of TLVs?

It can be achieved by using the None.Fixed prefix and putting composite field inside composite field making the parent composite subfield tag to be our CODE. The None.Fixed prefix returns the length of the data it has as an input (maybe None name is not ideal). The composite inside composite allows us to dynamically add data (e.g. CODE) depending on the subfields set.

In the following test we will have the following packed value for field 3:

[mti, bitmap and pan are removed for clarity ...]015A01123450267890

where:

package iso8583_test

import (
    "fmt"
    "os"
    "testing"

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

func TestSpecUsingNonePrefix(t *testing.T) {
    spec := &iso8583.MessageSpec{
        Fields: map[int]field.Field{
            0: field.NewString(&field.Spec{
                Length:      4,
                Description: "Message Type Indicator",
                Enc:         encoding.ASCII,
                Pref:        prefix.ASCII.Fixed,
            }),
            1: field.NewBitmap(&field.Spec{
                Description: "Bitmap",
                Enc:         encoding.BytesToASCIIHex,
                Pref:        prefix.Hex.Fixed,
            }),
            2: field.NewString(&field.Spec{
                Length:      19,
                Description: "Primary Account Number",
                Enc:         encoding.ASCII,
                Pref:        prefix.ASCII.LL,
            }),
            3: field.NewComposite(&field.Spec{
                Length:      999,
                Description: "Whatever data",
                Pref:        prefix.ASCII.LLL,
                Tag: &field.TagSpec{
                    Length: 1,
                    Enc:    encoding.ASCII,
                    Sort:   sort.StringsByInt,
                },
                Subfields: map[string]field.Field{
                    "A": field.NewComposite(&field.Spec{
                        Length:      998, // max, will be ignored with None prefix, all data read by parent field will be passed
                        Description: "Whatever data",
                        Pref:        prefix.None.Fixed,
                        Tag: &field.TagSpec{
                            Length: 2,
                            Enc:    encoding.ASCII,
                            Sort:   sort.Strings,
                        },
                        Subfields: map[string]field.Field{
                            "01": field.NewString(&field.Spec{
                                Length:      5,
                                Description: "code1",
                                Enc:         encoding.ASCII,
                                Pref:        prefix.ASCII.Fixed,
                            }),
                            "02": field.NewString(&field.Spec{
                                Length:      5,
                                Description: "code2",
                                Enc:         encoding.ASCII,
                                Pref:        prefix.ASCII.Fixed,
                            }),
                        },
                    }),
                },
            }),
        },
    }

    message := iso8583.NewMessage(spec)

    type transactionData struct {
        Code1 string `index:"01"`
        Code2 string `index:"02"`
    }

    type whateverData struct {
        TransactionData *transactionData `index:"A"`
    }

    type data struct {
        MTI                  string        `index:"0"`
        PrimaryAccountNumber string        `index:"2"`
        WhateverData         *whateverData `index:"3"`
    }

    in := &data{
        MTI:                  "0100",
        PrimaryAccountNumber: "4242424242424242",
        WhateverData: &whateverData{
            TransactionData: &transactionData{
                Code1: "12345",
                Code2: "67890",
            },
        },
    }
    err := message.Marshal(in)

    require.NoError(t, err)

    iso8583.Describe(message, os.Stdout)

    packed, err := message.Pack()

    fmt.Println(string(packed))

    message = iso8583.NewMessage(spec)

    err = message.Unpack(packed)
    require.NoError(t, err)

    iso8583.Describe(message, os.Stdout)

    out := &data{}
    err = message.Unmarshal(out)
    require.NoError(t, err)

    require.Equal(t, in, out)
}

If you define more than one subfield (e.g. A, B) then only one such subfield can have value either A or B.

tzzzoz commented 8 months ago

Hey @alovak, thanks for the explanation!

It works for us.

But the retrieval of subfield [CODE] is not very straightforward though. We have to check the presence of different [CODE] to get its actual value. Such as

if message.WhateverData.A != nil {
  code = "A"
}
if message.WhateverData.B != nil {
  code = "B"
}
...

Fortunately, we only have a few possible values to define in the message specification.

We will follow what you've suggested here, please let me know if you have other more elegant options. Thanks in advance!

alovak commented 8 months ago

One of the options can be creating a custom field, but it's more work and the resulting code will still require some ifs/switch/case statements. The approach with custom field is the following:

// The following code may not compile; it is provided just to illustrate an idea.
// In your main code, define the field as ASCII or Binary, not as Composite.
// When you unpack the message, get the data of the field and use custom method (or type) to "unpack" it

type DataStruct struct {
       TransactionData *TransactionData `index:"A"` // should be defined
       VerificationData *VerificationData `index:"B"` // should be defined
}

type CustomField struct {
    spec *field.Spec
    Data *DataStruct
}

func (f *CustomField) Unpack(data []byte) (interface{}, error) {
    // check the data length

    // read first byte as a CODE
        f.Code = string(data[0:1])

        f.Data := &DataStruct{}
        field := NewComposite(f.spec) // this spec is for subfields including CODE
        err := field.Unmarshal(f.Data)

        // ...
}

The more I think about the solution above, the less appealing it seems :)

In your specific case maybe adding the TransactionCode method to the whateverData struct is good solution:

func (wd *whateverData) TransactionCode() string {
    switch {
    case wd == nil:
        return ""
    case wd.A != nil:
        return "A"
    case wd.B != nil:
        return "B"
    default:
        return "unknown"
    }
}
tzzzoz commented 7 months ago

Hey @alovak , we found another way to handle this case.

    spec := &iso8583.MessageSpec{
        Fields: map[int]field.Field{
            0: field.NewString(&field.Spec{
                         ....
            3: field.NewComposite(&field.Spec{
                Length:      999,
                Description: "Whatever data",
                Pref:        prefix.ASCII.LLL,
                Tag: &field.TagSpec{
                    Sort: sort.StringsByInt,
                },
                Subfields: map[string]field.Field{
                    "1": field.NewString(&field.Spec{
                        Length:      1,
                        Description: "Type",
                        Enc:         encoding.ASCII,
                        Pref:        prefix.ASCII.Fixed,
                    }),
                    "2": field.NewComposite(&field.Spec{
                        Description: "Additional Data",
                        Pref:        prefix.None.Fixed,
                        Tag: &field.TagSpec{
                            Length: 2,
                            Enc:    encoding.ASCII,
                            Sort:   sort.StringsByInt,
                        },
                        Subfields: map[string]field.Field{
                            "01": field.NewString(&field.Spec{
                                Length:      5,
                                Description: "Code1",
                                Pref:        prefix.ASCII.Fixed,
                                Enc:         encoding.ASCII,
                            }),
                            "02": field.NewString(&field.Spec{
                                Length:      5,
                                Description: "Code2",
                                Pref:        prefix.ASCII.Fixed,
                                Enc:         encoding.ASCII,
                            }),
                        },
                    }),
                },
            }),
        },
    }

    type transactionData struct {
        Code1 string `index:"01"`
        Code2 string `index:"02"`
    }

    type whateverData struct {
        Type            string           `index:"1"`
        TransactionData *transactionData `index:"2"`
    }

    type data struct {
        MTI                  string        `index:"0"`
        PrimaryAccountNumber string        `index:"2"`
        WhateverData         *whateverData `index:"3"`
    }

    in := &data{
        MTI:                  "0100",
        PrimaryAccountNumber: "4242424242424242",
        WhateverData: &whateverData{
            Type: "A",
            TransactionData: &transactionData{
                Code1: "12345",
                Code2: "67890",
            },
        },
    }

For the following field: [LLL][CODE][TLVs]

[CODE] is defined as a subfield "1" with a fixed length. [TLVs] is defined as the second subfield "2". Thanks to prefix.None.Fixed, it can represent the rest of the parent composite field.

Inside the second subfield, we can use a regular composite field to represent all TLVs.

It avoids leaking the values into the message spec.