Removed net/http

This commit is contained in:
Eduard Urbach 2024-03-26 22:46:16 +01:00
parent f4617248d8
commit 271e1cd5bd
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
11 changed files with 230 additions and 629 deletions

View File

@ -2,50 +2,35 @@ package web
import ( import (
"errors" "errors"
"io"
"net/http"
"git.akyoto.dev/go/router"
) )
// Context represents the interface for a request & response context. // Context is the interface for a request and its response.
type Context interface { type Context interface {
Copy(io.Reader) error
Bytes([]byte) error Bytes([]byte) error
Error(...any) error Error(...any) error
File(string) error
Get(string) string
Next() error Next() error
Redirect(int, string) error
Request() Request Request() Request
Response() Response Response() Response
Status(int) Context Status(int) Context
String(string) error String(string) error
} }
// ctx represents a request & response context. // context contains the request and response data.
type ctx struct { type context struct {
request request request
response response response
server *server server *server
params []router.Parameter
handlerCount uint8 handlerCount uint8
} }
// Bytes responds with a raw byte slice. // Bytes adds the raw byte slice to the response body.
func (ctx *ctx) Bytes(body []byte) error { func (ctx *context) Bytes(body []byte) error {
_, err := ctx.response.Write(body) ctx.response.body = append(ctx.response.body, body...)
return err return nil
} }
// Copy sends the contents of the io.Reader without creating an in-memory copy. // Error provides a convenient way to wrap multiple errors.
func (ctx *ctx) Copy(reader io.Reader) error { func (ctx *context) Error(messages ...any) error {
_, err := io.Copy(ctx.response.ResponseWriter, reader)
return err
}
// Error is used for sending error messages to the client.
func (ctx *ctx) Error(messages ...any) error {
var combined []error var combined []error
for _, msg := range messages { for _, msg := range messages {
@ -60,64 +45,31 @@ func (ctx *ctx) Error(messages ...any) error {
return errors.Join(combined...) return errors.Join(combined...)
} }
// Get retrieves a parameter.
func (ctx *ctx) Get(param string) string {
for i := range len(ctx.params) {
p := ctx.params[i]
if p.Key == param {
return p.Value
}
}
return ""
}
// File serves the file at the given path.
func (ctx *ctx) File(path string) error {
http.ServeFile(ctx.response.ResponseWriter, ctx.request.Request, path)
return nil
}
// Next executes the next handler in the middleware chain. // Next executes the next handler in the middleware chain.
func (ctx *ctx) Next() error { func (ctx *context) Next() error {
ctx.handlerCount++ ctx.handlerCount++
return ctx.server.handlers[ctx.handlerCount](ctx) return ctx.server.handlers[ctx.handlerCount](ctx)
} }
// Request returns the HTTP request. // Request returns the HTTP request.
func (ctx *ctx) Request() Request { func (ctx *context) Request() Request {
return &ctx.request return &ctx.request
} }
// Response returns the HTTP response. // Response returns the HTTP response.
func (ctx *ctx) Response() Response { func (ctx *context) Response() Response {
return &ctx.response return &ctx.response
} }
// Redirect sets the Location header and writes the headers with the given status code. // Status sets the HTTP status of the response
func (ctx *ctx) Redirect(code int, url string) error { // and returns the context for method chaining.
ctx.Response().SetHeader("Location", url) func (ctx *context) Status(status int) Context {
ctx.Status(code) ctx.response.SetStatus(status)
return nil
}
// Status sets the HTTP status of the response.
func (ctx *ctx) Status(status int) Context {
ctx.response.WriteHeader(status)
return ctx return ctx
} }
// String responds with the given string. // String adds the given string to the response body.
func (ctx *ctx) String(body string) error { func (ctx *context) String(body string) error {
_, err := ctx.response.WriteString(body) ctx.response.body = append(ctx.response.body, body...)
return err return nil
}
// addParameter adds a new parameter to the context.
func (ctx *ctx) addParameter(key string, value string) {
ctx.params = append(ctx.params, router.Parameter{
Key: key,
Value: value,
})
} }

View File

@ -1,63 +1,34 @@
package web package web
import ( import "git.akyoto.dev/go/router"
"context"
"net/http"
)
// Request is an interface for HTTP requests. // Request is an interface for HTTP requests.
type Request interface { type Request interface {
Context() context.Context
Header(key string) string
Host() string
Method() string Method() string
Path() string Path() string
Protocol() string
Read([]byte) (int, error)
Scheme() string
} }
// request represents the HTTP request used in the given context. // request represents the HTTP request used in the given context.
type request struct { type request struct {
*http.Request method string
} path string
params []router.Parameter
// Context returns the request context.
func (req request) Context() context.Context {
return req.Request.Context()
}
// Header returns the header value for the given key.
func (req request) Header(key string) string {
return req.Request.Header.Get(key)
} }
// Method returns the request method. // Method returns the request method.
func (req request) Method() string { func (req *request) Method() string {
return req.Request.Method return req.method
}
// Protocol returns the request protocol.
func (req request) Protocol() string {
return req.Request.Proto
}
// Host returns the requested host.
func (req request) Host() string {
return req.Request.Host
} }
// Path returns the requested path. // Path returns the requested path.
func (req request) Path() string { func (req *request) Path() string {
return req.Request.URL.Path return req.path
} }
// // Read implements the io.Reader interface and reads the request body. // addParameter adds a new parameter to the request.
func (req request) Read(buffer []byte) (int, error) { func (req *request) addParameter(key string, value string) {
return req.Request.Body.Read(buffer) req.params = append(req.params, router.Parameter{
} Key: key,
Value: value,
// Scheme returns either `http` or `https`. })
func (req request) Scheme() string {
return req.Request.URL.Scheme
} }

View File

@ -2,48 +2,95 @@ package web
import ( import (
"io" "io"
"net/http" "strings"
"git.akyoto.dev/go/router"
) )
// Response is the interface for an HTTP response. // Response is the interface for an HTTP response.
type Response interface { type Response interface {
Flush() io.Writer
io.StringWriter
Body() []byte
Header(key string) string Header(key string) string
SetHeader(key string, value string) SetHeader(key string, value string)
Write([]byte) (int, error) SetBody([]byte)
WriteString(string) (int, error) SetStatus(status int)
Status() int
} }
// response represents the HTTP response used in the given context. // response represents the HTTP response used in the given context.
type response struct { type response struct {
http.ResponseWriter body []byte
status uint16
headers []router.Parameter
} }
// Flush flushes the response buffers to the client. // Body returns the response body.
func (res response) Flush() { func (res *response) Body() []byte {
flusher, ok := res.ResponseWriter.(http.Flusher) return res.body
if ok {
flusher.Flush()
}
} }
// Header returns the header value for the given key. // Header returns the header value for the given key.
func (res response) Header(key string) string { func (res *response) Header(key string) string {
return res.ResponseWriter.Header().Get(key) for _, header := range res.headers {
if header.Key == key {
return header.Value
}
}
return ""
} }
// SetHeader sets the header value for the given key. // SetHeader sets the header value for the given key.
func (res response) SetHeader(key string, value string) { func (res *response) SetHeader(key string, value string) {
res.ResponseWriter.Header().Set(key, value) for _, header := range res.headers {
if header.Key == key {
header.Value = value
return
}
}
res.headers = append(res.headers, router.Parameter{Key: key, Value: value})
}
// SetBody replaces the response body with the new contents.
func (res *response) SetBody(body []byte) {
res.body = body
}
// SetStatus sets the HTTP status code.
func (res *response) SetStatus(status int) {
res.status = uint16(status)
}
// Status returns the HTTP status code.
func (res *response) Status() int {
return int(res.status)
} }
// Write implements the io.Writer interface. // Write implements the io.Writer interface.
func (res response) Write(body []byte) (int, error) { func (res *response) Write(body []byte) (int, error) {
return res.ResponseWriter.Write(body) res.body = append(res.body, body...)
return len(body), nil
} }
// WriteString implements the io.StringWriter interface. // WriteString implements the io.StringWriter interface.
func (res response) WriteString(body string) (int, error) { func (res *response) WriteString(body string) (int, error) {
return res.ResponseWriter.(io.StringWriter).WriteString(body) res.body = append(res.body, body...)
return len(body), nil
}
// headerText combines all HTTP headers into a single string.
func (res *response) headerText() string {
combined := strings.Builder{}
for _, header := range res.headers {
combined.WriteString(header.Key)
combined.WriteString(": ")
combined.WriteString(header.Value)
combined.WriteString("\r\n")
}
return combined.String()
} }

168
Server.go
View File

@ -1,12 +1,14 @@
package web package web
import ( import (
"context" "bufio"
"fmt"
"io"
"log" "log"
"net" "net"
"net/http"
"os" "os"
"os/signal" "os/signal"
"strings"
"sync" "sync"
"syscall" "syscall"
@ -15,13 +17,13 @@ import (
// Server is the interface for an HTTP server. // Server is the interface for an HTTP server.
type Server interface { type Server interface {
http.Handler
Delete(path string, handler Handler) Delete(path string, handler Handler)
Get(path string, handler Handler) Get(path string, handler Handler)
Post(path string, handler Handler) Post(path string, handler Handler)
Put(path string, handler Handler) Put(path string, handler Handler)
Router() *router.Router[Handler] Router() *router.Router[Handler]
Run(address string) error Run(address string) error
Test(method string, path string, body io.Reader) Response
Use(handlers ...Handler) Use(handlers ...Handler)
} }
@ -29,41 +31,35 @@ type Server interface {
type server struct { type server struct {
pool sync.Pool pool sync.Pool
handlers []Handler handlers []Handler
router router.Router[Handler] router *router.Router[Handler]
errorHandler func(Context, error) errorHandler func(Context, error)
config config
} }
// NewServer creates a new HTTP server. // NewServer creates a new HTTP server.
func NewServer() Server { func NewServer() Server {
r := &router.Router[Handler]{}
s := &server{ s := &server{
router: router.Router[Handler]{}, router: r,
config: defaultConfig(),
handlers: []Handler{ handlers: []Handler{
func(c Context) error { func(c Context) error {
ctx := c.(*ctx) ctx := c.(*context)
method := ctx.request.Method() handler := r.LookupNoAlloc(ctx.request.method, ctx.request.path, ctx.request.addParameter)
path := ctx.request.Path()
handler := ctx.server.router.LookupNoAlloc(method, path, ctx.addParameter)
if handler == nil { if handler == nil {
return ctx.Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound)) ctx.SetStatus(404)
return nil
} }
return handler(c) return handler(c)
}, },
}, },
errorHandler: func(ctx Context, err error) { errorHandler: func(ctx Context, err error) {
ctx.Response().WriteString(err.Error())
log.Println(ctx.Request().Path(), err) log.Println(ctx.Request().Path(), err)
}, },
} }
s.pool.New = func() any { s.pool.New = func() any {
return &ctx{ return s.newContext()
server: s,
params: make([]router.Parameter, 0, 8),
}
} }
return s return s
@ -71,72 +67,55 @@ func NewServer() Server {
// 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 (s *server) Get(path string, handler Handler) { func (s *server) Get(path string, handler Handler) {
s.Router().Add(http.MethodGet, path, handler) s.Router().Add("GET", 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 (s *server) Post(path string, handler Handler) { func (s *server) Post(path string, handler Handler) {
s.Router().Add(http.MethodPost, path, handler) s.Router().Add("POST", 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 (s *server) Delete(path string, handler Handler) { func (s *server) Delete(path string, handler Handler) {
s.Router().Add(http.MethodDelete, path, handler) s.Router().Add("DELETE", 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 (s *server) Put(path string, handler Handler) { func (s *server) Put(path string, handler Handler) {
s.Router().Add(http.MethodPut, path, handler) s.Router().Add("PUT", path, handler)
}
// ServeHTTP responds to the given request.
func (s *server) ServeHTTP(res http.ResponseWriter, req *http.Request) {
ctx := s.pool.Get().(*ctx)
ctx.request = request{req}
ctx.response = response{res}
err := s.handlers[0](ctx)
if err != nil {
s.errorHandler(ctx, err)
}
ctx.params = ctx.params[:0]
ctx.handlerCount = 0
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 (s *server) Run(address string) error {
srv := &http.Server{
Addr: address,
Handler: server,
ReadTimeout: server.config.Timeout.Read,
WriteTimeout: server.config.Timeout.Write,
IdleTimeout: server.config.Timeout.Idle,
ReadHeaderTimeout: server.config.Timeout.ReadHeader,
}
listener, err := net.Listen("tcp", address) listener, err := net.Listen("tcp", address)
if err != nil { if err != nil {
return err return err
} }
go srv.Serve(listener) defer listener.Close()
go func() {
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go s.handleConnection(conn)
}
}()
stop := make(chan os.Signal, 1) stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM) signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop <-stop
return nil
ctx, cancel := context.WithTimeout(context.Background(), server.config.Timeout.Shutdown)
defer cancel()
return srv.Shutdown(ctx)
} }
// Router returns the router used by the server. // Router returns the router used by the server.
func (s *server) Router() *router.Router[Handler] { func (s *server) Router() *router.Router[Handler] {
return &s.router return s.router
} }
// Use adds handlers to your handlers chain. // Use adds handlers to your handlers chain.
@ -145,3 +124,84 @@ func (s *server) Use(handlers ...Handler) {
s.handlers = append(s.handlers[:len(s.handlers)-1], handlers...) s.handlers = append(s.handlers[:len(s.handlers)-1], handlers...)
s.handlers = append(s.handlers, last) 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)
}
}
func (s *server) Test(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()
}
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,
},
}
}

View File

@ -1,224 +1,32 @@
package web_test package web_test
import ( import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"strings"
"syscall"
"testing" "testing"
"git.akyoto.dev/go/assert" "git.akyoto.dev/go/assert"
"git.akyoto.dev/go/web" "git.akyoto.dev/go/web"
) )
func TestRouter(t *testing.T) { func TestBytes(t *testing.T) {
s := web.NewServer() s := web.NewServer()
s.Get("/", func(ctx web.Context) error { s.Get("/", func(ctx web.Context) error {
return ctx.Bytes([]byte("Hello")) return ctx.Bytes([]byte("Hello"))
}) })
s.Get("/string", func(ctx web.Context) error { response := s.Test("GET", "/", nil)
assert.Equal(t, response.Status(), 200)
assert.DeepEqual(t, response.Body(), []byte("Hello"))
}
func TestString(t *testing.T) {
s := web.NewServer()
s.Get("/", func(ctx web.Context) error {
return ctx.String("Hello") return ctx.String("Hello")
}) })
s.Get("/write", func(ctx web.Context) error { response := s.Test("GET", "/", nil)
_, err := ctx.Response().Write([]byte("Hello")) assert.Equal(t, response.Status(), 200)
return err assert.DeepEqual(t, response.Body(), []byte("Hello"))
})
s.Get("/writestring", func(ctx web.Context) error {
_, err := io.WriteString(ctx.Response(), "Hello")
return err
})
s.Get("/error", func(ctx web.Context) error {
return ctx.Status(http.StatusUnauthorized).Error("Not logged in")
})
s.Get("/error2", func(ctx web.Context) error {
return ctx.Status(http.StatusUnauthorized).Error("Not logged in", errors.New("Missing auth token"))
})
s.Get("/reader", func(ctx web.Context) error {
return ctx.Copy(strings.NewReader("Hello"))
})
s.Get("/file", func(ctx web.Context) error {
return ctx.File("testdata/file.txt")
})
s.Get("/flush", func(ctx web.Context) error {
ctx.Response().WriteString("Hello 1\n")
ctx.Response().WriteString("Hello 2\n")
ctx.Response().Flush()
return nil
})
s.Get("/echo", func(ctx web.Context) error {
return ctx.Copy(ctx.Request())
})
s.Get("/context", func(ctx web.Context) error {
return ctx.Request().Context().Err()
})
s.Get("/redirect", func(ctx web.Context) error {
return ctx.Redirect(http.StatusTemporaryRedirect, "/")
})
s.Get("/request/data", func(ctx web.Context) error {
request := ctx.Request()
method := request.Method()
protocol := request.Protocol()
host := request.Host()
path := request.Path()
return ctx.String(fmt.Sprintf("%s %s %s %s", method, protocol, host, path))
})
s.Get("/request/header", func(ctx web.Context) error {
acceptEncoding := ctx.Request().Header("Accept-Encoding")
return ctx.String(acceptEncoding)
})
s.Get("/response/header", func(ctx web.Context) error {
ctx.Response().SetHeader("Content-Type", "text/plain")
contentType := ctx.Response().Header("Content-Type")
return ctx.String(contentType)
})
s.Get("/blog/:article", func(ctx web.Context) error {
article := ctx.Get("article")
return ctx.String(article)
})
s.Get("/missing-parameter", func(ctx web.Context) error {
missing := ctx.Get("missing")
return ctx.String(missing)
})
s.Get("/scheme", func(ctx web.Context) error {
return ctx.String(ctx.Request().Scheme())
})
s.Post("/", func(ctx web.Context) error {
return ctx.String("Post")
})
s.Delete("/", func(ctx web.Context) error {
return ctx.String("Delete")
})
s.Put("/", func(ctx web.Context) error {
return ctx.String("Put")
})
tests := []struct {
Method string
URL string
Body string
Status int
Response string
}{
{Method: "GET", URL: "/", Body: "", Status: http.StatusOK, Response: "Hello"},
{Method: "GET", URL: "/context", Body: "", Status: http.StatusOK, Response: ""},
{Method: "GET", URL: "/echo", Body: "Echo", Status: http.StatusOK, Response: "Echo"},
{Method: "GET", URL: "/error", Body: "", Status: http.StatusUnauthorized, Response: "Not logged in"},
{Method: "GET", URL: "/error2", Body: "", Status: http.StatusUnauthorized, Response: "Not logged in\nMissing auth token"},
{Method: "GET", URL: "/file", Body: "", Status: http.StatusOK, Response: "Hello File"},
{Method: "GET", URL: "/flush", Body: "", Status: http.StatusOK, Response: "Hello 1\nHello 2\n"},
{Method: "GET", URL: "/not-found", Body: "", Status: http.StatusNotFound, Response: http.StatusText(http.StatusNotFound)},
{Method: "GET", URL: "/request/data", Body: "", Status: http.StatusOK, Response: "GET HTTP/1.1 example.com /request/data"},
{Method: "GET", URL: "/request/header", Body: "", Status: http.StatusOK, Response: ""},
{Method: "GET", URL: "/response/header", Body: "", Status: http.StatusOK, Response: "text/plain"},
{Method: "GET", URL: "/reader", Body: "", Status: http.StatusOK, Response: "Hello"},
{Method: "GET", URL: "/redirect", Body: "", Status: http.StatusTemporaryRedirect, Response: ""},
{Method: "GET", URL: "/string", Body: "", Status: http.StatusOK, Response: "Hello"},
{Method: "GET", URL: "/scheme", Body: "", Status: http.StatusOK, Response: "http"},
{Method: "GET", URL: "/write", Body: "", Status: http.StatusOK, Response: "Hello"},
{Method: "GET", URL: "/writestring", Body: "", Status: http.StatusOK, Response: "Hello"},
{Method: "GET", URL: "/blog/testing-my-router", Body: "", Status: http.StatusOK, Response: "testing-my-router"},
{Method: "GET", URL: "/missing-parameter", Body: "", Status: http.StatusOK, Response: ""},
{Method: "POST", URL: "/", Body: "", Status: http.StatusOK, Response: "Post"},
{Method: "DELETE", URL: "/", Body: "", Status: http.StatusOK, Response: "Delete"},
{Method: "PUT", URL: "/", Body: "", Status: http.StatusOK, Response: "Put"},
}
for _, test := range tests {
t.Run("example.com"+test.URL, func(t *testing.T) {
request := httptest.NewRequest(test.Method, "http://example.com"+test.URL, strings.NewReader(test.Body))
response := httptest.NewRecorder()
s.ServeHTTP(response, request)
result := response.Result()
assert.Equal(t, result.StatusCode, test.Status)
body, err := io.ReadAll(result.Body)
assert.Nil(t, err)
assert.Equal(t, string(body), test.Response)
})
}
}
func TestMiddleware(t *testing.T) {
s := web.NewServer()
s.Use(func(ctx web.Context) error {
ctx.Response().SetHeader("Middleware", "true")
return ctx.Next()
})
request := httptest.NewRequest(http.MethodGet, "/", nil)
response := httptest.NewRecorder()
s.ServeHTTP(response, request)
assert.Equal(t, response.Header().Get("Middleware"), "true")
}
func TestPanic(t *testing.T) {
s := web.NewServer()
s.Router().Add(http.MethodGet, "/panic", func(ctx web.Context) error {
panic("Something unbelievable happened")
})
t.Run("example.com/panic", func(t *testing.T) {
defer func() {
r := recover()
if r == nil {
t.Error("Didn't panic")
}
}()
request := httptest.NewRequest(http.MethodGet, "/panic", nil)
response := httptest.NewRecorder()
s.ServeHTTP(response, request)
})
}
func TestRun(t *testing.T) {
s := web.NewServer()
go func() {
_, err := http.Get("http://127.0.0.1:8080/")
assert.Nil(t, err)
err = syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
assert.Nil(t, err)
}()
s.Run(":8080")
}
func TestUnavailablePort(t *testing.T) {
listener, err := net.Listen("tcp", ":8080")
assert.Nil(t, err)
defer listener.Close()
s := web.NewServer()
s.Run(":8080")
} }

View File

@ -1,63 +0,0 @@
package web_test
import (
"net/http/httptest"
"strings"
"testing"
"git.akyoto.dev/go/router/testdata"
"git.akyoto.dev/go/web"
)
func BenchmarkStatic(b *testing.B) {
paths := []string{
"/",
"/hello",
"/hello/world",
}
s := web.NewServer()
for _, path := range paths {
s.Get(path, func(ctx web.Context) error {
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 {
s.ServeHTTP(response, request)
}
})
}
}
func BenchmarkGitHub(b *testing.B) {
paths := []string{
"/gists/:id",
"/repos/:a/:b",
}
s := web.NewServer()
for _, route := range testdata.Routes("testdata/github.txt") {
s.Router().Add(route.Method, route.Path, func(ctx web.Context) error {
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 {
s.ServeHTTP(response, request)
}
})
}
}

View File

@ -1,12 +0,0 @@
package web_test
import "net/http"
// NullResponse implements the http.ResponseWriter interface with
// empty methods to better understand memory usage in benchmarks.
type NullResponse struct{}
func (r *NullResponse) Header() http.Header { return nil }
func (r *NullResponse) Write([]byte) (int, error) { return 0, nil }
func (r *NullResponse) WriteString(string) (int, error) { return 0, nil }
func (r *NullResponse) WriteHeader(int) {}

View File

@ -1,30 +0,0 @@
package web
import "time"
// config represents the server configuration.
type config struct {
Timeout timeoutConfig `json:"timeout"`
}
// timeoutConfig lets you configure the different timeout durations.
type timeoutConfig struct {
Idle time.Duration `json:"idle"`
Read time.Duration `json:"read"`
ReadHeader time.Duration `json:"readHeader"`
Write time.Duration `json:"write"`
Shutdown time.Duration `json:"shutdown"`
}
// Reset resets all fields to the default configuration.
func defaultConfig() config {
return config{
Timeout: timeoutConfig{
Idle: 3 * time.Minute,
Write: 2 * time.Minute,
Read: 5 * time.Second,
ReadHeader: 5 * time.Second,
Shutdown: 250 * time.Millisecond,
},
}
}

View File

@ -1,80 +0,0 @@
package content_test
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.akyoto.dev/go/assert"
"git.akyoto.dev/go/web"
"git.akyoto.dev/go/web/content"
)
func TestContentTypes(t *testing.T) {
s := web.NewServer()
s.Get("/css", func(ctx web.Context) error {
return content.CSS(ctx, "body{}")
})
s.Get("/csv", func(ctx web.Context) error {
return content.CSV(ctx, "ID;Name\n")
})
s.Get("/html", func(ctx web.Context) error {
return content.HTML(ctx, "<html></html>")
})
s.Get("/js", func(ctx web.Context) error {
return content.JS(ctx, "console.log(42)")
})
s.Get("/json", func(ctx web.Context) error {
return content.JSON(ctx, struct{ Name string }{Name: "User 1"})
})
s.Get("/text", func(ctx web.Context) error {
return content.Text(ctx, "Hello")
})
s.Get("/xml", func(ctx web.Context) error {
return content.XML(ctx, "<xml></xml>")
})
tests := []struct {
Method string
URL string
Body string
Status int
Response string
ContentType string
}{
{Method: "GET", URL: "/css", Body: "", Status: http.StatusOK, Response: "body{}", ContentType: "text/css"},
{Method: "GET", URL: "/csv", Body: "", Status: http.StatusOK, Response: "ID;Name\n", ContentType: "text/csv"},
{Method: "GET", URL: "/html", Body: "", Status: http.StatusOK, Response: "<html></html>", ContentType: "text/html"},
{Method: "GET", URL: "/js", Body: "", Status: http.StatusOK, Response: "console.log(42)", ContentType: "text/javascript"},
{Method: "GET", URL: "/json", Body: "", Status: http.StatusOK, Response: "{\"Name\":\"User 1\"}\n", ContentType: "application/json"},
{Method: "GET", URL: "/text", Body: "", Status: http.StatusOK, Response: "Hello", ContentType: "text/plain"},
{Method: "GET", URL: "/xml", Body: "", Status: http.StatusOK, Response: "<xml></xml>", ContentType: "text/xml"},
}
for _, test := range tests {
t.Run("example.com"+test.URL, func(t *testing.T) {
request := httptest.NewRequest(test.Method, "http://example.com"+test.URL, strings.NewReader(test.Body))
response := httptest.NewRecorder()
s.ServeHTTP(response, request)
result := response.Result()
assert.Equal(t, result.StatusCode, test.Status)
contentType := result.Header.Get("Content-Type")
assert.Equal(t, contentType, test.ContentType)
body, err := io.ReadAll(result.Body)
assert.Nil(t, err)
assert.Equal(t, string(body), test.Response)
})
}
}

View File

@ -1,24 +0,0 @@
package main
import (
"fmt"
"time"
"git.akyoto.dev/go/web"
)
func main() {
s := web.NewServer()
s.Use(func(ctx web.Context) error {
start := time.Now()
defer func() {
fmt.Println(ctx.Request().Path(), time.Since(start))
}()
return ctx.Next()
})
s.Run(":8080")
}

View File

@ -1,28 +0,0 @@
package main
import (
"time"
"git.akyoto.dev/go/web"
)
func main() {
s := web.NewServer()
s.Get("/", func(ctx web.Context) error {
ticker := time.NewTicker(time.Second)
for {
select {
case <-ctx.Request().Context().Done():
return nil
case <-ticker.C:
ctx.Response().WriteString("Hello\n")
ctx.Response().Flush()
}
}
})
s.Run(":8080")
}