Repository: swaggest/rest (go)

Issue:
OpenAPI Collector not traversing child nodes, or parsing their mountpoints
I'm using this framework in some projects at work and really love it, but the rough edge I've run up _could_ be me missing something. 

In the go-chi world, it's common to have _different_ routers mounted to parent routers. This is a way of separating out / differentiating the middlewares as part of the radix tree. Thus your middlewares can be applied _closer_ to your domain objects in a way that makes sense. 

With this library, I've struggled with this, specifically because of the documentation side. If I create a default router:

```go
baseRouter := web.DefaultService()
```

But then try to add a sub router to it (for endpoints that require authentication via JWT for example)

```go
baseRouter := web.DefaultService()
authenticated := web.DefaultService()

baseRouter.Mount("/authenticated",authenticated)
```

Will work but without documentation. If I force the sub router to have the same collector as the parent during construction the docs are _present_ but missing the `/authenticated` prefix from the mount. 

I wrote [Usecase](https://github.com/muverum/usecase) as a workaround by basically allowing this kind of setup as the `UseCase` implementation layer as well to allow for HTTP level globals and the further down the stack to a more implementation specific one.

Is there something I'm just missing about how the collector is supposed to be traversing nodes to generate the docs? 

Pull request description:
Inspect wrapped handlers in Mount
Resolves #84.

Patch:
diff --git a/README.md b/README.md
index 021e63a..ce8e8b3 100644
--- a/README.md
+++ b/README.md
@@ -427,6 +427,26 @@ func main() {
 
 ![Documentation Page](./_examples/basic/screen.png)
 
+## Performance Optimization
+
+If top performance is critical for the service or particular endpoints, you can trade 
+simplicity for performance by implementing manual request loader on input type.
+
+```go
+func (i *myInput) LoadFromHTTPRequest(r *http.Request) (err error) {
+	i.Header = r.Header.Get("X-Header")
+
+	return nil
+}
+```
+
+If `request.Loader` is implemented, it will be called instead of both automatic decoding and validation.
+
+Check advanced [example](https://github.com/swaggest/rest/blob/v0.2.29/_examples/advanced-generic/json_body_manual.go#L58).
+
+To further improve performance you may try to use `fasthttp` instead of `net/http` with 
+[`rest-fasthttp`](https://github.com/swaggest/rest-fasthttp) fork.
+
 ## Versioning
 
 This project adheres to [Semantic Versioning](https://semver.org/#semantic-versioning-200).
diff --git a/_examples/advanced-generic/json_body_manual.go b/_examples/advanced-generic/json_body_manual.go
index 037e091..9de5d37 100644
--- a/_examples/advanced-generic/json_body_manual.go
+++ b/_examples/advanced-generic/json_body_manual.go
@@ -68,7 +68,7 @@ func (i *inputWithJSON) LoadFromHTTPRequest(r *http.Request) (err error) {
 	}
 
 	if err = json.Unmarshal(b, i); err != nil {
-		return fmt.Errorf("failsed to unmarshal request body: %w", err)
+		return fmt.Errorf("failed to unmarshal request body: %w", err)
 	}
 
 	i.Header = r.Header.Get("X-Header")
diff --git a/_examples/go.mod b/_examples/go.mod
index d1eaf19..3e0451e 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -6,13 +6,13 @@ replace github.com/swaggest/rest => ../
 
 require (
 	github.com/bool64/ctxd v1.1.2
-	github.com/bool64/dev v0.2.16
-	github.com/bool64/httpmock v0.1.1
+	github.com/bool64/dev v0.2.18
+	github.com/bool64/httpmock v0.1.6
 	github.com/bool64/httptestbench v0.1.3
 	github.com/go-chi/chi/v5 v5.0.7
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/rs/cors v1.8.2
-	github.com/stretchr/testify v1.7.2
+	github.com/stretchr/testify v1.8.0
 	github.com/swaggest/assertjson v1.7.0
 	github.com/swaggest/jsonschema-go v0.3.35
 	github.com/swaggest/openapi-go v0.2.18
@@ -25,7 +25,7 @@ require github.com/swaggest/rest v0.0.0-00010101000000-000000000000
 
 require (
 	github.com/andybalholm/brotli v1.0.4 // indirect
-	github.com/bool64/shared v0.1.4 // indirect
+	github.com/bool64/shared v0.1.5 // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
@@ -41,8 +41,6 @@ require (
 	github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff // indirect
 	github.com/yudai/gojsondiff v1.0.0 // indirect
 	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
-	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
-	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/_examples/go.sum b/_examples/go.sum
index 9fc1509..8148e6c 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -5,15 +5,14 @@ github.com/bool64/ctxd v1.1.2 h1:6J05q1vTG0op2O0kP+LosCd9wJ0qJybcvdE+bPjUJZE=
 github.com/bool64/ctxd v1.1.2/go.mod h1:5rwLykeZBJHK27q5sSfhfKig6076270V+3iiXbP+HFY=
 github.com/bool64/dev v0.1.35/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.1.41/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
-github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
-github.com/bool64/dev v0.2.16 h1:ZlybgWWXmHGMojqIjDrtl5QF6jmE4hNeojE00nioVk0=
-github.com/bool64/dev v0.2.16/go.mod h1:/csLrm+4oDSsKJRIVS0mrywAonLnYKFG8RvGT7Jh9b8=
-github.com/bool64/httpmock v0.1.1 h1:jpqM0S8efvJfN7Uy5fBUJKu2C640/ZS0yboxpeyVwm0=
-github.com/bool64/httpmock v0.1.1/go.mod h1:Ju7xrs8gVyxANbgIxoxX4Pkj1uHygzPEpGEnfqct+gA=
+github.com/bool64/dev v0.2.18 h1:FPXZxR4+bgNgBKtORwCr7W/s46bY/LkKFwVy8f63cqI=
+github.com/bool64/dev v0.2.18/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
+github.com/bool64/httpmock v0.1.6 h1:v9+AmoSabaHuR7abjnhGwGxsaNljtFAYtluF0p4Q3ng=
+github.com/bool64/httpmock v0.1.6/go.mod h1:pQukYUfoG9gW4fRAv+xHuvA2BlRoauEXK/XG5mKD04I=
 github.com/bool64/httptestbench v0.1.3 h1:bPDM3YPtE3o2sqIwjhdTC55wiqwfHsqps2RGcWBesuo=
 github.com/bool64/httptestbench v0.1.3/go.mod h1:3SoL5hfGcPylcOVSEF2+jJC8VG0oKMHlZYRoliBKbzE=
-github.com/bool64/shared v0.1.4 h1:zwtb1dl2QzDa9TJOq2jzDTdb5IPf9XlxTGKN8cySWT0=
-github.com/bool64/shared v0.1.4/go.mod h1:ryGjsnQFh6BnEXClfVlEJrzjwzat7CmA8PNS5E+jPp0=
+github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
+github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
 github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -49,9 +48,11 @@ github.com/santhosh-tekuri/jsonschema/v3 v3.1.0/go.mod h1:8kzK2TC0k0YjOForaAHdNE
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
-github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/swaggest/assertjson v1.7.0 h1:SKw5Rn0LQs6UvmGrIdaKQbMR1R3ncXm5KNon+QJ7jtw=
 github.com/swaggest/assertjson v1.7.0/go.mod h1:vxMJMehbSVJd+dDWFCKv3QRZKNTpy/ktZKTz9LOEDng=
 github.com/swaggest/form/v5 v5.0.1 h1:YQH0REX7iMKhtoVPWXREZgbt50VYXNCKK61psnD8Fgo=
@@ -82,15 +83,15 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf
 github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
 golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220702020025-31831981b65f h1:xdsejrW/0Wf2diT5CPp3XmKUNbr7Xvw8kYilQ+6qjRY=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -101,10 +102,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/_examples/mount/main.go b/_examples/mount/main.go
new file mode 100644
index 0000000..8ea6ba1
--- /dev/null
+++ b/_examples/mount/main.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"net/http"
+
+	"github.com/go-chi/chi/v5/middleware"
+	"github.com/swaggest/rest/nethttp"
+	"github.com/swaggest/rest/web"
+	swgui "github.com/swaggest/swgui/v4emb"
+	"github.com/swaggest/usecase"
+)
+
+func mul() usecase.Interactor {
+	return usecase.NewInteractor(func(ctx context.Context, input []int, output *int) error {
+		*output = 1
+
+		for _, v := range input {
+			*output *= v
+		}
+
+		return nil
+	})
+}
+
+func sum() usecase.Interactor {
+	return usecase.NewInteractor(func(ctx context.Context, input []int, output *int) error {
+		for _, v := range input {
+			*output += v
+		}
+
+		return nil
+	})
+}
+
+func main() {
+	service := web.DefaultService()
+	service.OpenAPI.Info.Title = "Security and Mount Example"
+
+	apiV1 := web.DefaultService()
+
+	apiV1.Wrap(
+		middleware.BasicAuth("Admin Access", map[string]string{"admin": "admin"}),
+		nethttp.HTTPBasicSecurityMiddleware(service.OpenAPICollector, "Admin", "Admin access"),
+	)
+
+	apiV1.Post("/sum", sum())
+	apiV1.Post("/mul", mul())
+
+	service.Mount("/api/v1", apiV1)
+	service.Docs("/api/v1/docs", swgui.New)
+
+	// Blanket handler, for example to serve static content.
+	service.Mount("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte("blanket handler got a request: " + r.URL.String()))
+	}))
+
+	fmt.Println("Swagger UI at http://localhost:8010/api/v1/docs.")
+	if err := http.ListenAndServe("localhost:8010", service); err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/chirouter/wrapper.go b/chirouter/wrapper.go
index b101192..7db2d94 100644
--- a/chirouter/wrapper.go
+++ b/chirouter/wrapper.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 
 	"github.com/go-chi/chi/v5"
+	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/nethttp"
 )
 
@@ -25,6 +26,7 @@ type Wrapper struct {
 
 	middlewares []func(http.Handler) http.Handler
 	wraps       []func(http.Handler) http.Handler
+	handlers    []http.Handler
 }
 
 var _ chi.Router = &Wrapper{}
@@ -89,8 +91,27 @@ func (r *Wrapper) Route(pattern string, fn func(r chi.Router)) chi.Router {
 
 // Mount attaches another http.Handler along "./basePattern/*".
 func (r *Wrapper) Mount(pattern string, h http.Handler) {
-	h = r.prepareHandler("", pattern, h)
-	r.captureHandler(h)
+	if hr, ok := h.(interface {
+		handlersWithRoute() []http.Handler
+		handlerWraps() []func(http.Handler) http.Handler
+	}); ok {
+		pattern = strings.TrimSuffix(pattern, "/")
+
+		for _, h := range hr.handlersWithRoute() {
+			var rh rest.HandlerWithRoute
+			if nethttp.HandlerAs(h, &rh) {
+				m := rh.RouteMethod()
+				p := r.resolvePattern(pattern + rh.RoutePattern())
+				h := nethttp.WrapHandler(h, nethttp.HandlerWithRouteMiddleware(m, p))
+				h = nethttp.WrapHandler(h, hr.handlerWraps()...)
+				_ = nethttp.WrapHandler(h, r.wraps...)
+			}
+		}
+	} else {
+		h = r.prepareHandler("", pattern, h)
+		r.captureHandler(h)
+	}
+
 	r.Router.Mount(pattern, h)
 }
 
@@ -167,9 +188,17 @@ func (r *Wrapper) captureHandler(h http.Handler) {
 }
 
 func (r *Wrapper) prepareHandler(method, pattern string, h http.Handler) http.Handler {
-	mw := r.wraps
-	mw = append(mw, nethttp.HandlerWithRouteMiddleware(method, r.resolvePattern(pattern)))
-	h = nethttp.WrapHandler(h, mw...)
+	h = nethttp.WrapHandler(h, nethttp.HandlerWithRouteMiddleware(method, r.resolvePattern(pattern)))
+	r.handlers = append(r.handlers, h)
+	h = nethttp.WrapHandler(h, r.wraps...)
 
 	return h
 }
+
+func (r *Wrapper) handlersWithRoute() []http.Handler {
+	return r.handlers
+}
+
+func (r *Wrapper) handlerWraps() []func(http.Handler) http.Handler {
+	return r.wraps
+}
diff --git a/go.mod b/go.mod
index d832ff4..c791cd6 100644
--- a/go.mod
+++ b/go.mod
@@ -3,13 +3,13 @@ module github.com/swaggest/rest
 go 1.17
 
 require (
-	github.com/bool64/dev v0.2.16
-	github.com/bool64/httpmock v0.1.1
-	github.com/bool64/shared v0.1.4
+	github.com/bool64/dev v0.2.18
+	github.com/bool64/httpmock v0.1.6
+	github.com/bool64/shared v0.1.5
 	github.com/cespare/xxhash/v2 v2.1.2
 	github.com/go-chi/chi/v5 v5.0.7
 	github.com/santhosh-tekuri/jsonschema/v3 v3.1.0
-	github.com/stretchr/testify v1.7.2
+	github.com/stretchr/testify v1.8.0
 	github.com/swaggest/assertjson v1.7.0
 	github.com/swaggest/form/v5 v5.0.1
 	github.com/swaggest/jsonschema-go v0.3.35
@@ -20,16 +20,15 @@ require (
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/fsnotify/fsnotify v1.4.9 // indirect
 	github.com/iancoleman/orderedmap v0.2.0 // indirect
-	github.com/mattn/go-isatty v0.0.14 // indirect
-	github.com/nxadm/tail v1.4.8 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/sergi/go-diff v1.2.0 // indirect
 	github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff // indirect
 	github.com/yudai/gojsondiff v1.0.0 // indirect
 	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
-	golang.org/x/net v0.0.0-20211105192438-b53810dc28af // indirect
-	golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect
+	golang.org/x/text v0.3.7 // indirect
+	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 8011432..6c06770 100644
--- a/go.sum
+++ b/go.sum
@@ -4,13 +4,16 @@ github.com/bool64/dev v0.1.41/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2z
 github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.2.10/go.mod h1:/csLrm+4oDSsKJRIVS0mrywAonLnYKFG8RvGT7Jh9b8=
 github.com/bool64/dev v0.2.12/go.mod h1:/csLrm+4oDSsKJRIVS0mrywAonLnYKFG8RvGT7Jh9b8=
-github.com/bool64/dev v0.2.16 h1:ZlybgWWXmHGMojqIjDrtl5QF6jmE4hNeojE00nioVk0=
 github.com/bool64/dev v0.2.16/go.mod h1:/csLrm+4oDSsKJRIVS0mrywAonLnYKFG8RvGT7Jh9b8=
-github.com/bool64/httpmock v0.1.1 h1:jpqM0S8efvJfN7Uy5fBUJKu2C640/ZS0yboxpeyVwm0=
-github.com/bool64/httpmock v0.1.1/go.mod h1:Ju7xrs8gVyxANbgIxoxX4Pkj1uHygzPEpGEnfqct+gA=
+github.com/bool64/dev v0.2.17/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
+github.com/bool64/dev v0.2.18 h1:FPXZxR4+bgNgBKtORwCr7W/s46bY/LkKFwVy8f63cqI=
+github.com/bool64/dev v0.2.18/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
+github.com/bool64/httpmock v0.1.6 h1:v9+AmoSabaHuR7abjnhGwGxsaNljtFAYtluF0p4Q3ng=
+github.com/bool64/httpmock v0.1.6/go.mod h1:pQukYUfoG9gW4fRAv+xHuvA2BlRoauEXK/XG5mKD04I=
 github.com/bool64/shared v0.1.3/go.mod h1:RF1p1Oi29ofgOvinBpetbF5mceOUP3kpMkvLbWOmtm0=
-github.com/bool64/shared v0.1.4 h1:zwtb1dl2QzDa9TJOq2jzDTdb5IPf9XlxTGKN8cySWT0=
 github.com/bool64/shared v0.1.4/go.mod h1:ryGjsnQFh6BnEXClfVlEJrzjwzat7CmA8PNS5E+jPp0=
+github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
+github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
 github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -65,10 +68,11 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
-github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/swaggest/assertjson v1.6.8/go.mod h1:Euf0upn9Vlaf1/llYHTs+Kx5K3vVbpMbsZhth7zlN7M=
 github.com/swaggest/assertjson v1.7.0 h1:SKw5Rn0LQs6UvmGrIdaKQbMR1R3ncXm5KNon+QJ7jtw=
 github.com/swaggest/assertjson v1.7.0/go.mod h1:vxMJMehbSVJd+dDWFCKv3QRZKNTpy/ktZKTz9LOEDng=
@@ -103,8 +107,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20211105192438-b53810dc28af h1:SMeNJG/vclJ5wyBBd4xupMsSJIHTd1coW9g7q6KOjmY=
-golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
+golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -118,17 +122,17 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
-golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220702020025-31831981b65f h1:xdsejrW/0Wf2diT5CPp3XmKUNbr7Xvw8kYilQ+6qjRY=
+golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=


Test patch:
diff --git a/.github/workflows/test-examples.yml b/.github/workflows/test-examples.yml
index 358958e..12e8551 100644
--- a/.github/workflows/test-examples.yml
+++ b/.github/workflows/test-examples.yml
@@ -11,7 +11,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [ 1.17.x, 1.18.x ]
+        go-version: [1.18.x ]
     runs-on: ubuntu-latest
     steps:
       - name: Install Go stable
diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml
index cb01973..fd1dd93 100644
--- a/.github/workflows/test-unit.yml
+++ b/.github/workflows/test-unit.yml
@@ -88,8 +88,10 @@ jobs:
         id: annotate
         if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != ''
         run: |
+          curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.3.6/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz
+          gocovdiff_hash=$(git hash-object ./gocovdiff)
+          [ "$gocovdiff_hash" == "8e507e0d671d4d6dfb3612309b72b163492f28eb" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1)
           git fetch origin master ${{ github.event.pull_request.base.sha }}
-          curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.3.4/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz && shasum -a 256 gocovdiff && echo "b351c67526eefeb0671c82e9271ae984875865eed19e911f40f78348cb98347c  gocovdiff" | shasum -c
           REP=$(./gocovdiff -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV})
           echo "${REP}"
           REP="${REP//$'\n'/%0A}"
diff --git a/chirouter/wrapper_test.go b/chirouter/wrapper_test.go
index 01bf740..3b3fe45 100644
--- a/chirouter/wrapper_test.go
+++ b/chirouter/wrapper_test.go
@@ -1,6 +1,9 @@
 package chirouter_test
 
 import (
+	"bytes"
+	"context"
+	"errors"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -10,9 +13,12 @@ import (
 	"github.com/go-chi/chi/v5/middleware"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"github.com/swaggest/assertjson"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/chirouter"
 	"github.com/swaggest/rest/nethttp"
+	"github.com/swaggest/rest/web"
+	"github.com/swaggest/usecase"
 )
 
 type HandlerWithFoo struct {
@@ -254,3 +260,99 @@ func TestWrapper_Use_StripSlashes(t *testing.T) {
 		"h", "h",
 	}, log)
 }
+
+func TestWrapper_Mount(t *testing.T) {
+	service := web.DefaultService()
+	service.OpenAPI.Info.Title = "Security and Mount Example"
+
+	apiV1 := web.DefaultService()
+
+	apiV1.Wrap(
+		middleware.BasicAuth("Admin Access", map[string]string{"admin": "admin"}),
+		nethttp.HTTPBasicSecurityMiddleware(service.OpenAPICollector, "Admin", "Admin access"),
+	)
+
+	apiV1.Post("/sum", usecase.NewIOI(new([]int), new(int), func(ctx context.Context, input, output interface{}) error {
+		return errors.New("oops")
+	}))
+
+	service.Mount("/api/v1", apiV1)
+
+	// Blanket handler, for example to serve static content.
+	service.Mount("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, err := w.Write([]byte("blanket handler got a request: " + r.URL.String()))
+		assert.NoError(t, err)
+	}))
+
+	req, err := http.NewRequest(http.MethodGet, "/foo", nil)
+	require.NoError(t, err)
+
+	rw := httptest.NewRecorder()
+	service.ServeHTTP(rw, req)
+
+	assert.Equal(t, "blanket handler got a request: /foo", rw.Body.String())
+
+	req, err = http.NewRequest(http.MethodPost, "/api/v1/sum", bytes.NewBufferString(`[1,2,3]`))
+	require.NoError(t, err)
+
+	rw = httptest.NewRecorder()
+
+	service.ServeHTTP(rw, req)
+	assert.Equal(t, http.StatusUnauthorized, rw.Code)
+
+	req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW4=")
+
+	rw = httptest.NewRecorder()
+
+	service.ServeHTTP(rw, req)
+	assert.Equal(t, `{"error":"oops"}`+"\n", rw.Body.String())
+
+	assertjson.EqualMarshal(t, []byte(`{
+	  "openapi":"3.0.3","info":{"title":"Security and Mount Example","version":""},
+	  "paths":{
+		"/api/v1/sum":{
+		  "post":{
+			"summary":"Test Wrapper _ Mount",
+			"operationId":"rest/chirouter_test.TestWrapper_Mount",
+			"requestBody":{
+			  "content":{
+				"application/json":{
+				  "schema":{"type":"array","items":{"type":"integer"},"nullable":true}
+				}
+			  }
+			},
+			"responses":{
+			  "200":{
+				"description":"OK",
+				"content":{"application/json":{"schema":{"type":"integer"}}}
+			  },
+			  "401":{
+				"description":"Unauthorized",
+				"content":{
+				  "application/json":{"schema":{"$ref":"#/components/schemas/RestErrResponse"}}
+				}
+			  }
+			},
+			"security":[{"Admin":[]}]
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "RestErrResponse":{
+			"type":"object",
+			"properties":{
+			  "code":{"type":"integer","description":"Application-specific error code."},
+			  "context":{
+				"type":"object","additionalProperties":{},
+				"description":"Application context."
+			  },
+			  "error":{"type":"string","description":"Error message."},
+			  "status":{"type":"string","description":"Status text."}
+			}
+		  }
+		},
+		"securitySchemes":{"Admin":{"type":"http","scheme":"basic","description":"Admin access"}}
+	  }
+	}`), service.OpenAPI)
+}