Added Server interface

This commit is contained in:
Eduard Urbach 2024-03-14 12:52:03 +01:00
parent e604017ecc
commit 1e4161de0c
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
6 changed files with 135 additions and 67 deletions

View File

@ -2,38 +2,62 @@ package server_test
import ( import (
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"git.akyoto.dev/go/router/testdata" "git.akyoto.dev/go/router/testdata"
"git.akyoto.dev/go/server" "git.akyoto.dev/go/server"
) )
func BenchmarkHello(b *testing.B) { func BenchmarkStatic(b *testing.B) {
request := httptest.NewRequest("GET", "/", nil) paths := []string{
response := &NullResponse{} "/",
"/hello",
"/hello/world",
}
s := server.New() s := server.New()
s.Get("/", func(ctx server.Context) error { for _, path := range paths {
s.Get(path, func(ctx server.Context) error {
return ctx.String("Hello") return ctx.String("Hello")
}) })
}
for _, path := range paths {
b.Run(strings.TrimPrefix(path, "/"), func(b *testing.B) {
request := httptest.NewRequest("GET", path, nil)
response := &NullResponse{}
for range b.N { for range b.N {
s.ServeHTTP(response, request) s.ServeHTTP(response, request)
} }
})
}
} }
func BenchmarkGitHub(b *testing.B) { func BenchmarkGitHub(b *testing.B) {
request := httptest.NewRequest("GET", "/repos/:owner/:repo", nil) paths := []string{
response := &NullResponse{} "/gists/:id",
"/repos/:a/:b",
}
s := server.New() s := server.New()
for _, route := range testdata.Routes("testdata/github.txt") { for _, route := range testdata.Routes("testdata/github.txt") {
s.Router.Add(route.Method, route.Path, func(server.Context) error { s.Router().Add(route.Method, route.Path, func(ctx server.Context) error {
return nil return ctx.String("Hello")
}) })
} }
for _, path := range paths {
b.Run(strings.TrimPrefix(path, "/"), func(b *testing.B) {
request := httptest.NewRequest("GET", path, nil)
response := &NullResponse{}
for range b.N { for range b.N {
s.ServeHTTP(response, request) s.ServeHTTP(response, request)
} }
})
}
} }

View File

