go-web/framework/context.go

170 lines
3.8 KiB
Go

package framework
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"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
handlers []ControllerHandler
// current handler index
index int
params map[string]any
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{},
index: -1, // will be set to 0 when at the beginning
}
}
// {{{ 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)
}
// Next runs the next function in the function chain
func (ctx *Context) Next() error {
ctx.index++
if ctx.index >= len(ctx.handlers) {
// This is the end of the chain
return nil
}
// Run this handler
if err := ctx.handlers[ctx.index](ctx); err != nil {
return err
}
return nil
}
// SetHandlers sets handlers for context
func (ctx *Context) SetHandlers(handlers []ControllerHandler) {
ctx.handlers = handlers
}
// }}}
// {{{ Implements request functions
// {{{ 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
}
// }}}
// }}}