// 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. Two are built in: "csv" for delimited exports with // configurable columns, and "cmd" for shelling out to an external extractor // (which is how the existing Python scripts are used until they are ported). // Ported extractors register themselves here and become usable by putting // their name in an account.toml. 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 // Counterparty is the other side's account number, where the statement // gives one. Optional. Counterparty string // 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 } // 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 }