summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.gmi12
-rw-r--r--README.md12
-rw-r--r--contrib/cgi/gemini.go4
-rw-r--r--contrib/cgi/gopher.go4
-rw-r--r--contrib/fs/dir_test.go4
-rw-r--r--contrib/fs/file_test.go2
-rw-r--r--contrib/fs/gemini.go12
-rw-r--r--contrib/fs/gopher.go12
-rw-r--r--contrib/sharedhost/replacement.go6
-rw-r--r--contrib/sharedhost/replacement_test.go6
-rw-r--r--contrib/tlsauth/auth_test.go20
-rw-r--r--contrib/tlsauth/gemini.go12
-rw-r--r--contrib/tlsauth/gemini_test.go20
-rw-r--r--examples/cowsay/main.go4
-rw-r--r--examples/inspectls/main.go4
-rw-r--r--finger/serve.go2
-rw-r--r--finger/system.go4
-rw-r--r--gemini/roundtrip_test.go8
-rw-r--r--gemini/serve.go8
-rw-r--r--gopher/serve.go2
-rw-r--r--handler.go34
-rw-r--r--handler_test.go22
-rw-r--r--logging/middleware.go6
-rw-r--r--logging/middleware_test.go6
-rw-r--r--router.go4
-rw-r--r--router_test.go16
26 files changed, 140 insertions, 106 deletions
diff --git a/README.gmi b/README.gmi
index 823de74..980d380 100644
--- a/README.gmi
+++ b/README.gmi
@@ -13,7 +13,7 @@ Gus is carefully structured as composable building blocks. The top-level package
* a "Server" interface type
* a "Handler" abstraction
* a "Middleware" abstraction
-* some useful Handler wrappers: filtering, falling through a list of handlers
+* some useful Handler wrappers: a router, request filtering, falling through a list of handlers
## Protocols
@@ -39,6 +39,16 @@ The gus/logging package provides everything you need to get a good basic start t
* A request-logging middleware with common diagnostics (time, duration, url, status codes, response body lengths)
* A simple constructor of useful default loggers at various levels. They output colorful logfmt lines to stdout.
+## Routing
+
+The router in the gus package supports slash-delimited path pattern strings. In the segments of these patterns:
+
+* A "/:wildcard/" segment matches anything in that position, and captures the value as a route parameter. Or if the paramter name is omitted like "/:/", it matches anything in a single segment without capturing a paramter.
+* A "/*remainder" segment is only allowed at the end and matches the rest of the path, capturing it into the paramter name. Or again, omitting a parameter name like "/*" simple matches any path suffix.
+* Any other segment in the pattern must match the corresponding segment of a request exactly.
+
+Router also supports maintaining a list of middlewares at the router level, mounting sub-routers under a pattern, looking up the matching handler for any request, and of course acting as a Handler itself.
+
## gus/contrib/*
This is where useful building blocks themselves start to come in. Sub-packages of contrib include Handler and Middleware implementations which accomplish the things your servers actually need to do.
diff --git a/README.md b/README.md
index ec1f735..592f652 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Gus is carefully structured as composable building blocks. The top-level package
* a "Server" interface type
* a "Handler" abstraction
* a "Middleware" abstraction
-* some useful Handler wrappers: filtering, falling through a list of handlers
+* some useful Handler wrappers: a router, request filtering, falling through a list of handlers
## Protocols
@@ -42,6 +42,16 @@ The gus/logging package provides everything you need to get a good basic start t
* A request-logging middleware with common diagnostics (time, duration, url, status codes, response body lengths)
* A simple constructor of useful default loggers at various levels. They output colorful logfmt lines to stdout.
+## Routing
+
+The router in the gus package supports slash-delimited path pattern strings. In the segments of these patterns:
+
+* A "/:wildcard/" segment matches anything in that position, and captures the value as a route parameter. Or if the paramter name is omitted like "/:/", it matches anything in a single segment without capturing a paramter.
+* A "/*remainder" segment is only allowed at the end and matches the rest of the path, capturing it into the paramter name. Or again, omitting a parameter name like "/*" simple matches any path suffix.
+* Any other segment in the pattern must match the corresponding segment of a request exactly.
+
+Router also supports maintaining a list of middlewares at the router level, mounting sub-routers under a pattern, looking up the matching handler for any request, and of course acting as a Handler itself.
+
## gus/contrib/*
This is where useful building blocks themselves start to come in. Sub-packages of contrib include Handler and Middleware implementations which accomplish the things your servers actually need to do.
diff --git a/contrib/cgi/gemini.go b/contrib/cgi/gemini.go
index 8302e7e..1587037 100644
--- a/contrib/cgi/gemini.go
+++ b/contrib/cgi/gemini.go
@@ -17,7 +17,7 @@ import (
// the URI path.
func GeminiCGIDirectory(pathRoot, fsRoot string) gus.Handler {
fsRoot = strings.TrimRight(fsRoot, "/")
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
if !strings.HasPrefix(request.Path, pathRoot) {
return nil
}
@@ -43,5 +43,5 @@ func GeminiCGIDirectory(pathRoot, fsRoot string) gus.Handler {
return gemini.Failure(err)
}
return response
- }
+ })
}
diff --git a/contrib/cgi/gopher.go b/contrib/cgi/gopher.go
index 29bfdba..4378eb7 100644
--- a/contrib/cgi/gopher.go
+++ b/contrib/cgi/gopher.go
@@ -17,7 +17,7 @@ import (
// the URI path.
func GopherCGIDirectory(pathRoot, fsRoot string) gus.Handler {
fsRoot = strings.TrimRight(fsRoot, "/")
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
if !strings.HasPrefix(request.Path, pathRoot) {
return nil
}
@@ -41,5 +41,5 @@ func GopherCGIDirectory(pathRoot, fsRoot string) gus.Handler {
}
return gopher.File(0, stdout)
- }
+ })
}
diff --git a/contrib/fs/dir_test.go b/contrib/fs/dir_test.go
index 6109a3c..9c6770d 100644
--- a/contrib/fs/dir_test.go
+++ b/contrib/fs/dir_test.go
@@ -47,7 +47,7 @@ func TestDirectoryDefault(t *testing.T) {
require.Nil(t, err)
request := &gus.Request{URL: u}
- response := handler(context.Background(), request)
+ response := handler.Handle(context.Background(), request)
if response == nil {
assert.Equal(t, test.status, gemini.StatusNotFound)
@@ -108,7 +108,7 @@ func TestDirectoryListing(t *testing.T) {
require.Nil(t, err)
request := &gus.Request{URL: u}
- response := handler(context.Background(), request)
+ response := handler.Handle(context.Background(), request)
if response == nil {
assert.Equal(t, test.status, gemini.StatusNotFound)
diff --git a/contrib/fs/file_test.go b/contrib/fs/file_test.go
index f97b66b..3949b83 100644
--- a/contrib/fs/file_test.go
+++ b/contrib/fs/file_test.go
@@ -58,7 +58,7 @@ func TestFileHandler(t *testing.T) {
require.Nil(t, err)
request := &gus.Request{URL: u}
- response := handler(context.Background(), request)
+ response := handler.Handle(context.Background(), request)
if response == nil {
assert.Equal(t, test.status, gemini.StatusNotFound)
diff --git a/contrib/fs/gemini.go b/contrib/fs/gemini.go
index 4193493..15677f1 100644
--- a/contrib/fs/gemini.go
+++ b/contrib/fs/gemini.go
@@ -14,7 +14,7 @@ import (
//
// It only serves responses for paths which do not correspond to directories on disk.
func GeminiFileHandler(fileSystem fs.FS) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
filepath, file, err := ResolveFile(request, fileSystem)
if err != nil {
return gemini.Failure(err)
@@ -25,7 +25,7 @@ func GeminiFileHandler(fileSystem fs.FS) gus.Handler {
}
return gemini.Success(mediaType(filepath), file)
- }
+ })
}
// GeminiDirectoryDefault serves up default files for directory path requests.
@@ -42,7 +42,7 @@ func GeminiFileHandler(fileSystem fs.FS) gus.Handler {
// It requires that files from the provided fs.FS implement fs.ReadDirFile. If they
// don't, it will produce nil responses for any directory paths.
func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
dirpath, dir, response := handleDirGemini(request, fileSystem)
if response != nil {
return response
@@ -61,7 +61,7 @@ func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
}
return gemini.Success(mediaType(filepath), file)
- }
+ })
}
// GeminiDirectoryListing produces a listing of the contents of any requested directories.
@@ -78,7 +78,7 @@ func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
// The template may be nil, in which case DefaultGeminiDirectoryList is used instead. The
// template is then processed with RenderDirectoryListing.
func GeminiDirectoryListing(fileSystem fs.FS, template *template.Template) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
dirpath, dir, response := handleDirGemini(request, fileSystem)
if response != nil {
return response
@@ -97,7 +97,7 @@ func GeminiDirectoryListing(fileSystem fs.FS, template *template.Template) gus.H
}
return gemini.Success("text/gemini", body)
- }
+ })
}
// DefaultGeminiDirectoryList is a template which renders a reasonable gemtext dir list.
diff --git a/contrib/fs/gopher.go b/contrib/fs/gopher.go
index 7b0d8bd..f63785c 100644
--- a/contrib/fs/gopher.go
+++ b/contrib/fs/gopher.go
@@ -16,7 +16,7 @@ import (
//
// It only serves responses for paths which correspond to files, not directories.
func GopherFileHandler(fileSystem fs.FS) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
filepath, file, err := ResolveFile(request, fileSystem)
if err != nil {
return gopher.Error(err).Response()
@@ -27,7 +27,7 @@ func GopherFileHandler(fileSystem fs.FS) gus.Handler {
}
return gopher.File(GuessGopherItemType(filepath), file)
- }
+ })
}
// GopherDirectoryDefault serves up default files for directory path requests.
@@ -40,7 +40,7 @@ func GopherFileHandler(fileSystem fs.FS) gus.Handler {
// It requires that files from the provided fs.FS implement fs.ReadDirFile. If
// they don't, it will produce nil responses for all directory paths.
func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
dirpath, dir, err := ResolveDirectory(request, fileSystem)
if err != nil {
return gopher.Error(err).Response()
@@ -59,7 +59,7 @@ func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
}
return gopher.File(gopher.MenuType, file)
- }
+ })
}
// GopherDirectoryListing produces a listing of the contents of any requested directories.
@@ -72,7 +72,7 @@ func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
// A template may be nil, in which case DefaultGopherDirectoryList is used instead. The
// template is then processed with RenderDirectoryListing.
func GopherDirectoryListing(fileSystem fs.FS, tpl *template.Template) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
dirpath, dir, err := ResolveDirectory(request, fileSystem)
if err != nil {
return gopher.Error(err).Response()
@@ -91,7 +91,7 @@ func GopherDirectoryListing(fileSystem fs.FS, tpl *template.Template) gus.Handle
}
return gopher.File(gopher.MenuType, body)
- }
+ })
}
// GopherTemplateFunctions is a map for templates providing useful functions for gophermaps.
diff --git a/contrib/sharedhost/replacement.go b/contrib/sharedhost/replacement.go
index 1fb2a0d..9267530 100644
--- a/contrib/sharedhost/replacement.go
+++ b/contrib/sharedhost/replacement.go
@@ -19,14 +19,14 @@ import (
// "users/", "domain.com/~jim/index.gmi" maps to "domain.com/users/jim/index.gmi".
func ReplaceTilde(replacement string) gus.Middleware {
return func(inner gus.Handler) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
if len(request.Path) > 1 && request.Path[0] == '/' && request.Path[1] == '~' {
request = cloneRequest(request)
request.Path = "/" + replacement + request.Path[2:]
}
- return inner(ctx, request)
- }
+ return inner.Handle(ctx, request)
+ })
}
}
diff --git a/contrib/sharedhost/replacement_test.go b/contrib/sharedhost/replacement_test.go
index cab80bb..67c3754 100644
--- a/contrib/sharedhost/replacement_test.go
+++ b/contrib/sharedhost/replacement_test.go
@@ -43,12 +43,12 @@ func TestReplaceTilde(t *testing.T) {
replacer := sharedhost.ReplaceTilde(test.replacement)
request := &gus.Request{URL: u}
- handler := replacer(func(_ context.Context, request *gus.Request) *gus.Response {
+ handler := replacer(gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
assert.Equal(t, test.replacedPath, request.Path)
return &gus.Response{}
- })
+ }))
- handler(context.Background(), request)
+ handler.Handle(context.Background(), request)
// original request was unmodified
assert.Equal(t, originalPath, request.Path)
diff --git a/contrib/tlsauth/auth_test.go b/contrib/tlsauth/auth_test.go
index 30b63f5..3cbc106 100644
--- a/contrib/tlsauth/auth_test.go
+++ b/contrib/tlsauth/auth_test.go
@@ -24,7 +24,7 @@ func TestIdentify(t *testing.T) {
server, client, clientCert := setup(t,
"testdata/server.crt", "testdata/server.key",
"testdata/client1.crt", "testdata/client1.key",
- func(_ context.Context, request *gus.Request) *gus.Response {
+ gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
invoked = true
ident := tlsauth.Identity(request)
@@ -33,7 +33,7 @@ func TestIdentify(t *testing.T) {
}
return nil
- },
+ }),
)
leafCert, err := x509.ParseCertificate(clientCert.Certificate[0])
require.Nil(t, err)
@@ -51,15 +51,15 @@ func TestRequiredAuth(t *testing.T) {
invoked1 := false
invoked2 := false
- handler1 := func(_ context.Context, request *gus.Request) *gus.Response {
+ handler1 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
invoked1 = true
return gemini.Success("", &bytes.Buffer{})
- }
+ })
- handler2 := func(_ context.Context, request *gus.Request) *gus.Response {
+ handler2 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
invoked2 = true
return gemini.Success("", &bytes.Buffer{})
- }
+ })
authMiddleware := gus.Filter(tlsauth.RequiredAuth(tlsauth.Allow), nil)
@@ -94,19 +94,19 @@ func TestOptionalAuth(t *testing.T) {
invoked1 := false
invoked2 := false
- handler1 := func(_ context.Context, request *gus.Request) *gus.Response {
+ handler1 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
if !strings.HasPrefix(request.Path, "/one") {
return nil
}
invoked1 = true
return gemini.Success("", &bytes.Buffer{})
- }
+ })
- handler2 := func(_ context.Context, request *gus.Request) *gus.Response {
+ handler2 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
invoked2 = true
return gemini.Success("", &bytes.Buffer{})
- }
+ })
mw := gus.Filter(tlsauth.OptionalAuth(tlsauth.Reject), nil)
handler := gus.FallthroughHandler(mw(handler1), mw(handler2))
diff --git a/contrib/tlsauth/gemini.go b/contrib/tlsauth/gemini.go
index 0db89de..40bee9e 100644
--- a/contrib/tlsauth/gemini.go
+++ b/contrib/tlsauth/gemini.go
@@ -14,7 +14,7 @@ import (
// not pass the approver it will be rejected with "62 certificate invalid".
func GeminiAuth(approver Approver) gus.Middleware {
return func(inner gus.Handler) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
identity := Identity(request)
if identity == nil {
return geminiMissingCert(ctx, request)
@@ -23,8 +23,8 @@ func GeminiAuth(approver Approver) gus.Middleware {
return geminiCertNotAuthorized(ctx, request)
}
- return inner(ctx, request)
- }
+ return inner.Handle(ctx, request)
+ })
}
}
@@ -35,14 +35,14 @@ func GeminiAuth(approver Approver) gus.Middleware {
// certificate, but it fails the approval.
func GeminiOptionalAuth(approver Approver) gus.Middleware {
return func(inner gus.Handler) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
identity := Identity(request)
if identity != nil && !approver(identity) {
return geminiCertNotAuthorized(ctx, request)
}
- return inner(ctx, request)
- }
+ return inner.Handle(ctx, request)
+ })
}
}
diff --git a/contrib/tlsauth/gemini_test.go b/contrib/tlsauth/gemini_test.go
index 8f1efda..7823de6 100644
--- a/contrib/tlsauth/gemini_test.go
+++ b/contrib/tlsauth/gemini_test.go
@@ -14,30 +14,30 @@ import (
)
func TestGeminiAuth(t *testing.T) {
- handler1 := func(_ context.Context, request *gus.Request) *gus.Response {
+ handler1 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
if !strings.HasPrefix(request.Path, "/one") {
return nil
}
return gemini.Success("", &bytes.Buffer{})
- }
- handler2 := func(_ context.Context, request *gus.Request) *gus.Response {
+ })
+ handler2 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
if !strings.HasPrefix(request.Path, "/two") {
return nil
}
return gemini.Success("", &bytes.Buffer{})
- }
- handler3 := func(_ context.Context, request *gus.Request) *gus.Response {
+ })
+ handler3 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
if !strings.HasPrefix(request.Path, "/three") {
return nil
}
return gemini.Success("", &bytes.Buffer{})
- }
- handler4 := func(_ context.Context, request *gus.Request) *gus.Response {
+ })
+ handler4 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
return gemini.Success("", &bytes.Buffer{})
- }
+ })
handler := gus.FallthroughHandler(
tlsauth.GeminiAuth(tlsauth.Allow)(handler1),
@@ -74,12 +74,12 @@ func TestGeminiAuth(t *testing.T) {
func TestGeminiOptionalAuth(t *testing.T) {
pathHandler := func(path string) gus.Handler {
- return func(_ context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
if !strings.HasPrefix(request.Path, path) {
return nil
}
return gemini.Success("", &bytes.Buffer{})
- }
+ })
}
handler := gus.FallthroughHandler(
diff --git a/examples/cowsay/main.go b/examples/cowsay/main.go
index 4a3f980..93f50d8 100644
--- a/examples/cowsay/main.go
+++ b/examples/cowsay/main.go
@@ -36,7 +36,7 @@ func main() {
server.Serve()
}
-func cowsayHandler(ctx context.Context, req *gus.Request) *gus.Response {
+var cowsayHandler = gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
// prompt for a query if there is none already
if req.RawQuery == "" {
return gemini.Input("enter a phrase")
@@ -82,7 +82,7 @@ func cowsayHandler(ctx context.Context, req *gus.Request) *gus.Response {
bytes.NewBufferString("\n```\n=> . again"),
)
return gemini.Success("text/gemini", out)
-}
+})
func envConfig() (string, string) {
certfile, ok := os.LookupEnv("SERVER_CERTIFICATE")
diff --git a/examples/inspectls/main.go b/examples/inspectls/main.go
index ce82f43..485b84e 100644
--- a/examples/inspectls/main.go
+++ b/examples/inspectls/main.go
@@ -54,11 +54,11 @@ func envConfig() (string, string) {
return certfile, keyfile
}
-func inspectHandler(ctx context.Context, req *gus.Request) *gus.Response {
+var inspectHandler = gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
// build and return a ```-wrapped description of the connection TLS state
body := "```\n" + displayTLSState(req.TLSState) + "\n```"
return gemini.Success("text/gemini", bytes.NewBufferString(body))
-}
+})
func displayTLSState(state *tls.ConnectionState) string {
builder := &strings.Builder{}
diff --git a/finger/serve.go b/finger/serve.go
index 8623de5..68b0fa5 100644
--- a/finger/serve.go
+++ b/finger/serve.go
@@ -58,7 +58,7 @@ func (fs *fingerServer) handleConn(conn net.Conn) {
_, _ = fmt.Fprint(conn, "Error handling request.\r\n")
}
}()
- response := fs.handler(fs.Ctx, request)
+ response := fs.handler.Handle(fs.Ctx, request)
if response == nil {
response = Error("No result found.")
}
diff --git a/finger/system.go b/finger/system.go
index 7112967..4bcf573 100644
--- a/finger/system.go
+++ b/finger/system.go
@@ -14,7 +14,7 @@ var ListingDenied = errors.New("Finger online user list denied.")
// SystemFinger handles finger requests by invoking the finger(1) command-line utility.
func SystemFinger(allowListings bool) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
fingerPath, err := exec.LookPath("finger")
if err != nil {
_ = request.Server.LogError(
@@ -44,5 +44,5 @@ func SystemFinger(allowListings bool) gus.Handler {
return Error(err.Error())
}
return Success(outbuf)
- }
+ })
}
diff --git a/gemini/roundtrip_test.go b/gemini/roundtrip_test.go
index a9d9b59..e8d2b48 100644
--- a/gemini/roundtrip_test.go
+++ b/gemini/roundtrip_test.go
@@ -20,9 +20,9 @@ func TestRoundTrip(t *testing.T) {
tlsConf, err := gemini.FileTLS("./testdata/server.crt", "./testdata/server.key")
require.Nil(t, err)
- handler := func(ctx context.Context, req *gus.Request) *gus.Response {
+ handler := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
return gemini.Success("text/gemini", bytes.NewBufferString("you've found my page"))
- }
+ })
server, err := gemini.NewServer(context.Background(), "localhost", "tcp", "127.0.0.1:0", handler, nil, tlsConf)
require.Nil(t, err)
@@ -54,7 +54,7 @@ func TestTitanRequest(t *testing.T) {
require.Nil(t, err)
invoked := false
- handler := func(ctx context.Context, request *gus.Request) *gus.Response {
+ handler := gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
invoked = true
body := ctx.Value(gemini.TitanRequestBody)
@@ -67,7 +67,7 @@ func TestTitanRequest(t *testing.T) {
assert.Equal(t, "the request body\n", string(bodyBytes))
return gemini.Success("", nil)
- }
+ })
server, err := gemini.NewServer(context.Background(), "localhost", "tcp", "127.0.0.1:0", handler, nil, tlsConf)
require.Nil(t, err)
diff --git a/gemini/serve.go b/gemini/serve.go
index 60e0242..2f93153 100644
--- a/gemini/serve.go
+++ b/gemini/serve.go
@@ -94,7 +94,7 @@ func (s *server) handleConn(conn net.Conn) {
_, _ = io.Copy(conn, NewResponseReader(Failure(err)))
}
}()
- response = s.handler(ctx, request)
+ response = s.handler.Handle(ctx, request)
if response == nil {
response = NotFound("Resource does not exist.")
}
@@ -127,12 +127,12 @@ func sizeParam(path string) (int, error) {
// Filtered requests will be turned away with a 53 response "proxy request refused".
func GeminiOnly(allowTitan bool) gus.Middleware {
return func(inner gus.Handler) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
if request.Scheme == "gemini" || (allowTitan && request.Scheme == "titan") {
- return inner(ctx, request)
+ return inner.Handle(ctx, request)
}
return RefuseProxy("Non-gemini protocol requests are not supported.")
- }
+ })
}
}
diff --git a/gopher/serve.go b/gopher/serve.go
index 84745d7..572fa55 100644
--- a/gopher/serve.go
+++ b/gopher/serve.go
@@ -61,7 +61,7 @@ func (gs *gopherServer) handleConn(conn net.Conn) {
_, _ = io.Copy(conn, rdr)
}
}()
- response = gs.handler(gs.Ctx, request)
+ response = gs.handler.Handle(gs.Ctx, request)
if response == nil {
response = Error(errors.New("Resource does not exist.")).Response()
}
diff --git a/handler.go b/handler.go
index a04cd33..7b784ed 100644
--- a/handler.go
+++ b/handler.go
@@ -2,11 +2,25 @@ package gus
import "context"
-// Handler is a function which can turn a request into a response.
+// Handler is a type which can turn a request into a response.
//
-// A Handler can return a nil response, in which case the Server is expected
+// Handle may return a nil response, in which case the Server is expected
// to build the protocol-appropriate "Not Found" response.
-type Handler func(context.Context, *Request) *Response
+type Handler interface {
+ Handle(context.Context, *Request) *Response
+}
+
+type handlerFunc func(context.Context, *Request) *Response
+
+// HandlerFunc is a wrapper to allow using a function as a Handler.
+func HandlerFunc(f func(context.Context, *Request) *Response) Handler {
+ return handlerFunc(f)
+}
+
+// Handle implements Handler.
+func (f handlerFunc) Handle(ctx context.Context, request *Request) *Response {
+ return f(ctx, request)
+}
// Middleware is a handler decorator.
//
@@ -19,14 +33,14 @@ type Middleware func(Handler) Handler
// The returned handler will invoke each of the passed-in handlers in order,
// stopping when it receives a non-nil response.
func FallthroughHandler(handlers ...Handler) Handler {
- return func(ctx context.Context, request *Request) *Response {
+ return HandlerFunc(func(ctx context.Context, request *Request) *Response {
for _, handler := range handlers {
- if response := handler(ctx, request); response != nil {
+ if response := handler.Handle(ctx, request); response != nil {
return response
}
}
return nil
- }
+ })
}
// Filter builds a middleware which only calls the wrapped Handler under a condition.
@@ -39,14 +53,14 @@ func Filter(
failure Handler,
) Middleware {
return func(success Handler) Handler {
- return func(ctx context.Context, request *Request) *Response {
+ return HandlerFunc(func(ctx context.Context, request *Request) *Response {
if condition(ctx, request) {
- return success(ctx, request)
+ return success.Handle(ctx, request)
}
if failure == nil {
return nil
}
- return failure(ctx, request)
- }
+ return failure.Handle(ctx, request)
+ })
}
}
diff --git a/handler_test.go b/handler_test.go
index a83ef3b..18ef562 100644
--- a/handler_test.go
+++ b/handler_test.go
@@ -13,19 +13,19 @@ import (
)
func TestFallthrough(t *testing.T) {
- h1 := func(ctx context.Context, req *gus.Request) *gus.Response {
+ h1 := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
if req.Path == "/one" {
return gemini.Success("text/gemini", bytes.NewBufferString("one"))
}
return nil
- }
+ })
- h2 := func(ctx context.Context, req *gus.Request) *gus.Response {
+ h2 := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
if req.Path == "/two" {
return gemini.Success("text/gemini", bytes.NewBufferString("two"))
}
return nil
- }
+ })
fth := gus.FallthroughHandler(h1, h2)
@@ -34,7 +34,7 @@ func TestFallthrough(t *testing.T) {
t.Fatalf("url.Parse: %s", err.Error())
}
- resp := fth(context.Background(), &gus.Request{URL: u})
+ resp := fth.Handle(context.Background(), &gus.Request{URL: u})
if resp.Status != gemini.StatusSuccess {
t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
@@ -57,7 +57,7 @@ func TestFallthrough(t *testing.T) {
t.Fatalf("url.Parse: %s", err.Error())
}
- resp = fth(context.Background(), &gus.Request{URL: u})
+ resp = fth.Handle(context.Background(), &gus.Request{URL: u})
if resp.Status != gemini.StatusSuccess {
t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
@@ -80,7 +80,7 @@ func TestFallthrough(t *testing.T) {
t.Fatalf("url.Parse: %s", err.Error())
}
- resp = fth(context.Background(), &gus.Request{URL: u})
+ resp = fth.Handle(context.Background(), &gus.Request{URL: u})
if resp != nil {
t.Errorf("expected nil, got %+v", resp)
@@ -91,9 +91,9 @@ func TestFilter(t *testing.T) {
pred := func(ctx context.Context, req *gus.Request) bool {
return strings.HasPrefix(req.Path, "/allow")
}
- base := func(ctx context.Context, req *gus.Request) *gus.Response {
+ base := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
return gemini.Success("text/gemini", bytes.NewBufferString("allowed!"))
- }
+ })
handler := gus.Filter(pred, nil)(base)
u, err := url.Parse("gemini://test.local/allow/please")
@@ -101,7 +101,7 @@ func TestFilter(t *testing.T) {
t.Fatalf("url.Parse: %s", err.Error())
}
- resp := handler(context.Background(), &gus.Request{URL: u})
+ resp := handler.Handle(context.Background(), &gus.Request{URL: u})
if resp.Status != gemini.StatusSuccess {
t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
}
@@ -111,7 +111,7 @@ func TestFilter(t *testing.T) {
t.Fatalf("url.Parse: %s", err.Error())
}
- resp = handler(context.Background(), &gus.Request{URL: u})
+ resp = handler.Handle(context.Background(), &gus.Request{URL: u})
if resp != nil {
t.Errorf("expected nil, got %+v", resp)
}
diff --git a/logging/middleware.go b/logging/middleware.go
index 5442203..4e23c1e 100644
--- a/logging/middleware.go
+++ b/logging/middleware.go
@@ -11,14 +11,14 @@ import (
func LogRequests(logger Logger) gus.Middleware {
return func(inner gus.Handler) gus.Handler {
- return func(ctx context.Context, request *gus.Request) *gus.Response {
- response := inner(ctx, request)
+ return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
+ response := inner.Handle(ctx, request)
if response != nil {
response.Body = loggingBody(logger, request, response)
}
return response
- }
+ })
}
}
diff --git a/logging/middleware_test.go b/logging/middleware_test.go
index 288c960..76406ef 100644
--- a/logging/middleware_test.go
+++ b/logging/middleware_test.go
@@ -14,11 +14,11 @@ import (
func TestLogRequests(t *testing.T) {
logger := logRecorder{}
- handler := logging.LogRequests(&logger)(func(_ context.Context, _ *gus.Request) *gus.Response {
+ handler := logging.LogRequests(&logger)(gus.HandlerFunc(func(_ context.Context, _ *gus.Request) *gus.Response {
return &gus.Response{}
- })
+ }))
- response := handler(context.Background(), &gus.Request{})
+ response := handler.Handle(context.Background(), &gus.Request{})
_, err := io.ReadAll(response.Body)
assert.Nil(t, err)
diff --git a/router.go b/router.go
index 41a3a04..71fd086 100644
--- a/router.go
+++ b/router.go
@@ -50,10 +50,10 @@ func (r Router) Handler(ctx context.Context, request *Request) *Response {
return nil
}
- return handler(context.WithValue(ctx, routeParamsKey, params), request)
+ return handler.Handle(context.WithValue(ctx, routeParamsKey, params), request)
}
-// Match returns the matched handler and captured path parameters, or nils.
+// Match returns the matched handler and captured path parameters, or (nil, nil).
//
// The returned handlers will be wrapped with any middleware attached to the router.
func (r Router) Match(request *Request) (Handler, map[string]string) {
diff --git a/router_test.go b/router_test.go
index 6f9c915..bfc48bd 100644
--- a/router_test.go
+++ b/router_test.go
@@ -13,24 +13,24 @@ import (
"tildegit.org/tjp/gus/gemini"
)
-func h1(_ context.Context, _ *gus.Request) *gus.Response {
+var h1 = gus.HandlerFunc(func(_ context.Context, _ *gus.Request) *gus.Response {
return gemini.Success("", &bytes.Buffer{})
-}
+})
func mw1(h gus.Handler) gus.Handler {
- return func(ctx context.Context, req *gus.Request) *gus.Response {
- resp := h(ctx, req)
+ return gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
+ resp := h.Handle(ctx, req)
resp.Body = io.MultiReader(resp.Body, bytes.NewBufferString("\nmiddleware 1"))
return resp
- }
+ })
}
func mw2(h gus.Handler) gus.Handler {
- return func(ctx context.Context, req *gus.Request) *gus.Response {
- resp := h(ctx, req)
+ return gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
+ resp := h.Handle(ctx, req)
resp.Body = io.MultiReader(resp.Body, bytes.NewBufferString("\nmiddleware 2"))
return resp
- }
+ })
}
func TestRouterUse(t *testing.T) {