simple handler

This commit is contained in:
Alexander Neonxp Kiryukhin 2024-09-17 01:19:25 +03:00
parent 5094ebf213
commit a054f480ad
Signed by: NeonXP
SSH key fingerprint: SHA256:SVt7TjxbVc87m1QYaQziOJ0N3OCFURv2g76gD/UTTXI
12 changed files with 164 additions and 268 deletions

44
ctxlib/context.go Normal file
View 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)
}

View file

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

View file

@ -1,7 +0,0 @@
package middleware
type ctxKey int
const (
requestIDKey ctxKey = iota
)

View file

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

View file

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

View file

@ -1,9 +0,0 @@
package session
type ctxKey int
const (
sessionManagerKey ctxKey = iota
sessionIDKey
sessionValueKey
)

View file

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

View file

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

View file

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

View file

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

View file

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