diff options
| author | tjpcc <tjp@ctrl-c.club> | 2023-02-15 16:44:29 -0700 | 
|---|---|---|
| committer | tjpcc <tjp@ctrl-c.club> | 2023-02-15 16:44:29 -0700 | 
| commit | 46ad450327111b9d28b592658d75ef57da498298 (patch) | |
| tree | 2b837bac9ae36d5a34dda06ba745850da216257d | |
| parent | bc96af40db6104580c22086c8db7c8119a404257 (diff) | |
Switch Handler to an interface.
HandlerFunc is much better as a function returning a Handler, rather
than a newtype for the function type itself. This way there is no
confusion creating a type-inferenced variable with HandlerFunc(func(...
and then using a HandlerFunc where a Handler is expected. Much better to
only have one public type.
| -rw-r--r-- | README.gmi | 12 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | contrib/cgi/gemini.go | 4 | ||||
| -rw-r--r-- | contrib/cgi/gopher.go | 4 | ||||
| -rw-r--r-- | contrib/fs/dir_test.go | 4 | ||||
| -rw-r--r-- | contrib/fs/file_test.go | 2 | ||||
| -rw-r--r-- | contrib/fs/gemini.go | 12 | ||||
| -rw-r--r-- | contrib/fs/gopher.go | 12 | ||||
| -rw-r--r-- | contrib/sharedhost/replacement.go | 6 | ||||
| -rw-r--r-- | contrib/sharedhost/replacement_test.go | 6 | ||||
| -rw-r--r-- | contrib/tlsauth/auth_test.go | 20 | ||||
| -rw-r--r-- | contrib/tlsauth/gemini.go | 12 | ||||
| -rw-r--r-- | contrib/tlsauth/gemini_test.go | 20 | ||||
| -rw-r--r-- | examples/cowsay/main.go | 4 | ||||
| -rw-r--r-- | examples/inspectls/main.go | 4 | ||||
| -rw-r--r-- | finger/serve.go | 2 | ||||
| -rw-r--r-- | finger/system.go | 4 | ||||
| -rw-r--r-- | gemini/roundtrip_test.go | 8 | ||||
| -rw-r--r-- | gemini/serve.go | 8 | ||||
| -rw-r--r-- | gopher/serve.go | 2 | ||||
| -rw-r--r-- | handler.go | 34 | ||||
| -rw-r--r-- | handler_test.go | 22 | ||||
| -rw-r--r-- | logging/middleware.go | 6 | ||||
| -rw-r--r-- | logging/middleware_test.go | 6 | ||||
| -rw-r--r-- | router.go | 4 | ||||
| -rw-r--r-- | router_test.go | 16 | 
26 files changed, 140 insertions, 106 deletions
| @@ -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. @@ -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()  		} @@ -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) @@ -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) { | 
