This commit is contained in:
Alexander Kiryukhin 2022-02-06 22:33:02 +03:00
commit d22326d5b8
No known key found for this signature in database
GPG key ID: 6DF7A2910D0699E9
11 changed files with 474 additions and 0 deletions

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/neonxp/geezer
go 1.18

48
hook.go Normal file
View file

@ -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

48
hook_string.go Normal file
View file

@ -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]]
}

103
httpKernel.go Normal file
View file

@ -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
}
}

162
kernel.go Normal file
View file

@ -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)
}

11
params.go Normal file
View file

@ -0,0 +1,11 @@
package geezer
import "context"
type Params struct {
Ctx context.Context
Path []string
Query Values
Headers Values
Provider string
}

22
render/json.go Normal file
View file

@ -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"
}

10
render/render.go Normal file
View file

@ -0,0 +1,10 @@
package render
import (
"io"
)
type Renderer interface {
Render(io.Writer) error
ContentType() string
}

21
render/text.go Normal file
View file

@ -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}
}

30
service.go Normal file
View file

@ -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

16
values.go Normal file
View file

@ -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
}