// Package server implements the Monkey Lang Web Server that implements
// a web-based Playground for testing out and trying Monkey on the Web.
package server
import (
"context"
"embed"
"errors"
"fmt"
"io/fs"
"net/http"
"time"
log "github.com/sirupsen/logrus"
"go.mills.io/logger"
"go.mills.io/router"
)
const (
// DefaultBind is the default [
]: to bind to
DefaultBind = ":8000"
)
//go:embed static/*
var staticFS embed.FS
// Option is a function type that configures the server
type Option func(svr *Server) error
// WithBind configures the server with a bind interface and port in the form of []:
// For example: WithBind(":8000")
func WithBind(bind string) Option {
return func(svr *Server) error {
svr.bind = bind
return nil
}
}
type Server struct {
bind string
routes *router.Router
}
func NewServer(opts ...Option) (*Server, error) {
routes := router.New()
routes.Use(router.Middleware(func(next router.Handle) router.Handle {
return func(w http.ResponseWriter, r *http.Request, p router.Params) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
next(w, r, p)
}
}))
svr := &Server{
bind: DefaultBind,
routes: routes,
}
for _, opt := range opts {
if err := opt(svr); err != nil {
return nil, fmt.Errorf("error configuring server: %w", err)
}
}
svr.initRoutes()
return svr, nil
}
func (s *Server) initRoutes() {
fs, err := fs.Sub(staticFS, "static")
if err != nil {
log.Fatal(err)
}
s.routes.ServeFiles("/*filepath", http.FS(fs))
s.routes.POST("/api/run", s.runHandler())
s.routes.POST("/api/format", s.formatHandler())
}
// Run runs the server with the provided context and shuts down when the context is cancelled
func (s *Server) Run(ctx context.Context) error {
svr := &http.Server{
Addr: s.bind,
Handler: logger.New(logger.Options{
Prefix: "monkey",
RemoteAddressHeaders: []string{"X-Forwarded-For"},
}).Handler(s.routes),
}
go func() {
if err := svr.ListenAndServe(); err != nil {
if !errors.Is(err, http.ErrServerClosed) {
log.WithError(err).Error("svr.ListenAndServe error")
}
}
}()
select {
case <-ctx.Done():
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
if err := svr.Shutdown(ctx); err != nil {
return fmt.Errorf("error shutting down server: %w", err)
}
return nil
}
}