Compare commits

...

4 Commits

Author SHA1 Message Date
Muyao CHEN
7be01627fa context: Get params from url 2024-09-26 19:01:34 +02:00
Muyao CHEN
cb1ea9f701 request/response: add implementations for response writer 2024-09-26 09:32:44 +02:00
Muyao CHEN
8f4b378fdd request/response: add more implementations 2024-09-25 22:45:25 +02:00
Muyao CHEN
099d1aeb0f request/response: add interfaces and implementations 2024-09-25 22:20:00 +02:00
7 changed files with 589 additions and 181 deletions

View File

@ -2,12 +2,7 @@ package framework
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strconv"
"sync"
"time"
)
@ -24,6 +19,8 @@ type Context struct {
// current handler index
index int
params map[string]string
hasTimeout bool
writerMux *sync.Mutex
}
@ -120,172 +117,8 @@ func (ctx *Context) SetHandlers(handlers []ControllerHandler) {
ctx.handlers = handlers
}
// }}}
// {{{ Implements request functions
// {{{ Request URI
// QueryInt gets an int value from the query request
func (ctx *Context) QueryInt(key string, defval int) (int, error) {
params, err := ctx.QueryAll()
if err != nil {
return defval, err
}
if vals, ok := params[key]; ok {
len := len(vals)
if len > 0 {
intval, err := strconv.Atoi(vals[len-1]) // return the last elem
if err != nil {
return defval, err
}
return intval, nil
}
}
return defval, errors.New("key not found")
}
// QueryString gets a string value from the query request
func (ctx *Context) QueryString(key string, defval string) (string, error) {
params, err := ctx.QueryAll()
if err != nil {
return defval, err
}
if vals, ok := params[key]; ok {
len := len(vals)
if len > 0 {
return vals[len-1], nil // return the last elem
}
}
return defval, errors.New("key not found")
}
// QueryArray gets an array of string values from the query request
func (ctx *Context) QueryArray(key string, defval []string) ([]string, error) {
params, err := ctx.QueryAll()
if err != nil {
return defval, err
}
if vals, ok := params[key]; ok {
return vals, nil // return the last elem
}
return defval, errors.New("key not found")
}
// QueryAll returns all queries in a request URL
func (ctx *Context) QueryAll() (url.Values, error) {
if ctx.request != nil {
return map[string][]string(ctx.request.URL.Query()), nil
}
return url.Values{}, errors.New("missing request in the context")
func (ctx *Context) SetParams(params map[string]string) {
ctx.params = params
}
// }}}
// {{{ Post form
// FormInt gets an int value from the submitted form
func (ctx *Context) FormInt(key string, defval int) (int, error) {
vals, err := ctx.FormAll()
if err != nil {
return defval, err
}
valStrs, ok := vals[key]
if !ok {
return defval, errors.New("key not found")
}
valInt, err := strconv.Atoi(valStrs[0]) // Get the first one as result
if err != nil {
return defval, err
}
return valInt, nil
}
// FormString gets a string value from the submitted form
func (ctx *Context) FormString(key string, defval string) (string, error) {
vals, err := ctx.FormAll()
if err != nil {
return defval, err
}
valStrs, ok := vals[key]
if !ok {
return defval, errors.New("key not found")
}
return valStrs[0], nil
}
// FormArray gets an array of string values from the submitted form
func (ctx *Context) FormArray(key string, defval []string) ([]string, error) {
vals, err := ctx.FormAll()
if err != nil {
return defval, err
}
valStrs, ok := vals[key]
if !ok {
return defval, errors.New("key not found")
}
return valStrs, nil
}
// FormAll gets everything from the submitted form
func (ctx *Context) FormAll() (url.Values, error) {
if ctx.request != nil {
err := ctx.request.ParseForm()
if err != nil {
return url.Values{}, err
}
return ctx.request.PostForm, err
}
return url.Values{}, errors.New("missing request in the context")
}
// }}}
// {{{ application/json
// ReadJSON binds the request JSON body to an object.
//
// A pointer of obj should be passed.
func (ctx *Context) ReadJSON(obj any) error {
if ctx.request == nil {
return errors.New("missing request in the context")
}
dec := json.NewDecoder(ctx.request.Body)
err := dec.Decode(obj)
if err != nil {
return err
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
return errors.New("body must have only a single JSON value")
}
return nil
}
// WriteJSON send back an object in JSON format with the status code
func (ctx *Context) WriteJSON(status int, obj any) error {
// There is a timeout, some error message data must have already been
// written to the output. Stop writing anything into the responseWriter.
if ctx.HasTimeout() {
return nil
}
data, err := json.Marshal(obj)
if err != nil {
return err
}
ctx.responseWriter.Header().Set("Content-type", "application/json")
ctx.responseWriter.WriteHeader(status)
_, err = ctx.responseWriter.Write(data)
if err != nil {
return err
}
return nil
}
// }}}
// }}}

View File

@ -66,7 +66,7 @@ func (c *Core) Use(middlewares ...ControllerHandler) {
}
// FindRouteByRequest finds route using the request
func (c *Core) FindRouteByRequest(r *http.Request) []ControllerHandler {
func (c *Core) FindRouteByRequest(r *http.Request) *node {
upperMethod := strings.ToUpper(r.Method)
mapper, ok := c.router[upperMethod]
@ -75,13 +75,13 @@ func (c *Core) FindRouteByRequest(r *http.Request) []ControllerHandler {
return nil
}
controllers := mapper.FindRoute(r.URL.Path)
if controllers == nil {
node := mapper.FindRoute(r.URL.Path)
if node == nil {
log.Printf("URI %q is not recognized\n", r.URL.Path)
return nil
}
return controllers
return node
}
func (c *Core) Group(prefix string) IGroup {
@ -97,13 +97,16 @@ func (c *Core) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := NewContext(w, r)
handlers := c.FindRouteByRequest(r)
if handlers == nil {
node := c.FindRouteByRequest(r)
if node == nil {
ctx.WriteJSON(http.StatusNotFound, "Request not found")
return
}
ctx.SetHandlers(handlers)
params := node.ParseParamsFromEndNode(r.URL.Path)
ctx.SetParams(params)
ctx.SetHandlers(node.handlers)
if err := ctx.Next(); err != nil {
ctx.WriteJSON(http.StatusInternalServerError, "Internal error")

411
framework/request.go Normal file
View File

@ -0,0 +1,411 @@
package framework
import (
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"io"
"mime/multipart"
"net/url"
"github.com/spf13/cast"
)
type IRequest interface {
// url query
// e.g. foo.com?a=1&b=bar&c[]=bar
QueryAll(key string) url.Values
QueryInt(key string, defval int) (int, bool)
QueryInt64(key string, defval int64) (int64, bool)
QueryFloat32(key string, defval float32) (float32, bool)
QueryFloat64(key string, defval float64) (float64, bool)
QueryBool(key string, defval bool) (bool, bool)
QueryString(key string, defval string) (string, bool)
QueryStringSlice(key string, defval []string) ([]string, bool)
// url params
// e.g. /book/:id
Param(key string) any
ParamInt(key string, defval int) (int, bool)
ParamInt64(key string, defval int64) (int64, bool)
ParamFloat32(key string, defval float32) (float32, bool)
ParamFloat64(key string, defval float64) (float64, bool)
ParamBool(key string, defval bool) (bool, bool)
ParamString(key string, defval string) (string, bool)
// form
FormAll(key string) url.Values
FormInt(key string, defval int) (int, bool)
FormInt64(key string, defval int64) (int64, bool)
FormFloat32(key string, defval float32) (float32, bool)
FormFloat64(key string, defval float64) (float64, bool)
FormBool(key string, defval bool) (bool, bool)
FormString(key string, defval string) (string, bool)
FormStringSlice(key string, defval []string) ([]string, bool)
FormFile(key string) (*multipart.FileHeader, error)
// JSON body
BindJSON(obj any) error
// XML body
BindXML(obj any) error
// RAW body
GetRawData() ([]byte, error)
// Basic informations
Uri() string
Method() string
Host() string
ClientIP() string
// Header
Headers() map[string][]string
Header(key string) (string, bool)
// Cookie
Cookies() map[string]string
Cookie(key string) (string, bool)
}
// {{{ url query
// QueryAll returns all queries in a request URL
func (ctx *Context) QueryAll() url.Values {
if ctx.request != nil {
return map[string][]string(ctx.request.URL.Query())
}
return url.Values{}
}
// QueryInt gets an int value from the query request
func (ctx *Context) QueryInt(key string, defval int) (int, bool) {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToInt(vals[0]), true
}
}
return defval, false
}
func (ctx *Context) QueryInt64(key string, defval int64) (int64, bool) {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToInt64(vals[0]), true
}
}
return defval, false
}
func (ctx *Context) QueryBool(key string, defval bool) (bool, bool) {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToBool(vals[0]), true
}
}
return defval, false
}
func (ctx *Context) QueryFloat32(key string, defval float32) (float32, bool) {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToFloat32(vals[0]), true
}
}
return defval, false
}
func (ctx *Context) QueryFloat64(key string, defval float64) (float64, bool) {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToFloat64(vals[0]), true
}
}
return defval, false
}
// QueryString gets a string value from the query request
func (ctx *Context) QueryString(key string, defval string) (string, bool) {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToString(vals[0]), true
}
}
return defval, false
}
// QueryArray gets an array of string values from the query request
func (ctx *Context) QueryStringSlice(key string, defval []string) ([]string, bool) {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
return cast.ToStringSlice(vals[0]), true
}
return defval, false
}
// }}}
// {{{ url params
func (ctx *Context) Param(key string) any {
if ctx.params != nil {
if val, ok := ctx.params[key]; ok {
return val
}
}
return nil
}
func (ctx *Context) ParamInt(key string, def int) (int, bool) {
if val := ctx.Param(key); val != nil {
return cast.ToInt(val), true
}
return def, false
}
func (ctx *Context) ParamInt64(key string, def int64) (int64, bool) {
if val := ctx.Param(key); val != nil {
return cast.ToInt64(val), true
}
return def, false
}
func (ctx *Context) ParamFloat64(key string, def float64) (float64, bool) {
if val := ctx.Param(key); val != nil {
return cast.ToFloat64(val), true
}
return def, false
}
func (ctx *Context) ParamFloat32(key string, def float32) (float32, bool) {
if val := ctx.Param(key); val != nil {
return cast.ToFloat32(val), true
}
return def, false
}
func (ctx *Context) ParamBool(key string, def bool) (bool, bool) {
if val := ctx.Param(key); val != nil {
return cast.ToBool(val), true
}
return def, false
}
func (ctx *Context) ParamString(key string, def string) (string, bool) {
if val := ctx.Param(key); val != nil {
return cast.ToString(val), true
}
return def, false
}
// }}}
// {{{ Post form
// FormAll gets everything from the submitted form
func (ctx *Context) FormAll() url.Values {
if ctx.request != nil {
_ = ctx.request.ParseForm()
return ctx.request.PostForm
}
return url.Values{}
}
// FormInt gets an int value from the submitted form
func (ctx *Context) FormInt(key string, defval int) (int, bool) {
params := ctx.FormAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToInt(vals[0]), true
}
}
return defval, false
}
func (ctx *Context) FormInt64(key string, defval int64) (int64, bool) {
params := ctx.FormAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToInt64(vals[0]), true
}
}
return defval, false
}
func (ctx *Context) FormBool(key string, defval bool) (bool, bool) {
params := ctx.FormAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToBool(vals[0]), true
}
}
return defval, false
}
func (ctx *Context) FormFloat32(key string, defval float32) (float32, bool) {
params := ctx.FormAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToFloat32(vals[0]), true
}
}
return defval, false
}
func (ctx *Context) FormFloat64(key string, defval float64) (float64, bool) {
params := ctx.FormAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToFloat64(vals[0]), true
}
}
return defval, false
}
func (ctx *Context) FormString(key string, defval string) (string, bool) {
params := ctx.FormAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
return cast.ToString(vals[0]), true
}
}
return defval, false
}
func (ctx *Context) FormStringSlice(key string, defval []string) ([]string, bool) {
params := ctx.FormAll()
if vals, ok := params[key]; ok {
return cast.ToStringSlice(vals[0]), true
}
return defval, false
}
// }}}
// {{{ type binder
var (
ErrNoRequest = errors.New("missing request in the context")
ErrNotSingleObj = errors.New("body must have only a single value")
)
// JSON body
func (ctx *Context) BindJSON(obj any) error {
if ctx.request == nil {
return ErrNoRequest
}
dec := json.NewDecoder(ctx.request.Body)
err := dec.Decode(obj)
if err != nil {
return err
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
return ErrNotSingleObj
}
return nil
}
// XML body
func (ctx *Context) BindXML(obj any) error {
if ctx.request == nil {
return ErrNoRequest
}
dec := xml.NewDecoder(ctx.request.Body)
err := dec.Decode(obj)
if err != nil {
return err
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
return ErrNotSingleObj
}
return nil
}
// RAW body
func (ctx *Context) GetRawData() ([]byte, error) {
if ctx.request == nil {
return []byte{}, ErrNoRequest
}
body, err := io.ReadAll(ctx.request.Body)
if err != nil {
return []byte{}, err
}
/* Restore the body (io.ReadCloser) to it's original state */
ctx.request.Body = io.NopCloser(bytes.NewBuffer(body))
return body, nil
}
// }}}
// {{{ Basic informations
func (ctx *Context) Uri() string {
return ctx.request.RequestURI
}
func (ctx *Context) Method() string {
return ctx.request.Method
}
func (ctx *Context) Host() string {
return ctx.request.Host
}
func (ctx *Context) ClientIP() string {
r := ctx.request
ipAddress := r.Header.Get("X-Real-Ip")
if ipAddress == "" {
ipAddress = r.Header.Get("X-Forwarded-For")
}
if ipAddress == "" {
ipAddress = r.RemoteAddr
}
return ipAddress
}
// }}}
// {{{ Headers
// Header
func (ctx *Context) Headers() map[string][]string {
return ctx.request.Header
}
func (ctx *Context) Header(key string) (string, bool) {
vals := ctx.request.Header.Values(key)
if vals == nil || len(vals) <= 0 {
return "", false
}
return vals[0], true
}
// }}}
// {{{ Cookies
// Cookies gets cookie key-value pairs
func (ctx *Context) Cookies() map[string]string {
cookies := ctx.request.Cookies()
ret := map[string]string{}
for _, c := range cookies {
ret[c.Name] = c.Value
}
return ret
}
func (ctx *Context) Cookie(key string) (string, bool) {
cookies := ctx.Cookies()
if val, ok := cookies[key]; ok {
return val, true
}
return "", false
}
// }}}

