Initial
This commit is contained in:
commit
d22326d5b8
11 changed files with 474 additions and 0 deletions
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/neonxp/geezer
|
||||||
|
|
||||||
|
go 1.18
|
48
hook.go
Normal file
48
hook.go
Normal 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
48
hook_string.go
Normal 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
103
httpKernel.go
Normal 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
162
kernel.go
Normal 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
11
params.go
Normal 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
22
render/json.go
Normal 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
10
render/render.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Renderer interface {
|
||||||
|
Render(io.Writer) error
|
||||||
|
ContentType() string
|
||||||
|
}
|
21
render/text.go
Normal file
21
render/text.go
Normal 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
30
service.go
Normal 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
16
values.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue