package framework import ( "context" "encoding/json" "errors" "io" "net/http" "net/url" "strconv" "sync" "time" ) // Context type is the customized context of Araneae framework // // It packages the internal context.Context with basic "wr" couple. type Context struct { ctx context.Context request *http.Request responseWriter http.ResponseWriter handler ControllerHandler hasTimeout bool writerMux *sync.Mutex } // NewContext create a new context func NewContext(w http.ResponseWriter, r *http.Request) *Context { return &Context{ ctx: r.Context(), request: r, responseWriter: w, writerMux: &sync.Mutex{}, } } // {{{ Basic functions // WriterMux returns the writer mutex. // // This is useful when goroutines concurrently write into responseWriter, // while at the same time we are writing into the responseWriter for a // panic or timeout. // We can protect it at the context level. func (ctx *Context) WriterMux() *sync.Mutex { return ctx.writerMux } // GetRequest returns the original request func (ctx *Context) GetRequest() *http.Request { return ctx.request } // GetResponseWriter returns the original response writer func (ctx *Context) GetResponseWriter() http.ResponseWriter { return ctx.responseWriter } // SetHasTimeout indicates that the context has timeout. // // So that other goroutines won't write into the responseWriter anymore func (ctx *Context) SetHasTimeout() { ctx.hasTimeout = true } // HasTimeout returns whether the context has timeout. func (ctx *Context) HasTimeout() bool { return ctx.hasTimeout } // }}} // {{{ Implements context interface // BaseContext return a request default Context func (ctx *Context) BaseContext() context.Context { return ctx.request.Context() } // Done calls the base function func (ctx *Context) Done() <-chan struct{} { return ctx.BaseContext().Done() } // Deadline calls the base function func (ctx *Context) Deadline() (deadline time.Time, ok bool) { return ctx.BaseContext().Deadline() } // Err calls the base function func (ctx *Context) Err() error { return ctx.BaseContext().Err() } // Value calls the base function func (ctx *Context) Value(key any) any { return ctx.BaseContext().Value(key) } // }}} // {{{ 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") } // }}} // {{{ 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 } // }}} // }}}