ofx died 3y ago

This commit is contained in:
Bel LaPointe
2025-05-23 18:59:39 -06:00
parent e581b7835c
commit f5d82fc6aa
96 changed files with 33152 additions and 0 deletions

280
vendor/github.com/aclindsa/ofxgo/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,280 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS

132
vendor/github.com/aclindsa/ofxgo/README.md generated vendored Normal file
View File

@@ -0,0 +1,132 @@
# OFXGo
[![Go Report Card](https://goreportcard.com/badge/github.com/aclindsa/ofxgo)](https://goreportcard.com/report/github.com/aclindsa/ofxgo)
[![Build Status](https://github.com/aclindsa/ofxgo/workflows/ofxgo%20CI%20Test/badge.svg?branch=master)](https://github.com/aclindsa/ofxgo/actions?query=workflow%3A%22ofxgo+CI+Test%22+branch%3Amaster)
[![Coverage Status](https://coveralls.io/repos/github/aclindsa/ofxgo/badge.svg?branch=master)](https://coveralls.io/github/aclindsa/ofxgo?branch=master)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/aclindsa?ofxgo)](https://pkg.go.dev/github.com/aclindsa/ofxgo)
**OFXGo** is a library for querying OFX servers and/or parsing the responses. It
also provides an example command-line client to demonstrate the use of the
library.
## Goals
The main purpose of this project is to provide a library to make it easier to
query financial information with OFX from the comfort of Golang, without having
to marshal/unmarshal to SGML or XML. The library does _not_ intend to abstract
away all of the details of the OFX specification, which would be difficult to do
well. Instead, it exposes the OFX SGML/XML hierarchy as structs which mostly
resemble it. Its primary goal is to enable the creation of other personal
finance software in Go (as it was created to allow me to fetch OFX transactions
for my own project, [MoneyGo](https://github.com/aclindsa/moneygo)).
Because the OFX specification is rather... 'comprehensive,' it can be difficult
for those unfamiliar with it to figure out where to start. To that end, I have
created a sample command-line client which uses the library to do simple tasks
(currently it does little more than list accounts and query for balances and
transactions). My hope is that by studying its code, new users will be able to
figure out how to use the library much faster than staring at the OFX
specification (or this library's [API
documentation](https://pkg.go.dev/github.com/aclindsa/ofxgo)). The command-line client
also serves as an easy way for me to test/debug the library with actual
financial institutions, which frequently have 'quirks' in their implementations.
The command-line client can be found in the [cmd/ofx
directory](https://github.com/aclindsa/ofxgo/tree/master/cmd/ofx) of this
repository.
## Library documentation
Documentation can be found with the `go doc` tool, or at
https://pkg.go.dev/github.com/aclindsa/ofxgo
## Example Usage
The following code snippet demonstrates how to use OFXGo to query and parse
OFX code from a checking account, printing the balance and returned transactions:
```go
client := ofxgo.BasicClient{} // Accept the default Client settings
// These values are specific to your bank
var query ofxgo.Request
query.URL = "https://secu.example.com/ofx"
query.Signon.Org = ofxgo.String("SECU")
query.Signon.Fid = ofxgo.String("1234")
// Set your username/password
query.Signon.UserID = ofxgo.String("username")
query.Signon.UserPass = ofxgo.String("hunter2")
uid, _ := ofxgo.RandomUID() // Handle error in real code
query.Bank = append(query.Bank, &ofxgo.StatementRequest{
TrnUID: *uid,
BankAcctFrom: ofxgo.BankAcct{
BankID: ofxgo.String("123456789"), // Possibly your routing number
AcctID: ofxgo.String("00011122233"), // Possibly your account number
AcctType: ofxgo.AcctTypeChecking,
},
Include: true, // Include transactions (instead of only balance information)
})
response, _ := client.Request(&query) // Handle error in real code
// Was there an OFX error while processing our request?
if response.Signon.Status.Code != 0 {
meaning, _ := response.Signon.Status.CodeMeaning()
fmt.Printf("Nonzero signon status (%d: %s) with message: %s\n", response.Signon.Status.Code, meaning, response.Signon.Status.Message)
os.Exit(1)
}
if len(response.Bank) < 1 {
fmt.Println("No banking messages received")
os.Exit(1)
}
if stmt, ok := response.Bank[0].(*ofxgo.StatementResponse); ok {
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf)
fmt.Println("Transactions:")
for _, tran := range stmt.BankTranList.Transactions {
currency := stmt.CurDef
if ok, _ := tran.Currency.Valid(); ok {
currency = tran.Currency.CurSym
}
fmt.Printf("%s %-15s %-11s %s%s%s\n", tran.DtPosted, tran.TrnAmt.String()+" "+currency.String(), tran.TrnType, tran.Name, tran.Payee.Name, tran.Memo)
}
}
```
Similarly, if you have an OFX file available locally, you can parse it directly:
```go
func main() {
f, err := os.Open("./transactions.qfx")
if err != nil {
fmt.Printf("can't open file: %v\n", err)
return
}
defer f.Close()
resp, err := ofxgo.ParseResponse(f)
if err != nil {
fmt.Printf("can't parse response: %v\n", err)
return
}
// do something with resp (*ofxgo.Response)
}
```
## Requirements
OFXGo requires go >= 1.12
## Using the command-line client
To install the command-line client and test it out, you may do the following:
$ go get -v github.com/aclindsa/ofxgo/cmd/ofx && go install -v github.com/aclindsa/ofxgo/cmd/ofx
Once installed (at ~/go/bin/ofx by default, if you haven't set $GOPATH), the
command's usage should help you to use it (`./ofx --help` for a listing of the
available subcommands and their purposes, `./ofx subcommand --help` for
individual subcommand usage).

366
vendor/github.com/aclindsa/ofxgo/bank.go generated vendored Normal file
View File

@@ -0,0 +1,366 @@
package ofxgo
import (
"errors"
"github.com/aclindsa/xml"
)
// StatementRequest represents a request for a bank statement. It is used to
// request balances and/or transactions for checking, savings, money market,
// and line of credit accounts. See CCStatementRequest for the analog for
// credit card accounts.
type StatementRequest struct {
XMLName xml.Name `xml:"STMTTRNRQ"`
TrnUID UID `xml:"TRNUID"`
CltCookie String `xml:"CLTCOOKIE,omitempty"`
TAN String `xml:"TAN,omitempty"` // Transaction authorization number
// TODO `xml:"OFXEXTENSION,omitempty"`
BankAcctFrom BankAcct `xml:"STMTRQ>BANKACCTFROM"`
DtStart *Date `xml:"STMTRQ>INCTRAN>DTSTART,omitempty"`
DtEnd *Date `xml:"STMTRQ>INCTRAN>DTEND,omitempty"`
Include Boolean `xml:"STMTRQ>INCTRAN>INCLUDE"` // Include transactions (instead of just balance)
IncludePending Boolean `xml:"STMTRQ>INCLUDEPENDING,omitempty"` // Include pending transactions
IncTranImg Boolean `xml:"STMTRQ>INCTRANIMG,omitempty"` // Include transaction images
}
// Name returns the name of the top-level transaction XML/SGML element
func (r *StatementRequest) Name() string {
return "STMTTRNRQ"
}
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *StatementRequest) Valid(version ofxVersion) (bool, error) {
if ok, err := r.TrnUID.Valid(); !ok {
return false, err
}
if r.IncludePending && version < OfxVersion220 {
return false, errors.New("StatementRequest.IncludePending invalid for OFX < 2.2")
}
if r.IncTranImg && version < OfxVersion210 {
return false, errors.New("StatementRequest.IncTranImg invalid for OFX < 2.1")
}
return r.BankAcctFrom.Valid()
}
// Type returns which message set this message belongs to (which Request
// element of type []Message it should appended to)
func (r *StatementRequest) Type() messageType {
return BankRq
}
// Payee specifies a complete billing address for a payee
type Payee struct {
XMLName xml.Name `xml:"PAYEE"`
Name String `xml:"NAME"`
Addr1 String `xml:"ADDR1"`
Addr2 String `xml:"ADDR2,omitempty"`
Addr3 String `xml:"ADDR3,omitempty"`
City String `xml:"CITY"`
State String `xml:"STATE"`
PostalCode String `xml:"POSTALCODE"`
Country String `xml:"COUNTRY,omitempty"`
Phone String `xml:"PHONE"`
}
// Valid returns (true, nil) if this struct is valid OFX
func (p Payee) Valid() (bool, error) {
if len(p.Name) == 0 {
return false, errors.New("Payee.Name empty")
} else if len(p.Addr1) == 0 {
return false, errors.New("Payee.Addr1 empty")
} else if len(p.City) == 0 {
return false, errors.New("Payee.City empty")
} else if len(p.State) == 0 {
return false, errors.New("Payee.State empty")
} else if len(p.PostalCode) == 0 {
return false, errors.New("Payee.PostalCode empty")
} else if len(p.Country) != 0 && len(p.Country) != 3 {
return false, errors.New("Payee.Country invalid length")
} else if len(p.Phone) == 0 {
return false, errors.New("Payee.Phone empty")
}
return true, nil
}
// ImageData represents the metadata surrounding a check or other image file,
// including how to retrieve the image
type ImageData struct {
XMLName xml.Name `xml:"IMAGEDATA"`
ImageType imageType `xml:"IMAGETYPE"` // One of STATEMENT, TRANSACTION, TAX
ImageRef String `xml:"IMAGEREF"` // URL or identifier, depending on IMAGEREFTYPE
ImageRefType imageRefType `xml:"IMAGEREFTYPE"` // One of OPAQUE, URL, FORMURL (see spec for more details on how to access images of each of these types)
// Only one of the next two should be valid at any given time
ImageDelay Int `xml:"IMAGEDELAY,omitempty"` // Number of calendar days from DTSERVER (for statement images) or DTPOSTED (for transaction image) the image will become available
DtImageAvail *Date `xml:"DTIMAGEAVAIL,omitempty"` // Date image will become available
ImageTTL Int `xml:"IMAGETTL,omitempty"` // Number of days after image becomes available that it will remain available
CheckSup checkSup `xml:"CHECKSUP,omitempty"` // What is contained in check images. One of FRONTONLY, BACKONLY, FRONTANDBACK
}
// Transaction represents a single banking transaction. At a minimum, it
// identifies the type of transaction (TrnType) and the date it was posted
// (DtPosted). Ideally it also provides metadata to help the user recognize
// this transaction (i.e. CheckNum, Name or Payee, Memo, etc.)
type Transaction struct {
XMLName xml.Name `xml:"STMTTRN"`
TrnType trnType `xml:"TRNTYPE"` // One of CREDIT, DEBIT, INT (interest earned or paid. Note: Depends on signage of amount), DIV, FEE, SRVCHG (service charge), DEP (deposit), ATM (Note: Depends on signage of amount), POS (Note: Depends on signage of amount), XFER, CHECK, PAYMENT, CASH, DIRECTDEP, DIRECTDEBIT, REPEATPMT, OTHER
DtPosted Date `xml:"DTPOSTED"`
DtUser *Date `xml:"DTUSER,omitempty"`
DtAvail *Date `xml:"DTAVAIL,omitempty"`
TrnAmt Amount `xml:"TRNAMT"`
FiTID String `xml:"FITID"` // Client uses FITID to detect whether it has previously downloaded the transaction
CorrectFiTID String `xml:"CORRECTFITID,omitempty"` // Transaction ID that this transaction corrects, if present
CorrectAction correctAction `xml:"CORRECTACTION,omitempty"` // One of DELETE, REPLACE
SrvrTID String `xml:"SRVRTID,omitempty"`
CheckNum String `xml:"CHECKNUM,omitempty"`
RefNum String `xml:"REFNUM,omitempty"`
SIC Int `xml:"SIC,omitempty"` // Standard Industrial Code
PayeeID String `xml:"PAYEEID,omitempty"`
// Note: Servers should provide NAME or PAYEE, but not both
Name String `xml:"NAME,omitempty"`
Payee *Payee `xml:"PAYEE,omitempty"`
ExtdName String `xml:"EXTDNAME,omitempty"` // Extended name of payee or transaction description
BankAcctTo *BankAcct `xml:"BANKACCTTO,omitempty"` // If the transfer was to a bank account we have the account information for
CCAcctTo *CCAcct `xml:"CCACCTTO,omitempty"` // If the transfer was to a credit card account we have the account information for
Memo String `xml:"MEMO,omitempty"` // Extra information (not in NAME)
ImageData []ImageData `xml:"IMAGEDATA,omitempty"`
// Only one of Currency and OrigCurrency can ever be Valid() for the same transaction
Currency *Currency `xml:"CURRENCY,omitempty"` // Represents the currency of TrnAmt (instead of CURDEF in STMTRS) if Valid
OrigCurrency *Currency `xml:"ORIGCURRENCY,omitempty"` // Represents the currency TrnAmt was converted to STMTRS' CURDEF from if Valid
Inv401kSource inv401kSource `xml:"INV401KSOURCE,omitempty"` // One of PRETAX, AFTERTAX, MATCH, PROFITSHARING, ROLLOVER, OTHERVEST, OTHERNONVEST (Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST.)
}
// Valid returns (true, nil) if this struct is valid OFX
func (t Transaction) Valid(version ofxVersion) (bool, error) {
var emptyDate Date
if !t.TrnType.Valid() || t.TrnType == TrnTypeHold {
return false, errors.New("Transaction.TrnType invalid")
} else if t.DtPosted.Equal(emptyDate) {
return false, errors.New("Transaction.DtPosted not filled")
} else if len(t.FiTID) == 0 {
return false, errors.New("Transaction.FiTID empty")
} else if len(t.CorrectFiTID) > 0 && t.CorrectAction.Valid() {
return false, errors.New("Transaction.CorrectFiTID nonempty but CorrectAction invalid")
} else if len(t.Name) > 0 && t.Payee != nil {
return false, errors.New("Only one of Transaction.Name and Payee may be specified")
}
if t.Payee != nil {
if ok, err := t.Payee.Valid(); !ok {
return false, err
}
}
if t.BankAcctTo != nil && t.CCAcctTo != nil {
return false, errors.New("Only one of Transaction.BankAcctTo and CCAcctTo may be specified")
} else if t.BankAcctTo != nil {
if ok, err := t.BankAcctTo.Valid(); !ok {
return false, err
}
} else if t.CCAcctTo != nil {
if ok, err := t.CCAcctTo.Valid(); !ok {
return false, err
}
}
if version < OfxVersion220 && len(t.ImageData) > 0 {
return false, errors.New("Transaction.ImageData only supportd for OFX > 220")
} else if len(t.ImageData) > 2 {
return false, errors.New("Only 2 of ImageData allowed in Transaction")
}
var ok1, ok2 bool
if t.Currency != nil {
ok1, _ = t.Currency.Valid()
}
if t.OrigCurrency != nil {
ok2, _ = t.OrigCurrency.Valid()
}
if ok1 && ok2 {
return false, errors.New("Currency and OrigCurrency both supplied for Pending Transaction, only one allowed")
}
return true, nil
}
// TransactionList represents a list of bank transactions, and also includes
// the date range its transactions cover.
type TransactionList struct {
XMLName xml.Name `xml:"BANKTRANLIST"`
DtStart Date `xml:"DTSTART"` // Start date for transaction data
DtEnd Date `xml:"DTEND"` // Value that client should send in next <DTSTART> request to ensure that it does not miss any transactions
Transactions []Transaction `xml:"STMTTRN,omitempty"`
}
// Valid returns (true, nil) if this struct is valid OFX
func (l TransactionList) Valid(version ofxVersion) (bool, error) {
var emptyDate Date
if l.DtStart.Equal(emptyDate) {
return false, errors.New("TransactionList.DtStart not filled")
} else if l.DtEnd.Equal(emptyDate) {
return false, errors.New("TransactionList.DtEnd not filled")
}
for _, t := range l.Transactions {
if ok, err := t.Valid(version); !ok {
return false, err
}
}
return true, nil
}
// PendingTransaction represents a single pending transaction. It is similar to
// Transaction, but is not finalized (and may never be). For instance, it lacks
// FiTID and DtPosted fields.
type PendingTransaction struct {
XMLName xml.Name `xml:"STMTTRNP"`
TrnType trnType `xml:"TRNTYPE"` // One of CREDIT, DEBIT, INT (interest earned or paid. Note: Depends on signage of amount), DIV, FEE, SRVCHG (service charge), DEP (deposit), ATM (Note: Depends on signage of amount), POS (Note: Depends on signage of amount), XFER, CHECK, PAYMENT, CASH, DIRECTDEP, DIRECTDEBIT, REPEATPMT, HOLD, OTHER
DtTran Date `xml:"DTTRAN"`
DtExpire *Date `xml:"DTEXPIRE,omitempty"` // only valid for TrnType==HOLD, the date the hold will expire
TrnAmt Amount `xml:"TRNAMT"`
RefNum String `xml:"REFNUM,omitempty"`
Name String `xml:"NAME,omitempty"`
ExtdName String `xml:"EXTDNAME,omitempty"` // Extended name of payee or transaction description
Memo String `xml:"MEMO,omitempty"` // Extra information (not in NAME)
ImageData []ImageData `xml:"IMAGEDATA,omitempty"`
// Only one of Currency and OrigCurrency can ever be Valid() for the same transaction
Currency Currency `xml:"CURRENCY,omitempty"` // Represents the currency of TrnAmt (instead of CURDEF in STMTRS) if Valid
OrigCurrency Currency `xml:"ORIGCURRENCY,omitempty"` // Represents the currency TrnAmt was converted to STMTRS' CURDEF from if Valid
}
// Valid returns (true, nil) if this struct is valid OFX
func (t PendingTransaction) Valid() (bool, error) {
var emptyDate Date
if !t.TrnType.Valid() {
return false, errors.New("PendingTransaction.TrnType invalid")
} else if t.DtTran.Equal(emptyDate) {
return false, errors.New("PendingTransaction.DtTran not filled")
} else if len(t.Name) == 0 {
return false, errors.New("PendingTransaction.Name empty")
}
ok1, _ := t.Currency.Valid()
ok2, _ := t.OrigCurrency.Valid()
if ok1 && ok2 {
return false, errors.New("Currency and OrigCurrency both supplied for Pending Transaction, only one allowed")
}
return true, nil
}
// PendingTransactionList represents a list of pending transactions, along with
// the date they were generated
type PendingTransactionList struct {
XMLName xml.Name `xml:"BANKTRANLISTP"`
DtAsOf Date `xml:"DTASOF"` // Date and time this set of pending transactions was generated
Transactions []PendingTransaction `xml:"STMTTRNP,omitempty"`
}
// Valid returns (true, nil) if this struct is valid OFX
func (l PendingTransactionList) Valid() (bool, error) {
var emptyDate Date
if l.DtAsOf.Equal(emptyDate) {
return false, errors.New("PendingTransactionList.DtAsOf not filled")
}
for _, t := range l.Transactions {
if ok, err := t.Valid(); !ok {
return false, err
}
}
return true, nil
}
// Balance represents a generic (free-form) balance defined by an FI.
type Balance struct {
XMLName xml.Name `xml:"BAL"`
Name String `xml:"NAME"`
Desc String `xml:"DESC"`
// Balance type:
// DOLLAR = dollar (value formatted DDDD.cc)
// PERCENT = percentage (value formatted XXXX.YYYY)
// NUMBER = number (value formatted as is)
BalType balType `xml:"BALTYPE"`
Value Amount `xml:"VALUE"`
DtAsOf *Date `xml:"DTASOF,omitempty"`
Currency *Currency `xml:"CURRENCY,omitempty"` // if BALTYPE is DOLLAR
}
// Valid returns (true, nil) if this struct is valid OFX
func (b Balance) Valid() (bool, error) {
if len(b.Name) == 0 || len(b.Desc) == 0 {
return false, errors.New("Balance Name and Desc not supplied")
}
if !b.BalType.Valid() {
return false, errors.New("Balance BALTYPE not specified")
}
return true, nil
}
// StatementResponse represents a bank account statement, including its
// balances and possibly transactions. It is a response to StatementRequest, or
// sometimes provided as part of an OFX file downloaded manually from an FI.
type StatementResponse struct {
XMLName xml.Name `xml:"STMTTRNRS"`
TrnUID UID `xml:"TRNUID"`
Status Status `xml:"STATUS"`
CltCookie String `xml:"CLTCOOKIE,omitempty"`
// TODO `xml:"OFXEXTENSION,omitempty"`
CurDef CurrSymbol `xml:"STMTRS>CURDEF"`
BankAcctFrom BankAcct `xml:"STMTRS>BANKACCTFROM"`
BankTranList *TransactionList `xml:"STMTRS>BANKTRANLIST,omitempty"`
BankTranListP *PendingTransactionList `xml:"STMTRS>BANKTRANLISTP,omitempty"`
BalAmt Amount `xml:"STMTRS>LEDGERBAL>BALAMT"`
DtAsOf Date `xml:"STMTRS>LEDGERBAL>DTASOF"`
AvailBalAmt *Amount `xml:"STMTRS>AVAILBAL>BALAMT,omitempty"`
AvailDtAsOf *Date `xml:"STMTRS>AVAILBAL>DTASOF,omitempty"`
CashAdvBalAmt *Amount `xml:"STMTRS>CASHADVBALAMT,omitempty"` // Only for CREDITLINE accounts, available balance for cash advances
IntRate *Amount `xml:"STMTRS>INTRATE,omitempty"` // Current interest rate
BalList []Balance `xml:"STMTRS>BALLIST>BAL,omitempty"`
MktgInfo String `xml:"STMTRS>MKTGINFO,omitempty"` // Marketing information
}
// Name returns the name of the top-level transaction XML/SGML element
func (sr *StatementResponse) Name() string {
return "STMTTRNRS"
}
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (sr *StatementResponse) Valid(version ofxVersion) (bool, error) {
var emptyDate Date
if ok, err := sr.TrnUID.Valid(); !ok {
return false, err
} else if ok, err := sr.Status.Valid(); !ok {
return false, err
} else if ok, err := sr.CurDef.Valid(); !ok {
return false, err
} else if ok, err := sr.BankAcctFrom.Valid(); !ok {
return false, err
} else if sr.DtAsOf.Equal(emptyDate) {
return false, errors.New("StatementResponse.DtAsOf not filled")
} else if (sr.AvailBalAmt == nil) != (sr.AvailDtAsOf == nil) {
return false, errors.New("StatementResponse.Avail* must both either be present or absent")
}
if sr.BankTranList != nil {
if ok, err := sr.BankTranList.Valid(version); !ok {
return false, err
}
}
if sr.BankTranListP != nil {
if version < OfxVersion220 {
return false, errors.New("StatementResponse.BankTranListP invalid for OFX < 2.2")
}
if ok, err := sr.BankTranListP.Valid(); !ok {
return false, err
}
}
for _, bal := range sr.BalList {
if ok, err := bal.Valid(); !ok {
return false, err
}
}
return true, nil
}
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (sr *StatementResponse) Type() messageType {
return BankRs
}

104
vendor/github.com/aclindsa/ofxgo/basic_client.go generated vendored Normal file
View File

@@ -0,0 +1,104 @@
package ofxgo
import (
"errors"
"io"
"net/http"
"strings"
)
// BasicClient provides a standard Client implementation suitable for most
// financial institutions. BasicClient uses default, non-zero settings, even if
// its fields are not initialized.
type BasicClient struct {
// Request fields to overwrite with the client's values. If nonempty,
// defaults are used
SpecVersion ofxVersion // VERSION in header
AppID string // SONRQ>APPID
AppVer string // SONRQ>APPVER
// Don't insert newlines or indentation when marshalling to SGML/XML
NoIndent bool
// Use carriage returns on new lines
CarriageReturn bool
// Set User-Agent header to this string, if not empty
UserAgent string
}
// OfxVersion returns the OFX specification version this BasicClient will marshal
// Requests as. Defaults to "203" if the client's SpecVersion field is empty.
func (c *BasicClient) OfxVersion() ofxVersion {
if c.SpecVersion.Valid() {
return c.SpecVersion
}
return OfxVersion203
}
// ID returns this BasicClient's OFX AppID field, defaulting to "OFXGO" if
// unspecified.
func (c *BasicClient) ID() String {
if len(c.AppID) > 0 {
return String(c.AppID)
}
return String("OFXGO")
}
// Version returns this BasicClient's version number as a string, defaulting to
// "0001" if unspecified.
func (c *BasicClient) Version() String {
if len(c.AppVer) > 0 {
return String(c.AppVer)
}
return String("0001")
}
// IndentRequests returns true if the marshaled XML should be indented (and
// contain newlines, since the two are linked in the current implementation)
func (c *BasicClient) IndentRequests() bool {
return !c.NoIndent
}
// CarriageReturnNewLines returns true if carriage returns should be used on new lines, false otherwise
func (c *BasicClient) CarriageReturnNewLines() bool {
return c.CarriageReturn
}
// RawRequest is a convenience wrapper around http.Post. It is exposed only for
// when you need to read/inspect the raw HTTP response yourself.
func (c *BasicClient) RawRequest(URL string, r io.Reader) (*http.Response, error) {
if !strings.HasPrefix(URL, "https://") {
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
}
request, err := http.NewRequest("POST", URL, r)
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "application/x-ofx")
request.Header.Add("Accept", "*/*, application/x-ofx")
if c.UserAgent != "" {
request.Header.Set("User-Agent", c.UserAgent)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return nil, err
}
if response.StatusCode != 200 {
return response, errors.New("OFXQuery request status: " + response.Status)
}
return response, nil
}
// RequestNoParse marshals a Request to XML, makes an HTTP request, and returns
// the raw HTTP response
func (c *BasicClient) RequestNoParse(r *Request) (*http.Response, error) {
return clientRequestNoParse(c, r)
}
// Request marshals a Request to XML, makes an HTTP request, and then
// unmarshals the response into a Response object.
func (c *BasicClient) Request(r *Request) (*Response, error) {
return clientRequest(c, r)
}

105
vendor/github.com/aclindsa/ofxgo/client.go generated vendored Normal file
View File

@@ -0,0 +1,105 @@
package ofxgo
import (
"io"
"net/http"
"strings"
)
// Client serves to aggregate OFX client settings that may be necessary to talk
// to a particular server due to quirks in that server's implementation.
// Client also provides the Request and RequestNoParse helper methods to aid in
// making and parsing requests.
type Client interface {
// Used to fill out a Request object
OfxVersion() ofxVersion
ID() String
Version() String
IndentRequests() bool
CarriageReturnNewLines() bool
// Request marshals a Request object into XML, makes an HTTP request
// against it's URL, and then unmarshals the response into a Response
// object.
//
// Before being marshaled, some of the the Request object's values are
// overwritten, namely those dictated by the BasicClient's configuration
// (Version, AppID, AppVer fields), and the client's current time
// (DtClient). These are updated in place in the supplied Request object so
// they may later be inspected by the caller.
Request(r *Request) (*Response, error)
// RequestNoParse marshals a Request object into XML, makes an HTTP
// request, and returns the raw HTTP response. Unlike RawRequest(), it
// takes client settings into account. Unlike Request(), it doesn't parse
// the response into an ofxgo.Request object.
//
// Caveat: The caller is responsible for closing the http Response.Body
// (see the http module's documentation for more information)
RequestNoParse(r *Request) (*http.Response, error)
// RawRequest is little more than a thin wrapper around http.Post
//
// In most cases, you should probably be using Request() instead, but
// RawRequest can be useful if you need to read the raw unparsed http
// response yourself (perhaps for downloading an OFX file for use by an
// external program, or debugging server behavior), or have a handcrafted
// request you'd like to try.
//
// Caveats: RawRequest does *not* take client settings into account as
// Client.Request() does, so your particular server may or may not like
// whatever we read from 'r'. The caller is responsible for closing the
// http Response.Body (see the http module's documentation for more
// information)
RawRequest(URL string, r io.Reader) (*http.Response, error)
}
type clientCreationFunc func(*BasicClient) Client
// GetClient returns a new Client for a given URL. It attempts to find a
// specialized client for this URL, but simply returns the passed-in
// BasicClient if no such match is found.
func GetClient(URL string, bc *BasicClient) Client {
clients := []struct {
URL string
Func clientCreationFunc
}{
{"https://ofx.discovercard.com", NewDiscoverCardClient},
{"https://vesnc.vanguard.com/us/OfxDirectConnectServlet", NewVanguardClient},
}
for _, client := range clients {
if client.URL == strings.Trim(URL, "/") {
return client.Func(bc)
}
}
return bc
}
// clientRequestNoParse can be used for building clients' RequestNoParse
// methods if they require fairly standard behavior
func clientRequestNoParse(c Client, r *Request) (*http.Response, error) {
r.SetClientFields(c)
b, err := r.Marshal()
if err != nil {
return nil, err
}
return c.RawRequest(r.URL, b)
}
// clientRequest can be used for building clients' Request methods if they
// require fairly standard behavior
func clientRequest(c Client, r *Request) (*Response, error) {
response, err := c.RequestNoParse(r)
if err != nil {
return nil, err
}
defer response.Body.Close()
ofxresp, err := ParseResponse(response.Body)
if err != nil {
return nil, err
}
return ofxresp, nil
}

372
vendor/github.com/aclindsa/ofxgo/common.go generated vendored Normal file
View File

@@ -0,0 +1,372 @@
package ofxgo
//go:generate ./generate_constants.py
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/aclindsa/xml"
)
func writeHeader(b *bytes.Buffer, v ofxVersion, carriageReturn bool) error {
// Write the header appropriate to our version
switch v {
case OfxVersion102, OfxVersion103, OfxVersion151, OfxVersion160:
header := `OFXHEADER:100
DATA:OFXSGML
VERSION:` + v.String() + `
SECURITY:NONE
ENCODING:USASCII
CHARSET:1252
COMPRESSION:NONE
OLDFILEUID:NONE
NEWFILEUID:NONE
`
if carriageReturn {
header = strings.Replace(header, "\n", "\r\n", -1)
}
b.WriteString(header)
case OfxVersion200, OfxVersion201, OfxVersion202, OfxVersion203, OfxVersion210, OfxVersion211, OfxVersion220:
b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>`)
if carriageReturn {
b.WriteByte('\r')
}
b.WriteByte('\n')
b.WriteString(`<?OFX OFXHEADER="200" VERSION="` + v.String() + `" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>`)
if carriageReturn {
b.WriteByte('\r')
}
b.WriteByte('\n')
default:
return fmt.Errorf("%d is not a valid OFX version string", v)
}
return nil
}
// Message represents an OFX message in a message set. it is used to ease
// marshalling and unmarshalling.
type Message interface {
Name() string // The name of the OFX transaction wrapper element this represents
Valid(version ofxVersion) (bool, error) // Called before a Message is marshaled and after it's unmarshaled to ensure the request or response is valid
Type() messageType // The message set this message belongs to
}
type messageType uint
// These constants are returned by Messages' Type() functions to determine
// which message set they belong to
const (
// Requests
SignonRq messageType = iota
SignupRq
BankRq
CreditCardRq
LoanRq
InvStmtRq
InterXferRq
WireXferRq
BillpayRq
EmailRq
SecListRq
PresDirRq
PresDlvRq
ProfRq
ImageRq
// Responses
SignonRs
SignupRs
BankRs
CreditCardRs
LoanRs
InvStmtRs
InterXferRs
WireXferRs
BillpayRs
EmailRs
SecListRs
PresDirRs
PresDlvRs
ProfRs
ImageRs
)
func (t messageType) String() string {
switch t {
case SignonRq:
return "SIGNONMSGSRQV1"
case SignupRq:
return "SIGNUPMSGSRQV1"
case BankRq:
return "BANKMSGSRQV1"
case CreditCardRq:
return "CREDITCARDMSGSRQV1"
case LoanRq:
return "LOANMSGSRQV1"
case InvStmtRq:
return "INVSTMTMSGSRQV1"
case InterXferRq:
return "INTERXFERMSGSRQV1"
case WireXferRq:
return "WIREXFERMSGSRQV1"
case BillpayRq:
return "BILLPAYMSGSRQV1"
case EmailRq:
return "EMAILMSGSRQV1"
case SecListRq:
return "SECLISTMSGSRQV1"
case PresDirRq:
return "PRESDIRMSGSRQV1"
case PresDlvRq:
return "PRESDLVMSGSRQV1"
case ProfRq:
return "PROFMSGSRQV1"
case ImageRq:
return "IMAGEMSGSRQV1"
case SignonRs:
return "SIGNONMSGSRSV1"
case SignupRs:
return "SIGNUPMSGSRSV1"
case BankRs:
return "BANKMSGSRSV1"
case CreditCardRs:
return "CREDITCARDMSGSRSV1"
case LoanRs:
return "LOANMSGSRSV1"
case InvStmtRs:
return "INVSTMTMSGSRSV1"
case InterXferRs:
return "INTERXFERMSGSRSV1"
case WireXferRs:
return "WIREXFERMSGSRSV1"
case BillpayRs:
return "BILLPAYMSGSRSV1"
case EmailRs:
return "EMAILMSGSRSV1"
case SecListRs:
return "SECLISTMSGSRSV1"
case PresDirRs:
return "PRESDIRMSGSRSV1"
case PresDlvRs:
return "PRESDLVMSGSRSV1"
case ProfRs:
return "PROFMSGSRSV1"
case ImageRs:
return "IMAGEMSGSRSV1"
}
panic("Invalid messageType")
}
// Map of error codes to their meanings, SEVERITY, and conditions under which
// OFX servers are expected to return them
var statusMeanings = map[Int][3]string{
0: {"Success", "INFO", "The server successfully processed the request."},
1: {"Client is up-to-date", "INFO", "Based on the client timestamp, the client has the latest information. The response does not supply any additional information."},
2000: {"General error", "ERROR", "Error other than those specified by the remaining error codes. Note: Servers should provide a more specific error whenever possible. Error code 2000 should be reserved for cases in which a more specific code is not available."},
2001: {"Invalid account", "ERROR", ""},
2002: {"General account error", "ERROR", "Account error not specified by the remaining error codes."},
2003: {"Account not found", "ERROR", "The specified account number does not correspond to one of the users accounts."},
2004: {"Account closed", "ERROR", "The specified account number corresponds to an account that has been closed."},
2005: {"Account not authorized", "ERROR", "The user is not authorized to perform this action on the account, or the server does not allow this type of action to be performed on the account."},
2006: {"Source account not found", "ERROR", "The specified account number does not correspond to one of the users accounts."},
2007: {"Source account closed", "ERROR", "The specified account number corresponds to an account that has been closed."},
2008: {"Source account not authorized", "ERROR", "The user is not authorized to perform this action on the account, or the server does not allow this type of action to be performed on the account."},
2009: {"Destination account not found", "ERROR", "The specified account number does not correspond to one of the users accounts."},
2010: {"Destination account closed", "ERROR", "The specified account number corresponds to an account that has been closed."},
2011: {"Destination account not authorized", "ERROR", "The user is not authorized to perform this action on the account, or the server does not allow this type of action to be performed on the account."},
2012: {"Invalid amount", "ERROR", "The specified amount is not valid for this action; for example, the user specified a negative payment amount."},
2014: {"Date too soon", "ERROR", "The server cannot process the requested action by the date specified by the user."},
2015: {"Date too far in future", "ERROR", "The server cannot accept requests for an action that far in the future."},
2016: {"Transaction already committed", "ERROR", "Transaction has entered the processing loop and cannot be modified/cancelled using OFX. The transaction may still be cancelled or modified using other means (for example, a phone call to Customer Service)."},
2017: {"Already canceled", "ERROR", "The transaction cannot be canceled or modified because it has already been canceled."},
2018: {"Unknown server ID", "ERROR", "The specified server ID does not exist or no longer exists."},
2019: {"Duplicate request", "ERROR", "A request with this <TRNUID> has already been received and processed."},
2020: {"Invalid date", "ERROR", "The specified datetime stamp cannot be parsed; for instance, the datetime stamp specifies 25:00 hours."},
2021: {"Unsupported version", "ERROR", "The server does not support the requested version. The version of the message set specified by the client is not supported by this server."},
2022: {"Invalid TAN", "ERROR", "The server was unable to validate the TAN sent in the request."},
2023: {"Unknown FITID", "ERROR", "The specified FITID/BILLID does not exist or no longer exists. [BILLID not found (ERROR) in the billing message sets]"},
2025: {"Branch ID missing", "ERROR", "A <BRANCHID> value must be provided in the <BANKACCTFROM> aggregate for this country system, but this field is missing."},
2026: {"Bank name doesnt match bank ID", "ERROR", "The value of <BANKNAME> in the <EXTBANKACCTTO> aggregate is inconsistent with the value of <BANKID> in the <BANKACCTTO> aggregate."},
2027: {"Invalid date range", "ERROR", "Response for non-overlapping dates, date ranges in the future, et cetera."},
2028: {"Requested element unknown", "WARN", "One or more elements of the request were not recognized by the server or the server (as noted in the FI Profile) does not support the elements. The server executed the element transactions it understood and supported. For example, the request file included private tags in a <PMTRQ> but the server was able to execute the rest of the request."},
3000: {"MFA Challenge authentication required", "ERROR", "User credentials are correct, but further authentication required. Client should send <MFACHALLENGERQ> in next request."},
3001: {"MFA Challenge information is invalid", "ERROR", "User or client information sent in MFACHALLENGEA contains invalid information"},
6500: {"<REJECTIFMISSING>Y invalid without <TOKEN>", "ERROR", "This error code may appear in the <SYNCERROR> element of an <xxxSYNCRS> wrapper (in <PRESDLVMSGSRSV1> and V2 message set responses) or the <CODE> contained in any embedded transaction wrappers within a sync response. The corresponding sync request wrapper included <REJECTIFMISSING>Y with <REFRESH>Y or <TOKENONLY>Y, which is illegal."},
6501: {"Embedded transactions in request failed to process: Out of date", "WARN", "<REJECTIFMISSING>Y and embedded transactions appeared in the request sync wrapper and the provided <TOKEN> was out of date. This code should be used in the <SYNCERROR> of the response sync wrapper."},
6502: {"Unable to process embedded transaction due to out-of-date <TOKEN>", "ERROR", "Used in response transaction wrapper for embedded transactions when <SYNCERROR>6501 appears in the surrounding sync wrapper."},
10000: {"Stop check in process", "INFO", "Stop check is already in process."},
10500: {"Too many checks to process", "ERROR", "The stop-payment request <STPCHKRQ> specifies too many checks."},
10501: {"Invalid payee", "ERROR", "Payee error not specified by the remaining error codes."},
10502: {"Invalid payee address", "ERROR", "Some portion of the payees address is incorrect or unknown."},
10503: {"Invalid payee account number", "ERROR", "The account number <PAYACCT> of the requested payee is invalid."},
10504: {"Insufficient funds", "ERROR", "The server cannot process the request because the specified account does not have enough funds."},
10505: {"Cannot modify element", "ERROR", "The server does not allow modifications to one or more values in a modification request."},
10506: {"Cannot modify source account", "ERROR", "Reserved for future use."},
10507: {"Cannot modify destination account", "ERROR", "Reserved for future use."},
10508: {"Invalid frequency", "ERROR", "The specified frequency <FREQ> does not match one of the accepted frequencies for recurring transactions."},
10509: {"Model already canceled", "ERROR", "The server has already canceled the specified recurring model."},
10510: {"Invalid payee ID", "ERROR", "The specified payee ID does not exist or no longer exists."},
10511: {"Invalid payee city", "ERROR", "The specified city is incorrect or unknown."},
10512: {"Invalid payee state", "ERROR", "The specified state is incorrect or unknown."},
10513: {"Invalid payee postal code", "ERROR", "The specified postal code is incorrect or unknown."},
10514: {"Transaction already processed", "ERROR", "Transaction has already been sent or date due is past"},
10515: {"Payee not modifiable by client", "ERROR", "The server does not allow clients to change payee information."},
10516: {"Wire beneficiary invalid", "ERROR", "The specified wire beneficiary does not exist or no longer exists."},
10517: {"Invalid payee name", "ERROR", "The server does not recognize the specified payee name."},
10518: {"Unknown model ID", "ERROR", "The specified model ID does not exist or no longer exists."},
10519: {"Invalid payee list ID", "ERROR", "The specified payee list ID does not exist or no longer exists."},
10600: {"Table type not found", "ERROR", "The specified table type is not recognized or does not exist."},
12250: {"Investment transaction download not supported", "WARN", "The server does not support investment transaction download."},
12251: {"Investment position download not supported", "WARN", "The server does not support investment position download."},
12252: {"Investment positions for specified date not available", "WARN", "The server does not support investment positions for the specified date."},
12253: {"Investment open order download not supported", "WARN", "The server does not support open order download."},
12254: {"Investment balances download not supported", "WARN", "The server does not support investment balances download."},
12255: {"401(k) not available for this account", "ERROR", "401(k) information requested from a non- 401(k) account."},
12500: {"One or more securities not found", "ERROR", "The server could not find the requested securities."},
13000: {"User ID & password will be sent out-of-band", "INFO", "The server will send the user ID and password via postal mail, e-mail, or another means. The accompanying message will provide details."},
13500: {"Unable to enroll user", "ERROR", "The server could not enroll the user."},
13501: {"User already enrolled", "ERROR", "The server has already enrolled the user."},
13502: {"Invalid service", "ERROR", "The server does not support the service <SVC> specified in the service-activation request."},
13503: {"Cannot change user information", "ERROR", "The server does not support the <CHGUSERINFORQ> request."},
13504: {"<FI> Missing or Invalid in <SONRQ>", "ERROR", "The FI requires the client to provide the <FI> aggregate in the <SONRQ> request, but either none was provided, or the one provided was invalid."},
14500: {"1099 forms not available", "ERROR", "1099 forms are not yet available for the tax year requested."},
14501: {"1099 forms not available for user ID", "ERROR", "This user does not have any 1099 forms available."},
14600: {"W2 forms not available", "ERROR", "W2 forms are not yet available for the tax year requested."},
14601: {"W2 forms not available for user ID", "ERROR", "The user does not have any W2 forms available."},
14700: {"1098 forms not available", "ERROR", "1098 forms are not yet available for the tax year requested."},
14701: {"1098 forms not available for user ID", "ERROR", "The user does not have any 1098 forms available."},
15000: {"Must change USERPASS", "INFO", "The user must change his or her <USERPASS> number as part of the next OFX request."},
15500: {"Signon invalid", "ERROR", "The user cannot signon because he or she entered an invalid user ID or password."},
15501: {"Customer account already in use", "ERROR", "The server allows only one connection at a time, and another user is already signed on. Please try again later."},
15502: {"USERPASS lockout", "ERROR", "The server has received too many failed signon attempts for this user. Please call the FIs technical support number."},
15503: {"Could not change USERPASS", "ERROR", "The server does not support the <PINCHRQ> request."},
15504: {"Could not provide random data", "ERROR", "The server could not generate random data as requested by the <CHALLENGERQ>."},
15505: {"Country system not supported", "ERROR", "The server does not support the country specified in the <COUNTRY> field of the <SONRQ> aggregate."},
15506: {"Empty signon not supported", "ERROR", "The server does not support signons not accompanied by some other transaction."},
15507: {"Signon invalid without supporting pin change request", "ERROR", "The OFX block associated with the signon does not contain a pin change request and should."},
15508: {"Transaction not authorized. ", "ERROR", "Current user is not authorized to perform this action on behalf of the <USERID>."},
15510: {"CLIENTUID error", "ERROR", "The CLIENTUID sent by the client was incorrect. User must register the Client UID."},
15511: {"MFA error", "ERROR", "User should contact financial institution."},
15512: {"AUTHTOKEN required", "ERROR", "User needs to contact financial institution to obtain AUTHTOKEN. Client should send it in the next request."},
15513: {"AUTHTOKEN invalid", "ERROR", "The AUTHTOKEN sent by the client was invalid."},
16500: {"HTML not allowed", "ERROR", "The server does not accept HTML formatting in the request."},
16501: {"Unknown mail To:", "ERROR", "The server was unable to send mail to the specified Internet address."},
16502: {"Invalid URL", "ERROR", "The server could not parse the URL."},
16503: {"Unable to get URL", "ERROR", "The server was unable to retrieve the information at this URL (e.g., an HTTP 400 or 500 series error)."},
}
// Status represents the status of a Response (both top-level Request objects,
// and *Response objects)
type Status struct {
XMLName xml.Name `xml:"STATUS"`
Code Int `xml:"CODE"`
Severity String `xml:"SEVERITY"`
Message String `xml:"MESSAGE,omitempty"`
}
// Valid returns whether the Status is valid according to the OFX spec
func (s *Status) Valid() (bool, error) {
switch s.Severity {
case "INFO", "WARN", "ERROR":
default:
return false, errors.New("Invalid STATUS>SEVERITY")
}
if arr, ok := statusMeanings[s.Code]; ok {
if arr[1] != string(s.Severity) {
return false, errors.New("Unexpected SEVERITY for STATUS>CODE")
}
} else {
return false, errors.New("Unknown OFX status code")
}
return true, nil
}
// CodeMeaning returns the meaning of the current status Code
func (s *Status) CodeMeaning() (string, error) {
if arr, ok := statusMeanings[s.Code]; ok {
return arr[0], nil
}
return "", errors.New("Unknown OFX status code")
}
// CodeConditions returns the conditions under which an OFX server is expected
// to return the current status Code
func (s *Status) CodeConditions() (string, error) {
if arr, ok := statusMeanings[s.Code]; ok {
return arr[2], nil
}
return "", errors.New("Unknown OFX status code")
}
// BankAcct represents the identifying information for one bank account
type BankAcct struct {
XMLName xml.Name // BANKACCTTO or BANKACCTFROM
BankID String `xml:"BANKID"`
BranchID String `xml:"BRANCHID,omitempty"` // Unused in USA
AcctID String `xml:"ACCTID"`
AcctType acctType `xml:"ACCTTYPE"` // One of CHECKING, SAVINGS, MONEYMRKT, CREDITLINE, CD
AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA
}
// Valid returns whether the BankAcct is valid according to the OFX spec
func (b BankAcct) Valid() (bool, error) {
if len(b.BankID) == 0 {
return false, errors.New("BankAcct.BankID empty")
}
if len(b.AcctID) == 0 {
return false, errors.New("BankAcct.AcctID empty")
}
if !b.AcctType.Valid() {
return false, errors.New("Invalid or unspecified BankAcct.AcctType")
}
return true, nil
}
// CCAcct represents the identifying information for one checking account
type CCAcct struct {
XMLName xml.Name // CCACCTTO or CCACCTFROM
AcctID String `xml:"ACCTID"`
AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA
}
// Valid returns whether the CCAcct is valid according to the OFX spec
func (c CCAcct) Valid() (bool, error) {
if len(c.AcctID) == 0 {
return false, errors.New("CCAcct.AcctID empty")
}
return true, nil
}
// InvAcct represents the identifying information for one investment account
type InvAcct struct {
XMLName xml.Name // INVACCTTO or INVACCTFROM
BrokerID String `xml:"BROKERID"`
AcctID String `xml:"ACCTID"`
}
// Currency represents one ISO-4217 currency. CURRENCY elements signify that
// the transaction containing this Currency struct is in this currency instead
// of being converted to the statement's default. ORIGCURRENCY elements signify
// that the transaction containing this Currency struct was converted to the
// statement's default from the specified currency.
type Currency struct {
XMLName xml.Name // CURRENCY or ORIGCURRENCY
CurRate Amount `xml:"CURRATE"` // Ratio of statement's currency (CURDEF) to transaction currency (CURSYM)
CurSym CurrSymbol `xml:"CURSYM"` // ISO-4217 3-character currency identifier
}
// Valid returns whether the Currency is valid according to the OFX spec
func (c Currency) Valid() (bool, error) {
if c.CurRate.IsInt() && c.CurRate.Num().Int64() == 0 {
return false, errors.New("CurRate may not be zero")
} else if ok, err := c.CurSym.Valid(); !ok {
return false, err
}
return true, nil
}

2703
vendor/github.com/aclindsa/ofxgo/constants.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

91
vendor/github.com/aclindsa/ofxgo/creditcard.go generated vendored Normal file
View File

@@ -0,0 +1,91 @@
package ofxgo
import (
"github.com/aclindsa/xml"
)
// CCStatementRequest represents a request for a credit card statement. It is
// used to request balances and/or transactions. See StatementRequest for the
// analog for all other bank accounts.
type CCStatementRequest struct {
XMLName xml.Name `xml:"CCSTMTTRNRQ"`
TrnUID UID `xml:"TRNUID"`
CltCookie String `xml:"CLTCOOKIE,omitempty"`
TAN String `xml:"TAN,omitempty"`
// TODO OFXEXTENSION
CCAcctFrom CCAcct `xml:"CCSTMTRQ>CCACCTFROM"`
DtStart *Date `xml:"CCSTMTRQ>INCTRAN>DTSTART,omitempty"`
DtEnd *Date `xml:"CCSTMTRQ>INCTRAN>DTEND,omitempty"`
Include Boolean `xml:"CCSTMTRQ>INCTRAN>INCLUDE"` // Include transactions (instead of just balance)
IncludePending Boolean `xml:"CCSTMTRQ>INCLUDEPENDING,omitempty"` // Include pending transactions
IncTranImg Boolean `xml:"CCSTMTRQ>INCTRANIMG,omitempty"` // Include transaction images
}
// Name returns the name of the top-level transaction XML/SGML element
func (r *CCStatementRequest) Name() string {
return "CCSTMTTRNRQ"
}
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *CCStatementRequest) Valid(version ofxVersion) (bool, error) {
if ok, err := r.TrnUID.Valid(); !ok {
return false, err
}
// TODO implement
return true, nil
}
// Type returns which message set this message belongs to (which Request
// element of type []Message it should appended to)
func (r *CCStatementRequest) Type() messageType {
return CreditCardRq
}
// CCStatementResponse represents a credit card statement, including its
// balances and possibly transactions. It is a response to CCStatementRequest,
// or sometimes provided as part of an OFX file downloaded manually from an FI.
type CCStatementResponse struct {
XMLName xml.Name `xml:"CCSTMTTRNRS"`
TrnUID UID `xml:"TRNUID"`
Status Status `xml:"STATUS"`
CltCookie String `xml:"CLTCOOKIE,omitempty"`
// TODO `xml:"OFXEXTENSION,omitempty"`
CurDef CurrSymbol `xml:"CCSTMTRS>CURDEF"`
CCAcctFrom CCAcct `xml:"CCSTMTRS>CCACCTFROM"`
BankTranList *TransactionList `xml:"CCSTMTRS>BANKTRANLIST,omitempty"`
//BANKTRANLISTP
BalAmt Amount `xml:"CCSTMTRS>LEDGERBAL>BALAMT"`
DtAsOf Date `xml:"CCSTMTRS>LEDGERBAL>DTASOF"`
AvailBalAmt *Amount `xml:"CCSTMTRS>AVAILBAL>BALAMT,omitempty"`
AvailDtAsOf *Date `xml:"CCSTMTRS>AVAILBAL>DTASOF,omitempty"`
CashAdvBalAmt Amount `xml:"CCSTMTRS>CASHADVBALAMT,omitempty"` // Only for CREDITLINE accounts, available balance for cash advances
IntRatePurch Amount `xml:"CCSTMTRS>INTRATEPURCH,omitempty"` // Current interest rate for purchases
IntRateCash Amount `xml:"CCSTMTRS>INTRATECASH,omitempty"` // Current interest rate for cash advances
IntRateXfer Amount `xml:"CCSTMTRS>INTRATEXFER,omitempty"` // Current interest rate for cash advances
RewardName String `xml:"CCSTMTRS>REWARDINFO>NAME,omitempty"` // Name of the reward program referred to by the next two elements
RewardBal Amount `xml:"CCSTMTRS>REWARDINFO>REWARDBAL,omitempty"` // Current balance of the reward program
RewardEarned Amount `xml:"CCSTMTRS>REWARDINFO>REWARDEARNED,omitempty"` // Reward amount earned YTD
BalList []Balance `xml:"CCSTMTRS>BALLIST>BAL,omitempty"`
MktgInfo String `xml:"CCSTMTRS>MKTGINFO,omitempty"` // Marketing information
}
// Name returns the name of the top-level transaction XML/SGML element
func (sr *CCStatementResponse) Name() string {
return "CCSTMTTRNRS"
}
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (sr *CCStatementResponse) Valid(version ofxVersion) (bool, error) {
if ok, err := sr.TrnUID.Valid(); !ok {
return false, err
}
//TODO implement
return true, nil
}
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (sr *CCStatementResponse) Type() messageType {
return CreditCardRs
}

109
vendor/github.com/aclindsa/ofxgo/discovercard_client.go generated vendored Normal file
View File

@@ -0,0 +1,109 @@
package ofxgo
import (
"bufio"
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// DiscoverCardClient provides a Client implementation which handles
// DiscoverCard's broken HTTP header behavior. DiscoverCardClient uses default,
// non-zero settings, if its fields are not initialized.
type DiscoverCardClient struct {
*BasicClient
}
// NewDiscoverCardClient returns a Client interface configured to handle
// Discover Card's brand of idiosyncrasy
func NewDiscoverCardClient(bc *BasicClient) Client {
return &DiscoverCardClient{bc}
}
func discoverCardHTTPPost(URL string, r io.Reader) (*http.Response, error) {
// Either convert or copy to a bytes.Buffer to be able to determine the
// request length for the Content-Length header
buf, ok := r.(*bytes.Buffer)
if !ok {
buf = &bytes.Buffer{}
_, err := io.Copy(buf, r)
if err != nil {
return nil, err
}
}
url, err := url.Parse(URL)
if err != nil {
return nil, err
}
path := url.Path
if path == "" {
path = "/"
}
// Discover requires only these headers and in this exact order, or it
// returns HTTP 403
headers := fmt.Sprintf("POST %s HTTP/1.1\r\n"+
"Content-Type: application/x-ofx\r\n"+
"Host: %s\r\n"+
"Content-Length: %d\r\n"+
"Connection: Keep-Alive\r\n"+
"\r\n", path, url.Hostname(), buf.Len())
host := url.Host
if url.Port() == "" {
host += ":443"
}
// BUGBUG: cannot do defer conn.Close() until body is read,
// we are "leaking" a socket here, but it will be finalized
conn, err := tls.Dial("tcp", host, nil)
if err != nil {
return nil, err
}
fmt.Fprint(conn, headers)
_, err = io.Copy(conn, buf)
if err != nil {
return nil, err
}
return http.ReadResponse(bufio.NewReader(conn), nil)
}
// RawRequest is a convenience wrapper around http.Post. It is exposed only for
// when you need to read/inspect the raw HTTP response yourself.
func (c *DiscoverCardClient) RawRequest(URL string, r io.Reader) (*http.Response, error) {
if !strings.HasPrefix(URL, "https://") {
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
}
response, err := discoverCardHTTPPost(URL, r)
if err != nil {
return nil, err
}
if response.StatusCode != 200 {
return nil, errors.New("OFXQuery request status: " + response.Status)
}
return response, nil
}
// RequestNoParse marshals a Request to XML, makes an HTTP request, and returns
// the raw HTTP response
func (c *DiscoverCardClient) RequestNoParse(r *Request) (*http.Response, error) {
return clientRequestNoParse(c, r)
}
// Request marshals a Request to XML, makes an HTTP request, and then
// unmarshals the response into a Response object.
func (c *DiscoverCardClient) Request(r *Request) (*Response, error) {
return clientRequest(c, r)
}

127
vendor/github.com/aclindsa/ofxgo/doc.go generated vendored Normal file
View File

@@ -0,0 +1,127 @@
/*
Package ofxgo seeks to provide a library to make it easier to query and/or
parse financial information with OFX from the comfort of Golang, without having
to deal with marshalling/unmarshalling the SGML or XML. The library does *not*
intend to abstract away all of the details of the OFX specification, which
would be difficult to do well. Instead, it exposes the OFX SGML/XML hierarchy
as structs which mostly resemble it. For more information on OFX and to read
the specification, see http://ofx.net.
There are three main top-level objects defined in ofxgo. These are Client,
Request, and Response. The Request and Response objects represent OFX requests
and responses as Golang structs. Client contains settings which control how
requests and responses are marshalled and unmarshalled (the OFX version used,
client id and version, whether to indent SGML/XML tags, etc.), and provides
helper methods for making requests and optionally parsing the response using
those settings.
Every Request object contains a SignonRequest element, called Signon. This
element contains the username, password (or key), and the ORG and FID fields
particular to the financial institution being queried, and an optional ClientUID
field (required by some FIs). Likewise, each Response contains a SignonResponse
object which contains, among other things, the Status of the request. Any status
with a nonzero Code should be inspected for a possible error (using the Severity
and Message fields populated by the server, or the CodeMeaning() and
CodeConditions() functions which return information about a particular code as
specified by the OFX specification).
Each top-level Request or Response object may contain zero or more messages,
sorted into named slices by message set, just as the OFX specification groups
them. Here are the supported types of Request/Response objects (along with the
name of the slice of Messages they belong to in parentheses):
Requests:
var r AcctInfoRequest // (Signup) Request a list of the valid accounts
// for this user
var r CCStatementRequest // (CreditCard) Request the balance (and optionally
// list of transactions) for a credit card
var r StatementRequest // (Bank) Request the balance (and optionally list
// of transactions) for a bank account
var r InvStatementRequest // (InvStmt) Request balance, transactions,
// existing positions, and/or open orders for an
// investment account
var r SecListRequest // (SecList) Request securities details and prices
var r ProfileRequest // (Prof) Request the server's capabilities (which
// messages sets it supports, along with features)
Responses:
var r AcctInfoResponse // (Signup) List of the valid accounts for this
// user
var r CCStatementResponse // (CreditCard) The balance (and optionally list of
// transactions) for a credit card
var r StatementResponse // (Bank) The balance (and optionally list of
// transactions) for a bank account
var r InvStatementResponse // (InvStmt) The balance, transactions, existing
// positions, and/or open orders for an
// investment account
var r SecListResponse // (SecList) Returned as a result of
// SecListRequest, but only contains request
// status
var r SecurityList // (SecList) The actual list of securities, prices,
// etc. (sent as a result of SecListRequest or
// InvStatementRequest)
var r ProfileResponse // (Prof) Describes the server's capabilities
When constructing a Request, simply append the desired message to the message
set it belongs to. For Responses, it is the user's responsibility to make type
assertions on objects found inside one of these message sets before using them.
For example, the following code would request a bank statement for a checking
account and print the balance:
import (
"fmt"
"os"
)
var client Client // By not initializing them, we accept all default
// client values
var request Request
// These are all specific to you and your financial institution
request.URL = "https://ofx.example.com"
request.Signon.UserID = String("john")
request.Signon.UserPass = String("hunter2")
request.Signon.Org = String("MyBank")
request.Signon.Fid = String("0001")
uid, err := RandomUID()
if err != nil {
fmt.Println("Error creating uid for transaction:", err)
os.Exit(1)
}
statementRequest := StatementRequest{
TrnUID: *uid,
BankAcctFrom: BankAcct{
BankID: String("123456789"),
AcctID: String("11111111111"),
AcctType: AcctTypeChecking,
},
}
request.Bank = append(request.Bank, &statementRequest)
response, err := client.Request(request)
if err != nil {
fmt.Println("Error requesting account statement:", err)
os.Exit(1)
}
if response.Signon.Status.Code != 0 {
meaning, _ := response.Signon.Status.CodeMeaning()
fmt.Printf("Nonzero signon status (%d: %s) with message: %s\n", response.Signon.Status.Code, meaning, response.Signon.Status.Message)
os.Exit(1)
}
if len(response.Bank) < 1 {
fmt.Println("No banking messages received")
} else if stmt, ok := response.Bank[0].(*StatementResponse); ok {
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf)
}
More usage examples may be found in the example command-line client provided
with this library, in the cmd/ofx directory of the source.
*/
package ofxgo

239
vendor/github.com/aclindsa/ofxgo/generate_constants.py generated vendored Normal file
View File

@@ -0,0 +1,239 @@
#!/usr/bin/env python
enums = {
# OFX spec version
"OfxVersion": (["102", "103", "151", "160", "200", "201", "202", "203", "210", "211", "220"], "the OFX specification version in use"),
# Bank/general
"AcctType": (["Checking", "Savings", "MoneyMrkt", "CreditLine", "CD"], "types of bank accounts"),
"TrnType": (["Credit", "Debit", "Int", "Div", "Fee", "SrvChg", "Dep", "ATM", "POS", "Xfer", "Check", "Payment", "Cash", "DirectDep", "DirectDebit", "RepeatPmt", "Hold", "Other"], "types of transactions. INT, ATM, and POS depend on the signage of the account."),
"ImageType": (["Statement", "Transaction", "Tax"], "what this image contains"),
"ImageRefType": (["Opaque", "URL", "FormURL"], "the type of reference to the image"),
"CheckSup": (["FrontOnly", "BackOnly", "FrontAndBack"], "what portions of the check this image contains"),
"CorrectAction": (["Delete", "Replace"], "whether this transaction correction replaces or deletes the transaction matching its CORRECTFITID"),
"BalType": (["Dollar", "Percent", "Number"], "how this BAL's VALUE field should be interpreted"),
# InvStmt
"Inv401kSource": (["PreTax", "AfterTax", "Match", "ProfitSharing", "Rollover", "OtherVest", "OtherNonVest"], "the source of money used for this security in a 401(k) account. Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST."),
"SubAcctType": (["Cash", "Margin", "Short", "Other"], "the sub-account type for a source and/or destination of a transaction. Used in fields named SubAcctFrom, SubAcctTo, SubAcctSec, SubAcctFund, HeldInAcct."),
"BuyType": (["Buy", "BuyToCover"], "types of purchases"),
"OptAction": (["Exercise", "Assign", "Expire"], "types of actions for options"),
"TferAction": (["In", "Out"], "whether the transfer is into or out of this account"),
"PosType": (["Long", "Short"], "position type"),
"Secured": (["Naked", "Covered"], "how an option is secured"),
"Duration": (["Day", "GoodTilCancel", "Immediate"], "how long the investment order is good for"),
"Restriction": (["AllOrNone", "MinUnits", "None"], "a special restriction on an investment order"),
"UnitType": (["Shares", "Currency"], "type of the UNITS value"),
"OptBuyType": (["BuyToOpen", "BuyToClose"], "types of purchases for options"),
"SellType": (["Sell", "SellShort"], "types of sales"),
"LoanPmtFreq": (["Weekly", "Biweekly", "TwiceMonthly", "Monthly", "FourWeeks", "BiMonthly", "Quarterly", "Semiannually", "Annually", "Other"], "the frequency of loan payments"),
"IncomeType": (["CGLong", "CGShort", "Div", "Interest", "Misc"], "types of investment income"),
"SellReason": (["Call", "Sell", "Maturity"], "the reason the sell of a debt security was generated: CALL (the debt was called), SELL (the debt was sold), MATURITY (the debt reached maturity)"),
"OptSellType": (["SellToClose", "SellToOpen"], "types of sales for options"),
"RelType": (["Spread", "Straddle", "None", "Other"], "related option transaction types"),
# Prof
"CharType": (["AlphaOnly", "NumericOnly", "AlphaOrNumeric", "AlphaAndNumeric"], "types of characters allowed in password"),
"SyncMode": (["Full", "Lite"], "data synchronization mode supported (see OFX spec for more details)"),
"OfxSec": (["None", "Type 1"], "the type of application-level security required for the message set"),
# SecList
"DebtType": (["Coupon", "Zero"], "debt type"),
"DebtClass": (["Treasury", "Municipal", "Corporate", "Other"], "the class of debt"),
"CouponFreq": (["Monthly", "Quarterly", "Semiannual", "Annual", "Other"], "when debt coupons mature"),
"CallType": (["Call", "Put", "Prefund", "Maturity"], "type of next call (for a debt)"),
"AssetClass": (["DomesticBond", "IntlBond", "LargeStock", "SmallStock", "IntlStock", "MoneyMrkt", "Other"], "type of asset classes"),
"MfType": (["OpenEnd", "CloseEnd", "Other"], "types of mutual funds"),
"OptType": (["Put", "Call"], "whether the option is a PUT or a CALL"),
"StockType": (["Common", "Preferred", "Convertible", "Other"], "types of stock"),
# Signup
"HolderType": (["Individual", "Joint", "Custodial", "Trust", "Other"], "how the account is held"),
"AcctClassification": (["Personal", "Business", "Corporate", "Other"], "the type of an account"),
"SvcStatus": (["Avail", "Pend", "Active"], "the status of the account: AVAIL = Available, but not yet requested, PEND = Requested, but not yet available, ACTIVE = In use"),
"UsProductType": (["401K", "403B", "IRA", "KEOGH", "Other", "SARSEP", "Simple", "Normal", "TDA", "Trust", "UGMA"], "type of investment account (in the US)"),
}
header = """package ofxgo
/*
* Do not edit this file by hand. It is auto-generated by calling `go generate`.
* To make changes, edit generate_constants.py, re-run `go generate`, and check
* in the result.
*/
import (
"errors"
"fmt"
"strings"
"github.com/aclindsa/xml"
)
"""
template = """
type {enumLower} uint
// {enum}* constants represent {comment}
const (
{constNames})
var {enumLower}s = [...]string{{"{upperValueString}"}}
func (e {enumLower}) Valid() bool {{
// This check is mostly out of paranoia, ensuring e != 0 should be
// sufficient
return e >= {firstValue} && e <= {lastValue}
}}
func (e {enumLower}) String() string {{
if e.Valid() {{
return {enumLower}s[e-1]
}}
return fmt.Sprintf("invalid {enumLower} (%d)", e)
}}
func (e *{enumLower}) FromString(in string) error {{
value := strings.TrimSpace(in)
for i, s := range {enumLower}s {{
if s == value {{
*e = {enumLower}(i + 1)
return nil
}}
}}
*e = 0
return errors.New("Invalid {enum}: \\\"" + in + "\\\"")
}}
func (e *{enumLower}) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {{
var value string
err := d.DecodeElement(&value, &start)
if err != nil {{
return err
}}
return e.FromString(value)
}}
func (e *{enumLower}) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {{
if !e.Valid() {{
return nil
}}
enc.EncodeElement({enumLower}s[*e-1], start)
return nil
}}
// New{enum} returns returns an 'enum' value of type {enumLower} given its
// string representation
func New{enum}(s string) ({enumLower}, error) {{
var e {enumLower}
err := e.FromString(s)
if err != nil {{
return 0, err
}}
return e, nil
}}
"""
with open("constants.go", 'w') as f:
f.write(header)
for enum in enums:
enumLower = enum[:1].lower() + enum[1:].replace(" ", "")
firstValue = enum+enums[enum][0][0].replace(" ", "")
lastValue = enum+enums[enum][0][-1].replace(" ", "")
comment = enums[enum][1]
constNames = "\t{firstValue} {enumLower} = 1 + iota\n".format(
enum=enum,
firstValue=firstValue,
enumLower=enumLower)
for value in enums[enum][0][1:]:
constNames += "\t{enum}{value}\n".format(
enum=enum,
value=value.replace(" ", ""))
upperValueString = "\", \"".join([s.upper() for s in enums[enum][0]])
f.write(template.format(enum=enum,
enumLower=enumLower,
comment=comment,
firstValue=firstValue,
lastValue=lastValue,
constNames=constNames,
upperValueString=upperValueString))
test_header = """package ofxgo
/*
* Do not edit this file by hand. It is auto-generated by calling `go generate`.
* To make changes, edit generate_constants.py, re-run `go generate`, and check
* in the result.
*/
import (
"strings"
"testing"
"github.com/aclindsa/xml"
)
"""
test_template = """
func Test{enum}(t *testing.T) {{
e, err := New{enum}("{firstValueUpper}")
if err != nil {{
t.Fatalf("Unexpected error creating new {enum} from string \\\"{firstValueUpper}\\\"\\n")
}}
if !e.Valid() {{
t.Fatalf("{enum} unexpectedly invalid\\n")
}}
err = e.FromString("{lastValueUpper}")
if err != nil {{
t.Fatalf("Unexpected error on {enum}.FromString(\\\"{lastValueUpper}\\\")\\n")
}}
if e.String() != "{lastValueUpper}" {{
t.Fatalf("{enum}.String() expected to be \\\"{lastValueUpper}\\\"\\n")
}}
marshalHelper(t, "{lastValueUpper}", &e)
overwritten, err := New{enum}("THISWILLNEVERBEAVALIDENUMSTRING")
if err == nil {{
t.Fatalf("Expected error creating new {enum} from string \\\"THISWILLNEVERBEAVALIDENUMSTRING\\\"\\n")
}}
if overwritten.Valid() {{
t.Fatalf("{enum} created with string \\\"THISWILLNEVERBEAVALIDENUMSTRING\\\" should not be valid\\n")
}}
if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") {{
t.Fatalf("{enum} created with string \\\"THISWILLNEVERBEAVALIDENUMSTRING\\\" should not return valid string from String()\\n")
}}
b, err := xml.Marshal(&overwritten)
if err != nil {{
t.Fatalf("Unexpected error on xml.Marshal({enum}): %s\\n", err)
}}
if string(b) != "" {{
t.Fatalf("Expected empty string, got '%s'\\n", string(b))
}}
unmarshalHelper(t, "{lastValueUpper}", &e, &overwritten)
err = xml.Unmarshal([]byte("<GARBAGE><!LALDK>"), &overwritten)
if err == nil {{
t.Fatalf("Expected error unmarshalling garbage value\\n")
}}
}}
"""
with open("constants_test.go", 'w') as f:
f.write(test_header)
for enum in enums:
firstValueUpper = enums[enum][0][0].upper()
lastValueUpper = enums[enum][0][-1].upper()
f.write(test_template.format(enum=enum,
firstValueUpper=firstValueUpper,
lastValueUpper=lastValueUpper))

1414
vendor/github.com/aclindsa/ofxgo/invstmt.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

344
vendor/github.com/aclindsa/ofxgo/leaf_elements.go generated vendored Normal file
View File

@@ -0,0 +1,344 @@
package ofxgo
// A list of all the leaf elements in OFX 1.0.3 (the last SGML version of the
// spec). These are all the elements that are possibly left unclosed, and which
// can have no children of their own. Fortunately these two sets of elements
// are the same. We use this list when parsing to remove ambiguities about
// element nesting.
//
// Generated using the following command with the 1.0.3 SPEC .dtd file:
// # sed -rn 's/^<!ELEMENT\s+([A-Z0-9]+)\s+-\s+[oO]\s+%.*TYPE\s*>.*$/\t"\1",/p' *.dtd | sort
var ofxLeafElements = []string{
"ACCESSKEY",
"ACCRDINT",
"ACCTID",
"ACCTKEY",
"ACCTREQUIRED",
"ACCTTYPE",
"ADDR1",
"ADDR2",
"ADDR3",
"ADJAMT",
"ADJDATE",
"ADJDESC",
"ADJNO",
"APPID",
"APPVER",
"ASSETCLASS",
"AUCTION",
"AUTHTOKEN",
"AUTHTOKENFIRST",
"AUTHTOKENINFOURL",
"AUTHTOKENLABEL",
"AVAILACCTS",
"AVAILCASH",
"AVGCOSTBASIS",
"BALAMT",
"BALCLOSE",
"BALDNLD",
"BALMIN",
"BALOPEN",
"BALTYPE",
"BANKID",
"BILLREFINFO",
"BRANCHID",
"BROKERID",
"BUYPOWER",
"BUYTYPE",
"CALLPRICE",
"CALLTYPE",
"CANADDPAYEE",
"CANBILLPAY",
"CANCELWND",
"CANEMAIL",
"CANMODMDLS",
"CANMODPMTS",
"CANMODXFERS",
"CANNOTIFY",
"CANPENDING",
"CANRECUR",
"CANSCHED",
"CANUSEDESC",
"CANUSERANGE",
"CASESEN",
"CHARTYPE",
"CHECKING",
"CHECKNUM",
"CHGPINFIRST",
"CHGUSERINFO",
"CHKANDDEB",
"CHKERROR",
"CHKNUMEND",
"CHKNUMSTART",
"CHKSTATUS",
"CITY",
"CLIENTACTREQ",
"CLIENTROUTING",
"CLIENTUID",
"CLIENTUIDREQ",
"CLOSINGAVAIL",
"CLTCOOKIE",
"CODE",
"COMMISSION",
"CONFMSG",
"CORRECTACTION",
"CORRECTFITID",
"COUNTRY",
"COUPONFREQ",
"COUPONRT",
"CREDITLIMIT",
"CSPHONE",
"CURDEF",
"CURRATE",
"CURSYM",
"DATEBIRTH",
"DAYPHONE",
"DAYSTOPAY",
"DAYSWITH",
"DEBADJ",
"DEBTCLASS",
"DEBTTYPE",
"DENOMINATOR",
"DEPANDCREDIT",
"DESC",
"DFLTDAYSTOPAY",
"DIFFFIRSTPMT",
"DIFFLASTPMT",
"DOMXFERFEE",
"DSCAMT",
"DSCDATE",
"DSCDESC",
"DSCRATE",
"DTACCTUP",
"DTASOF",
"DTAUCTION",
"DTAVAIL",
"DTCALL",
"DTCHANGED",
"DTCLIENT",
"DTCLOSE",
"DTCOUPON",
"DTCREATED",
"DTDUE",
"DTEND",
"DTEXPIRE",
"DTINFOCHG",
"DTMAT",
"DTNEXT",
"DTOPEN",
"DTPLACED",
"DTPMTDUE",
"DTPMTPRC",
"DTPOSTED",
"DTPOSTEND",
"DTPOSTSTART",
"DTPRICEASOF",
"DTPROFUP",
"DTPURCHASE",
"DTSERVER",
"DTSETTLE",
"DTSTART",
"DTTRADE",
"DTUSER",
"DTXFERPRC",
"DTXFERPRJ",
"DTYIELDASOF",
"DURATION",
"EMAIL",
"EVEPHONE",
"EXTDPMTCHK",
"EXTDPMTFOR",
"FAXPHONE",
"FEE",
"FEEMSG",
"FEES",
"FIASSETCLASS",
"FICERTID",
"FID",
"FIID",
"FINALAMT",
"FINAME",
"FINCHG",
"FIRSTNAME",
"FITID",
"FRACCASH",
"FREQ",
"FROM",
"GAIN",
"GENUSERKEY",
"GETMIMESUP",
"HASEXTDPMT",
"HELDINACCT",
"IDSCOPE",
"INCBAL",
"INCIMAGES",
"INCLUDE",
"INCOMETYPE",
"INCOO",
"INITIALAMT",
"INTLXFERFEE",
"INVACCTTYPE",
"INVALIDACCTTYPE",
"INVDATE",
"INVDESC",
"INVNO",
"INVPAIDAMT",
"INVTOTALAMT",
"LANGUAGE",
"LASTNAME",
"LIMITPRICE",
"LITMAMT",
"LITMDESC",
"LOAD",
"LOSTSYNC",
"MAILSUP",
"MARGINBALANCE",
"MARKDOWN",
"MARKUP",
"MAX",
"MEMO",
"MESSAGE",
"MFACHALLENGEFIRST",
"MFACHALLENGESUPT",
"MFAPHRASEA",
"MFAPHRASEID",
"MFAPHRASELABEL",
"MFTYPE",
"MIDDLENAME",
"MIN",
"MINPMTDUE",
"MINUNITS",
"MKTGINFO",
"MKTVAL",
"MODELWND",
"MODPENDING",
"NAME",
"NEWUNITS",
"NEWUSERPASS",
"NINSTS",
"NONCE",
"NUMERATOR",
"OFXSEC",
"OLDUNITS",
"OODNLD",
"OPTACTION",
"OPTBUYTYPE",
"OPTIONLEVEL",
"OPTSELLTYPE",
"OPTTYPE",
"ORG",
"PARVALUE",
"PAYACCT",
"PAYANDCREDIT",
"PAYEEID",
"PAYEELSTID",
"PAYINSTRUCT",
"PERCENT",
"PHONE",
"PINCH",
"PMTBYADDR",
"PMTBYPAYEEID",
"PMTBYXFER",
"PMTPRCCODE",
"POSDNLD",
"POSTALCODE",
"POSTPROCWND",
"POSTYPE",
"PROCDAYSOFF",
"PROCENDTM",
"PURANDADV",
"RATING",
"RECSRVRTID",
"REFNUM",
"REFRESH",
"REFRESHSUPT",
"REINVCG",
"REINVDIV",
"REJECTIFMISSING",
"RELFITID",
"RELTYPE",
"RESPFILEER",
"RESTRICTION",
"SECLISTRQDNLD",
"SECNAME",
"SECURED",
"SECURITYNAME",
"SELLALL",
"SELLREASON",
"SELLTYPE",
"SESSCOOKIE",
"SEVERITY",
"SHORTBALANCE",
"SHPERCTRCT",
"SIC",
"SIGNONREALM",
"SPACES",
"SPECIAL",
"SPNAME",
"SRVRTID",
"STATE",
"STOCKTYPE",
"STOPPRICE",
"STPCHKFEE",
"STRIKEPRICE",
"STSVIAMODS",
"SUBACCT",
"SUBACCTFROM",
"SUBACCTSEC",
"SUBACCTTO",
"SUBJECT",
"SUPTXDL",
"SVC",
"SVCSTATUS",
"SWITCHALL",
"SYNCMODE",
"TAN",
"TAXES",
"TAXEXEMPT",
"TAXID",
"TEMPPASS",
"TFERACTION",
"TICKER",
"TO",
"TOKEN",
"TOKENONLY",
"TOTAL",
"TOTALFEES",
"TOTALINT",
"TRANDNLD",
"TRANSPSEC",
"TRNAMT",
"TRNTYPE",
"TRNUID",
"TSKEYEXPIRE",
"TSPHONE",
"TYPEDESC",
"UNIQUEID",
"UNIQUEIDTYPE",
"UNITPRICE",
"UNITS",
"UNITSSTREET",
"UNITSUSER",
"UNITTYPE",
"URL",
"USEHTML",
"USERCRED1",
"USERCRED1LABEL",
"USERCRED2",
"USERCRED2LABEL",
"USERID",
"USERKEY",
"USERPASS",
"USPRODUCTTYPE",
"VALUE",
"VER",
"WITHHOLDING",
"XFERDAYSWITH",
"XFERDEST",
"XFERDFLTDAYSTOPAY",
"XFERPRCCODE",
"XFERSRC",
"YIELD",
"YIELDTOCALL",
"YIELDTOMAT",
}

206
vendor/github.com/aclindsa/ofxgo/profile.go generated vendored Normal file
View File

@@ -0,0 +1,206 @@
package ofxgo
import (
"errors"
"github.com/aclindsa/xml"
"strings"
)
// ProfileRequest represents a request for a server to provide a profile of its
// capabilities (which message sets and versions it supports, how to access
// them, which languages and which types of synchronization they support, etc.)
type ProfileRequest struct {
XMLName xml.Name `xml:"PROFTRNRQ"`
TrnUID UID `xml:"TRNUID"`
CltCookie String `xml:"CLTCOOKIE,omitempty"`
TAN String `xml:"TAN,omitempty"` // Transaction authorization number
// TODO `xml:"OFXEXTENSION,omitempty"`
ClientRouting String `xml:"PROFRQ>CLIENTROUTING"` // Forced to NONE
DtProfUp Date `xml:"PROFRQ>DTPROFUP"` // Date and time client last received a profile update
}
// Name returns the name of the top-level transaction XML/SGML element
func (r *ProfileRequest) Name() string {
return "PROFTRNRQ"
}
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *ProfileRequest) Valid(version ofxVersion) (bool, error) {
if ok, err := r.TrnUID.Valid(); !ok {
return false, err
}
// TODO implement
r.ClientRouting = "NONE"
return true, nil
}
// Type returns which message set this message belongs to (which Request
// element of type []Message it should appended to)
func (r *ProfileRequest) Type() messageType {
return ProfRq
}
// SignonInfo provides the requirements to login to a single signon realm. A
// signon realm consists of all MessageSets which can be accessed using one set
// of login credentials. Most FI's only use one signon realm to make it easier
// and less confusing for the user.
type SignonInfo struct {
XMLName xml.Name `xml:"SIGNONINFO"`
SignonRealm String `xml:"SIGNONREALM"` // The SignonRealm for which this SignonInfo provides information. This SignonInfo is valid for all MessageSets with SignonRealm fields matching this one
Min Int `xml:"MIN"` // Minimum number of password characters
Max Int `xml:"MAX"` // Maximum number of password characters
CharType charType `xml:"CHARTYPE"` // One of ALPHAONLY, NUMERICONLY, ALPHAORNUMERIC, ALPHAANDNUMERIC
CaseSen Boolean `xml:"CASESEN"` // Password is case-sensitive?
Special Boolean `xml:"SPECIAL"` // Special characters allowed?
Spaces Boolean `xml:"SPACES"` // Spaces allowed?
PinCh Boolean `xml:"PINCH"` // Pin change <PINCHRQ> requests allowed
ChgPinFirst Boolean `xml:"CHGPINFIRST"` // Server requires user to change password at first signon
UserCred1Label String `xml:"USERCRED1LABEL,omitempty"` // Prompt for USERCRED1 (if this field is present, USERCRED1 is required)
UserCred2Label String `xml:"USERCRED2LABEL,omitempty"` // Prompt for USERCRED2 (if this field is present, USERCRED2 is required)
ClientUIDReq Boolean `xml:"CLIENTUIDREQ,omitempty"` // CLIENTUID required?
AuthTokenFirst Boolean `xml:"AUTHTOKENFIRST,omitempty"` // Server requires AUTHTOKEN as part of first signon
AuthTokenLabel String `xml:"AUTHTOKENLABEL,omitempty"`
AuthTokenInfoURL String `xml:"AUTHTOKENINFOURL,omitempty"`
MFAChallengeSupt Boolean `xml:"MFACHALLENGESUPT,omitempty"` // Server supports MFACHALLENGE
MFAChallengeFIRST Boolean `xml:"MFACHALLENGEFIRST,omitempty"` // Server requires MFACHALLENGE to be sent with first signon
AccessTokenReq Boolean `xml:"ACCESSTOKENREQ,omitempty"` // Server requires ACCESSTOKEN to be sent with all requests except profile
}
// MessageSet represents one message set supported by an FI and its
// capabilities
type MessageSet struct {
XMLName xml.Name // <xxxMSGSETVn>
Name string // <xxxMSGSETVn> (copy of XMLName.Local)
Ver Int `xml:"MSGSETCORE>VER"` // Message set version - should always match 'n' in <xxxMSGSETVn> of Name
URL String `xml:"MSGSETCORE>URL"` // URL where messages in this set are to be set
OfxSec ofxSec `xml:"MSGSETCORE>OFXSEC"` // NONE or 'TYPE 1'
TranspSec Boolean `xml:"MSGSETCORE>TRANSPSEC"` // Transport-level security must be used
SignonRealm String `xml:"MSGSETCORE>SIGNONREALM"` // Used to identify which SignonInfo to use for to this MessageSet
Language []String `xml:"MSGSETCORE>LANGUAGE"` // List of supported languages
SyncMode syncMode `xml:"MSGSETCORE>SYNCMODE"` // One of FULL, LITE
RefreshSupt Boolean `xml:"MSGSETCORE>REFRESHSUPT,omitempty"` // Y if server supports <REFRESH>Y within synchronizations. This option is irrelevant for full synchronization servers. Clients must ignore <REFRESHSUPT> (or its absence) if the profile also specifies <SYNCMODE>FULL. For lite synchronization, the default is N. Without <REFRESHSUPT>Y, lite synchronization servers are not required to support <REFRESH>Y requests
RespFileER Boolean `xml:"MSGSETCORE>RESPFILEER"` // server supports file-based error recovery
SpName String `xml:"MSGSETCORE>SPNAME"` // Name of service provider
// TODO MessageSet-specific stuff?
}
// MessageSetList is a list of MessageSets (necessary because they must be
// manually parsed)
type MessageSetList []MessageSet
// UnmarshalXML handles unmarshalling a MessageSetList element from an XML string
func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for {
var msgset MessageSet
tok, err := nextNonWhitespaceToken(d)
if err != nil {
return err
} 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
return nil
} else if _, ok := tok.(xml.StartElement); ok {
// Found starting tag for <xxxMSGSET>. Get the next one (xxxMSGSETVn) and decode that struct
tok, err := nextNonWhitespaceToken(d)
if err != nil {
return err
} else if versionStart, ok := tok.(xml.StartElement); ok {
if err := d.DecodeElement(&msgset, &versionStart); err != nil {
return err
}
} else {
return errors.New("Invalid MSGSETLIST formatting")
}
msgset.Name = msgset.XMLName.Local
// Eat ending tags for <xxxMSGSET>
tok, err = nextNonWhitespaceToken(d)
if err != nil {
return err
} else if _, ok := tok.(xml.EndElement); !ok {
return errors.New("Invalid MSGSETLIST formatting")
}
} else {
return errors.New("MSGSETLIST didn't find an opening xxxMSGSETVn element")
}
*msl = MessageSetList(append(*(*[]MessageSet)(msl), msgset))
}
}
// MarshalXML handles marshalling a MessageSetList element to an XML string
func (msl *MessageSetList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
messageSetListElement := xml.StartElement{Name: xml.Name{Local: "MSGSETLIST"}}
if err := e.EncodeToken(messageSetListElement); err != nil {
return err
}
for _, messageset := range *msl {
if !strings.HasSuffix(messageset.Name, "V1") {
return errors.New("Expected MessageSet.Name to end with \"V1\"")
}
messageSetName := strings.TrimSuffix(messageset.Name, "V1")
messageSetElement := xml.StartElement{Name: xml.Name{Local: messageSetName}}
if err := e.EncodeToken(messageSetElement); err != nil {
return err
}
start := xml.StartElement{Name: xml.Name{Local: messageset.Name}}
if err := e.EncodeElement(&messageset, start); err != nil {
return err
}
if err := e.EncodeToken(messageSetElement.End()); err != nil {
return err
}
}
if err := e.EncodeToken(messageSetListElement.End()); err != nil {
return err
}
return nil
}
// ProfileResponse contains a requested profile of the server's capabilities
// (which message sets and versions it supports, how to access them, which
// languages and which types of synchronization they support, etc.). Note that
// if the server does not support ClientRouting=NONE (as we always send with
// ProfileRequest), this may be an error)
type ProfileResponse struct {
XMLName xml.Name `xml:"PROFTRNRS"`
TrnUID UID `xml:"TRNUID"`
Status Status `xml:"STATUS"`
CltCookie String `xml:"CLTCOOKIE,omitempty"`
// TODO `xml:"OFXEXTENSION,omitempty"`
MessageSetList MessageSetList `xml:"PROFRS>MSGSETLIST"`
SignonInfoList []SignonInfo `xml:"PROFRS>SIGNONINFOLIST>SIGNONINFO"`
DtProfUp Date `xml:"PROFRS>DTPROFUP"`
FiName String `xml:"PROFRS>FINAME"`
Addr1 String `xml:"PROFRS>ADDR1"`
Addr2 String `xml:"PROFRS>ADDR2,omitempty"`
Addr3 String `xml:"PROFRS>ADDR3,omitempty"`
City String `xml:"PROFRS>CITY"`
State String `xml:"PROFRS>STATE"`
PostalCode String `xml:"PROFRS>POSTALCODE"`
Country String `xml:"PROFRS>COUNTRY"`
CsPhone String `xml:"PROFRS>CSPHONE,omitempty"`
TsPhone String `xml:"PROFRS>TSPHONE,omitempty"`
FaxPhone String `xml:"PROFRS>FAXPHONE,omitempty"`
URL String `xml:"PROFRS>URL,omitempty"`
Email String `xml:"PROFRS>EMAIL,omitempty"`
}
// Name returns the name of the top-level transaction XML/SGML element
func (pr *ProfileResponse) Name() string {
return "PROFTRNRS"
}
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (pr *ProfileResponse) Valid(version ofxVersion) (bool, error) {
if ok, err := pr.TrnUID.Valid(); !ok {
return false, err
}
//TODO implement
return true, nil
}
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (pr *ProfileResponse) Type() messageType {
return ProfRs
}

151
vendor/github.com/aclindsa/ofxgo/request.go generated vendored Normal file
View File

@@ -0,0 +1,151 @@
package ofxgo
import (
"bytes"
"errors"
"github.com/aclindsa/xml"
"time"
)
// Request is the top-level object marshalled and sent to OFX servers. It is
// constructed by appending one or more request objects to the message set they
// correspond to (i.e. appending StatementRequest to Request.Bank to get a bank
// statemement). If a *Request object is appended to the wrong message set, an
// error will be returned when Marshal() is called on this Request.
type Request struct {
URL string
Version ofxVersion // OFX version, overwritten in Client.Request()
Signon SignonRequest //<SIGNONMSGSETV1>
Signup []Message //<SIGNUPMSGSETV1>
Bank []Message //<BANKMSGSETV1>
CreditCard []Message //<CREDITCARDMSGSETV1>
Loan []Message //<LOANMSGSETV1>
InvStmt []Message //<INVSTMTMSGSETV1>
InterXfer []Message //<INTERXFERMSGSETV1>
WireXfer []Message //<WIREXFERMSGSETV1>
Billpay []Message //<BILLPAYMSGSETV1>
Email []Message //<EMAILMSGSETV1>
SecList []Message //<SECLISTMSGSETV1>
PresDir []Message //<PRESDIRMSGSETV1>
PresDlv []Message //<PRESDLVMSGSETV1>
Prof []Message //<PROFMSGSETV1>
Image []Message //<IMAGEMSGSETV1>
indent bool // Whether to indent the marshaled XML
carriageReturn bool // Whether to user carriage returns in new lines for marshaled XML
}
func encodeMessageSet(e *xml.Encoder, requests []Message, set messageType, version ofxVersion) error {
if len(requests) > 0 {
messageSetElement := xml.StartElement{Name: xml.Name{Local: set.String()}}
if err := e.EncodeToken(messageSetElement); err != nil {
return err
}
for _, request := range requests {
if request.Type() != set {
return errors.New("Expected " + set.String() + " message , found " + request.Type().String())
}
if ok, err := request.Valid(version); !ok {
return err
}
if err := e.Encode(request); err != nil {
return err
}
}
if err := e.EncodeToken(messageSetElement.End()); err != nil {
return err
}
}
return nil
}
// SetClientFields overwrites the fields in this Request object controlled by
// the Client
func (oq *Request) SetClientFields(c Client) {
oq.Signon.DtClient.Time = time.Now()
// Overwrite fields that the client controls
oq.Version = c.OfxVersion()
oq.Signon.AppID = c.ID()
oq.Signon.AppVer = c.Version()
oq.indent = c.IndentRequests()
oq.carriageReturn = c.CarriageReturnNewLines()
}
// Marshal this Request into its SGML/XML representation held in a bytes.Buffer
//
// If error is non-nil, this bytes.Buffer is ready to be sent to an OFX server
func (oq *Request) Marshal() (*bytes.Buffer, error) {
var b bytes.Buffer
// Write the header appropriate to our version
writeHeader(&b, oq.Version, oq.carriageReturn)
encoder := xml.NewEncoder(&b)
if oq.indent {
encoder.Indent("", " ")
}
if oq.carriageReturn {
encoder.CarriageReturn(true)
}
if oq.Version < OfxVersion200 {
// OFX 100 series versions should avoid element close tags for compatibility
encoder.SetDisableAutoClose(ofxLeafElements...)
}
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
if err := encoder.EncodeToken(ofxElement); err != nil {
return nil, err
}
if ok, err := oq.Signon.Valid(oq.Version); !ok {
return nil, err
}
signonMsgSet := xml.StartElement{Name: xml.Name{Local: SignonRq.String()}}
if err := encoder.EncodeToken(signonMsgSet); err != nil {
return nil, err
}
if err := encoder.Encode(&oq.Signon); err != nil {
return nil, err
}
if err := encoder.EncodeToken(signonMsgSet.End()); err != nil {
return nil, err
}
messageSets := []struct {
Messages []Message
Type messageType
}{
{oq.Signup, SignupRq},
{oq.Bank, BankRq},
{oq.CreditCard, CreditCardRq},
{oq.Loan, LoanRq},
{oq.InvStmt, InvStmtRq},
{oq.InterXfer, InterXferRq},
{oq.WireXfer, WireXferRq},
{oq.Billpay, BillpayRq},
{oq.Email, EmailRq},
{oq.SecList, SecListRq},
{oq.PresDir, PresDirRq},
{oq.PresDlv, PresDlvRq},
{oq.Prof, ProfRq},
{oq.Image, ImageRq},
}
for _, set := range messageSets {
if err := encodeMessageSet(encoder, set.Messages, set.Type, oq.Version); err != nil {
return nil, err
}
}
if err := encoder.EncodeToken(ofxElement.End()); err != nil {
return nil, err
}
if err := encoder.Flush(); err != nil {
return nil, err
}
return &b, nil
}

505
vendor/github.com/aclindsa/ofxgo/response.go generated vendored Normal file
View File

@@ -0,0 +1,505 @@
package ofxgo
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"reflect"
"regexp"
"strings"
"github.com/aclindsa/xml"
)
// Response is the top-level object returned from a parsed OFX response file.
// It can be inspected by using type assertions or switches on the message set
// you're interested in.
type Response struct {
Version ofxVersion // OFX header version
Signon SignonResponse //<SIGNONMSGSETV1>
Signup []Message //<SIGNUPMSGSETV1>
Bank []Message //<BANKMSGSETV1>
CreditCard []Message //<CREDITCARDMSGSETV1>
Loan []Message //<LOANMSGSETV1>
InvStmt []Message //<INVSTMTMSGSETV1>
InterXfer []Message //<INTERXFERMSGSETV1>
WireXfer []Message //<WIREXFERMSGSETV1>
Billpay []Message //<BILLPAYMSGSETV1>
Email []Message //<EMAILMSGSETV1>
SecList []Message //<SECLISTMSGSETV1>
PresDir []Message //<PRESDIRMSGSETV1>
PresDlv []Message //<PRESDLVMSGSETV1>
Prof []Message //<PROFMSGSETV1>
Image []Message //<IMAGEMSGSETV1>
}
func (or *Response) readSGMLHeaders(r *bufio.Reader) error {
b, err := r.ReadSlice('<')
if err != nil {
return err
}
s := string(b)
err = r.UnreadByte()
if err != nil {
return err
}
// According to the latest OFX SGML spec (1.6), headers should be CRLF-separated
// and written as KEY:VALUE. However, some banks include a whitespace after the
// colon (KEY: VALUE), while others include no line breaks at all. The spec doesn't
// require a line break after the OFX headers, but it is allowed, and will be
// optionally captured & discarded by the trailing `\s*`. Valid SGML headers must
// always be present in exactly this order, so a regular expression is acceptable.
headerExp := regexp.MustCompile(
`^OFXHEADER:\s*(?P<OFXHEADER>\d+)\s*` +
`DATA:\s*(?P<DATA>[A-Z]+)\s*` +
`VERSION:\s*(?P<VERSION>\d+)\s*` +
`SECURITY:\s*(?P<SECURITY>[\w]+)\s*` +
`ENCODING:\s*(?P<ENCODING>[A-Z0-9-]+)\s*` +
`CHARSET:\s*(?P<CHARSET>[\w-]+)\s*` +
`COMPRESSION:\s*(?P<COMPRESSION>[A-Z]+)\s*` +
`OLDFILEUID:\s*(?P<OLDFILEUID>[\w-]+)\s*` +
`NEWFILEUID:\s*(?P<NEWFILEUID>[\w-]+)\s*<$`)
matches := headerExp.FindStringSubmatch(s)
if len(matches) == 0 {
return errors.New("OFX headers malformed")
}
for i, name := range headerExp.SubexpNames() {
if i == 0 {
continue
}
headerValue := matches[i]
switch name {
case "OFXHEADER":
if headerValue != "100" {
return errors.New("OFXHEADER is not 100")
}
case "DATA":
if headerValue != "OFXSGML" {
return errors.New("OFX DATA header does not contain OFXSGML")
}
case "VERSION":
err := or.Version.FromString(headerValue)
if err != nil {
return err
}
if or.Version > OfxVersion160 {
return errors.New("OFX VERSION > 160 in SGML header")
}
case "SECURITY":
if !(headerValue == "NONE" || headerValue == "TYPE1") {
return errors.New("OFX SECURITY header must be NONE or TYPE1")
}
case "COMPRESSION":
if headerValue != "NONE" {
return errors.New("OFX COMPRESSION header not NONE")
}
case "ENCODING", "CHARSET", "OLDFILEUID", "NEWFILEUID":
// TODO: check/handle these headers?
}
}
return nil
}
func (or *Response) readXMLHeaders(decoder *xml.Decoder) error {
var tok xml.Token
tok, err := nextNonWhitespaceToken(decoder)
if err != nil {
return err
} else if xmlElem, ok := tok.(xml.ProcInst); !ok || xmlElem.Target != "xml" {
return errors.New("Missing xml processing instruction")
}
// parse the OFX header
tok, err = nextNonWhitespaceToken(decoder)
if err != nil {
return err
} else if ofxElem, ok := tok.(xml.ProcInst); ok && ofxElem.Target == "OFX" {
var seenHeader, seenVersion bool = false, false
headers := bytes.TrimSpace(ofxElem.Inst)
for len(headers) > 0 {
tmp := bytes.SplitN(headers, []byte("=\""), 2)
if len(tmp) != 2 {
return errors.New("Malformed OFX header")
}
header := string(tmp[0])
headers = tmp[1]
tmp = bytes.SplitN(headers, []byte("\""), 2)
if len(tmp) != 2 {
return errors.New("Malformed OFX header")
}
value := string(tmp[0])
headers = bytes.TrimSpace(tmp[1])
switch header {
case "OFXHEADER":
if value != "200" {
return errors.New("OFXHEADER is not 200")
}
seenHeader = true
case "VERSION":
err := or.Version.FromString(value)
if err != nil {
return err
}
seenVersion = true
if or.Version < OfxVersion200 {
return errors.New("OFX VERSION < 200 in XML header")
}
case "SECURITY":
if value != "NONE" {
return errors.New("OFX SECURITY header not NONE")
}
case "OLDFILEUID", "NEWFILEUID":
// TODO check/handle these headers?
default:
return errors.New("Invalid OFX header: " + header)
}
}
if !seenHeader {
return errors.New("OFXHEADER version missing")
}
if !seenVersion {
return errors.New("OFX VERSION header missing")
}
} else {
return errors.New("Missing xml 'OFX' processing instruction")
}
return nil
}
// Number of bytes of response to read when attempting to figure out whether
// we're using OFX or SGML
const guessVersionCheckBytes = 1024
// Defaults to XML if it can't determine the version or if there is any
// ambiguity
// Returns false for SGML, true (for XML) otherwise.
func guessVersion(r *bufio.Reader) (bool, error) {
b, _ := r.Peek(guessVersionCheckBytes)
if b == nil {
return false, errors.New("Failed to read OFX header")
}
sgmlIndex := bytes.Index(b, []byte("OFXHEADER:"))
xmlIndex := bytes.Index(b, []byte("OFXHEADER="))
if sgmlIndex < 0 {
return true, nil
} else if xmlIndex < 0 {
return false, nil
} else {
return xmlIndex <= sgmlIndex, nil
}
}
// A map of message set tags to a map of transaction wrapper tags to the
// reflect.Type of the struct for that transaction type. Used when decoding
// Responses. Newly-implemented response transaction types *must* be added to
// this map in order to be unmarshalled.
var responseTypes = map[string]map[string]reflect.Type{
SignupRs.String(): {
(&AcctInfoResponse{}).Name(): reflect.TypeOf(AcctInfoResponse{})},
BankRs.String(): {
(&StatementResponse{}).Name(): reflect.TypeOf(StatementResponse{})},
CreditCardRs.String(): {
(&CCStatementResponse{}).Name(): reflect.TypeOf(CCStatementResponse{})},
LoanRs.String(): {},
InvStmtRs.String(): {
(&InvStatementResponse{}).Name(): reflect.TypeOf(InvStatementResponse{})},
InterXferRs.String(): {},
WireXferRs.String(): {},
BillpayRs.String(): {},
EmailRs.String(): {},
SecListRs.String(): {
(&SecListResponse{}).Name(): reflect.TypeOf(SecListResponse{}),
(&SecurityList{}).Name(): reflect.TypeOf(SecurityList{})},
PresDirRs.String(): {},
PresDlvRs.String(): {},
ProfRs.String(): {
(&ProfileResponse{}).Name(): reflect.TypeOf(ProfileResponse{})},
ImageRs.String(): {},
}
func decodeMessageSet(d *xml.Decoder, start xml.StartElement, msgs *[]Message, version ofxVersion) error {
setTypes, ok := responseTypes[start.Name.Local]
if !ok {
return errors.New("Invalid message set: " + start.Name.Local)
}
for {
tok, err := nextNonWhitespaceToken(d)
if err != nil {
return err
} 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
return nil
} else if startElement, ok := tok.(xml.StartElement); ok {
responseType, ok := setTypes[startElement.Name.Local]
if !ok {
// If you are a developer and received this message after you
// thought you added a new transaction type, make sure you
// added it to the responseTypes map above
return errors.New("Unsupported response transaction for " +
start.Name.Local + ": " + startElement.Name.Local)
}
response := reflect.New(responseType).Interface()
responseMessage := response.(Message)
if err := d.DecodeElement(responseMessage, &startElement); err != nil {
return err
}
*msgs = append(*msgs, responseMessage)
} else {
return errors.New("Didn't find an opening element")
}
}
}
// ParseResponse parses and validates an OFX response in SGML or XML into a
// Response object from the given io.Reader
//
// It is commonly used as part of Client.Request(), but may be used on its own
// to parse already-downloaded OFX files (such as those from 'Web Connect'). It
// performs version autodetection if it can and attempts to be as forgiving as
// possible about the input format.
func ParseResponse(reader io.Reader) (*Response, error) {
resp, err := DecodeResponse(reader)
if err != nil {
return nil, err
}
_, err = resp.Valid()
return resp, err
}
// DecodeResponse parses an OFX response in SGML or XML into a Response object
// from the given io.Reader
func DecodeResponse(reader io.Reader) (*Response, error) {
var or Response
r := bufio.NewReaderSize(reader, guessVersionCheckBytes)
xmlVersion, err := guessVersion(r)
if err != nil {
return nil, err
}
// parse SGML headers before creating XML decoder
if !xmlVersion {
if err := or.readSGMLHeaders(r); err != nil {
return nil, err
}
}
decoder := xml.NewDecoder(r)
if !xmlVersion {
decoder.Strict = false
decoder.AutoCloseAfterCharData = ofxLeafElements
}
decoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
return input, nil
}
if xmlVersion {
// parse the xml header
if err := or.readXMLHeaders(decoder); err != nil {
return nil, err
}
}
tok, err := nextNonWhitespaceToken(decoder)
if err != nil {
return nil, err
} else if ofxStart, ok := tok.(xml.StartElement); !ok || ofxStart.Name.Local != "OFX" {
return nil, errors.New("Missing opening OFX xml element")
}
// Unmarshal the signon message
tok, err = nextNonWhitespaceToken(decoder)
if err != nil {
return nil, err
} else if signonStart, ok := tok.(xml.StartElement); ok && signonStart.Name.Local == SignonRs.String() {
if err := decoder.Decode(&or.Signon); err != nil {
return nil, err
}
} else {
return nil, errors.New("Missing opening SIGNONMSGSRSV1 xml element")
}
tok, err = nextNonWhitespaceToken(decoder)
if err != nil {
return nil, err
} else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != SignonRs.String() {
return nil, errors.New("Missing closing SIGNONMSGSRSV1 xml element")
}
var messageSlices = map[string]*[]Message{
SignupRs.String(): &or.Signup,
BankRs.String(): &or.Bank,
CreditCardRs.String(): &or.CreditCard,
LoanRs.String(): &or.Loan,
InvStmtRs.String(): &or.InvStmt,
InterXferRs.String(): &or.InterXfer,
WireXferRs.String(): &or.WireXfer,
BillpayRs.String(): &or.Billpay,
EmailRs.String(): &or.Email,
SecListRs.String(): &or.SecList,
PresDirRs.String(): &or.PresDir,
PresDlvRs.String(): &or.PresDlv,
ProfRs.String(): &or.Prof,
ImageRs.String(): &or.Image,
}
for {
tok, err = nextNonWhitespaceToken(decoder)
if err != nil {
return nil, err
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
return &or, nil // found closing XML element, so we're done
} else if start, ok := tok.(xml.StartElement); ok {
slice, ok := messageSlices[start.Name.Local]
if !ok {
return nil, errors.New("Invalid message set: " + start.Name.Local)
}
if err := decodeMessageSet(decoder, start, slice, or.Version); err != nil {
return nil, err
}
} else {
return nil, errors.New("Found unexpected token")
}
}
}
// Valid returns whether the Response is valid according to the OFX spec
func (or *Response) Valid() (bool, error) {
var errs errInvalid
if ok, err := or.Signon.Valid(or.Version); !ok {
errs.AddErr(err)
}
for _, messageSet := range [][]Message{
or.Signup,
or.Bank,
or.CreditCard,
or.Loan,
or.InvStmt,
or.InterXfer,
or.WireXfer,
or.Billpay,
or.Email,
or.SecList,
or.PresDir,
or.PresDlv,
or.Prof,
or.Image,
} {
for _, message := range messageSet {
if ok, err := message.Valid(or.Version); !ok {
errs.AddErr(err)
}
}
}
err := errs.ErrOrNil()
return err == nil, err
}
// Marshal this Response into its SGML/XML representation held in a bytes.Buffer
//
// If error is non-nil, this bytes.Buffer is ready to be sent to an OFX client
func (or *Response) Marshal() (*bytes.Buffer, error) {
var b bytes.Buffer
// Write the header appropriate to our version
writeHeader(&b, or.Version, false)
encoder := xml.NewEncoder(&b)
encoder.Indent("", " ")
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
if err := encoder.EncodeToken(ofxElement); err != nil {
return nil, err
}
if ok, err := or.Signon.Valid(or.Version); !ok {
return nil, err
}
signonMsgSet := xml.StartElement{Name: xml.Name{Local: SignonRs.String()}}
if err := encoder.EncodeToken(signonMsgSet); err != nil {
return nil, err
}
if err := encoder.Encode(&or.Signon); err != nil {
return nil, err
}
if err := encoder.EncodeToken(signonMsgSet.End()); err != nil {
return nil, err
}
messageSets := []struct {
Messages []Message
Type messageType
}{
{or.Signup, SignupRs},
{or.Bank, BankRs},
{or.CreditCard, CreditCardRs},
{or.Loan, LoanRs},
{or.InvStmt, InvStmtRs},
{or.InterXfer, InterXferRs},
{or.WireXfer, WireXferRs},
{or.Billpay, BillpayRs},
{or.Email, EmailRs},
{or.SecList, SecListRs},
{or.PresDir, PresDirRs},
{or.PresDlv, PresDlvRs},
{or.Prof, ProfRs},
{or.Image, ImageRs},
}
for _, set := range messageSets {
if err := encodeMessageSet(encoder, set.Messages, set.Type, or.Version); err != nil {
return nil, err
}
}
if err := encoder.EncodeToken(ofxElement.End()); err != nil {
return nil, err
}
if err := encoder.Flush(); err != nil {
return nil, err
}
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
}

332
vendor/github.com/aclindsa/ofxgo/seclist.go generated vendored Normal file
View File

@@ -0,0 +1,332 @@
package ofxgo
import (
"errors"
"github.com/aclindsa/xml"
)
// SecurityID identifies a security by its CUSIP (for US-based FI's, others may
// use UniqueID types other than CUSIP)
type SecurityID struct {
XMLName xml.Name `xml:"SECID"`
UniqueID String `xml:"UNIQUEID"` // CUSIP for US FI's
UniqueIDType String `xml:"UNIQUEIDTYPE"` // Should always be "CUSIP" for US FI's
}
// SecurityRequest represents a request for one security. It is specified with
// a SECID aggregate, a ticker symbol, or an FI assigned identifier (but no
// more than one of them at a time)
type SecurityRequest struct {
XMLName xml.Name `xml:"SECRQ"`
// Only one of the next three should be present
SecID *SecurityID `xml:"SECID,omitempty"`
Ticker String `xml:"TICKER,omitempty"`
FiID String `xml:"FIID,omitempty"`
}
// SecListRequest represents a request for information (namely price) about one
// or more securities
type SecListRequest struct {
XMLName xml.Name `xml:"SECLISTTRNRQ"`
TrnUID UID `xml:"TRNUID"`
CltCookie String `xml:"CLTCOOKIE,omitempty"`
TAN String `xml:"TAN,omitempty"` // Transaction authorization number
// TODO `xml:"OFXEXTENSION,omitempty"`
Securities []SecurityRequest `xml:"SECLISTRQ>SECRQ,omitempty"`
}
// Name returns the name of the top-level transaction XML/SGML element
func (r *SecListRequest) Name() string {
return "SECLISTTRNRQ"
}
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *SecListRequest) Valid(version ofxVersion) (bool, error) {
if ok, err := r.TrnUID.Valid(); !ok {
return false, err
}
// TODO implement
return true, nil
}
// Type returns which message set this message belongs to (which Request
// element of type []Message it should appended to)
func (r *SecListRequest) Type() messageType {
return SecListRq
}
// SecListResponse is always empty (except for the transaction UID, status, and
// optional client cookie). Its presence signifies that the SecurityList (a
// different element from this one) immediately after this element in
// Response.SecList was been generated in response to the same SecListRequest
// this is a response to.
type SecListResponse struct {
XMLName xml.Name `xml:"SECLISTTRNRS"`
TrnUID UID `xml:"TRNUID"`
Status Status `xml:"STATUS"`
CltCookie String `xml:"CLTCOOKIE,omitempty"`
// TODO `xml:"OFXEXTENSION,omitempty"`
// SECLISTRS is always empty, so we don't parse it here. The actual securities list will be in a top-level element parallel to SECLISTTRNRS
}
// Name returns the name of the top-level transaction XML/SGML element
func (r *SecListResponse) Name() string {
return "SECLISTTRNRS"
}
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (r *SecListResponse) Valid(version ofxVersion) (bool, error) {
if ok, err := r.TrnUID.Valid(); !ok {
return false, err
}
// TODO implement
return true, nil
}
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (r *SecListResponse) Type() messageType {
return SecListRs
}
// Security is satisfied by all *Info elements providing information about
// securities for SecurityList
type Security interface {
SecurityType() string
}
// SecInfo represents the generic information about a security. It is included
// in most other *Info elements.
type SecInfo struct {
XMLName xml.Name `xml:"SECINFO"`
SecID SecurityID `xml:"SECID"`
SecName String `xml:"SECNAME"` // Full name of security
Ticker String `xml:"TICKER,omitempty"` // Ticker symbol
FiID String `xml:"FIID,omitempty"`
Rating String `xml:"RATING,omitempty"`
UnitPrice Amount `xml:"UNITPRICE,omitempty"` // Current price, as of DTASOF
DtAsOf *Date `xml:"DTASOF,omitempty"` // Date UNITPRICE was for
Currency *Currency `xml:"CURRENCY,omitempty"` // Overriding currency for UNITPRICE
Memo String `xml:"MEMO,omitempty"`
}
// DebtInfo provides information about a debt security
type DebtInfo struct {
XMLName xml.Name `xml:"DEBTINFO"`
SecInfo SecInfo `xml:"SECINFO"`
ParValue Amount `xml:"PARVALUE"`
DebtType debtType `xml:"DEBTTYPE"` // One of COUPON, ZERO (zero coupon)
DebtClass debtClass `xml:"DEBTCLASS,omitempty"` // One of TREASURY, MUNICIPAL, CORPORATE, OTHER
CouponRate Amount `xml:"COUPONRT,omitempty"` // Bond coupon rate for next closest call date
DtCoupon *Date `xml:"DTCOUPON,omitempty"` // Maturity date for next coupon
CouponFreq couponFreq `xml:"COUPONFREQ,omitempty"` // When coupons mature - one of MONTHLY, QUARTERLY, SEMIANNUAL, ANNUAL, or OTHER
CallPrice Amount `xml:"CALLPRICE,omitempty"` // Bond call price
YieldToCall Amount `xml:"YIELDTOCALL,omitempty"` // Yield to next call
DtCall *Date `xml:"DTCALL,omitempty"` // Next call date
CallType callType `xml:"CALLTYPE,omitempt"` // Type of next call. One of CALL, PUT, PREFUND, MATURITY
YieldToMat Amount `xml:"YIELDTOMAT,omitempty"` // Yield to maturity
DtMat *Date `xml:"DTMAT,omitempty"` // Debt maturity date
AssetClass assetClass `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
}
// SecurityType returns a string representation of this security's type
func (i DebtInfo) SecurityType() string {
return "DEBTINFO"
}
// AssetPortion represents the percentage of a mutual fund with the given asset
// classification
type AssetPortion struct {
XMLName xml.Name `xml:"PORTION"`
AssetClass assetClass `xml:"ASSETCLASS"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class
}
// FiAssetPortion represents the percentage of a mutual fund with the given
// FI-defined asset classification (AssetPortion should be used for all asset
// classifications defined by the assetClass enum)
type FiAssetPortion struct {
XMLName xml.Name `xml:"FIPORTION"`
FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class
}
// MFInfo provides information about a mutual fund
type MFInfo struct {
XMLName xml.Name `xml:"MFINFO"`
SecInfo SecInfo `xml:"SECINFO"`
MfType mfType `xml:"MFTYPE"` // One of OPEN, END, CLOSEEND, OTHER
Yield Amount `xml:"YIELD,omitempty"` // Current yield reported as the dividend expressed as a portion of the current stock price
DtYieldAsOf *Date `xml:"DTYIELDASOF,omitempty"` // Date YIELD is valid for
AssetClasses []AssetPortion `xml:"MFASSETCLASS>PORTION"`
FiAssetClasses []FiAssetPortion `xml:"FIMFASSETCLASS>FIPORTION"`
}
// SecurityType returns a string representation of this security's type
func (i MFInfo) SecurityType() string {
return "MFINFO"
}
// OptInfo provides information about an option
type OptInfo struct {
XMLName xml.Name `xml:"OPTINFO"`
SecInfo SecInfo `xml:"SECINFO"`
OptType optType `xml:"OPTTYPE"` // One of PUT, CALL
StrikePrice Amount `xml:"STRIKEPRICE"`
DtExpire Date `xml:"DTEXPIRE"` // Expiration date
ShPerCtrct Int `xml:"SHPERCTRCT"` // Shares per contract
SecID *SecurityID `xml:"SECID,omitempty"` // Security ID of the underlying security
AssetClass assetClass `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
}
// SecurityType returns a string representation of this security's type
func (i OptInfo) SecurityType() string {
return "OPTINFO"
}
// OtherInfo provides information about a security type not covered by the
// other *Info elements
type OtherInfo struct {
XMLName xml.Name `xml:"OTHERINFO"`
SecInfo SecInfo `xml:"SECINFO"`
TypeDesc String `xml:"TYPEDESC,omitempty"` // Description of security type
AssetClass assetClass `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
}
// SecurityType returns a string representation of this security's type
func (i OtherInfo) SecurityType() string {
return "OTHERINFO"
}
// StockInfo provides information about a security type
type StockInfo struct {
XMLName xml.Name `xml:"STOCKINFO"`
SecInfo SecInfo `xml:"SECINFO"`
StockType stockType `xml:"STOCKTYPE,omitempty"` // One of COMMON, PREFERRED, CONVERTIBLE, OTHER
Yield Amount `xml:"YIELD,omitempty"` // Current yield reported as the dividend expressed as a portion of the current stock price
DtYieldAsOf *Date `xml:"DTYIELDASOF,omitempty"` // Date YIELD is valid for
AssetClass assetClass `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
}
// SecurityType returns a string representation of this security's type
func (i StockInfo) SecurityType() string {
return "STOCKINFO"
}
// SecurityList is a container for Security objects containaing information
// about securities
type SecurityList struct {
XMLName xml.Name `xml:"SECLIST"`
Securities []Security
}
// Name returns the name of the top-level transaction XML/SGML element
func (r *SecurityList) Name() string {
return "SECLIST"
}
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (r *SecurityList) Valid(version ofxVersion) (bool, error) {
// TODO implement
return true, nil
}
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (r *SecurityList) Type() messageType {
return SecListRs
}
// UnmarshalXML handles unmarshalling a SecurityList from an SGML/XML string
func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := nextNonWhitespaceToken(d)
if err != nil {
return err
} 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
return nil
} else if startElement, ok := tok.(xml.StartElement); ok {
switch startElement.Name.Local {
case "DEBTINFO":
var info DebtInfo
if err := d.DecodeElement(&info, &startElement); err != nil {
return err
}
r.Securities = append(r.Securities, Security(info))
case "MFINFO":
var info MFInfo
if err := d.DecodeElement(&info, &startElement); err != nil {
return err
}
r.Securities = append(r.Securities, Security(info))
case "OPTINFO":
var info OptInfo
if err := d.DecodeElement(&info, &startElement); err != nil {
return err
}
r.Securities = append(r.Securities, Security(info))
case "OTHERINFO":
var info OtherInfo
if err := d.DecodeElement(&info, &startElement); err != nil {
return err
}
r.Securities = append(r.Securities, Security(info))
case "STOCKINFO":
var info StockInfo
if err := d.DecodeElement(&info, &startElement); err != nil {
return err
}
r.Securities = append(r.Securities, Security(info))
default:
return errors.New("Invalid SECLIST child tag: " + startElement.Name.Local)
}
} else {
return errors.New("Didn't find an opening element")
}
}
}
// MarshalXML handles marshalling a SecurityList to an SGML/XML string
func (r *SecurityList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
secListElement := xml.StartElement{Name: xml.Name{Local: "SECLIST"}}
if err := e.EncodeToken(secListElement); err != nil {
return err
}
for _, s := range r.Securities {
start := xml.StartElement{Name: xml.Name{Local: s.SecurityType()}}
switch sec := s.(type) {
case DebtInfo:
if err := e.EncodeElement(&sec, start); err != nil {
return err
}
case MFInfo:
if err := e.EncodeElement(&sec, start); err != nil {
return err
}
case OptInfo:
if err := e.EncodeElement(&sec, start); err != nil {
return err
}
case OtherInfo:
if err := e.EncodeElement(&sec, start); err != nil {
return err
}
case StockInfo:
if err := e.EncodeElement(&sec, start); err != nil {
return err
}
default:
return errors.New("Invalid SECLIST child type: " + sec.SecurityType())
}
}
if err := e.EncodeToken(secListElement.End()); err != nil {
return err
}
return nil
}

88
vendor/github.com/aclindsa/ofxgo/signon.go generated vendored Normal file
View File

@@ -0,0 +1,88 @@
package ofxgo
import (
"errors"
"fmt"
"github.com/aclindsa/xml"
)
// SignonRequest identifies and authenticates a user to their FI and is
// provided with every Request
type SignonRequest struct {
XMLName xml.Name `xml:"SONRQ"`
DtClient Date `xml:"DTCLIENT"` // Current time on client, overwritten in Client.Request()
UserID String `xml:"USERID"`
UserPass String `xml:"USERPASS,omitempty"`
UserKey String `xml:"USERKEY,omitempty"`
GenUserKey Boolean `xml:"GENUSERKEY,omitempty"`
Language String `xml:"LANGUAGE"` // Defaults to ENG
Org String `xml:"FI>ORG"`
Fid String `xml:"FI>FID"`
AppID String `xml:"APPID"` // Overwritten in Client.Request()
AppVer String `xml:"APPVER"` // Overwritten in Client.Request()
ClientUID UID `xml:"CLIENTUID,omitempty"`
}
// Name returns the name of the top-level transaction XML/SGML element
func (r *SignonRequest) Name() string {
return "SONRQ"
}
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *SignonRequest) Valid(version ofxVersion) (bool, error) {
if len(r.UserID) < 1 || len(r.UserID) > 32 {
return false, errors.New("SONRQ>USERID invalid length")
}
if (len(r.UserPass) == 0) == (len(r.UserKey) == 0) {
return false, errors.New("One and only one of SONRQ>USERPASS and USERKEY must be supplied")
}
if len(r.UserPass) > 32 {
return false, errors.New("SONRQ>USERPASS invalid length")
}
if len(r.UserKey) > 64 {
return false, errors.New("SONRQ>USERKEY invalid length")
}
if len(r.Language) == 0 {
r.Language = "ENG"
} else if len(r.Language) != 3 {
return false, fmt.Errorf("SONRQ>LANGUAGE invalid length: \"%s\"", r.Language)
}
if len(r.AppID) < 1 || len(r.AppID) > 5 {
return false, errors.New("SONRQ>APPID invalid length")
}
if len(r.AppVer) < 1 || len(r.AppVer) > 4 {
return false, errors.New("SONRQ>APPVER invalid length")
}
return true, nil
}
// SignonResponse is provided with every Response and indicates the success or
// failure of the SignonRequest in the corresponding Request
type SignonResponse struct {
XMLName xml.Name `xml:"SONRS"`
Status Status `xml:"STATUS"`
DtServer Date `xml:"DTSERVER"`
UserKey String `xml:"USERKEY,omitempty"`
TsKeyExpire *Date `xml:"TSKEYEXPIRE,omitempty"`
Language String `xml:"LANGUAGE"`
DtProfUp *Date `xml:"DTPROFUP,omitempty"`
DtAcctUp *Date `xml:"DTACCTUP,omitempty"`
Org String `xml:"FI>ORG"`
Fid String `xml:"FI>FID"`
SessCookie String `xml:"SESSCOOKIE,omitempty"`
AccessKey String `xml:"ACCESSKEY,omitempty"`
}
// Name returns the name of the top-level transaction XML/SGML element
func (r *SignonResponse) Name() string {
return "SONRS"
}
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (r *SignonResponse) Valid(version ofxVersion) (bool, error) {
if len(r.Language) != 3 {
return false, fmt.Errorf("SONRS>LANGUAGE invalid length: \"%s\"", r.Language)
}
return r.Status.Valid()
}

166
vendor/github.com/aclindsa/ofxgo/signup.go generated vendored Normal file
View File

@@ -0,0 +1,166 @@
package ofxgo
import (
"fmt"
"github.com/aclindsa/xml"
)
// AcctInfoRequest represents a request for the server to provide information
// for all of the user's available accounts at this FI
type AcctInfoRequest struct {
XMLName xml.Name `xml:"ACCTINFOTRNRQ"`
TrnUID UID `xml:"TRNUID"`
CltCookie String `xml:"CLTCOOKIE,omitempty"`
TAN String `xml:"TAN,omitempty"` // Transaction authorization number
// TODO `xml:"OFXEXTENSION,omitempty"`
DtAcctUp Date `xml:"ACCTINFORQ>DTACCTUP"`
}
// Name returns the name of the top-level transaction XML/SGML element
func (r *AcctInfoRequest) Name() string {
return "ACCTINFOTRNRQ"
}
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *AcctInfoRequest) Valid(version ofxVersion) (bool, error) {
if ok, err := r.TrnUID.Valid(); !ok {
return false, err
}
// TODO implement
return true, nil
}
// Type returns which message set this message belongs to (which Request
// element of type []Message it should appended to)
func (r *AcctInfoRequest) Type() messageType {
return SignupRq
}
// HolderInfo contains the information a FI has about an account-holder
type HolderInfo struct {
XMLName xml.Name
FirstName String `xml:"FIRSTNAME"`
MiddleName String `xml:"MIDDLENAME,omitempty"`
LastName String `xml:"LASTNAME"`
Addr1 String `xml:"ADDR1"`
Addr2 String `xml:"ADDR2,omitempty"`
Addr3 String `xml:"ADDR3,omitempty"`
City String `xml:"CITY"`
State String `xml:"STATE"`
PostalCode String `xml:"POSTALCODE"`
Country String `xml:"COUNTRY,omitempty"`
DayPhone String `xml:"DAYPHONE,omitempty"`
EvePhone String `xml:"EVEPHONE,omitempty"`
Email String `xml:"EMAIL,omitempty"`
HolderType holderType `xml:"HOLDERTYPE,omitempty"` // One of INDIVIDUAL, JOINT, CUSTODIAL, TRUST, OTHER
}
// BankAcctInfo contains information about a bank account, including how to
// access it (BankAcct), and whether it supports downloading transactions
// (SupTxDl).
type BankAcctInfo struct {
XMLName xml.Name `xml:"BANKACCTINFO"`
BankAcctFrom BankAcct `xml:"BANKACCTFROM"`
SupTxDl Boolean `xml:"SUPTXDL"` // Supports downloading transactions (as opposed to balance only)
XferSrc Boolean `xml:"XFERSRC"` // Enabled as source for intra/interbank transfer
XferDest Boolean `xml:"XFERDEST"` // Enabled as destination for intra/interbank transfer
MaturityDate Date `xml:"MATURITYDATE,omitempty"` // Maturity date for CD, if CD
MaturityAmt Amount `xml:"MATURITYAMOUNT,omitempty"` // Maturity amount for CD, if CD
MinBalReq Amount `xml:"MINBALREQ,omitempty"` // Minimum balance required to avoid service fees
AcctClassification acctClassification `xml:"ACCTCLASSIFICATION,omitempty"` // One of PERSONAL, BUSINESS, CORPORATE, OTHER
OverdraftLimit Amount `xml:"OVERDRAFTLIMIT,omitempty"`
SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE
}
// String makes pointers to BankAcctInfo structs print nicely
func (bai *BankAcctInfo) String() string {
return fmt.Sprintf("%+v", *bai)
}
// CCAcctInfo contains information about a credit card account, including how
// to access it (CCAcct), and whether it supports downloading transactions
// (SupTxDl).
type CCAcctInfo struct {
XMLName xml.Name `xml:"CCACCTINFO"`
CCAcctFrom CCAcct `xml:"CCACCTFROM"`
SupTxDl Boolean `xml:"SUPTXDL"` // Supports downloading transactions (as opposed to balance only)
XferSrc Boolean `xml:"XFERSRC"` // Enabled as source for intra/interbank transfer
XferDest Boolean `xml:"XFERDEST"` // Enabled as destination for intra/interbank transfer
AcctClassification acctClassification `xml:"ACCTCLASSIFICATION,omitempty"` // One of PERSONAL, BUSINESS, CORPORATE, OTHER
SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE
}
// String makes pointers to CCAcctInfo structs print nicely
func (ci *CCAcctInfo) String() string {
return fmt.Sprintf("%+v", *ci)
}
// InvAcctInfo contains information about an investment account, including how
// to access it (InvAcct), and whether it supports downloading transactions
// (SupTxDl).
type InvAcctInfo struct {
XMLName xml.Name `xml:"INVACCTINFO"`
InvAcctFrom InvAcct `xml:"INVACCTFROM"`
UsProductType usProductType `xml:"USPRODUCTTYPE"` // One of 401K, 403B, IRA, KEOGH, OTHER, SARSEP, SIMPLE, NORMAL, TDA, TRUST, UGMA
Checking Boolean `xml:"CHECKING"` // Has check-writing privileges
SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE
InvAcctType holderType `xml:"INVACCTTYPE,omitempty"` // One of INDIVIDUAL, JOINT, TRUST, CORPORATE
OptionLevel String `xml:"OPTIONLEVEL,omitempty"` // Text desribing option trading privileges
}
// String makes pointers to InvAcctInfo structs print nicely
func (iai *InvAcctInfo) String() string {
return fmt.Sprintf("%+v", *iai)
}
// AcctInfo represents generic account information. It should contain one (and
// only one) *AcctInfo element corresponding to the tyep of account it
// represents.
type AcctInfo struct {
XMLName xml.Name `xml:"ACCTINFO"`
Name String `xml:"NAME,omitempty"`
Desc String `xml:"DESC,omitempty"`
Phone String `xml:"PHONE,omitempty"`
PrimaryHolder HolderInfo `xml:"HOLDERINFO>PRIMARYHOLDER,omitempty"`
SecondaryHolder HolderInfo `xml:"HOLDERINFO>SECONDARYHOLDER,omitempty"`
// Only one of the rest of the fields will be valid for any given AcctInfo
BankAcctInfo *BankAcctInfo `xml:"BANKACCTINFO,omitempty"`
CCAcctInfo *CCAcctInfo `xml:"CCACCTINFO,omitempty"`
InvAcctInfo *InvAcctInfo `xml:"INVACCTINFO,omitempty"`
// TODO LOANACCTINFO
// TODO BPACCTINFO?
}
// AcctInfoResponse contains the information about all a user's accounts
// accessible from this FI
type AcctInfoResponse struct {
XMLName xml.Name `xml:"ACCTINFOTRNRS"`
TrnUID UID `xml:"TRNUID"`
Status Status `xml:"STATUS"`
CltCookie String `xml:"CLTCOOKIE,omitempty"`
// TODO `xml:"OFXEXTENSION,omitempty"`
DtAcctUp Date `xml:"ACCTINFORS>DTACCTUP"`
AcctInfo []AcctInfo `xml:"ACCTINFORS>ACCTINFO,omitempty"`
}
// Name returns the name of the top-level transaction XML/SGML element
func (air *AcctInfoResponse) Name() string {
return "ACCTINFOTRNRS"
}
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (air *AcctInfoResponse) Valid(version ofxVersion) (bool, error) {
if ok, err := air.TrnUID.Valid(); !ok {
return false, err
}
//TODO implement
return true, nil
}
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (air *AcctInfoResponse) Type() messageType {
return SignupRs
}

386
vendor/github.com/aclindsa/ofxgo/types.go generated vendored Normal file
View File

@@ -0,0 +1,386 @@
package ofxgo
import (
"crypto/rand"
"errors"
"fmt"
"github.com/aclindsa/xml"
"golang.org/x/text/currency"
"math/big"
"regexp"
"strconv"
"strings"
"time"
)
// Int provides helper methods to unmarshal int64 values from SGML/XML
type Int int64
// UnmarshalXML handles unmarshalling an Int from an SGML/XML string. Leading
// and trailing whitespace is ignored.
func (i *Int) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
value = strings.TrimSpace(value)
i2, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
*i = Int(i2)
return nil
}
// Equal returns true if the two Ints are equal in value
func (i Int) Equal(o Int) bool {
return i == o
}
// Amount represents non-integer values (or at least values for fields that may
// not necessarily be integers)
type Amount struct {
big.Rat
}
// UnmarshalXML handles unmarshalling an Amount from an SGML/XML string.
// Leading and trailing whitespace is ignored.
func (a *Amount) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
value = strings.TrimSpace(value)
// The OFX spec allows the start of the fractional amount to be delineated
// by a comma, so fix that up before attempting to parse it into big.Rat
value = strings.Replace(value, ",", ".", 1)
if _, ok := a.SetString(value); !ok {
return errors.New("Failed to parse OFX amount")
}
return nil
}
// String prints a string representation of an Amount
func (a Amount) String() string {
return strings.TrimRight(strings.TrimRight(a.FloatString(100), "0"), ".")
}
// MarshalXML marshals an Amount to SGML/XML
func (a *Amount) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(a.String(), start)
}
// Equal returns true if two Amounts are equal in value
func (a Amount) Equal(o Amount) bool {
return (&a).Cmp(&o.Rat) == 0
}
// Date represents OFX date/time values
type Date struct {
time.Time
}
var ofxDateFormats = []string{
"20060102150405.000",
"20060102150405",
"200601021504",
"2006010215",
"20060102",
}
var ofxDateZoneRegex = regexp.MustCompile(`^([+-]?[0-9]+)(\.([0-9]{2}))?(:([A-Z]+))?$`)
// UnmarshalXML handles unmarshalling a Date from an SGML/XML string. It
// attempts to unmarshal the valid date formats in order of decreasing length
// and defaults to GMT if a time zone is not provided, as per the OFX spec.
// Leading and trailing whitespace is ignored.
func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value, zone, zoneFormat string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
value = strings.SplitN(value, "]", 2)[0]
value = strings.TrimSpace(value)
// Split the time zone off, if any
split := strings.SplitN(value, "[", 2)
if len(split) == 2 {
value = split[0]
zoneFormat = " -0700"
zone = strings.TrimRight(split[1], "]")
matches := ofxDateZoneRegex.FindStringSubmatch(zone)
if matches == nil {
return errors.New("Invalid OFX Date timezone format: " + zone)
}
var err error
var zonehours, zoneminutes int
zonehours, err = strconv.Atoi(matches[1])
if err != nil {
return err
}
if len(matches[3]) > 0 {
zoneminutes, err = strconv.Atoi(matches[3])
if err != nil {
return err
}
zoneminutes = zoneminutes * 60 / 100
}
zone = fmt.Sprintf(" %+03d%02d", zonehours, zoneminutes)
// Get the time zone name if it's there, default to GMT if the offset
// is 0 and a name isn't supplied
if len(matches[5]) > 0 {
zone = zone + " " + matches[5]
zoneFormat = zoneFormat + " MST"
} else if zonehours == 0 && zoneminutes == 0 {
zone = zone + " GMT"
zoneFormat = zoneFormat + " MST"
}
} else {
// Default to GMT if no time zone was specified
zone = " +0000 GMT"
zoneFormat = " -0700 MST"
}
// Try all the date formats, from longest to shortest
for _, format := range ofxDateFormats {
t, err := time.Parse(format+zoneFormat, value+zone)
if err == nil {
od.Time = t
return nil
}
}
return errors.New("OFX: Couldn't parse date:" + value)
}
// String returns a string representation of the Date abiding by the OFX spec
func (od Date) String() string {
format := od.Format(ofxDateFormats[0])
zonename, zoneoffset := od.Zone()
if zoneoffset < 0 {
format += "[" + fmt.Sprintf("%+d", zoneoffset/3600)
} else {
format += "[" + fmt.Sprintf("%d", zoneoffset/3600)
}
fractionaloffset := (zoneoffset % 3600) / 36
if fractionaloffset > 0 {
format += "." + fmt.Sprintf("%02d", fractionaloffset)
} else if fractionaloffset < 0 {
format += "." + fmt.Sprintf("%02d", -fractionaloffset)
}
if len(zonename) > 0 {
return format + ":" + zonename + "]"
}
return format + "]"
}
// MarshalXML marshals a Date to XML
func (od *Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(od.String(), start)
}
// Equal returns true if the two Dates represent the same time (time zones are
// accounted for when comparing, but are not required to match)
func (od Date) Equal(o Date) bool {
return od.Time.Equal(o.Time)
}
// NewDate returns a new Date object with the provided date, time, and timezone
func NewDate(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) *Date {
return &Date{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)}
}
var gmt = time.FixedZone("GMT", 0)
// NewDateGMT returns a new Date object with the provided date and time in the
// GMT timezone
func NewDateGMT(year int, month time.Month, day, hour, min, sec, nsec int) *Date {
return &Date{Time: time.Date(year, month, day, hour, min, sec, nsec, gmt)}
}
// String provides helper methods to unmarshal OFX string values from SGML/XML
type String string
// UnmarshalXML handles unmarshalling a String from an SGML/XML string. Leading
// and trailing whitespace is ignored.
func (os *String) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
*os = String(strings.TrimSpace(value))
return nil
}
// String returns the string
func (os *String) String() string {
return string(*os)
}
// Equal returns true if the two Strings are equal in value
func (os String) Equal(o String) bool {
return os == o
}
// Boolean provides helper methods to unmarshal bool values from OFX SGML/XML
type Boolean bool
// UnmarshalXML handles unmarshalling a Boolean from an SGML/XML string.
// Leading and trailing whitespace is ignored.
func (ob *Boolean) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
tmpob := strings.TrimSpace(value)
switch tmpob {
case "Y":
*ob = Boolean(true)
case "N":
*ob = Boolean(false)
default:
return errors.New("Invalid OFX Boolean")
}
return nil
}
// MarshalXML marshals a Boolean to XML
func (ob *Boolean) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if *ob {
return e.EncodeElement("Y", start)
}
return e.EncodeElement("N", start)
}
// String returns a string representation of a Boolean value
func (ob *Boolean) String() string {
return fmt.Sprintf("%v", *ob)
}
// Equal returns true if the two Booleans are the same
func (ob Boolean) Equal(o Boolean) bool {
return ob == o
}
// UID represents an UID according to the OFX spec
type UID string
// UnmarshalXML handles unmarshalling an UID from an SGML/XML string. Leading
// and trailing whitespace is ignored.
func (ou *UID) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
*ou = UID(strings.TrimSpace(value))
return nil
}
// RecommendedFormat returns true iff this UID meets the OFX specification's
// recommendation that UIDs follow the standard UUID 36-character format
func (ou UID) RecommendedFormat() (bool, error) {
if len(ou) != 36 {
return false, errors.New("UID not 36 characters long")
}
if ou[8] != '-' || ou[13] != '-' || ou[18] != '-' || ou[23] != '-' {
return false, errors.New("UID missing hyphens at the appropriate places")
}
return true, nil
}
// Valid returns true, nil if the UID is valid. This is less strict than
// RecommendedFormat, and will always return true, nil if it does.
func (ou UID) Valid() (bool, error) {
if len(ou) == 0 || len(ou) > 36 {
return false, errors.New("UID invalid length")
}
return true, nil
}
// Equal returns true if the two UIDs are the same
func (ou UID) Equal(o UID) bool {
return ou == o
}
// RandomUID creates a new randomly-generated UID
func RandomUID() (*UID, error) {
uidbytes := make([]byte, 16)
n, err := rand.Read(uidbytes[:])
if err != nil {
return nil, err
}
if n != 16 {
return nil, errors.New("RandomUID failed to read 16 random bytes")
}
uid := UID(fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", uidbytes[:4], uidbytes[4:6], uidbytes[6:8], uidbytes[8:10], uidbytes[10:]))
return &uid, nil
}
// CurrSymbol represents an ISO-4217 currency
type CurrSymbol struct {
currency.Unit
}
// UnmarshalXML handles unmarshalling a CurrSymbol from an SGML/XML string.
// Leading and trailing whitespace is ignored.
func (c *CurrSymbol) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
value = strings.TrimSpace(value)
unit, err := currency.ParseISO(value)
if err != nil {
return errors.New("Error parsing CurrSymbol:" + err.Error())
}
c.Unit = unit
return nil
}
// MarshalXML marshals a CurrSymbol to SGML/XML
func (c *CurrSymbol) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(c.String(), start)
}
// Equal returns true if the two Currencies are the same
func (c CurrSymbol) Equal(o CurrSymbol) bool {
return c.String() == o.String()
}
// Valid returns true, nil if the CurrSymbol is valid.
func (c CurrSymbol) Valid() (bool, error) {
if c.String() == "XXX" {
return false, fmt.Errorf("Invalid CurrSymbol: %s", c.Unit)
}
return true, nil
}
// NewCurrSymbol returns a new CurrSymbol given a three-letter ISO-4217
// currency symbol as a string
func NewCurrSymbol(s string) (*CurrSymbol, error) {
unit, err := currency.ParseISO(s)
if err != nil {
return nil, errors.New("Error parsing string to create new CurrSymbol:" + err.Error())
}
return &CurrSymbol{unit}, nil
}

