Skip to content

Commit

Permalink
Add support for gorilla/mux (#165)
Browse files Browse the repository at this point in the history
* Refactor OpenAPI collector

* Clean up a few things

* Refactor OpenAPI dependencies to be more generalized

* Move handler options evaluation to constructor

* Fix some lint issues in examples

* Deprecate direct openapi3 access
  • Loading branch information
vearutop committed Aug 3, 2023
1 parent 18f55a4 commit e7f8540
Show file tree
Hide file tree
Showing 45 changed files with 1,315 additions and 461 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,10 @@ func main() {

![Documentation Page](./_examples/basic/screen.png)

## Additional Integrations

* [`github.com/gorilla/mux`](https://github.com/gorilla/mux), see [example](./gorillamux/example_openapi_collector_test.go).

## Performance Optimization

If top performance is critical for the service or particular endpoints, you can trade
Expand Down
5 changes: 5 additions & 0 deletions _examples/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ linters-settings:
linters:
enable-all: true
disable:
- funlen
- exhaustruct
- musttag
- nonamedreturns
- goerr113
- lll
- maligned
- gochecknoglobals
Expand Down
15 changes: 3 additions & 12 deletions _examples/advanced-generic/_testdata/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,7 @@
"schema":{"type":"boolean","description":"Invokes internal decoding of compressed data."}
}
],
"responses":{
"200":{
"description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}},
"content":{"application/json":{}}
}
},
"responses":{"200":{"description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}}}},
"x-forbid-unknown-query":true
}
},
Expand Down Expand Up @@ -470,13 +465,9 @@
"style":"simple","description":"Receives req value of X-Foo reduced by 30.",
"schema":{"type":"integer","description":"Receives req value of X-Foo reduced by 30."}
}
},
"content":{"application/json":{}}
}
},
"500":{
"description":"Internal Server Error",
"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}}
}
"500":{"description":"Internal Server Error"}
}
}
},
Expand Down
1 change: 1 addition & 0 deletions _examples/advanced-generic/gzip_pass_through.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func directGzip() usecase.Interactor {

// Precompute compressed data container. Generally this step should be owned by a caching storage of data.
dataFromCache := gzipPassThroughContainer{}

err := dataFromCache.PackJSON(rawData)
if err != nil {
panic(err)
Expand Down
2 changes: 2 additions & 0 deletions _examples/advanced-generic/gzip_pass_through_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func Test_directGzip(t *testing.T) {
require.NoError(t, err)

req.Header.Set("Accept-Encoding", "gzip")

rw := httptest.NewRecorder()

r.ServeHTTP(rw, req)
Expand All @@ -37,6 +38,7 @@ func Test_noDirectGzip(t *testing.T) {
require.NoError(t, err)

req.Header.Set("Accept-Encoding", "gzip")

rw := httptest.NewRecorder()

r.ServeHTTP(rw, req)
Expand Down
1 change: 1 addition & 0 deletions _examples/advanced-generic/json_slice_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/swaggest/usecase"
)

// JSONSlicePayload is an example non-scalar type without `json` tags.
type JSONSlicePayload []int

type jsonSliceReq struct {
Expand Down
1 change: 1 addition & 0 deletions _examples/advanced-generic/output_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func outputCSVWriter() usecase.Interactor {
out.ContentHash = contentHash

c := csv.NewWriter(out)

return c.WriteAll([][]string{{"abc", "def", "hij"}, {"klm", "nop", "qrs"}})
})

Expand Down
45 changes: 30 additions & 15 deletions _examples/advanced-generic/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/google/uuid"
"github.com/rs/cors"
"github.com/swaggest/jsonschema-go"
oapi "github.com/swaggest/openapi-go"
"github.com/swaggest/openapi-go/openapi3"
"github.com/swaggest/rest"
"github.com/swaggest/rest/nethttp"
Expand All @@ -27,12 +28,17 @@ import (
func NewRouter() http.Handler {
s := web.DefaultService()

s.OpenAPI.Info.Title = "Advanced Example"
s.OpenAPI.Info.WithDescription("This app showcases a variety of features.")
s.OpenAPI.Info.Version = "v1.2.3"
s.OpenAPICollector.Reflector().InterceptDefName(func(t reflect.Type, defaultDefName string) string {
return strings.ReplaceAll(defaultDefName, "Generic", "")
})
s.OpenAPISchema().SetTitle("Advanced Example")
s.OpenAPISchema().SetDescription("This app showcases a variety of features.")
s.OpenAPISchema().SetVersion("v1.2.3")

jsr := s.OpenAPIReflector().JSONSchemaReflector()

jsr.DefaultOptions = append(jsr.DefaultOptions, jsonschema.InterceptDefName(
func(t reflect.Type, defaultDefName string) string {
return strings.ReplaceAll(defaultDefName, "Generic", "")
},
))

// Usecase middlewares can be added to web.Service or chirouter.Wrapper.
s.Wrap(nethttp.UseCaseMiddlewares(usecase.MiddlewareFunc(func(next usecase.Interactor) usecase.Interactor {
Expand All @@ -47,7 +53,7 @@ func NewRouter() http.Handler {

return usecase.Interact(func(ctx context.Context, input, output interface{}) error {
err := next.Interact(ctx, input, output)
if err != nil && err != rest.HTTPCodeAsError(http.StatusNotModified) {
if err != nil && !errors.Is(err, rest.HTTPCodeAsError(http.StatusNotModified)) {
log.Printf("usecase %s request (%+v) failed: %v\n", name, input, err)
}

Expand All @@ -56,11 +62,11 @@ func NewRouter() http.Handler {
})))

// An example of global schema override to disable additionalProperties for all object schemas.
s.OpenAPICollector.Reflector().DefaultOptions = append(s.OpenAPICollector.Reflector().DefaultOptions,
jsr.DefaultOptions = append(jsr.DefaultOptions,
jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) {
// Allow unknown request headers and skip response.
if oc, ok := openapi3.OperationCtx(params.Context); !params.Processed || !ok ||
oc.ProcessingResponse || oc.ProcessingIn == string(rest.ParamInHeader) {
if oc, ok := oapi.OperationCtx(params.Context); !params.Processed || !ok ||
oc.IsProcessingResponse() || oc.ProcessingIn() == oapi.InHeader {
return false, nil
}

Expand All @@ -79,7 +85,7 @@ func NewRouter() http.Handler {
uuidDef.AddType(jsonschema.String)
uuidDef.WithFormat("uuid")
uuidDef.WithExamples("248df4b7-aa70-47b8-a036-33ac447e668d")
s.OpenAPICollector.Reflector().AddTypeMapping(uuid.UUID{}, uuidDef)
jsr.AddTypeMapping(uuid.UUID{}, uuidDef)

// When multiple structures can be returned with the same HTTP status code, it is possible to combine them into a
// single schema with such configuration.
Expand Down Expand Up @@ -130,7 +136,14 @@ func NewRouter() http.Handler {
)

// Annotations can be used to alter documentation of operation identified by method and path.
s.OpenAPICollector.Annotate(http.MethodPost, "/validation", func(op *openapi3.Operation) error {
s.OpenAPICollector.AnnotateOperation(http.MethodPost, "/validation", func(oc oapi.OperationContext) error {
o3, ok := oc.(openapi3.OperationExposer)
if !ok {
return nil
}

op := o3.Operation()

if op.Description != nil {
*op.Description = *op.Description + " Custom annotation."
}
Expand All @@ -153,8 +166,8 @@ func NewRouter() http.Handler {

s.Post("/json-map-body", jsonMapBody(),
// Annotate operation to add post-processing if necessary.
nethttp.AnnotateOperation(func(op *openapi3.Operation) error {
op.WithDescription("Request with JSON object (map) body.")
nethttp.AnnotateOpenAPIOperation(func(oc oapi.OperationContext) error {
oc.SetDescription("Request with JSON object (map) body.")

return nil
}))
Expand All @@ -181,7 +194,7 @@ func NewRouter() http.Handler {
s.Post("/no-validation", noValidation())

// Type mapping is necessary to pass interface as structure into documentation.
s.OpenAPICollector.Reflector().AddTypeMapping(new(gzipPassThroughOutput), new(gzipPassThroughStruct))
jsr.AddTypeMapping(new(gzipPassThroughOutput), new(gzipPassThroughStruct))
s.Get("/gzip-pass-through", directGzip())
s.Head("/gzip-pass-through", directGzip())

Expand All @@ -197,6 +210,8 @@ func NewRouter() http.Handler {
if c, err := r.Cookie("sessid"); err == nil {
r = r.WithContext(context.WithValue(r.Context(), "sessionID", c.Value))
}

handler.ServeHTTP(w, r)
})
}

Expand Down
10 changes: 2 additions & 8 deletions _examples/advanced/_testdata/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,7 @@
"schema":{"type":"boolean","description":"Invokes internal decoding of compressed data."}
}
],
"responses":{
"200":{
"description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}},
"content":{"application/dummy+json":{}}
}
},
"responses":{"200":{"description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}}}},
"x-forbid-unknown-query":true
}
},
Expand Down Expand Up @@ -358,8 +353,7 @@
"style":"simple","description":"Sample response header.",
"schema":{"type":"string","description":"Sample response header."}
}
},
"content":{"application/dummy+json":{}}
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions _examples/advanced/gzip_pass_through.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func directGzip() usecase.Interactor {

// Precompute compressed data container. Generally this step should be owned by a caching storage of data.
dataFromCache := gzipPassThroughContainer{}

err := dataFromCache.PackJSON(rawData)
if err != nil {
panic(err)
Expand Down
2 changes: 2 additions & 0 deletions _examples/advanced/gzip_pass_through_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func Test_directGzip(t *testing.T) {
require.NoError(t, err)

req.Header.Set("Accept-Encoding", "gzip")

rw := httptest.NewRecorder()

r.ServeHTTP(rw, req)
Expand All @@ -35,6 +36,7 @@ func Test_noDirectGzip(t *testing.T) {
require.NoError(t, err)

req.Header.Set("Accept-Encoding", "gzip")

rw := httptest.NewRecorder()

r.ServeHTTP(rw, req)
Expand Down
1 change: 1 addition & 0 deletions _examples/advanced/output_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func outputCSVWriter() usecase.Interactor {
out.Header = "abc"

c := csv.NewWriter(out)

return c.WriteAll([][]string{{"abc", "def", "hij"}, {"klm", "nop", "qrs"}})
})

Expand Down
33 changes: 22 additions & 11 deletions _examples/advanced/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/swaggest/jsonschema-go"
oapi "github.com/swaggest/openapi-go"
"github.com/swaggest/openapi-go/openapi3"
"github.com/swaggest/rest"
"github.com/swaggest/rest/nethttp"
Expand All @@ -23,16 +24,17 @@ func NewRouter() http.Handler {

s := web.DefaultService()

s.OpenAPI.Info.Title = "Advanced Example"
s.OpenAPI.Info.WithDescription("This app showcases a variety of features.")
s.OpenAPI.Info.Version = "v1.2.3"
s.OpenAPISchema().SetTitle("Advanced Example")
s.OpenAPISchema().SetDescription("This app showcases a variety of features.")
s.OpenAPISchema().SetVersion("v1.2.3")

// An example of global schema override to disable additionalProperties for all object schemas.
s.OpenAPICollector.Reflector().DefaultOptions = append(s.OpenAPICollector.Reflector().DefaultOptions,
jsr := s.OpenAPIReflector().JSONSchemaReflector()
jsr.DefaultOptions = append(jsr.DefaultOptions,
jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) {
// Allow unknown request headers and skip response.
if oc, ok := openapi3.OperationCtx(params.Context); !params.Processed || !ok ||
oc.ProcessingResponse || oc.ProcessingIn == string(rest.ParamInHeader) {
if oc, ok := oapi.OperationCtx(params.Context); !params.Processed || !ok ||
oc.IsProcessingResponse() || oc.ProcessingIn() == oapi.InHeader {
return false, nil
}

Expand All @@ -51,7 +53,7 @@ func NewRouter() http.Handler {
uuidDef.AddType(jsonschema.String)
uuidDef.WithFormat("uuid")
uuidDef.WithExamples("248df4b7-aa70-47b8-a036-33ac447e668d")
s.OpenAPICollector.Reflector().AddTypeMapping(uuid.UUID{}, uuidDef)
jsr.AddTypeMapping(uuid.UUID{}, uuidDef)

s.OpenAPICollector.CombineErrors = "anyOf"

Expand Down Expand Up @@ -88,7 +90,14 @@ func NewRouter() http.Handler {
)

// Annotations can be used to alter documentation of operation identified by method and path.
s.OpenAPICollector.Annotate(http.MethodPost, "/validation", func(op *openapi3.Operation) error {
s.OpenAPICollector.AnnotateOperation(http.MethodPost, "/validation", func(oc oapi.OperationContext) error {
o3, ok := oc.(openapi3.OperationExposer)
if !ok {
return nil
}

op := o3.Operation()

if op.Description != nil {
*op.Description = *op.Description + " Custom annotation."
}
Expand All @@ -108,8 +117,8 @@ func NewRouter() http.Handler {

s.Post("/json-map-body", jsonMapBody(),
// Annotate operation to add post-processing if necessary.
nethttp.AnnotateOperation(func(op *openapi3.Operation) error {
op.WithDescription("Request with JSON object (map) body.")
nethttp.AnnotateOpenAPIOperation(func(oc oapi.OperationContext) error {
oc.SetDescription("Request with JSON object (map) body.")

return nil
}))
Expand All @@ -134,7 +143,7 @@ func NewRouter() http.Handler {
s.Post("/no-validation", noValidation())

// Type mapping is necessary to pass interface as structure into documentation.
s.OpenAPICollector.Reflector().AddTypeMapping(new(gzipPassThroughOutput), new(gzipPassThroughStruct))
jsr.AddTypeMapping(new(gzipPassThroughOutput), new(gzipPassThroughStruct))
s.Get("/gzip-pass-through", directGzip())
s.Head("/gzip-pass-through", directGzip())

Expand All @@ -148,6 +157,8 @@ func NewRouter() http.Handler {
if c, err := r.Cookie("sessid"); err == nil {
r = r.WithContext(context.WithValue(r.Context(), "sessionID", c.Value))
}

handler.ServeHTTP(w, r)
})
}

Expand Down
6 changes: 3 additions & 3 deletions _examples/basic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ func main() {
s := web.DefaultService()

// Init API documentation schema.
s.OpenAPI.Info.Title = "Basic Example"
s.OpenAPI.Info.WithDescription("This app showcases a trivial REST API.")
s.OpenAPI.Info.Version = "v1.2.3"
s.OpenAPISchema().SetTitle("Basic Example")
s.OpenAPISchema().SetDescription("This app showcases a trivial REST API.")
s.OpenAPISchema().SetVersion("v1.2.3")

// Setup middlewares.
s.Wrap(
Expand Down
6 changes: 3 additions & 3 deletions _examples/generic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ func main() {
s := web.DefaultService()

// Init API documentation schema.
s.OpenAPI.Info.Title = "Basic Example"
s.OpenAPI.Info.WithDescription("This app showcases a trivial REST API.")
s.OpenAPI.Info.Version = "v1.2.3"
s.OpenAPISchema().SetTitle("Basic Example")
s.OpenAPISchema().SetDescription("This app showcases a trivial REST API.")
s.OpenAPISchema().SetVersion("v1.2.3")

// Setup middlewares.
s.Wrap(
Expand Down
Loading

0 comments on commit e7f8540

Please sign in to comment.