From f411ff1c3e67258961e27d9f52287adcf1a40392 Mon Sep 17 00:00:00 2001 From: Ronaldo Santana Date: Wed, 4 Sep 2024 15:29:01 +1200 Subject: [PATCH 1/7] Add description to request and response and version to the API. --- README.md | 8 ++-- api.go | 51 ++++++++++++++-------- chiadapter/route_test.go | 7 +-- examples/chiexample/go.mod | 4 +- examples/chiexample/go.sum | 2 + examples/chiexample/main.go | 31 ++++++++------ examples/offline/go.mod | 4 +- examples/offline/go.sum | 2 + examples/offline/main.go | 11 ++--- examples/stdlib/go.mod | 4 +- examples/stdlib/go.sum | 2 + examples/stdlib/main.go | 17 ++++---- schema.go | 40 ++++++++++------- schema_test.go | 85 ++++++++++++++++++------------------- 14 files changed, 154 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index ff454f3..43c4277 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ See the [./examples](./examples) directory for complete examples. ```go // Configure the models. -api := rest.NewAPI("messages") +api := rest.NewAPI("messages", "1.0.0") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} -api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { +api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { status := s.Properties["statusCode"] status.Value.WithMin(100).WithMax(600) }) @@ -57,11 +57,11 @@ router := http.NewServeMux() router.Handle("/topics", &get.Handler{}) router.Handle("/topic", &post.Handler{}) -api := rest.NewAPI("messages") +api := rest.NewAPI("messages", "1.0.0") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} // Register the error type with customisations. -api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { +api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { status := s.Properties["statusCode"] status.Value.WithMin(100).WithMax(600) }) diff --git a/api.go b/api.go index ae64f1a..ccba937 100644 --- a/api.go +++ b/api.go @@ -20,9 +20,10 @@ func WithApplyCustomSchemaToType(f func(t reflect.Type, s *openapi3.Schema)) API } // NewAPI creates a new API from the router. -func NewAPI(name string, opts ...APIOpts) *API { +func NewAPI(name string, version string, opts ...APIOpts) *API { api := &API{ Name: name, + Version: version, KnownTypes: defaultKnownTypes, Routes: make(map[Pattern]MethodToRoute), // map of model name to schema. @@ -121,6 +122,8 @@ type Pattern string type API struct { // Name of the API. Name string + // Version of the API. + Version string // Routes of the API. // From patterns, to methods, to route. Routes map[Pattern]MethodToRoute @@ -161,7 +164,7 @@ func (api *API) Merge(r Route) { toUpdate := api.Route(string(r.Method), string(r.Pattern)) mergeMap(toUpdate.Params.Path, r.Params.Path) mergeMap(toUpdate.Params.Query, r.Params.Query) - if toUpdate.Models.Request.Type == nil { + if toUpdate.Models.Request.Content == nil { toUpdate.Models.Request = r.Models.Request } mergeMap(toUpdate.Models.Responses, r.Models.Responses) @@ -198,7 +201,7 @@ func (api *API) Route(method, pattern string) (r *Route) { Method: Method(method), Pattern: Pattern(pattern), Models: Models{ - Responses: make(map[int]Model), + Responses: make(map[int]Response), }, Params: Params{ Path: make(map[string]PathParam), @@ -255,21 +258,25 @@ func (api *API) Trace(pattern string) (r *Route) { return api.Route(http.MethodTrace, pattern) } -// HasResponseModel configures a response for the route. +// HasResponse configures a response for the route. // Example: // -// api.Get("/user").HasResponseModel(http.StatusOK, rest.ModelOf[User]()) -func (rm *Route) HasResponseModel(status int, response Model) *Route { - rm.Models.Responses[status] = response +// api.Get("/user").HasResponse(http.StatusOK, rest.ModelOf[User]()) +func (rm *Route) HasResponse(status int, resp *Model, desc string) *Route { + rm.Models.Responses[status] = Response{ + Description: desc, + Content: resp, + } return rm } -// HasResponseModel configures the request model of the route. -// Example: -// -// api.Post("/user").HasRequestModel(http.StatusOK, rest.ModelOf[User]()) -func (rm *Route) HasRequestModel(request Model) *Route { - rm.Models.Request = request +// HasRequest configures the request model of the route. +// Example: api.Post("/user").HasRequest(http.StatusOK, rest.ModelOf[User]()) +func (rm *Route) HasRequest(request *Model, desc string) *Route { + rm.Models.Request = Request{ + Description: desc, + Content: request, + } return rm } @@ -303,14 +310,24 @@ func (rm *Route) HasDescription(description string) *Route { return rm } +type Request struct { + Description string + Content *Model +} + +type Response struct { + Description string + Content *Model +} + // Models defines the models used by a route. type Models struct { - Request Model - Responses map[int]Model + Request Request + Responses map[int]Response } // ModelOf creates a model of type T. -func ModelOf[T any]() Model { +func ModelOf[T any]() *Model { var t T m := Model{ Type: reflect.TypeOf(t), @@ -318,7 +335,7 @@ func ModelOf[T any]() Model { if sm, ok := any(t).(CustomSchemaApplier); ok { m.s = sm.ApplyCustomSchema } - return m + return &m } func modelFromType(t reflect.Type) Model { diff --git a/chiadapter/route_test.go b/chiadapter/route_test.go index 0a42575..f4f588b 100644 --- a/chiadapter/route_test.go +++ b/chiadapter/route_test.go @@ -4,10 +4,11 @@ import ( "net/http" "testing" - "github.com/a-h/rest" - "github.com/a-h/rest/chiadapter" "github.com/go-chi/chi/v5" "github.com/google/go-cmp/cmp" + + "github.com/a-h/rest" + "github.com/a-h/rest/chiadapter" ) func TestMerge(t *testing.T) { @@ -16,7 +17,7 @@ func TestMerge(t *testing.T) { router := chi.NewRouter() router.Method(http.MethodGet, pattern, http.RedirectHandler("/elsewhere", http.StatusMovedPermanently)) - api := rest.NewAPI("test") + api := rest.NewAPI("test", "1.0.0") // Act. err := chiadapter.Merge(api, router) diff --git a/examples/chiexample/go.mod b/examples/chiexample/go.mod index ded5047..aa2b40b 100644 --- a/examples/chiexample/go.mod +++ b/examples/chiexample/go.mod @@ -16,12 +16,12 @@ require ( require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/invopop/yaml v0.2.0 // indirect + github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect + golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/tools v0.20.0 // indirect diff --git a/examples/chiexample/go.sum b/examples/chiexample/go.sum index 4d7c8d2..bec0416 100644 --- a/examples/chiexample/go.sum +++ b/examples/chiexample/go.sum @@ -16,6 +16,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -38,6 +39,7 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= diff --git a/examples/chiexample/main.go b/examples/chiexample/main.go index 19ff00f..8d671bb 100644 --- a/examples/chiexample/main.go +++ b/examples/chiexample/main.go @@ -6,12 +6,13 @@ import ( "net/http" "github.com/a-h/respond" + "github.com/getkin/kin-openapi/openapi3" + "github.com/go-chi/chi/v5" + "github.com/a-h/rest" "github.com/a-h/rest/chiadapter" "github.com/a-h/rest/examples/chiexample/models" "github.com/a-h/rest/swaggerui" - "github.com/getkin/kin-openapi/openapi3" - "github.com/go-chi/chi/v5" ) func main() { @@ -51,35 +52,41 @@ func main() { }) // Create the API definition. - api := rest.NewAPI("Messaging API") + api := rest.NewAPI("Messaging API", "1.0.0") // Create the routes and parameters of the Router in the REST API definition with an // adapter, or do it manually. - chiadapter.Merge(api, router) + err := chiadapter.Merge(api, router) + if err != nil { + log.Fatalf("failed to create routes: %v", err) + } // Because this example is all in the main package, we can strip the `main_` namespace from // the types. api.StripPkgPaths = []string{"main", "github.com/a-h"} // It's possible to customise the OpenAPI schema for each type. - api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { + _, _, err = api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { status := s.Properties["statusCode"] status.Value.WithMin(100).WithMax(600) }) + if err != nil { + log.Fatalf("failed to register model: %v", err) + } // Document the routes. api.Get("/topic/{id}"). - HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()). - HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) + HasResponse(http.StatusOK, rest.ModelOf[models.TopicsGetResponse](), "topic response"). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "error response") api.Get("/topics"). - HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()). - HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) + HasResponse(http.StatusOK, rest.ModelOf[models.TopicsGetResponse](), "topic response"). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "error response") api.Post("/topics"). - HasRequestModel(rest.ModelOf[models.TopicsPostRequest]()). - HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsPostResponse]()). - HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) + HasRequest(rest.ModelOf[models.TopicsPostRequest](), "topic request"). + HasResponse(http.StatusOK, rest.ModelOf[models.TopicsPostResponse](), "topic response"). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "error response") // Create the spec. spec, err := api.Spec() diff --git a/examples/offline/go.mod b/examples/offline/go.mod index 1e1b9b2..3f3621f 100644 --- a/examples/offline/go.mod +++ b/examples/offline/go.mod @@ -15,12 +15,12 @@ require ( require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/invopop/yaml v0.2.0 // indirect + github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect + golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/tools v0.20.0 // indirect diff --git a/examples/offline/go.sum b/examples/offline/go.sum index 7324cda..1e92f19 100644 --- a/examples/offline/go.sum +++ b/examples/offline/go.sum @@ -14,6 +14,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -36,6 +37,7 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= diff --git a/examples/offline/main.go b/examples/offline/main.go index 8f413e4..ac26e55 100644 --- a/examples/offline/main.go +++ b/examples/offline/main.go @@ -7,17 +7,18 @@ import ( "os" "github.com/a-h/respond" + "github.com/getkin/kin-openapi/openapi3" + "github.com/a-h/rest" "github.com/a-h/rest/examples/offline/models" - "github.com/getkin/kin-openapi/openapi3" ) func main() { // Configure the models. - api := rest.NewAPI("messages") + api := rest.NewAPI("messages", "1.0.0") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} - api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { + api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { status := s.Properties["statusCode"] status.Value.WithMin(100).WithMax(600) }) @@ -27,8 +28,8 @@ func main() { Description: "id of the topic", Regexp: `\d+`, }). - HasResponseModel(http.StatusOK, rest.ModelOf[models.Topic]()). - HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()). + HasResponse(http.StatusOK, rest.ModelOf[models.Topic](), ""). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), ""). HasTags([]string{"Topic"}). HasDescription("Get one topic by id"). HasOperationID("getOneTopic") diff --git a/examples/stdlib/go.mod b/examples/stdlib/go.mod index 9787750..2cc7815 100644 --- a/examples/stdlib/go.mod +++ b/examples/stdlib/go.mod @@ -15,12 +15,12 @@ require ( require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/invopop/yaml v0.2.0 // indirect + github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect + golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/tools v0.20.0 // indirect diff --git a/examples/stdlib/go.sum b/examples/stdlib/go.sum index 7324cda..1e92f19 100644 --- a/examples/stdlib/go.sum +++ b/examples/stdlib/go.sum @@ -14,6 +14,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -36,6 +37,7 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= diff --git a/examples/stdlib/main.go b/examples/stdlib/main.go index 6db3292..1154424 100644 --- a/examples/stdlib/main.go +++ b/examples/stdlib/main.go @@ -6,11 +6,12 @@ import ( "net/http" "github.com/a-h/respond" + "github.com/getkin/kin-openapi/openapi3" + "github.com/a-h/rest" "github.com/a-h/rest/examples/stdlib/handlers/topic/post" "github.com/a-h/rest/examples/stdlib/handlers/topics/get" "github.com/a-h/rest/swaggerui" - "github.com/getkin/kin-openapi/openapi3" ) func main() { @@ -19,25 +20,25 @@ func main() { router.Handle("/topics", &get.Handler{}) router.Handle("/topic", &post.Handler{}) - api := rest.NewAPI("messages") + api := rest.NewAPI("messages", "1.0.0") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} // It's possible to customise the OpenAPI schema for each type. // You can use helper functions, or write your own function that works // directly on the openapi3.Schema type. - api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { + api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { status := s.Properties["statusCode"] status.Value.WithMin(100).WithMax(600) }) api.Get("/topics"). - HasResponseModel(http.StatusOK, rest.ModelOf[get.TopicsGetResponse]()). - HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) + HasResponse(http.StatusOK, rest.ModelOf[get.TopicsGetResponse](), ""). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "") api.Post("/topic"). - HasRequestModel(rest.ModelOf[post.TopicPostRequest]()). - HasResponseModel(http.StatusOK, rest.ModelOf[post.TopicPostResponse]()). - HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) + HasRequest(rest.ModelOf[post.TopicPostRequest](), ""). + HasResponse(http.StatusOK, rest.ModelOf[post.TopicPostResponse](), ""). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "") // Create the spec. spec, err := api.Spec() diff --git a/schema.go b/schema.go index 3850b27..000fa96 100644 --- a/schema.go +++ b/schema.go @@ -7,10 +7,11 @@ import ( "sort" "strings" - "github.com/a-h/rest/enums" - "github.com/a-h/rest/getcomments/parser" "github.com/getkin/kin-openapi/openapi3" "golang.org/x/exp/constraints" + + "github.com/a-h/rest/enums" + "github.com/a-h/rest/getcomments/parser" ) func newSpec(name string) *openapi3.T { @@ -61,6 +62,7 @@ func newPrimitiveSchema(paramType PrimitiveType) *openapi3.Schema { func (api *API) createOpenAPI() (spec *openapi3.T, err error) { spec = newSpec(api.Name) + spec.Info.Version = api.Version // Add all the routes. for pattern, methodToRoute := range api.Routes { path := &openapi3.PathItem{} @@ -106,33 +108,39 @@ func (api *API) createOpenAPI() (spec *openapi3.T, err error) { } // Handle request types. - if route.Models.Request.Type != nil { - name, schema, err := api.RegisterModel(route.Models.Request) + if route.Models.Request.Content != nil { + name, schema, err := api.RegisterModel(*route.Models.Request.Content) if err != nil { return spec, err } op.RequestBody = &openapi3.RequestBodyRef{ - Value: openapi3.NewRequestBody().WithContent(map[string]*openapi3.MediaType{ - "application/json": { - Schema: getSchemaReferenceOrValue(name, schema), - }, - }), + Value: openapi3.NewRequestBody(). + WithContent(map[string]*openapi3.MediaType{ + "application/json": { + Schema: getSchemaReferenceOrValue(name, schema), + }, + }). + WithDescription(route.Models.Request.Description), } } // Handle response types. - for status, model := range route.Models.Responses { - name, schema, err := api.RegisterModel(model) - if err != nil { - return spec, err - } + for status, response := range route.Models.Responses { resp := openapi3.NewResponse(). - WithDescription(""). - WithContent(map[string]*openapi3.MediaType{ + WithDescription(response.Description) + + if response.Content != nil { + name, schema, err := api.RegisterModel(*response.Content) + if err != nil { + return spec, err + } + resp.WithContent(map[string]*openapi3.MediaType{ "application/json": { Schema: getSchemaReferenceOrValue(name, schema), }, }) + } + op.AddResponse(status, resp) } diff --git a/schema_test.go b/schema_test.go index 2fc5041..02e0ec8 100644 --- a/schema_test.go +++ b/schema_test.go @@ -2,6 +2,7 @@ package rest import ( "embed" + _ "embed" "encoding/json" "fmt" "net/http" @@ -10,8 +11,6 @@ import ( "testing" "time" - _ "embed" - "github.com/getkin/kin-openapi/openapi3" "github.com/google/go-cmp/cmp" "gopkg.in/yaml.v2" @@ -193,8 +192,8 @@ func TestSchema(t *testing.T) { name: "test001.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequestModel(ModelOf[TestRequestType]()). - HasResponseModel(http.StatusOK, ModelOf[TestResponseType]()). + HasRequest(ModelOf[TestRequestType](), ""). + HasResponse(http.StatusOK, ModelOf[TestResponseType](), ""). HasDescription("Test request type description"). HasTags([]string{"TestRequest"}) return nil @@ -204,8 +203,8 @@ func TestSchema(t *testing.T) { name: "basic-data-types.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequestModel(ModelOf[AllBasicDataTypes]()). - HasResponseModel(http.StatusOK, ModelOf[AllBasicDataTypes]()). + HasRequest(ModelOf[AllBasicDataTypes](), ""). + HasResponse(http.StatusOK, ModelOf[AllBasicDataTypes](), ""). HasOperationID("postAllBasicDataTypes"). HasTags([]string{"BasicData"}). HasDescription("Post all basic data types description") @@ -216,8 +215,8 @@ func TestSchema(t *testing.T) { name: "basic-data-types-pointers.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequestModel(ModelOf[AllBasicDataTypesPointers]()). - HasResponseModel(http.StatusOK, ModelOf[AllBasicDataTypesPointers]()) + HasRequest(ModelOf[AllBasicDataTypesPointers](), ""). + HasResponse(http.StatusOK, ModelOf[AllBasicDataTypesPointers](), "") return nil }, }, @@ -225,8 +224,8 @@ func TestSchema(t *testing.T) { name: "omit-empty-fields.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequestModel(ModelOf[OmitEmptyFields]()). - HasResponseModel(http.StatusOK, ModelOf[OmitEmptyFields]()) + HasRequest(ModelOf[OmitEmptyFields](), ""). + HasResponse(http.StatusOK, ModelOf[OmitEmptyFields](), "") return nil }, }, @@ -234,8 +233,8 @@ func TestSchema(t *testing.T) { name: "anonymous-type.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequestModel(ModelOf[struct{ A string }]()). - HasResponseModel(http.StatusOK, ModelOf[struct{ B string }]()) + HasRequest(ModelOf[struct{ A string }](), ""). + HasResponse(http.StatusOK, ModelOf[struct{ B string }](), "") return nil }, }, @@ -243,10 +242,10 @@ func TestSchema(t *testing.T) { name: "embedded-structs.yaml", setup: func(api *API) error { api.Get("/embedded"). - HasResponseModel(http.StatusOK, ModelOf[EmbeddedStructA]()) + HasResponse(http.StatusOK, ModelOf[EmbeddedStructA](), "") api.Post("/test"). - HasRequestModel(ModelOf[WithEmbeddedStructs]()). - HasResponseModel(http.StatusOK, ModelOf[WithEmbeddedStructs]()) + HasRequest(ModelOf[WithEmbeddedStructs](), ""). + HasResponse(http.StatusOK, ModelOf[WithEmbeddedStructs](), "") return nil }, }, @@ -254,8 +253,8 @@ func TestSchema(t *testing.T) { name: "with-name-struct-tags.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequestModel(ModelOf[WithNameStructTags]()). - HasResponseModel(http.StatusOK, ModelOf[WithNameStructTags]()) + HasRequest(ModelOf[WithNameStructTags](), ""). + HasResponse(http.StatusOK, ModelOf[WithNameStructTags](), "") return nil }, }, @@ -263,22 +262,22 @@ func TestSchema(t *testing.T) { name: "known-types.yaml", setup: func(api *API) error { api.Route(http.MethodGet, "/test"). - HasResponseModel(http.StatusOK, ModelOf[KnownTypes]()) + HasResponse(http.StatusOK, ModelOf[KnownTypes](), "") return nil }, }, { name: "all-methods.yaml", setup: func(api *API) (err error) { - api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[OK]()) - api.Head("/head").HasResponseModel(http.StatusOK, ModelOf[OK]()) - api.Post("/post").HasResponseModel(http.StatusOK, ModelOf[OK]()) - api.Put("/put").HasResponseModel(http.StatusOK, ModelOf[OK]()) - api.Patch("/patch").HasResponseModel(http.StatusOK, ModelOf[OK]()) - api.Delete("/delete").HasResponseModel(http.StatusOK, ModelOf[OK]()) - api.Connect("/connect").HasResponseModel(http.StatusOK, ModelOf[OK]()) - api.Options("/options").HasResponseModel(http.StatusOK, ModelOf[OK]()) - api.Trace("/trace").HasResponseModel(http.StatusOK, ModelOf[OK]()) + api.Get("/get").HasResponse(http.StatusOK, ModelOf[OK](), "") + api.Head("/head").HasResponse(http.StatusOK, ModelOf[OK](), "") + api.Post("/post").HasResponse(http.StatusOK, ModelOf[OK](), "") + api.Put("/put").HasResponse(http.StatusOK, ModelOf[OK](), "") + api.Patch("/patch").HasResponse(http.StatusOK, ModelOf[OK](), "") + api.Delete("/delete").HasResponse(http.StatusOK, ModelOf[OK](), "") + api.Connect("/connect").HasResponse(http.StatusOK, ModelOf[OK](), "") + api.Options("/options").HasResponse(http.StatusOK, ModelOf[OK](), "") + api.Trace("/trace").HasResponse(http.StatusOK, ModelOf[OK](), "") return }, }, @@ -286,10 +285,10 @@ func TestSchema(t *testing.T) { name: "enums.yaml", setup: func(api *API) (err error) { // Register the enums and values. - api.RegisterModel(ModelOf[StringEnum](), WithEnumValues(StringEnumA, StringEnumB, StringEnumC)) - api.RegisterModel(ModelOf[IntEnum](), WithEnumValues(IntEnum1, IntEnum2, IntEnum3)) + api.RegisterModel(*ModelOf[StringEnum](), WithEnumValues(StringEnumA, StringEnumB, StringEnumC)) + api.RegisterModel(*ModelOf[IntEnum](), WithEnumValues(IntEnum1, IntEnum2, IntEnum3)) - api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithEnums]()) + api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithEnums](), "") return }, }, @@ -297,17 +296,17 @@ func TestSchema(t *testing.T) { name: "enum-constants.yaml", setup: func(api *API) (err error) { // Register the enums and values. - api.RegisterModel(ModelOf[StringEnum](), WithEnumConstants[StringEnum]()) - api.RegisterModel(ModelOf[IntEnum](), WithEnumConstants[IntEnum]()) + api.RegisterModel(*ModelOf[StringEnum](), WithEnumConstants[StringEnum]()) + api.RegisterModel(*ModelOf[IntEnum](), WithEnumConstants[IntEnum]()) - api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithEnums]()) + api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithEnums](), "") return }, }, { name: "with-maps.yaml", setup: func(api *API) (err error) { - api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithMaps]()) + api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithMaps](), "") return }, }, @@ -322,7 +321,7 @@ func TestSchema(t *testing.T) { HasPathParameter("userId", PathParam{ Description: "User ID", }). - HasResponseModel(http.StatusOK, ModelOf[User]()) + HasResponse(http.StatusOK, ModelOf[User](), "") return }, }, @@ -339,7 +338,7 @@ func TestSchema(t *testing.T) { HasPathParameter("userId", PathParam{ Description: "User ID", }). - HasResponseModel(http.StatusOK, ModelOf[User]()) + HasResponse(http.StatusOK, ModelOf[User](), "") return }, }, @@ -358,7 +357,7 @@ func TestSchema(t *testing.T) { Type: PrimitiveTypeString, Regexp: `field|otherField`, }). - HasResponseModel(http.StatusOK, ModelOf[User]()) + HasResponse(http.StatusOK, ModelOf[User](), "") return }, }, @@ -381,7 +380,7 @@ func TestSchema(t *testing.T) { s.Description = "The field to order the results by" }, }). - HasResponseModel(http.StatusOK, ModelOf[User]()) + HasResponse(http.StatusOK, ModelOf[User](), "") return }, }, @@ -389,7 +388,7 @@ func TestSchema(t *testing.T) { name: "multiple-dates-with-comments.yaml", setup: func(api *API) (err error) { api.Get("/dates"). - HasResponseModel(http.StatusOK, ModelOf[MultipleDateFieldsWithComments]()) + HasResponse(http.StatusOK, ModelOf[MultipleDateFieldsWithComments](), "") return }, }, @@ -397,9 +396,9 @@ func TestSchema(t *testing.T) { name: "custom-models.yaml", setup: func(api *API) (err error) { api.Get("/struct-with-customisation"). - HasResponseModel(http.StatusOK, ModelOf[StructWithCustomisation]()) + HasResponse(http.StatusOK, ModelOf[StructWithCustomisation](), "") api.Get("/struct-ptr-with-customisation"). - HasResponseModel(http.StatusOK, ModelOf[*StructWithCustomisation]()) + HasResponse(http.StatusOK, ModelOf[*StructWithCustomisation](), "") return }, }, @@ -437,7 +436,7 @@ func TestSchema(t *testing.T) { }, setup: func(api *API) error { api.Get("/"). - HasResponseModel(http.StatusOK, ModelOf[StructWithTags]()) + HasResponse(http.StatusOK, ModelOf[StructWithTags](), "") return nil }, }, @@ -471,7 +470,7 @@ func TestSchema(t *testing.T) { go func() { defer wg.Done() // Create the API. - api := NewAPI(test.name, test.opts...) + api := NewAPI(test.name, "1.0.0", test.opts...) api.StripPkgPaths = []string{"github.com/a-h/rest"} // Configure it. test.setup(api) From aa5eb97f70fb93b569e2449cd66cb72a6d02b629 Mon Sep 17 00:00:00 2001 From: Ronaldo Santana Date: Thu, 5 Sep 2024 09:03:06 +1200 Subject: [PATCH 2/7] Make the changes backward compatible. --- README.md | 4 +- api.go | 26 +++++++++---- chiadapter/route_test.go | 2 +- examples/chiexample/main.go | 16 ++++---- examples/offline/main.go | 6 +-- examples/stdlib/main.go | 12 +++--- schema.go | 5 ++- schema_test.go | 74 ++++++++++++++++++------------------- 8 files changed, 79 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 43c4277..87744f0 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ See the [./examples](./examples) directory for complete examples. ```go // Configure the models. -api := rest.NewAPI("messages", "1.0.0") +api := rest.NewAPI("messages") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { @@ -57,7 +57,7 @@ router := http.NewServeMux() router.Handle("/topics", &get.Handler{}) router.Handle("/topic", &post.Handler{}) -api := rest.NewAPI("messages", "1.0.0") +api := rest.NewAPI("messages") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} // Register the error type with customisations. diff --git a/api.go b/api.go index ccba937..02f8a5a 100644 --- a/api.go +++ b/api.go @@ -9,6 +9,7 @@ import ( ) type APIOpts func(*API) +type RouteOpts func(*Route) // WithApplyCustomSchemaToType enables customisation of types in the OpenAPI specification. // Apply customisation to a specific type by checking the t parameter. @@ -20,10 +21,9 @@ func WithApplyCustomSchemaToType(f func(t reflect.Type, s *openapi3.Schema)) API } // NewAPI creates a new API from the router. -func NewAPI(name string, version string, opts ...APIOpts) *API { +func NewAPI(name string, opts ...APIOpts) *API { api := &API{ Name: name, - Version: version, KnownTypes: defaultKnownTypes, Routes: make(map[Pattern]MethodToRoute), // map of model name to schema. @@ -179,6 +179,12 @@ func mergeMap[TKey comparable, TValue any](into, from map[TKey]TValue) { } } +// WithVersion sets the API version +func (api *API) WithVersion(version string) *API { + api.Version = version + return api +} + // Spec creates an OpenAPI 3.0 specification document for the API. func (api *API) Spec() (spec *openapi3.T, err error) { spec, err = api.createOpenAPI() @@ -262,20 +268,24 @@ func (api *API) Trace(pattern string) (r *Route) { // Example: // // api.Get("/user").HasResponse(http.StatusOK, rest.ModelOf[User]()) -func (rm *Route) HasResponse(status int, resp *Model, desc string) *Route { +func (rm *Route) HasResponse(status int, resp *Model, opts ...RouteOpts) *Route { rm.Models.Responses[status] = Response{ - Description: desc, - Content: resp, + Content: resp, + } + for _, o := range opts { + o(rm) } return rm } // HasRequest configures the request model of the route. // Example: api.Post("/user").HasRequest(http.StatusOK, rest.ModelOf[User]()) -func (rm *Route) HasRequest(request *Model, desc string) *Route { +func (rm *Route) HasRequest(request *Model, opts ...RouteOpts) *Route { rm.Models.Request = Request{ - Description: desc, - Content: request, + Content: request, + } + for _, o := range opts { + o(rm) } return rm } diff --git a/chiadapter/route_test.go b/chiadapter/route_test.go index f4f588b..39f7aa7 100644 --- a/chiadapter/route_test.go +++ b/chiadapter/route_test.go @@ -17,7 +17,7 @@ func TestMerge(t *testing.T) { router := chi.NewRouter() router.Method(http.MethodGet, pattern, http.RedirectHandler("/elsewhere", http.StatusMovedPermanently)) - api := rest.NewAPI("test", "1.0.0") + api := rest.NewAPI("test") // Act. err := chiadapter.Merge(api, router) diff --git a/examples/chiexample/main.go b/examples/chiexample/main.go index 8d671bb..7c6b987 100644 --- a/examples/chiexample/main.go +++ b/examples/chiexample/main.go @@ -52,7 +52,7 @@ func main() { }) // Create the API definition. - api := rest.NewAPI("Messaging API", "1.0.0") + api := rest.NewAPI("Messaging API") // Create the routes and parameters of the Router in the REST API definition with an // adapter, or do it manually. @@ -76,17 +76,17 @@ func main() { // Document the routes. api.Get("/topic/{id}"). - HasResponse(http.StatusOK, rest.ModelOf[models.TopicsGetResponse](), "topic response"). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "error response") + HasResponse(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) api.Get("/topics"). - HasResponse(http.StatusOK, rest.ModelOf[models.TopicsGetResponse](), "topic response"). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "error response") + HasResponse(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) api.Post("/topics"). - HasRequest(rest.ModelOf[models.TopicsPostRequest](), "topic request"). - HasResponse(http.StatusOK, rest.ModelOf[models.TopicsPostResponse](), "topic response"). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "error response") + HasRequest(rest.ModelOf[models.TopicsPostRequest]()). + HasResponse(http.StatusOK, rest.ModelOf[models.TopicsPostResponse]()). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) // Create the spec. spec, err := api.Spec() diff --git a/examples/offline/main.go b/examples/offline/main.go index ac26e55..b014c32 100644 --- a/examples/offline/main.go +++ b/examples/offline/main.go @@ -15,7 +15,7 @@ import ( func main() { // Configure the models. - api := rest.NewAPI("messages", "1.0.0") + api := rest.NewAPI("messages") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { @@ -28,8 +28,8 @@ func main() { Description: "id of the topic", Regexp: `\d+`, }). - HasResponse(http.StatusOK, rest.ModelOf[models.Topic](), ""). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), ""). + HasResponse(http.StatusOK, rest.ModelOf[models.Topic]()). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()). HasTags([]string{"Topic"}). HasDescription("Get one topic by id"). HasOperationID("getOneTopic") diff --git a/examples/stdlib/main.go b/examples/stdlib/main.go index 1154424..4fab878 100644 --- a/examples/stdlib/main.go +++ b/examples/stdlib/main.go @@ -20,7 +20,7 @@ func main() { router.Handle("/topics", &get.Handler{}) router.Handle("/topic", &post.Handler{}) - api := rest.NewAPI("messages", "1.0.0") + api := rest.NewAPI("messages") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} // It's possible to customise the OpenAPI schema for each type. @@ -32,13 +32,13 @@ func main() { }) api.Get("/topics"). - HasResponse(http.StatusOK, rest.ModelOf[get.TopicsGetResponse](), ""). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "") + HasResponse(http.StatusOK, rest.ModelOf[get.TopicsGetResponse]()). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) api.Post("/topic"). - HasRequest(rest.ModelOf[post.TopicPostRequest](), ""). - HasResponse(http.StatusOK, rest.ModelOf[post.TopicPostResponse](), ""). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "") + HasRequest(rest.ModelOf[post.TopicPostRequest]()). + HasResponse(http.StatusOK, rest.ModelOf[post.TopicPostResponse]()). + HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) // Create the spec. spec, err := api.Spec() diff --git a/schema.go b/schema.go index 000fa96..a2e59e9 100644 --- a/schema.go +++ b/schema.go @@ -62,7 +62,9 @@ func newPrimitiveSchema(paramType PrimitiveType) *openapi3.Schema { func (api *API) createOpenAPI() (spec *openapi3.T, err error) { spec = newSpec(api.Name) - spec.Info.Version = api.Version + if api.Version != "" { + spec.Info.Version = api.Version + } // Add all the routes. for pattern, methodToRoute := range api.Routes { path := &openapi3.PathItem{} @@ -289,6 +291,7 @@ func (api *API) RegisterModel(model Model, opts ...ModelOpts) (name string, sche var elementName string var elementSchema *openapi3.Schema + switch t.Kind() { case reflect.Slice, reflect.Array: elementName, elementSchema, err = api.RegisterModel(modelFromType(t.Elem())) diff --git a/schema_test.go b/schema_test.go index 02e0ec8..ee87145 100644 --- a/schema_test.go +++ b/schema_test.go @@ -192,8 +192,8 @@ func TestSchema(t *testing.T) { name: "test001.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[TestRequestType](), ""). - HasResponse(http.StatusOK, ModelOf[TestResponseType](), ""). + HasRequest(ModelOf[TestRequestType]()). + HasResponse(http.StatusOK, ModelOf[TestResponseType]()). HasDescription("Test request type description"). HasTags([]string{"TestRequest"}) return nil @@ -203,8 +203,8 @@ func TestSchema(t *testing.T) { name: "basic-data-types.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[AllBasicDataTypes](), ""). - HasResponse(http.StatusOK, ModelOf[AllBasicDataTypes](), ""). + HasRequest(ModelOf[AllBasicDataTypes]()). + HasResponse(http.StatusOK, ModelOf[AllBasicDataTypes]()). HasOperationID("postAllBasicDataTypes"). HasTags([]string{"BasicData"}). HasDescription("Post all basic data types description") @@ -215,8 +215,8 @@ func TestSchema(t *testing.T) { name: "basic-data-types-pointers.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[AllBasicDataTypesPointers](), ""). - HasResponse(http.StatusOK, ModelOf[AllBasicDataTypesPointers](), "") + HasRequest(ModelOf[AllBasicDataTypesPointers]()). + HasResponse(http.StatusOK, ModelOf[AllBasicDataTypesPointers]()) return nil }, }, @@ -224,8 +224,8 @@ func TestSchema(t *testing.T) { name: "omit-empty-fields.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[OmitEmptyFields](), ""). - HasResponse(http.StatusOK, ModelOf[OmitEmptyFields](), "") + HasRequest(ModelOf[OmitEmptyFields]()). + HasResponse(http.StatusOK, ModelOf[OmitEmptyFields]()) return nil }, }, @@ -233,8 +233,8 @@ func TestSchema(t *testing.T) { name: "anonymous-type.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[struct{ A string }](), ""). - HasResponse(http.StatusOK, ModelOf[struct{ B string }](), "") + HasRequest(ModelOf[struct{ A string }]()). + HasResponse(http.StatusOK, ModelOf[struct{ B string }]()) return nil }, }, @@ -242,10 +242,10 @@ func TestSchema(t *testing.T) { name: "embedded-structs.yaml", setup: func(api *API) error { api.Get("/embedded"). - HasResponse(http.StatusOK, ModelOf[EmbeddedStructA](), "") + HasResponse(http.StatusOK, ModelOf[EmbeddedStructA]()) api.Post("/test"). - HasRequest(ModelOf[WithEmbeddedStructs](), ""). - HasResponse(http.StatusOK, ModelOf[WithEmbeddedStructs](), "") + HasRequest(ModelOf[WithEmbeddedStructs]()). + HasResponse(http.StatusOK, ModelOf[WithEmbeddedStructs]()) return nil }, }, @@ -253,8 +253,8 @@ func TestSchema(t *testing.T) { name: "with-name-struct-tags.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[WithNameStructTags](), ""). - HasResponse(http.StatusOK, ModelOf[WithNameStructTags](), "") + HasRequest(ModelOf[WithNameStructTags]()). + HasResponse(http.StatusOK, ModelOf[WithNameStructTags]()) return nil }, }, @@ -262,22 +262,22 @@ func TestSchema(t *testing.T) { name: "known-types.yaml", setup: func(api *API) error { api.Route(http.MethodGet, "/test"). - HasResponse(http.StatusOK, ModelOf[KnownTypes](), "") + HasResponse(http.StatusOK, ModelOf[KnownTypes]()) return nil }, }, { name: "all-methods.yaml", setup: func(api *API) (err error) { - api.Get("/get").HasResponse(http.StatusOK, ModelOf[OK](), "") - api.Head("/head").HasResponse(http.StatusOK, ModelOf[OK](), "") - api.Post("/post").HasResponse(http.StatusOK, ModelOf[OK](), "") - api.Put("/put").HasResponse(http.StatusOK, ModelOf[OK](), "") - api.Patch("/patch").HasResponse(http.StatusOK, ModelOf[OK](), "") - api.Delete("/delete").HasResponse(http.StatusOK, ModelOf[OK](), "") - api.Connect("/connect").HasResponse(http.StatusOK, ModelOf[OK](), "") - api.Options("/options").HasResponse(http.StatusOK, ModelOf[OK](), "") - api.Trace("/trace").HasResponse(http.StatusOK, ModelOf[OK](), "") + api.Get("/get").HasResponse(http.StatusOK, ModelOf[OK]()) + api.Head("/head").HasResponse(http.StatusOK, ModelOf[OK]()) + api.Post("/post").HasResponse(http.StatusOK, ModelOf[OK]()) + api.Put("/put").HasResponse(http.StatusOK, ModelOf[OK]()) + api.Patch("/patch").HasResponse(http.StatusOK, ModelOf[OK]()) + api.Delete("/delete").HasResponse(http.StatusOK, ModelOf[OK]()) + api.Connect("/connect").HasResponse(http.StatusOK, ModelOf[OK]()) + api.Options("/options").HasResponse(http.StatusOK, ModelOf[OK]()) + api.Trace("/trace").HasResponse(http.StatusOK, ModelOf[OK]()) return }, }, @@ -288,7 +288,7 @@ func TestSchema(t *testing.T) { api.RegisterModel(*ModelOf[StringEnum](), WithEnumValues(StringEnumA, StringEnumB, StringEnumC)) api.RegisterModel(*ModelOf[IntEnum](), WithEnumValues(IntEnum1, IntEnum2, IntEnum3)) - api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithEnums](), "") + api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithEnums]()) return }, }, @@ -299,14 +299,14 @@ func TestSchema(t *testing.T) { api.RegisterModel(*ModelOf[StringEnum](), WithEnumConstants[StringEnum]()) api.RegisterModel(*ModelOf[IntEnum](), WithEnumConstants[IntEnum]()) - api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithEnums](), "") + api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithEnums]()) return }, }, { name: "with-maps.yaml", setup: func(api *API) (err error) { - api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithMaps](), "") + api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithMaps]()) return }, }, @@ -321,7 +321,7 @@ func TestSchema(t *testing.T) { HasPathParameter("userId", PathParam{ Description: "User ID", }). - HasResponse(http.StatusOK, ModelOf[User](), "") + HasResponse(http.StatusOK, ModelOf[User]()) return }, }, @@ -338,7 +338,7 @@ func TestSchema(t *testing.T) { HasPathParameter("userId", PathParam{ Description: "User ID", }). - HasResponse(http.StatusOK, ModelOf[User](), "") + HasResponse(http.StatusOK, ModelOf[User]()) return }, }, @@ -357,7 +357,7 @@ func TestSchema(t *testing.T) { Type: PrimitiveTypeString, Regexp: `field|otherField`, }). - HasResponse(http.StatusOK, ModelOf[User](), "") + HasResponse(http.StatusOK, ModelOf[User]()) return }, }, @@ -380,7 +380,7 @@ func TestSchema(t *testing.T) { s.Description = "The field to order the results by" }, }). - HasResponse(http.StatusOK, ModelOf[User](), "") + HasResponse(http.StatusOK, ModelOf[User]()) return }, }, @@ -388,7 +388,7 @@ func TestSchema(t *testing.T) { name: "multiple-dates-with-comments.yaml", setup: func(api *API) (err error) { api.Get("/dates"). - HasResponse(http.StatusOK, ModelOf[MultipleDateFieldsWithComments](), "") + HasResponse(http.StatusOK, ModelOf[MultipleDateFieldsWithComments]()) return }, }, @@ -396,9 +396,9 @@ func TestSchema(t *testing.T) { name: "custom-models.yaml", setup: func(api *API) (err error) { api.Get("/struct-with-customisation"). - HasResponse(http.StatusOK, ModelOf[StructWithCustomisation](), "") + HasResponse(http.StatusOK, ModelOf[StructWithCustomisation]()) api.Get("/struct-ptr-with-customisation"). - HasResponse(http.StatusOK, ModelOf[*StructWithCustomisation](), "") + HasResponse(http.StatusOK, ModelOf[*StructWithCustomisation]()) return }, }, @@ -436,7 +436,7 @@ func TestSchema(t *testing.T) { }, setup: func(api *API) error { api.Get("/"). - HasResponse(http.StatusOK, ModelOf[StructWithTags](), "") + HasResponse(http.StatusOK, ModelOf[StructWithTags]()) return nil }, }, @@ -470,7 +470,7 @@ func TestSchema(t *testing.T) { go func() { defer wg.Done() // Create the API. - api := NewAPI(test.name, "1.0.0", test.opts...) + api := NewAPI(test.name, test.opts...) api.StripPkgPaths = []string{"github.com/a-h/rest"} // Configure it. test.setup(api) From 0e990177edfe532444eafbd0cb557c3a760a5ff5 Mon Sep 17 00:00:00 2001 From: Ronaldo Santana Date: Thu, 5 Sep 2024 09:18:32 +1200 Subject: [PATCH 3/7] Undo some of the changes and make the code more compatible with current style. --- README.md | 4 +- api.go | 22 +++++----- examples/chiexample/main.go | 16 ++++---- examples/offline/main.go | 6 +-- examples/stdlib/main.go | 12 +++--- schema.go | 9 ++--- schema_test.go | 80 ++++++++++++++++++------------------- 7 files changed, 74 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 87744f0..ff454f3 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ See the [./examples](./examples) directory for complete examples. api := rest.NewAPI("messages") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} -api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { +api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { status := s.Properties["statusCode"] status.Value.WithMin(100).WithMax(600) }) @@ -61,7 +61,7 @@ api := rest.NewAPI("messages") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} // Register the error type with customisations. -api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { +api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { status := s.Properties["statusCode"] status.Value.WithMin(100).WithMax(600) }) diff --git a/api.go b/api.go index 02f8a5a..4670408 100644 --- a/api.go +++ b/api.go @@ -164,7 +164,7 @@ func (api *API) Merge(r Route) { toUpdate := api.Route(string(r.Method), string(r.Pattern)) mergeMap(toUpdate.Params.Path, r.Params.Path) mergeMap(toUpdate.Params.Query, r.Params.Query) - if toUpdate.Models.Request.Content == nil { + if toUpdate.Models.Request.Content.Type == nil { toUpdate.Models.Request = r.Models.Request } mergeMap(toUpdate.Models.Responses, r.Models.Responses) @@ -264,11 +264,11 @@ func (api *API) Trace(pattern string) (r *Route) { return api.Route(http.MethodTrace, pattern) } -// HasResponse configures a response for the route. +// HasResponseModel configures a response for the route. // Example: // -// api.Get("/user").HasResponse(http.StatusOK, rest.ModelOf[User]()) -func (rm *Route) HasResponse(status int, resp *Model, opts ...RouteOpts) *Route { +// api.Get("/user").HasResponseModel(http.StatusOK, rest.ModelOf[User]()) +func (rm *Route) HasResponseModel(status int, resp Model, opts ...RouteOpts) *Route { rm.Models.Responses[status] = Response{ Content: resp, } @@ -278,9 +278,9 @@ func (rm *Route) HasResponse(status int, resp *Model, opts ...RouteOpts) *Route return rm } -// HasRequest configures the request model of the route. -// Example: api.Post("/user").HasRequest(http.StatusOK, rest.ModelOf[User]()) -func (rm *Route) HasRequest(request *Model, opts ...RouteOpts) *Route { +// HasRequestModel configures the request model of the route. +// Example: api.Post("/user").HasRequestModel(http.StatusOK, rest.ModelOf[User]()) +func (rm *Route) HasRequestModel(request Model, opts ...RouteOpts) *Route { rm.Models.Request = Request{ Content: request, } @@ -322,12 +322,12 @@ func (rm *Route) HasDescription(description string) *Route { type Request struct { Description string - Content *Model + Content Model } type Response struct { Description string - Content *Model + Content Model } // Models defines the models used by a route. @@ -337,7 +337,7 @@ type Models struct { } // ModelOf creates a model of type T. -func ModelOf[T any]() *Model { +func ModelOf[T any]() Model { var t T m := Model{ Type: reflect.TypeOf(t), @@ -345,7 +345,7 @@ func ModelOf[T any]() *Model { if sm, ok := any(t).(CustomSchemaApplier); ok { m.s = sm.ApplyCustomSchema } - return &m + return m } func modelFromType(t reflect.Type) Model { diff --git a/examples/chiexample/main.go b/examples/chiexample/main.go index 7c6b987..264ec66 100644 --- a/examples/chiexample/main.go +++ b/examples/chiexample/main.go @@ -66,7 +66,7 @@ func main() { api.StripPkgPaths = []string{"main", "github.com/a-h"} // It's possible to customise the OpenAPI schema for each type. - _, _, err = api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { + api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { status := s.Properties["statusCode"] status.Value.WithMin(100).WithMax(600) }) @@ -76,17 +76,17 @@ func main() { // Document the routes. api.Get("/topic/{id}"). - HasResponse(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) + HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()). + HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) api.Get("/topics"). - HasResponse(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) + HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()). + HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) api.Post("/topics"). - HasRequest(rest.ModelOf[models.TopicsPostRequest]()). - HasResponse(http.StatusOK, rest.ModelOf[models.TopicsPostResponse]()). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) + HasRequestModel(rest.ModelOf[models.TopicsPostRequest]()). + HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsPostResponse]()). + HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) // Create the spec. spec, err := api.Spec() diff --git a/examples/offline/main.go b/examples/offline/main.go index b014c32..420a6d0 100644 --- a/examples/offline/main.go +++ b/examples/offline/main.go @@ -18,7 +18,7 @@ func main() { api := rest.NewAPI("messages") api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"} - api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { + api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { status := s.Properties["statusCode"] status.Value.WithMin(100).WithMax(600) }) @@ -28,8 +28,8 @@ func main() { Description: "id of the topic", Regexp: `\d+`, }). - HasResponse(http.StatusOK, rest.ModelOf[models.Topic]()). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()). + HasResponseModel(http.StatusOK, rest.ModelOf[models.Topic]()). + HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()). HasTags([]string{"Topic"}). HasDescription("Get one topic by id"). HasOperationID("getOneTopic") diff --git a/examples/stdlib/main.go b/examples/stdlib/main.go index 4fab878..b0f0fa4 100644 --- a/examples/stdlib/main.go +++ b/examples/stdlib/main.go @@ -26,19 +26,19 @@ func main() { // It's possible to customise the OpenAPI schema for each type. // You can use helper functions, or write your own function that works // directly on the openapi3.Schema type. - api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { + api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) { status := s.Properties["statusCode"] status.Value.WithMin(100).WithMax(600) }) api.Get("/topics"). - HasResponse(http.StatusOK, rest.ModelOf[get.TopicsGetResponse]()). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) + HasResponseModel(http.StatusOK, rest.ModelOf[get.TopicsGetResponse]()). + HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) api.Post("/topic"). - HasRequest(rest.ModelOf[post.TopicPostRequest]()). - HasResponse(http.StatusOK, rest.ModelOf[post.TopicPostResponse]()). - HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) + HasRequestModel(rest.ModelOf[post.TopicPostRequest]()). + HasResponseModel(http.StatusOK, rest.ModelOf[post.TopicPostResponse]()). + HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()) // Create the spec. spec, err := api.Spec() diff --git a/schema.go b/schema.go index a2e59e9..7f5613f 100644 --- a/schema.go +++ b/schema.go @@ -110,8 +110,8 @@ func (api *API) createOpenAPI() (spec *openapi3.T, err error) { } // Handle request types. - if route.Models.Request.Content != nil { - name, schema, err := api.RegisterModel(*route.Models.Request.Content) + if route.Models.Request.Content.Type != nil { + name, schema, err := api.RegisterModel(route.Models.Request.Content) if err != nil { return spec, err } @@ -131,8 +131,8 @@ func (api *API) createOpenAPI() (spec *openapi3.T, err error) { resp := openapi3.NewResponse(). WithDescription(response.Description) - if response.Content != nil { - name, schema, err := api.RegisterModel(*response.Content) + if response.Content.Type != nil { + name, schema, err := api.RegisterModel(response.Content) if err != nil { return spec, err } @@ -291,7 +291,6 @@ func (api *API) RegisterModel(model Model, opts ...ModelOpts) (name string, sche var elementName string var elementSchema *openapi3.Schema - switch t.Kind() { case reflect.Slice, reflect.Array: elementName, elementSchema, err = api.RegisterModel(modelFromType(t.Elem())) diff --git a/schema_test.go b/schema_test.go index ee87145..3156314 100644 --- a/schema_test.go +++ b/schema_test.go @@ -192,8 +192,8 @@ func TestSchema(t *testing.T) { name: "test001.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[TestRequestType]()). - HasResponse(http.StatusOK, ModelOf[TestResponseType]()). + HasRequestModel(ModelOf[TestRequestType]()). + HasResponseModel(http.StatusOK, ModelOf[TestResponseType]()). HasDescription("Test request type description"). HasTags([]string{"TestRequest"}) return nil @@ -203,8 +203,8 @@ func TestSchema(t *testing.T) { name: "basic-data-types.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[AllBasicDataTypes]()). - HasResponse(http.StatusOK, ModelOf[AllBasicDataTypes]()). + HasRequestModel(ModelOf[AllBasicDataTypes]()). + HasResponseModel(http.StatusOK, ModelOf[AllBasicDataTypes]()). HasOperationID("postAllBasicDataTypes"). HasTags([]string{"BasicData"}). HasDescription("Post all basic data types description") @@ -215,8 +215,8 @@ func TestSchema(t *testing.T) { name: "basic-data-types-pointers.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[AllBasicDataTypesPointers]()). - HasResponse(http.StatusOK, ModelOf[AllBasicDataTypesPointers]()) + HasRequestModel(ModelOf[AllBasicDataTypesPointers]()). + HasResponseModel(http.StatusOK, ModelOf[AllBasicDataTypesPointers]()) return nil }, }, @@ -224,8 +224,8 @@ func TestSchema(t *testing.T) { name: "omit-empty-fields.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[OmitEmptyFields]()). - HasResponse(http.StatusOK, ModelOf[OmitEmptyFields]()) + HasRequestModel(ModelOf[OmitEmptyFields]()). + HasResponseModel(http.StatusOK, ModelOf[OmitEmptyFields]()) return nil }, }, @@ -233,8 +233,8 @@ func TestSchema(t *testing.T) { name: "anonymous-type.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[struct{ A string }]()). - HasResponse(http.StatusOK, ModelOf[struct{ B string }]()) + HasRequestModel(ModelOf[struct{ A string }]()). + HasResponseModel(http.StatusOK, ModelOf[struct{ B string }]()) return nil }, }, @@ -242,10 +242,10 @@ func TestSchema(t *testing.T) { name: "embedded-structs.yaml", setup: func(api *API) error { api.Get("/embedded"). - HasResponse(http.StatusOK, ModelOf[EmbeddedStructA]()) + HasResponseModel(http.StatusOK, ModelOf[EmbeddedStructA]()) api.Post("/test"). - HasRequest(ModelOf[WithEmbeddedStructs]()). - HasResponse(http.StatusOK, ModelOf[WithEmbeddedStructs]()) + HasRequestModel(ModelOf[WithEmbeddedStructs]()). + HasResponseModel(http.StatusOK, ModelOf[WithEmbeddedStructs]()) return nil }, }, @@ -253,8 +253,8 @@ func TestSchema(t *testing.T) { name: "with-name-struct-tags.yaml", setup: func(api *API) error { api.Post("/test"). - HasRequest(ModelOf[WithNameStructTags]()). - HasResponse(http.StatusOK, ModelOf[WithNameStructTags]()) + HasRequestModel(ModelOf[WithNameStructTags]()). + HasResponseModel(http.StatusOK, ModelOf[WithNameStructTags]()) return nil }, }, @@ -262,22 +262,22 @@ func TestSchema(t *testing.T) { name: "known-types.yaml", setup: func(api *API) error { api.Route(http.MethodGet, "/test"). - HasResponse(http.StatusOK, ModelOf[KnownTypes]()) + HasResponseModel(http.StatusOK, ModelOf[KnownTypes]()) return nil }, }, { name: "all-methods.yaml", setup: func(api *API) (err error) { - api.Get("/get").HasResponse(http.StatusOK, ModelOf[OK]()) - api.Head("/head").HasResponse(http.StatusOK, ModelOf[OK]()) - api.Post("/post").HasResponse(http.StatusOK, ModelOf[OK]()) - api.Put("/put").HasResponse(http.StatusOK, ModelOf[OK]()) - api.Patch("/patch").HasResponse(http.StatusOK, ModelOf[OK]()) - api.Delete("/delete").HasResponse(http.StatusOK, ModelOf[OK]()) - api.Connect("/connect").HasResponse(http.StatusOK, ModelOf[OK]()) - api.Options("/options").HasResponse(http.StatusOK, ModelOf[OK]()) - api.Trace("/trace").HasResponse(http.StatusOK, ModelOf[OK]()) + api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[OK]()) + api.Head("/head").HasResponseModel(http.StatusOK, ModelOf[OK]()) + api.Post("/post").HasResponseModel(http.StatusOK, ModelOf[OK]()) + api.Put("/put").HasResponseModel(http.StatusOK, ModelOf[OK]()) + api.Patch("/patch").HasResponseModel(http.StatusOK, ModelOf[OK]()) + api.Delete("/delete").HasResponseModel(http.StatusOK, ModelOf[OK]()) + api.Connect("/connect").HasResponseModel(http.StatusOK, ModelOf[OK]()) + api.Options("/options").HasResponseModel(http.StatusOK, ModelOf[OK]()) + api.Trace("/trace").HasResponseModel(http.StatusOK, ModelOf[OK]()) return }, }, @@ -285,10 +285,10 @@ func TestSchema(t *testing.T) { name: "enums.yaml", setup: func(api *API) (err error) { // Register the enums and values. - api.RegisterModel(*ModelOf[StringEnum](), WithEnumValues(StringEnumA, StringEnumB, StringEnumC)) - api.RegisterModel(*ModelOf[IntEnum](), WithEnumValues(IntEnum1, IntEnum2, IntEnum3)) + api.RegisterModel(ModelOf[StringEnum](), WithEnumValues(StringEnumA, StringEnumB, StringEnumC)) + api.RegisterModel(ModelOf[IntEnum](), WithEnumValues(IntEnum1, IntEnum2, IntEnum3)) - api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithEnums]()) + api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithEnums]()) return }, }, @@ -296,17 +296,17 @@ func TestSchema(t *testing.T) { name: "enum-constants.yaml", setup: func(api *API) (err error) { // Register the enums and values. - api.RegisterModel(*ModelOf[StringEnum](), WithEnumConstants[StringEnum]()) - api.RegisterModel(*ModelOf[IntEnum](), WithEnumConstants[IntEnum]()) + api.RegisterModel(ModelOf[StringEnum](), WithEnumConstants[StringEnum]()) + api.RegisterModel(ModelOf[IntEnum](), WithEnumConstants[IntEnum]()) - api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithEnums]()) + api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithEnums]()) return }, }, { name: "with-maps.yaml", setup: func(api *API) (err error) { - api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithMaps]()) + api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithMaps]()) return }, }, @@ -321,7 +321,7 @@ func TestSchema(t *testing.T) { HasPathParameter("userId", PathParam{ Description: "User ID", }). - HasResponse(http.StatusOK, ModelOf[User]()) + HasResponseModel(http.StatusOK, ModelOf[User]()) return }, }, @@ -338,7 +338,7 @@ func TestSchema(t *testing.T) { HasPathParameter("userId", PathParam{ Description: "User ID", }). - HasResponse(http.StatusOK, ModelOf[User]()) + HasResponseModel(http.StatusOK, ModelOf[User]()) return }, }, @@ -357,7 +357,7 @@ func TestSchema(t *testing.T) { Type: PrimitiveTypeString, Regexp: `field|otherField`, }). - HasResponse(http.StatusOK, ModelOf[User]()) + HasResponseModel(http.StatusOK, ModelOf[User]()) return }, }, @@ -380,7 +380,7 @@ func TestSchema(t *testing.T) { s.Description = "The field to order the results by" }, }). - HasResponse(http.StatusOK, ModelOf[User]()) + HasResponseModel(http.StatusOK, ModelOf[User]()) return }, }, @@ -388,7 +388,7 @@ func TestSchema(t *testing.T) { name: "multiple-dates-with-comments.yaml", setup: func(api *API) (err error) { api.Get("/dates"). - HasResponse(http.StatusOK, ModelOf[MultipleDateFieldsWithComments]()) + HasResponseModel(http.StatusOK, ModelOf[MultipleDateFieldsWithComments]()) return }, }, @@ -396,9 +396,9 @@ func TestSchema(t *testing.T) { name: "custom-models.yaml", setup: func(api *API) (err error) { api.Get("/struct-with-customisation"). - HasResponse(http.StatusOK, ModelOf[StructWithCustomisation]()) + HasResponseModel(http.StatusOK, ModelOf[StructWithCustomisation]()) api.Get("/struct-ptr-with-customisation"). - HasResponse(http.StatusOK, ModelOf[*StructWithCustomisation]()) + HasResponseModel(http.StatusOK, ModelOf[*StructWithCustomisation]()) return }, }, @@ -436,7 +436,7 @@ func TestSchema(t *testing.T) { }, setup: func(api *API) error { api.Get("/"). - HasResponse(http.StatusOK, ModelOf[StructWithTags]()) + HasResponseModel(http.StatusOK, ModelOf[StructWithTags]()) return nil }, }, From d0dd6c112156967ff7f4c2395c83bfe52066f5bb Mon Sep 17 00:00:00 2001 From: Ronaldo Santana Date: Thu, 5 Sep 2024 09:20:33 +1200 Subject: [PATCH 4/7] Remove double import. --- schema_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/schema_test.go b/schema_test.go index 3156314..491b969 100644 --- a/schema_test.go +++ b/schema_test.go @@ -2,7 +2,6 @@ package rest import ( "embed" - _ "embed" "encoding/json" "fmt" "net/http" From e617bcc9002064dd05c080a391d64935a8b409f4 Mon Sep 17 00:00:00 2001 From: Ronaldo Santana Date: Thu, 17 Oct 2024 13:26:46 +1300 Subject: [PATCH 5/7] Add support to enum with the validate package. --- examples/stdlib/go.sum | 7 +-- examples/stdlib/models/models.go | 4 +- schema.go | 81 +++++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/examples/stdlib/go.sum b/examples/stdlib/go.sum index 1e92f19..1475909 100644 --- a/examples/stdlib/go.sum +++ b/examples/stdlib/go.sum @@ -12,8 +12,7 @@ github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= -github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -35,8 +34,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= -golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -49,6 +47,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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/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/stdlib/models/models.go b/examples/stdlib/models/models.go index 31b79ce..87a3c9f 100644 --- a/examples/stdlib/models/models.go +++ b/examples/stdlib/models/models.go @@ -2,8 +2,8 @@ package models // Topic of a thread. type Topic struct { - Namespace string `json:"namespace"` - Topic string `json:"topic"` + Namespace string `json:"namespace" validate:"required"` + Topic string `json:"topic" validate:"omitempty,oneof=general politics health"` Private bool `json:"private"` ViewCount int64 `json:"viewCount"` } diff --git a/schema.go b/schema.go index 7f5613f..0b8d637 100644 --- a/schema.go +++ b/schema.go @@ -3,9 +3,11 @@ package rest import ( "fmt" "reflect" + "regexp" "slices" "sort" "strings" + "sync" "github.com/getkin/kin-openapi/openapi3" "golang.org/x/exp/constraints" @@ -336,9 +338,13 @@ func (api *API) RegisterModel(model Model, opts ...ModelOpts) (name string, sche // Get JSON fieldName. jsonTags := strings.Split(f.Tag.Get("json"), ",") fieldName := jsonTags[0] + if fieldName == "-" { + continue + } if fieldName == "" { fieldName = f.Name } + // If the model doesn't exist. _, alreadyExists := api.models[api.getModelName(f.Type)] fieldSchemaName, fieldSchema, err := api.RegisterModel(modelFromType(f.Type)) @@ -358,16 +364,35 @@ func (api *API) RegisterModel(model Model, opts ...ModelOpts) (name string, sche schema.Required = append(schema.Required, fieldSchema.Required...) continue } + + // get the validate tag + // Get JSON fieldName. + validateTags := strings.Split(f.Tag.Get("validate"), ",") + validateRequired := !slices.Contains(validateTags, "omitempty") + var enumParams []string + if IsEnum(f) { + enumParams = parseOneOfParam(f) + } + ref := getSchemaReferenceOrValue(fieldSchemaName, fieldSchema) if ref.Value != nil { if ref.Value.Description, ref.Value.Deprecated, err = api.getTypeFieldComment(t.PkgPath(), t.Name(), f.Name); err != nil { return name, schema, fmt.Errorf("failed to get comments for field %q in type %q: %w", fieldName, name, err) } + if len(enumParams) > 0 { + ref.Value = ref.Value.WithEnum(enumParams) + } + + } else { + if len(enumParams) > 0 { + schema = schema.WithEnum(enumParams) + } } + schema.Properties[fieldName] = ref isPtr := f.Type.Kind() == reflect.Pointer hasOmitEmptySet := slices.Contains(jsonTags, "omitempty") - if isFieldRequired(isPtr, hasOmitEmptySet) { + if isFieldRequired(isPtr, hasOmitEmptySet) && validateRequired { schema.Required = append(schema.Required, fieldName) } } @@ -459,3 +484,57 @@ func (api *API) normalizeTypeName(pkgPath, name string) string { } return normalizer.Replace(pkgPath + "/" + name) } + +const ( + splitParamsRegexString = `'[^']*'|\S+` +) + +var ( + oneofValsCache = map[string][]string{} + oneofValsCacheRWLock = sync.RWMutex{} + splitParamsRegex = lazyRegexCompile(splitParamsRegexString) +) + +func lazyRegexCompile(str string) func() *regexp.Regexp { + var regex *regexp.Regexp + var once sync.Once + return func() *regexp.Regexp { + once.Do(func() { + regex = regexp.MustCompile(str) + }) + return regex + } +} + +func IsEnum(f reflect.StructField) bool { + return strings.Contains(f.Tag.Get("validate"), "oneof=") +} + +func parseOneOfParam(f reflect.StructField) []string { + + validateTags := strings.Split(f.Tag.Get("validate"), ",") + + var enumTag string + for _, validation := range validateTags { + if strings.Contains(validation, "oneof=") { + enumTag = validation + } + } + if enumTag == "" { + return []string{} + } + enumTag = strings.Replace(enumTag, "oneof=", "", 1) + oneofValsCacheRWLock.RLock() + vals, ok := oneofValsCache[enumTag] + oneofValsCacheRWLock.RUnlock() + if !ok { + oneofValsCacheRWLock.Lock() + vals = splitParamsRegex().FindAllString(enumTag, -1) + for i := 0; i < len(vals); i++ { + vals[i] = strings.Replace(vals[i], "'", "", -1) + } + oneofValsCache[enumTag] = vals + oneofValsCacheRWLock.Unlock() + } + return vals +} From b714dbbe26d5d9e935dabff7214bf8df4618c91a Mon Sep 17 00:00:00 2001 From: Ronaldo Santana Date: Thu, 17 Oct 2024 14:10:23 +1300 Subject: [PATCH 6/7] Handle package names better for more complex types. --- schema.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/schema.go b/schema.go index 0b8d637..7c0862e 100644 --- a/schema.go +++ b/schema.go @@ -474,10 +474,18 @@ var normalizer = strings.NewReplacer("/", "_", func (api *API) normalizeTypeName(pkgPath, name string) string { var omitPackage bool for _, pkg := range api.StripPkgPaths { + if strings.Contains(pkgPath, "query") { + fmt.Printf("PACKAGE %v :::: %v ::: %v \n", pkgPath, pkg, name) + } + if strings.HasPrefix(pkgPath, pkg) { omitPackage = true break } + + if strings.Contains(name, pkg) { + name = strings.Replace(name, pkg+".", "", 1) + } } if omitPackage || pkgPath == "" { return normalizer.Replace(name) From c5c74f32580c60e13607814acee8ae73dc89d11d Mon Sep 17 00:00:00 2001 From: Ronaldo Santana Date: Thu, 17 Oct 2024 17:01:49 +1300 Subject: [PATCH 7/7] Handle date and time better. --- examples/stdlib/models/models.go | 10 ++++--- schema.go | 51 ++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/examples/stdlib/models/models.go b/examples/stdlib/models/models.go index 87a3c9f..3eb7723 100644 --- a/examples/stdlib/models/models.go +++ b/examples/stdlib/models/models.go @@ -2,8 +2,10 @@ package models // Topic of a thread. type Topic struct { - Namespace string `json:"namespace" validate:"required"` - Topic string `json:"topic" validate:"omitempty,oneof=general politics health"` - Private bool `json:"private"` - ViewCount int64 `json:"viewCount"` + Namespace string `json:"namespace" validate:"required"` + Topic string `json:"topic" validate:"omitempty,oneof=general politics health"` + Private bool `json:"private"` + ViewCount int64 `json:"viewCount"` + Roles []string `json:"roles" validate:"gt=0,dive,oneof=admin user staff"` + CreatedAt string `json:"createdAt" validate:"datetime=2006-01-02T15:04:05Z07:00"` } diff --git a/schema.go b/schema.go index 7c0862e..105cdb9 100644 --- a/schema.go +++ b/schema.go @@ -299,7 +299,7 @@ func (api *API) RegisterModel(model Model, opts ...ModelOpts) (name string, sche if err != nil { return name, schema, fmt.Errorf("error getting schema of slice element %v: %w", t.Elem(), err) } - schema = openapi3.NewArraySchema().WithNullable() // Arrays are always nilable in Go. + schema = openapi3.NewArraySchema().WithNullable() // Arrays are always nullable in Go. schema.Items = getSchemaReferenceOrValue(elementName, elementSchema) case reflect.String: schema = openapi3.NewStringSchema() @@ -374,6 +374,11 @@ func (api *API) RegisterModel(model Model, opts ...ModelOpts) (name string, sche enumParams = parseOneOfParam(f) } + var dateFormat string + if IsDateTime(f) { + dateFormat = parseDateTimeParam(f) + } + ref := getSchemaReferenceOrValue(fieldSchemaName, fieldSchema) if ref.Value != nil { if ref.Value.Description, ref.Value.Deprecated, err = api.getTypeFieldComment(t.PkgPath(), t.Name(), f.Name); err != nil { @@ -383,14 +388,23 @@ func (api *API) RegisterModel(model Model, opts ...ModelOpts) (name string, sche ref.Value = ref.Value.WithEnum(enumParams) } + if dateFormat != "" { + ref.Value = ref.Value.WithFormat(dateFormat) + ref.Value.Example = dateFormat + } + } else { if len(enumParams) > 0 { schema = schema.WithEnum(enumParams) } + if dateFormat != "" { + ref.Value = ref.Value.WithFormat(dateFormat) + ref.Value.Example = dateFormat + } } schema.Properties[fieldName] = ref - isPtr := f.Type.Kind() == reflect.Pointer + isPtr := f.Type.Kind() == reflect.Pointer || f.Type.Kind() == reflect.Array hasOmitEmptySet := slices.Contains(jsonTags, "omitempty") if isFieldRequired(isPtr, hasOmitEmptySet) && validateRequired { schema.Required = append(schema.Required, fieldName) @@ -546,3 +560,36 @@ func parseOneOfParam(f reflect.StructField) []string { } return vals } + +func IsDateTime(f reflect.StructField) bool { + return strings.Contains(f.Tag.Get("validate"), "datetime=") +} + +func parseDateTimeParam(f reflect.StructField) string { + validateTags := strings.Split(f.Tag.Get("validate"), ",") + + var dateTimeTag string + for _, validation := range validateTags { + if strings.Contains(validation, "datetime=") { + dateTimeTag = validation + } + } + + return strings.Replace(dateTimeTag, "datetime=", "", 1) +} + +func parseSetValues(f reflect.StructField) []string { + validateTags := strings.Split(f.Tag.Get("validate"), ",") + + var diveTag string + for _, validation := range validateTags { + if validation == "dive" { + diveTag = validation + } + } + if diveTag == "" { + return []string{} + } + + return parseOneOfParam(f) +}