diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 37791ed5..e0e4c70c 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -30,6 +30,15 @@ const ( // FormatOfStringForEmail pattern catches only some suspiciously wrong-looking email addresses. // Use DefineStringFormat(...) if you need something stricter. FormatOfStringForEmail = `^[^@]+@[^@<>",\s]+$` + + // FormatOfStringByte is a regexp for base64-encoded characters, for example, "U3dhZ2dlciByb2Nrcw==" + FormatOfStringByte = `(^$|^[a-zA-Z0-9+/\-_]*=*$)` + + // FormatOfStringDate is a RFC3339 date format regexp, for example "2017-07-21". + FormatOfStringDate = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$` + + // FormatOfStringDateTime is a RFC3339 date-time format regexp, for example "2017-07-21T17:32:28Z". + FormatOfStringDateTime = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$` ) const ( SerializationSimple = "simple" @@ -65,6 +74,14 @@ var ( // ErrSchemaInputInf may be returned when validating a number ErrSchemaInputInf = errors.New("floating point Inf is not allowed") ) +var ( + // SchemaStringFormats is a map of custom string format validators. + SchemaStringFormats = make(map[string]StringFormatValidator) + // SchemaNumberFormats is a map of custom number format validators. + SchemaNumberFormats = make(map[string]NumberFormatValidator) + // SchemaIntegerFormats is a map of custom integer format validators. + SchemaIntegerFormats = make(map[string]IntegerFormatValidator) +) var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile)) DefaultReadFromURI returns a caching ReadFromURIFunc which can read remote HTTP URIs and local file URIs. @@ -73,9 +90,6 @@ var ErrURINotSupported = errors.New("unsupported URI") ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a given URI. -var SchemaStringFormats = make(map[string]Format, 4) - SchemaStringFormats allows for validating string formats - FUNCTIONS @@ -105,12 +119,27 @@ func DefineIPv4Format() func DefineIPv6Format() DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec +func DefineIntegerFormatValidator(name string, validator IntegerFormatValidator) + DefineIntegerFormatValidator defines a custom format validator for a given + integer format. + +func DefineNumberFormatValidator(name string, validator NumberFormatValidator) + DefineNumberFormatValidator defines a custom format validator for a given + number format. + func DefineStringFormat(name string, pattern string) - DefineStringFormat defines a new regexp pattern for a given format + DefineStringFormat defines a regexp pattern for a given string + format Deprecated: Use openapi3.DefineStringFormatValidator(name, + NewRegexpFormatValidator(pattern)) instead. -func DefineStringFormatCallback(name string, callback FormatCallback) - DefineStringFormatCallback adds a validation function for a specific schema - format entry +func DefineStringFormatCallback(name string, callback func(string) error) + DefineStringFormatCallback defines a callback function for a given + string format Deprecated: Use openapi3.DefineStringFormatValidator(name, + NewCallbackValidator(fn)) instead. + +func DefineStringFormatValidator(name string, validator StringFormatValidator) + DefineStringFormatValidator defines a custom format validator for a given + string format. func Float64Ptr(value float64) *float64 Float64Ptr is a helper for defining OpenAPI schemas. @@ -512,14 +541,22 @@ func (e *ExternalDocs) Validate(ctx context.Context, opts ...ValidationOption) e Validate returns an error if ExternalDocs does not comply with the OpenAPI spec. -type Format struct { - // Has unexported fields. +type FormatValidator[T any] interface { + Validate(value T) error } - Format represents a format validator registered by either DefineStringFormat - or DefineStringFormatCallback + FormatValidator is an interface for custom format validators. + +func NewCallbackValidator[T any](fn func(T) error) FormatValidator[T] + NewCallbackValidator creates a new FormatValidator that uses a callback + function to validate the value. + +func NewIPValidator(isIPv4 bool) FormatValidator[string] + NewIPValidator creates a new FormatValidator that validates the value is an + IP address. -type FormatCallback func(value string) error - FormatCallback performs custom checks on exotic formats +func NewRangeFormatValidator[T int64 | float64](min, max T) FormatValidator[T] + NewRangeFormatValidator creates a new FormatValidator that validates the + value is within a given range. type Header struct { Parameter @@ -617,6 +654,9 @@ func (info *Info) UnmarshalJSON(data []byte) error func (info *Info) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Info does not comply with the OpenAPI spec. +type IntegerFormatValidator = FormatValidator[int64] + IntegerFormatValidator is a type alias for FormatValidator[int64] + type License struct { Extensions map[string]any `json:"-" yaml:"-"` @@ -821,6 +861,9 @@ func WithName(name string, response *Response) NewResponsesOption func WithStatus(status int, responseRef *ResponseRef) NewResponsesOption WithStatus adds a status code keyed ResponseRef +type NumberFormatValidator = FormatValidator[float64] + NumberFormatValidator is a type alias for FormatValidator[float64] + type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` @@ -1917,6 +1960,13 @@ type SliceUniqueItemsChecker func(items []any) bool SliceUniqueItemsChecker is an function used to check if an given slice have unique items. +type StringFormatValidator = FormatValidator[string] + StringFormatValidator is a type alias for FormatValidator[string] + +func NewRegexpFormatValidator(pattern string) StringFormatValidator + NewRegexpFormatValidator creates a new FormatValidator that uses a regular + expression to validate the value. + type T struct { Extensions map[string]any `json:"-" yaml:"-"` diff --git a/README.md b/README.md index 54e58b7c..976075c0 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,7 @@ for _, path := range doc.Paths.InMatchingOrder() { * `openapi3.CircularReferenceError` and `openapi3.CircularReferenceCounter` are removed. `openapi3.Loader` now implements reference backtracking, so any kind of circular references should be properly resolved. * `InternalizeRefs` now takes a refNameResolver that has access to `openapi3.T` and more properties of the reference needing resolving. * The `DefaultRefNameResolver` has been updated, choosing names that will be less likely to collide with each other. Because of this internalized specs will likely change slightly. +* `openapi3.Format` and `openapi3.FormatCallback` are removed and the type of `openapi3.SchemaStringFormats` has changed. ### v0.125.0 * The `openapi3filter.ErrFunc` and `openapi3filter.LogFunc` func types now take the validated request's context as first argument. diff --git a/openapi3/issue735_test.go b/openapi3/issue735_test.go index ac316af3..a846caa0 100644 --- a/openapi3/issue735_test.go +++ b/openapi3/issue735_test.go @@ -16,8 +16,8 @@ type testCase struct { } func TestIssue735(t *testing.T) { - DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) - DefineStringFormat("email", FormatOfStringForEmail) + DefineStringFormatValidator("uuid", NewRegexpFormatValidator(FormatOfStringForUUIDOfRFC4122)) + DefineStringFormatValidator("email", NewRegexpFormatValidator(FormatOfStringForEmail)) DefineIPv4Format() DefineIPv6Format() diff --git a/openapi3/schema.go b/openapi3/schema.go index 508c0d2f..7be6bd38 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -28,12 +28,6 @@ const ( TypeObject = "object" TypeString = "string" TypeNull = "null" - - // constants for integer formats - formatMinInt32 = float64(math.MinInt32) - formatMaxInt32 = float64(math.MaxInt32) - formatMinInt64 = float64(math.MinInt64) - formatMaxInt64 = float64(math.MaxInt64) ) var ( @@ -988,7 +982,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, switch format { case "float", "double": default: - if validationOpts.schemaFormatValidationEnabled { + if _, ok := SchemaNumberFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { return stack, unsupportedFormat(format) } } @@ -998,7 +992,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, switch format { case "int32", "int64": default: - if validationOpts.schemaFormatValidationEnabled { + if _, ok := SchemaIntegerFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { return stack, unsupportedFormat(format) } } @@ -1019,7 +1013,6 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, // Defined in some other specification case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": default: - // Try to check for custom defined formats if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { return stack, unsupportedFormat(format) } @@ -1523,39 +1516,56 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value } // formats - if requireInteger && schema.Format != "" { - formatMin := float64(0) - formatMax := float64(0) - switch schema.Format { - case "int32": - formatMin = formatMinInt32 - formatMax = formatMaxInt32 - case "int64": - formatMin = formatMinInt64 - formatMax = formatMaxInt64 - default: - if settings.formatValidationEnabled { - return unsupportedFormat(schema.Format) - } - } - if formatMin != 0 && formatMax != 0 && !(formatMin <= value && value <= formatMax) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "format", - Reason: fmt.Sprintf("number must be an %s", schema.Format), - customizeMessageError: settings.customizeMessageError, + var formatStrErr string + var formatErr error + format := schema.Format + if format != "" { + if requireInteger { + if f, ok := SchemaIntegerFormats[format]; ok { + if err := f.Validate(int64(value)); err != nil { + var reason string + schemaErr := &SchemaError{} + if errors.As(err, &schemaErr) { + reason = schemaErr.Reason + } else { + reason = err.Error() + } + formatStrErr = fmt.Sprintf(`integer doesn't match the format %q (%v)`, format, reason) + formatErr = fmt.Errorf("integer doesn't match the format %q: %w", format, err) + } } - if !settings.multiError { - return err + } else { + if f, ok := SchemaNumberFormats[format]; ok { + if err := f.Validate(value); err != nil { + var reason string + schemaErr := &SchemaError{} + if errors.As(err, &schemaErr) { + reason = schemaErr.Reason + } else { + reason = err.Error() + } + formatStrErr = fmt.Sprintf(`number doesn't match the format %q (%v)`, format, reason) + formatErr = fmt.Errorf("number doesn't match the format %q: %w", format, err) + } } - me = append(me, err) } } + if formatStrErr != "" || formatErr != nil { + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "format", + Reason: formatStrErr, + Origin: formatErr, + customizeMessageError: settings.customizeMessageError, + } + if !settings.multiError { + return err + } + me = append(me, err) + } + // "exclusiveMinimum" if v := schema.ExclusiveMin; v && !(*schema.Min < value) { if settings.failfast { @@ -1749,23 +1759,16 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value var formatErr error if format := schema.Format; format != "" { if f, ok := SchemaStringFormats[format]; ok { - switch { - case f.regexp != nil && f.callback == nil: - if cp := f.regexp; !cp.MatchString(value) { - formatStrErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String()) - } - case f.regexp == nil && f.callback != nil: - if err := f.callback(value); err != nil { - schemaErr := &SchemaError{} - if errors.As(err, &schemaErr) { - formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%s)`, format, schemaErr.Reason) - } else { - formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%v)`, format, err) - } - formatErr = err + if err := f.Validate(value); err != nil { + var reason string + schemaErr := &SchemaError{} + if errors.As(err, &schemaErr) { + reason = schemaErr.Reason + } else { + reason = err.Error() } - default: - formatStrErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format) + formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%v)`, format, reason) + formatErr = fmt.Errorf("string doesn't match the format %q: %w", format, err) } } } diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index f69d1a28..023c2669 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -2,10 +2,33 @@ package openapi3 import ( "fmt" + "math" "net/netip" "regexp" ) +type ( + // FormatValidator is an interface for custom format validators. + FormatValidator[T any] interface { + Validate(value T) error + } + // StringFormatValidator is a type alias for FormatValidator[string] + StringFormatValidator = FormatValidator[string] + // NumberFormatValidator is a type alias for FormatValidator[float64] + NumberFormatValidator = FormatValidator[float64] + // IntegerFormatValidator is a type alias for FormatValidator[int64] + IntegerFormatValidator = FormatValidator[int64] +) + +var ( + // SchemaStringFormats is a map of custom string format validators. + SchemaStringFormats = make(map[string]StringFormatValidator) + // SchemaNumberFormats is a map of custom number format validators. + SchemaNumberFormats = make(map[string]NumberFormatValidator) + // SchemaIntegerFormats is a map of custom integer format validators. + SchemaIntegerFormats = make(map[string]IntegerFormatValidator) +) + const ( // FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122 FormatOfStringForUUIDOfRFC4122 = `^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$` @@ -13,89 +36,134 @@ const ( // FormatOfStringForEmail pattern catches only some suspiciously wrong-looking email addresses. // Use DefineStringFormat(...) if you need something stricter. FormatOfStringForEmail = `^[^@]+@[^@<>",\s]+$` + + // FormatOfStringByte is a regexp for base64-encoded characters, for example, "U3dhZ2dlciByb2Nrcw==" + FormatOfStringByte = `(^$|^[a-zA-Z0-9+/\-_]*=*$)` + + // FormatOfStringDate is a RFC3339 date format regexp, for example "2017-07-21". + FormatOfStringDate = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$` + + // FormatOfStringDateTime is a RFC3339 date-time format regexp, for example "2017-07-21T17:32:28Z". + FormatOfStringDateTime = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$` ) -// FormatCallback performs custom checks on exotic formats -type FormatCallback func(value string) error +func init() { + DefineStringFormatValidator("byte", NewRegexpFormatValidator(FormatOfStringByte)) + DefineStringFormatValidator("date", NewRegexpFormatValidator(FormatOfStringDate)) + DefineStringFormatValidator("date-time", NewRegexpFormatValidator(FormatOfStringDateTime)) + DefineIntegerFormatValidator("int32", NewRangeFormatValidator(int64(math.MinInt32), int64(math.MaxInt32))) + DefineIntegerFormatValidator("int64", NewRangeFormatValidator(int64(math.MinInt64), int64(math.MaxInt64))) +} + +// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec +func DefineIPv4Format() { + DefineStringFormatValidator("ipv4", NewIPValidator(true)) +} -// Format represents a format validator registered by either DefineStringFormat or DefineStringFormatCallback -type Format struct { - regexp *regexp.Regexp - callback FormatCallback +// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec +func DefineIPv6Format() { + DefineStringFormatValidator("ipv6", NewIPValidator(false)) } -// SchemaStringFormats allows for validating string formats -var SchemaStringFormats = make(map[string]Format, 4) +type stringRegexpFormatValidator struct { + re *regexp.Regexp +} -// DefineStringFormat defines a new regexp pattern for a given format -func DefineStringFormat(name string, pattern string) { - re, err := regexp.Compile(pattern) - if err != nil { - err := fmt.Errorf("format %q has invalid pattern %q: %w", name, pattern, err) - panic(err) +func (s stringRegexpFormatValidator) Validate(value string) error { + if !s.re.MatchString(value) { + return fmt.Errorf(`string doesn't match pattern "%s"`, s.re.String()) } - SchemaStringFormats[name] = Format{regexp: re} + return nil } -// DefineStringFormatCallback adds a validation function for a specific schema format entry -func DefineStringFormatCallback(name string, callback FormatCallback) { - SchemaStringFormats[name] = Format{callback: callback} +type callbackValidator[T any] struct { + fn func(T) error } -func validateIPv4(ip string) error { - addr, err := netip.ParseAddr(ip) - if err != nil { - return &SchemaError{ - Value: ip, - Reason: "Not an IP address", - } - } - if !addr.Is4() { - return &SchemaError{ - Value: ip, - Reason: "Not an IPv4 address (it's IPv6)", - } +func (c callbackValidator[T]) Validate(value T) error { + return c.fn(value) +} + +type rangeFormat[T int64 | float64] struct { + min, max T +} + +func (r rangeFormat[T]) Validate(value T) error { + if value < r.min || value > r.max { + return fmt.Errorf("value should be between %v and %v", r.min, r.max) } return nil } -func validateIPv6(ip string) error { - addr, err := netip.ParseAddr(ip) +// NewRangeFormatValidator creates a new FormatValidator that validates the value is within a given range. +func NewRangeFormatValidator[T int64 | float64](min, max T) FormatValidator[T] { + return rangeFormat[T]{min: min, max: max} +} + +// NewRegexpFormatValidator creates a new FormatValidator that uses a regular expression to validate the value. +func NewRegexpFormatValidator(pattern string) StringFormatValidator { + re, err := regexp.Compile(pattern) if err != nil { - return &SchemaError{ - Value: ip, - Reason: "Not an IP address", - Origin: err, - } - } - if !addr.Is6() { - return &SchemaError{ - Value: ip, - Reason: "Not an IPv6 address (it's IPv4)", - } + err := fmt.Errorf("string regexp format has invalid pattern %q: %w", pattern, err) + panic(err) } - return nil + return stringRegexpFormatValidator{re: re} } -func init() { - // Base64 - // The pattern supports base64 and b./ase64url. Padding ('=') is supported. - DefineStringFormat("byte", `(^$|^[a-zA-Z0-9+/\-_]*=*$)`) +// NewCallbackValidator creates a new FormatValidator that uses a callback function to validate the value. +func NewCallbackValidator[T any](fn func(T) error) FormatValidator[T] { + return callbackValidator[T]{fn: fn} +} - // date - DefineStringFormat("date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`) +// DefineStringFormatValidator defines a custom format validator for a given string format. +func DefineStringFormatValidator(name string, validator StringFormatValidator) { + SchemaStringFormats[name] = validator +} - // date-time - DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`) +// DefineNumberFormatValidator defines a custom format validator for a given number format. +func DefineNumberFormatValidator(name string, validator NumberFormatValidator) { + SchemaNumberFormats[name] = validator +} +// DefineIntegerFormatValidator defines a custom format validator for a given integer format. +func DefineIntegerFormatValidator(name string, validator IntegerFormatValidator) { + SchemaIntegerFormats[name] = validator } -// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec -func DefineIPv4Format() { - DefineStringFormatCallback("ipv4", validateIPv4) +// DefineStringFormat defines a regexp pattern for a given string format +// Deprecated: Use openapi3.DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) instead. +func DefineStringFormat(name string, pattern string) { + DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) } -// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec -func DefineIPv6Format() { - DefineStringFormatCallback("ipv6", validateIPv6) +// DefineStringFormatCallback defines a callback function for a given string format +// Deprecated: Use openapi3.DefineStringFormatValidator(name, NewCallbackValidator(fn)) instead. +func DefineStringFormatCallback(name string, callback func(string) error) { + DefineStringFormatValidator(name, NewCallbackValidator(callback)) +} + +// NewIPValidator creates a new FormatValidator that validates the value is an IP address. +func NewIPValidator(isIPv4 bool) FormatValidator[string] { + return callbackValidator[string]{fn: func(ip string) error { + addr, err := netip.ParseAddr(ip) + if err != nil { + return &SchemaError{ + Value: ip, + Reason: "Not an IP address", + } + } + if isIPv4 && !addr.Is4() { + return &SchemaError{ + Value: ip, + Reason: "Not an IPv4 address (it's IPv6)", + } + } + if !isIPv4 && !addr.Is6() { + return &SchemaError{ + Value: ip, + Reason: "Not an IPv6 address (it's IPv4)", + } + } + return nil + }} } diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go index ac9ba644..09cf3bfe 100644 --- a/openapi3/schema_formats_test.go +++ b/openapi3/schema_formats_test.go @@ -50,15 +50,18 @@ func TestIssue430(t *testing.T) { DefineIPv4Format() DefineIPv6Format() + ipv4Validator := NewIPValidator(true) + ipv6Validator := NewIPValidator(false) + for datum, isV4 := range data { err = schema.VisitJSON(datum) require.NoError(t, err) if isV4 { - assert.Nil(t, validateIPv4(datum), "%q should be IPv4", datum) - assert.NotNil(t, validateIPv6(datum), "%q should not be IPv6", datum) + assert.Nil(t, ipv4Validator.Validate(datum), "%q should be IPv4", datum) + assert.NotNil(t, ipv6Validator.Validate(datum), "%q should not be IPv6", datum) } else { - assert.NotNil(t, validateIPv4(datum), "%q should not be IPv4", datum) - assert.Nil(t, validateIPv6(datum), "%q should be IPv6", datum) + assert.NotNil(t, ipv4Validator.Validate(datum), "%q should not be IPv4", datum) + assert.Nil(t, ipv6Validator.Validate(datum), "%q should be IPv6", datum) } } } @@ -66,9 +69,9 @@ func TestIssue430(t *testing.T) { func TestFormatCallback_WrapError(t *testing.T) { var errSomething = errors.New("something error") - DefineStringFormatCallback("foobar", func(value string) error { + DefineStringFormatValidator("foobar", NewCallbackValidator(func(value string) error { return errSomething - }) + })) s := &Schema{Format: "foobar"} err := s.VisitJSONString("blablabla") @@ -102,7 +105,7 @@ components: `ip`: `123.0.0.11111`, }) - require.EqualError(t, err, `Error at "/ip": Not an IP address`) + require.EqualError(t, err, `Error at "/ip": string doesn't match the format "ipv4": Not an IP address`) delete(SchemaStringFormats, "ipv4") SchemaErrorDetailsDisabled = false @@ -116,7 +119,7 @@ func TestUuidFormat(t *testing.T) { wantErr bool } - DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) + DefineStringFormatValidator("uuid", NewRegexpFormatValidator(FormatOfStringForUUIDOfRFC4122)) testCases := []testCase{ { name: "invalid", @@ -155,3 +158,81 @@ func TestUuidFormat(t *testing.T) { }) } } + +func TestNumberFormats(t *testing.T) { + type testCase struct { + name string + typ string + format string + value any + wantErr bool + } + DefineNumberFormatValidator("lessThan10", NewCallbackValidator(func(value float64) error { + if value >= 10 { + return fmt.Errorf("not less than 10") + } + return nil + })) + DefineIntegerFormatValidator("odd", NewCallbackValidator(func(value int64) error { + if value%2 == 0 { + return fmt.Errorf("not odd") + } + return nil + })) + testCases := []testCase{ + { + name: "invalid number", + value: "test", + typ: "number", + format: "", + wantErr: true, + }, + { + name: "zero float64", + value: 0.0, + typ: "number", + format: "lessThan10", + wantErr: false, + }, + { + name: "11", + value: 11.0, + typ: "number", + format: "lessThan10", + wantErr: true, + }, + { + name: "odd 11", + value: 11.0, + typ: "integer", + format: "odd", + wantErr: false, + }, + { + name: "even 12", + value: 12.0, + typ: "integer", + format: "odd", + wantErr: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + schema := &Schema{ + Type: &Types{tc.typ}, + Format: tc.format, + } + err := schema.VisitJSON(tc.value) + var schemaError = &SchemaError{} + if tc.wantErr { + require.Error(t, err) + require.ErrorAs(t, err, &schemaError) + + require.NotZero(t, schemaError.Reason) + require.NotContains(t, schemaError.Reason, fmt.Sprint(tc.value)) + } else { + require.Nil(t, err) + } + }) + } +} diff --git a/openapi3/schema_issue492_test.go b/openapi3/schema_issue492_test.go index fa5c74b0..2fdfcbce 100644 --- a/openapi3/schema_issue492_test.go +++ b/openapi3/schema_issue492_test.go @@ -46,5 +46,5 @@ info: "name": "kin-openapi", "time": "2001-02-03T04:05:06:789Z", }) - require.ErrorContains(t, err, `Error at "/time": string doesn't match the format "date-time" (regular expression "^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$")`) + require.ErrorContains(t, err, `Error at "/time": string doesn't match the format "date-time": string doesn't match pattern "^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$"`) } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 12f0340d..d678361b 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -22,13 +22,13 @@ type schemaExample struct { } func TestSchemas(t *testing.T) { - DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) + DefineStringFormatValidator("uuid", NewRegexpFormatValidator(FormatOfStringForUUIDOfRFC4122)) for _, example := range schemaExamples { - t.Run(example.Title, testSchema(t, example)) + t.Run(example.Title, testSchema(example)) } } -func testSchema(t *testing.T, example schemaExample) func(*testing.T) { +func testSchema(example schemaExample) func(*testing.T) { return func(t *testing.T) { schema := example.Schema if serialized := example.Serialization; serialized != nil {