Compare commits

..

10 Commits

Author SHA1 Message Date
Bel LaPointe
aca2019552 mode 2020-05-18 14:34:48 -06:00
bel
ddba8ff6a1 tdd set up 2020-04-13 02:49:08 +00:00
bel
14692392e2 clean cleans for tests 2020-04-13 02:29:46 +00:00
bel
117ad922e2 Create stubs and limit page size 2020-04-13 02:19:56 +00:00
bel
eec859ed48 Test mongo basic ops with random ns 2020-04-13 01:22:46 +00:00
bel
b43c8b1f7a Start and stop mongod with ps 2020-04-13 00:29:39 +00:00
bel
3a8af0551e Set up mongodb c 2020-04-12 20:06:42 +00:00
bel
a255d391b5 Create public static files to serve 2020-04-12 17:28:05 +00:00
bel
1a3ba7c5e9 Add api prefix to routes 2020-04-12 17:02:23 +00:00
bel
0b4ecd1916 Not impl endpoints 2020-04-12 17:02:01 +00:00
35 changed files with 568 additions and 0 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ cheqbooq
exec-cheqbooq exec-cheqbooq
**/*.sw* **/*.sw*
vendor vendor
testdata

40
config/config.go Executable file
View File

@@ -0,0 +1,40 @@
package config
import (
"fmt"
"local/args"
"os"
"strings"
)
var (
Port string
Public string
StoreAddr string
StoreNS string
Page int
)
func init() {
New()
}
func New() {
if strings.Contains(fmt.Sprint(os.Args), "-test") {
return
}
as := args.NewArgSet()
as.Append(args.INT, "p", "port to listen on", 52222)
as.Append(args.INT, "page", "page size for requests", 20)
as.Append(args.STRING, "d", "dir with public files", "./public")
as.Append(args.STRING, "s", "mongodb address", "localhost:27017")
as.Append(args.STRING, "ns", "mongodb database", "cheqbooq")
if err := as.Parse(); err != nil {
panic(err)
}
Port = fmt.Sprintf(":%d", as.GetInt("p"))
Page = as.GetInt("page")
Public = as.GetString("d")
StoreAddr = as.GetString("s")
StoreNS = as.GetString("ns")
}

15
main.go Executable file
View File

@@ -0,0 +1,15 @@
package main
import (
"local/cheqbooq/server"
)
func main() {
if s, err := server.New(); err != nil {
panic(err)
} else if err := s.Routes(); err != nil {
panic(err)
} else if err := s.Listen(); err != nil {
panic(err)
}
}

8
public/index.html Executable file
View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>Hi</h1>
</body>
</html>

7
server/account/read.go Executable file
View File

@@ -0,0 +1,7 @@
package account
import "net/http"
func Read(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not impl", http.StatusNotImplemented)
}

5
server/account/readrequest.go Executable file
View File

@@ -0,0 +1,5 @@
package account
type ReadRequest struct {
Accounts []string `json:"accounts"`
}

8
server/account/readresponse.go Executable file
View File

@@ -0,0 +1,8 @@
package account
import "encoding/json"
type ReadResponse map[string]struct {
Name string `json:"name"`
Meta json.RawMessage `json:"meta"`
}

7
server/account/write.go Executable file
View File

@@ -0,0 +1,7 @@
package account
import "net/http"
func Write(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not impl", http.StatusNotImplemented)
}

5
server/account/writerequest.go Executable file
View File

@@ -0,0 +1,5 @@
package account
import "local/cheqbooq/server/transaction"
type WriteRequest transaction.WriteRequest

View File

@@ -0,0 +1,5 @@
package account
import "local/cheqbooq/server/transaction"
type WriteResponse transaction.WriteResponse

7
server/balance/read.go Executable file
View File

@@ -0,0 +1,7 @@
package balance
import "net/http"
func Read(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not impl", http.StatusNotImplemented)
}

7
server/balance/readrequest.go Executable file
View File

