Browse Source

go-wisp: v0.0.1

Timo Riegebauer 2 weeks ago
parent
commit
667c0e3bb1
7 changed files with 479 additions and 1 deletions
  1. 1 1
      LICENSE
  2. 236 0
      context.go
  3. 5 0
      go.mod
  4. 2 0
      go.sum
  5. 90 0
      group.go
  6. 9 0
      types.go
  7. 136 0
      wisp.go

+ 1 - 1
LICENSE

@@ -1,5 +1,5 @@
 MIT License
-Copyright (c) <year> <copyright holders>
+Copyright (c) 2025 Timo Riegebauer
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 

+ 236 - 0
context.go

@@ -0,0 +1,236 @@
+package wisp
+
+import (
+	"context"
+	"encoding/json"
+	"encoding/xml"
+	"errors"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"github.com/julienschmidt/httprouter"
+)
+
+type Context struct {
+	response http.ResponseWriter
+	request  *http.Request
+	params   httprouter.Params
+	ctx      context.Context
+}
+
+// Basic getters
+
+func (c *Context) Response() http.ResponseWriter {
+	return c.response
+}
+
+func (c *Context) Request() *http.Request {
+	return c.request
+}
+
+func (c *Context) Params() httprouter.Params {
+	return c.params
+}
+
+func (c *Context) Context() context.Context {
+	return c.ctx
+}
+
+// URL Param retrieval (from path)
+
+func (c *Context) Param(name string) string {
+	return c.params.ByName(name)
+}
+
+// Query param retrieval
+
+func (c *Context) Query(name string) string {
+	return c.request.URL.Query().Get(name)
+}
+
+func (c *Context) QueryDefault(name, defaultValue string) string {
+	if v := c.Query(name); v != "" {
+		return v
+	}
+	return defaultValue
+}
+
+// Form param retrieval (POST, PUT with form data)
+
+func (c *Context) FormValue(name string) string {
+	return c.request.FormValue(name)
+}
+
+func (c *Context) FormValueDefault(name, defaultValue string) string {
+	if v := c.FormValue(name); v != "" {
+		return v
+	}
+	return defaultValue
+}
+
+// Body binding helpers
+
+func (c *Context) BindJSON(dest interface{}) error {
+	defer c.request.Body.Close()
+	dec := json.NewDecoder(c.request.Body)
+	dec.DisallowUnknownFields()
+	return dec.Decode(dest)
+}
+
+func (c *Context) BindXML(dest interface{}) error {
+	defer c.request.Body.Close()
+	dec := xml.NewDecoder(c.request.Body)
+	return dec.Decode(dest)
+}
+
+func (c *Context) BindForm() (url.Values, error) {
+	defer c.request.Body.Close()
+	contentType := c.request.Header.Get("Content-Type")
+	if !strings.Contains(contentType, "application/x-www-form-urlencoded") {
+		return nil, errors.New("content-type is not application/x-www-form-urlencoded")
+	}
+	return url.ParseQuery(c.request.PostForm.Encode())
+}
+
+func (c *Context) BindMultipartForm(maxMemory int64) error {
+	return c.request.ParseMultipartForm(maxMemory)
+}
+
+// Cookie helpers
+
+func (c *Context) Cookie(name string) (*http.Cookie, error) {
+	return c.request.Cookie(name)
+}
+
+func (c *Context) SetCookie(cookie *http.Cookie) {
+	http.SetCookie(c.response, cookie)
+}
+
+// Response helpers
+
+func (c *Context) Write(data []byte) (int, error) {
+	return c.response.Write(data)
+}
+
+func (c *Context) String(status int, s string) error {
+	_, err := io.WriteString(c.response, s)
+	return err
+}
+
+func (c *Context) JSON(status int, data any) error {
+	c.response.Header().Set("Content-Type", "application/json")
+	c.response.WriteHeader(status)
+	return json.NewEncoder(c.response).Encode(data)
+}
+
+func (c *Context) XML(status int, data any) error {
+	c.response.Header().Set("Content-Type", "application/xml")
+	c.response.WriteHeader(status)
+	return xml.NewEncoder(c.response).Encode(data)
+}
+
+func (c *Context) Redirect(status int, urlStr string) {
+	http.Redirect(c.response, c.request, urlStr, status)
+}
+
+func (c *Context) Status(code int) {
+	c.response.WriteHeader(code)
+}
+
+// Helpers for common status responses
+
+func (c *Context) NoContent() {
+	c.Status(http.StatusNoContent)
+}
+
+func (c *Context) NotFound() {
+	c.Status(http.StatusNotFound)
+}
+
+func (c *Context) BadRequest(e error) error {
+	c.response.WriteHeader(http.StatusBadRequest)
+	_, err := c.response.Write([]byte(e.Error()))
+	return err
+}
+
+func (c *Context) InternalServerError(e error) error {
+	c.response.WriteHeader(http.StatusInternalServerError)
+	_, err := c.response.Write([]byte(e.Error()))
+	return err
+}
+
+// Context cancellation / deadline
+
+func (c *Context) Done() <-chan struct{} {
+	return c.ctx.Done()
+}
+
+func (c *Context) Err() error {
+	return c.ctx.Err()
+}
+
+// Request parsing helpers for common data types
+
+func (c *Context) ParamInt(name string) (int, error) {
+	v := c.Param(name)
+	return strconv.Atoi(v)
+}
+
+func (c *Context) ParamIntDefault(name string, defaultValue int) int {
+	v := c.Param(name)
+	if v == "" {
+		return defaultValue
+	}
+	i, err := strconv.Atoi(v)
+	if err != nil {
+		return defaultValue
+	}
+	return i
+}
+
+func (c *Context) QueryInt(name string) (int, error) {
+	v := c.Query(name)
+	return strconv.Atoi(v)
+}
+
+func (c *Context) QueryIntDefault(name string, defaultValue int) int {
+	v := c.Query(name)
+	if v == "" {
+		return defaultValue
+	}
+	i, err := strconv.Atoi(v)
+	if err != nil {
+		return defaultValue
+	}
+	return i
+}
+
+func (c *Context) FormInt(name string) (int, error) {
+	v := c.FormValue(name)
+	return strconv.Atoi(v)
+}
+
+func (c *Context) FormIntDefault(name string, defaultValue int) int {
+	v := c.FormValue(name)
+	if v == "" {
+		return defaultValue
+	}
+	i, err := strconv.Atoi(v)
+	if err != nil {
+		return defaultValue
+	}
+	return i
+}
+
+// Context management helpers
+
+func (c *Context) SetValue(key, val any) {
+	c.ctx = context.WithValue(c.ctx, key, val)
+}
+
+func (c *Context) Value(key any) any {
+	return c.ctx.Value(key)
+}

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module git.trcreatives.com/GoKitten/go-wisp
+
+go 1.24.3
+
+require github.com/julienschmidt/httprouter v1.3.0