25
vendor/github.com/aclindsa/ofxgo/util.go generated vendored Normal file
View File

@@ -0,0 +1,25 @@
package ofxgo
import (
"bytes"
"github.com/aclindsa/xml"
)
// Returns the next available Token from the xml.Decoder that is not CharData
// made up entirely of whitespace. This is useful to skip whitespace when
// manually unmarshaling XML.
func nextNonWhitespaceToken(decoder *xml.Decoder) (xml.Token, error) {
for {
tok, err := decoder.Token()
if err != nil {
return nil, err
} else if chars, ok := tok.(xml.CharData); ok {
strippedBytes := bytes.TrimSpace(chars)
if len(strippedBytes) != 0 {
return tok, nil
}
} else {
return tok, nil
}
}
}

83
vendor/github.com/aclindsa/ofxgo/vanguard_client.go generated vendored Normal file
View File

@@ -0,0 +1,83 @@
package ofxgo
import (
"errors"
"io"
"net/http"
"strings"
)
// VanguardClient provides a Client implementation which handles Vanguard's
// cookie-passing requirements. VanguardClient uses default, non-zero settings,
// if its fields are not initialized.
type VanguardClient struct {
*BasicClient
}
// NewVanguardClient returns a Client interface configured to handle Vanguard's
// brand of idiosyncrasy
func NewVanguardClient(bc *BasicClient) Client {
return &VanguardClient{bc}
}
// rawRequestCookies is RawRequest with the added feature of sending cookies
func rawRequestCookies(URL string, r io.Reader, cookies []*http.Cookie) (*http.Response, error) {
if !strings.HasPrefix(URL, "https://") {
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
}
request, err := http.NewRequest("POST", URL, r)
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "application/x-ofx")
for _, cookie := range cookies {
request.AddCookie(cookie)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return nil, err
}
if response.StatusCode != 200 {
return nil, errors.New("OFXQuery request status: " + response.Status)
}
return response, nil
}
// RequestNoParse marshals a Request to XML, makes an HTTP request, and returns
// the raw HTTP response
func (c *VanguardClient) RequestNoParse(r *Request) (*http.Response, error) {
r.SetClientFields(c)
b, err := r.Marshal()
if err != nil {
return nil, err
}
response, err := c.RawRequest(r.URL, b)
// Some financial institutions (cough, Vanguard, cough), require a cookie
// to be set on the http request, or they return empty responses.
// Fortunately, the initial response contains the cookie we need, so if we
// detect an empty response with cookies set that didn't have any errors,
// re-try the request while sending their cookies back to them.
if response != nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 {
b, err = r.Marshal()
if err != nil {
return nil, err
}
return rawRequestCookies(r.URL, b, response.Cookies())
}
return response, err
}
// Request marshals a Request to XML, makes an HTTP request, and then
// unmarshals the response into a Response object.
func (c *VanguardClient) Request(r *Request) (*Response, error) {
return clientRequest(c, r)
}

