From a5ec367c307325f8732c966d66a3e7a2f2077618 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Sat, 8 Oct 2016 20:35:14 -0700 Subject: [PATCH 1/2] Add a dependency on github.com/exponent-io/jsonpath --- Godeps/Godeps.json | 4 + Godeps/LICENSES | 29 +++ .../exponent-io/jsonpath/.gitignore | 24 ++ .../exponent-io/jsonpath/.travis.yml | 5 + .../github.com/exponent-io/jsonpath/LICENSE | 21 ++ .../github.com/exponent-io/jsonpath/README.md | 66 ++++++ .../exponent-io/jsonpath/decoder.go | 210 ++++++++++++++++++ .../github.com/exponent-io/jsonpath/path.go | 67 ++++++ .../exponent-io/jsonpath/pathaction.go | 61 +++++ 9 files changed, 487 insertions(+) create mode 100644 vendor/github.com/exponent-io/jsonpath/.gitignore create mode 100644 vendor/github.com/exponent-io/jsonpath/.travis.yml create mode 100644 vendor/github.com/exponent-io/jsonpath/LICENSE create mode 100644 vendor/github.com/exponent-io/jsonpath/README.md create mode 100644 vendor/github.com/exponent-io/jsonpath/decoder.go create mode 100644 vendor/github.com/exponent-io/jsonpath/path.go create mode 100644 vendor/github.com/exponent-io/jsonpath/pathaction.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index ebb6757ea7b..e9d847be66f 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -889,6 +889,10 @@ "ImportPath": "github.com/evanphx/json-patch", "Rev": "465937c80b3c07a7c7ad20cc934898646a91c1de" }, + { + "ImportPath": "github.com/exponent-io/jsonpath", + "Rev": "d6023ce2651d8eafb5c75bb0c7167536102ec9f5" + }, { "ImportPath": "github.com/fsnotify/fsnotify", "Comment": "v1.3.1-1-gf12c623", diff --git a/Godeps/LICENSES b/Godeps/LICENSES index df98fd04f9b..f9ac45ea1b1 100644 --- a/Godeps/LICENSES +++ b/Godeps/LICENSES @@ -31026,6 +31026,35 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================================ +================================================================================ += vendor/github.com/exponent-io/jsonpath licensed under: = + +The MIT License (MIT) + +Copyright (c) 2015 Exponent Labs LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + += vendor/github.com/exponent-io/jsonpath/LICENSE 42f582355f11b1d4bc8615214b7f0c38 - +================================================================================ + + ================================================================================ = vendor/github.com/fsnotify/fsnotify licensed under: = diff --git a/vendor/github.com/exponent-io/jsonpath/.gitignore b/vendor/github.com/exponent-io/jsonpath/.gitignore new file mode 100644 index 00000000000..daf913b1b34 --- /dev/null +++ b/vendor/github.com/exponent-io/jsonpath/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/exponent-io/jsonpath/.travis.yml b/vendor/github.com/exponent-io/jsonpath/.travis.yml new file mode 100644 index 00000000000..f4f458a416d --- /dev/null +++ b/vendor/github.com/exponent-io/jsonpath/.travis.yml @@ -0,0 +1,5 @@ +language: go + +go: + - 1.5 + - tip diff --git a/vendor/github.com/exponent-io/jsonpath/LICENSE b/vendor/github.com/exponent-io/jsonpath/LICENSE new file mode 100644 index 00000000000..54197725078 --- /dev/null +++ b/vendor/github.com/exponent-io/jsonpath/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Exponent Labs LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/exponent-io/jsonpath/README.md b/vendor/github.com/exponent-io/jsonpath/README.md new file mode 100644 index 00000000000..382fb3138cb --- /dev/null +++ b/vendor/github.com/exponent-io/jsonpath/README.md @@ -0,0 +1,66 @@ +[![GoDoc](https://godoc.org/github.com/exponent-io/jsonpath?status.svg)](https://godoc.org/github.com/exponent-io/jsonpath) +[![Build Status](https://travis-ci.org/exponent-io/jsonpath.svg?branch=master)](https://travis-ci.org/exponent-io/jsonpath) + +# jsonpath + +This package extends the [json.Decoder](https://golang.org/pkg/encoding/json/#Decoder) to support navigating a stream of JSON tokens. You should be able to use this extended Decoder places where a json.Decoder would have been used. + +This Decoder has the following enhancements... + * The [Scan](https://godoc.org/github.com/exponent-io/jsonpath/#Decoder.Scan) method supports scanning a JSON stream while extracting particular values along the way using [PathActions](https://godoc.org/github.com/exponent-io/jsonpath#PathActions). + * The [SeekTo](https://godoc.org/github.com/exponent-io/jsonpath#Decoder.SeekTo) method supports seeking forward in a JSON token stream to a particular path. + * The [Path](https://godoc.org/github.com/exponent-io/jsonpath#Decoder.Path) method returns the path of the most recently parsed token. + * The [Token](https://godoc.org/github.com/exponent-io/jsonpath#Decoder.Token) method has been modified to distinguish between strings that are object keys and strings that are values. Object key strings are returned as the [KeyString](https://godoc.org/github.com/exponent-io/jsonpath#KeyString) type rather than a native string. + +## Installation + + go get -u github.com/exponent-io/jsonpath + +## Example Usage + +#### SeekTo + +```go +import "github.com/exponent-io/jsonpath" + +var j = []byte(`[ + {"Space": "YCbCr", "Point": {"Y": 255, "Cb": 0, "Cr": -10}}, + {"Space": "RGB", "Point": {"R": 98, "G": 218, "B": 255}} +]`) + +w := json.NewDecoder(bytes.NewReader(j)) +var v interface{} + +w.SeekTo(1, "Point", "G") +w.Decode(&v) // v is 218 +``` + +#### Scan with PathActions + +```go +var j = []byte(`{"colors":[ + {"Space": "YCbCr", "Point": {"Y": 255, "Cb": 0, "Cr": -10, "A": 58}}, + {"Space": "RGB", "Point": {"R": 98, "G": 218, "B": 255, "A": 231}} +]}`) + +var actions PathActions + +// Extract the value at Point.A +actions.Add(func(d *Decoder) error { + var alpha int + err := d.Decode(&alpha) + fmt.Printf("Alpha: %v\n", alpha) + return err +}, "Point", "A") + +w := NewDecoder(bytes.NewReader(j)) +w.SeekTo("colors", 0) + +var ok = true +var err error +for ok { + ok, err = w.Scan(&actions) + if err != nil && err != io.EOF { + panic(err) + } +} +``` diff --git a/vendor/github.com/exponent-io/jsonpath/decoder.go b/vendor/github.com/exponent-io/jsonpath/decoder.go new file mode 100644 index 00000000000..31de46c7381 --- /dev/null +++ b/vendor/github.com/exponent-io/jsonpath/decoder.go @@ -0,0 +1,210 @@ +package jsonpath + +import ( + "encoding/json" + "io" +) + +// KeyString is returned from Decoder.Token to represent each key in a JSON object value. +type KeyString string + +// Decoder extends the Go runtime's encoding/json.Decoder to support navigating in a stream of JSON tokens. +type Decoder struct { + json.Decoder + + path JsonPath + context jsonContext +} + +// NewDecoder creates a new instance of the extended JSON Decoder. +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{Decoder: *json.NewDecoder(r)} +} + +// SeekTo causes the Decoder to move forward to a given path in the JSON structure. +// +// The path argument must consist of strings or integers. Each string specifies an JSON object key, and +// each integer specifies an index into a JSON array. +// +// Consider the JSON structure +// +// { "a": [0,"s",12e4,{"b":0,"v":35} ] } +// +// SeekTo("a",3,"v") will move to the value referenced by the "a" key in the current object, +// followed by a move to the 4th value (index 3) in the array, followed by a move to the value at key "v". +// In this example, a subsequent call to the decoder's Decode() would unmarshal the value 35. +// +// SeekTo returns a boolean value indicating whether a match was found. +// +// Decoder is intended to be used with a stream of tokens. As a result it navigates forward only. +func (d *Decoder) SeekTo(path ...interface{}) (bool, error) { + + if len(path) == 0 { + return len(d.path) == 0, nil + } + last := len(path) - 1 + if i, ok := path[last].(int); ok { + path[last] = i - 1 + } + + for { + if d.path.Equal(path) { + return true, nil + } + _, err := d.Token() + if err == io.EOF { + return false, nil + } else if err != nil { + return false, err + } + } +} + +// Decode reads the next JSON-encoded value from its input and stores it in the value pointed to by v. This is +// equivalent to encoding/json.Decode(). +func (d *Decoder) Decode(v interface{}) error { + switch d.context { + case objValue: + d.context = objKey + break + case arrValue: + d.path.incTop() + break + } + return d.Decoder.Decode(v) +} + +// Path returns a slice of string and/or int values representing the path from the root of the JSON object to the +// position of the most-recently parsed token. +func (d *Decoder) Path() JsonPath { + p := make(JsonPath, len(d.path)) + copy(p, d.path) + return p +} + +// Token is equivalent to the Token() method on json.Decoder. The primary difference is that it distinguishes +// between strings that are keys and and strings that are values. String tokens that are object keys are returned as a +// KeyString rather than as a native string. +func (d *Decoder) Token() (json.Token, error) { + t, err := d.Decoder.Token() + if err != nil { + return t, err + } + + if t == nil { + switch d.context { + case objValue: + d.context = objKey + break + case arrValue: + d.path.incTop() + break + } + return t, err + } + + switch t := t.(type) { + case json.Delim: + switch t { + case json.Delim('{'): + if d.context == arrValue { + d.path.incTop() + } + d.path.push("") + d.context = objKey + break + case json.Delim('}'): + d.path.pop() + d.context = d.path.inferContext() + break + case json.Delim('['): + if d.context == arrValue { + d.path.incTop() + } + d.path.push(-1) + d.context = arrValue + break + case json.Delim(']'): + d.path.pop() + d.context = d.path.inferContext() + break + } + case float64, json.Number, bool: + switch d.context { + case objValue: + d.context = objKey + break + case arrValue: + d.path.incTop() + break + } + break + case string: + switch d.context { + case objKey: + d.path.nameTop(t) + d.context = objValue + return KeyString(t), err + case objValue: + d.context = objKey + case arrValue: + d.path.incTop() + } + break + } + + return t, err +} + +// Scan moves forward over the JSON stream consuming all the tokens at the current level (current object, current array) +// invoking each matching PathAction along the way. +// +// Scan returns true if there are more contiguous values to scan (for example in an array). +func (d *Decoder) Scan(ext *PathActions) (bool, error) { + + rootPath := d.Path() + + // If this is an array path, increment the root path in our local copy. + if rootPath.inferContext() == arrValue { + rootPath.incTop() + } + + for { + // advance the token position + _, err := d.Token() + if err != nil { + return false, err + } + + match: + var relPath JsonPath + + // capture the new JSON path + path := d.Path() + + if len(path) > len(rootPath) { + // capture the path relative to where the scan started + relPath = path[len(rootPath):] + } else { + // if the path is not longer than the root, then we are done with this scan + // return boolean flag indicating if there are more items to scan at the same level + return d.Decoder.More(), nil + } + + // match the relative path against the path actions + if node := ext.node.match(relPath); node != nil { + if node.action != nil { + // we have a match so execute the action + err = node.action(d) + if err != nil { + return d.Decoder.More(), err + } + // The action may have advanced the decoder. If we are in an array, advancing it further would + // skip tokens. So, if we are scanning an array, jump to the top without advancing the token. + if d.path.inferContext() == arrValue && d.Decoder.More() { + goto match + } + } + } + } +} diff --git a/vendor/github.com/exponent-io/jsonpath/path.go b/vendor/github.com/exponent-io/jsonpath/path.go new file mode 100644 index 00000000000..d7db2ad336e --- /dev/null +++ b/vendor/github.com/exponent-io/jsonpath/path.go @@ -0,0 +1,67 @@ +// Extends the Go runtime's json.Decoder enabling navigation of a stream of json tokens. +package jsonpath + +import "fmt" + +type jsonContext int + +const ( + none jsonContext = iota + objKey + objValue + arrValue +) + +// AnyIndex can be used in a pattern to match any array index. +const AnyIndex = -2 + +// JsonPath is a slice of strings and/or integers. Each string specifies an JSON object key, and +// each integer specifies an index into a JSON array. +type JsonPath []interface{} + +func (p *JsonPath) push(n interface{}) { *p = append(*p, n) } +func (p *JsonPath) pop() { *p = (*p)[:len(*p)-1] } + +// increment the index at the top of the stack (must be an array index) +func (p *JsonPath) incTop() { (*p)[len(*p)-1] = (*p)[len(*p)-1].(int) + 1 } + +// name the key at the top of the stack (must be an object key) +func (p *JsonPath) nameTop(n string) { (*p)[len(*p)-1] = n } + +// infer the context from the item at the top of the stack +func (p *JsonPath) inferContext() jsonContext { + if len(*p) == 0 { + return none + } + t := (*p)[len(*p)-1] + switch t.(type) { + case string: + return objKey + case int: + return arrValue + default: + panic(fmt.Sprintf("Invalid stack type %T", t)) + } +} + +// Equal tests for equality between two JsonPath types. +func (p *JsonPath) Equal(o JsonPath) bool { + if len(*p) != len(o) { + return false + } + for i, v := range *p { + if v != o[i] { + return false + } + } + return true +} + +func (p *JsonPath) HasPrefix(o JsonPath) bool { + for i, v := range o { + if v != (*p)[i] { + return false + } + } + return true +} diff --git a/vendor/github.com/exponent-io/jsonpath/pathaction.go b/vendor/github.com/exponent-io/jsonpath/pathaction.go new file mode 100644 index 00000000000..497ed686ca9 --- /dev/null +++ b/vendor/github.com/exponent-io/jsonpath/pathaction.go @@ -0,0 +1,61 @@ +package jsonpath + +// pathNode is used to construct a trie of paths to be matched +type pathNode struct { + matchOn interface{} // string, or integer + childNodes []pathNode + action DecodeAction +} + +// match climbs the trie to find a node that matches the given JSON path. +func (n *pathNode) match(path JsonPath) *pathNode { + var node *pathNode = n + for _, ps := range path { + found := false + for i, n := range node.childNodes { + if n.matchOn == ps { + node = &node.childNodes[i] + found = true + break + } else if _, ok := ps.(int); ok && n.matchOn == AnyIndex { + node = &node.childNodes[i] + found = true + break + } + } + if !found { + return nil + } + } + return node +} + +// PathActions represents a collection of DecodeAction functions that should be called at certain path positions +// when scanning the JSON stream. PathActions can be created once and used many times in one or more JSON streams. +type PathActions struct { + node pathNode +} + +// DecodeAction handlers are called by the Decoder when scanning objects. See PathActions.Add for more detail. +type DecodeAction func(d *Decoder) error + +// Add specifies an action to call on the Decoder when the specified path is encountered. +func (je *PathActions) Add(action DecodeAction, path ...interface{}) { + + var node *pathNode = &je.node + for _, ps := range path { + found := false + for i, n := range node.childNodes { + if n.matchOn == ps { + node = &node.childNodes[i] + found = true + break + } + } + if !found { + node.childNodes = append(node.childNodes, pathNode{matchOn: ps}) + node = &node.childNodes[len(node.childNodes)-1] + } + } + node.action = action +} From d9d06f6680c7a0546c511f69bb345704aa3e5334 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Thu, 13 Oct 2016 21:58:44 -0700 Subject: [PATCH 2/2] Add some checking for the presence of the same key twice. --- pkg/api/validation/schema.go | 65 +++++++++++++++++ pkg/api/validation/schema_test.go | 115 ++++++++++++++++++++++++++++++ pkg/kubectl/cmd/util/factory.go | 6 +- 3 files changed, 185 insertions(+), 1 deletion(-) diff --git a/pkg/api/validation/schema.go b/pkg/api/validation/schema.go index f6a4d0798bb..f694baeaeb2 100644 --- a/pkg/api/validation/schema.go +++ b/pkg/api/validation/schema.go @@ -17,6 +17,7 @@ limitations under the License. package validation import ( + "bytes" "encoding/json" "fmt" "reflect" @@ -24,6 +25,7 @@ import ( "strings" "github.com/emicklei/go-restful/swagger" + ejson "github.com/exponent-io/jsonpath" "github.com/golang/glog" apiutil "k8s.io/kubernetes/pkg/api/util" "k8s.io/kubernetes/pkg/runtime" @@ -62,6 +64,69 @@ type NullSchema struct{} func (NullSchema) ValidateBytes(data []byte) error { return nil } +type NoDoubleKeySchema struct{} + +func (NoDoubleKeySchema) ValidateBytes(data []byte) error { + var list []error = nil + if err := validateNoDuplicateKeys(data, "metadata", "labels"); err != nil { + list = append(list, err) + } + if err := validateNoDuplicateKeys(data, "metadata", "annotations"); err != nil { + list = append(list, err) + } + return utilerrors.NewAggregate(list) +} + +func validateNoDuplicateKeys(data []byte, path ...string) error { + r := ejson.NewDecoder(bytes.NewReader(data)) + // This is Go being unfriendly. The 'path ...string' comes in as a + // []string, and SeekTo takes ...interface{}, so we can't just pass + // the path straight in, we have to copy it. *sigh* + ifacePath := []interface{}{} + for ix := range path { + ifacePath = append(ifacePath, path[ix]) + } + found, err := r.SeekTo(ifacePath...) + if err != nil { + return err + } + if !found { + return nil + } + seen := map[string]bool{} + for { + tok, err := r.Token() + if err != nil { + return err + } + switch t := tok.(type) { + case json.Delim: + if t.String() == "}" { + return nil + } + case ejson.KeyString: + if seen[string(t)] { + return fmt.Errorf("duplicate key: %s", string(t)) + } else { + seen[string(t)] = true + } + } + } +} + +type ConjunctiveSchema []Schema + +func (c ConjunctiveSchema) ValidateBytes(data []byte) error { + var list []error = nil + schemas := []Schema(c) + for ix := range schemas { + if err := schemas[ix].ValidateBytes(data); err != nil { + list = append(list, err) + } + } + return utilerrors.NewAggregate(list) +} + type SwaggerSchema struct { api swagger.ApiDeclaration delegate Schema // For delegating to other api groups diff --git a/pkg/api/validation/schema_test.go b/pkg/api/validation/schema_test.go index 41eea8c0bc6..8caf0ef098d 100644 --- a/pkg/api/validation/schema_test.go +++ b/pkg/api/validation/schema_test.go @@ -309,3 +309,118 @@ func TestTypeAny(t *testing.T) { } } } + +func TestValidateDuplicateLabelsFailCases(t *testing.T) { + strs := []string{ + `{ + "metadata": { + "labels": { + "foo": "bar", + "foo": "baz" + } + } +}`, + `{ + "metadata": { + "annotations": { + "foo": "bar", + "foo": "baz" + } + } +}`, + `{ + "metadata": { + "labels": { + "foo": "blah" + }, + "annotations": { + "foo": "bar", + "foo": "baz" + } + } +}`, + } + schema := NoDoubleKeySchema{} + for _, str := range strs { + err := schema.ValidateBytes([]byte(str)) + if err == nil { + t.Errorf("Unexpected non-error %s", str) + } + } +} + +func TestValidateDuplicateLabelsPassCases(t *testing.T) { + strs := []string{ + `{ + "metadata": { + "labels": { + "foo": "bar" + }, + "annotations": { + "foo": "baz" + } + } +}`, + `{ + "metadata": {} +}`, + `{ + "metadata": { + "labels": {} + } +}`, + } + schema := NoDoubleKeySchema{} + for _, str := range strs { + err := schema.ValidateBytes([]byte(str)) + if err != nil { + t.Errorf("Unexpected error: %v %s", err, str) + } + } +} + +type AlwaysInvalidSchema struct{} + +func (AlwaysInvalidSchema) ValidateBytes([]byte) error { + return fmt.Errorf("Always invalid!") +} + +func TestConjunctiveSchema(t *testing.T) { + tests := []struct { + schemas []Schema + shouldPass bool + name string + }{ + { + schemas: []Schema{NullSchema{}, NullSchema{}}, + shouldPass: true, + name: "all pass", + }, + { + schemas: []Schema{NullSchema{}, AlwaysInvalidSchema{}}, + shouldPass: false, + name: "one fail", + }, + { + schemas: []Schema{AlwaysInvalidSchema{}, AlwaysInvalidSchema{}}, + shouldPass: false, + name: "all fail", + }, + { + schemas: []Schema{}, + shouldPass: true, + name: "empty", + }, + } + + for _, test := range tests { + schema := ConjunctiveSchema(test.schemas) + err := schema.ValidateBytes([]byte{}) + if err != nil && test.shouldPass { + t.Errorf("Unexpected error: %v in %s", err, test.name) + } + if err == nil && !test.shouldPass { + t.Errorf("Unexpected non-error: %s", test.name) + } + } +} diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 78ae76cda2d..ddbe4f5ddc8 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -762,10 +762,14 @@ func (f *factory) Validator(validate bool, cacheDir string) (validation.Schema, if err != nil { return nil, err } - return &clientSwaggerSchema{ + swaggerSchema := &clientSwaggerSchema{ c: restclient, fedc: fedClient, cacheDir: dir, + } + return validation.ConjunctiveSchema{ + swaggerSchema, + validation.NoDoubleKeySchema{}, }, nil } return validation.NullSchema{}, nil