package main import ( "errors" "io" "log" "net/http" ) type HTTPRequest struct { request *http.Request route string debug bool result chan *HTTPResponse } type EvalRequest struct { code string result chan error } type HTTPResponse struct { Code int Headers map[string]string Body string } type Worker struct { read chan interface{} lua *Lua api LuaRef routes map[string]LuaRef started bool } func NewWorker(read chan interface{}) *Worker { return &Worker { read: read, routes: make(map[string]LuaRef), lua: &Lua{}, } } func (w *Worker) Start(filename string) error { if w.started { return errors.New("already started") } w.lua.Start() api, err := w.lua.Require(filename) if err != nil { return err } w.api = api err = w.initRoutes() if err != nil { return err } return nil } func (w *Worker) Listen() { for { resStack := w.lua.RestoreStackFunc() r := <- w.read switch r.(type) { case *HTTPRequest: r := r.(*HTTPRequest) // If in debug mode always use handlers from api table // instead of cached references. Makes it much easier // to hot-replace route handlers. if r.debug { w.lua.PushFromRef(w.api) w.lua.PushTableItem("routes") w.lua.PushTableItem(r.route) } else { w.lua.PushFromRef(w.routes[r.route]) } w.lua.PushString(r.request.Method) w.lua.PushString(r.request.URL.Path) fh := make(map[string]string) for k := range r.request.Header { fh[k] = r.request.Header.Get(k) } w.lua.PushTable(fh) body, err := io.ReadAll(r.request.Body) if err != nil { r.result <- &HTTPResponse { Code: 500, Headers: make(map[string]string), Body: "server error", } log.Println("could not read a request body") resStack() continue } w.lua.PushString(string(body)) w.lua.PCall(4, 3) code := w.lua.ToInt(-3) rbody := w.lua.ToString(-1) // Parse headers. headers := make(map[string]string) w.lua.Pop(1) w.lua.PushNil() for w.lua.Next() { if !w.lua.IsString(-2) || !w.lua.IsString(-2) { w.lua.Pop(1) continue } v := w.lua.ToString(-1) w.lua.Pop(1) // We must not pop the item key from the stack // because otherwise C.lua_next won't work // properly. k := w.lua.ToString(-1) headers[k] = v } r.result <- &HTTPResponse { Code: int(code), Headers: headers, Body: rbody, } case *EvalRequest: r := r.(*EvalRequest) err := w.lua.LoadString(r.code) r.result <- err default: log.Fatal("unknown request") } resStack() } } func (w *Worker) Request( route string, r *http.Request, debug bool, ) chan *HTTPResponse { res := make(chan *HTTPResponse) w.read <- &HTTPRequest{ request: r, route: route, debug: debug, result: res, } return res } func (w *Worker) Eval(code string) chan error { res := make(chan error) w.read <- &EvalRequest{code: code, result: res} return res } func (w *Worker) ListRoutes() []string { res := []string{} for route, _ := range w.routes { res = append(res, route) } return res } func (w *Worker) Stop() { w.lua.Close() } func (w *Worker) initRoutes() error { defer w.lua.RestoreStackFunc()() w.lua.PushFromRef(w.api) w.lua.PushTableItem("routes") if !w.lua.IsTable(-1) { return errors.New("\"routes\" must be a table") } w.lua.PushNil() for w.lua.Next() { if !w.lua.IsString(-2) || !w.lua.IsFunction(-1) { w.lua.Pop(1) continue } handlerRef := w.lua.PopToRef() // we must not pop the item key from the stack because // otherwise C.lua_next won't work properly route := w.lua.ToString(-1) w.routes[route] = handlerRef } w.lua.Pop(2) return nil }