123
framework/response.go Normal file
View File

@ -0,0 +1,123 @@
package framework
import (
"encoding/json"
"encoding/xml"
"fmt"
"html/template"
"net/http"
"net/url"
)
type IResponse interface {
WriteJSON(status int, obj any) IResponse
WriteXML(status int, obj any) IResponse
WriteHTML(status int, filepath string, obj any) IResponse
WriteText(format string, values ...any) IResponse
Redirect(path string) IResponse
SetHeader(key string, val string) IResponse
SetCookie(
key string,
val string,
maxAge int,
path, domain string,
secure, httpOnly bool,
) IResponse
SetStatus(code int) IResponse
// set 200
SetOkStatus() IResponse
}
func (ctx *Context) WriteJSON(status int, obj any) IResponse {
// There is a timeout, some error message data must have already been
// written to the output. Stop writing anything into the responseWriter.
if ctx.HasTimeout() {
return nil
}
data, err := json.Marshal(obj)
if err != nil {
return ctx.SetStatus(http.StatusInternalServerError)
}
ctx.responseWriter.Header().Set("Content-type", "application/json")
ctx.responseWriter.WriteHeader(status)
_, _ = ctx.responseWriter.Write(data)
return ctx
}
func (ctx *Context) WriteXML(status int, obj any) IResponse {
// There is a timeout, some error message data must have already been
// written to the output. Stop writing anything into the responseWriter.
if ctx.HasTimeout() {
return nil
}
data, err := xml.Marshal(obj)
if err != nil {
return ctx.SetStatus(http.StatusInternalServerError)
}
ctx.responseWriter.Header().Set("Content-type", "application/json")
ctx.responseWriter.WriteHeader(status)
_, _ = ctx.responseWriter.Write(data)
return ctx
}
func (ctx *Context) WriteHTML(status int, filepath string, obj any) IResponse {
ctx.SetHeader("Content-Type", "application/html")
t, _ := template.New("output").ParseFiles(filepath)
t.Execute(ctx.responseWriter, obj)
return ctx
}
func (ctx *Context) WriteText(format string, values ...any) IResponse {
out := fmt.Sprintf(format, values...)
ctx.SetHeader("Content-Type", "application/text")
ctx.responseWriter.Write([]byte(out))
return ctx
}
func (ctx *Context) Redirect(path string) IResponse {
http.Redirect(ctx.responseWriter, ctx.request, path, http.StatusTemporaryRedirect)
return ctx
}
func (ctx *Context) SetHeader(key string, val string) IResponse {
ctx.responseWriter.Header().Add(key, val)
return ctx
}
func (ctx *Context) SetCookie(
key string,
val string,
maxAge int,
path, domain string,
secure, httpOnly bool,
) IResponse {
if path == "" {
path = "/"
}
http.SetCookie(ctx.responseWriter, &http.Cookie{
Name: key,
Value: url.QueryEscape(val),
MaxAge: maxAge,
Path: path,
Domain: domain,
SameSite: http.SameSiteDefaultMode,
Secure: secure,
HttpOnly: httpOnly,
})
return ctx
}
func (ctx *Context) SetStatus(code int) IResponse {
ctx.responseWriter.WriteHeader(code)
return ctx
}
// set 200
func (ctx *Context) SetOkStatus() IResponse {
ctx.responseWriter.WriteHeader(http.StatusOK)
return ctx
}

