package web import ( "bufio" "fmt" "io" "log" "net" "os" "os/signal" "strings" "sync" "syscall" "git.akyoto.dev/go/router" ) // Server is the interface for an HTTP server. type Server interface { Get(path string, handler Handler) Request(method string, path string, body io.Reader) Response Router() *router.Router[Handler] Run(address string) error Use(handlers ...Handler) } // server is an HTTP server. type server struct { pool sync.Pool handlers []Handler router *router.Router[Handler] errorHandler func(Context, error) } // NewServer creates a new HTTP server. func NewServer() Server { r := &router.Router[Handler]{} s := &server{ router: r, handlers: []Handler{ func(c Context) error { ctx := c.(*context) handler := r.LookupNoAlloc(ctx.request.method, ctx.request.path, ctx.request.addParameter) if handler == nil { ctx.SetStatus(404) return nil } return handler(c) }, }, errorHandler: func(ctx Context, err error) { log.Println(ctx.Request().Path(), err) }, } s.pool.New = func() any { return s.newContext() } return s } // Get registers your function to be called when the given GET path has been requested. func (s *server) Get(path string, handler Handler) { s.Router().Add("GET", path, handler) } // Request performs a synthetic request and returns the response. // This function keeps the response in memory so it's slightly slower than a real request. // However it is very useful inside tests where you don't want to spin up a real web server. func (s *server) Request(method string, path string, body io.Reader) Response { ctx := s.newContext() ctx.method = method ctx.path = path s.handleRequest(ctx, method, path, io.Discard) return ctx.Response() } // Run starts the server on the given address. func (s *server) Run(address string) error { listener, err := net.Listen("tcp", address) if err != nil { return err } defer listener.Close() go func() { for { conn, err := listener.Accept() if err != nil { continue } go s.handleConnection(conn) } }() stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop return nil } // Router returns the router used by the server. func (s *server) Router() *router.Router[Handler] { return s.router } // Use adds handlers to your handlers chain. func (s *server) Use(handlers ...Handler) { last := s.handlers[len(s.handlers)-1] s.handlers = append(s.handlers[:len(s.handlers)-1], handlers...) s.handlers = append(s.handlers, last) } // handleConnection handles an accepted connection. func (s *server) handleConnection(conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) for { message, err := reader.ReadString('\n') if err != nil { return } space := strings.IndexByte(message, ' ') if space <= 0 { continue } method := message[:space] if method != "GET" { continue } lastSpace := strings.LastIndexByte(message, ' ') if lastSpace == -1 { lastSpace = len(message) } path := message[space+1 : lastSpace] ctx := s.pool.Get().(*context) s.handleRequest(ctx, method, path, conn) ctx.body = ctx.body[:0] ctx.params = ctx.params[:0] ctx.handlerCount = 0 ctx.status = 200 s.pool.Put(ctx) } } // handleRequest handles the given request. func (s *server) handleRequest(ctx *context, method string, path string, writer io.Writer) { ctx.method = method ctx.path = path err := s.handlers[0](ctx) if err != nil { s.errorHandler(ctx, err) } _, err = fmt.Fprintf(writer, "HTTP/1.1 %d %s\r\nContent-Length: %d\r\n%s\r\n%s", ctx.status, "OK", len(ctx.body), ctx.response.headerText(), ctx.body) if err != nil { s.errorHandler(ctx, err) } } // newContext allocates a new context with the default state. func (s *server) newContext() *context { return &context{ server: s, request: request{ params: make([]router.Parameter, 0, 8), }, response: response{ body: make([]byte, 0, 1024), status: 200, }, } }