@@ -0,0 +1,7 @@
package balance
type ReadRequest struct {
Accounts []string `json:"accounts"`
Start int64 `json:"start"`
Stop int64 `json:"stop"`
}

3
server/balance/readresponse.go Executable file
View File

@@ -0,0 +1,3 @@
package balance
type ReadResponse map[string]map[int64]float32

1
server/balance/writerequest.go Executable file
View File

@@ -0,0 +1 @@
package balance

View File

@@ -0,0 +1 @@
package balance

10
server/listen.go Executable file
View File

@@ -0,0 +1,10 @@
package server
import (
"local/cheqbooq/config"
"net/http"
)
func (s *Server) Listen() error {
return http.ListenAndServe(config.Port, s)
}

62
server/routes.go Executable file
View File

@@ -0,0 +1,62 @@
package server
import (
"fmt"
"local/cheqbooq/config"
"local/cheqbooq/server/account"
"local/cheqbooq/server/balance"
"local/cheqbooq/server/transaction"
"local/router"
"net/http"
)
func (s *Server) Routes() error {
routes := map[string]http.HandlerFunc{
"api/v1/balance": s.balance1,
"api/v1/transaction": s.transaction1,
"api/v1/account": s.account1,
fmt.Sprintf("%s%s", router.Wildcard, router.Wildcard): s.public,
}
for path, handler := range routes {
if err := s.Add(path, handler); err != nil {
return err
}
}
return nil
}
func (s *Server) balance1(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
balance.Read(w, r)
default:
http.NotFound(w, r)
}
}
func (s *Server) transaction1(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
transaction.Read(w, r)
case http.MethodPatch:
transaction.Write(w, r)
default:
http.NotFound(w, r)
}
}
func (s *Server) account1(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
account.Read(w, r)
case http.MethodPatch:
account.Write(w, r)
default:
http.NotFound(w, r)
}
}
func (s *Server) public(w http.ResponseWriter, r *http.Request) {
d := http.FileServer(http.Dir(config.Public))
d.ServeHTTP(w, r)
}

13
server/server.go Executable file
View File

@@ -0,0 +1,13 @@
package server
import "local/router"
type Server struct {
*router.Router
}
func New() (*Server, error) {
return &Server{
Router: router.New(),
}, nil
}

7
server/transaction/read.go Executable file
View File

@@ -0,0 +1,7 @@
package transaction
import "net/http"
func Read(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not impl", http.StatusNotImplemented)
}

View File

@@ -0,0 +1,5 @@
package transaction
import "local/cheqbooq/server/balance"
type ReadRequest balance.ReadRequest

View File

@@ -0,0 +1,3 @@
package transaction
type ReadResponse map[string]map[string]Transaction

View File

@@ -0,0 +1,12 @@
package transaction
import "encoding/json"
type Transaction struct {
From string `json:"from"`
To string `json:"to"`
Category string `json:"category"`
Amount float32 `json:"amount"`
Timestamp int64 `json:"timestamp"`
Meta json.RawMessage `json:"meta"`
}

7
server/transaction/write.go Executable file
View File

@@ -0,0 +1,7 @@
package transaction
import "net/http"
func Write(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not impl", http.StatusNotImplemented)
}

View File

@@ -0,0 +1,7 @@
package transaction
type WriteRequest map[string][]struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value"`
}

View File

@@ -0,0 +1,6 @@
package transaction
type WriteResponse map[string][]struct {
OK bool `json:"ok"`
Message string `json:"message,omitempty"`
}

15
storage/account.go Executable file
View File

@@ -0,0 +1,15 @@
package storage
import "errors"
type Account struct {
ID string `json:"_id"`
}
func (s *Storage) Accounts(token string) ([]Account, error) {
return nil, errors.New("not impl")
}
func (s *Storage) PrimaryAccounts(token string) ([]Account, error) {
return nil, errors.New("not impl")
}

11
storage/account_test.go Executable file
View File

