From d22326d5b8cebdf9e53ab1ee4b13a714cc0eac3f Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Sun, 6 Feb 2022 22:33:02 +0300 Subject: [PATCH] Initial --- go.mod | 3 + hook.go | 48 ++++++++++++++ hook_string.go | 48 ++++++++++++++ httpKernel.go | 103 ++++++++++++++++++++++++++++++ kernel.go | 162 +++++++++++++++++++++++++++++++++++++++++++++++ params.go | 11 ++++ render/json.go | 22 +++++++ render/render.go | 10 +++ render/text.go | 21 ++++++ service.go | 30 +++++++++ values.go | 16 +++++ 11 files changed, 474 insertions(+) create mode 100644 go.mod create mode 100644 hook.go create mode 100644 hook_string.go create mode 100644 httpKernel.go create mode 100644 kernel.go create mode 100644 params.go create mode 100644 render/json.go create mode 100644 render/render.go create mode 100644 render/text.go create mode 100644 service.go create mode 100644 values.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1eb4bb5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/neonxp/geezer + +go 1.18 diff --git a/hook.go b/hook.go new file mode 100644 index 0000000..724b99d --- /dev/null +++ b/hook.go @@ -0,0 +1,48 @@ +package geezer + +import "github.com/neonxp/geezer/render" + +//go:generate stringer -type=HookLifecycle,HookType -output hook_string.go +type HookLifecycle int + +const ( + HookBefore HookLifecycle = iota + HookAfter + HookError +) + +type HookType int + +const ( + HookAll HookType = iota + HookFind + HookGet + HookCreate + HookUpdate + HookPatch + HookRemove +) + +var hookTypeFromMethod = map[Method]HookType{ + MethodFind: HookFind, + MethodGet: HookGet, + MethodCreate: HookCreate, + MethodUpdate: HookUpdate, + MethodPatch: HookPatch, + MethodRemove: HookRemove, +} + +type HookContext struct { + App Kernel + Path []string + Method Method + Type HookLifecycle + ID string + Params Params + Data Data + Err error + Result render.Renderer + StatusCode int +} + +type Hook func(ctx *HookContext) error diff --git a/hook_string.go b/hook_string.go new file mode 100644 index 0000000..1be4812 --- /dev/null +++ b/hook_string.go @@ -0,0 +1,48 @@ +// Code generated by "stringer -type=HookLifecycle,HookType -output hook_string.go"; DO NOT EDIT. + +package geezer + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[HookBefore-0] + _ = x[HookAfter-1] + _ = x[HookError-2] +} + +const _HookLifecycle_name = "HookBeforeHookAfterHookError" + +var _HookLifecycle_index = [...]uint8{0, 10, 19, 28} + +func (i HookLifecycle) String() string { + if i < 0 || i >= HookLifecycle(len(_HookLifecycle_index)-1) { + return "HookLifecycle(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _HookLifecycle_name[_HookLifecycle_index[i]:_HookLifecycle_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[HookAll-0] + _ = x[HookFind-1] + _ = x[HookGet-2] + _ = x[HookCreate-3] + _ = x[HookUpdate-4] + _ = x[HookPatch-5] + _ = x[HookRemove-6] +} + +const _HookType_name = "HookAllHookFindHookGetHookCreateHookUpdateHookPatchHookRemove" + +var _HookType_index = [...]uint8{0, 7, 15, 22, 32, 42, 51, 61} + +func (i HookType) String() string { + if i < 0 || i >= HookType(len(_HookType_index)-1) { + return "HookType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _HookType_name[_HookType_index[i]:_HookType_index[i+1]] +} diff --git a/httpKernel.go b/httpKernel.go new file mode 100644 index 0000000..8058bfa --- /dev/null +++ b/httpKernel.go @@ -0,0 +1,103 @@ +package geezer + +import ( + "io" + "net/http" + "net/url" + "strings" +) + +type HttpKernel struct { + Kernel +} + +func NewHttpKernel() *HttpKernel { + return &HttpKernel{ + Kernel: newKernel(), + } +} + +func (s *HttpKernel) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u, err := url.ParseRequestURI(r.RequestURI) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + parts := strings.Split(strings.Trim(u.Path, "/"), "/") + if len(parts) == 0 { + w.WriteHeader(http.StatusNotFound) + return + } + name := parts[0] + id := "" + if len(parts) > 1 { + id = parts[1] + } + method := MethodFind + switch { + case r.Method == http.MethodGet && id == "": + method = MethodFind + case r.Method == http.MethodGet && id != "": + method = MethodGet + case r.Method == http.MethodPost && id == "": + method = MethodCreate + case r.Method == http.MethodPost && id != "": + method = MethodUpdate + case r.Method == http.MethodPut: + method = MethodUpdate + case r.Method == http.MethodPatch: + method = MethodPatch + case r.Method == http.MethodDelete: + method = MethodRemove + default: + w.WriteHeader(http.StatusBadRequest) + return + } + + params := Params{ + Ctx: ctx, + Path: parts, + Query: Values(u.Query()), + Headers: Values(r.Header), + Provider: "http", + } + b, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + defer r.Body.Close() + data := Data(b) + + result, err := s.Call(method, name, id, data, params) + if err != nil { + if err == ErrMethodNotFound { + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusInternalServerError) + // TODO log + return + } + + w.Header().Set("Content-Type", result.ContentType()) + + switch method { + case MethodFind, MethodGet, MethodUpdate, MethodPatch: + w.WriteHeader(http.StatusOK) + case MethodCreate: + w.WriteHeader(http.StatusCreated) + case MethodRemove: + w.WriteHeader(http.StatusNoContent) + } + if method == MethodRemove { + return + } + if err := result.Render(w); err != nil { + w.WriteHeader(http.StatusInternalServerError) + // TODO log + } +} diff --git a/kernel.go b/kernel.go new file mode 100644 index 0000000..0f0356e --- /dev/null +++ b/kernel.go @@ -0,0 +1,162 @@ +package geezer + +import ( + "errors" + "strings" + + "github.com/neonxp/geezer/render" +) + +var ( + ErrServiceNotFound = errors.New("service not found") + ErrMethodNotFound = errors.New("method not found") +) + +type defaultKernel struct { + routes map[string]Service + hooks map[string]map[HookLifecycle]map[HookType][]Hook +} + +func newKernel() *defaultKernel { + return &defaultKernel{ + routes: map[string]Service{}, + hooks: map[string]map[HookLifecycle]map[HookType][]Hook{}, + } +} + +func (s *defaultKernel) Register(name string, service Service) error { + name = strings.ToLower(name) + s.routes[name] = service + if _, exist := s.hooks[name]; !exist { + s.hooks[name] = map[HookLifecycle]map[HookType][]Hook{} + } + if err := service.Setup(s, name); err != nil { + return err + } + return nil +} + +func (s *defaultKernel) Hook(service string, lifecycle HookLifecycle, hookType HookType, hook Hook) { + service = strings.ToLower(service) + if _, exist := s.hooks[service]; !exist { + s.hooks[service] = map[HookLifecycle]map[HookType][]Hook{} + } + if _, exist := s.hooks[service][lifecycle]; !exist { + s.hooks[service][lifecycle] = map[HookType][]Hook{} + } + if _, exist := s.hooks[service][lifecycle][hookType]; !exist { + s.hooks[service][lifecycle][hookType] = []Hook{} + } + s.hooks[service][lifecycle][hookType] = append(s.hooks[service][lifecycle][hookType], hook) +} + +func (s *defaultKernel) Service(name string) Service { + if service, exist := s.routes[name]; exist { + return service + } + return nil +} + +func (s *defaultKernel) Call(method Method, name, id string, data Data, params Params) (render.Renderer, error) { + name = strings.ToLower(name) + service := s.Service(name) + if service == nil { + return nil, ErrServiceNotFound + } + + hookCtx, result, err := s.callBeforeHooks(method, name, id, data, params) + if err != nil { + return result, err + } + switch hookCtx.Method { + case MethodFind: + result, err = service.Find(hookCtx.Params) + case MethodGet: + result, err = service.Get(hookCtx.ID, hookCtx.Params) + case MethodCreate: + result, err = service.Create(hookCtx.Data, hookCtx.Params) + case MethodUpdate: + result, err = service.Update(hookCtx.ID, hookCtx.Data, hookCtx.Params) + case MethodPatch: + result, err = service.Patch(hookCtx.ID, hookCtx.Data, hookCtx.Params) + case MethodRemove: + err = service.Remove(hookCtx.ID, hookCtx.Params) + default: + return nil, ErrMethodNotFound + } + return s.callAfterHooks(method, name, hookCtx, result, err) +} + +func (s *defaultKernel) callBeforeHooks(method Method, name string, id string, data Data, params Params) (*HookContext, render.Renderer, error) { + var beforeHooks []Hook + if hooks, ok := s.hooks[name][HookBefore]; ok { + if allHooks, ok := hooks[HookAll]; ok { + beforeHooks = append(beforeHooks, allHooks...) + } + if methodHooks, ok := hooks[hookTypeFromMethod[method]]; ok { + beforeHooks = append(beforeHooks, methodHooks...) + } + } + + hookCtx := &HookContext{ + App: s, + Method: method, + Type: HookBefore, + ID: id, + Params: params, + Data: data, + Err: nil, + Result: nil, + StatusCode: 0, + } + for _, hook := range beforeHooks { + if err := hook(hookCtx); err != nil { + return nil, nil, err + } + } + return hookCtx, nil, nil +} + +func (s *defaultKernel) callAfterHooks(method Method, name string, hookCtx *HookContext, result render.Renderer, err error) (render.Renderer, error) { + var afterHooks []Hook + if hooks, ok := s.hooks[name][HookAfter]; ok { + if allHooks, ok := hooks[HookAll]; ok { + afterHooks = append(afterHooks, allHooks...) + } + if methodHooks, ok := hooks[hookTypeFromMethod[method]]; ok { + afterHooks = append(afterHooks, methodHooks...) + } + } + var errorHooks []Hook + if hooks, ok := s.hooks[name][HookError]; ok { + if allHooks, ok := hooks[HookAll]; ok { + errorHooks = append(errorHooks, allHooks...) + } + if methodHooks, ok := hooks[hookTypeFromMethod[method]]; ok { + errorHooks = append(errorHooks, methodHooks...) + } + } + hookCtx.Result = result + hookCtx.Err = err + if err != nil { + for _, hook := range errorHooks { + if err := hook(hookCtx); err != nil { + return nil, err + } + } + return hookCtx.Result, hookCtx.Err + } + for _, hook := range afterHooks { + if err := hook(hookCtx); err != nil { + return nil, err + } + } + return hookCtx.Result, hookCtx.Err +} + +type Kernel interface { + Register(name string, service Service) error + Hook(service string, lifecycle HookLifecycle, hookType HookType, hook Hook) + Service(name string) Service + Call(method Method, name, id string, data Data, params Params) (render.Renderer, error) +} diff --git a/params.go b/params.go new file mode 100644 index 0000000..c57df1b --- /dev/null +++ b/params.go @@ -0,0 +1,11 @@ +package geezer + +import "context" + +type Params struct { + Ctx context.Context + Path []string + Query Values + Headers Values + Provider string +} diff --git a/render/json.go b/render/json.go new file mode 100644 index 0000000..8eadb46 --- /dev/null +++ b/render/json.go @@ -0,0 +1,22 @@ +package render + +import ( + "encoding/json" + "io" +) + +type JsonRender struct { + Data any +} + +func JSON(data any) *JsonRender { + return &JsonRender{Data: data} +} + +func (j *JsonRender) Render(writer io.Writer) error { + return json.NewEncoder(writer).Encode(j.Data) +} + +func (JsonRender) ContentType() string { + return "application/json; charset=utf-8" +} diff --git a/render/render.go b/render/render.go new file mode 100644 index 0000000..da9d60a --- /dev/null +++ b/render/render.go @@ -0,0 +1,10 @@ +package render + +import ( + "io" +) + +type Renderer interface { + Render(io.Writer) error + ContentType() string +} diff --git a/render/text.go b/render/text.go new file mode 100644 index 0000000..140a467 --- /dev/null +++ b/render/text.go @@ -0,0 +1,21 @@ +package render + +import "io" + +type TextRender struct { + contentType string + Body string +} + +func (t *TextRender) Render(writer io.Writer) error { + _, err := writer.Write([]byte(t.Body)) + return err +} + +func (t *TextRender) ContentType() string { + return t.contentType +} + +func Text(contentType string, body string) *TextRender { + return &TextRender{contentType: contentType, Body: body} +} diff --git a/service.go b/service.go new file mode 100644 index 0000000..2e6f4bd --- /dev/null +++ b/service.go @@ -0,0 +1,30 @@ +package geezer + +import ( + "encoding/json" + + "github.com/neonxp/geezer/render" +) + +type Service interface { + Find(params Params) (render.Renderer, error) + Get(id string, params Params) (render.Renderer, error) + Create(data Data, params Params) (render.Renderer, error) + Update(id string, data Data, params Params) (render.Renderer, error) + Patch(id string, data Data, params Params) (render.Renderer, error) + Remove(id string, params Params) error + Setup(app Kernel, path string) error +} + +type Method int + +const ( + MethodFind Method = iota + MethodGet + MethodCreate + MethodUpdate + MethodPatch + MethodRemove +) + +type Data json.RawMessage diff --git a/values.go b/values.go new file mode 100644 index 0000000..a9e3f05 --- /dev/null +++ b/values.go @@ -0,0 +1,16 @@ +package geezer + +type Values map[string][]string + +func (v Values) Get(key string) string { + vs := v[key] + if len(vs) == 0 { + return "" + } + return vs[0] +} + +func (v Values) Has(key string) bool { + _, has := v[key] + return has +}