diff options
author | NeonXP <i@neonxp.dev> | 2022-11-21 03:47:16 +0300 |
---|---|---|
committer | NeonXP <i@neonxp.dev> | 2022-12-04 19:06:13 +0300 |
commit | 9d46ca252151a2c48434f9ec201bcb3c9133ec78 (patch) | |
tree | b146450cf0c09355f06656768f88043078195f0e | |
parent | 340a623e1a35efe0182cadd780a5a3385b526705 (diff) |
Chi router
-rw-r--r-- | cmd/api/flags.go | 10 | ||||
-rw-r--r-- | cmd/api/main.go | 75 | ||||
-rw-r--r-- | go.mod | 11 | ||||
-rw-r--r-- | go.sum | 30 | ||||
-rw-r--r-- | internal/api/handler.go | 155 | ||||
-rw-r--r-- | internal/handler/auth.go | 1 | ||||
-rw-r--r-- | internal/handler/contract.go | 12 | ||||
-rw-r--r-- | internal/handler/tree.go | 147 | ||||
-rw-r--r-- | internal/storage/contract.go (renamed from internal/storage/storage.go) | 0 | ||||
-rw-r--r-- | internal/storage/file.go | 31 | ||||
-rw-r--r-- | internal/tree/contract.go | 23 | ||||
-rw-r--r-- | internal/tree/core.go | 47 | ||||
-rw-r--r-- | internal/tree/engine.go | 42 | ||||
-rw-r--r-- | internal/tree/mutations.go | 48 |
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() } @@ -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 ) @@ -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) } |