@ -24,16 +24,18 @@ type Context interface {
Reader(io.Reader) error Reader(io.Reader) error
RequestHeader(key string) string RequestHeader(key string) string
ResponseHeader(key string) string ResponseHeader(key string) string
Scheme() string
Status(status int) Context Status(status int) Context
String(string) error String(string) error
Write([]byte) (int, error) Write([]byte) (int, error)
WriteString(string) (int, error)
} }
// ctx represents a request & response context. // ctx represents a request & response context.
type ctx struct { type ctx struct {
request *http.Request request *http.Request
response http.ResponseWriter response http.ResponseWriter
server *Server server *server
paramNames [maxParams]string paramNames [maxParams]string
paramValues [maxParams]string paramValues [maxParams]string
paramCount int paramCount int
@ -64,7 +66,7 @@ func (ctx *ctx) Error(messages ...any) error {
// Get retrieves a parameter. // Get retrieves a parameter.
func (ctx *ctx) Get(param string) string { func (ctx *ctx) Get(param string) string {
for i := 0; i < ctx.paramCount; i++ { for i := range ctx.paramCount {
if ctx.paramNames[i] == param { if ctx.paramNames[i] == param {
return ctx.paramValues[i] return ctx.paramValues[i]
} }
@ -120,6 +122,11 @@ func (ctx *ctx) Reader(reader io.Reader) error {
return err return err
} }
// Scheme returns either `http` or `https`.
func (ctx *ctx) Scheme() string {
return ctx.request.URL.Scheme
}
// Status sets the HTTP status of the response. // Status sets the HTTP status of the response.
func (ctx *ctx) Status(status int) Context { func (ctx *ctx) Status(status int) Context {
ctx.response.WriteHeader(status) ctx.response.WriteHeader(status)
@ -137,6 +144,11 @@ func (ctx *ctx) Write(body []byte) (int, error) {
return ctx.response.Write(body) return ctx.response.Write(body)
} }
// WriteString implements the io.StringWriter interface.
func (ctx *ctx) WriteString(body string) (int, error) {
return ctx.response.(io.StringWriter).WriteString(body)
}
// addParameter adds a new parameter to the context. // addParameter adds a new parameter to the context.
func (ctx *ctx) addParameter(name string, value string) { func (ctx *ctx) addParameter(name string, value string) {
ctx.paramNames[ctx.paramCount] = name ctx.paramNames[ctx.paramCount] = name

View File

@ -50,8 +50,11 @@ coverage: 100.0% of statements
## Benchmarks ## Benchmarks
``` ```
BenchmarkHello-12 35983602 33.28 ns/op 0 B/op 0 allocs/op BenchmarkStatic/#00-12 33616044 33.82 ns/op 0 B/op 0 allocs/op
BenchmarkGitHub-12 18320769 68.66 ns/op 0 B/op 0 allocs/op BenchmarkStatic/hello-12 26220148 43.75 ns/op 0 B/op 0 allocs/op
BenchmarkStatic/hello/world-12 19777713 58.89 ns/op 0 B/op 0 allocs/op
BenchmarkGitHub/gists/:id-12 20842587 56.36 ns/op 0 B/op 0 allocs/op
BenchmarkGitHub/repos/:a/:b-12 17875575 65.04 ns/op 0 B/op 0 allocs/op
``` ```
## License ## License

103
Server.go
View File

@ -2,32 +2,46 @@ package server
import ( import (
"context" "context"
"io"
"log" "log"
"net" "net"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"sync"
"syscall" "syscall"
"git.akyoto.dev/go/router" "git.akyoto.dev/go/router"
) )
// Server represents a single web service. // Server is the interface for an HTTP server.
type Server struct { type Server interface {
Router *router.Router[Handler] http.Handler
Config Configuration Delete(path string, handler Handler)
handlers []Handler Get(path string, handler Handler)
Post(path string, handler Handler)
Put(path string, handler Handler)
Router() *router.Router[Handler]
Run(address string) error
Use(handlers ...Handler)
} }
// New creates a new server. // server is an HTTP server.
func New() *Server { type server struct {
return &Server{ pool sync.Pool
Router: router.New[Handler](), handlers []Handler
Config: defaultConfig(), router router.Router[Handler]
errorHandler func(Context, error)
config Configuration
}
// New creates a new HTTP server.
func New() Server {
s := &server{
router: router.Router[Handler]{},
config: defaultConfig(),
handlers: []Handler{ handlers: []Handler{
func(c Context) error { func(c Context) error {
handler := c.(*ctx).server.Router.LookupNoAlloc(c.Method(), c.Path(), c.(*ctx).addParameter) handler := c.(*ctx).server.router.LookupNoAlloc(c.Method(), c.Path(), c.(*ctx).addParameter)
if handler == nil { if handler == nil {
return c.Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound)) return c.Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound))
@ -36,56 +50,64 @@ func New() *Server {
return handler(c) return handler(c)
}, },
}, },
errorHandler: func(ctx Context, err error) {
ctx.WriteString(err.Error())
log.Println(ctx.Path(), err)
},
} }
s.pool.New = func() any {
return &ctx{server: s}
}
return s
} }
// Get registers your function to be called when the given GET path has been requested. // Get registers your function to be called when the given GET path has been requested.
func (server *Server) Get(path string, handler Handler) { func (s *server) Get(path string, handler Handler) {
server.Router.Add(http.MethodGet, path, handler) s.Router().Add(http.MethodGet, path, handler)
} }
// Post registers your function to be called when the given POST path has been requested. // Post registers your function to be called when the given POST path has been requested.
func (server *Server) Post(path string, handler Handler) { func (s *server) Post(path string, handler Handler) {
server.Router.Add(http.MethodPost, path, handler) s.Router().Add(http.MethodPost, path, handler)
} }
// Delete registers your function to be called when the given DELETE path has been requested. // Delete registers your function to be called when the given DELETE path has been requested.
func (server *Server) Delete(path string, handler Handler) { func (s *server) Delete(path string, handler Handler) {
server.Router.Add(http.MethodDelete, path, handler) s.Router().Add(http.MethodDelete, path, handler)
} }
// Put registers your function to be called when the given PUT path has been requested. // Put registers your function to be called when the given PUT path has been requested.
func (server *Server) Put(path string, handler Handler) { func (s *server) Put(path string, handler Handler) {
server.Router.Add(http.MethodPut, path, handler) s.Router().Add(http.MethodPut, path, handler)
} }
// ServeHTTP responds to the given request. // ServeHTTP responds to the given request.
func (server *Server) ServeHTTP(response http.ResponseWriter, request *http.Request) { func (s *server) ServeHTTP(response http.ResponseWriter, request *http.Request) {
ctx := contextPool.Get().(*ctx) ctx := s.pool.Get().(*ctx)
ctx.request = request ctx.request = request
ctx.response = response ctx.response = response
ctx.server = server err := s.handlers[0](ctx)
err := server.handlers[0](ctx)
if err != nil { if err != nil {
response.(io.StringWriter).WriteString(err.Error()) s.errorHandler(ctx, err)
log.Println(request.URL, err)
} }
ctx.paramCount = 0 ctx.paramCount = 0
ctx.handlerCount = 0 ctx.handlerCount = 0
contextPool.Put(ctx) s.pool.Put(ctx)
} }
// Run starts the server on the given address. // Run starts the server on the given address.
func (server *Server) Run(address string) error { func (server *server) Run(address string) error {
srv := &http.Server{ srv := &http.Server{
Addr: address, Addr: address,
Handler: server, Handler: server,
ReadTimeout: server.Config.Timeout.Read, ReadTimeout: server.config.Timeout.Read,
WriteTimeout: server.Config.Timeout.Write, WriteTimeout: server.config.Timeout.Write,
IdleTimeout: server.Config.Timeout.Idle, IdleTimeout: server.config.Timeout.Idle,
ReadHeaderTimeout: server.Config.Timeout.ReadHeader, ReadHeaderTimeout: server.config.Timeout.ReadHeader,
} }
listener, err := net.Listen("tcp", address) listener, err := net.Listen("tcp", address)
@ -100,15 +122,20 @@ func (server *Server) Run(address string) error {
signal.Notify(stop, os.Interrupt, syscall.SIGTERM) signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop <-stop
ctx, cancel := context.WithTimeout(context.Background(), server.Config.Timeout.Shutdown) ctx, cancel := context.WithTimeout(context.Background(), server.config.Timeout.Shutdown)
defer cancel() defer cancel()
return srv.Shutdown(ctx) return srv.Shutdown(ctx)
} }
// Use adds handlers to your handlers chain. // Router returns the router used by the server.
func (server *Server) Use(handlers ...Handler) { func (s *server) Router() *router.Router[Handler] {
last := server.handlers[len(server.handlers)-1] return &s.router
server.handlers = append(server.handlers[:len(server.handlers)-1], handlers...) }
server.handlers = append(server.handlers, last)
// 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)
} }

View File

@ -27,6 +27,11 @@ func TestRouter(t *testing.T) {
}) })
s.Get("/write", func(ctx server.Context) error { s.Get("/write", func(ctx server.Context) error {
_, err := ctx.Write([]byte("Hello"))
return err
})
s.Get("/writestring", func(ctx server.Context) error {
_, err := io.WriteString(ctx, "Hello") _, err := io.WriteString(ctx, "Hello")
return err return err
}) })
@ -72,6 +77,10 @@ func TestRouter(t *testing.T) {
return ctx.String(missing) return ctx.String(missing)
}) })
s.Get("/scheme", func(ctx server.Context) error {
return ctx.String(ctx.Scheme())
})
s.Post("/", func(ctx server.Context) error { s.Post("/", func(ctx server.Context) error {
return ctx.String("Post") return ctx.String("Post")
}) })
@ -99,7 +108,9 @@ func TestRouter(t *testing.T) {
{Method: "GET", URL: "/response/header", Status: http.StatusOK, Body: "text/plain"}, {Method: "GET", URL: "/response/header", Status: http.StatusOK, Body: "text/plain"},
{Method: "GET", URL: "/reader", Status: http.StatusOK, Body: "Hello"}, {Method: "GET", URL: "/reader", Status: http.StatusOK, Body: "Hello"},
{Method: "GET", URL: "/string", Status: http.StatusOK, Body: "Hello"}, {Method: "GET", URL: "/string", Status: http.StatusOK, Body: "Hello"},
{Method: "GET", URL: "/scheme", Status: http.StatusOK, Body: "http"},
{Method: "GET", URL: "/write", Status: http.StatusOK, Body: "Hello"}, {Method: "GET", URL: "/write", Status: http.StatusOK, Body: "Hello"},
{Method: "GET", URL: "/writestring", Status: http.StatusOK, Body: "Hello"},
{Method: "GET", URL: "/blog/testing-my-router", Status: http.StatusOK, Body: "testing-my-router"}, {Method: "GET", URL: "/blog/testing-my-router", Status: http.StatusOK, Body: "testing-my-router"},
{Method: "GET", URL: "/missing-parameter", Status: http.StatusOK, Body: ""}, {Method: "GET", URL: "/missing-parameter", Status: http.StatusOK, Body: ""},
{Method: "POST", URL: "/", Status: http.StatusOK, Body: "Post"}, {Method: "POST", URL: "/", Status: http.StatusOK, Body: "Post"},
@ -109,7 +120,7 @@ func TestRouter(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run("example.com"+test.URL, func(t *testing.T) { t.Run("example.com"+test.URL, func(t *testing.T) {
request := httptest.NewRequest(test.Method, test.URL, nil) request := httptest.NewRequest(test.Method, "http://example.com"+test.URL, nil)
response := httptest.NewRecorder() response := httptest.NewRecorder()
s.ServeHTTP(response, request) s.ServeHTTP(response, request)
@ -141,7 +152,7 @@ func TestMiddleware(t *testing.T) {
func TestPanic(t *testing.T) { func TestPanic(t *testing.T) {
s := server.New() s := server.New()
s.Router.Add(http.MethodGet, "/panic", func(ctx server.Context) error { s.Router().Add(http.MethodGet, "/panic", func(ctx server.Context) error {
panic("Something unbelievable happened") panic("Something unbelievable happened")
}) })

View File

@ -1,9 +0,0 @@
package server
import "sync"
var contextPool = sync.Pool{
New: func() any {
return &ctx{}
},
}