+ 2 - 0
go.sum

@@ -0,0 +1,2 @@
+github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=

+ 90 - 0
group.go

@@ -0,0 +1,90 @@
+package wisp
+
+import (
+	"net/http"
+	"path"
+	"slices"
+	"strings"
+)
+
+type Group struct {
+	prefix      string
+	parent      *Wisp
+	middlewares []Middleware
+}
+
+func (g *Group) Use(m ...Middleware) {
+	g.middlewares = append(g.middlewares, m...)
+}
+
+func (g *Group) GET(path string, h Handler, mws ...Middleware) {
+	g.handle(http.MethodGet, path, h, mws...)
+}
+
+func (g *Group) POST(path string, h Handler, mws ...Middleware) {
+	g.handle(http.MethodPost, path, h, mws...)
+}
+
+func (g *Group) PUT(path string, h Handler, mws ...Middleware) {
+	g.handle(http.MethodPut, path, h, mws...)
+}
+
+func (g *Group) DELETE(path string, h Handler, mws ...Middleware) {
+	g.handle(http.MethodDelete, path, h, mws...)
+}
+
+func (g *Group) PATCH(path string, h Handler, mws ...Middleware) {
+	g.handle(http.MethodPatch, path, h, mws...)
+}
+
+func (g *Group) OPTIONS(path string, h Handler, mws ...Middleware) {
+	g.handle(http.MethodOptions, path, h, mws...)
+}
+
+func (g *Group) HEAD(path string, h Handler, mws ...Middleware) {
+	g.handle(http.MethodHead, path, h, mws...)
+}
+
+func (g *Group) Static(relativePath, root string) {
+	if !strings.HasPrefix(relativePath, "/") {
+		relativePath = "/" + relativePath
+	}
+
+	fullPath := path.Join(g.prefix, relativePath)
+
+	fileServer := http.StripPrefix(fullPath, http.FileServer(http.Dir(root)))
+
+	handler := func(ctx *Context) error {
+		fileServer.ServeHTTP(ctx.Response(), ctx.Request())
+		return nil
+	}
+
+	g.GET(relativePath+"/*filepath", handler)
+}
+
+func (g *Group) handle(method, path string, h Handler, routeMws ...Middleware) {
+	allMws := slices.Clone(g.parent.middlewares)
+	allMws = append(allMws, g.middlewares...)
+	allMws = append(allMws, routeMws...)
+
+	h = g.parent.chainMiddlewares(h, allMws)
+	fullPath := g.prefix + path
+	finalHandler := g.parent.makeHTTPRouterHandler(h)
+
+	switch method {
+	case http.MethodGet:
+		g.parent.router.GET(fullPath, finalHandler)
+	case http.MethodPost:
+		g.parent.router.POST(fullPath, finalHandler)
+	case http.MethodPut:
+		g.parent.router.PUT(fullPath, finalHandler)
+	case http.MethodDelete:
+		g.parent.router.DELETE(fullPath, finalHandler)
+	case http.MethodPatch:
+		g.parent.router.PATCH(fullPath, finalHandler)
+	case http.MethodOptions:
+		g.parent.router.OPTIONS(fullPath, finalHandler)
+	case http.MethodHead:
+		g.parent.router.HEAD(fullPath, finalHandler)
+	}
+}

