// Package parser turns a statement file into raw transactions. // // Every bank needs its own extraction logic, so parsers are looked up by name // from a registry that each one joins from an init. A parser becomes usable by // putting its name in an account.toml; there is no generic column-mapped // parser, because a statement layout is better described in Go, where it can // be tested, than in a table of column indexes. package parser import ( "fmt" "sort" "sync" "git.petrovv.com/nikola/money/internal/config" ) // RawTxn is one line as it came out of a statement, before fingerprinting, // deduplication or tagging. type RawTxn struct { Date string // YYYY-MM-DD Description string AmountMinor int64 // signed; negative is an outflow // Type is the bank's own classification of the transaction. Optional. Type string // BalanceMinor is the running balance after this transaction, when the // statement reports one. Optional; enables the balance-chain check. BalanceMinor *int64 } // Parser extracts transactions from a single statement file. type Parser interface { // Parse reads the statement at path. digits is the account's minor-unit // scale, so parsers can convert decimal strings without guessing. Parse(path string, acc *config.Account) ([]RawTxn, error) } // Warner is an optional interface for parsers that legitimately drop rows -- // pending transactions, other currencies -- and want to say so. The importer // collects the warnings from the most recent Parse call and reports them. type Warner interface { Warnings() []string } // Namer is an optional interface for parsers whose bank names its downloads // unhelpfully. StatementName reads the statement at path and returns the name, // without extension, it should be stored under — the upload's extension is // kept, lowercased — or "" when the statement does not say, in which case it // keeps the name it came with. Only an upload uses // it; files already in a folder are never renamed, since the index records // them by path. type Namer interface { StatementName(path string) (string, error) } // Factory builds a parser from an account's config, validating it up front so // a bad account.toml fails before any file is read. type Factory func(acc *config.Account) (Parser, error) var ( mu sync.RWMutex registry = map[string]Factory{} ) // Register adds a named parser. It panics on a duplicate name, since that can // only be a programming error at init time. func Register(name string, f Factory) { mu.Lock() defer mu.Unlock() if _, dup := registry[name]; dup { panic("parser: duplicate registration of " + name) } registry[name] = f } // For builds the parser named by acc.Parser. func For(acc *config.Account) (Parser, error) { mu.RLock() f, ok := registry[acc.Parser] mu.RUnlock() if !ok { return nil, fmt.Errorf("account %s: unknown parser %q (available: %v)", acc.Slug, acc.Parser, Names()) } return f(acc) } // Names lists the registered parsers, for error messages and `money parsers`. func Names() []string { mu.RLock() defer mu.RUnlock() out := make([]string, 0, len(registry)) for name := range registry { out = append(out, name) } sort.Strings(out) return out }