mirror of
https://github.com/aclindsa/ofxgo.git
synced 2024-11-24 20:10:06 -05:00
Continue parsing after hitting validation errors
Supports mixed-case severity for Ally Bank's Quicken downloads 🙄
This commit is contained in:
parent
f75592381a
commit
677a09295a
49
response.go
49
response.go
@ -4,10 +4,12 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/aclindsa/xml"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aclindsa/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Response is the top-level object returned from a parsed OFX response file.
|
// Response is the top-level object returned from a parsed OFX response file.
|
||||||
@ -234,13 +236,14 @@ func decodeMessageSet(d *xml.Decoder, start xml.StartElement, msgs *[]Message, v
|
|||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("Invalid message set: " + start.Name.Local)
|
return errors.New("Invalid message set: " + start.Name.Local)
|
||||||
}
|
}
|
||||||
|
var errs ErrInvalid
|
||||||
for {
|
for {
|
||||||
tok, err := nextNonWhitespaceToken(d)
|
tok, err := nextNonWhitespaceToken(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
||||||
// If we found the end of our starting element, we're done parsing
|
// If we found the end of our starting element, we're done parsing
|
||||||
return nil
|
return errs.ErrOrNil()
|
||||||
} else if startElement, ok := tok.(xml.StartElement); ok {
|
} else if startElement, ok := tok.(xml.StartElement); ok {
|
||||||
responseType, ok := setTypes[startElement.Name.Local]
|
responseType, ok := setTypes[startElement.Name.Local]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -256,7 +259,7 @@ func decodeMessageSet(d *xml.Decoder, start xml.StartElement, msgs *[]Message, v
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ok, err := responseMessage.Valid(version); !ok {
|
if ok, err := responseMessage.Valid(version); !ok {
|
||||||
return err
|
errs.AddErr(err)
|
||||||
}
|
}
|
||||||
*msgs = append(*msgs, responseMessage)
|
*msgs = append(*msgs, responseMessage)
|
||||||
} else {
|
} else {
|
||||||
@ -323,6 +326,8 @@ func ParseResponse(reader io.Reader) (*Response, error) {
|
|||||||
return nil, errors.New("Missing opening SIGNONMSGSRSV1 xml element")
|
return nil, errors.New("Missing opening SIGNONMSGSRSV1 xml element")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errs ErrInvalid
|
||||||
|
|
||||||
tok, err = nextNonWhitespaceToken(decoder)
|
tok, err = nextNonWhitespaceToken(decoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -330,7 +335,7 @@ func ParseResponse(reader io.Reader) (*Response, error) {
|
|||||||
return nil, errors.New("Missing closing SIGNONMSGSRSV1 xml element")
|
return nil, errors.New("Missing closing SIGNONMSGSRSV1 xml element")
|
||||||
}
|
}
|
||||||
if ok, err := or.Signon.Valid(or.Version); !ok {
|
if ok, err := or.Signon.Valid(or.Version); !ok {
|
||||||
return nil, err
|
errs.AddErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var messageSlices = map[string]*[]Message{
|
var messageSlices = map[string]*[]Message{
|
||||||
@ -355,14 +360,17 @@ func ParseResponse(reader io.Reader) (*Response, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
|
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
|
||||||
return &or, nil // found closing XML element, so we're done
|
return &or, errs.ErrOrNil() // found closing XML element, so we're done
|
||||||
} else if start, ok := tok.(xml.StartElement); ok {
|
} else if start, ok := tok.(xml.StartElement); ok {
|
||||||
slice, ok := messageSlices[start.Name.Local]
|
slice, ok := messageSlices[start.Name.Local]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("Invalid message set: " + start.Name.Local)
|
return nil, errors.New("Invalid message set: " + start.Name.Local)
|
||||||
}
|
}
|
||||||
if err := decodeMessageSet(decoder, start, slice, or.Version); err != nil {
|
if err := decodeMessageSet(decoder, start, slice, or.Version); err != nil {
|
||||||
return nil, err
|
if _, ok := err.(ErrInvalid); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
errs.AddErr(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("Found unexpected token")
|
return nil, errors.New("Found unexpected token")
|
||||||
@ -436,3 +444,32 @@ func (or *Response) Marshal() (*bytes.Buffer, error) {
|
|||||||
}
|
}
|
||||||
return &b, nil
|
return &b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrInvalid represents validation failures while parsing an OFX response
|
||||||
|
// If an institution returns slightly malformed data, ParseResponse will return a best-effort parsed response and a validation error.
|
||||||
|
type ErrInvalid []error
|
||||||
|
|
||||||
|
func (e ErrInvalid) Error() string {
|
||||||
|
var errStrings []string
|
||||||
|
for _, err := range e {
|
||||||
|
errStrings = append(errStrings, err.Error())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Validation failed: %s", strings.Join(errStrings, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrInvalid) AddErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
if errs, ok := err.(ErrInvalid); ok {
|
||||||
|
*e = append(*e, errs...)
|
||||||
|
} else {
|
||||||
|
*e = append(*e, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrInvalid) ErrOrNil() error {
|
||||||
|
if len(e) > 0 {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
118
response_test.go
118
response_test.go
@ -1,13 +1,16 @@
|
|||||||
package ofxgo_test
|
package ofxgo_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"github.com/aclindsa/xml"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aclindsa/ofxgo"
|
||||||
|
"github.com/aclindsa/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Attempt to find a method on the provided Value called 'Equal' which is a
|
// Attempt to find a method on the provided Value called 'Equal' which is a
|
||||||
@ -171,3 +174,114 @@ func TestValidSamples(t *testing.T) {
|
|||||||
filepath.Walk("samples/valid_responses", fn)
|
filepath.Walk("samples/valid_responses", fn)
|
||||||
filepath.Walk("samples/busted_responses", fn)
|
filepath.Walk("samples/busted_responses", fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInvalidResponse(t *testing.T) {
|
||||||
|
// in this example, the severity is invalid due to mixed upper and lower case letters
|
||||||
|
resp, err := ofxgo.ParseResponse(bytes.NewReader([]byte(`
|
||||||
|
OFXHEADER:100
|
||||||
|
DATA:OFXSGML
|
||||||
|
VERSION:102
|
||||||
|
SECURITY:NONE
|
||||||
|
ENCODING:USASCII
|
||||||
|
CHARSET:1252
|
||||||
|
COMPRESSION:NONE
|
||||||
|
OLDFILEUID:NONE
|
||||||
|
NEWFILEUID:NONE
|
||||||
|
|
||||||
|
<OFX>
|
||||||
|
<SIGNONMSGSRSV1>
|
||||||
|
<SONRS>
|
||||||
|
<STATUS>
|
||||||
|
<CODE>0</CODE>
|
||||||
|
<SEVERITY>Info</SEVERITY>
|
||||||
|
</STATUS>
|
||||||
|
<LANGUAGE>ENG</LANGUAGE>
|
||||||
|
</SONRS>
|
||||||
|
</SIGNONMSGSRSV1>
|
||||||
|
<BANKMSGSRSV1>
|
||||||
|
<STMTTRNRS>
|
||||||
|
<TRNUID>0</TRNUID>
|
||||||
|
<STATUS>
|
||||||
|
<CODE>0</CODE>
|
||||||
|
<SEVERITY>Info</SEVERITY>
|
||||||
|
</STATUS>
|
||||||
|
</STMTTRNRS>
|
||||||
|
</BANKMSGSRSV1>
|
||||||
|
</OFX>
|
||||||
|
`)))
|
||||||
|
expectedErr := "Validation failed: Invalid STATUS>SEVERITY; Invalid STATUS>SEVERITY"
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("ParseResponse should fail with %q, found nil", expectedErr)
|
||||||
|
}
|
||||||
|
if _, ok := err.(ofxgo.ErrInvalid); !ok {
|
||||||
|
t.Errorf("ParseResponse should return an error with type ErrInvalid, found %T", err)
|
||||||
|
}
|
||||||
|
if err.Error() != expectedErr {
|
||||||
|
t.Errorf("ParseResponse should fail with %q, found %v", expectedErr, err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Errorf("Response must not be nil if only validation errors are present")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrInvalidError(t *testing.T) {
|
||||||
|
expectedErr := `Validation failed: A; B; C`
|
||||||
|
actualErr := ofxgo.ErrInvalid{
|
||||||
|
errors.New("A"),
|
||||||
|
errors.New("B"),
|
||||||
|
errors.New("C"),
|
||||||
|
}.Error()
|
||||||
|
if expectedErr != actualErr {
|
||||||
|
t.Errorf("Unexpected invalid error message to be %q, but was: %s", expectedErr, actualErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrInvalidAddErr(t *testing.T) {
|
||||||
|
t.Run("nil error should be a no-op", func(t *testing.T) {
|
||||||
|
var errs ofxgo.ErrInvalid
|
||||||
|
errs.AddErr(nil)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("Nil err should not be added")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adds an error normally", func(t *testing.T) {
|
||||||
|
var errs ofxgo.ErrInvalid
|
||||||
|
errs.AddErr(errors.New("some error"))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adding the same type should flatten the errors", func(t *testing.T) {
|
||||||
|
var errs ofxgo.ErrInvalid
|
||||||
|
errs.AddErr(ofxgo.ErrInvalid{
|
||||||
|
errors.New("A"),
|
||||||
|
errors.New("B"),
|
||||||
|
})
|
||||||
|
errs.AddErr(ofxgo.ErrInvalid{
|
||||||
|
errors.New("C"),
|
||||||
|
})
|
||||||
|
if len(errs) != 3 {
|
||||||
|
t.Errorf("Errors should be flattened like [A, B, C], but found: %+v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrInvalidErrOrNil(t *testing.T) {
|
||||||
|
var errs ofxgo.ErrInvalid
|
||||||
|
if err := errs.ErrOrNil(); err != nil {
|
||||||
|
t.Errorf("No added errors should return nil, found: %v", err)
|
||||||
|
}
|
||||||
|
someError := errors.New("some error")
|
||||||
|
errs.AddErr(someError)
|
||||||
|
err := errs.ErrOrNil()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected an error, found nil.")
|
||||||
|
}
|
||||||
|
if _, ok := err.(ofxgo.ErrInvalid); !ok {
|
||||||
|
t.Fatalf("Expected err to be of type ErrInvalid, found: %T", err)
|
||||||
|
}
|
||||||
|
errInvalid := err.(ofxgo.ErrInvalid)
|
||||||
|
if len(errInvalid) != 1 || errInvalid[0] != someError {
|
||||||
|
t.Errorf("Expected ErrOrNil to return itself, found: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user