summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Neonxp Kiryukhin <i@neonxp.ru>2024-09-17 01:19:25 +0300
committerAlexander Neonxp Kiryukhin <i@neonxp.ru>2024-09-17 01:19:25 +0300
commita054f480adf26e90ddcb66408688486704ab7094 (patch)
tree4600b6e7633979e1a4b94cfcaeb7ddf156733575
parent5094ebf213243023c6325310b3f710a0974025dd (diff)
simple handlerv0.0.2
-rw-r--r--ctxlib/context.go44
-rw-r--r--error.go4
-rw-r--r--handler.go91
-rw-r--r--middleware/context.go7
-rw-r--r--middleware/request_id.go5
-rw-r--r--middleware/session/bbolt.go70
-rw-r--r--middleware/session/context.go9
-rw-r--r--middleware/session/memstore.go30
-rw-r--r--middleware/session/session.go128
-rw-r--r--middleware/session/store.go9
-rw-r--r--redirect.go8
-rw-r--r--render.go27
12 files changed, 164 insertions, 268 deletions
diff --git a/ctxlib/context.go b/ctxlib/context.go
new file mode 100644
index 0000000..79851ce
--- /dev/null
+++ b/ctxlib/context.go
@@ -0,0 +1,44 @@
+package ctxlib
+
+import (
+ "context"
+ "net/http"
+)
+
+type ctxKey int
+
+const (
+ Method ctxKey = iota
+ Headers
+ Request
+ RequestID
+ Response
+)
+
+func ResponseFromContext(ctx context.Context) http.ResponseWriter {
+ return ctx.Value(Response).(http.ResponseWriter)
+}
+
+func RequestFromContext(ctx context.Context) *http.Request {
+ c := ctx.Value(Request)
+ if c == nil {
+ return &http.Request{}
+ }
+ return c.(*http.Request)
+}
+
+func HeadersFromContext(ctx context.Context) http.Header {
+ c := ctx.Value(Headers)
+ if c == nil {
+ return http.Header{}
+ }
+ return c.(http.Header)
+}
+
+func MethodFromContext(ctx context.Context) string {
+ c := ctx.Value(Method)
+ if c == nil {
+ return http.MethodGet
+ }
+ return c.(string)
+}
diff --git a/error.go b/error.go
index 416d578..f85174e 100644
--- a/error.go
+++ b/error.go
@@ -3,11 +3,11 @@ package mux
import (
"context"
"encoding/json"
- "io"
+ "net/http"
)
var DefaultErrorHandler func(err error) Renderer = func(err error) Renderer {
- return RendererFunc(func(ctx context.Context, w io.Writer) error {
+ return RendererFunc(func(ctx context.Context, w http.ResponseWriter) error {
return json.NewEncoder(w).Encode(errorStruct{
Message: err.Error(),
})
diff --git a/handler.go b/handler.go
new file mode 100644
index 0000000..c7f9b66
--- /dev/null
+++ b/handler.go
@@ -0,0 +1,91 @@
+package mux
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+
+ "go.neonxp.ru/mux/ctxlib"
+)
+
+// Handler API handler and returns standard http.HandlerFunc function
+func Handler[RQ any, RS any](handler func(ctx context.Context, request *RQ) (RS, error)) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ req := new(RQ)
+ ctx := r.Context()
+ ctx = context.WithValue(ctx, ctxlib.Request, r)
+ ctx = context.WithValue(ctx, ctxlib.Method, r.Method)
+ ctx = context.WithValue(ctx, ctxlib.Headers, r.Header)
+
+ switch r.Method {
+ case http.MethodPost, http.MethodPatch, http.MethodDelete, http.MethodPut:
+ if err := Bind(r, req); err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(err.Error()))
+ return
+ }
+ }
+ resp, err := handler(ctx, req)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte(err.Error()))
+ return
+ }
+
+ statusCode := http.StatusOK
+ contentType := "application/json"
+ var body []byte
+
+ if v, ok := (any)(resp).(WithContentType); ok {
+ contentType = v.ContentType()
+ }
+ if v, ok := (any)(resp).(WithHTTPStatus); ok {
+ statusCode = v.Status()
+ }
+ if v, ok := (any)(resp).(Renderer); ok {
+ err = v.Render(ctx, w)
+ return
+ }
+
+ body, err = json.Marshal(resp)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte(err.Error()))
+ return
+ }
+ w.WriteHeader(statusCode)
+ w.Header().Set("Content-Type", contentType)
+ w.Write(body)
+
+ }
+}
+
+type NilRequest struct{}
+
+// WithContentType returns custom content type for response
+type WithContentType interface {
+ ContentType() string
+}
+
+// WithHTTPStatus returns custom status code
+type WithHTTPStatus interface {
+ Status() int
+}
+
+type RedirectResponse struct {
+ Code int
+ Location string
+}
+
+func (rr *RedirectResponse) Render(ctx context.Context, w http.ResponseWriter) error {
+ w.Header().Add("Location", rr.Location)
+ w.WriteHeader(rr.Code)
+ return nil
+}
+
+func Redirect(code int, location string) *RedirectResponse {
+ return &RedirectResponse{
+ Code: code,
+ Location: location,
+ }
+}
diff --git a/middleware/context.go b/middleware/context.go
deleted file mode 100644
index 8671140..0000000
--- a/middleware/context.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package middleware
-
-type ctxKey int
-
-const (
- requestIDKey ctxKey = iota
-)
diff --git a/middleware/request_id.go b/middleware/request_id.go
index 016b44a..feb5afa 100644
--- a/middleware/request_id.go
+++ b/middleware/request_id.go
@@ -4,6 +4,7 @@ import (
"context"
"net/http"
+ "go.neonxp.ru/mux/ctxlib"
"go.neonxp.ru/objectid"
)
@@ -17,12 +18,12 @@ func RequestID(next http.Handler) http.Handler {
requestID = objectid.New().String()
}
- next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), requestIDKey, requestID)))
+ next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxlib.RequestID, requestID)))
})
}
func GetRequestID(r *http.Request) string {
- rid := r.Context().Value(requestIDKey)
+ rid := r.Context().Value(ctxlib.RequestID)
if rid == nil {
return ""
}
diff --git a/middleware/session/bbolt.go b/middleware/session/bbolt.go
deleted file mode 100644
index bf46953..0000000
--- a/middleware/session/bbolt.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package session
-
-import (
- "bytes"
- "encoding/gob"
- "log/slog"
-
- "go.etcd.io/bbolt"
-)
-
-func NewBoltStore(db *bbolt.DB, bucketName []byte) Store {
- return &BoltStore{
- db: db,
- bucketName: bucketName,
- }
-}
-
-type BoltStore struct {
- db *bbolt.DB
- bucketName []byte
-}
-
-func (s *BoltStore) Load(sessionID string) Values {
- v := Values{}
- err := s.db.View(func(tx *bbolt.Tx) error {
- bucket := tx.Bucket(s.bucketName)
- if bucket == nil {
- // no bucket -- normal situation
- return nil
- }
- vb := bucket.Get([]byte(sessionID))
- if vb == nil {
- // no session -- no error
- return nil
- }
- rdr := bytes.NewBuffer(vb)
-
- return gob.NewDecoder(rdr).Decode(&v)
- })
- if err != nil {
- slog.Warn("failed load session", slog.Any("error", err))
- }
- return v
-}
-
-func (s *BoltStore) Save(sessionID string, value Values) error {
- return s.db.Update(func(tx *bbolt.Tx) error {
- bucket, err := tx.CreateBucketIfNotExists(s.bucketName)
- if err != nil {
- return err
- }
- wrt := bytes.NewBuffer([]byte{})
- if err := gob.NewEncoder(wrt).Encode(value); err != nil {
- return err
- }
-
- return bucket.Put([]byte(sessionID), wrt.Bytes())
- })
-}
-
-func (s *BoltStore) Remove(sessionID string) error {
- return s.db.Update(func(tx *bbolt.Tx) error {
- bucket, err := tx.CreateBucketIfNotExists(s.bucketName)
- if err != nil {
- return err
- }
-
- return bucket.Delete([]byte(sessionID))
- })
-}
diff --git a/middleware/session/context.go b/middleware/session/context.go
deleted file mode 100644
index 870957d..0000000
--- a/middleware/session/context.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package session
-
-type ctxKey int
-
-const (
- sessionManagerKey ctxKey = iota
- sessionIDKey
- sessionValueKey
-)
diff --git a/middleware/session/memstore.go b/middleware/session/memstore.go
deleted file mode 100644
index d8cb958..0000000
--- a/middleware/session/memstore.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package session
-
-import (
- "sync"
-)
-
-type MemoryStore struct {
- store sync.Map
-}
-
-func (s *MemoryStore) Load(sessionID string) Values {
- val, ok := s.store.Load(sessionID)
- if ok {
- return val.(Values)
- }
-
- return Values{}
-}
-
-func (s *MemoryStore) Save(sessionID string, value Values) error {
- s.store.Store(sessionID, value)
-
- return nil
-}
-
-func (s *MemoryStore) Remove(sessionID string) error {
- s.store.Delete(sessionID)
-
- return nil
-}
diff --git a/middleware/session/session.go b/middleware/session/session.go
deleted file mode 100644
index 47fc0fb..0000000
--- a/middleware/session/session.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package session
-
-import (
- "context"
- "errors"
- "net/http"
- "time"
-
- "go.neonxp.ru/mux"
- "go.neonxp.ru/objectid"
-)
-
-type Config struct {
- SessionCookie string
- Path string
- Domain string
- Secure bool
- HttpOnly bool
- MaxAge time.Duration
-}
-
-var DefaultConfig Config = Config{
- SessionCookie: "_session",
- Path: "/",
- Domain: "",
- Secure: false,
- HttpOnly: true,
- MaxAge: 365 * 24 * time.Hour,
-}
-
-var (
- ErrSessionNotFound = errors.New("session not found")
- ErrNoSessionInContext = errors.New("no session in context")
-)
-
-type SessionManager struct {
- config *Config
- storer Store
-}
-
-func New(storer Store) *SessionManager {
- return NewWithConfig(&DefaultConfig, storer)
-}
-
-func NewWithConfig(config *Config, storer Store) *SessionManager {
- return &SessionManager{
- config: config,
- storer: storer,
- }
-}
-
-func (s *SessionManager) Middleware() mux.Middleware {
- return func(h http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- var (
- sessionID string
- values Values
- )
- cookie, err := r.Cookie(s.config.SessionCookie)
- switch {
- case err == nil:
- sessionID = cookie.Value
- values = s.storer.Load(sessionID)
- case errors.Is(err, http.ErrNoCookie):
- sessionID = objectid.New().String()
- }
-
- ctx := context.WithValue(r.Context(), sessionManagerKey, s)
- ctx = context.WithValue(ctx, sessionIDKey, sessionID)
- ctx = context.WithValue(ctx, sessionValueKey, values)
-
- h.ServeHTTP(w, r.WithContext(ctx))
- })
- }
-}
-
-func (s *SessionManager) Values(ctx context.Context) Values {
- aValue := ctx.Value(sessionValueKey)
- values, ok := aValue.(Values)
- if !ok || values == nil {
- values = Values{}
- }
-
- return values
-}
-
-func (s *SessionManager) Save(w http.ResponseWriter, r *http.Request, values Values) error {
- aSessionID := r.Context().Value(sessionIDKey)
- sessionID, ok := aSessionID.(string)
- if !ok {
- return ErrNoSessionInContext
- }
-
- http.SetCookie(w, &http.Cookie{
- Name: s.config.SessionCookie,
- Value: sessionID,
- Path: s.config.Path,
- Domain: s.config.Domain,
- Secure: s.config.Secure,
- HttpOnly: s.config.HttpOnly,
- MaxAge: int(s.config.MaxAge.Seconds()),
- })
-
- return s.storer.Save(sessionID, values)
-}
-func (s *SessionManager) Clear(w http.ResponseWriter, r *http.Request) error {
- aSessionID := r.Context().Value(sessionIDKey)
- sessionID, ok := aSessionID.(string)
- if !ok {
- return ErrNoSessionInContext
- }
-
- http.SetCookie(w, &http.Cookie{
- Name: s.config.SessionCookie,
- Value: sessionID,
- Path: s.config.Path,
- Domain: s.config.Domain,
- Secure: s.config.Secure,
- HttpOnly: s.config.HttpOnly,
- MaxAge: -1,
- })
-
- return s.storer.Remove(sessionID)
-}
-
-func FromRequest(r *http.Request) *SessionManager {
- return r.Context().Value(sessionManagerKey).(*SessionManager)
-}
diff --git a/middleware/session/store.go b/middleware/session/store.go
deleted file mode 100644
index a02ba1e..0000000
--- a/middleware/session/store.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package session
-
-type Store interface {
- Load(sessionID string) Values
- Save(sessionID string, value Values) error
- Remove(sessionID string) error
-}
-
-type Values map[string]any
diff --git a/redirect.go b/redirect.go
deleted file mode 100644
index a392234..0000000
--- a/redirect.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package mux
-
-import "net/http"
-
-func Redirect(w http.ResponseWriter, code int, location string) {
- w.Header().Add("Location", location)
- w.WriteHeader(code)
-}
diff --git a/render.go b/render.go
index 8b39090..ee58b64 100644
--- a/render.go
+++ b/render.go
@@ -8,7 +8,7 @@ import (
)
type Renderer interface {
- Render(context.Context, io.Writer) error
+ Render(context.Context, http.ResponseWriter) error
}
func Render(w http.ResponseWriter, r *http.Request, renderable Renderer) {
@@ -17,8 +17,29 @@ func Render(w http.ResponseWriter, r *http.Request, renderable Renderer) {
}
}
-type RendererFunc func(context.Context, io.Writer) error
+type RendererFunc func(context.Context, http.ResponseWriter) error
-func (r RendererFunc) Render(ctx context.Context, w io.Writer) error {
+func (r RendererFunc) Render(ctx context.Context, w http.ResponseWriter) error {
return r(ctx, w)
}
+
+type IoRenderer interface {
+ Render(context.Context, io.Writer) error
+}
+
+func Http(code int, inner IoRenderer) Renderer {
+ return httpWrapper{
+ code: code,
+ inner: inner,
+ }
+}
+
+type httpWrapper struct {
+ code int
+ inner IoRenderer
+}
+
+func (r httpWrapper) Render(ctx context.Context, w http.ResponseWriter) error {
+ w.WriteHeader(r.code)
+ return r.inner.Render(ctx, w)
+}