1107
vendor/github.com/aclindsa/xml/AUTHORS generated vendored Normal file

File diff suppressed because it is too large Load Diff

1445
vendor/github.com/aclindsa/xml/CONTRIBUTORS generated vendored Normal file

File diff suppressed because it is too large Load Diff

27
vendor/github.com/aclindsa/xml/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

22
vendor/github.com/aclindsa/xml/PATENTS generated vendored Normal file
View File

@@ -0,0 +1,22 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.

1075
vendor/github.com/aclindsa/xml/marshal.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

749
vendor/github.com/aclindsa/xml/read.go generated vendored Normal file
View File

@@ -0,0 +1,749 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xml
import (
"bytes"
"encoding"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
)
// BUG(rsc): Mapping between XML elements and data structures is inherently flawed:
// an XML element is an order-dependent collection of anonymous
// values, while a data structure is an order-independent collection
// of named values.
// See package json for a textual representation more suitable
// to data structures.
// Unmarshal parses the XML-encoded data and stores the result in
// the value pointed to by v, which must be an arbitrary struct,
// slice, or string. Well-formed data that does not fit into v is
// discarded.
//
// Because Unmarshal uses the reflect package, it can only assign
// to exported (upper case) fields. Unmarshal uses a case-sensitive
// comparison to match XML element names to tag values and struct
// field names.
//
// Unmarshal maps an XML element to a struct using the following rules.
// In the rules, the tag of a field refers to the value associated with the
// key 'xml' in the struct field's tag (see the example above).
//
// * If the struct has a field of type []byte or string with tag
// ",innerxml", Unmarshal accumulates the raw XML nested inside the
// element in that field. The rest of the rules still apply.
//
// * If the struct has a field named XMLName of type Name,
// Unmarshal records the element name in that field.
//
// * If the XMLName field has an associated tag of the form
// "name" or "namespace-URL name", the XML element must have
// the given name (and, optionally, name space) or else Unmarshal
// returns an error.
//
// * If the XML element has an attribute whose name matches a
// struct field name with an associated tag containing ",attr" or
// the explicit name in a struct field tag of the form "name,attr",
// Unmarshal records the attribute value in that field.
//
// * If the XML element has an attribute not handled by the previous
// rule and the struct has a field with an associated tag containing
// ",any,attr", Unmarshal records the attribute value in the first
// such field.
//
// * If the XML element contains character data, that data is
// accumulated in the first struct field that has tag ",chardata".
// The struct field may have type []byte or string.
// If there is no such field, the character data is discarded.
//
// * If the XML element contains comments, they are accumulated in
// the first struct field that has tag ",comment". The struct
// field may have type []byte or string. If there is no such
// field, the comments are discarded.
//
// * If the XML element contains a sub-element whose name matches
// the prefix of a tag formatted as "a" or "a>b>c", unmarshal
// will descend into the XML structure looking for elements with the
// given names, and will map the innermost elements to that struct
// field. A tag starting with ">" is equivalent to one starting
// with the field name followed by ">".
//
// * If the XML element contains a sub-element whose name matches
// a struct field's XMLName tag and the struct field has no
// explicit name tag as per the previous rule, unmarshal maps
// the sub-element to that struct field.
//
// * If the XML element contains a sub-element whose name matches a
// field without any mode flags (",attr", ",chardata", etc), Unmarshal
// maps the sub-element to that struct field.
//
// * If the XML element contains a sub-element that hasn't matched any
// of the above rules and the struct has a field with tag ",any",
// unmarshal maps the sub-element to that struct field.
//
// * An anonymous struct field is handled as if the fields of its
// value were part of the outer struct.
//
// * A struct field with tag "-" is never unmarshaled into.
//
// Unmarshal maps an XML element to a string or []byte by saving the
// concatenation of that element's character data in the string or
// []byte. The saved []byte is never nil.
//
// Unmarshal maps an attribute value to a string or []byte by saving
// the value in the string or slice.
//
// Unmarshal maps an attribute value to an Attr by saving the attribute,
// including its name, in the Attr.
//
// Unmarshal maps an XML element or attribute value to a slice by
// extending the length of the slice and mapping the element or attribute
// to the newly created value.
//
// Unmarshal maps an XML element or attribute value to a bool by
// setting it to the boolean value represented by the string.
//
// Unmarshal maps an XML element or attribute value to an integer or
// floating-point field by setting the field to the result of
// interpreting the string value in decimal. There is no check for
// overflow.
//
// Unmarshal maps an XML element to a Name by recording the element
// name.
//
// Unmarshal maps an XML element to a pointer by setting the pointer
// to a freshly allocated value and then mapping the element to that value.
//
// A missing element or empty attribute value will be unmarshaled as a zero value.
// If the field is a slice, a zero value will be appended to the field. Otherwise, the
// field will be set to its zero value.
func Unmarshal(data []byte, v interface{}) error {
return NewDecoder(bytes.NewReader(data)).Decode(v)
}
// Decode works like Unmarshal, except it reads the decoder
// stream to find the start element.
func (d *Decoder) Decode(v interface{}) error {
return d.DecodeElement(v, nil)
}
// DecodeElement works like Unmarshal except that it takes
// a pointer to the start XML element to decode into v.
// It is useful when a client reads some raw XML tokens itself
// but also wants to defer to Unmarshal for some elements.
func (d *Decoder) DecodeElement(v interface{}, start *StartElement) error {
val := reflect.ValueOf(v)
if val.Kind() != reflect.Ptr {
return errors.New("non-pointer passed to Unmarshal")
}
return d.unmarshal(val.Elem(), start)
}
// An UnmarshalError represents an error in the unmarshaling process.
type UnmarshalError string
func (e UnmarshalError) Error() string { return string(e) }
// Unmarshaler is the interface implemented by objects that can unmarshal
// an XML element description of themselves.
//
// UnmarshalXML decodes a single XML element
// beginning with the given start element.
// If it returns an error, the outer call to Unmarshal stops and
// returns that error.
// UnmarshalXML must consume exactly one XML element.
// One common implementation strategy is to unmarshal into
// a separate value with a layout matching the expected XML
// using d.DecodeElement, and then to copy the data from
// that value into the receiver.
// Another common strategy is to use d.Token to process the
// XML object one token at a time.
// UnmarshalXML may not use d.RawToken.
type Unmarshaler interface {
UnmarshalXML(d *Decoder, start StartElement) error
}
// UnmarshalerAttr is the interface implemented by objects that can unmarshal
// an XML attribute description of themselves.
//
// UnmarshalXMLAttr decodes a single XML attribute.
// If it returns an error, the outer call to Unmarshal stops and
// returns that error.
// UnmarshalXMLAttr is used only for struct fields with the
// "attr" option in the field tag.
type UnmarshalerAttr interface {
UnmarshalXMLAttr(attr Attr) error
}
// receiverType returns the receiver type to use in an expression like "%s.MethodName".
func receiverType(val interface{}) string {
t := reflect.TypeOf(val)
if t.Name() != "" {
return t.String()
}
return "(" + t.String() + ")"
}
// unmarshalInterface unmarshals a single XML element into val.
// start is the opening tag of the element.
func (d *Decoder) unmarshalInterface(val Unmarshaler, start *StartElement) error {
// Record that decoder must stop at end tag corresponding to start.
d.pushEOF()
d.unmarshalDepth++
err := val.UnmarshalXML(d, *start)
d.unmarshalDepth--
if err != nil {
d.popEOF()
return err
}
if !d.popEOF() {
return fmt.Errorf("xml: %s.UnmarshalXML did not consume entire <%s> element", receiverType(val), start.Name.Local)
}
return nil
}
// unmarshalTextInterface unmarshals a single XML element into val.
// The chardata contained in the element (but not its children)
// is passed to the text unmarshaler.
func (d *Decoder) unmarshalTextInterface(val encoding.TextUnmarshaler) error {
var buf []byte
depth := 1
for depth > 0 {
t, err := d.Token()
if err != nil {
return err
}
switch t := t.(type) {
case CharData:
if depth == 1 {
buf = append(buf, t...)
}
case StartElement:
depth++
case EndElement:
depth--
}
}
return val.UnmarshalText(buf)
}
// unmarshalAttr unmarshals a single XML attribute into val.
func (d *Decoder) unmarshalAttr(val reflect.Value, attr Attr) error {
if val.Kind() == reflect.Ptr {
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
val = val.Elem()
}
if val.CanInterface() && val.Type().Implements(unmarshalerAttrType) {
// This is an unmarshaler with a non-pointer receiver,
// so it's likely to be incorrect, but we do what we're told.
return val.Interface().(UnmarshalerAttr).UnmarshalXMLAttr(attr)
}
if val.CanAddr() {
pv := val.Addr()
if pv.CanInterface() && pv.Type().Implements(unmarshalerAttrType) {
return pv.Interface().(UnmarshalerAttr).UnmarshalXMLAttr(attr)
}
}
// Not an UnmarshalerAttr; try encoding.TextUnmarshaler.
if val.CanInterface() && val.Type().Implements(textUnmarshalerType) {
// This is an unmarshaler with a non-pointer receiver,
// so it's likely to be incorrect, but we do what we're told.
return val.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(attr.Value))
}
if val.CanAddr() {
pv := val.Addr()
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {
return pv.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(attr.Value))
}
}
if val.Type().Kind() == reflect.Slice && val.Type().Elem().Kind() != reflect.Uint8 {
// Slice of element values.
// Grow slice.
n := val.Len()
val.Set(reflect.Append(val, reflect.Zero(val.Type().Elem())))
// Recur to read element into slice.
if err := d.unmarshalAttr(val.Index(n), attr); err != nil {
val.SetLen(n)
return err
}
return nil
}
if val.Type() == attrType {
val.Set(reflect.ValueOf(attr))
return nil
}
return copyValue(val, []byte(attr.Value))
}
var (
attrType = reflect.TypeOf(Attr{})
unmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
unmarshalerAttrType = reflect.TypeOf((*UnmarshalerAttr)(nil)).Elem()
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
)
// Unmarshal a single XML element into val.
func (d *Decoder) unmarshal(val reflect.Value, start *StartElement) error {
// Find start element if we need it.
if start == nil {
for {
tok, err := d.Token()
if err != nil {
return err
}
if t, ok := tok.(StartElement); ok {
start = &t
break
}
}
}
// Load value from interface, but only if the result will be
// usefully addressable.
if val.Kind() == reflect.Interface && !val.IsNil() {
e := val.Elem()
if e.Kind() == reflect.Ptr && !e.IsNil() {
val = e
}
}
if val.Kind() == reflect.Ptr {
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
val = val.Elem()
}
if val.CanInterface() && val.Type().Implements(unmarshalerType) {
// This is an unmarshaler with a non-pointer receiver,
// so it's likely to be incorrect, but we do what we're told.
return d.unmarshalInterface(val.Interface().(Unmarshaler), start)
}
if val.CanAddr() {
pv := val.Addr()
if pv.CanInterface() && pv.Type().Implements(unmarshalerType) {
return d.unmarshalInterface(pv.Interface().(Unmarshaler), start)
}
}
if val.CanInterface() && val.Type().Implements(textUnmarshalerType) {
return d.unmarshalTextInterface(val.Interface().(encoding.TextUnmarshaler))
}
if val.CanAddr() {
pv := val.Addr()
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {
return d.unmarshalTextInterface(pv.Interface().(encoding.TextUnmarshaler))
}
}
var (
data []byte
saveData reflect.Value
comment []byte
saveComment reflect.Value
saveXML reflect.Value
saveXMLIndex int
saveXMLData []byte
saveAny reflect.Value
sv reflect.Value
tinfo *typeInfo
err error
)
switch v := val; v.Kind() {
default:
return errors.New("unknown type " + v.Type().String())
case reflect.Interface:
// TODO: For now, simply ignore the field. In the near
// future we may choose to unmarshal the start
// element on it, if not nil.
return d.Skip()
case reflect.Slice:
typ := v.Type()
if typ.Elem().Kind() == reflect.Uint8 {
// []byte
saveData = v
break
}
// Slice of element values.
// Grow slice.
n := v.Len()
v.Set(reflect.Append(val, reflect.Zero(v.Type().Elem())))
// Recur to read element into slice.
if err := d.unmarshal(v.Index(n), start); err != nil {
v.SetLen(n)
return err
}
return nil
case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.String:
saveData = v
case reflect.Struct:
typ := v.Type()
if typ == nameType {
v.Set(reflect.ValueOf(start.Name))
break
}
sv = v
tinfo, err = getTypeInfo(typ)
if err != nil {
return err
}
// Validate and assign element name.
if tinfo.xmlname != nil {
finfo := tinfo.xmlname
if finfo.name != "" && finfo.name != start.Name.Local {
return UnmarshalError("expected element type <" + finfo.name + "> but have <" + start.Name.Local + ">")
}
if finfo.xmlns != "" && finfo.xmlns != start.Name.Space {
e := "expected element <" + finfo.name + "> in name space " + finfo.xmlns + " but have "
if start.Name.Space == "" {
e += "no name space"
} else {
e += start.Name.Space
}
return UnmarshalError(e)
}
fv := finfo.value(sv)
if _, ok := fv.Interface().(Name); ok {
fv.Set(reflect.ValueOf(start.Name))
}
}
// Assign attributes.
for _, a := range start.Attr {
handled := false
any := -1
for i := range tinfo.fields {
finfo := &tinfo.fields[i]
switch finfo.flags & fMode {
case fAttr:
strv := finfo.value(sv)
if a.Name.Local == finfo.name && (finfo.xmlns == "" || finfo.xmlns == a.Name.Space) {
if err := d.unmarshalAttr(strv, a); err != nil {
return err
}
handled = true
}
case fAny | fAttr:
if any == -1 {
any = i
}
}
}
if !handled && any >= 0 {
finfo := &tinfo.fields[any]
strv := finfo.value(sv)
if err := d.unmarshalAttr(strv, a); err != nil {
return err
}
}
}
// Determine whether we need to save character data or comments.
for i := range tinfo.fields {
finfo := &tinfo.fields[i]
switch finfo.flags & fMode {
case fCDATA, fCharData:
if !saveData.IsValid() {
saveData = finfo.value(sv)
}
case fComment:
if !saveComment.IsValid() {
saveComment = finfo.value(sv)
}
case fAny, fAny | fElement:
if !saveAny.IsValid() {
saveAny = finfo.value(sv)
}
case fInnerXml:
if !saveXML.IsValid() {
saveXML = finfo.value(sv)
if d.saved == nil {
saveXMLIndex = 0
d.saved = new(bytes.Buffer)
} else {
saveXMLIndex = d.savedOffset()
}
}
}
}
}
// Find end element.
// Process sub-elements along the way.
Loop:
for {
var savedOffset int
if saveXML.IsValid() {
savedOffset = d.savedOffset()
}
tok, err := d.Token()
if err != nil {
return err
}
switch t := tok.(type) {
case StartElement:
consumed := false
if sv.IsValid() {
consumed, err = d.unmarshalPath(tinfo, sv, nil, &t)
if err != nil {
return err
}
if !consumed && saveAny.IsValid() {
consumed = true
if err := d.unmarshal(saveAny, &t); err != nil {
return err
}
}
}
if !consumed {
if err := d.Skip(); err != nil {
return err
}
}
case EndElement:
if saveXML.IsValid() {
saveXMLData = d.saved.Bytes()[saveXMLIndex:savedOffset]
if saveXMLIndex == 0 {
d.saved = nil
}
}
break Loop
case CharData:
if saveData.IsValid() {
data = append(data, t...)
}
case Comment:
if saveComment.IsValid() {
comment = append(comment, t...)
}
}
}
if saveData.IsValid() && saveData.CanInterface() && saveData.Type().Implements(textUnmarshalerType) {
if err := saveData.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil {
return err
}
saveData = reflect.Value{}
}
if saveData.IsValid() && saveData.CanAddr() {
pv := saveData.Addr()
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {
if err := pv.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil {
return err
}
saveData = reflect.Value{}
}
}
if err := copyValue(saveData, data); err != nil {
return err
}
switch t := saveComment; t.Kind() {
case reflect.String:
t.SetString(string(comment))
case reflect.Slice:
t.Set(reflect.ValueOf(comment))
}
switch t := saveXML; t.Kind() {
case reflect.String:
t.SetString(string(saveXMLData))
case reflect.Slice:
if t.Type().Elem().Kind() == reflect.Uint8 {
t.Set(reflect.ValueOf(saveXMLData))
}
}
return nil
}
func copyValue(dst reflect.Value, src []byte) (err error) {
dst0 := dst
if dst.Kind() == reflect.Ptr {
if dst.IsNil() {
dst.Set(reflect.New(dst.Type().Elem()))
}
dst = dst.Elem()
}
// Save accumulated data.
switch dst.Kind() {
case reflect.Invalid:
// Probably a comment.
default:
return errors.New("cannot unmarshal into " + dst0.Type().String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if len(src) == 0 {
dst.SetInt(0)
return nil
}
itmp, err := strconv.ParseInt(string(src), 10, dst.Type().Bits())
if err != nil {
return err
}
dst.SetInt(itmp)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
if len(src) == 0 {
dst.SetUint(0)
return nil
}
utmp, err := strconv.ParseUint(string(src), 10, dst.Type().Bits())
if err != nil {
return err
}
dst.SetUint(utmp)
case reflect.Float32, reflect.Float64:
if len(src) == 0 {
dst.SetFloat(0)
return nil
}
ftmp, err := strconv.ParseFloat(string(src), dst.Type().Bits())
if err != nil {
return err
}
dst.SetFloat(ftmp)
case reflect.Bool:
if len(src) == 0 {
dst.SetBool(false)
return nil
}
value, err := strconv.ParseBool(strings.TrimSpace(string(src)))
if err != nil {
return err
}
dst.SetBool(value)
case reflect.String:
dst.SetString(string(src))
case reflect.Slice:
if len(src) == 0 {
// non-nil to flag presence
src = []byte{}
}
dst.SetBytes(src)
}
return nil
}
// unmarshalPath walks down an XML structure looking for wanted
// paths, and calls unmarshal on them.
// The consumed result tells whether XML elements have been consumed
// from the Decoder until start's matching end element, or if it's
// still untouched because start is uninteresting for sv's fields.
func (d *Decoder) unmarshalPath(tinfo *typeInfo, sv reflect.Value, parents []string, start *StartElement) (consumed bool, err error) {
recurse := false
Loop:
for i := range tinfo.fields {
finfo := &tinfo.fields[i]
if finfo.flags&fElement == 0 || len(finfo.parents) < len(parents) || finfo.xmlns != "" && finfo.xmlns != start.Name.Space {
continue
}
for j := range parents {
if parents[j] != finfo.parents[j] {
continue Loop
}
}
if len(finfo.parents) == len(parents) && finfo.name == start.Name.Local {
// It's a perfect match, unmarshal the field.
return true, d.unmarshal(finfo.value(sv), start)
}
if len(finfo.parents) > len(parents) && finfo.parents[len(parents)] == start.Name.Local {
// It's a prefix for the field. Break and recurse
// since it's not ok for one field path to be itself
// the prefix for another field path.
recurse = true
// We can reuse the same slice as long as we
// don't try to append to it.
parents = finfo.parents[:len(parents)+1]
break
}
}
if !recurse {
// We have no business with this element.
return false, nil
}
// The element is not a perfect match for any field, but one
// or more fields have the path to this element as a parent
// prefix. Recurse and attempt to match these.
for {
var tok Token
tok, err = d.Token()
if err != nil {
return true, err
}
switch t := tok.(type) {
case StartElement:
consumed2, err := d.unmarshalPath(tinfo, sv, parents, &t)
if err != nil {
return true, err
}
if !consumed2 {
if err := d.Skip(); err != nil {
return true, err
}
}
case EndElement:
return true, nil
}
}
}
// Skip reads tokens until it has consumed the end element
// matching the most recent start element already consumed.
// It recurs if it encounters a start element, so it can be used to
// skip nested structures.
// It returns nil if it finds an end element matching the start
// element; otherwise it returns an error describing the problem.
func (d *Decoder) Skip() error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch tok.(type) {
case StartElement:
if err := d.Skip(); err != nil {
return err
}
case EndElement:
return nil
}
}
}