@@ -0,0 +1,11 @@
package storage
import "testing"
func TestAccounts(t *testing.T) {
t.Fatal("not impl")
}
func TestPrimaryAccounts(t *testing.T) {
t.Fatal("not impl")
}

17
storage/balance.go Executable file
View File

@@ -0,0 +1,17 @@
package storage
import "errors"
type Balance struct {
ID string `json:"id"`
At int64 `json:"at"`
Is float32 `json:"is"`
}
func (s *Storage) CurrentBalances(accounts ...Account) ([]Balance, error) {
return nil, errors.New("not impl")
}
func (s *Storage) BalancesOverTime(from, to int64, accounts ...Account) ([]Balance, error) {
return nil, errors.New("not impl")
}

11
storage/balance_test.go Executable file
View File

@@ -0,0 +1,11 @@
package storage
import "testing"
func TestCurrentBalances(t *testing.T) {
t.Fatal("not impl")
}
func TestBalancesOverTime(t *testing.T) {
t.Fatal("not impl")
}

108
storage/mongo.go Executable file
View File

@@ -0,0 +1,108 @@
package storage
import (
"context"
"log"
"os/exec"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type Mongo struct {
client *mongo.Client
ns string
page int
}
func init() {
go func() {
kick := func() error {
cmd := exec.Command("bash", "-c", "true; until [ $(basename $PWD) == cheqbooq ]; do cd ..; done; NOFORK=1 bash ./testdata/start_mdb.sh")
b, err := cmd.CombinedOutput()
if err != nil {
log.Printf("%s", b)
}
return err
}
block := func() error {
cmd := exec.Command("bash", "-c", "true; tail --pid=$(ps aux | grep mongod | grep -v grep | awk '{print $2}') -f /dev/null")
b, err := cmd.CombinedOutput()
if err != nil {
log.Printf("%s", b)
}
return err
}
for {
if err := kick(); err != nil {
log.Println(err)
}
if err := block(); err != nil {
log.Println(err)
}
}
}()
}
func NewMongo(page int, ns, addr string) (*Mongo, error) {
ctx, can := context.WithTimeout(context.Background(), time.Second*5)
defer can()
opt := options.Client()
opt.ApplyURI(addr)
client, err := mongo.Connect(ctx, opt)
if err != nil {
return nil, err
}
return &Mongo{
client: client,
ns: ns,
}, client.Ping(ctx, nil)
}
func (m *Mongo) Close() error {
return m.client.Disconnect(context.TODO())
}
func (m *Mongo) Account() *mongo.Collection {
return m.client.Database(m.ns).Collection("account")
}
func (m *Mongo) Balance() *mongo.Collection {
return m.client.Database(m.ns).Collection("balance")
}
func (m *Mongo) Transaction() *mongo.Collection {
return m.client.Database(m.ns).Collection("transaction")
}
func (m *Mongo) Find(c *mongo.Collection, where interface{}, next func() interface{}) error {
ctx, can := context.WithCancel(context.TODO())
defer can()
cur, err := c.Find(ctx, where, options.Find().SetLimit(int64(m.page)))
if err != nil {
return err
}
defer cur.Close(ctx)
for cur.Next(ctx) {
ptr := next()
if err := cur.Decode(ptr); err != nil {
return err
}
}
return cur.Err()
}
func (m *Mongo) Upsert(c *mongo.Collection, where, op interface{}) error {
ctx, can := context.WithCancel(context.TODO())
defer can()
_, err := c.UpdateMany(ctx, where, op, options.Update().SetUpsert(true))
return err
}

101
storage/mongo_test.go Executable file
View File

@@ -0,0 +1,101 @@
package storage
import (
"context"
"crypto/rand"
"encoding/base64"
"testing"
"time"
)
func testMongoNew(t *testing.T) (*Mongo, func()) {
b := make([]byte, 5)
rand.Read(b)
ns := "gotest_" + base64.URLEncoding.EncodeToString(b)
m, err := NewMongo(10, ns, "mongodb://localhost:27017")
if err != nil {
t.Fatal(err)
}
return m, func() {
ctx, can := context.WithTimeout(context.Background(), time.Second*30)
defer can()
m.client.Database(ns).Drop(ctx)
m.Close()
}
}
func TestMongoNew(t *testing.T) {
_, can := testMongoNew(t)
can()
}
func TestMongoFind(t *testing.T) {
m, can := testMongoNew(t)
defer can()
c := m.Account()
if _, err := c.InsertMany(context.TODO(), []interface{}{
map[string]interface{}{"_id": "1", "a": "b"},
map[string]interface{}{"_id": "2", "a": "b", "c": "d"},
map[string]interface{}{"_id": "3", "c": "d"},
}); err != nil {
t.Fatal(err)
}
cnt := 0
inc := func() interface{} {
cnt += 1
var v interface{}
return &v
}
if err := m.Find(c, map[string]interface{}{"a": "b"}, inc); err != nil {
t.Fatal(err)
} else if cnt != 2 {
t.Fatal(cnt)
}
cnt = 0
if err := m.Find(c, map[string]interface{}{"_id": "1"}, inc); err != nil {
t.Fatal(err)
} else if cnt != 1 {
t.Fatal(cnt)
}
cnt = 0
if err := m.Find(c, map[string]interface{}{}, inc); err != nil {
t.Fatal(err)
} else if cnt != 3 {
t.Fatal(cnt)
}
}
func TestMongoUpsert(t *testing.T) {
m, can := testMongoNew(t)
defer can()
c := m.Account()
if _, err := c.InsertMany(context.TODO(), []interface{}{
map[string]interface{}{"_id": "1", "a": "b"},
map[string]interface{}{"_id": "2", "a": "b", "c": "d"},
map[string]interface{}{"_id": "3", "c": "d"},
}); err != nil {
t.Fatal(err)
}
if n, err := c.EstimatedDocumentCount(context.TODO()); err != nil {
t.Fatal(err)
} else if n != 3 {
t.Fatal(n)
}
if err := m.Upsert(c, map[string]interface{}{"_id": "1"}, map[string]interface{}{"$set": map[string]interface{}{"c": "d"}}); err != nil {
t.Fatal(err)
}
mapped := map[string]string{}
if err := c.FindOne(context.TODO(), map[string]interface{}{"_id": "1"}).Decode(&mapped); err != nil {
t.Fatal(err)
} else if mapped["c"] != "d" {
t.Fatal(mapped)
}
}

18
storage/storage.go Executable file
View File

@@ -0,0 +1,18 @@
package storage
import (
"fmt"
"local/cheqbooq/config"
"strings"
)
type Storage struct {
mongo *Mongo
}
func New() (*Storage, error) {
mongo, err := NewMongo(config.Page, config.StoreNS, fmt.Sprintf("mongodb://%s", strings.TrimPrefix(config.StoreAddr, "mongodb://")))
return &Storage{
mongo: mongo,
}, err
}

13
storage/storage_test.go Executable file
View File

@@ -0,0 +1,13 @@
package storage
import "testing"
func testStorage(t *testing.T) (*Storage, func()) {
m, can := testMongoNew(t)
return &Storage{m}, can
}
func TestStorage(t *testing.T) {
_, can := testStorage(t)
can()
}

15
storage/transaction.go Executable file
View File

@@ -0,0 +1,15 @@
package storage
import "errors"
type Transaction struct {
From string `json:"from"`
To string `json:"to"`
Category string `json:"category"`
Amount float32 `json:"amount"`
Timestamp int64 `json:"timestamp"`
}
func (s *Storage) Transactions(from, to int64, accounts ...Account) ([]Transaction, error) {
return nil, errors.New("not impl")
}

7
storage/transaction_test.go Executable file
View File

@@ -0,0 +1,7 @@
package storage
import "testing"
func TestTransactions(t *testing.T) {
t.Fatal("not impl")
}