Compare commits
10 Commits
79600941be
...
aca2019552
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aca2019552 | ||
|
|
ddba8ff6a1 | ||
|
|
14692392e2 | ||
|
|
117ad922e2 | ||
|
|
eec859ed48 | ||
|
|
b43c8b1f7a | ||
|
|
3a8af0551e | ||
|
|
a255d391b5 | ||
|
|
1a3ba7c5e9 | ||
|
|
0b4ecd1916 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ cheqbooq
|
||||
exec-cheqbooq
|
||||
**/*.sw*
|
||||
vendor
|
||||
testdata
|
||||
|
||||
40
config/config.go
Executable file
40
config/config.go
Executable 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
15
main.go
Executable 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
8
public/index.html
Executable file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hi</h1>
|
||||
</body>
|
||||
</html>
|
||||
7
server/account/read.go
Executable file
7
server/account/read.go
Executable 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
5
server/account/readrequest.go
Executable file
@@ -0,0 +1,5 @@
|
||||
package account
|
||||
|
||||
type ReadRequest struct {
|
||||
Accounts []string `json:"accounts"`
|
||||
}
|
||||
8
server/account/readresponse.go
Executable file
8
server/account/readresponse.go
Executable 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
7
server/account/write.go
Executable 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
5
server/account/writerequest.go
Executable file
@@ -0,0 +1,5 @@
|
||||
package account
|
||||
|
||||
import "local/cheqbooq/server/transaction"
|
||||
|
||||
type WriteRequest transaction.WriteRequest
|
||||
5
server/account/writeresponse.go
Executable file
5
server/account/writeresponse.go
Executable file
@@ -0,0 +1,5 @@
|
||||
package account
|
||||
|
||||
import "local/cheqbooq/server/transaction"
|
||||
|
||||
type WriteResponse transaction.WriteResponse
|
||||
7
server/balance/read.go
Executable file
7
server/balance/read.go
Executable 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
7
server/balance/readrequest.go
Executable 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
3
server/balance/readresponse.go
Executable file
@@ -0,0 +1,3 @@
|
||||
package balance
|
||||
|
||||
type ReadResponse map[string]map[int64]float32
|
||||
1
server/balance/writerequest.go
Executable file
1
server/balance/writerequest.go
Executable file
@@ -0,0 +1 @@
|
||||
package balance
|
||||
1
server/balance/writeresponse.go
Executable file
1
server/balance/writeresponse.go
Executable file
@@ -0,0 +1 @@
|
||||
package balance
|
||||
10
server/listen.go
Executable file
10
server/listen.go
Executable 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
62
server/routes.go
Executable 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
13
server/server.go
Executable 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
7
server/transaction/read.go
Executable 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)
|
||||
}
|
||||
5
server/transaction/readrequest.go
Executable file
5
server/transaction/readrequest.go
Executable file
@@ -0,0 +1,5 @@
|
||||
package transaction
|
||||
|
||||
import "local/cheqbooq/server/balance"
|
||||
|
||||
type ReadRequest balance.ReadRequest
|
||||
3
server/transaction/readresponse.go
Executable file
3
server/transaction/readresponse.go
Executable file
@@ -0,0 +1,3 @@
|
||||
package transaction
|
||||
|
||||
type ReadResponse map[string]map[string]Transaction
|
||||
12
server/transaction/transaction.go
Executable file
12
server/transaction/transaction.go
Executable 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
7
server/transaction/write.go
Executable 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)
|
||||
}
|
||||
7
server/transaction/writerequest.go
Executable file
7
server/transaction/writerequest.go
Executable file
@@ -0,0 +1,7 @@
|
||||
package transaction
|
||||
|
||||
type WriteRequest map[string][]struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
6
server/transaction/writeresponse.go
Executable file
6
server/transaction/writeresponse.go
Executable 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
15
storage/account.go
Executable 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
11
storage/account_test.go
Executable 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
17
storage/balance.go
Executable 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
11
storage/balance_test.go
Executable 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
108
storage/mongo.go
Executable 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
101
storage/mongo_test.go
Executable 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
18
storage/storage.go
Executable 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
13
storage/storage_test.go
Executable 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
15
storage/transaction.go
Executable 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
7
storage/transaction_test.go
Executable file
@@ -0,0 +1,7 @@
|
||||
package storage
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTransactions(t *testing.T) {
|
||||
t.Fatal("not impl")
|
||||
}
|
||||
Reference in New Issue
Block a user