Skip to content

Commit ebcd76e

Browse files
committed
feat #582: add minor version support to string formats
1 parent 04fb379 commit ebcd76e

File tree

4 files changed

+417
-20
lines changed

4 files changed

+417
-20
lines changed

.github/docs/openapi3.txt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,20 @@ var CircularReferenceError = "kin-openapi bug found: circular schema reference n
99
var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile))
1010
var ErrURINotSupported = errors.New("unsupported URI")
1111
var IdentifierRegExp = regexp.MustCompile(identifierPattern)
12-
var SchemaStringFormats = make(map[string]Format, 4)
12+
var SchemaStringFormats = make(map[string]*Format, 4)
1313
func BoolPtr(value bool) *bool
1414
func DefaultRefNameResolver(ref string) string
1515
func DefineIPv4Format()
1616
func DefineIPv6Format()
17-
func DefineStringFormat(name string, pattern string)
18-
func DefineStringFormatCallback(name string, callback FormatCallback)
17+
func DefineStringFormat(name string, pattern string, options ...SchemaFormatOption)
18+
func DefineStringFormatCallback(name string, callback FormatCallback, options ...SchemaFormatOption)
1919
func Float64Ptr(value float64) *float64
2020
func Int64Ptr(value int64) *int64
2121
func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error)
2222
func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker)
23+
func RestoreDefaultStringFormats()
24+
func RestoreStringFormats(formatToRestore map[string]*Format)
25+
func SaveStringFormats(map[string]*Format) map[string]*Format
2326
func Uint64Ptr(value uint64) *uint64
2427
func ValidateIdentifier(value string) error
2528
func WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context
@@ -97,8 +100,12 @@ type Schema struct{ ... }
97100
func NewArraySchema() *Schema
98101
func NewBoolSchema() *Schema
99102
func NewBytesSchema() *Schema
103+
func NewDateSchema() *Schema
100104
func NewDateTimeSchema() *Schema
101105
func NewFloat64Schema() *Schema
106+
func NewHostnameSchema() *Schema
107+
func NewIPv4Schema() *Schema
108+
func NewIPv6Schema() *Schema
102109
func NewInt32Schema() *Schema
103110
func NewInt64Schema() *Schema
104111
func NewIntegerSchema() *Schema
@@ -108,6 +115,9 @@ type Schema struct{ ... }
108115
func NewStringSchema() *Schema
109116
func NewUUIDSchema() *Schema
110117
type SchemaError struct{ ... }
118+
type SchemaFormatOption func(options *SchemaFormatOptions)
119+
func FromOpenAPIMinorVersion(fromMinorVersion uint64) SchemaFormatOption
120+
type SchemaFormatOptions struct{ ... }
111121
type SchemaRef struct{ ... }
112122
func NewSchemaRef(ref string, value *Schema) *SchemaRef
113123
type SchemaRefs []*SchemaRef
@@ -119,6 +129,7 @@ type SchemaValidationOption func(*schemaValidationSettings)
119129
func EnableFormatValidation() SchemaValidationOption
120130
func FailFast() SchemaValidationOption
121131
func MultiErrors() SchemaValidationOption
132+
func SetOpenAPIMinorVersion(minorVersion uint64) SchemaValidationOption
122133
func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaValidationOption
123134
func VisitAsRequest() SchemaValidationOption
124135
func VisitAsResponse() SchemaValidationOption

openapi3/schema_formats.go

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,99 @@ type FormatCallback func(value string) error
2121

2222
// Format represents a format validator registered by either DefineStringFormat or DefineStringFormatCallback
2323
type Format struct {
24+
versionedFormats []*versionedFormat
25+
}
26+
27+
type versionedFormat struct {
2428
regexp *regexp.Regexp
2529
callback FormatCallback
2630
}
2731

32+
func (format *Format) add(minMinorVersion uint64, vFormat *versionedFormat) {
33+
if format != nil {
34+
if format.versionedFormats == nil {
35+
format.versionedFormats = make([]*versionedFormat, minMinorVersion+1)
36+
format.versionedFormats[minMinorVersion] = vFormat
37+
} else {
38+
numVersionedFormats := uint64(len(format.versionedFormats))
39+
if minMinorVersion >= numVersionedFormats {
40+
// grow array
41+
lastValue := format.versionedFormats[numVersionedFormats-1]
42+
additionalEntries := make([]*versionedFormat, minMinorVersion+1-numVersionedFormats)
43+
if lastValue != nil {
44+
for i := 0; i < len(additionalEntries); i++ {
45+
additionalEntries[i] = lastValue
46+
}
47+
}
48+
format.versionedFormats = append(format.versionedFormats, additionalEntries...)
49+
format.versionedFormats[minMinorVersion] = vFormat
50+
return
51+
}
52+
for i := minMinorVersion; i < numVersionedFormats; i++ {
53+
format.versionedFormats[i] = vFormat
54+
}
55+
}
56+
}
57+
}
58+
59+
func (format Format) get(minorVersion uint64) *versionedFormat {
60+
if format.versionedFormats != nil {
61+
if minorVersion >= uint64(len(format.versionedFormats)) {
62+
return format.versionedFormats[len(format.versionedFormats)-1]
63+
}
64+
return format.versionedFormats[minorVersion]
65+
}
66+
return nil
67+
}
68+
69+
func (format Format) DefinedForMinorVersion(minorVersion uint64) bool {
70+
return format.get(minorVersion) != nil
71+
}
72+
2873
// SchemaStringFormats allows for validating string formats
29-
var SchemaStringFormats = make(map[string]Format, 4)
74+
var SchemaStringFormats = make(map[string]*Format, 4)
75+
var defaultSchemaStringFormats map[string]*Format
3076

