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 (
|
||||
"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(),
|
||||
})
|
||||
|
|
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"
|
||||
"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 ""
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue