summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorNeonXP <i@neonxp.dev>2022-11-21 03:47:16 +0300
committerNeonXP <i@neonxp.dev>2022-12-04 19:06:13 +0300
commit9d46ca252151a2c48434f9ec201bcb3c9133ec78 (patch)
treeb146450cf0c09355f06656768f88043078195f0e /internal
parent340a623e1a35efe0182cadd780a5a3385b526705 (diff)
Chi router
Diffstat (limited to 'internal')
-rw-r--r--internal/api/handler.go155
-rw-r--r--internal/handler/auth.go1
-rw-r--r--internal/handler/contract.go12
-rw-r--r--internal/handler/tree.go147
-rw-r--r--internal/storage/contract.go (renamed from internal/storage/storage.go)0
-rw-r--r--internal/storage/file.go31
-rw-r--r--internal/tree/contract.go23
-rw-r--r--internal/tree/core.go47
-rw-r--r--internal/tree/engine.go42
-rw-r--r--internal/tree/mutations.go48
10 files changed, 289 insertions, 217 deletions
diff --git a/internal/api/handler.go b/internal/api/handler.go
deleted file mode 100644
index 0e51807..0000000
--- a/internal/api/handler.go
+++ /dev/null
@@ -1,155 +0,0 @@
-package api
-
-import (
- "context"
- "fmt"
- "io"
- "log"
- "net/http"
- "path/filepath"
- "strings"
- "time"
-
- "go.neonxp.dev/djson/internal/model"
- "go.neonxp.dev/djson/internal/tree"
- "go.neonxp.dev/json"
-)
-
-func NewHandler(tree *tree.Engine) *Handler {
- return &Handler{tree: tree}
-}
-
-type Handler struct {
- tree *tree.Engine
-}
-
-func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- case http.MethodGet:
- res, err := h.get(r.Context(), r.URL.Path)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- log.Println(err.Error())
- return
- }
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- log.Println(err.Error())
- return
- }
- result, err := res.MarshalJSON()
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- log.Println(err.Error())
- return
- }
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write(result)
- case http.MethodPost:
- jsonBody, err := io.ReadAll(r.Body)
- r.Body.Close()
-
- if err := h.post(r.Context(), r.URL.Path, jsonBody); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- log.Println(err.Error())
- return
- }
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- log.Println(err.Error())
- return
- }
- w.WriteHeader(http.StatusCreated)
- case http.MethodPatch:
- jsonBody, err := io.ReadAll(r.Body)
- r.Body.Close()
-
- if err := h.patch(r.Context(), r.URL.Path, jsonBody); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- log.Println(err.Error())
- return
- }
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- log.Println(err.Error())
- return
- }
- w.WriteHeader(http.StatusOK)
- case http.MethodDelete:
- if err := h.remove(r.Context(), r.URL.Path); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- log.Println(err.Error())
- return
- }
- w.WriteHeader(http.StatusNoContent)
- default:
- w.WriteHeader(http.StatusMethodNotAllowed)
- _, _ = w.Write([]byte(fmt.Sprintf("Unknown method: %s", r.Method)))
- }
-}
-
-func (h *Handler) get(ctx context.Context, p string) (Marshaller, error) {
- return h.tree.Get(parsePath(p))
-}
-
-func (h *Handler) post(ctx context.Context, p string, jsonBody []byte) error {
- node, err := json.Unmarshal(jsonBody)
- if err != nil {
- return err
- }
-
- return h.tree.Mutation(ctx, &model.Mutation{
- Date: time.Now(),
- Type: model.Create,
- Path: parsePath(p),
- Body: *node,
- })
-}
-
-func (h *Handler) patch(ctx context.Context, p string, jsonBody []byte) error {
- node, err := json.Unmarshal(jsonBody)
- if err != nil {
- return err
- }
-
- return h.tree.Mutation(ctx, &model.Mutation{
- Date: time.Now(),
- Type: model.Merge,
- Path: parsePath(p),
- Body: *node,
- })
-}
-
-func (h *Handler) remove(ctx context.Context, p string) error {
- return h.tree.Mutation(ctx, &model.Mutation{
- Date: time.Now(),
- Type: model.Remove,
- Path: parsePath(p),
- })
-}
-
-type ErrorStruct struct {
- Error string `json:"error"`
-}
-
-func newError(err error) *ErrorStruct {
- return &ErrorStruct{
- Error: err.Error(),
- }
-}
-
-type Marshaller interface {
- MarshalJSON() ([]byte, error)
-}
-
-func parsePath(nodePath string) []string {
- nodePath = filepath.Clean(nodePath)
- nodePath = strings.Trim(nodePath, "/")
- arr := []string{}
- for _, v := range strings.Split(nodePath, "/") {
- if v != "" {
- arr = append(arr, v)
- }
- }
- return arr
-}
diff --git a/internal/handler/auth.go b/internal/handler/auth.go
new file mode 100644
index 0000000..abeebd1
--- /dev/null
+++ b/internal/handler/auth.go
@@ -0,0 +1 @@
+package handler
diff --git a/internal/handler/contract.go b/internal/handler/contract.go
new file mode 100644
index 0000000..ed79158
--- /dev/null
+++ b/internal/handler/contract.go
@@ -0,0 +1,12 @@
+package handler
+
+import (
+ "net/http"
+
+ "github.com/go-chi/chi/v5"
+)
+
+type Handler interface {
+ Handle(r chi.Router)
+ Hearthbeat(w http.ResponseWriter, r *http.Request)
+}
diff --git a/internal/handler/tree.go b/internal/handler/tree.go
new file mode 100644
index 0000000..e574b2b
--- /dev/null
+++ b/internal/handler/tree.go
@@ -0,0 +1,147 @@
+package handler
+
+import (
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/chi/v5/middleware"
+ "go.neonxp.dev/json"
+ jsonModel "go.neonxp.dev/json/model"
+
+ "go.neonxp.dev/djson/internal/model"
+ "go.neonxp.dev/djson/internal/tree"
+)
+
+func New(core tree.Core) Handler {
+ return &handler{
+ core: core,
+ }
+}
+
+type handler struct {
+ core tree.Core
+}
+
+func (h *handler) Handle(r chi.Router) {
+ r.Use(middleware.CleanPath)
+ r.Use(middleware.StripSlashes)
+
+ r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
+ rctx := chi.RouteContext(r.Context())
+ res, err := h.core.Get(parsePath(rctx.RoutePath))
+ if err != nil {
+ writeError(http.StatusNotFound, err, w)
+ return
+ }
+ result, err := res.MarshalJSON()
+ if err != nil {
+ writeError(http.StatusInternalServerError, err, w)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(result)
+ })
+
+ r.Post("/*", func(w http.ResponseWriter, r *http.Request) {
+ rctx := chi.RouteContext(r.Context())
+ path := parsePath(rctx.RoutePath)
+ jsonBody, err := io.ReadAll(r.Body)
+ if err != nil {
+ writeError(http.StatusBadRequest, err, w)
+ return
+ }
+ r.Body.Close()
+ node, err := json.Unmarshal(jsonBody)
+ if err != nil {
+ writeError(http.StatusBadRequest, err, w)
+ return
+ }
+ mutation := &model.Mutation{
+ Date: time.Now(),
+ Type: model.Create,
+ Path: path,
+ Body: node,
+ }
+ if err := h.core.Mutation(r.Context(), mutation); err != nil {
+ writeError(http.StatusInternalServerError, err, w)
+ return
+ }
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ r.Patch("/*", func(w http.ResponseWriter, r *http.Request) {
+ rctx := chi.RouteContext(r.Context())
+ path := parsePath(rctx.RoutePath)
+ jsonBody, err := io.ReadAll(r.Body)
+ if err != nil {
+ writeError(http.StatusBadRequest, err, w)
+ return
+ }
+ r.Body.Close()
+ node, err := json.Unmarshal(jsonBody)
+ if err != nil {
+ writeError(http.StatusBadRequest, err, w)
+ return
+ }
+ mutation := &model.Mutation{
+ Date: time.Now(),
+ Type: model.Merge,
+ Path: path,
+ Body: node,
+ }
+ if err := h.core.Mutation(r.Context(), mutation); err != nil {
+ writeError(http.StatusInternalServerError, err, w)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ })
+
+ r.Delete("/*", func(w http.ResponseWriter, r *http.Request) {
+ rctx := chi.RouteContext(r.Context())
+ path := parsePath(rctx.RoutePath)
+ mutation := &model.Mutation{
+ Date: time.Now(),
+ Type: model.Remove,
+ Path: path,
+ Body: nil,
+ }
+ if err := h.core.Mutation(r.Context(), mutation); err != nil {
+ writeError(http.StatusInternalServerError, err, w)
+ return
+ }
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+func (h *handler) Hearthbeat(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ switch h.core.State() {
+ case tree.Ready:
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte("."))
+ case tree.Failed:
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte("start failed"))
+ case tree.Running:
+ w.WriteHeader(http.StatusServiceUnavailable)
+ _, _ = w.Write([]byte("starting..."))
+ }
+}
+
+func writeError(code int, err error, w http.ResponseWriter) {
+ jsonErr, _ := json.Marshal(jsonModel.NewNode(err.Error()))
+ _, _ = w.Write(jsonErr)
+}
+
+func parsePath(nodePath string) []string {
+ arr := []string{}
+ for _, v := range strings.Split(nodePath, "/") {
+ if v != "" {
+ arr = append(arr, v)
+ }
+ }
+ return arr
+}
diff --git a/internal/storage/storage.go b/internal/storage/contract.go
index 5847bcc..5847bcc 100644
--- a/internal/storage/storage.go
+++ b/internal/storage/contract.go
diff --git a/internal/storage/file.go b/internal/storage/file.go
index 7619e32..885d5ef 100644
--- a/internal/storage/file.go
+++ b/internal/storage/file.go
@@ -5,13 +5,24 @@ import (
"encoding/gob"
"fmt"
"io"
- "log"
"os"
+ "github.com/rs/zerolog"
"go.neonxp.dev/djson/internal/model"
+ jsonModel "go.neonxp.dev/json/model"
)
-type FileStorage struct {
+func init() {
+ gob.Register(new(jsonModel.ArrayNode))
+ gob.Register(new(jsonModel.ObjectNode))
+ gob.Register(new(jsonModel.StringNode))
+ gob.Register(new(jsonModel.BooleanNode))
+ gob.Register(new(jsonModel.NullNode))
+ gob.Register(new(jsonModel.NumberNode))
+}
+
+type fsStorage struct {
+ logger zerolog.Logger
enc *gob.Encoder
dec *gob.Decoder
fh *os.File
@@ -19,13 +30,15 @@ type FileStorage struct {
mutationsLog []model.Mutation
}
-func NewFileStorage(fileName string) (*FileStorage, error) {
+func New(fileName string, logger zerolog.Logger) (Storage, error) {
+ logger.Info().Str("path", fileName).Msg("loading db")
fh, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0o666)
if err != nil {
return nil, err
}
- return &FileStorage{
+ return &fsStorage{
+ logger: logger,
fileName: fileName,
fh: fh,
enc: gob.NewEncoder(fh),
@@ -34,26 +47,26 @@ func NewFileStorage(fileName string) (*FileStorage, error) {
}, nil
}
-func (fs *FileStorage) Commit(ctx context.Context, mut model.Mutation) error {
+func (fs *fsStorage) Commit(ctx context.Context, mut model.Mutation) error {
if fs.enc == nil {
return fmt.Errorf("file storage not initiated")
}
return fs.enc.Encode(mut)
}
-func (fs *FileStorage) Load() chan model.Mutation {
+func (fs *fsStorage) Load() chan model.Mutation {
ch := make(chan model.Mutation)
go func() {
for {
m := model.Mutation{}
if err := fs.dec.Decode(&m); err != nil {
if err != io.EOF {
- log.Println(err.Error())
+ fs.logger.Err(err)
}
close(ch)
return
}
- log.Println("Loaded from fs", m.String())
+ fs.logger.Debug().RawJSON("json", []byte(m.String())).Msg("loaded mutation")
fs.mutationsLog = append(fs.mutationsLog, m)
ch <- m
}
@@ -61,6 +74,6 @@ func (fs *FileStorage) Load() chan model.Mutation {
return ch
}
-func (fs *FileStorage) Close() error {
+func (fs *fsStorage) Close() error {
return fs.fh.Close()
}
diff --git a/internal/tree/contract.go b/internal/tree/contract.go
new file mode 100644
index 0000000..b949339
--- /dev/null
+++ b/internal/tree/contract.go
@@ -0,0 +1,23 @@
+package tree
+
+import (
+ "context"
+
+ dmodel "go.neonxp.dev/djson/internal/model"
+ "go.neonxp.dev/json/model"
+)
+
+type Core interface {
+ Init() error
+ Get(nodes []string) (model.Node, error)
+ Mutation(ctx context.Context, mut *dmodel.Mutation) error
+ State() CoreState
+}
+
+type CoreState int
+
+const (
+ Running CoreState = iota
+ Ready
+ Failed
+)
diff --git a/internal/tree/core.go b/internal/tree/core.go
new file mode 100644
index 0000000..aa47fb6
--- /dev/null
+++ b/internal/tree/core.go
@@ -0,0 +1,47 @@
+package tree
+
+import (
+ "sync"
+
+ "go.neonxp.dev/djson/internal/storage"
+ "go.neonxp.dev/json/model"
+)
+
+type stdCore struct {
+ Root model.ObjectNode
+ state CoreState
+ mu sync.RWMutex
+ storage storage.Storage
+}
+
+func New(storage storage.Storage) Core {
+ return &stdCore{
+ Root: model.ObjectNode{},
+ state: Running,
+ mu: sync.RWMutex{},
+ storage: storage,
+ }
+}
+
+func (t *stdCore) Init() error {
+ // Load initial mutations
+ for m := range t.storage.Load() {
+ if err := t.execute(&m); err != nil {
+ t.state = Failed
+ return err
+ }
+ }
+ t.state = Ready
+ return nil
+}
+
+func (t *stdCore) Get(nodes []string) (model.Node, error) {
+ if len(nodes) == 0 {
+ return &t.Root, nil
+ }
+ return model.Query(&t.Root, nodes)
+}
+
+func (t *stdCore) State() CoreState {
+ return t.state
+}
diff --git a/internal/tree/engine.go b/internal/tree/engine.go
deleted file mode 100644
index 0f28eb7..0000000
--- a/internal/tree/engine.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package tree
-
-import (
- "context"
- "sync"
-
- "go.neonxp.dev/djson/internal/storage"
- "go.neonxp.dev/json/model"
-)
-
-type Engine struct {
- Root model.Node
- mu sync.RWMutex
- storage storage.Storage
-}
-
-func New(storage storage.Storage) *Engine {
- return &Engine{
- Root: model.Node{},
- mu: sync.RWMutex{},
- storage: storage,
- }
-}
-
-func (t *Engine) Run(ctx context.Context) error {
- // Load initial mutations
- for m := range t.storage.Load() {
- if err := t.execute(&m); err != nil {
- return err
- }
- }
-
- <-ctx.Done()
- return nil
-}
-
-func (t *Engine) Get(nodes []string) (*model.Node, error) {
- if len(nodes) == 0 {
- return &t.Root, nil
- }
- return t.Root.Query(nodes)
-}
diff --git a/internal/tree/mutations.go b/internal/tree/mutations.go
index ec80ebd..9aa9056 100644
--- a/internal/tree/mutations.go
+++ b/internal/tree/mutations.go
@@ -3,11 +3,13 @@ package tree
import (
"context"
"fmt"
+ "strings"
"go.neonxp.dev/djson/internal/model"
+ json "go.neonxp.dev/json/model"
)
-func (t *Engine) Mutation(ctx context.Context, mut *model.Mutation) error {
+func (t *stdCore) Mutation(ctx context.Context, mut *model.Mutation) error {
t.mu.Lock()
defer t.mu.Unlock()
if err := t.execute(mut); err != nil {
@@ -16,45 +18,69 @@ func (t *Engine) Mutation(ctx context.Context, mut *model.Mutation) error {
return t.storage.Commit(ctx, *mut)
}
-func (t *Engine) execute(mut *model.Mutation) error {
+func (t *stdCore) execute(mut *model.Mutation) error {
switch mut.Type {
case model.Create:
if len(mut.Path) == 0 {
// create root node
- t.Root = mut.Body
+ inObject, ok := mut.Body.(*json.ObjectNode)
+ if !ok {
+ return fmt.Errorf("root node must be object")
+ }
+ t.Root = *inObject
return nil
}
key := mut.Path[len(mut.Path)-1]
path := mut.Path[:len(mut.Path)-1]
- target, err := t.Root.Query(path)
+ target, err := json.Query(&t.Root, path)
if err != nil {
return err
}
- return target.Set(key, mut.Body)
+ targetObject, ok := target.(*json.ObjectNode)
+ if !ok {
+ return fmt.Errorf("node %s is not object", strings.Join(path, "/"))
+ }
+ return targetObject.Value.Set(key, mut.Body)
case model.Merge:
+ inObject, ok := mut.Body.(*json.ObjectNode)
+ if !ok {
+ return fmt.Errorf("patch allowed only for objects")
+ }
if len(mut.Path) == 0 {
// patch root node
- return t.Root.Merge(&mut.Body)
+ t.Root.Merge(inObject)
+ return nil
}
- target, err := t.Root.Query(mut.Path)
+ target, err := json.Query(&t.Root, mut.Path)
if err != nil {
return err
}
- return target.Merge(&mut.Body)
+ targetObject, ok := target.(*json.ObjectNode)
+ if !ok {
+ return fmt.Errorf("patch allowed only for objects")
+ }
+ targetObject.Merge(inObject)
+ return nil
case model.Remove:
if len(mut.Path) == 0 {
return fmt.Errorf("can't remove root node. Only replace or create avaliable")
}
key := mut.Path[len(mut.Path)-1]
if len(mut.Path) == 1 {
- return t.Root.Remove(key)
+ t.Root.Remove(key)
+ return nil
}
path := mut.Path[:len(mut.Path)-1]
- target, err := t.Root.Query(path)
+ target, err := json.Query(&t.Root, path)
if err != nil {
return err
}
- return target.Remove(key)
+ targetObject, ok := target.(*json.ObjectNode)
+ if !ok {
+ return fmt.Errorf("remove allowed only from objects")
+ }
+ targetObject.Remove(key)
+ return nil
}
return fmt.Errorf("invalid command type: %d", mut.Type)
}