3177
// DefineStringFormat defines a new regexp pattern for a given format
32-
func DefineStringFormat(name string, pattern string) {
78+
// Will enforce regexp usage for minor versions of OpenAPI (3.Y.Z)
79+
func DefineStringFormat(name string, pattern string, options ...SchemaFormatOption) {
80+
var schemaFormatOptions SchemaFormatOptions
81+
for _, option := range options {
82+
option(&schemaFormatOptions)
83+
}
3384
re, err := regexp.Compile(pattern)
3485
if err != nil {
3586
err := fmt.Errorf("format %q has invalid pattern %q: %w", name, pattern, err)
3687
panic(err)
3788
}
38-
SchemaStringFormats[name] = Format{regexp: re}
89+
updateSchemaStringFormats(name, schemaFormatOptions.fromOpenAPIMinorVersion, &versionedFormat{regexp: re})
90+
}
91+
92+
func getSchemaStringFormats(name string, minorVersion uint64) *versionedFormat {
93+
if currentStringFormat, found := SchemaStringFormats[name]; found {
94+
return currentStringFormat.get(minorVersion)
95+
}
96+
return nil
97+
}
98+
99+
func updateSchemaStringFormats(name string, minMinorVersion uint64, vFormat *versionedFormat) {
100+
if currentStringFormat, found := SchemaStringFormats[name]; found {
101+
currentStringFormat.add(minMinorVersion, vFormat)
102+
return
103+
}
104+
var newFormat Format
105+
newFormat.add(minMinorVersion, vFormat)
106+
SchemaStringFormats[name] = &newFormat
39107
}
40108

41109
// DefineStringFormatCallback adds a validation function for a specific schema format entry
42-
func DefineStringFormatCallback(name string, callback FormatCallback) {
43-
SchemaStringFormats[name] = Format{callback: callback}
110+
// Will enforce regexp usage for minor versions of OpenAPI (3.Y.Z)
111+
func DefineStringFormatCallback(name string, callback FormatCallback, options ...SchemaFormatOption) {
112+
var schemaFormatOptions SchemaFormatOptions
113+
for _, option := range options {
114+
option(&schemaFormatOptions)
115+
}
116+
updateSchemaStringFormats(name, schemaFormatOptions.fromOpenAPIMinorVersion, &versionedFormat{callback: callback})
44117
}
45118

46119
func validateIP(ip string) error {
@@ -82,6 +155,51 @@ func validateIPv6(ip string) error {
82155
return nil
83156
}
84157

158+
// SaveStringFormats allows to save (obtain a deep copy) of your current string formats
159+
// so you can later restore it if needed
160+
func SaveStringFormats(map[string]*Format) map[string]*Format {
161+
savedStringFormats := map[string]*Format{}
162+
for name, value := range SchemaStringFormats {
163+
var savedFormat Format
164+
savedFormat.versionedFormats = make([]*versionedFormat, len(value.versionedFormats))
165+
for index, versionedFormatValue := range value.versionedFormats {
166+
if versionedFormatValue != nil {
167+
savedVersionedFormat := versionedFormat{
168+
regexp: versionedFormatValue.regexp,
169+
callback: versionedFormatValue.callback,
170+
}
171+
savedFormat.versionedFormats[index] = &savedVersionedFormat
172+
}
173+
}
174+
savedStringFormats[name] = &savedFormat
175+
}
176+
return savedStringFormats
177+
}
178+
179+
// RestoreStringFormats allows to restore string format back to default values
180+
func RestoreStringFormats(formatToRestore map[string]*Format) {
181+
restoredStringFormats := map[string]*Format{}
182+
for name, value := range formatToRestore {
183+
var restoredFormat Format
184+
restoredFormat.versionedFormats = make([]*versionedFormat, len(value.versionedFormats))
185+
for index, versionedFormatValue := range value.versionedFormats {
186+
if versionedFormatValue != nil {
187+
restoredVersionedFormat := versionedFormat{
188+
regexp: versionedFormatValue.regexp,
189+
callback: versionedFormatValue.callback,
190+
}
191+
restoredFormat.versionedFormats[index] = &restoredVersionedFormat
192+
}
193+
}
194+
restoredStringFormats[name] = &restoredFormat
195+
}
196+
SchemaStringFormats = restoredStringFormats
197+
}
198+
199+
// RestoreDefaultStringFormats allows to restore string format back to default values
200+
func RestoreDefaultStringFormats() {
201+
RestoreStringFormats(defaultSchemaStringFormats)
202+
}
85203
func init() {
86204
// Base64
87205
// The pattern supports base64 and b./ase64url. Padding ('=') is supported.
@@ -93,6 +211,7 @@ func init() {
93211
// date-time
94212
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})?$`)
95213

214+
defaultSchemaStringFormats = SaveStringFormats(SchemaStringFormats)
96215
}
97216

98217
// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec

openapi3/schema_formats_options.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package openapi3
2+
3+
// SchemaFormatOption allows the modification of how the OpenAPI document is validated.
4+
type SchemaFormatOption func(options *SchemaFormatOptions)
5+
6+
// SchemaFormatOptions provides configuration for validating OpenAPI documents.
7+
type SchemaFormatOptions struct {
8+
fromOpenAPIMinorVersion uint64
9+
}
10+
11+
// FromOpenAPIMinorVersion allows to declare a string format available only at some minor OpenAPI version
12+
func FromOpenAPIMinorVersion(fromMinorVersion uint64) SchemaFormatOption {
13+
return func(options *SchemaFormatOptions) {
14+
options.fromOpenAPIMinorVersion = fromMinorVersion
15+
}
16+
}

0 commit comments

Comments
 (0)