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 /internal | |
parent | 340a623e1a35efe0182cadd780a5a3385b526705 (diff) |
Chi router
Diffstat (limited to 'internal')
-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 |
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) } |