diff --git a/api.go b/api.go index ae64f1a..4670408 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. @@ -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.Type == nil { toUpdate.Models.Request = r.Models.Request } mergeMap(toUpdate.Models.Responses, r.Models.Responses) @@ -176,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() @@ -198,7 +207,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), @@ -259,17 +268,25 @@ func (api *API) Trace(pattern string) (r *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 +func (rm *Route) HasResponseModel(status int, resp Model, opts ...RouteOpts) *Route { + rm.Models.Responses[status] = Response{ + Content: resp, + } + for _, o := range opts { + o(rm) + } 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 +// 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, + } + for _, o := range opts { + o(rm) + } return rm } @@ -303,10 +320,20 @@ 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. diff --git a/chiadapter/route_test.go b/chiadapter/route_test.go index 0a42575..39f7aa7 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) { 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..264ec66 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() { @@ -55,7 +56,10 @@ func main() { // 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. @@ -66,6 +70,9 @@ func main() { 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}"). 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..420a6d0 100644 --- a/examples/offline/main.go +++ b/examples/offline/main.go @@ -7,9 +7,10 @@ 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() { 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..1475909 100644 --- a/examples/stdlib/go.sum +++ b/examples/stdlib/go.sum @@ -12,8 +12,8 @@ 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= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -34,8 +34,8 @@ 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= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= @@ -47,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/main.go b/examples/stdlib/main.go index 6db3292..b0f0fa4 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() { diff --git a/examples/stdlib/models/models.go b/examples/stdlib/models/models.go index 31b79ce..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"` - Topic string `json:"topic"` - 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 3850b27..105cdb9 100644 --- a/schema.go +++ b/schema.go @@ -3,14 +3,17 @@ package rest import ( "fmt" "reflect" + "regexp" "slices" "sort" "strings" + "sync" - "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 +64,9 @@ func newPrimitiveSchema(paramType PrimitiveType) *openapi3.Schema { func (api *API) createOpenAPI() (spec *openapi3.T, err error) { spec = newSpec(api.Name) + if api.Version != "" { + spec.Info.Version = api.Version + } // Add all the routes. for pattern, methodToRoute := range api.Routes { path := &openapi3.PathItem{} @@ -106,33 +112,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.Type != 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.Type != 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) } @@ -287,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() @@ -326,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)) @@ -348,16 +364,49 @@ 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) + } + + 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 { 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) + } + + 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) { + if isFieldRequired(isPtr, hasOmitEmptySet) && validateRequired { schema.Required = append(schema.Required, fieldName) } } @@ -439,13 +488,108 @@ 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) } 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 +} + +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) +} diff --git a/schema_test.go b/schema_test.go index 2fc5041..491b969 100644 --- a/schema_test.go +++ b/schema_test.go @@ -10,8 +10,6 @@ import ( "testing" "time" - _ "embed" - "github.com/getkin/kin-openapi/openapi3" "github.com/google/go-cmp/cmp" "gopkg.in/yaml.v2"