summaryrefslogtreecommitdiff
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
parent340a623e1a35efe0182cadd780a5a3385b526705 (diff)
Chi router
-rw-r--r--cmd/api/flags.go10
-rw-r--r--cmd/api/main.go75
-rw-r--r--go.mod11
-rw-r--r--go.sum30
-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
14 files changed, 375 insertions, 257 deletions
diff --git a/cmd/api/flags.go b/cmd/api/flags.go
new file mode 100644
index 0000000..b4910fa
--- /dev/null
+++ b/cmd/api/flags.go
@@ -0,0 +1,10 @@
+package main
+
+import "flag"
+
+var (
+ cluster = flag.String("cluster", "", "Master node address (ex: 192.168.1.10:5657)")
+ clusterAddr = flag.String("cluster_addr", ":5657", "Self address for cluster")
+ apiAddr = flag.String("api_addr", ":5656", "API address")
+ dbPath = flag.String("db_path", "/var/djson.db", "DB file")
+)
diff --git a/cmd/api/main.go b/cmd/api/main.go
index f64b43c..796837b 100644
--- a/cmd/api/main.go
+++ b/cmd/api/main.go
@@ -3,54 +3,82 @@ package main
import (
"context"
"flag"
- "log"
"net/http"
"os"
"os/signal"
- "go.neonxp.dev/djson/internal/api"
- "go.neonxp.dev/djson/internal/storage"
- "go.neonxp.dev/djson/internal/tree"
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/chi/v5/middleware"
+ "github.com/go-chi/httplog"
+ "github.com/rs/zerolog"
"golang.org/x/sync/errgroup"
-)
-var (
- cluster = flag.String("cluster", "", "Master node address (ex: 192.168.1.10:5657)")
- clusterAddr = flag.String("cluster_addr", ":5657", "Self address for cluster")
- apiAddr = flag.String("api_addr", ":5656", "API address")
+ "go.neonxp.dev/djson/internal/handler"
+ "go.neonxp.dev/djson/internal/storage"
+ "go.neonxp.dev/djson/internal/tree"
)
func main() {
flag.Parse()
+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()
+ logger := httplog.NewLogger("djson", httplog.Options{
+ JSON: true,
+ LogLevel: "debug",
+ })
+
+ // TBD raft cluster initialization
if cluster != nil {
- log.Printf("Connecting to cluster %s", *cluster)
+ logger.Info().Str("cluster connect", *cluster).Send()
} else {
- log.Printf("Creating cluster at %s", *clusterAddr)
+ logger.Info().Str("cluster create", *clusterAddr).Send()
}
- storage, err := storage.NewFileStorage("test.wal")
+
+ // Tree storage
+ storage, err := storage.New(*dbPath, logger)
if err != nil {
- log.Fatal(err)
+ panic(err)
}
- service := tree.New(
+
+ // Tree engine
+ core := tree.New(
storage,
)
+ if err := core.Init(); err != nil {
+ panic(err)
+ }
- server := http.Server{
+ // Tree HTTP wrapper
+ treeHandler := handler.New(core)
+
+ // HTTP router
+ r := chi.NewRouter()
+
+ r.Use(httplog.RequestLogger(logger))
+ r.Use(middleware.RequestID)
+ r.Use(middleware.RealIP)
+ r.Use(middleware.Recoverer)
+
+ r.Get("/health", treeHandler.Hearthbeat)
+ r.Route("/tree", treeHandler.Handle)
+
+ server := &http.Server{
Addr: *apiAddr,
- Handler: api.NewHandler(service),
+ Handler: r,
}
- eg, ctx := errgroup.WithContext(ctx)
+ if err := run(ctx, logger, server, storage); err != nil {
+ panic(err)
+ }
+}
- eg.Go(func() error {
- return service.Run(ctx)
- })
+func run(ctx context.Context, logger zerolog.Logger, server *http.Server, storage storage.Storage) error {
+ eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
- log.Printf("Server started at %s", *apiAddr)
+ logger.Info().Str("server listen", *apiAddr).Send()
if err := server.ListenAndServe(); err != http.ErrServerClosed {
return err
}
@@ -59,10 +87,9 @@ func main() {
eg.Go(func() error {
<-ctx.Done()
+ storage.Close()
return server.Close()
})
- if err := eg.Wait(); err != nil {
- log.Fatal(err)
- }
+ return eg.Wait()
}
diff --git a/go.mod b/go.mod
index 93e2040..9c9f7d6 100644
--- a/go.mod
+++ b/go.mod
@@ -3,12 +3,15 @@ module go.neonxp.dev/djson
go 1.19
require (
- github.com/vmihailenco/msgpack/v5 v5.3.5
- go.neonxp.dev/json v0.0.2
+ github.com/go-chi/chi/v5 v5.0.7
+ github.com/go-chi/httplog v0.2.5
+ go.neonxp.dev/json v0.0.4
golang.org/x/sync v0.1.0
)
require (
- github.com/gohobby/deepcopy v1.0.1
- github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ github.com/mattn/go-colorable v0.1.12 // indirect
+ github.com/mattn/go-isatty v0.0.14 // indirect
+ github.com/rs/zerolog v1.27.0 // indirect
+ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
)
diff --git a/go.sum b/go.sum
index b02d2f1..214bd7d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,16 +1,22 @@
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/gohobby/deepcopy v1.0.1 h1:PTUkRUVlg+Mph0f0RIKEmJJRcOClBrNJlk0weRtMcIs=
-github.com/gohobby/deepcopy v1.0.1/go.mod h1:J/RNFAQlLyrU0ZfJ86LUh9o7vdvn9C6HRvHfIsV1YFk=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
-github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
-github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
-github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
+github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/httplog v0.2.5 h1:S02eG9NTrB/9kk3Q3RA3F6CR2b+v8WzB8IxK+zq3dBo=
+github.com/go-chi/httplog v0.2.5/go.mod h1:/pIXuFSrOdc5heKIJRA5Q2mW7cZCI2RySqFZNFoZjKg=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
+github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
go.neonxp.dev/json v0.0.2 h1:wrq3Xm70qWrAVUQdfpCR5+cwzJ2QaVFMiGTzQn2dtAw=
go.neonxp.dev/json v0.0.2/go.mod h1:d+el/XRG46QCbCsly5PgUqyiwuU9GzBxFTGCuGHyd0w=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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)
}