From eaa75c485c11bb4947394f680efd084d2aee517a Mon Sep 17 00:00:00 2001 From: Alexey Dudko Date: Fri, 9 Oct 2020 15:37:39 +0300 Subject: [PATCH 1/3] added generation of validation tags (https://github.com/go-playground/validator) --- pkg/codegen/schema.go | 153 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 14 deletions(-) diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index ebca0315e8..a6510c31ac 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -2,6 +2,7 @@ package codegen import ( "fmt" + "math" "strings" "github.com/getkin/kin-openapi/openapi3" @@ -55,11 +56,12 @@ func (s Schema) GetAdditionalTypeDefs() []TypeDefinition { } type Property struct { - Description string - JsonFieldName string - Schema Schema - Required bool - Nullable bool + Description string + JsonFieldName string + Schema Schema + Required bool + Nullable bool + validationTags string } func (p Property) GoFieldName() string { @@ -199,12 +201,19 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { if p.Value != nil { description = p.Value.Description } + var validationTags string + if p.Value.Type == "array" { + validationTags = goPlaygroundValidator(p.Value.Items.Value, required) + } else { + validationTags = goPlaygroundValidator(p.Value, required) + } prop := Property{ - JsonFieldName: pName, - Schema: pSchema, - Required: required, - Description: description, - Nullable: p.Value.Nullable, + JsonFieldName: pName, + Schema: pSchema, + Required: required, + Description: description, + Nullable: p.Value.Nullable, + validationTags: validationTags, } outSchema.Properties = append(outSchema.Properties, prop) } @@ -326,16 +335,132 @@ func GenFieldsFromProperties(props []Property) []string { field += fmt.Sprintf("\n%s\n", StringToGoComment(p.Description)) } field += fmt.Sprintf(" %s %s", p.GoFieldName(), p.GoTypeDef()) - if p.Required || p.Nullable { - field += fmt.Sprintf(" `json:\"%s\"`", p.JsonFieldName) - } else { - field += fmt.Sprintf(" `json:\"%s,omitempty\"`", p.JsonFieldName) + var validationTags string + if len(p.validationTags) > 0 { + validationTags = " " + p.validationTags } + field += fmt.Sprintf(" `json:\"%s\"%s`", jsonTag(p), validationTags) fields = append(fields, field) } return fields } +func jsonTag(p Property) string { + if p.Required || p.Nullable { + return p.JsonFieldName + } else { + return fmt.Sprintf("%s,omitempty", p.JsonFieldName) + } +} + +// adds validation tags +// https://github.com/go-playground/validator +func goPlaygroundValidator(s *openapi3.Schema, required bool) string { + var values []string + + if required { + values = append(values, "required") + } + + addMin := func(min float64) { + exclusiveMin := s.ExclusiveMin + if s.Type == "integer" { + // the "validator" panic when compare int with float + if min != math.Ceil(min) { + min = math.Ceil(min) + exclusiveMin = false + } + } + if exclusiveMin { + values = append(values, fmt.Sprintf("gt=%g", min)) + } else { + values = append(values, fmt.Sprintf("gte=%g", min)) + } + } + + addMax := func(max float64) { + exclusiveMax := s.ExclusiveMax + if s.Type == "integer" { + // the "validator" panic when compare int with float + if max != math.Floor(max) { + max = math.Floor(max) + exclusiveMax = false + } + } + if exclusiveMax { + values = append(values, fmt.Sprintf("lt=%g", max)) + } else { + values = append(values, fmt.Sprintf("lte=%g", max)) + } + } + + if s.Min != nil { + addMin(*s.Min) + } else if s.MinLength != 0 { + addMin(float64(s.MinLength)) + } else if s.MinItems != 0 { + addMin(float64(s.MinItems)) + } + + if s.Max != nil { + addMax(*s.Max) + } else if s.MaxLength != nil { + addMax(float64(*s.MaxLength)) + } else if s.MaxItems != nil { + addMax(float64(*s.MaxItems)) + } + + if s.MultipleOf != nil { + // todo: generate custom validation function MultipleOf + } + + if s.Format != "" { + switch s.Format { + case "int32": + case "int64": + case "float": + case "double": + case "byte": + case "binary": + case "date": + case "date-time": + case "password": + default: + values = append(values, s.Format) + } + } + + if s.Pattern != "" { + // todo: generate custom validation function with precompiled regex + } + + if len(s.Enum) > 0 { + var items []string + for _, item := range s.Enum { + typed := item.(string) + if strings.Contains(typed, " ") { + typed = fmt.Sprintf("'%s'", item) + } + items = append(items, typed) + } + values = append(values, "oneof="+strings.Join(items, " ")) + } + + if s.UniqueItems { + values = append(values, "unique") + } + + if s.MinProps > 0 { + // todo + } + + if s.MaxProps != nil { + // todo + } + + return fmt.Sprintf(`validate:"%s"`, strings.Join(values, ",")) +} + func GenStructFromSchema(schema Schema) string { // Start out with struct { objectParts := []string{"struct {"} From 10ee929520cf06070bf58b2d3d40f3d22c7f96ca Mon Sep 17 00:00:00 2001 From: Alexey Dudko Date: Mon, 12 Oct 2020 18:48:37 +0300 Subject: [PATCH 2/3] generate code after adding tag validation --- .../petstore-expanded/chi/api/petstore.gen.go | 10 +-- .../echo/api/petstore-types.gen.go | 10 +-- .../petstore-expanded/petstore-client.gen.go | 10 +-- go.mod | 5 +- go.sum | 14 +++- internal/test/client/client.gen.go | 4 +- internal/test/components/components.gen.go | 66 +++++++-------- internal/test/externalref/externalref.gen.go | 4 +- .../externalref/packageA/externalref.gen.go | 4 +- .../externalref/packageB/externalref.gen.go | 2 +- internal/test/issues/issue-52/issue.gen.go | 6 +- internal/test/parameters/parameters.gen.go | 10 +-- internal/test/schemas/schemas.gen.go | 20 ++--- internal/test/server/server.gen.go | 84 +++++++++---------- 14 files changed, 130 insertions(+), 119 deletions(-) diff --git a/examples/petstore-expanded/chi/api/petstore.gen.go b/examples/petstore-expanded/chi/api/petstore.gen.go index 1c5c0fbb6b..8703e0da01 100644 --- a/examples/petstore-expanded/chi/api/petstore.gen.go +++ b/examples/petstore-expanded/chi/api/petstore.gen.go @@ -20,20 +20,20 @@ import ( type Error struct { // Error code - Code int32 `json:"code"` + Code int32 `json:"code" validate:"required"` // Error message - Message string `json:"message"` + Message string `json:"message" validate:"required"` } // NewPet defines model for NewPet. type NewPet struct { // Name of the pet - Name string `json:"name"` + Name string `json:"name" validate:"required"` // Type of the pet - Tag *string `json:"tag,omitempty"` + Tag *string `json:"tag,omitempty" validate:""` } // Pet defines model for Pet. @@ -43,7 +43,7 @@ type Pet struct { // Embedded fields due to inline allOf schema // Unique id of the pet - Id int64 `json:"id"` + Id int64 `json:"id" validate:"required"` } // FindPetsParams defines parameters for FindPets. diff --git a/examples/petstore-expanded/echo/api/petstore-types.gen.go b/examples/petstore-expanded/echo/api/petstore-types.gen.go index 652b41e346..4435a10a20 100644 --- a/examples/petstore-expanded/echo/api/petstore-types.gen.go +++ b/examples/petstore-expanded/echo/api/petstore-types.gen.go @@ -7,20 +7,20 @@ package api type Error struct { // Error code - Code int32 `json:"code"` + Code int32 `json:"code" validate:"required"` // Error message - Message string `json:"message"` + Message string `json:"message" validate:"required"` } // NewPet defines model for NewPet. type NewPet struct { // Name of the pet - Name string `json:"name"` + Name string `json:"name" validate:"required"` // Type of the pet - Tag *string `json:"tag,omitempty"` + Tag *string `json:"tag,omitempty" validate:""` } // Pet defines model for Pet. @@ -30,7 +30,7 @@ type Pet struct { // Embedded fields due to inline allOf schema // Unique id of the pet - Id int64 `json:"id"` + Id int64 `json:"id" validate:"required"` } // FindPetsParams defines parameters for FindPets. diff --git a/examples/petstore-expanded/petstore-client.gen.go b/examples/petstore-expanded/petstore-client.gen.go index 7f562d8cc9..3dee515d58 100644 --- a/examples/petstore-expanded/petstore-client.gen.go +++ b/examples/petstore-expanded/petstore-client.gen.go @@ -21,20 +21,20 @@ import ( type Error struct { // Error code - Code int32 `json:"code"` + Code int32 `json:"code" validate:"required"` // Error message - Message string `json:"message"` + Message string `json:"message" validate:"required"` } // NewPet defines model for NewPet. type NewPet struct { // Name of the pet - Name string `json:"name"` + Name string `json:"name" validate:"required"` // Type of the pet - Tag *string `json:"tag,omitempty"` + Tag *string `json:"tag,omitempty" validate:""` } // Pet defines model for Pet. @@ -44,7 +44,7 @@ type Pet struct { // Embedded fields due to inline allOf schema // Unique id of the pet - Id int64 `json:"id"` + Id int64 `json:"id" validate:"required"` } // FindPetsParams defines parameters for FindPets. diff --git a/go.mod b/go.mod index 2e92758bbe..6ede64b935 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,9 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/getkin/kin-openapi v0.13.0 github.com/go-chi/chi v4.0.2+incompatible + github.com/go-playground/locales v0.13.0 + github.com/go-playground/universal-translator v0.17.0 + github.com/go-playground/validator/v10 v10.4.0 github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 github.com/kr/pretty v0.1.0 // indirect github.com/labstack/echo/v4 v4.1.11 @@ -14,10 +17,8 @@ require ( github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.5.1 github.com/valyala/fasttemplate v1.1.0 // indirect - golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 // indirect golang.org/x/net v0.0.0-20191112182307-2180aed22343 // indirect golang.org/x/sys v0.0.0-20191115151921-52ab43148777 // indirect - golang.org/x/text v0.3.2 // indirect golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.3.0 diff --git a/go.sum b/go.sum index 1fb5ee48c7..b6682cd463 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,14 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.0 h1:72qIR/m8ybvL8L5TIyfgrigqkrw7kVYAvjEvpT85l70= +github.com/go-playground/validator/v10 v10.4.0/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -24,6 +32,8 @@ github.com/labstack/echo/v4 v4.1.11 h1:z0BZoArY4FqdpUEl+wlHp4hnr/oSR6MTmQmv8OHSo github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd h1:HvFwW+cm9bCbZ/+vuGNq7CRWXql8c0y8nGeYpqmpvmk= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= @@ -54,8 +64,8 @@ github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4= -golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/internal/test/client/client.gen.go b/internal/test/client/client.gen.go index 09e2fb482e..9bcfa9f289 100644 --- a/internal/test/client/client.gen.go +++ b/internal/test/client/client.gen.go @@ -22,8 +22,8 @@ import ( // SchemaObject defines model for SchemaObject. type SchemaObject struct { - FirstName string `json:"firstName"` - Role string `json:"role"` + FirstName string `json:"firstName" validate:"required"` + Role string `json:"role" validate:"required"` } // PostBothJSONBody defines parameters for PostBoth. diff --git a/internal/test/components/components.gen.go b/internal/test/components/components.gen.go index 222b4c25d5..25932f6648 100644 --- a/internal/test/components/components.gen.go +++ b/internal/test/components/components.gen.go @@ -24,34 +24,34 @@ import ( // AdditionalPropertiesObject1 defines model for AdditionalPropertiesObject1. type AdditionalPropertiesObject1 struct { - Id int `json:"id"` - Name string `json:"name"` - Optional *string `json:"optional,omitempty"` + Id int `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Optional *string `json:"optional,omitempty" validate:""` AdditionalProperties map[string]int `json:"-"` } // AdditionalPropertiesObject2 defines model for AdditionalPropertiesObject2. type AdditionalPropertiesObject2 struct { - Id int `json:"id"` - Name string `json:"name"` + Id int `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` } // AdditionalPropertiesObject3 defines model for AdditionalPropertiesObject3. type AdditionalPropertiesObject3 struct { - Name string `json:"name"` + Name string `json:"name" validate:"required"` AdditionalProperties map[string]interface{} `json:"-"` } // AdditionalPropertiesObject4 defines model for AdditionalPropertiesObject4. type AdditionalPropertiesObject4 struct { - Inner AdditionalPropertiesObject4_Inner `json:"inner"` - Name string `json:"name"` + Inner AdditionalPropertiesObject4_Inner `json:"inner" validate:"required"` + Name string `json:"name" validate:"required"` AdditionalProperties map[string]interface{} `json:"-"` } // AdditionalPropertiesObject4_Inner defines model for AdditionalPropertiesObject4.Inner. type AdditionalPropertiesObject4_Inner struct { - Name string `json:"name"` + Name string `json:"name" validate:"required"` AdditionalProperties map[string]interface{} `json:"-"` } @@ -62,25 +62,25 @@ type AdditionalPropertiesObject5 struct { // ObjectWithJsonField defines model for ObjectWithJsonField. type ObjectWithJsonField struct { - Name string `json:"name"` - Value1 json.RawMessage `json:"value1"` - Value2 json.RawMessage `json:"value2,omitempty"` + Name string `json:"name" validate:"required"` + Value1 json.RawMessage `json:"value1" validate:"required,json"` + Value2 json.RawMessage `json:"value2,omitempty" validate:"json"` } // SchemaObject defines model for SchemaObject. type SchemaObject struct { - FirstName string `json:"firstName"` - Role string `json:"role"` + FirstName string `json:"firstName" validate:"required"` + Role string `json:"role" validate:"required"` } // ResponseObject defines model for ResponseObject. type ResponseObject struct { - Field SchemaObject `json:"Field"` + Field SchemaObject `json:"Field" validate:"required"` } // RequestBody defines model for RequestBody. type RequestBody struct { - Field SchemaObject `json:"Field"` + Field SchemaObject `json:"Field" validate:"required"` } // ParamsWithAddPropsParams_P1 defines parameters for ParamsWithAddProps. @@ -97,7 +97,7 @@ type ParamsWithAddPropsParams struct { // This parameter has an anonymous inner property which needs to be // turned into a proper type for additionalProperties to work P2 struct { - Inner ParamsWithAddPropsParams_P2_Inner `json:"inner"` + Inner ParamsWithAddPropsParams_P2_Inner `json:"inner" validate:"required"` } `json:"p2"` } @@ -108,8 +108,8 @@ type ParamsWithAddPropsParams_P2_Inner struct { // BodyWithAddPropsJSONBody defines parameters for BodyWithAddProps. type BodyWithAddPropsJSONBody struct { - Inner BodyWithAddPropsJSONBody_Inner `json:"inner"` - Name string `json:"name"` + Inner BodyWithAddPropsJSONBody_Inner `json:"inner" validate:"required"` + Name string `json:"name" validate:"required"` AdditionalProperties map[string]interface{} `json:"-"` } @@ -1062,23 +1062,23 @@ type EnsureEverythingIsReferencedResponse struct { JSON200 *struct { // Has additional properties with schema for dictionaries - Five *AdditionalPropertiesObject5 `json:"five,omitempty"` + Five *AdditionalPropertiesObject5 `json:"five,omitempty" validate:""` // Has anonymous field which has additional properties - Four *AdditionalPropertiesObject4 `json:"four,omitempty"` - JsonField *ObjectWithJsonField `json:"jsonField,omitempty"` + Four *AdditionalPropertiesObject4 `json:"four,omitempty" validate:""` + JsonField *ObjectWithJsonField `json:"jsonField,omitempty" validate:""` // Has additional properties of type int - One *AdditionalPropertiesObject1 `json:"one,omitempty"` + One *AdditionalPropertiesObject1 `json:"one,omitempty" validate:""` // Allows any additional property - Three *AdditionalPropertiesObject3 `json:"three,omitempty"` + Three *AdditionalPropertiesObject3 `json:"three,omitempty" validate:""` // Does not allow additional properties - Two *AdditionalPropertiesObject2 `json:"two,omitempty"` + Two *AdditionalPropertiesObject2 `json:"two,omitempty" validate:""` } JSONDefault *struct { - Field SchemaObject `json:"Field"` + Field SchemaObject `json:"Field" validate:"required"` } } @@ -1201,20 +1201,20 @@ func ParseEnsureEverythingIsReferencedResponse(rsp *http.Response) (*EnsureEvery var dest struct { // Has additional properties with schema for dictionaries - Five *AdditionalPropertiesObject5 `json:"five,omitempty"` + Five *AdditionalPropertiesObject5 `json:"five,omitempty" validate:""` // Has anonymous field which has additional properties - Four *AdditionalPropertiesObject4 `json:"four,omitempty"` - JsonField *ObjectWithJsonField `json:"jsonField,omitempty"` + Four *AdditionalPropertiesObject4 `json:"four,omitempty" validate:""` + JsonField *ObjectWithJsonField `json:"jsonField,omitempty" validate:""` // Has additional properties of type int - One *AdditionalPropertiesObject1 `json:"one,omitempty"` + One *AdditionalPropertiesObject1 `json:"one,omitempty" validate:""` // Allows any additional property - Three *AdditionalPropertiesObject3 `json:"three,omitempty"` + Three *AdditionalPropertiesObject3 `json:"three,omitempty" validate:""` // Does not allow additional properties - Two *AdditionalPropertiesObject2 `json:"two,omitempty"` + Two *AdditionalPropertiesObject2 `json:"two,omitempty" validate:""` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err @@ -1223,7 +1223,7 @@ func ParseEnsureEverythingIsReferencedResponse(rsp *http.Response) (*EnsureEvery case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: var dest struct { - Field SchemaObject `json:"Field"` + Field SchemaObject `json:"Field" validate:"required"` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err diff --git a/internal/test/externalref/externalref.gen.go b/internal/test/externalref/externalref.gen.go index 5fc09bd60a..3c366e1a3b 100644 --- a/internal/test/externalref/externalref.gen.go +++ b/internal/test/externalref/externalref.gen.go @@ -10,6 +10,6 @@ import ( // Container defines model for Container. type Container struct { - ObjectA *externalRef0.ObjectA `json:"object_a,omitempty"` - ObjectB *externalRef1.ObjectB `json:"object_b,omitempty"` + ObjectA *externalRef0.ObjectA `json:"object_a,omitempty" validate:""` + ObjectB *externalRef1.ObjectB `json:"object_b,omitempty" validate:""` } diff --git a/internal/test/externalref/packageA/externalref.gen.go b/internal/test/externalref/packageA/externalref.gen.go index 066f2b71b0..ba685fd732 100644 --- a/internal/test/externalref/packageA/externalref.gen.go +++ b/internal/test/externalref/packageA/externalref.gen.go @@ -9,6 +9,6 @@ import ( // ObjectA defines model for ObjectA. type ObjectA struct { - Name *string `json:"name,omitempty"` - ObjectB *externalRef0.ObjectB `json:"object_b,omitempty"` + Name *string `json:"name,omitempty" validate:""` + ObjectB *externalRef0.ObjectB `json:"object_b,omitempty" validate:""` } diff --git a/internal/test/externalref/packageB/externalref.gen.go b/internal/test/externalref/packageB/externalref.gen.go index 831b7e8d3f..52e0fd5257 100644 --- a/internal/test/externalref/packageB/externalref.gen.go +++ b/internal/test/externalref/packageB/externalref.gen.go @@ -5,5 +5,5 @@ package packageB // ObjectB defines model for ObjectB. type ObjectB struct { - Name *string `json:"name,omitempty"` + Name *string `json:"name,omitempty" validate:""` } diff --git a/internal/test/issues/issue-52/issue.gen.go b/internal/test/issues/issue-52/issue.gen.go index 33ef17219a..f77ba8b7d3 100644 --- a/internal/test/issues/issue-52/issue.gen.go +++ b/internal/test/issues/issue-52/issue.gen.go @@ -25,7 +25,7 @@ type ArrayValue []Value // Document defines model for Document. type Document struct { - Fields *Document_Fields `json:"fields,omitempty"` + Fields *Document_Fields `json:"fields,omitempty" validate:""` } // Document_Fields defines model for Document.Fields. @@ -35,8 +35,8 @@ type Document_Fields struct { // Value defines model for Value. type Value struct { - ArrayValue *ArrayValue `json:"arrayValue,omitempty"` - StringValue *string `json:"stringValue,omitempty"` + ArrayValue *ArrayValue `json:"arrayValue,omitempty" validate:""` + StringValue *string `json:"stringValue,omitempty" validate:""` } // Getter for additional properties for Document_Fields. Returns the specified diff --git a/internal/test/parameters/parameters.gen.go b/internal/test/parameters/parameters.gen.go index 5a57bfa0bd..5bcbd0f15a 100644 --- a/internal/test/parameters/parameters.gen.go +++ b/internal/test/parameters/parameters.gen.go @@ -22,15 +22,15 @@ import ( // ComplexObject defines model for ComplexObject. type ComplexObject struct { - Id int `json:"Id"` - IsAdmin bool `json:"IsAdmin"` - Object Object `json:"Object"` + Id int `json:"Id" validate:"required"` + IsAdmin bool `json:"IsAdmin" validate:"required"` + Object Object `json:"Object" validate:"required"` } // Object defines model for Object. type Object struct { - FirstName string `json:"firstName"` - Role string `json:"role"` + FirstName string `json:"firstName" validate:"required"` + Role string `json:"role" validate:"required"` } // GetCookieParams defines parameters for GetCookie. diff --git a/internal/test/schemas/schemas.gen.go b/internal/test/schemas/schemas.gen.go index bab13e6479..b48aa374e1 100644 --- a/internal/test/schemas/schemas.gen.go +++ b/internal/test/schemas/schemas.gen.go @@ -41,10 +41,10 @@ type GenericObject map[string]interface{} // NullableProperties defines model for NullableProperties. type NullableProperties struct { - Optional *string `json:"optional,omitempty"` - OptionalAndNullable *string `json:"optionalAndNullable"` - Required string `json:"required"` - RequiredAndNullable *string `json:"requiredAndNullable"` + Optional *string `json:"optional,omitempty" validate:""` + OptionalAndNullable *string `json:"optionalAndNullable" validate:""` + Required string `json:"required" validate:"required"` + RequiredAndNullable *string `json:"requiredAndNullable" validate:"required"` } // StringInPath defines model for StringInPath. @@ -608,11 +608,11 @@ type EnsureEverythingIsReferencedResponse struct { Body []byte HTTPResponse *http.Response JSON200 *struct { - AnyType1 *AnyType1 `json:"anyType1,omitempty"` + AnyType1 *AnyType1 `json:"anyType1,omitempty" validate:""` // This should be an interface{} - AnyType2 *AnyType2 `json:"anyType2,omitempty"` - CustomStringType *CustomStringType `json:"customStringType,omitempty"` + AnyType2 *AnyType2 `json:"anyType2,omitempty" validate:""` + CustomStringType *CustomStringType `json:"customStringType,omitempty" validate:"custom"` } } @@ -857,11 +857,11 @@ func ParseEnsureEverythingIsReferencedResponse(rsp *http.Response) (*EnsureEvery switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: var dest struct { - AnyType1 *AnyType1 `json:"anyType1,omitempty"` + AnyType1 *AnyType1 `json:"anyType1,omitempty" validate:""` // This should be an interface{} - AnyType2 *AnyType2 `json:"anyType2,omitempty"` - CustomStringType *CustomStringType `json:"customStringType,omitempty"` + AnyType2 *AnyType2 `json:"anyType2,omitempty" validate:""` + CustomStringType *CustomStringType `json:"customStringType,omitempty" validate:"custom"` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err diff --git a/internal/test/server/server.gen.go b/internal/test/server/server.gen.go index 1fd6623d1b..926af71bb3 100644 --- a/internal/test/server/server.gen.go +++ b/internal/test/server/server.gen.go @@ -15,63 +15,63 @@ import ( // EveryTypeOptional defines model for EveryTypeOptional. type EveryTypeOptional struct { - ArrayInlineField *[]int `json:"array_inline_field,omitempty"` - ArrayReferencedField *[]SomeObject `json:"array_referenced_field,omitempty"` - BoolField *bool `json:"bool_field,omitempty"` - ByteField *[]byte `json:"byte_field,omitempty"` - DateField *openapi_types.Date `json:"date_field,omitempty"` - DateTimeField *time.Time `json:"date_time_field,omitempty"` - DoubleField *float64 `json:"double_field,omitempty"` - FloatField *float32 `json:"float_field,omitempty"` + ArrayInlineField *[]int `json:"array_inline_field,omitempty" validate:""` + ArrayReferencedField *[]SomeObject `json:"array_referenced_field,omitempty" validate:""` + BoolField *bool `json:"bool_field,omitempty" validate:""` + ByteField *[]byte `json:"byte_field,omitempty" validate:""` + DateField *openapi_types.Date `json:"date_field,omitempty" validate:""` + DateTimeField *time.Time `json:"date_time_field,omitempty" validate:""` + DoubleField *float64 `json:"double_field,omitempty" validate:""` + FloatField *float32 `json:"float_field,omitempty" validate:""` InlineObjectField *struct { - Name string `json:"name"` - Number int `json:"number"` - } `json:"inline_object_field,omitempty"` - Int32Field *int32 `json:"int32_field,omitempty"` - Int64Field *int64 `json:"int64_field,omitempty"` - IntField *int `json:"int_field,omitempty"` - NumberField *float32 `json:"number_field,omitempty"` - ReferencedField *SomeObject `json:"referenced_field,omitempty"` - StringField *string `json:"string_field,omitempty"` + Name string `json:"name" validate:"required"` + Number int `json:"number" validate:"required"` + } `json:"inline_object_field,omitempty" validate:""` + Int32Field *int32 `json:"int32_field,omitempty" validate:""` + Int64Field *int64 `json:"int64_field,omitempty" validate:""` + IntField *int `json:"int_field,omitempty" validate:""` + NumberField *float32 `json:"number_field,omitempty" validate:""` + ReferencedField *SomeObject `json:"referenced_field,omitempty" validate:""` + StringField *string `json:"string_field,omitempty" validate:""` } // EveryTypeRequired defines model for EveryTypeRequired. type EveryTypeRequired struct { - ArrayInlineField []int `json:"array_inline_field"` - ArrayReferencedField []SomeObject `json:"array_referenced_field"` - BoolField bool `json:"bool_field"` - ByteField []byte `json:"byte_field"` - DateField openapi_types.Date `json:"date_field"` - DateTimeField time.Time `json:"date_time_field"` - DoubleField float64 `json:"double_field"` - EmailField *openapi_types.Email `json:"email_field,omitempty"` - FloatField float32 `json:"float_field"` + ArrayInlineField []int `json:"array_inline_field" validate:"required"` + ArrayReferencedField []SomeObject `json:"array_referenced_field" validate:"required"` + BoolField bool `json:"bool_field" validate:"required"` + ByteField []byte `json:"byte_field" validate:"required"` + DateField openapi_types.Date `json:"date_field" validate:"required"` + DateTimeField time.Time `json:"date_time_field" validate:"required"` + DoubleField float64 `json:"double_field" validate:"required"` + EmailField *openapi_types.Email `json:"email_field,omitempty" validate:"email"` + FloatField float32 `json:"float_field" validate:"required"` InlineObjectField struct { - Name string `json:"name"` - Number int `json:"number"` - } `json:"inline_object_field"` - Int32Field int32 `json:"int32_field"` - Int64Field int64 `json:"int64_field"` - IntField int `json:"int_field"` - NumberField float32 `json:"number_field"` - ReferencedField SomeObject `json:"referenced_field"` - StringField string `json:"string_field"` + Name string `json:"name" validate:"required"` + Number int `json:"number" validate:"required"` + } `json:"inline_object_field" validate:"required"` + Int32Field int32 `json:"int32_field" validate:"required"` + Int64Field int64 `json:"int64_field" validate:"required"` + IntField int `json:"int_field" validate:"required"` + NumberField float32 `json:"number_field" validate:"required"` + ReferencedField SomeObject `json:"referenced_field" validate:"required"` + StringField string `json:"string_field" validate:"required"` } // ReservedKeyword defines model for ReservedKeyword. type ReservedKeyword struct { - Channel *string `json:"channel,omitempty"` + Channel *string `json:"channel,omitempty" validate:""` } // Resource defines model for Resource. type Resource struct { - Name string `json:"name"` - Value float32 `json:"value"` + Name string `json:"name" validate:"required"` + Value float32 `json:"value" validate:"required"` } // SomeObject defines model for some_object. type SomeObject struct { - Name string `json:"name"` + Name string `json:"name" validate:"required"` } // Argument defines model for argument. @@ -82,7 +82,7 @@ type ResponseWithReference SomeObject // SimpleResponse defines model for SimpleResponse. type SimpleResponse struct { - Name string `json:"name"` + Name string `json:"name" validate:"required"` } // GetWithArgsParams defines parameters for GetWithArgs. @@ -113,8 +113,8 @@ type CreateResource2Params struct { // UpdateResource3JSONBody defines parameters for UpdateResource3. type UpdateResource3JSONBody struct { - Id *int `json:"id,omitempty"` - Name *string `json:"name,omitempty"` + Id *int `json:"id,omitempty" validate:""` + Name *string `json:"name,omitempty" validate:""` } // CreateResourceRequestBody defines body for CreateResource for application/json ContentType. From 1f8593934ee47629c102bc0d43ff609e4eb2a9f7 Mon Sep 17 00:00:00 2001 From: Alexey Dudko Date: Mon, 12 Oct 2020 18:48:54 +0300 Subject: [PATCH 3/3] test tag validation --- internal/test/validator/doc.go | 4 ++ internal/test/validator/spec.yaml | 38 ++++++++++ internal/test/validator/validator.gen.go | 29 ++++++++ internal/test/validator/validator_test.go | 87 +++++++++++++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 internal/test/validator/doc.go create mode 100644 internal/test/validator/spec.yaml create mode 100644 internal/test/validator/validator.gen.go create mode 100644 internal/test/validator/validator_test.go diff --git a/internal/test/validator/doc.go b/internal/test/validator/doc.go new file mode 100644 index 0000000000..30b0cfb07a --- /dev/null +++ b/internal/test/validator/doc.go @@ -0,0 +1,4 @@ +package validator + +//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen --package=validator -o validator.gen.go -generate=types,skip-prune spec.yaml + diff --git a/internal/test/validator/spec.yaml b/internal/test/validator/spec.yaml new file mode 100644 index 0000000000..9b8a3d7e2d --- /dev/null +++ b/internal/test/validator/spec.yaml @@ -0,0 +1,38 @@ +openapi: "3.0.1" +info: + version: 1.0.0 + title: Test validator + license: + name: MIT +paths: {} +components: + schemas: + StructA: + required: + - requiredString + properties: + requiredString: + type: string + rangeInt: + type: integer + format: int64 + minimum: 2.1 # will generate a tag "min=2", otherwise the validator will panic + maximum: 42 + StructB: + properties: + listItem: + type: array + items: + type: string + minLength: 1 + StructC: + required: + - color + properties: + color: + $ref: '#/components/schemas/Color' + Color: + type: string + enum: + - black + - white diff --git a/internal/test/validator/validator.gen.go b/internal/test/validator/validator.gen.go new file mode 100644 index 0000000000..d9f5cf00e9 --- /dev/null +++ b/internal/test/validator/validator.gen.go @@ -0,0 +1,29 @@ +// Package validator provides primitives to interact the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT. +package validator + +// Color defines model for Color. +type Color string + +// List of Color +const ( + Color_black Color = "black" + Color_white Color = "white" +) + +// StructA defines model for StructA. +type StructA struct { + RangeInt *int64 `json:"rangeInt,omitempty" validate:"gte=3,lte=42"` + RequiredString string `json:"requiredString" validate:"required"` +} + +// StructB defines model for StructB. +type StructB struct { + ListItem *[]string `json:"listItem,omitempty" validate:"gte=1"` +} + +// StructC defines model for StructC. +type StructC struct { + Color Color `json:"color" validate:"required,oneof=black white"` +} diff --git a/internal/test/validator/validator_test.go b/internal/test/validator/validator_test.go new file mode 100644 index 0000000000..fcd0dcf7d1 --- /dev/null +++ b/internal/test/validator/validator_test.go @@ -0,0 +1,87 @@ +package validator_test + +import ( + "testing" + + types "github.com/deepmap/oapi-codegen/internal/test/validator" + + "github.com/stretchr/testify/assert" + + "github.com/go-playground/locales/en" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + en_translations "github.com/go-playground/validator/v10/translations/en" +) + +var ( + translator ut.Translator + validate *validator.Validate +) + +func init() { + en := en.New() + uni := ut.New(en, en) + + var ok bool + // this is usually know or extracted from http 'Accept-Language' header + // also see uni.FindTranslator(...) + if translator, ok = uni.GetTranslator("en"); !ok { + panic("language not found") + } + validate = validator.New() + if err := en_translations.RegisterDefaultTranslations(validate, translator); err != nil { + panic("err") + } +} + +func TestStructA(t *testing.T) { + a := types.StructA{} + + err := validate.Struct(a) + if assert.Error(t, err) { + typedErr := err.(validator.ValidationErrors) + assert.Equal(t, "Key: 'StructA.RangeInt' Error:Field validation for 'RangeInt' failed on the 'gte' tag\nKey: 'StructA.RequiredString' Error:Field validation for 'RequiredString' failed on the 'required' tag", typedErr.Error()) + assert.Equal(t, validator.ValidationErrorsTranslations{ + "StructA.RangeInt": "RangeInt must be 3 or greater", + "StructA.RequiredString": "RequiredString is a required field", + }, typedErr.Translate(translator)) + } + + var i int64 = 50 + a.RangeInt = &i + a.RequiredString = "hello" + // + err = validate.Struct(a) + if assert.Error(t, err) { + typedErr := err.(validator.ValidationErrors) + assert.Equal(t, validator.ValidationErrorsTranslations{ + "StructA.RangeInt": "RangeInt must be 42 or less", + }, typedErr.Translate(translator)) + } +} + +func TestStructB(t *testing.T) { + b := types.StructB{} + + err := validate.Struct(b) + if assert.Error(t, err) { + typedErr := err.(validator.ValidationErrors) + assert.Equal(t, validator.ValidationErrorsTranslations{ + "StructB.ListItem": "ListItem must contain at least 1 item", + }, typedErr.Translate(translator)) + } +} + +func TestStructC(t *testing.T) { + c := types.StructC{ + Color: "orange", + } + + err := validate.Struct(c) + if assert.Error(t, err) { + typedErr := err.(validator.ValidationErrors) + assert.Equal(t, validator.ValidationErrorsTranslations{ + "StructC.Color": "Color must be one of [black white]", + }, typedErr.Translate(translator)) + } +} \ No newline at end of file