364
vendor/github.com/aclindsa/xml/typeinfo.go generated vendored Normal file
View File

@@ -0,0 +1,364 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xml
import (
"fmt"
"reflect"
"strings"
"sync"
)
// typeInfo holds details for the xml representation of a type.
type typeInfo struct {
xmlname *fieldInfo
fields []fieldInfo
}
// fieldInfo holds details for the xml representation of a single field.
type fieldInfo struct {
idx []int
name string
xmlns string
flags fieldFlags
parents []string
}
type fieldFlags int
const (
fElement fieldFlags = 1 << iota
fAttr
fCDATA
fCharData
fInnerXml
fComment
fAny
fOmitEmpty
fMode = fElement | fAttr | fCDATA | fCharData | fInnerXml | fComment | fAny
xmlName = "XMLName"
)
var tinfoMap sync.Map // map[reflect.Type]*typeInfo
var nameType = reflect.TypeOf(Name{})
// getTypeInfo returns the typeInfo structure with details necessary
// for marshaling and unmarshaling typ.
func getTypeInfo(typ reflect.Type) (*typeInfo, error) {
if ti, ok := tinfoMap.Load(typ); ok {
return ti.(*typeInfo), nil
}
tinfo := &typeInfo{}
if typ.Kind() == reflect.Struct && typ != nameType {
n := typ.NumField()
for i := 0; i < n; i++ {
f := typ.Field(i)
if (f.PkgPath != "" && !f.Anonymous) || f.Tag.Get("xml") == "-" {
continue // Private field
}
// For embedded structs, embed its fields.
if f.Anonymous {
t := f.Type
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() == reflect.Struct {
inner, err := getTypeInfo(t)
if err != nil {
return nil, err
}
if tinfo.xmlname == nil {
tinfo.xmlname = inner.xmlname
}
for _, finfo := range inner.fields {
finfo.idx = append([]int{i}, finfo.idx...)
if err := addFieldInfo(typ, tinfo, &finfo); err != nil {
return nil, err
}
}
continue
}
}
finfo, err := structFieldInfo(typ, &f)
if err != nil {
return nil, err
}
if f.Name == xmlName {
tinfo.xmlname = finfo
continue
}
// Add the field if it doesn't conflict with other fields.
if err := addFieldInfo(typ, tinfo, finfo); err != nil {
return nil, err
}
}
}
ti, _ := tinfoMap.LoadOrStore(typ, tinfo)
return ti.(*typeInfo), nil
}
// structFieldInfo builds and returns a fieldInfo for f.
func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, error) {
finfo := &fieldInfo{idx: f.Index}
// Split the tag from the xml namespace if necessary.
tag := f.Tag.Get("xml")
if i := strings.IndexByte(tag, ' '); i >= 0 {
finfo.xmlns, tag = tag[:i], tag[i+1:]
}
// Parse flags.
tokens := strings.Split(tag, ",")
if len(tokens) == 1 {
finfo.flags = fElement
} else {
tag = tokens[0]
for _, flag := range tokens[1:] {
switch flag {
case "attr":
finfo.flags |= fAttr
case "cdata":
finfo.flags |= fCDATA
case "chardata":
finfo.flags |= fCharData
case "innerxml":
finfo.flags |= fInnerXml
case "comment":
finfo.flags |= fComment
case "any":
finfo.flags |= fAny
case "omitempty":
finfo.flags |= fOmitEmpty
}
}
// Validate the flags used.
valid := true
switch mode := finfo.flags & fMode; mode {
case 0:
finfo.flags |= fElement
case fAttr, fCDATA, fCharData, fInnerXml, fComment, fAny, fAny | fAttr:
if f.Name == xmlName || tag != "" && mode != fAttr {
valid = false
}
default:
// This will also catch multiple modes in a single field.
valid = false
}
if finfo.flags&fMode == fAny {
finfo.flags |= fElement
}
if finfo.flags&fOmitEmpty != 0 && finfo.flags&(fElement|fAttr) == 0 {
valid = false
}
if !valid {
return nil, fmt.Errorf("xml: invalid tag in field %s of type %s: %q",
f.Name, typ, f.Tag.Get("xml"))
}
}
// Use of xmlns without a name is not allowed.
if finfo.xmlns != "" && tag == "" {
return nil, fmt.Errorf("xml: namespace without name in field %s of type %s: %q",
f.Name, typ, f.Tag.Get("xml"))
}
if f.Name == xmlName {
// The XMLName field records the XML element name. Don't
// process it as usual because its name should default to
// empty rather than to the field name.
finfo.name = tag
return finfo, nil
}
if tag == "" {
// If the name part of the tag is completely empty, get
// default from XMLName of underlying struct if feasible,
// or field name otherwise.
if xmlname := lookupXMLName(f.Type); xmlname != nil {
finfo.xmlns, finfo.name = xmlname.xmlns, xmlname.name
} else {
finfo.name = f.Name
}
return finfo, nil
}
// Prepare field name and parents.
parents := strings.Split(tag, ">")
if parents[0] == "" {
parents[0] = f.Name
}
if parents[len(parents)-1] == "" {
return nil, fmt.Errorf("xml: trailing '>' in field %s of type %s", f.Name, typ)
}
finfo.name = parents[len(parents)-1]
if len(parents) > 1 {
if (finfo.flags & fElement) == 0 {
return nil, fmt.Errorf("xml: %s chain not valid with %s flag", tag, strings.Join(tokens[1:], ","))
}
finfo.parents = parents[:len(parents)-1]
}
// If the field type has an XMLName field, the names must match
// so that the behavior of both marshaling and unmarshaling
// is straightforward and unambiguous.
if finfo.flags&fElement != 0 {
ftyp := f.Type
xmlname := lookupXMLName(ftyp)
if xmlname != nil && xmlname.name != finfo.name {
return nil, fmt.Errorf("xml: name %q in tag of %s.%s conflicts with name %q in %s.XMLName",
finfo.name, typ, f.Name, xmlname.name, ftyp)
}
}
return finfo, nil
}
// lookupXMLName returns the fieldInfo for typ's XMLName field
// in case it exists and has a valid xml field tag, otherwise
// it returns nil.
func lookupXMLName(typ reflect.Type) (xmlname *fieldInfo) {
for typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
if typ.Kind() != reflect.Struct {
return nil
}
for i, n := 0, typ.NumField(); i < n; i++ {
f := typ.Field(i)
if f.Name != xmlName {
continue
}
finfo, err := structFieldInfo(typ, &f)
if finfo.name != "" && err == nil {
return finfo
}
// Also consider errors as a non-existent field tag
// and let getTypeInfo itself report the error.
break
}
return nil
}
func min(a, b int) int {
if a <= b {
return a
}
return b
}
// addFieldInfo adds finfo to tinfo.fields if there are no
// conflicts, or if conflicts arise from previous fields that were
// obtained from deeper embedded structures than finfo. In the latter
// case, the conflicting entries are dropped.
// A conflict occurs when the path (parent + name) to a field is
// itself a prefix of another path, or when two paths match exactly.
// It is okay for field paths to share a common, shorter prefix.
func addFieldInfo(typ reflect.Type, tinfo *typeInfo, newf *fieldInfo) error {
var conflicts []int
Loop:
// First, figure all conflicts. Most working code will have none.
for i := range tinfo.fields {
oldf := &tinfo.fields[i]
if oldf.flags&fMode != newf.flags&fMode {
continue
}
if oldf.xmlns != "" && newf.xmlns != "" && oldf.xmlns != newf.xmlns {
continue
}
minl := min(len(newf.parents), len(oldf.parents))
for p := 0; p < minl; p++ {
if oldf.parents[p] != newf.parents[p] {
continue Loop
}
}
if len(oldf.parents) > len(newf.parents) {
if oldf.parents[len(newf.parents)] == newf.name {
conflicts = append(conflicts, i)
}
} else if len(oldf.parents) < len(newf.parents) {
if newf.parents[len(oldf.parents)] == oldf.name {
conflicts = append(conflicts, i)
}
} else {
if newf.name == oldf.name {
conflicts = append(conflicts, i)
}
}
}
// Without conflicts, add the new field and return.
if conflicts == nil {
tinfo.fields = append(tinfo.fields, *newf)
return nil
}
// If any conflict is shallower, ignore the new field.
// This matches the Go field resolution on embedding.
for _, i := range conflicts {
if len(tinfo.fields[i].idx) < len(newf.idx) {
return nil
}
}
// Otherwise, if any of them is at the same depth level, it's an error.
for _, i := range conflicts {
oldf := &tinfo.fields[i]
if len(oldf.idx) == len(newf.idx) {
f1 := typ.FieldByIndex(oldf.idx)
f2 := typ.FieldByIndex(newf.idx)
return &TagPathError{typ, f1.Name, f1.Tag.Get("xml"), f2.Name, f2.Tag.Get("xml")}
}
}
// Otherwise, the new field is shallower, and thus takes precedence,
// so drop the conflicting fields from tinfo and append the new one.
for c := len(conflicts) - 1; c >= 0; c-- {
i := conflicts[c]
copy(tinfo.fields[i:], tinfo.fields[i+1:])
tinfo.fields = tinfo.fields[:len(tinfo.fields)-1]
}
tinfo.fields = append(tinfo.fields, *newf)
return nil
}
// A TagPathError represents an error in the unmarshaling process
// caused by the use of field tags with conflicting paths.
type TagPathError struct {
Struct reflect.Type
Field1, Tag1 string
Field2, Tag2 string
}
func (e *TagPathError) Error() string {
return fmt.Sprintf("%s field %q with tag %q conflicts with field %q with tag %q", e.Struct, e.Field1, e.Tag1, e.Field2, e.Tag2)
}
// value returns v's field value corresponding to finfo.
// It's equivalent to v.FieldByIndex(finfo.idx), but initializes
// and dereferences pointers as necessary.
func (finfo *fieldInfo) value(v reflect.Value) reflect.Value {
for i, x := range finfo.idx {
if i > 0 {
t := v.Type()
if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
v = v.Elem()
}
}
v = v.Field(x)
}
return v
}

2075
vendor/github.com/aclindsa/xml/xml.go generated vendored Normal file

File diff suppressed because it is too large Load Diff