Added URL parsing

This commit is contained in:
Eduard Urbach 2024-03-27 14:06:13 +01:00
parent fff7b6ae47
commit 98e1a47328
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
3 changed files with 126 additions and 12 deletions

View File

@ -4,17 +4,26 @@ import "git.akyoto.dev/go/router"
// Request is an interface for HTTP requests. // Request is an interface for HTTP requests.
type Request interface { type Request interface {
Host() string
Method() string Method() string
Path() string Path() string
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 {
scheme string
host string
method string method string
path string path string
params []router.Parameter params []router.Parameter
} }
// Host returns the requested host.
func (req *request) Host() string {
return req.host
}
// Method returns the request method. // Method returns the request method.
func (req *request) Method() string { func (req *request) Method() string {
return req.method return req.method
@ -25,6 +34,11 @@ func (req *request) Path() string {
return req.path return req.path
} }
// Scheme returns either `http`, `https` or an empty string.
func (req request) Scheme() string {
return req.scheme
}
// addParameter adds a new parameter to the request. // addParameter adds a new parameter to the request.
func (req *request) addParameter(key string, value string) { func (req *request) addParameter(key string, value string) {
req.params = append(req.params, router.Parameter{ req.params = append(req.params, router.Parameter{

View File

@ -70,11 +70,9 @@ func (s *server) Get(path string, handler Handler) {
// Request performs a synthetic request and returns the response. // 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. // 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. // 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 { func (s *server) Request(method string, url string, body io.Reader) Response {
ctx := s.newContext() ctx := s.newContext()
ctx.method = method s.handleRequest(ctx, method, url, io.Discard)
ctx.path = path
s.handleRequest(ctx, method, path, io.Discard)
return ctx.Response() return ctx.Response()
} }
@ -148,10 +146,10 @@ func (s *server) handleConnection(conn net.Conn) {
lastSpace = len(message) lastSpace = len(message)
} }
path := message[space+1 : lastSpace] url := message[space+1 : lastSpace]
ctx := s.pool.Get().(*context) ctx := s.pool.Get().(*context)
s.handleRequest(ctx, method, path, conn) s.handleRequest(ctx, method, url, conn)
ctx.body = ctx.body[:0] ctx.body = ctx.body[:0]
ctx.params = ctx.params[:0] ctx.params = ctx.params[:0]
ctx.handlerCount = 0 ctx.handlerCount = 0
@ -161,8 +159,29 @@ func (s *server) handleConnection(conn net.Conn) {
} }
// handleRequest handles the given request. // handleRequest handles the given request.
func (s *server) handleRequest(ctx *context, method string, path string, writer io.Writer) { func (s *server) handleRequest(ctx *context, method string, url string, writer io.Writer) {
schemePos := strings.Index(url, "://")
schemeEnd := 0
if schemePos != -1 {
schemeEnd = schemePos + len("://")
} else {
schemePos = 0
}
pathPos := strings.IndexByte(url[schemeEnd:], '/')
if pathPos == -1 {
return
}
scheme := url[:schemePos]
host := url[schemeEnd : schemeEnd+pathPos]
path := url[schemeEnd+pathPos:]
ctx.method = method ctx.method = method
ctx.scheme = scheme
ctx.host = host
ctx.path = path ctx.path = path
err := s.handlers[0](ctx) err := s.handlers[0](ctx)

View File

@ -1,22 +1,103 @@
package web_test package web_test
import ( import (
"errors"
"fmt"
"io"
"strings"
"testing" "testing"
"git.akyoto.dev/go/assert" "git.akyoto.dev/go/assert"
"git.akyoto.dev/go/web" "git.akyoto.dev/go/web"
) )
func TestBytes(t *testing.T) { func TestContext(t *testing.T) {
s := web.NewServer() s := web.NewServer()
s.Get("/", func(ctx web.Context) error { s.Get("/bytes", func(ctx web.Context) error {
return ctx.Bytes([]byte("Hello")) return ctx.Bytes([]byte("Hello"))
}) })
response := s.Request("GET", "/", nil) s.Get("/string", func(ctx web.Context) error {
assert.Equal(t, response.Status(), 200) return ctx.String("Hello")
assert.DeepEqual(t, response.Body(), []byte("Hello")) })
s.Get("/write", func(ctx web.Context) error {
_, err := ctx.Response().Write([]byte("Hello"))
return err
})
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(401).Error("Not logged in")
})
s.Get("/error2", func(ctx web.Context) error {
return ctx.Status(401).Error("Not logged in", errors.New("Missing auth token"))
})
s.Get("/request/data", func(ctx web.Context) error {
req := ctx.Request()
method := req.Method()
scheme := req.Scheme()
host := req.Host()
path := req.Path()
return ctx.String(fmt.Sprintf("%s %s %s %s", method, scheme, host, path))
})
s.Router().Add("POST", "/", func(ctx web.Context) error {
return ctx.String("Post")
})
s.Router().Add("DELETE", "/", func(ctx web.Context) error {
return ctx.String("Delete")
})
s.Router().Add("PUT", "/", func(ctx web.Context) error {
return ctx.String("Put")
})
tests := []struct {
Method string
Path string
Body string
Status int
Response string
}{
{Method: "GET", Path: "/bytes", Body: "", Status: 200, Response: "Hello"},
// {Method: "GET", Path: "/context", Body: "", Status: 200, Response: ""},
// {Method: "GET", Path: "/echo", Body: "Echo", Status: 200, Response: "Echo"},
{Method: "GET", Path: "/error", Body: "", Status: 401, Response: ""},
{Method: "GET", Path: "/error2", Body: "", Status: 401, Response: ""},
// {Method: "GET", Path: "/file", Body: "", Status: 200, Response: "Hello File"},
// {Method: "GET", Path: "/flush", Body: "", Status: 200, Response: "Hello 1\nHello 2\n"},
// {Method: "GET", Path: "/not-found", Body: "", Status: 404, Response: ""},
{Method: "GET", Path: "/request/data", Body: "", Status: 200, Response: "GET http example.com /request/data"},
// {Method: "GET", Path: "/request/header", Body: "", Status: 200, Response: ""},
// {Method: "GET", Path: "/response/header", Body: "", Status: 200, Response: "text/plain"},
// {Method: "GET", Path: "/reader", Body: "", Status: 200, Response: "Hello"},
// {Method: "GET", Path: "/redirect", Body: "", Status: 307, Response: ""},
{Method: "GET", Path: "/string", Body: "", Status: 200, Response: "Hello"},
{Method: "GET", Path: "/write", Body: "", Status: 200, Response: "Hello"},
{Method: "GET", Path: "/writestring", Body: "", Status: 200, Response: "Hello"},
// {Method: "GET", Path: "/blog/testing-my-router", Body: "", Status: 200, Response: "testing-my-router"},
// {Method: "GET", Path: "/missing-parameter", Body: "", Status: 200, Response: ""},
{Method: "POST", Path: "/", Body: "", Status: 200, Response: "Post"},
{Method: "DELETE", Path: "/", Body: "", Status: 200, Response: "Delete"},
{Method: "PUT", Path: "/", Body: "", Status: 200, Response: "Put"},
}
for _, test := range tests {
t.Run(test.Path, func(t *testing.T) {
response := s.Request(test.Method, "http://example.com"+test.Path, strings.NewReader(test.Body))
assert.Equal(t, response.Status(), test.Status)
assert.Equal(t, string(response.Body()), test.Response)
})
}
} }
func TestString(t *testing.T) { func TestString(t *testing.T) {