mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-19 01:04:25 +08:00
code refactor
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package ofx
|
package ofx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"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
|
// ofxFile represents the struct of open financial exchange (ofx) file
|
||||||
type ofxFile struct {
|
type ofxFile struct {
|
||||||
|
XMLName xml.Name `xml:"OFX"`
|
||||||
FileHeader *ofxFileHeader
|
FileHeader *ofxFileHeader
|
||||||
BankMessageResponseV1 *ofxBankMessageResponseV1
|
BankMessageResponseV1 *ofxBankMessageResponseV1 `xml:"BANKMSGSRSV1"`
|
||||||
CreditCardMessageResponseV1 *ofxCreditCardMessageResponseV1
|
CreditCardMessageResponseV1 *ofxCreditCardMessageResponseV1 `xml:"CREDITCARDMSGSRSV1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxFileHeader represents the struct of open financial exchange (ofx) file header
|
// 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
|
// ofxBankMessageResponseV1 represents the struct of open financial exchange (ofx) bank message response v1
|
||||||
type ofxBankMessageResponseV1 struct {
|
type ofxBankMessageResponseV1 struct {
|
||||||
StatementTransactionResponse *ofxBankStatementTransactionResponse
|
StatementTransactionResponse *ofxBankStatementTransactionResponse `xml:"STMTTRNRS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxCreditCardMessageResponseV1 represents the struct of open financial exchange (ofx) credit card message response v1
|
// ofxCreditCardMessageResponseV1 represents the struct of open financial exchange (ofx) credit card message response v1
|
||||||
type ofxCreditCardMessageResponseV1 struct {
|
type ofxCreditCardMessageResponseV1 struct {
|
||||||
StatementTransactionResponse *ofxCreditCardStatementTransactionResponse
|
StatementTransactionResponse *ofxCreditCardStatementTransactionResponse `xml:"CCSTMTTRNRS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxBankStatementTransactionResponse represents the struct of open financial exchange (ofx) bank statement transaction response
|
// ofxBankStatementTransactionResponse represents the struct of open financial exchange (ofx) bank statement transaction response
|
||||||
type ofxBankStatementTransactionResponse struct {
|
type ofxBankStatementTransactionResponse struct {
|
||||||
StatementResponse *ofxBankStatementResponse
|
StatementResponse *ofxBankStatementResponse `xml:"STMTRS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxCreditCardStatementTransactionResponse represents the struct of open financial exchange (ofx) credit card statement transaction response
|
// ofxCreditCardStatementTransactionResponse represents the struct of open financial exchange (ofx) credit card statement transaction response
|
||||||
type ofxCreditCardStatementTransactionResponse struct {
|
type ofxCreditCardStatementTransactionResponse struct {
|
||||||
StatementResponse *ofxCreditCardStatementResponse
|
StatementResponse *ofxCreditCardStatementResponse `xml:"CCSTMTRS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxBankStatementResponse represents the struct of open financial exchange (ofx) bank statement response
|
// ofxBankStatementResponse represents the struct of open financial exchange (ofx) bank statement response
|
||||||
type ofxBankStatementResponse struct {
|
type ofxBankStatementResponse struct {
|
||||||
DefaultCurrency string
|
DefaultCurrency string `xml:"CURDEF"`
|
||||||
AccountFrom *ofxBankAccount
|
AccountFrom *ofxBankAccount `xml:"BANKACCTFROM"`
|
||||||
TransactionList *ofxBankTransactionList
|
TransactionList *ofxBankTransactionList `xml:"BANKTRANLIST"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxCreditCardStatementResponse represents the struct of open financial exchange (ofx) credit card statement response
|
// ofxCreditCardStatementResponse represents the struct of open financial exchange (ofx) credit card statement response
|
||||||
type ofxCreditCardStatementResponse struct {
|
type ofxCreditCardStatementResponse struct {
|
||||||
DefaultCurrency string
|
DefaultCurrency string `xml:"CURDEF"`
|
||||||
AccountFrom *ofxCreditCardAccount
|
AccountFrom *ofxCreditCardAccount `xml:"CCACCTFROM"`
|
||||||
TransactionList *ofxCreditCardTransactionList
|
TransactionList *ofxCreditCardTransactionList `xml:"BANKTRANLIST"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxBankAccount represents the struct of open financial exchange (ofx) bank account
|
// ofxBankAccount represents the struct of open financial exchange (ofx) bank account
|
||||||
type ofxBankAccount struct {
|
type ofxBankAccount struct {
|
||||||
BankId string
|
BankId string `xml:"BANKID"`
|
||||||
BranchId string
|
BranchId string `xml:"BRANCHID"`
|
||||||
AccountId string
|
AccountId string `xml:"ACCTID"`
|
||||||
AccountType ofxAccountType
|
AccountType ofxAccountType `xml:"ACCTTYPE"`
|
||||||
AccountKey string
|
AccountKey string `xml:"ACCTKEY"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxCreditCardAccount represents the struct of open financial exchange (ofx) credit card account
|
// ofxCreditCardAccount represents the struct of open financial exchange (ofx) credit card account
|
||||||
type ofxCreditCardAccount struct {
|
type ofxCreditCardAccount struct {
|
||||||
AccountId string
|
AccountId string `xml:"ACCTID"`
|
||||||
AccountKey string
|
AccountKey string `xml:"ACCTKEY"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxBankTransactionList represents the struct of open financial exchange (ofx) bank transaction list
|
// ofxBankTransactionList represents the struct of open financial exchange (ofx) bank transaction list
|
||||||
type ofxBankTransactionList struct {
|
type ofxBankTransactionList struct {
|
||||||
StartDate string
|
StartDate string `xml:"DTSTART"`
|
||||||
EndDate string
|
EndDate string `xml:"DTEND"`
|
||||||
StatementTransactions []*ofxBankStatementTransaction
|
StatementTransactions []*ofxBankStatementTransaction `xml:"STMTTRN"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxCreditCardTransactionList represents the struct of open financial exchange (ofx) credit card transaction list
|
// ofxCreditCardTransactionList represents the struct of open financial exchange (ofx) credit card transaction list
|
||||||
type ofxCreditCardTransactionList struct {
|
type ofxCreditCardTransactionList struct {
|
||||||
StartDate string
|
StartDate string `xml:"DTSTART"`
|
||||||
EndDate string
|
EndDate string `xml:"DTEND"`
|
||||||
StatementTransactions []*ofxCreditCardStatementTransaction
|
StatementTransactions []*ofxCreditCardStatementTransaction `xml:"STMTTRN"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxBaseStatementTransaction represents the struct of open financial exchange (ofx) base statement transaction
|
// ofxBaseStatementTransaction represents the struct of open financial exchange (ofx) base statement transaction
|
||||||
type ofxBaseStatementTransaction struct {
|
type ofxBaseStatementTransaction struct {
|
||||||
TransactionId string
|
TransactionId string `xml:"FITID"`
|
||||||
TransactionType ofxTransactionType
|
TransactionType ofxTransactionType `xml:"TRNTYPE"`
|
||||||
PostedDate string
|
PostedDate string `xml:"DTPOSTED"`
|
||||||
Amount string
|
Amount string `xml:"TRNAMT"`
|
||||||
Name string
|
Name string `xml:"NAME"`
|
||||||
Payee *ofxPayee
|
Payee *ofxPayee `xml:"PAYEE"`
|
||||||
Memo string
|
Memo string `xml:"MEMO"`
|
||||||
Currency string
|
Currency string `xml:"CURRENCY"`
|
||||||
OriginalCurrency string
|
OriginalCurrency string `xml:"ORIGCURRENCY"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxBankStatementTransaction represents the struct of open financial exchange (ofx) bank statement transaction
|
// ofxBankStatementTransaction represents the struct of open financial exchange (ofx) bank statement transaction
|
||||||
type ofxBankStatementTransaction struct {
|
type ofxBankStatementTransaction struct {
|
||||||
ofxBaseStatementTransaction
|
ofxBaseStatementTransaction
|
||||||
AccountTo *ofxBankAccount
|
AccountTo *ofxBankAccount `xml:"BANKACCTTO"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxCreditCardStatementTransaction represents the struct of open financial exchange (ofx) credit card statement transaction
|
// ofxCreditCardStatementTransaction represents the struct of open financial exchange (ofx) credit card statement transaction
|
||||||
type ofxCreditCardStatementTransaction struct {
|
type ofxCreditCardStatementTransaction struct {
|
||||||
ofxBaseStatementTransaction
|
ofxBaseStatementTransaction
|
||||||
AccountTo *ofxCreditCardAccount
|
AccountTo *ofxCreditCardAccount `xml:"CCACCTTO"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ofxPayee represents the struct of open financial exchange (ofx) payee info
|
// ofxPayee represents the struct of open financial exchange (ofx) payee info
|
||||||
type ofxPayee struct {
|
type ofxPayee struct {
|
||||||
Name string
|
Name string `xml:"NAME"`
|
||||||
Address1 string
|
Address1 string `xml:"ADDR1"`
|
||||||
Address2 string
|
Address2 string `xml:"ADDR2"`
|
||||||
Address3 string
|
Address3 string `xml:"ADDR3"`
|
||||||
City string
|
City string `xml:"CITY"`
|
||||||
State string
|
State string `xml:"STATE"`
|
||||||
PostalCode string
|
PostalCode string `xml:"POSTALCODE"`
|
||||||
Country string
|
Country string `xml:"COUNTRY"`
|
||||||
Phone string
|
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)
|
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()
|
context := core.NewNullContext()
|
||||||
reader, err := createNewOFXFileReader(context, []byte(
|
reader, err := createNewOFXFileReader(context, []byte(
|
||||||
"\n"+
|
"\n"+
|
||||||
@@ -194,6 +559,104 @@ func TestCreateNewOFXFileReader_OFX1WithBlanklines(t *testing.T) {
|
|||||||
assert.Equal(t, "123.45", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].Amount)
|
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) {
|
func TestCreateNewOFXFileReader_OFX2(t *testing.T) {
|
||||||
context := core.NewNullContext()
|
context := core.NewNullContext()
|
||||||
reader, err := createNewOFXFileReader(context, []byte(
|
reader, err := createNewOFXFileReader(context, []byte(
|
||||||
@@ -328,14 +791,15 @@ func TestCreateNewOFXFileReader_OFX2WithInvalidHeader(t *testing.T) {
|
|||||||
"<?OFX?>"+
|
"<?OFX?>"+
|
||||||
"<OFX>"+
|
"<OFX>"+
|
||||||
"</OFX>"))
|
"</OFX>"))
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
|
||||||
|
|
||||||
_, err = createNewOFXFileReader(context, []byte(
|
_, err = createNewOFXFileReader(context, []byte(
|
||||||
"<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+
|
"<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+
|
||||||
"<?OFX OFXHEADER=200?>"+
|
"<?OFX OFXHEADER=200?>"+
|
||||||
"<OFX>"+
|
"<OFX>"+
|
||||||
"</OFX>"))
|
"</OFX>"))
|
||||||
|
|
||||||
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
|
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
|
||||||
|
|
||||||
_, err = createNewOFXFileReader(context, []byte(
|
_, err = createNewOFXFileReader(context, []byte(
|
||||||
"<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+
|
"<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+
|
||||||
"<?OFX OFXHEADER=\"200\" VERSION=\"211\" SECURITY=\"NONE\" OLDFILEUID=\"NONE\" NEWFILEUID=\"NONE\" test=\"\"?>"+
|
"<?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)
|
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) {
|
func TestCreateNewOFXFileReader_OFX2WithSGML(t *testing.T) {
|
||||||
context := core.NewNullContext()
|
context := core.NewNullContext()
|
||||||
reader, err := createNewOFXFileReader(context, []byte(
|
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")
|
ErrThereAreNotSupportedTransactionType = NewNormalError(NormalSubcategoryConverter, 17, http.StatusBadRequest, "there are not supported transaction type")
|
||||||
ErrInvalidIIFFile = NewNormalError(NormalSubcategoryConverter, 18, http.StatusBadRequest, "invalid iif file")
|
ErrInvalidIIFFile = NewNormalError(NormalSubcategoryConverter, 18, http.StatusBadRequest, "invalid iif file")
|
||||||
ErrInvalidOFXFile = NewNormalError(NormalSubcategoryConverter, 19, http.StatusBadRequest, "invalid ofx file")
|
ErrInvalidOFXFile = NewNormalError(NormalSubcategoryConverter, 19, http.StatusBadRequest, "invalid ofx file")
|
||||||
|
ErrInvalidSGMLFile = NewNormalError(NormalSubcategoryConverter, 20, http.StatusBadRequest, "invalid sgml file")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1135,6 +1135,7 @@
|
|||||||
"there are not supported transaction type": "There are not supported transaction type in import file",
|
"there are not supported transaction type": "There are not supported transaction type in import file",
|
||||||
"invalid iif file": "Invalid IIF file",
|
"invalid iif file": "Invalid IIF file",
|
||||||
"invalid ofx file": "Invalid OFX file",
|
"invalid ofx file": "Invalid OFX file",
|
||||||
|
"invalid sgml file": "Invalid SGML file",
|
||||||
"query items cannot be blank": "There are no query items",
|
"query items cannot be blank": "There are no query items",
|
||||||
"query items too much": "There are too many query items",
|
"query items too much": "There are too many query items",
|
||||||
"query items have invalid item": "There is invalid item in query items",
|
"query items have invalid item": "There is invalid item in query items",
|
||||||
|
|||||||
@@ -1135,6 +1135,7 @@
|
|||||||
"there are not supported transaction type": "导入文件中有不支持的交易类型",
|
"there are not supported transaction type": "导入文件中有不支持的交易类型",
|
||||||
"invalid iif file": "无效的 IIF 文件",
|
"invalid iif file": "无效的 IIF 文件",
|
||||||
"invalid ofx file": "无效的 OFX 文件",
|
"invalid ofx file": "无效的 OFX 文件",
|
||||||
|
"invalid sgml file": "无效的 SGML 文件",
|
||||||
"query items cannot be blank": "请求项目不能为空",
|
"query items cannot be blank": "请求项目不能为空",
|
||||||
"query items too much": "请求项目过多",
|
"query items too much": "请求项目过多",
|
||||||
"query items have invalid item": "请求项目中有非法项目",
|
"query items have invalid item": "请求项目中有非法项目",
|
||||||
|
|||||||
Reference in New Issue
Block a user