Skip to content

Commit

Permalink
Merge 30e7298 into 84cf4c3
Browse files Browse the repository at this point in the history
  • Loading branch information
mtlynch committed Oct 8, 2016
2 parents 84cf4c3 + 30e7298 commit f09c9de
Show file tree
Hide file tree
Showing 26 changed files with 1,177 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -22,3 +22,4 @@ _testmain.go
*.exe
*.test
*.prof
*.swp
10 changes: 10 additions & 0 deletions .travis.yml
@@ -0,0 +1,10 @@
---
sudo: false
language: go
go:
- 1.7.1
before_install:
- go get github.com/mattn/goveralls
script:
- ./build
- $HOME/gopath/bin/goveralls -service=travis-ci
7 changes: 6 additions & 1 deletion README.md
@@ -1 +1,6 @@
# prosperbot-frontend
# ProsperBot Frontend

[![Build Status](https://travis-ci.org/mtlynch/prosperbot-frontend.svg?branch=master)](https://travis-ci.org/mtlynch/prosperbot-frontend)
[![Coverage Status](https://coveralls.io/repos/github/mtlynch/prosperbot-frontend/badge.svg?branch=master)](https://coveralls.io/github/mtlynch/prosperbot-frontend?branch=master)
[![GoDoc](https://godoc.org/github.com/mtlynch/prosperbot-frontend?status.svg)](https://godoc.org/github.com/mtlynch/prosperbot-frontend)
[![Go Report Card](https://goreportcard.com/badge/github.com/mtlynch/prosperbot-frontend)](https://goreportcard.com/report/github.com/mtlynch/prosperbot-frontend)
87 changes: 87 additions & 0 deletions account/account.go
@@ -0,0 +1,87 @@
package account

import (
"encoding/json"
"errors"
"time"

"github.com/mtlynch/prosperbot/redis"
)

type (
redisListReader interface {
LRange(key string, start int64, stop int64) ([]string, error)
Quit() (string, error)
}

accountInfoManager struct {
redis redisListReader
}

accountAttributeFunc func(r redis.AccountRecord) float64

AccountAttributeRecord struct {
Value float64
Timestamp time.Time
}
)

func NewAccountInfoManager() (accountInfoManager, error) {
r, err := redis.New()
if err != nil {
return accountInfoManager{}, err
}
return accountInfoManager{r}, nil
}

var ErrNoAccountRecords = errors.New("no record of account cash balance")

func accountRecordToAccountAttributeRecord(r redis.AccountRecord, f accountAttributeFunc) AccountAttributeRecord {
return AccountAttributeRecord{
Value: f(r),
Timestamp: r.Timestamp,
}
}

func accountRecordsToAccountAttributeRecords(accountRecords []redis.AccountRecord, f accountAttributeFunc) []AccountAttributeRecord {
cashBalanceRecords := make([]AccountAttributeRecord, len(accountRecords))
for i, r := range accountRecords {
cashBalanceRecords[i] = accountRecordToAccountAttributeRecord(r, f)
}
return cashBalanceRecords
}

func collapseAccountAttributeRecords(r []AccountAttributeRecord) []AccountAttributeRecord {
if len(r) <= 1 {
return r
}
prev := r[len(r)-1]
collapsed := []AccountAttributeRecord{prev}
for i := len(r) - 2; i >= 0; i-- {
if prev.Value != r[i].Value {
collapsed = append(collapsed, r[i])
}
prev = r[i]
}
return collapsed
}

func (aim accountInfoManager) history(f accountAttributeFunc) ([]AccountAttributeRecord, error) {
accountRecordsSerialized, err := aim.redis.LRange(redis.KeyAccountInformation, 0, -1)
if err != nil {
return []AccountAttributeRecord{}, err
}
accountRecords := make([]redis.AccountRecord, len(accountRecordsSerialized))
for i, v := range accountRecordsSerialized {
err = json.Unmarshal([]byte(v), &accountRecords[i])
if err != nil {
return []AccountAttributeRecord{}, err
}
}
records := accountRecordsToAccountAttributeRecords(accountRecords, f)
return collapseAccountAttributeRecords(records), nil
}

func (aim accountInfoManager) Close() {
aim.redis.Quit()
}
125 changes: 125 additions & 0 deletions account/account_test.go
@@ -0,0 +1,125 @@
package account

import (
"errors"
"reflect"
"testing"
"time"
)

type mockRedisListReader struct {
KeyGot string
Err error
List []string
}

func (lr *mockRedisListReader) LRange(key string, start int64, stop int64) ([]string, error) {
lr.KeyGot = key
if lr.Err != nil {
return []string{}, lr.Err
}
if (start == 0) && (stop == -1) {
return lr.List, nil
}
if (start > int64(len(lr.List))) || ((stop + 1) > int64(len(lr.List))) {
return []string{}, nil
}
return lr.List[start : stop+1], nil
}

func (lr mockRedisListReader) Quit() (string, error) {
return "", nil
}

const (
accountInformationSerializedA = `{"Value":{"AvailableCashBalance":100,"TotalPrincipalReceivedOnActiveNotes":0,"OutstandingPrincipalOnActiveNotes":0,"LastWithdrawAmount":0,"LastDepositAmount":0,"LastDepositDate":"0001-01-01T00:00:00Z","PendingInvestmentsPrimaryMarket":0,"PendingInvestmentsSecondaryMarket":0,"PendingQuickInvestOrders":0,"TotalAmountInvestedOnActiveNotes":0,"TotalAccountValue":0,"InflightGross":0,"LastWithdrawDate":"0001-01-01T00:00:00Z"},"Timestamp":"2016-01-28T15:35:04.000000022Z"}`
accountInformationSerializedB = `{"Value":{"AvailableCashBalance":125.5,"TotalPrincipalReceivedOnActiveNotes":0,"OutstandingPrincipalOnActiveNotes":0,"LastWithdrawAmount":0,"LastDepositAmount":0,"LastDepositDate":"0001-01-01T00:00:00Z","PendingInvestmentsPrimaryMarket":0,"PendingInvestmentsSecondaryMarket":0,"PendingQuickInvestOrders":0,"TotalAmountInvestedOnActiveNotes":0,"TotalAccountValue":0,"InflightGross":0,"LastWithdrawDate":"0001-01-01T00:00:00Z"},"Timestamp":"2016-02-14T12:28:15.000000022Z"}`
accountInformationSerializedC = `{"Value":{"AvailableCashBalance":125.5,"TotalPrincipalReceivedOnActiveNotes":0,"OutstandingPrincipalOnActiveNotes":0,"LastWithdrawAmount":0,"LastDepositAmount":0,"LastDepositDate":"0001-01-01T00:00:00Z","PendingInvestmentsPrimaryMarket":0,"PendingInvestmentsSecondaryMarket":0,"PendingQuickInvestOrders":0,"TotalAmountInvestedOnActiveNotes":0,"TotalAccountValue":0,"InflightGross":0,"LastWithdrawDate":"0001-01-01T00:00:00Z"},"Timestamp":"2016-02-14T12:29:15.000000022Z"}`
accountInformationSerializedD = `{"Value":{"AvailableCashBalance":95.25,"TotalPrincipalReceivedOnActiveNotes":0,"OutstandingPrincipalOnActiveNotes":0,"LastWithdrawAmount":0,"LastDepositAmount":0,"LastDepositDate":"0001-01-01T00:00:00Z","PendingInvestmentsPrimaryMarket":0,"PendingInvestmentsSecondaryMarket":0,"PendingQuickInvestOrders":0,"TotalAmountInvestedOnActiveNotes":0,"TotalAccountValue":0,"InflightGross":0,"LastWithdrawDate":"0001-01-01T00:00:00Z"},"Timestamp":"2016-02-14T12:30:15.000000022Z"}`
badJSON = "{{mock bad JSON"
)

func TestCashBalanceHistory(t *testing.T) {
var tests = []struct {
accountList []string
lrangeErr error
wantRecords []AccountAttributeRecord
wantSuccess bool
msg string
}{
{
accountList: []string{
accountInformationSerializedB,
accountInformationSerializedA,
},
lrangeErr: errors.New("mock LRange error"),
wantSuccess: false,
msg: "should return error when redis call fails",
},
{
accountList: []string{},
wantRecords: []AccountAttributeRecord{},
wantSuccess: true,
msg: "should return empty list when there is no account history",
},
{
accountList: []string{
accountInformationSerializedB,
},
wantRecords: []AccountAttributeRecord{
{
Value: 125.50,
Timestamp: time.Date(2016, 2, 14, 12, 28, 15, 22, time.UTC),
},
},
wantSuccess: true,
msg: "should return valid record when single valid record exists",
},
{
accountList: []string{
accountInformationSerializedD,
accountInformationSerializedC,
accountInformationSerializedB,
accountInformationSerializedA,
},
wantRecords: []AccountAttributeRecord{
{
Value: 100.0,
Timestamp: time.Date(2016, 1, 28, 15, 35, 4, 22, time.UTC),
},
{
Value: 125.50,
Timestamp: time.Date(2016, 2, 14, 12, 28, 15, 22, time.UTC),
},
{
Value: 95.25,
Timestamp: time.Date(2016, 2, 14, 12, 30, 15, 22, time.UTC),
},
},
wantSuccess: true,
msg: "should return valid record when valid records exist",
},
{
accountList: []string{badJSON},
wantSuccess: false,
msg: "malformed JSON in redis should cause error",
},
}
for _, tt := range tests {
aim := accountInfoManager{
redis: &mockRedisListReader{
List: tt.accountList,
},
}
gotRecords, gotErr := aim.CashBalanceHistory()
if gotErr != nil && tt.wantSuccess {
t.Errorf("%s: unexpected error from CashBalanceHistory, got: %v, want: nil", tt.msg, gotErr)
}
if !tt.wantSuccess {
continue
}
if !reflect.DeepEqual(gotRecords, tt.wantRecords) {
t.Errorf("%s: unexpected records from CashBalanceHistory, got: %v, want: %v", tt.msg, gotRecords, tt.wantRecords)
}
}
}
13 changes: 13 additions & 0 deletions account/cash.go
@@ -0,0 +1,13 @@
package account

import (
"github.com/mtlynch/prosperbot/redis"
)

func cashBalanceAttribute(r redis.AccountRecord) float64 {
return r.Value.AvailableCashBalance
}

func (aim accountInfoManager) CashBalanceHistory() ([]AccountAttributeRecord, error) {
return aim.history(cashBalanceAttribute)
}
46 changes: 46 additions & 0 deletions account/handlers.go
@@ -0,0 +1,46 @@
package account

import (
"encoding/json"
"fmt"
"net/http"
)

type accountInfoFunc func(m accountInfoManager) (interface{}, error)

func accountAttributeHandler(f accountInfoFunc) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
cbm, err := NewAccountInfoManager()
if err != nil {
w.Write([]byte(fmt.Sprintf("failed to get account information: %v", err)))
return
}
defer cbm.Close()
b, err := f(cbm)
if err != nil {
w.Write([]byte(fmt.Sprintf("failed to get account information: %v", err)))
return
}
response, err := json.Marshal(b)
if err != nil {
w.Write([]byte(fmt.Sprintf("failed to get account information: %v", err)))
return
}
w.Write(response)
}
return http.HandlerFunc(fn)
}

func CashBalanceHistoryHandler() http.Handler {
return accountAttributeHandler(
func(m accountInfoManager) (interface{}, error) {
return m.CashBalanceHistory()
})
}

func AccountValueHistoryHandler() http.Handler {
return accountAttributeHandler(
func(m accountInfoManager) (interface{}, error) {
return m.AccountValueHistory()
})
}
13 changes: 13 additions & 0 deletions account/value.go
@@ -0,0 +1,13 @@
package account

import (
"github.com/mtlynch/prosperbot/redis"
)

func accountValueAttribute(r redis.AccountRecord) float64 {
return r.Value.TotalAccountValue
}

func (aim accountInfoManager) AccountValueHistory() ([]AccountAttributeRecord, error) {
return aim.history(accountValueAttribute)
}
11 changes: 11 additions & 0 deletions build
@@ -0,0 +1,11 @@
#!/bin/bash

# Exit on first error.
set -e

go test ./...

# Make sure there is no output from gofmt.
gofmt -s -d . 2>&1 | read && (echo 'error: not formatted' || exit 1)

go vet ./...
2 changes: 2 additions & 0 deletions hooks/enable_hooks
@@ -0,0 +1,2 @@
#!/bin/sh
rm -rf .git/hooks/ && ln -s -f ../hooks .git/
2 changes: 2 additions & 0 deletions hooks/pre-commit
@@ -0,0 +1,2 @@
#!/bin/sh
./build
31 changes: 31 additions & 0 deletions main.go
@@ -0,0 +1,31 @@
package main

import (
"log"
"net/http"

"github.com/mtlynch/prosperbot-frontend/account"
"github.com/mtlynch/prosperbot-frontend/notes"
)

func serveSingle(pattern string, filename string) {
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
log.Printf("Requested static file: %v\n", r.URL.Path)
http.ServeFile(w, r, filename)
})
}

func main() {
log.Println("starting up dashboard")

http.Handle("/cashBalanceHistory", account.CashBalanceHistoryHandler())
http.Handle("/accountValueHistory", account.AccountValueHistoryHandler())
http.Handle("/notes.json", notes.NotesHandler())
http.Handle("/static/",
http.StripPrefix("/static/",
http.FileServer(http.Dir("./static/"))))
serveSingle("/", "./static/dashboard.html")
serveSingle("/notes", "./static/notes.html")

log.Fatal(http.ListenAndServe(":8082", nil))
}
30 changes: 30 additions & 0 deletions notes/handlers.go
@@ -0,0 +1,30 @@
package notes

import (
"encoding/json"
"fmt"
"net/http"
)

func NotesHandler() http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
reader, err := NewNoteReader()
if err != nil {
w.Write([]byte(fmt.Sprintf("failed to get note information: %v", err)))
return
}
defer reader.Close()
n, err := reader.notes()
if err != nil {
w.Write([]byte(fmt.Sprintf("failed to get note information: %v", err)))
return
}
response, err := json.Marshal(n)
if err != nil {
w.Write([]byte(fmt.Sprintf("failed to get note information: %v", err)))
return
}
w.Write(response)
}
return http.HandlerFunc(fn)
}

0 comments on commit f09c9de

Please sign in to comment.