code refactor
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package ofx
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
@@ -69,9 +71,10 @@ var ofxTransactionTypeMapping = map[ofxTransactionType]models.TransactionType{
|
||||
|
||||
// ofxFile represents the struct of open financial exchange (ofx) file
|
||||
type ofxFile struct {
|
||||
XMLName xml.Name `xml:"OFX"`
|
||||
FileHeader *ofxFileHeader
|
||||
BankMessageResponseV1 *ofxBankMessageResponseV1
|
||||
CreditCardMessageResponseV1 *ofxCreditCardMessageResponseV1
|
||||
BankMessageResponseV1 *ofxBankMessageResponseV1 `xml:"BANKMSGSRSV1"`
|
||||
CreditCardMessageResponseV1 *ofxCreditCardMessageResponseV1 `xml:"CREDITCARDMSGSRSV1"`
|
||||
}
|
||||
|
||||
// ofxFileHeader represents the struct of open financial exchange (ofx) file header
|
||||
@@ -85,101 +88,101 @@ type ofxFileHeader struct {
|
||||
|
||||
// ofxBankMessageResponseV1 represents the struct of open financial exchange (ofx) bank message response v1
|
||||
type ofxBankMessageResponseV1 struct {
|
||||
StatementTransactionResponse *ofxBankStatementTransactionResponse
|
||||
StatementTransactionResponse *ofxBankStatementTransactionResponse `xml:"STMTTRNRS"`
|
||||
}
|
||||
|
||||
// ofxCreditCardMessageResponseV1 represents the struct of open financial exchange (ofx) credit card message response v1
|
||||
type ofxCreditCardMessageResponseV1 struct {
|
||||
StatementTransactionResponse *ofxCreditCardStatementTransactionResponse
|
||||
StatementTransactionResponse *ofxCreditCardStatementTransactionResponse `xml:"CCSTMTTRNRS"`
|
||||
}
|
||||
|
||||
// ofxBankStatementTransactionResponse represents the struct of open financial exchange (ofx) bank statement transaction response
|
||||
type ofxBankStatementTransactionResponse struct {
|
||||
StatementResponse *ofxBankStatementResponse
|
||||
StatementResponse *ofxBankStatementResponse `xml:"STMTRS"`
|
||||
}
|
||||
|
||||
// ofxCreditCardStatementTransactionResponse represents the struct of open financial exchange (ofx) credit card statement transaction response
|
||||
type ofxCreditCardStatementTransactionResponse struct {
|
||||
StatementResponse *ofxCreditCardStatementResponse
|
||||
StatementResponse *ofxCreditCardStatementResponse `xml:"CCSTMTRS"`
|
||||
}
|
||||
|
||||
// ofxBankStatementResponse represents the struct of open financial exchange (ofx) bank statement response
|
||||
type ofxBankStatementResponse struct {
|
||||
DefaultCurrency string
|
||||
AccountFrom *ofxBankAccount
|
||||
TransactionList *ofxBankTransactionList
|
||||
DefaultCurrency string `xml:"CURDEF"`
|
||||
AccountFrom *ofxBankAccount `xml:"BANKACCTFROM"`
|
||||
TransactionList *ofxBankTransactionList `xml:"BANKTRANLIST"`
|
||||
}
|
||||
|
||||
// ofxCreditCardStatementResponse represents the struct of open financial exchange (ofx) credit card statement response
|
||||
type ofxCreditCardStatementResponse struct {
|
||||
DefaultCurrency string
|
||||
AccountFrom *ofxCreditCardAccount
|
||||
TransactionList *ofxCreditCardTransactionList
|
||||
DefaultCurrency string `xml:"CURDEF"`
|
||||
AccountFrom *ofxCreditCardAccount `xml:"CCACCTFROM"`
|
||||
TransactionList *ofxCreditCardTransactionList `xml:"BANKTRANLIST"`
|
||||
}
|
||||
|
||||
// ofxBankAccount represents the struct of open financial exchange (ofx) bank account
|
||||
type ofxBankAccount struct {
|
||||
BankId string
|
||||
BranchId string
|
||||
AccountId string
|
||||
AccountType ofxAccountType
|
||||
AccountKey string
|
||||
BankId string `xml:"BANKID"`
|
||||
BranchId string `xml:"BRANCHID"`
|
||||
AccountId string `xml:"ACCTID"`
|
||||
AccountType ofxAccountType `xml:"ACCTTYPE"`
|
||||
AccountKey string `xml:"ACCTKEY"`
|
||||
}
|
||||
|
||||
// ofxCreditCardAccount represents the struct of open financial exchange (ofx) credit card account
|
||||
type ofxCreditCardAccount struct {
|
||||
AccountId string
|
||||
AccountKey string
|
||||
AccountId string `xml:"ACCTID"`
|
||||
AccountKey string `xml:"ACCTKEY"`
|
||||
}
|
||||
|
||||
// ofxBankTransactionList represents the struct of open financial exchange (ofx) bank transaction list
|
||||
type ofxBankTransactionList struct {
|
||||
StartDate string
|
||||
EndDate string
|
||||
StatementTransactions []*ofxBankStatementTransaction
|
||||
StartDate string `xml:"DTSTART"`
|
||||
EndDate string `xml:"DTEND"`
|
||||
StatementTransactions []*ofxBankStatementTransaction `xml:"STMTTRN"`
|
||||
}
|
||||
|
||||
// ofxCreditCardTransactionList represents the struct of open financial exchange (ofx) credit card transaction list
|
||||
type ofxCreditCardTransactionList struct {
|
||||
StartDate string
|
||||
EndDate string
|
||||
StatementTransactions []*ofxCreditCardStatementTransaction
|
||||
StartDate string `xml:"DTSTART"`
|
||||
EndDate string `xml:"DTEND"`
|
||||
StatementTransactions []*ofxCreditCardStatementTransaction `xml:"STMTTRN"`
|
||||
}
|
||||
|
||||
// ofxBaseStatementTransaction represents the struct of open financial exchange (ofx) base statement transaction
|
||||
type ofxBaseStatementTransaction struct {
|
||||
TransactionId string
|
||||
TransactionType ofxTransactionType
|
||||
PostedDate string
|
||||
Amount string
|
||||
Name string
|
||||
Payee *ofxPayee
|
||||
Memo string
|
||||
Currency string
|
||||
OriginalCurrency string
|
||||
TransactionId string `xml:"FITID"`
|
||||
TransactionType ofxTransactionType `xml:"TRNTYPE"`
|
||||
PostedDate string `xml:"DTPOSTED"`
|
||||
Amount string `xml:"TRNAMT"`
|
||||
Name string `xml:"NAME"`
|
||||
Payee *ofxPayee `xml:"PAYEE"`
|
||||
Memo string `xml:"MEMO"`
|
||||
Currency string `xml:"CURRENCY"`
|
||||
OriginalCurrency string `xml:"ORIGCURRENCY"`
|
||||
}
|
||||
|
||||
// ofxBankStatementTransaction represents the struct of open financial exchange (ofx) bank statement transaction
|
||||
type ofxBankStatementTransaction struct {
|
||||
ofxBaseStatementTransaction
|
||||
AccountTo *ofxBankAccount
|
||||
AccountTo *ofxBankAccount `xml:"BANKACCTTO"`
|
||||
}
|
||||
|
||||
// ofxCreditCardStatementTransaction represents the struct of open financial exchange (ofx) credit card statement transaction
|
||||
type ofxCreditCardStatementTransaction struct {
|
||||
ofxBaseStatementTransaction
|
||||
AccountTo *ofxCreditCardAccount
|
||||
AccountTo *ofxCreditCardAccount `xml:"CCACCTTO"`
|
||||
}
|
||||
|
||||
// ofxPayee represents the struct of open financial exchange (ofx) payee info
|
||||
type ofxPayee struct {
|
||||
Name string
|
||||
Address1 string
|
||||
Address2 string
|
||||
Address3 string
|
||||
City string
|
||||
State string
|
||||
PostalCode string
|
||||
Country string
|
||||
Phone string
|
||||
Name string `xml:"NAME"`
|
||||
Address1 string `xml:"ADDR1"`
|
||||
Address2 string `xml:"ADDR2"`
|
||||
Address3 string `xml:"ADDR3"`
|
||||
City string `xml:"CITY"`
|
||||
State string `xml:"STATE"`
|
||||
PostalCode string `xml:"POSTALCODE"`
|
||||
Country string `xml:"COUNTRY"`
|
||||
Phone string `xml:"PHONE"`
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -131,7 +131,372 @@ func TestCreateNewOFXFileReader_OFX1WithoutBreakLine(t *testing.T) {
|
||||
assert.Equal(t, "123.45", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].Amount)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1WithBlanklines(t *testing.T) {
|
||||
func TestCreateNewOFXFileReader_OFX1ParseBankAccountFrom(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:100\n"+
|
||||
"DATA:OFXSGML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:1252\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"<BANKMSGSRSV1>\n"+
|
||||
"<STMTTRNRS>\n"+
|
||||
"<STMTRS>\n"+
|
||||
"<BANKACCTFROM>\n"+
|
||||
"<BANKID>1234567890\n"+
|
||||
"<BRANCHID>2345678901\n"+
|
||||
"<ACCTID>3456789012\n"+
|
||||
"<ACCTTYPE>CHECKING\n"+
|
||||
"<ACCTKEY>4567890123\n"+
|
||||
"</BANKACCTFROM>\n"+
|
||||
"</STMTRS>\n"+
|
||||
"</STMTTRNRS>\n"+
|
||||
"</BANKMSGSRSV1>\n"+
|
||||
"</OFX>"))
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
ofxFile, err := reader.read(context)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ofxFile)
|
||||
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom)
|
||||
|
||||
account := ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom
|
||||
assert.Equal(t, "1234567890", account.BankId)
|
||||
assert.Equal(t, "2345678901", account.BranchId)
|
||||
assert.Equal(t, "3456789012", account.AccountId)
|
||||
assert.Equal(t, ofxCheckingAccount, account.AccountType)
|
||||
assert.Equal(t, "4567890123", account.AccountKey)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1ParseCreditCardAccountFrom(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:100\n"+
|
||||
"DATA:OFXSGML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:1252\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"<CREDITCARDMSGSRSV1>\n"+
|
||||
"<CCSTMTTRNRS>\n"+
|
||||
"<CCSTMTRS>\n"+
|
||||
"<CCACCTFROM>\n"+
|
||||
"<ACCTID>3456789012\n"+
|
||||
"<ACCTKEY>4567890123\n"+
|
||||
"</CCACCTFROM>\n"+
|
||||
"</CCSTMTRS>\n"+
|
||||
"</CCSTMTTRNRS>\n"+
|
||||
"</CREDITCARDMSGSRSV1>\n"+
|
||||
"</OFX>"))
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
ofxFile, err := reader.read(context)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ofxFile)
|
||||
|
||||
assert.NotNil(t, ofxFile.CreditCardMessageResponseV1)
|
||||
assert.NotNil(t, ofxFile.CreditCardMessageResponseV1.StatementTransactionResponse)
|
||||
assert.NotNil(t, ofxFile.CreditCardMessageResponseV1.StatementTransactionResponse.StatementResponse)
|
||||
assert.NotNil(t, ofxFile.CreditCardMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom)
|
||||
|
||||
account := ofxFile.CreditCardMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom
|
||||
assert.Equal(t, "3456789012", account.AccountId)
|
||||
assert.Equal(t, "4567890123", account.AccountKey)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1ParseBankTransactionList(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:100\n"+
|
||||
"DATA:OFXSGML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:1252\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"<BANKMSGSRSV1>\n"+
|
||||
"<STMTTRNRS>\n"+
|
||||
"<STMTRS>\n"+
|
||||
"<BANKTRANLIST>\n"+
|
||||
"<DTSTART>20240901012345.000[+8:CST]\n"+
|
||||
"<DTEND>20240901235959.000[+8:CST]\n"+
|
||||
"</BANKTRANLIST>\n"+
|
||||
"</STMTRS>\n"+
|
||||
"</STMTTRNRS>\n"+
|
||||
"</BANKMSGSRSV1>\n"+
|
||||
"</OFX>"))
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
ofxFile, err := reader.read(context)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ofxFile)
|
||||
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList)
|
||||
|
||||
transactionList := ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList
|
||||
assert.Equal(t, "20240901012345.000[+8:CST]", transactionList.StartDate)
|
||||
assert.Equal(t, "20240901235959.000[+8:CST]", transactionList.EndDate)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1ParseCreditTransactionList(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:100\n"+
|
||||
"DATA:OFXSGML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:1252\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"<CREDITCARDMSGSRSV1>\n"+
|
||||
"<CCSTMTTRNRS>\n"+
|
||||
"<CCSTMTRS>\n"+
|
||||
"<BANKTRANLIST>\n"+
|
||||
"<DTSTART>20240901012345.000[+8:CST]\n"+
|
||||
"<DTEND>20240901235959.000[+8:CST]\n"+
|
||||
"</BANKTRANLIST>\n"+
|
||||
"</CCSTMTRS>\n"+
|
||||
"</CCSTMTTRNRS>\n"+
|
||||
"</CREDITCARDMSGSRSV1>\n"+
|
||||
"</OFX>"))
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
ofxFile, err := reader.read(context)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ofxFile)
|
||||
|
||||
assert.NotNil(t, ofxFile.CreditCardMessageResponseV1)
|
||||
assert.NotNil(t, ofxFile.CreditCardMessageResponseV1.StatementTransactionResponse)
|
||||
assert.NotNil(t, ofxFile.CreditCardMessageResponseV1.StatementTransactionResponse.StatementResponse)
|
||||
assert.NotNil(t, ofxFile.CreditCardMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList)
|
||||
|
||||
transactionList := ofxFile.CreditCardMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList
|
||||
assert.Equal(t, "20240901012345.000[+8:CST]", transactionList.StartDate)
|
||||
assert.Equal(t, "20240901235959.000[+8:CST]", transactionList.EndDate)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1ParseTransaction(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:100\n"+
|
||||
"DATA:OFXSGML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:1252\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"<BANKMSGSRSV1>\n"+
|
||||
"<STMTTRNRS>\n"+
|
||||
"<STMTRS>\n"+
|
||||
"<BANKACCTFROM>\n"+
|
||||
"<ACCTID>123\n"+
|
||||
"</BANKACCTFROM>\n"+
|
||||
"<BANKTRANLIST>\n"+
|
||||
"<STMTTRN>\n"+
|
||||
"<FITID>1234567890\n"+
|
||||
"<TRNTYPE>CASH\n"+
|
||||
"<DTPOSTED>20240901012345.000[+8:CST]\n"+
|
||||
"<TRNAMT>123.45\n"+
|
||||
"<NAME>Test Name\n"+
|
||||
"<MEMO>Some Text\n"+
|
||||
"<CURRENCY>CNY\n"+
|
||||
"<ORIGCURRENCY>USD\n"+
|
||||
"</STMTTRN>\n"+
|
||||
"</BANKTRANLIST>\n"+
|
||||
"</STMTRS>\n"+
|
||||
"</STMTTRNRS>\n"+
|
||||
"</BANKMSGSRSV1>\n"+
|
||||
"</OFX>"))
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
ofxFile, err := reader.read(context)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ofxFile)
|
||||
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList)
|
||||
assert.Equal(t, 1, len(ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions))
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0])
|
||||
|
||||
transaction := ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0]
|
||||
assert.Equal(t, "1234567890", transaction.TransactionId)
|
||||
assert.Equal(t, ofxCashWithdrawalTransaction, transaction.TransactionType)
|
||||
assert.Equal(t, "20240901012345.000[+8:CST]", transaction.PostedDate)
|
||||
assert.Equal(t, "123.45", transaction.Amount)
|
||||
assert.Equal(t, "Test Name", transaction.Name)
|
||||
assert.Equal(t, "Some Text", transaction.Memo)
|
||||
assert.Equal(t, "CNY", transaction.Currency)
|
||||
assert.Equal(t, "USD", transaction.OriginalCurrency)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1ParseTransactionPayee(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:100\n"+
|
||||
"DATA:OFXSGML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:1252\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"<BANKMSGSRSV1>\n"+
|
||||
"<STMTTRNRS>\n"+
|
||||
"<STMTRS>\n"+
|
||||
"<BANKACCTFROM>\n"+
|
||||
"<ACCTID>123\n"+
|
||||
"</BANKACCTFROM>\n"+
|
||||
"<BANKTRANLIST>\n"+
|
||||
"<STMTTRN>\n"+
|
||||
"<TRNTYPE>DEP\n"+
|
||||
"<DTPOSTED>20240901012345.000[+8:CST]\n"+
|
||||
"<TRNAMT>123.45\n"+
|
||||
"<PAYEE>\n"+
|
||||
"<NAME>Test Name\n"+
|
||||
"<ADDR1>Address 1\n"+
|
||||
"<ADDR2>Address 2\n"+
|
||||
"<ADDR3>Address 3\n"+
|
||||
"<CITY>City Name\n"+
|
||||
"<STATE>State Name\n"+
|
||||
"<POSTALCODE>10000000\n"+
|
||||
"<COUNTRY>Country Name\n"+
|
||||
"<PHONE>11111111111\n"+
|
||||
"</PAYEE>\n"+
|
||||
"</STMTTRN>\n"+
|
||||
"</BANKTRANLIST>\n"+
|
||||
"</STMTRS>\n"+
|
||||
"</STMTTRNRS>\n"+
|
||||
"</BANKMSGSRSV1>\n"+
|
||||
"</OFX>"))
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
ofxFile, err := reader.read(context)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ofxFile)
|
||||
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList)
|
||||
assert.Equal(t, 1, len(ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions))
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0])
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].Payee)
|
||||
|
||||
payee := ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].Payee
|
||||
assert.Equal(t, "Test Name", payee.Name)
|
||||
assert.Equal(t, "Address 1", payee.Address1)
|
||||
assert.Equal(t, "Address 2", payee.Address2)
|
||||
assert.Equal(t, "Address 3", payee.Address3)
|
||||
assert.Equal(t, "City Name", payee.City)
|
||||
assert.Equal(t, "State Name", payee.State)
|
||||
assert.Equal(t, "10000000", payee.PostalCode)
|
||||
assert.Equal(t, "Country Name", payee.Country)
|
||||
assert.Equal(t, "11111111111", payee.Phone)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1WithEndElement(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:100\n"+
|
||||
"DATA:OFXSGML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:1252\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"<BANKMSGSRSV1>\n"+
|
||||
"<STMTTRNRS>\n"+
|
||||
"<STMTRS>\n"+
|
||||
"<CURDEF>CNY</CURDEF>\n"+
|
||||
"<BANKACCTFROM>\n"+
|
||||
"<ACCTID>123</ACCTID>\n"+
|
||||
"</BANKACCTFROM>\n"+
|
||||
"<BANKTRANLIST>\n"+
|
||||
"<STMTTRN>\n"+
|
||||
"<TRNTYPE>DEP</TRNTYPE>\n"+
|
||||
"<DTPOSTED>20240901012345.000[+8:CST]</DTPOSTED>\n"+
|
||||
"<TRNAMT>123.45</TRNAMT>\n"+
|
||||
"</STMTTRN>\n"+
|
||||
"</BANKTRANLIST>\n"+
|
||||
"</STMTRS>\n"+
|
||||
"</STMTTRNRS>\n"+
|
||||
"</BANKMSGSRSV1>\n"+
|
||||
"</OFX>"))
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
ofxFile, err := reader.read(context)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ofxFile)
|
||||
|
||||
assert.NotNil(t, ofxFile.FileHeader)
|
||||
assert.Equal(t, ofxVersion1, ofxFile.FileHeader.OFXDeclarationVersion)
|
||||
assert.Equal(t, "103", ofxFile.FileHeader.OFXDataVersion)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.Security)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.OldFileUid)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.NewFileUid)
|
||||
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse)
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse)
|
||||
|
||||
assert.Equal(t, "CNY", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.DefaultCurrency)
|
||||
|
||||
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom)
|
||||
assert.Equal(t, "123", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom.AccountId)
|
||||
|
||||
assert.Equal(t, 1, len(ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions))
|
||||
assert.Equal(t, ofxDepositTransaction, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].TransactionType)
|
||||
assert.Equal(t, "20240901012345.000[+8:CST]", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].PostedDate)
|
||||
assert.Equal(t, "123.45", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].Amount)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1WithBlanklinesInHeader(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"\n"+
|
||||
@@ -194,6 +559,104 @@ func TestCreateNewOFXFileReader_OFX1WithBlanklines(t *testing.T) {
|
||||
assert.Equal(t, "123.45", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].Amount)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1WithoutCharset(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:100\n"+
|
||||
"DATA:OFXSGML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"FOO:BAR\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"</OFX>"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
ofxFile, err := reader.read(context)
|
||||
assert.NotNil(t, ofxFile)
|
||||
|
||||
assert.NotNil(t, ofxFile.FileHeader)
|
||||
assert.Equal(t, ofxVersion1, ofxFile.FileHeader.OFXDeclarationVersion)
|
||||
assert.Equal(t, "103", ofxFile.FileHeader.OFXDataVersion)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.Security)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.OldFileUid)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.NewFileUid)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1WithInvalidHeaderVersion(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
_, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:200\n"+
|
||||
"DATA:OFXSGML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:1252\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"</OFX>"))
|
||||
|
||||
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1WithInvalidHeader(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
_, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:100\n"+
|
||||
"DATA:XML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:1252\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"</OFX>"))
|
||||
|
||||
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX1WithUnknownHeader(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"OFXHEADER:100\n"+
|
||||
"DATA:OFXSGML\n"+
|
||||
"VERSION:103\n"+
|
||||
"SECURITY:NONE\n"+
|
||||
"ENCODING:USASCII\n"+
|
||||
"CHARSET:1252\n"+
|
||||
"COMPRESSION:NONE\n"+
|
||||
"OLDFILEUID:NONE\n"+
|
||||
"NEWFILEUID:NONE\n"+
|
||||
"FOO:BAR\n"+
|
||||
"\n"+
|
||||
"<OFX>\n"+
|
||||
"</OFX>"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
ofxFile, err := reader.read(context)
|
||||
assert.NotNil(t, ofxFile)
|
||||
|
||||
assert.NotNil(t, ofxFile.FileHeader)
|
||||
assert.Equal(t, ofxVersion1, ofxFile.FileHeader.OFXDeclarationVersion)
|
||||
assert.Equal(t, "103", ofxFile.FileHeader.OFXDataVersion)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.Security)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.OldFileUid)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.NewFileUid)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX2(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
@@ -328,14 +791,15 @@ func TestCreateNewOFXFileReader_OFX2WithInvalidHeader(t *testing.T) {
|
||||
"<?OFX?>"+
|
||||
"<OFX>"+
|
||||
"</OFX>"))
|
||||
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
|
||||
|
||||
_, err = createNewOFXFileReader(context, []byte(
|
||||
"<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+
|
||||
"<?OFX OFXHEADER=200?>"+
|
||||
"<OFX>"+
|
||||
"</OFX>"))
|
||||
|
||||
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
|
||||
|
||||
_, err = createNewOFXFileReader(context, []byte(
|
||||
"<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+
|
||||
"<?OFX OFXHEADER=\"200\" VERSION=\"211\" SECURITY=\"NONE\" OLDFILEUID=\"NONE\" NEWFILEUID=\"NONE\" test=\"\"?>"+
|
||||
@@ -344,6 +808,27 @@ func TestCreateNewOFXFileReader_OFX2WithInvalidHeader(t *testing.T) {
|
||||
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX2WithUnknownHeader(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
"<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+
|
||||
"<?OFX OFXHEADER=\"200\" VERSION=\"211\" SECURITY=\"NONE\" OLDFILEUID=\"NONE\" NEWFILEUID=\"NONE\" FOO=\"BAR\"?>"+
|
||||
"<OFX>"+
|
||||
"</OFX>"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
ofxFile, err := reader.read(context)
|
||||
assert.NotNil(t, ofxFile)
|
||||
|
||||
assert.NotNil(t, ofxFile.FileHeader)
|
||||
assert.Equal(t, ofxVersion2, ofxFile.FileHeader.OFXDeclarationVersion)
|
||||
assert.Equal(t, "211", ofxFile.FileHeader.OFXDataVersion)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.Security)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.OldFileUid)
|
||||
assert.Equal(t, "NONE", ofxFile.FileHeader.NewFileUid)
|
||||
}
|
||||
|
||||
func TestCreateNewOFXFileReader_OFX2WithSGML(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader, err := createNewOFXFileReader(context, []byte(
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
package sgml
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
)
|
||||
|
||||
const sgmlTagName = "sgml"
|
||||
const sgmlNameFieldName = "SGMLName"
|
||||
const xmlTagName = "xml" // reuse xml tag
|
||||
const xmlNameFieldName = "XMLName" // reuse xml tag
|
||||
|
||||
// sgmlFieldType represents SGML field type
|
||||
type sgmlFieldType byte
|
||||
|
||||
// Transaction template types
|
||||
const (
|
||||
sgmlNotSupportedField sgmlFieldType = 0
|
||||
sgmlTextualField sgmlFieldType = 1
|
||||
sgmlStructField sgmlFieldType = 2
|
||||
sgmlStructSliceField sgmlFieldType = 3
|
||||
)
|
||||
|
||||
// sgmlTypeInfo represents the struct of SGML type reflection info
|
||||
type sgmlTypeInfo struct {
|
||||
supportedFields map[string]*sgmlFieldInfo
|
||||
}
|
||||
|
||||
// sgmlFieldInfo represents the struct of SGML field info
|
||||
type sgmlFieldInfo struct {
|
||||
sgmlFieldName string
|
||||
sgmlFieldType sgmlFieldType
|
||||
structFieldName string
|
||||
}
|
||||
|
||||
type Decoder struct {
|
||||
xmlDecoder *xml.Decoder
|
||||
}
|
||||
|
||||
var sgmlTypeInfoMap sync.Map // map[reflect.Type]*typeInfo
|
||||
|
||||
// Decode unmarshal the specified struct instance and returns whether error occurs
|
||||
func (d *Decoder) Decode(v any) error {
|
||||
value := reflect.ValueOf(v).Elem()
|
||||
finalValue := value
|
||||
finalType := value.Type()
|
||||
|
||||
for finalValue.Kind() == reflect.Pointer {
|
||||
finalValue = value.Elem()
|
||||
finalType = finalValue.Type()
|
||||
}
|
||||
|
||||
rootNameField, exists := finalType.FieldByName(sgmlNameFieldName)
|
||||
|
||||
if !exists {
|
||||
rootNameField, exists = finalType.FieldByName(xmlNameFieldName)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
rootElementName := rootNameField.Tag.Get(sgmlTagName)
|
||||
|
||||
if rootElementName == "" {
|
||||
rootElementName = rootNameField.Tag.Get(xmlTagName)
|
||||
}
|
||||
|
||||
for {
|
||||
token, err := d.xmlDecoder.RawToken()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
switch token := token.(type) {
|
||||
case xml.StartElement:
|
||||
if token.Name.Local == rootElementName {
|
||||
return d.unmarshal(value.Elem(), rootElementName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Decoder) unmarshal(element reflect.Value, elementName string) error {
|
||||
typeInfo, err := d.getStructTypeInfo(element.Type())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if typeInfo == nil {
|
||||
return errs.ErrInvalidSGMLFile
|
||||
}
|
||||
|
||||
textualFieldWithoutEndElementNames := make(map[string]bool)
|
||||
textualFieldValues := make(map[string]string)
|
||||
|
||||
hasEndElement := false
|
||||
currentSGMLFieldName := ""
|
||||
|
||||
for {
|
||||
token, err := d.xmlDecoder.RawToken()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
switch token := token.(type) {
|
||||
case xml.StartElement:
|
||||
if fieldInfo, exists := typeInfo.supportedFields[token.Name.Local]; exists {
|
||||
if fieldInfo.sgmlFieldType == sgmlStructField || fieldInfo.sgmlFieldType == sgmlStructSliceField {
|
||||
field := element.FieldByName(fieldInfo.structFieldName)
|
||||
childElementType := field.Type()
|
||||
childElementKind := field.Kind()
|
||||
var childElement reflect.Value
|
||||
|
||||
if fieldInfo.sgmlFieldType == sgmlStructSliceField {
|
||||
childElementType = childElementType.Elem()
|
||||
childElementKind = childElementType.Kind()
|
||||
}
|
||||
|
||||
if childElementKind == reflect.Pointer {
|
||||
childElement = reflect.New(childElementType.Elem())
|
||||
} else if childElementKind == reflect.Struct {
|
||||
childElement = reflect.New(childElementType)
|
||||
}
|
||||
|
||||
err := d.unmarshal(childElement.Elem(), fieldInfo.sgmlFieldName)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if childElementKind == reflect.Struct {
|
||||
childElement = childElement.Elem()
|
||||
}
|
||||
|
||||
if fieldInfo.sgmlFieldType == sgmlStructField {
|
||||
field.Set(childElement)
|
||||
} else if fieldInfo.sgmlFieldType == sgmlStructSliceField {
|
||||
if field.Len() == 0 {
|
||||
slice := reflect.MakeSlice(reflect.SliceOf(childElement.Type()), 0, 0)
|
||||
field.Set(reflect.Append(slice, childElement))
|
||||
} else {
|
||||
field.Set(reflect.Append(field.Addr().Elem(), childElement))
|
||||
}
|
||||
}
|
||||
} else if fieldInfo.sgmlFieldType == sgmlTextualField {
|
||||
currentSGMLFieldName = token.Name.Local
|
||||
textualFieldWithoutEndElementNames[token.Name.Local] = true
|
||||
}
|
||||
}
|
||||
case xml.EndElement:
|
||||
if fieldInfo, exists := typeInfo.supportedFields[token.Name.Local]; exists {
|
||||
if fieldInfo.sgmlFieldType == sgmlTextualField {
|
||||
delete(textualFieldWithoutEndElementNames, token.Name.Local)
|
||||
}
|
||||
} else if token.Name.Local == elementName {
|
||||
hasEndElement = true
|
||||
break
|
||||
}
|
||||
case xml.CharData:
|
||||
if currentSGMLFieldName != "" {
|
||||
if fieldInfo, exists := typeInfo.supportedFields[currentSGMLFieldName]; exists {
|
||||
if fieldInfo.sgmlFieldType == sgmlTextualField {
|
||||
textualFieldValues[currentSGMLFieldName] = string(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentSGMLFieldName = ""
|
||||
}
|
||||
|
||||
if hasEndElement {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEndElement {
|
||||
return errs.ErrInvalidSGMLFile
|
||||
}
|
||||
|
||||
for sgmlFieldName, fieldValue := range textualFieldValues {
|
||||
finalValue := d.getActualFieldValue(sgmlFieldName, fieldValue, textualFieldWithoutEndElementNames)
|
||||
fieldInfo, exists := typeInfo.supportedFields[sgmlFieldName]
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
field := element.FieldByName(fieldInfo.structFieldName)
|
||||
field.SetString(finalValue)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Decoder) getStructTypeInfo(reflectType reflect.Type) (*sgmlTypeInfo, error) {
|
||||
if reflectType.Kind() != reflect.Struct {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
typeInfo, exists := sgmlTypeInfoMap.Load(reflectType)
|
||||
|
||||
if exists {
|
||||
return typeInfo.(*sgmlTypeInfo), nil
|
||||
}
|
||||
|
||||
newTypeInfo := &sgmlTypeInfo{
|
||||
supportedFields: make(map[string]*sgmlFieldInfo),
|
||||
}
|
||||
|
||||
for i := 0; i < reflectType.NumField(); i++ {
|
||||
field := reflectType.Field(i)
|
||||
|
||||
if field.Anonymous {
|
||||
fieldType := field.Type
|
||||
|
||||
if fieldType.Kind() == reflect.Struct {
|
||||
fieldSgmlTypeInfo, err := d.getStructTypeInfo(fieldType)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for sgmlFieldName, fieldInfo := range fieldSgmlTypeInfo.supportedFields {
|
||||
newTypeInfo.supportedFields[sgmlFieldName] = fieldInfo
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
} else if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
sgmlFieldName := field.Tag.Get(sgmlTagName)
|
||||
|
||||
if sgmlFieldName == "" {
|
||||
sgmlFieldName = field.Tag.Get(xmlTagName)
|
||||
}
|
||||
|
||||
if sgmlFieldName == "" || field.Name == sgmlNameFieldName || field.Name == xmlNameFieldName {
|
||||
continue
|
||||
}
|
||||
|
||||
sgmlFieldType := sgmlNotSupportedField
|
||||
finalFieldType := field.Type
|
||||
|
||||
for finalFieldType.Kind() == reflect.Pointer {
|
||||
finalFieldType = finalFieldType.Elem()
|
||||
}
|
||||
|
||||
switch finalFieldType.Kind() {
|
||||
case reflect.String:
|
||||
sgmlFieldType = sgmlTextualField
|
||||
case reflect.Struct:
|
||||
sgmlFieldType = sgmlStructField
|
||||
case reflect.Slice:
|
||||
childFinalFieldType := finalFieldType.Elem()
|
||||
|
||||
for childFinalFieldType.Kind() == reflect.Pointer {
|
||||
childFinalFieldType = childFinalFieldType.Elem()
|
||||
}
|
||||
|
||||
if childFinalFieldType.Kind() == reflect.Struct {
|
||||
sgmlFieldType = sgmlStructSliceField
|
||||
}
|
||||
default:
|
||||
sgmlFieldType = sgmlNotSupportedField
|
||||
}
|
||||
|
||||
if sgmlFieldType == sgmlNotSupportedField {
|
||||
return nil, errs.ErrInvalidSGMLFile
|
||||
}
|
||||
|
||||
newTypeInfo.supportedFields[sgmlFieldName] = &sgmlFieldInfo{
|
||||
sgmlFieldName: sgmlFieldName,
|
||||
sgmlFieldType: sgmlFieldType,
|
||||
structFieldName: field.Name,
|
||||
}
|
||||
}
|
||||
|
||||
typeInfo, _ = sgmlTypeInfoMap.LoadOrStore(reflectType, newTypeInfo)
|
||||
|
||||
return typeInfo.(*sgmlTypeInfo), nil
|
||||
}
|
||||
|
||||
func (d *Decoder) getActualFieldValue(fieldName string, fieldValue string, textualFieldWithoutEndElementNames map[string]bool) string {
|
||||
_, notHasEndElement := textualFieldWithoutEndElementNames[fieldName]
|
||||
|
||||
if !notHasEndElement {
|
||||
return fieldValue
|
||||
}
|
||||
|
||||
for i := 0; i < len(fieldValue); i++ {
|
||||
if fieldValue[i] == '\r' || fieldValue[i] == '\n' {
|
||||
return fieldValue[0:i]
|
||||
}
|
||||
}
|
||||
|
||||
return fieldValue
|
||||
}
|
||||
|
||||
// NewDecoder creates a new SGML parser reading from specified io reader
|
||||
func NewDecoder(reader io.Reader) *Decoder {
|
||||
xmlDecoder := xml.NewDecoder(reader)
|
||||
xmlDecoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
return &Decoder{
|
||||
xmlDecoder: xmlDecoder,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
package sgml
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
)
|
||||
|
||||
type TestSimpleStruct struct {
|
||||
SGMLName string `sgml:"Root"`
|
||||
Text1 string `sgml:"Text1"`
|
||||
Text2 string `sgml:"Text2"`
|
||||
}
|
||||
|
||||
type TestNestedStruct1 struct {
|
||||
SGMLName string `sgml:"Root"`
|
||||
Child TestSimpleStruct `sgml:"Child"`
|
||||
Text3 string `sgml:"Text3"`
|
||||
Text4 string `sgml:"Text4"`
|
||||
}
|
||||
|
||||
type TestNestedStruct2 struct {
|
||||
SGMLName string `sgml:"Root"`
|
||||
Child *TestSimpleStruct `sgml:"Child"`
|
||||
Text3 string `sgml:"Text3"`
|
||||
Text4 string `sgml:"Text4"`
|
||||
}
|
||||
|
||||
type TestEmbeddedStruct struct {
|
||||
TestSimpleStruct
|
||||
Text5 string `sgml:"Text5"`
|
||||
Text6 string `sgml:"Text6"`
|
||||
}
|
||||
|
||||
type TestSliceStruct1 struct {
|
||||
SGMLName string `sgml:"Root"`
|
||||
Children []TestSimpleStruct `sgml:"Child"`
|
||||
Text7 string `sgml:"Text7"`
|
||||
}
|
||||
|
||||
type TestSliceStruct2 struct {
|
||||
SGMLName string `sgml:"Root"`
|
||||
Children []*TestSimpleStruct `sgml:"Child"`
|
||||
Text7 string `sgml:"Text7"`
|
||||
}
|
||||
|
||||
type TestSimpleStructWithXMLTag struct {
|
||||
XMLName xml.Name `xml:"Root"`
|
||||
Text1 string `xml:"Text1"`
|
||||
Text2 string `xml:"Text2"`
|
||||
}
|
||||
|
||||
type TestStructWithXMLTag struct {
|
||||
XMLName xml.Name `xml:"Root"`
|
||||
Child TestSimpleStructWithXMLTag `xml:"Child"`
|
||||
Text3 string `xml:"Text3"`
|
||||
Text4 string `xml:"Text4"`
|
||||
}
|
||||
|
||||
type TestNotExportedFieldStruct struct {
|
||||
SGMLName string `sgml:"Root"`
|
||||
Text1 string `sgml:"Text1"`
|
||||
Text2 string
|
||||
text3 string `sgml:"Text3"`
|
||||
}
|
||||
|
||||
type TestUnsupportedStruct struct {
|
||||
SGMLName string `sgml:"Root"`
|
||||
Number int `sgml:"Number"`
|
||||
}
|
||||
|
||||
type TestEmbeddedUnsupportedStruct struct {
|
||||
TestUnsupportedStruct
|
||||
Text1 string `sgml:"Text1"`
|
||||
}
|
||||
|
||||
func TestDecoderDecode(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Text1>Foo\n" +
|
||||
"<Text2>Bar\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestSimpleStruct{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.Equal(t, "Foo", testStruct.Text1)
|
||||
assert.Equal(t, "Bar", testStruct.Text2)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_WithRedundantFields(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Text1>Foo\n" +
|
||||
"<Text2>Bar\n" +
|
||||
"<Text3>Hello\n" +
|
||||
"<Child>\n" +
|
||||
"<Text4>World\n" +
|
||||
"</Child>\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestSimpleStruct{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.Equal(t, "Foo", testStruct.Text1)
|
||||
assert.Equal(t, "Bar", testStruct.Text2)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_WithEndElement(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Text1>Foo</Text1>\n" +
|
||||
"<Text2>Bar</Text2>\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestSimpleStruct{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.Equal(t, "Foo", testStruct.Text1)
|
||||
assert.Equal(t, "Bar", testStruct.Text2)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_WithoutBreakLine(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>" +
|
||||
"<Text1>Foo" +
|
||||
"<Text2>Bar" +
|
||||
"</Root>"))
|
||||
|
||||
testStruct := &TestSimpleStruct{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.Equal(t, "Foo", testStruct.Text1)
|
||||
assert.Equal(t, "Bar", testStruct.Text2)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_NestedStruct(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Child>\n" +
|
||||
"<Text1>Hello\n" +
|
||||
"<Text2>World\n" +
|
||||
"</Child>\n" +
|
||||
"<Text3>Foo\n" +
|
||||
"<Text4>Bar\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestNestedStruct1{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.NotNil(t, testStruct.Child)
|
||||
assert.Equal(t, "Hello", testStruct.Child.Text1)
|
||||
assert.Equal(t, "World", testStruct.Child.Text2)
|
||||
assert.Equal(t, "Foo", testStruct.Text3)
|
||||
assert.Equal(t, "Bar", testStruct.Text4)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_NestedStructUsingPointer(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Child>\n" +
|
||||
"<Text1>Hello\n" +
|
||||
"<Text2>World\n" +
|
||||
"</Child>\n" +
|
||||
"<Text3>Foo\n" +
|
||||
"<Text4>Bar\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestNestedStruct2{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.NotNil(t, testStruct.Child)
|
||||
assert.Equal(t, "Hello", testStruct.Child.Text1)
|
||||
assert.Equal(t, "World", testStruct.Child.Text2)
|
||||
assert.Equal(t, "Foo", testStruct.Text3)
|
||||
assert.Equal(t, "Bar", testStruct.Text4)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_EmbeddedStruct(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Text1>Hello\n" +
|
||||
"<Text2>World\n" +
|
||||
"<Text5>Foo\n" +
|
||||
"<Text6>Bar\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestEmbeddedStruct{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.Equal(t, "Hello", testStruct.Text1)
|
||||
assert.Equal(t, "World", testStruct.Text2)
|
||||
assert.Equal(t, "Foo", testStruct.Text5)
|
||||
assert.Equal(t, "Bar", testStruct.Text6)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_StructSlice(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Child>\n" +
|
||||
"<Text1>Hello\n" +
|
||||
"<Text2>World\n" +
|
||||
"</Child>\n" +
|
||||
"<Child>\n" +
|
||||
"<Text1>Hello2\n" +
|
||||
"<Text2>World2\n" +
|
||||
"</Child>\n" +
|
||||
"<Text7>Foo\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestSliceStruct1{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.Equal(t, 2, len(testStruct.Children))
|
||||
assert.Equal(t, "Hello", testStruct.Children[0].Text1)
|
||||
assert.Equal(t, "World", testStruct.Children[0].Text2)
|
||||
assert.Equal(t, "Hello2", testStruct.Children[1].Text1)
|
||||
assert.Equal(t, "World2", testStruct.Children[1].Text2)
|
||||
assert.Equal(t, "Foo", testStruct.Text7)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_StructSliceUsingPointer(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Child>\n" +
|
||||
"<Text1>Hello\n" +
|
||||
"<Text2>World\n" +
|
||||
"</Child>\n" +
|
||||
"<Child>\n" +
|
||||
"<Text1>Hello2\n" +
|
||||
"<Text2>World2\n" +
|
||||
"</Child>\n" +
|
||||
"<Text7>Foo\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestSliceStruct2{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.Equal(t, 2, len(testStruct.Children))
|
||||
assert.Equal(t, "Hello", testStruct.Children[0].Text1)
|
||||
assert.Equal(t, "World", testStruct.Children[0].Text2)
|
||||
assert.Equal(t, "Hello2", testStruct.Children[1].Text1)
|
||||
assert.Equal(t, "World2", testStruct.Children[1].Text2)
|
||||
assert.Equal(t, "Foo", testStruct.Text7)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_UsingXMLTag(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Child>\n" +
|
||||
"<Text1>Hello\n" +
|
||||
"<Text2>World\n" +
|
||||
"</Child>\n" +
|
||||
"<Text3>Foo\n" +
|
||||
"<Text4>Bar\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestStructWithXMLTag{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.NotNil(t, testStruct.Child)
|
||||
assert.Equal(t, "Hello", testStruct.Child.Text1)
|
||||
assert.Equal(t, "World", testStruct.Child.Text2)
|
||||
assert.Equal(t, "Foo", testStruct.Text3)
|
||||
assert.Equal(t, "Bar", testStruct.Text4)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_WithNotExportedFields(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Text1>Foo\n" +
|
||||
"<Text2>Bar\n" +
|
||||
"<Text3>Hello World\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestNotExportedFieldStruct{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, testStruct)
|
||||
assert.Equal(t, "Foo", testStruct.Text1)
|
||||
assert.Equal(t, "", testStruct.Text2)
|
||||
assert.Equal(t, "", testStruct.text3)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_StructWithoutEndElement(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Text1>Foo\n" +
|
||||
"<Text2>Bar\n"))
|
||||
|
||||
testStruct := &TestSimpleStruct{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.EqualError(t, err, errs.ErrInvalidSGMLFile.Message)
|
||||
|
||||
sgmlDecoder = NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Child>\n" +
|
||||
"<Text1>Hello\n" +
|
||||
"<Text2>World\n" +
|
||||
"<Text3>Foo\n" +
|
||||
"<Text4>Bar\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct2 := &TestNestedStruct2{}
|
||||
err = sgmlDecoder.Decode(&testStruct2)
|
||||
|
||||
assert.EqualError(t, err, errs.ErrInvalidSGMLFile.Message)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_WithNotSupportedField(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Number>1234\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestUnsupportedStruct{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.EqualError(t, err, errs.ErrInvalidSGMLFile.Message)
|
||||
}
|
||||
|
||||
func TestDecoderDecode_WithEmbeddedNotSupportedField(t *testing.T) {
|
||||
sgmlDecoder := NewDecoder(strings.NewReader(
|
||||
"<Root>\n" +
|
||||
"<Number>1234\n" +
|
||||
"<Text1>Foo\n" +
|
||||
"</Root>\n"))
|
||||
|
||||
testStruct := &TestEmbeddedUnsupportedStruct{}
|
||||
err := sgmlDecoder.Decode(&testStruct)
|
||||
|
||||
assert.EqualError(t, err, errs.ErrInvalidSGMLFile.Message)
|
||||
}
|
||||
@@ -24,4 +24,5 @@ var (
|
||||
ErrThereAreNotSupportedTransactionType = NewNormalError(NormalSubcategoryConverter, 17, http.StatusBadRequest, "there are not supported transaction type")
|
||||
ErrInvalidIIFFile = NewNormalError(NormalSubcategoryConverter, 18, http.StatusBadRequest, "invalid iif file")
|
||||
ErrInvalidOFXFile = NewNormalError(NormalSubcategoryConverter, 19, http.StatusBadRequest, "invalid ofx file")
|
||||
ErrInvalidSGMLFile = NewNormalError(NormalSubcategoryConverter, 20, http.StatusBadRequest, "invalid sgml file")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user