Repository: getkin/kin-openapi (go)

Issue:
Problem with server url using variables with new routers
Ciao,

I've updated to >0.50 (using gorilla mux), and it is not validating specs with this syntax because of the second url colon:

```
servers:
  - url: /
    description: devmode
  - url: '{protocol}://{server}/{prefix}/'
    description: production
    variables:
      protocol:
        enum:
          - 'http'
          - 'https'
        default: 'https'
        description: Protocol (http/https)
      server:
        default: 123z456789y0
        description: Server IP / Hostname
      prefix:
        default: ''
        description: Service platform prefix
```
Specifically [this line](https://github.com/getkin/kin-openapi/blob/master/routers/gorillamux/router.go#L36)  is failing with this error:

     "first path segment in URL cannot contain colon"

Presumibly `url.Parse` is possibly not working when variables are included in the url path (since it's possibly not anymore a URL according to RFC).


Pull request description:
openapi3: allow variables in schemes in gorillamux router + better server variables validation
Fixes #335 

Patch:
diff --git a/openapi3/server.go b/openapi3/server.go
index 682cb4d..6cdeb4a 100644
--- a/openapi3/server.go
+++ b/openapi3/server.go
@@ -150,9 +150,9 @@ func (server *Server) Validate(c context.Context) (err error) {
 // ServerVariable is specified by OpenAPI/Swagger standard version 3.0.
 type ServerVariable struct {
 	ExtensionProps
-	Enum        []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"`
-	Default     interface{}   `json:"default,omitempty" yaml:"default,omitempty"`
-	Description string        `json:"description,omitempty" yaml:"description,omitempty"`
+	Enum        []string `json:"enum,omitempty" yaml:"enum,omitempty"`
+	Default     string   `json:"default,omitempty" yaml:"default,omitempty"`
+	Description string   `json:"description,omitempty" yaml:"description,omitempty"`
 }
 
 func (serverVariable *ServerVariable) MarshalJSON() ([]byte, error) {
@@ -164,17 +164,12 @@ func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error {
 }
 
 func (serverVariable *ServerVariable) Validate(c context.Context) error {
-	switch serverVariable.Default.(type) {
-	case float64, string, nil:
-	default:
-		return errors.New("value of default must be either a number or a string")
-	}
-	for _, item := range serverVariable.Enum {
-		switch item.(type) {
-		case float64, string:
-		default:
-			return errors.New("all 'enum' items must be either a number or a string")
+	if serverVariable.Default == "" {
+		data, err := serverVariable.MarshalJSON()
+		if err != nil {
+			return err
 		}
+		return fmt.Errorf("field default is required in %s", data)
 	}
 	return nil
 }
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index d192664..b9b0cd8 100644
--- a/openapi3filter/req_resp_decoder.go
+++ b/openapi3filter/req_resp_decoder.go
@@ -767,10 +767,19 @@ type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (
 // By default, there is content type "application/json" is supported only.
 var bodyDecoders = make(map[string]BodyDecoder)
 
+// RegisteredBodyDecoder returns the registered body decoder for the given content type.
+//
+// If no decoder was registered for the given content type, nil is returned.
+// This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines.
+func RegisteredBodyDecoder(contentType string) BodyDecoder {
+	return bodyDecoders[contentType]
+}
+
 // RegisterBodyDecoder registers a request body's decoder for a content type.
 //
 // If a decoder for the specified content type already exists, the function replaces
 // it with the specified decoder.
+// This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines.
 func RegisterBodyDecoder(contentType string, decoder BodyDecoder) {
 	if contentType == "" {
 		panic("contentType is empty")
@@ -784,6 +793,7 @@ func RegisterBodyDecoder(contentType string, decoder BodyDecoder) {
 // UnregisterBodyDecoder dissociates a body decoder from a content type.
 //
 // Decoding this content type will result in an error.
+// This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines.
 func UnregisterBodyDecoder(contentType string) {
 	if contentType == "" {
 		panic("contentType is empty")
diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go
index 7973398..6bdda1a 100644
--- a/routers/gorillamux/router.go
+++ b/routers/gorillamux/router.go
@@ -28,12 +28,17 @@ type Router struct {
 // TODO: Handle/HandlerFunc + ServeHTTP (When there is a match, the route variables can be retrieved calling mux.Vars(request))
 func NewRouter(doc *openapi3.Swagger) (routers.Router, error) {
 	type srv struct {
-		scheme, host, base string
-		server             *openapi3.Server
+		schemes    []string
+		host, base string
+		server     *openapi3.Server
 	}
 	servers := make([]srv, 0, len(doc.Servers))
 	for _, server := range doc.Servers {
-		u, err := url.Parse(bEncode(server.URL))
+		serverURL := server.URL
+		scheme0 := strings.Split(serverURL, "://")[0]
+		schemes := permutePart(scheme0, server)
+
+		u, err := url.Parse(bEncode(strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1)))
 		if err != nil {
 			return nil, err
 		}
@@ -42,10 +47,10 @@ func NewRouter(doc *openapi3.Swagger) (routers.Router, error) {
 			path = path[:len(path)-1]
 		}
 		servers = append(servers, srv{
-			host:   bDecode(u.Host), //u.Hostname()?
-			base:   path,
-			scheme: bDecode(u.Scheme),
-			server: server,
+			host:    bDecode(u.Host), //u.Hostname()?
+			base:    path,
+			schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624
+			server:  server,
 		})
 	}
 	if len(servers) == 0 {
@@ -65,8 +70,8 @@ func NewRouter(doc *openapi3.Swagger) (routers.Router, error) {
 
 		for _, s := range servers {
 			muxRoute := muxRouter.Path(s.base + path).Methods(methods...)
-			if scheme := s.scheme; scheme != "" {
-				muxRoute.Schemes(scheme)
+			if schemes := s.schemes; len(schemes) != 0 {
+				muxRoute.Schemes(schemes...)
 			}
 			if host := s.host; host != "" {
 				muxRoute.Host(host)
@@ -151,3 +156,48 @@ func bDecode(s string) string {
 	s = strings.Replace(s, brURL, "}", -1)
 	return s
 }
+
+func permutePart(part0 string, srv *openapi3.Server) []string {
+	type mapAndSlice struct {
+		m map[string]struct{}
+		s []string
+	}
+	var2val := make(map[string]mapAndSlice)
+	max := 0
+	for name0, v := range srv.Variables {
+		name := "{" + name0 + "}"
+		if !strings.Contains(part0, name) {
+			continue
+		}
+		m := map[string]struct{}{v.Default: {}}
+		for _, value := range v.Enum {
+			m[value] = struct{}{}
+		}
+		if l := len(m); l > max {
+			max = l
+		}
+		s := make([]string, 0, len(m))
+		for value := range m {
+			s = append(s, value)
+		}
+		var2val[name] = mapAndSlice{m: m, s: s}
+	}
+	if len(var2val) == 0 {
+		return []string{part0}
+	}
+
+	partsMap := make(map[string]struct{}, max*len(var2val))
+	for i := 0; i < max; i++ {
+		part := part0
+		for name, mas := range var2val {
+			part = strings.Replace(part, name, mas.s[i%len(mas.s)], -1)
+		}
+		partsMap[part] = struct{}{}
+	}
+	parts := make([]string, 0, len(partsMap))
+	for part := range partsMap {
+		parts = append(parts, part)
+	}
+	sort.Strings(parts)
+	return parts
+}


Test patch:
diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go
index 71c1763..0662f16 100644
--- a/openapi3/swagger_loader_test.go
+++ b/openapi3/swagger_loader_test.go
@@ -514,10 +514,11 @@ servers:
 		`{url: "http://{x}.{y}.example.com"}`: errors.New("invalid servers: server has undeclared variables"),
 		`{url: "http://{x}.y}.example.com"}`:  errors.New("invalid servers: server URL has mismatched { and }"),
 		`{url: "http://{x.example.com"}`:      errors.New("invalid servers: server URL has mismatched { and }"),
-		`{url: "http://{x}.example.com", variables: {x: {default: "www"}}}`: nil,
-		`{url: "http://{x}.example.com", variables: {x: {enum: ["www"]}}}`:  nil,
-		`{url: "http://www.example.com", variables: {x: {enum: ["www"]}}}`:  errors.New("invalid servers: server has undeclared variables"),
-		`{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`:  errors.New("invalid servers: server has undeclared variables"),
+		`{url: "http://{x}.example.com", variables: {x: {default: "www"}}}`:                nil,
+		`{url: "http://{x}.example.com", variables: {x: {default: "www", enum: ["www"]}}}`: nil,
+		`{url: "http://{x}.example.com", variables: {x: {enum: ["www"]}}}`:                 errors.New(`invalid servers: field default is required in {"enum":["www"]}`),
+		`{url: "http://www.example.com", variables: {x: {enum: ["www"]}}}`:                 errors.New("invalid servers: server has undeclared variables"),
+		`{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`:                 errors.New("invalid servers: server has undeclared variables"),
 	} {
 		t.Run(value, func(t *testing.T) {
 			loader := NewSwaggerLoader()
diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go
index b6d603e..c461da9 100644
--- a/openapi3filter/req_resp_decoder_test.go
+++ b/openapi3filter/req_resp_decoder_test.go
@@ -1187,39 +1187,42 @@ func newTestMultipartForm(parts []*testFormPart) (io.Reader, string, error) {
 }
 
 func TestRegisterAndUnregisterBodyDecoder(t *testing.T) {
-	var (
-		contentType = "text/csv"
-		decoder     = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
-			data, err := ioutil.ReadAll(body)
-			if err != nil {
-				return nil, err
-			}
-			var vv []interface{}
-			for _, v := range strings.Split(string(data), ",") {
-				vv = append(vv, v)
-			}
-			return vv, nil
+	var decoder BodyDecoder
+	decoder = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (decoded interface{}, err error) {
+		var data []byte
+		if data, err = ioutil.ReadAll(body); err != nil {
+			return
 		}
-		schema  = openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema()).NewRef()
-		encFn   = func(string) *openapi3.Encoding { return nil }
-		body    = strings.NewReader("foo,bar")
-		want    = []interface{}{"foo", "bar"}
-		wantErr = &ParseError{Kind: KindUnsupportedFormat}
-	)
+		return strings.Split(string(data), ","), nil
+	}
+	contentType := "text/csv"
 	h := make(http.Header)
 	h.Set(headerCT, contentType)
 
+	originalDecoder := RegisteredBodyDecoder(contentType)
+	require.Nil(t, originalDecoder)
+
 	RegisterBodyDecoder(contentType, decoder)
+	require.Equal(t, fmt.Sprintf("%v", decoder), fmt.Sprintf("%v", RegisteredBodyDecoder(contentType)))
+
+	body := strings.NewReader("foo,bar")
+	schema := openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema()).NewRef()
+	encFn := func(string) *openapi3.Encoding { return nil }
 	got, err := decodeBody(body, h, schema, encFn)
 
 	require.NoError(t, err)
-	require.Truef(t, reflect.DeepEqual(got, want), "got %v, want %v", got, want)
+	require.Equal(t, []string{"foo", "bar"}, got)
 
 	UnregisterBodyDecoder(contentType)
-	_, err = decodeBody(body, h, schema, encFn)
 
-	require.Error(t, err)
-	require.Truef(t, matchParseError(err, wantErr), "got error:\n%v\nwant error:\n%v", err, wantErr)
+	originalDecoder = RegisteredBodyDecoder(contentType)
+	require.Nil(t, originalDecoder)
+
+	_, err = decodeBody(body, h, schema, encFn)
+	require.Equal(t, &ParseError{
+		Kind:   KindUnsupportedFormat,
+		Reason: prefixUnsupportedCT + ` "text/csv"`,
+	}, err)
 }
 
 func matchParseError(got, want error) bool {
diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go
index fd04d4e..1a3d112 100644
--- a/routers/gorillamux/router_test.go
+++ b/routers/gorillamux/router_test.go
@@ -158,9 +158,10 @@ func TestRouter(t *testing.T) {
 
 	doc.Servers = []*openapi3.Server{
 		{URL: "https://www.example.com/api/v1"},
-		{URL: "https://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{
-			"d0": {Default: "www"},
-			"d1": {Enum: []interface{}{"example"}},
+		{URL: "{scheme}://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{
+			"d0":     {Default: "www"},
+			"d1":     {Default: "example", Enum: []string{"example"}},
+			"scheme": {Default: "https", Enum: []string{"https", "http"}},
 		}},
 	}
 	err = doc.Validate(context.Background())
@@ -176,6 +177,7 @@ func TestRouter(t *testing.T) {
 	expect(r, http.MethodGet, "https://domain0.domain1.com/api/v1/hello", helloGET, map[string]string{
 		"d0": "domain0",
 		"d1": "domain1",
+		// "scheme": "https", TODO: https://github.com/gorilla/mux/issues/624
 	})
 
 	{
@@ -190,3 +192,17 @@ func TestRouter(t *testing.T) {
 		require.Nil(t, pathParams)
 	}
 }
+
+func TestPermuteScheme(t *testing.T) {
+	scheme0 := "{sche}{me}"
+	server := &openapi3.Server{URL: scheme0 + "://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{
+		"d0":   {Default: "www"},
+		"d1":   {Default: "example", Enum: []string{"example"}},
+		"sche": {Default: "http"},
+		"me":   {Default: "s", Enum: []string{"", "s"}},
+	}}
+	err := server.Validate(context.Background())
+	require.NoError(t, err)
+	perms := permutePart(scheme0, server)
+	require.Equal(t, []string{"http", "https"}, perms)
+}
diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go
index af0dfba..082eab5 100644
--- a/routers/legacy/router_test.go
+++ b/routers/legacy/router_test.go
@@ -163,7 +163,7 @@ func TestRouter(t *testing.T) {
 		{URL: "https://www.example.com/api/v1"},
 		{URL: "https://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{
 			"d0": {Default: "www"},
-			"d1": {Enum: []interface{}{"example"}},
+			"d1": {Default: "example", Enum: []string{"example"}},
 		}},
 	}
 	err = doc.Validate(context.Background())