package server import ( "io/ioutil" "net/http" "strings" "time" lib "git.sr.ht/~cco/go-scopes" "git.sr.ht/~cco/go-scopes/config" "git.sr.ht/~cco/go-scopes/core" "git.sr.ht/~cco/go-scopes/core/action" "git.sr.ht/~cco/go-scopes/core/message" "git.sr.ht/~cco/go-scopes/logging" "github.com/gin-gonic/gin" ) type ServerState struct { server *http.Server } func Start(cfg *Cfg) lib.Proc { return func(ctx lib.Context) { gin.SetMode(gin.ReleaseMode) lib.GetCfg[*config.BaseCfg](ctx).WithDoneHandler(HandleDone) Serve(ctx, cfg) lib.RunCtx(ctx, core.Listen) } } func HandleDone(ctx lib.Context) bool { lib.GetState[*ServerState](ctx).server.Shutdown(ctx) return false } func Serve(ctx lib.Context, cfg *Cfg) { r := gin.New() r.Use(gin.Recovery()) r.Use(Logger(ctx)) if cfg.Addr == "" { if cfg.Port == "" { cfg.Port = "8123" } cfg.Addr = ":" + cfg.Port } for _, rcfg := range cfg.routes { setRoute(ctx, rcfg, r) } srv := &http.Server{Addr: cfg.Addr, Handler: r} ctx.WithState(&ServerState{server: srv}) logging.Info(ctx).Str("addr", cfg.Addr).Msg("server.Serve") lib.RunCtx(ctx, func(ctx lib.Context) { srv.ListenAndServe() }) } func setRoute(ctx lib.Context, rcfg routeCfg, r *gin.Engine) { switch spec := rcfg.spec.(type) { case *fsSpec: r.Static(rcfg.path, spec.docRoot) case *mhSpec: r.Match(rcfg.methods, rcfg.path+"/*msg", func(c *gin.Context) { handleMsg(ctx, spec, c) }) } } // scopes standard request (= message) handler implementation func handleMsg(ctx lib.Context, cfg *mhSpec, gc *gin.Context) { head := strings.Split(gc.Param("msg"), "/")[1:] cctx := ctx.ChildContext(cfg) msg := message.New(head...).WithSender(cctx) if body, _ := ioutil.ReadAll(gc.Request.Body); len(body) > 0 { pl := message.PayloadFromJson[lib.Data](string(body)) msg.WithPayload(pl) } logging.DebugM(cctx, msg).Msg("server.handleMsg") var proc msgProc for _, act := range action.Select(cctx, msg) { act.Handle() proc = act.Spec().(*actionSpec).proc } code, data := proc(cctx, msg) gc.JSON(code, data) } func Async(ctx lib.Context, msg lib.Message) (int, lib.Data) { return http.StatusOK, lib.Map{"status": "OK"} } func Call(clbl callable) msgProc { return func(ctx lib.Context, msg lib.Message) (int, lib.Data) { data, err := clbl(ctx, msg) if err != nil { data = lib.Map{"error": err.Error()} return http.StatusInternalServerError, data } return http.StatusOK, data } } func Sync(timeout int) msgProc { return func(ctx lib.Context, msg lib.Message) (int, lib.Data) { return sync(ctx, msg, timeout) } } func sync(ctx lib.Context, msg lib.Message, to int) (int, lib.Data) { timeout := time.Duration(to) * time.Second select { case msg := <-ctx.Mailbox(): return http.StatusOK, msg.Payload().Data() case <-time.After(timeout): return http.StatusNoContent, nil case <-ctx.Done(): return http.StatusGone, nil } return http.StatusNoContent, nil } // message processing function types type msgProc func(lib.Context, lib.Message) (int, lib.Data) type callable func(lib.Context, lib.Message) (lib.Data, error)