View File

@ -14,11 +14,11 @@ func NewTrie() *Trie {
return &Trie{root: newNode("")}
}
func (t *Trie) FindRoute(uri string) []ControllerHandler {
func (t *Trie) FindRoute(uri string) *node {
uri = strings.ToUpper(uri)
uri = strings.TrimPrefix(uri, "/")
if uri == "" {
return t.root.handlers
return t.root
}
found := t.root.findRoute(uri)
@ -26,7 +26,7 @@ func (t *Trie) FindRoute(uri string) []ControllerHandler {
return nil
}
return found.handlers
return found
}
func (t *Trie) AddRouter(uri string, handlers []ControllerHandler) error {
@ -58,6 +58,29 @@ type node struct {
segment string
handlers []ControllerHandler
children []*node
parent *node
}
func (n *node) ParseParamsFromEndNode(uri string) map[string]string {
ret := map[string]string{}
uri = strings.ToUpper(uri)
uri = strings.TrimPrefix(uri, "/")
if uri == "" {
// root
return ret
}
segments := strings.Split(uri, "/")
cnt := len(segments)
cur := n
for i := cnt - 1; i >= 0; i-- {
if isWildcard(cur.segment) {
// set params
ret[cur.segment[1:]] = segments[i]
}
cur = cur.parent
}
return ret
}
func newNode(segment string) *node {
@ -138,6 +161,7 @@ func (n *node) addRoute(uri string, handlers []ControllerHandler) error {
// create a new node
new := newNode(splitted[0])
new.parent = n
if isLast {
// this is the end
new.handlers = append(new.handlers, handlers...)

2
go.mod
View File

@ -1,3 +1,5 @@
module git.vinchent.xyz/vinchent/go-web
go 1.22.5
require github.com/spf13/cast v1.7.0

12
go.sum
View File

@ -0,0 +1,12 @@
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=