+ 9 - 0
types.go

@@ -0,0 +1,9 @@
+package wisp
+
+type Handler func(ctx *Context) error
+type Middleware func(next Handler) Handler
+type ErrorHandler func(err error, ctx *Context) error
+
+func defaultErrorHandler(err error, ctx *Context) error {
+	return nil
+}

+ 136 - 0
wisp.go

@@ -0,0 +1,136 @@
+package wisp
+
+import (
+	"context"
+	"net/http"
+	"strings"
+
+	"github.com/julienschmidt/httprouter"
+)
+
+type Wisp struct {
+	router       *httprouter.Router
+	errorHandler ErrorHandler
+	middlewares  []Middleware
+}
+
+func New() *Wisp {
+	return &Wisp{
+		router:       httprouter.New(),
+		errorHandler: defaultErrorHandler,
+		middlewares:  []Middleware{},
+	}
+}
+
+func (w *Wisp) SetErrorHandler(errorHandler ErrorHandler) {
+	w.errorHandler = errorHandler
+}
+
+func (w *Wisp) Use(m ...Middleware) {
+	w.middlewares = append(w.middlewares, m...)
+}
+
+func (w *Wisp) Group(prefix string, middlewares ...Middleware) *Group {
+	return &Group{
+		prefix:      prefix,
+		parent:      w,
+		middlewares: middlewares,
+	}
+}
+
+func (w *Wisp) GET(path string, h Handler, mws ...Middleware) {
+	w.handle(http.MethodGet, path, h, mws...)
+}
+
+func (w *Wisp) POST(path string, h Handler, mws ...Middleware) {
+	w.handle(http.MethodPost, path, h, mws...)
+}
+
+func (w *Wisp) PUT(path string, h Handler, mws ...Middleware) {
+	w.handle(http.MethodPut, path, h, mws...)
+}
+
+func (w *Wisp) DELETE(path string, h Handler, mws ...Middleware) {
+	w.handle(http.MethodDelete, path, h, mws...)
+}
+
+func (w *Wisp) PATCH(path string, h Handler, mws ...Middleware) {
+	w.handle(http.MethodPatch, path, h, mws...)
+}
+
+func (w *Wisp) OPTIONS(path string, h Handler, mws ...Middleware) {
+	w.handle(http.MethodOptions, path, h, mws...)
+}
+
+func (w *Wisp) HEAD(path string, h Handler, mws ...Middleware) {
+	w.handle(http.MethodHead, path, h, mws...)
+}
+
+func (w *Wisp) Static(path, root string) {
+	if !strings.HasSuffix(path, "/*filepath") {
+		if strings.HasSuffix(path, "/") {
+			path += "*filepath"
+		} else {
+			path += "/*filepath"
+		}
+	}
+	fileServer := http.StripPrefix(strings.TrimSuffix(path, "/*filepath"), http.FileServer(http.Dir(root)))
+	w.router.GET(path, func(wr http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+		fileServer.ServeHTTP(wr, r)
+	})
+}
+
+func (w *Wisp) Start(addr string) error {
+	return http.ListenAndServe(addr, w.router)
+}
+
+func (w *Wisp) StartWithTLS(addr, certFile, keyFile string) error {
+	return http.ListenAndServeTLS(addr, certFile, keyFile, w.router)
+}
+
+func (w *Wisp) handle(method, path string, h Handler, mws ...Middleware) {
+	all := append(w.middlewares, mws...)
+	final := w.chainMiddlewares(h, all)
+	w.register(method, path, w.makeHTTPRouterHandler(final))
+}
+
+func (w *Wisp) register(method, path string, h httprouter.Handle) {
+	switch method {
+	case http.MethodGet:
+		w.router.GET(path, h)
+	case http.MethodPost:
+		w.router.POST(path, h)
+	case http.MethodPut:
+		w.router.PUT(path, h)
+	case http.MethodDelete:
+		w.router.DELETE(path, h)
+	case http.MethodPatch:
+		w.router.PATCH(path, h)
+	case http.MethodOptions:
+		w.router.OPTIONS(path, h)
+	case http.MethodHead:
+		w.router.HEAD(path, h)
+	}
+}
+
+func (w *Wisp) chainMiddlewares(h Handler, mws []Middleware) Handler {
+	for i := len(mws) - 1; i >= 0; i-- {
+		h = mws[i](h)
+	}
+	return h
+}
+
+func (w *Wisp) makeHTTPRouterHandler(h Handler) httprouter.Handle {
+	return func(wr http.ResponseWriter, r *http.Request, p httprouter.Params) {
+		context := &Context{
+			response: wr,
+			request:  r,
+			params:   p,
+			ctx:      context.Background(),
+		}
+
+		if err := h(context); err != nil {
+			w.errorHandler(err, context)
+		}
+	}
+}