diff --git a/cmd/http/router.go b/cmd/http/router.go index b42f803..78fb9d8 100644 --- a/cmd/http/router.go +++ b/cmd/http/router.go @@ -72,7 +72,7 @@ func (router Router) API(w http.ResponseWriter, r *http.Request) { case "/api/transactions": router.APITransactions(w, r) case "/api/amend": - router.APILastNLines(w, r) + router.APIAmend(w, r) case "/api/reg": router.APIReg(w, r) default: @@ -80,6 +80,29 @@ func (router Router) API(w http.ResponseWriter, r *http.Request) { } } +func (router Router) APITransactions(w http.ResponseWriter, r *http.Request) { + bpis, err := router.bpis() + if err != nil { + panic(err) + } + + deltas, err := router.files.Deltas() + if err != nil { + panic(err) + } + json.NewEncoder(w).Encode(map[string]any{ + "deltas": deltas.Like(ledger.LikeAfter(time.Now().Add(-1 * time.Hour * 24 * 365 / 2).Format("2006-01"))), + "balances": deltas.Balances(). + Like("^(Bel:Asset|Zach:Asset|HouseyMcHouseface:Debts:Credit)"). + Group(`^[^:]*`). + WithBPIs(bpis), + }) +} + +func (router Router) APIAmend(w http.ResponseWriter, r *http.Request) { + http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) +} + func (router Router) APIReg(w http.ResponseWriter, r *http.Request) { deltas, err := router.files.Deltas() if err != nil { @@ -312,34 +335,6 @@ func (router Router) APIReg(w http.ResponseWriter, r *http.Request) { } } -func (router Router) APITransactions(w http.ResponseWriter, r *http.Request) { - bpis, err := router.bpis() - if err != nil { - panic(err) - } - - lastNLines, err := router.files.TempGetLastNLines(20) - if err != nil { - panic(err) - } - deltas, err := router.files.Deltas() - if err != nil { - panic(err) - } - json.NewEncoder(w).Encode(map[string]any{ - "deltas": deltas.Like(ledger.LikeAfter(time.Now().Add(-1 * time.Hour * 24 * 365 / 2).Format("2006-01"))), - "balances": deltas.Balances(). - Like("^(Bel:Asset|Zach:Asset|HouseyMcHouseface:Debts:Credit)"). - Group(`^[^:]*`). - WithBPIs(bpis), - "lastNLines": lastNLines, - }) -} - -func (router Router) APILastNLines(w http.ResponseWriter, r *http.Request) { - http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) -} - func (r Router) bpis() (ledger.BPIs, error) { if r.bpiPath == "" { return make(ledger.BPIs), nil diff --git a/src/ledger/file.go b/src/ledger/file.go index 818485c..20c083e 100644 --- a/src/ledger/file.go +++ b/src/ledger/file.go @@ -1,17 +1,13 @@ package ledger import ( - "bufio" - "bytes" "fmt" "io" "io/fs" - "io/ioutil" "os" "path" "path/filepath" "sort" - "strings" "unicode" ) @@ -65,89 +61,6 @@ func (files Files) Amend(old, now Delta) error { return files.Add(transaction.payee, []Delta{old, now}...) } -func (files Files) TempGetLastNLines(n int) ([]string, error) { - p := files.paths()[0] - f, err := os.Open(p) - if err != nil { - return nil, err - } - defer f.Close() - return peekLastNLines(io.Discard, bufio.NewReader(f), n) -} - -func (files Files) TempSetLastNLines(n int, lines []string) error { - p := files.paths()[0] - - newFile, err := func() (string, error) { - w, err := ioutil.TempFile(os.TempDir(), path.Base(p)) - if err != nil { - return "", err - } - defer w.Close() - - r, err := os.Open(p) - if err != nil { - return "", err - } - defer r.Close() - - if _, err := peekLastNLines(w, bufio.NewReader(r), n); err != nil { - return "", err - } - for i := range lines { - if len(strings.TrimSpace(lines[i])) == 0 { - continue - } - if _, err := fmt.Fprintln(w, lines[i]); err != nil { - return "", err - } - } - if err := w.Close(); err != nil { - return "", err - } - return w.Name(), nil - }() - if err != nil { - return err - } - - r, err := os.Open(newFile) - if err != nil { - return err - } - defer r.Close() - - w, err := os.Create(p) - if err != nil { - return err - } - defer w.Close() - - _, err = io.Copy(w, r) - return err -} - -func peekLastNLines(w io.Writer, r *bufio.Reader, n int) ([]string, error) { - lastNLines := make([]string, 0, n) - for { - line, err := r.ReadBytes('\n') - if len(bytes.TrimSpace(line)) > 0 { - lastNLines = append(lastNLines, string(bytes.TrimRight(line, "\n"))) - for i := 0; i < len(lastNLines)-n; i++ { - fmt.Fprintln(w, lastNLines[i]) - } - lastNLines = lastNLines[max(0, len(lastNLines)-n):] - } - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - } - return lastNLines, nil -} - func (files Files) paths() []string { result := make([]string, 0, len(files)) for i := range files { diff --git a/src/ledger/file_test.go b/src/ledger/file_test.go index 2e8fb16..5ce1124 100644 --- a/src/ledger/file_test.go +++ b/src/ledger/file_test.go @@ -1,9 +1,6 @@ package ledger import ( - "bytes" - "encoding/base64" - "fmt" "os" "path" "path/filepath" @@ -450,148 +447,3 @@ func TestFilesOfDir(t *testing.T) { t.Error(paths) } } - -func TestFilesTempGetLastNLines(t *testing.T) { - cases := map[string]struct { - input string - n int - want []string - }{ - "empty": {}, - "get n lines from empty file": { - input: "", - n: 5, - want: []string{}, - }, - "get 0 lines from file": { - input: "#a\n#b", - n: 0, - want: []string{}, - }, - "get 3 lines from 2 line file": { - input: "#a\n#b", - n: 3, - want: []string{"#a", "#b"}, - }, - "get 2 lines from 3 line file": { - input: "#a\n#b\n#c", - n: 2, - want: []string{"#b", "#c"}, - }, - } - - for name, d := range cases { - c := d - t.Run(name, func(t *testing.T) { - p := path.Join(t.TempDir(), base64.URLEncoding.EncodeToString([]byte(t.Name()))) - os.WriteFile(p, []byte(c.input), os.ModePerm) - files, err := NewFiles(p) - if err != nil { - panic(err) - } - if got, err := files.TempGetLastNLines(c.n); err != nil { - t.Fatal(err) - } else if fmt.Sprint(got) != fmt.Sprint(c.want) { - for i := range c.want { - t.Logf("want[%d] = %q", i, c.want[i]) - } - for i := range got { - t.Logf(" got[%d] = %q", i, got[i]) - } - t.Errorf("wanted\n\t%+v, got\n\t%+v", c.want, got) - } - }) - } -} - -func TestFilesTempSetLastNLines(t *testing.T) { - cases := map[string]struct { - given string - input []string - n int - want string - }{ - "empty": {}, - "append to empty": { - input: []string{"hello", "world"}, - n: 100, - want: "hello\nworld\n", - }, - "replace last 10 of 1 lines with 2": { - given: "ohno", - input: []string{"hello", "world"}, - n: 10, - want: "hello\nworld\n", - }, - "replace last 1 of 1 lines with 2": { - given: "ohno", - input: []string{"hello", "world"}, - n: 1, - want: "hello\nworld\n", - }, - "replace last 0 of 1 lines with 2": { - given: "ohno", - input: []string{"hello", "world"}, - n: 0, - want: "ohno\nhello\nworld\n", - }, - "replace last 1 of 1 lines with 0": { - given: "ohno", - input: []string{}, - n: 1, - want: "", - }, - "replace last 1 of 2 lines with 1": { - given: "ohno\nhaha", - input: []string{"replaced"}, - n: 1, - want: "ohno\nreplaced\n", - }, - "replace last 1 of 2 lines with 2": { - given: "ohno\nhaha", - input: []string{"replac", "ed"}, - n: 1, - want: "ohno\nreplac\ned\n", - }, - } - - for name, d := range cases { - c := d - t.Run(name, func(t *testing.T) { - p := path.Join(t.TempDir(), base64.URLEncoding.EncodeToString([]byte(t.Name()))) - realp := p + ".real" - - os.WriteFile(realp, []byte(c.given), os.ModePerm) - if err := os.Symlink(realp, p); err != nil { - t.Fatal(err) - } - if stat, err := os.Lstat(p); err != nil { - t.Error(err) - } else if stat.Mode().IsRegular() { - t.Error("p is already a regular file") - } - - files := Files([]string{p}) - if err := files.TempSetLastNLines(c.n, c.input); err != nil { - t.Fatal(err) - } - - got, _ := os.ReadFile(p) - if string(got) != c.want { - t.Errorf("want\n%s, got\n%s", c.want, got) - } - - realb, _ := os.ReadFile(realp) - b, _ := os.ReadFile(realp) - if !bytes.Equal(b, realb) { - t.Errorf("%s no longer links to %s", p, realp) - } - - if stat, err := os.Lstat(p); err != nil { - t.Error(err) - } else if stat.Mode().IsRegular() { - t.Error("p is now a regular file") - } - }) - } -}