Skip to content

Commit 10dbe1a

Browse files
Merge pull request #208 from chrisbygrave/ff-openapi-extensions
Add support for ffextensions
2 parents c1fccfd + 55e8926 commit 10dbe1a

3 files changed

Lines changed: 103 additions & 1 deletion

File tree

pkg/ffapi/openapi3.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"log"
2323
"net/http"
24+
"net/url"
2425
"reflect"
2526
"regexp"
2627
"sort"
@@ -74,7 +75,11 @@ type BaseURLVariable struct {
7475
Description string
7576
}
7677

77-
var customRegexRemoval = regexp.MustCompile(`{(\w+)\:[^}]+}`)
78+
var (
79+
customRegexRemoval = regexp.MustCompile(`{(\w+)\:[^}]+}`)
80+
// Check ffExtension key starts with "x-"
81+
ffExtensionKeyRegexp = regexp.MustCompile(`^x-.+$`)
82+
)
7883

7984
type SwaggerGen struct {
8085
options *SwaggerGenOptions
@@ -184,10 +189,37 @@ func (sg *SwaggerGen) ffOutputTagHandler(ctx context.Context, route *Route, name
184189
return sg.ffTagHandler(ctx, route, name, tag, schema)
185190
}
186191

192+
func (sg *SwaggerGen) applyFFExtensionsTag(ctx context.Context, schema *openapi3.Schema, tag string) error {
193+
if tag == "" {
194+
return nil
195+
}
196+
q, err := url.ParseQuery(tag)
197+
if err != nil {
198+
return i18n.WrapError(ctx, err, i18n.MsgFFExtensionsInvalid, tag)
199+
}
200+
for extension, values := range q {
201+
for _, value := range values {
202+
if !ffExtensionKeyRegexp.MatchString(extension) {
203+
return i18n.NewError(ctx, i18n.MsgFFExtensionsInvalidEncoding, extension)
204+
}
205+
if schema.Extensions == nil {
206+
schema.Extensions = make(map[string]interface{})
207+
}
208+
schema.Extensions[extension] = value
209+
}
210+
}
211+
return nil
212+
}
213+
187214
func (sg *SwaggerGen) ffTagHandler(ctx context.Context, route *Route, name string, tag reflect.StructTag, schema *openapi3.Schema) error {
188215
if ffEnum := tag.Get("ffenum"); ffEnum != "" {
189216
schema.Enum = fftypes.FFEnumValues(ffEnum)
190217
}
218+
if ffExtensions := tag.Get("ffschemaext"); ffExtensions != "" {
219+
if err := sg.applyFFExtensionsTag(ctx, schema, ffExtensions); err != nil {
220+
return err
221+
}
222+
}
191223
if sg.isTrue(tag.Get("ffexclude")) {
192224
return &openapi3gen.ExcludeSchemaSentinel{}
193225
}

pkg/ffapi/openapi3_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ type TestStruct2 struct {
6464
JSONAny1 *fftypes.JSONAny `ffstruct:"ut1" json:"jsonAny1,omitempty"`
6565
}
6666

67+
type TestExtensions struct {
68+
String1 string `ffstruct:"ut1" json:"string1" ffschemaext:"x-key1=value1"`
69+
String2 string `ffstruct:"ut1" json:"string2" ffschemaext:"x-key1=value1&x-key1=value2&x-key2=value2%26value3"`
70+
String3 string `ffstruct:"ut1" json:"string3" ffschemaext:""`
71+
}
72+
73+
type TestExtensionsBadKey struct {
74+
String1 string `ffstruct:"ut1" json:"string1" ffschemaext:"x-=value1"`
75+
}
76+
77+
type TestExtensionsBadEncoding struct {
78+
String1 string `ffstruct:"ut1" json:"string1" ffschemaext:"x-key1=value1%"`
79+
}
80+
6781
var ExampleDesc = i18n.FFM(language.AmericanEnglish, "TestKey", "Test Description")
6882

6983
var example2TagName = "Example 2"
@@ -193,6 +207,18 @@ var testRoutes = []*Route{
193207
},
194208
Tag: example2TagName,
195209
},
210+
{
211+
Name: "op8",
212+
Path: "example8",
213+
Method: http.MethodGet,
214+
PathParams: nil,
215+
QueryParams: nil,
216+
Description: ExampleDesc,
217+
JSONInputValue: func() interface{} { return nil},
218+
JSONOutputValue: func() interface{} { return &TestExtensions{} },
219+
JSONOutputCodes: []int{http.StatusOK},
220+
},
221+
196222
}
197223

198224
type TestInOutType struct {
@@ -578,3 +604,45 @@ func TestExcludeFromOpenAPI(t *testing.T) {
578604
err := doc.Validate(context.Background())
579605
assert.NoError(t, err)
580606
}
607+
608+
func TestExtensionsBadEncodingFail(t *testing.T) {
609+
routes := []*Route{
610+
{
611+
Name: "badEncoding",
612+
Path: "extensions",
613+
Method: http.MethodGet,
614+
JSONInputValue: func() interface{} { return nil },
615+
JSONOutputValue: func() interface{} { return &TestExtensionsBadEncoding{} },
616+
JSONOutputCodes: []int{http.StatusOK},
617+
},
618+
}
619+
620+
assert.PanicsWithValue(t, "invalid schema: FF00258: Invalid extension 'x-key1=value1%' - extensions should be RFC 3986 compliant query parameter format (e.g. x-name=value with percent-encoding for special characters): invalid URL escape \"%\"", func() {
621+
_ = NewSwaggerGen(&SwaggerGenOptions{
622+
Title: "UnitTest",
623+
Version: "1.0",
624+
BaseURL: "http://localhost:12345/api/v1",
625+
}).Generate(context.Background(), routes)
626+
})
627+
}
628+
629+
func TestExtensionsBadKeyFail(t *testing.T) {
630+
routes := []*Route{
631+
{
632+
Name: "bad3",
633+
Path: "extensions",
634+
Method: http.MethodGet,
635+
JSONInputValue: func() interface{} { return nil },
636+
JSONOutputValue: func() interface{} { return &TestExtensionsBadKey{} },
637+
JSONOutputCodes: []int{http.StatusOK},
638+
},
639+
}
640+
641+
assert.PanicsWithValue(t, "invalid schema: FF00259: Invalid extension key 'x-' - extension keys must follow the format 'x-<name>'", func() {
642+
_ = NewSwaggerGen(&SwaggerGenOptions{
643+
Title: "UnitTest",
644+
Version: "1.0",
645+
BaseURL: "http://localhost:12345/api/v1",
646+
}).Generate(context.Background(), routes)
647+
})
648+
}

pkg/i18n/en_base_error_messages.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,6 @@ var (
192192
MsgRoutePathNotStartWithSlash = ffe("FF00255", "Route path '%s' must not start with '/'")
193193
MsgMethodNotAllowed = ffe("FF00256", "Method not allowed", http.StatusMethodNotAllowed)
194194
MsgInvalidLogLevel = ffe("FF00257", "Invalid log level: '%s'", http.StatusBadRequest)
195+
MsgFFExtensionsInvalid = ffe("FF00258", "Invalid extension '%s' - extensions should be RFC 3986 compliant query parameter format (e.g. x-name=value with percent-encoding for special characters)", http.StatusBadRequest)
196+
MsgFFExtensionsInvalidEncoding = ffe("FF00259", "Invalid extension key '%s' - extension keys must follow the format 'x-<name>'", http.StatusBadRequest)
195197
)

0 commit comments

Comments
 (0)