simple handler
This commit is contained in:
parent
5094ebf213
commit
a054f480ad
12 changed files with 164 additions and 268 deletions
44
ctxlib/context.go
Normal file
44
ctxlib/context.go
Normal file
|
@ -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)
|
||||||
|
}
|
4
error.go
4
error.go
|
@ -3,11 +3,11 @@ package mux
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DefaultErrorHandler func(err error) Renderer = func(err error) Renderer {
|
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{
|
return json.NewEncoder(w).Encode(errorStruct{
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
})
|
})
|
||||||
|
|
91
handler.go
Normal file
91
handler.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
type ctxKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
requestIDKey ctxKey = iota
|
|
||||||
)
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"go.neonxp.ru/mux/ctxlib"
|
||||||
"go.neonxp.ru/objectid"
|
"go.neonxp.ru/objectid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,12 +18,12 @@ func RequestID(next http.Handler) http.Handler {
|
||||||
requestID = objectid.New().String()
|
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 {
|
func GetRequestID(r *http.Request) string {
|
||||||
rid := r.Context().Value(requestIDKey)
|
rid := r.Context().Value(ctxlib.RequestID)
|
||||||
if rid == nil {
|
if rid == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package session
|
|
||||||
|
|
||||||
type ctxKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
sessionManagerKey ctxKey = iota
|
|
||||||
sessionIDKey
|
|
||||||
sessionValueKey
|
|
||||||
)
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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)
|
|
||||||
}
|
|
27
render.go
27
render.go
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Renderer interface {
|
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) {
|
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)
|
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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue