Cleanup filter package.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2017-09-06 11:25:28 -04:00
parent 945d80cd6a
commit 065118390a
3 changed files with 188 additions and 135 deletions

View File

@ -0,0 +1,24 @@
package filters
func ExampleArgs_MatchKVList() {
args := NewArgs(
Arg("label", "image=foo"),
Arg("label", "state=running"))
// returns true because there are no values for bogus
args.MatchKVList("bogus", nil)
// returns false because there are no sources
args.MatchKVList("label", nil)
// returns true because all sources are matched
args.MatchKVList("label", map[string]string{
"image": "foo",
"state": "running",
})
// returns false because the values do not match
args.MatchKVList("label", map[string]string{
"image": "other",
})
}

View File

@ -1,5 +1,6 @@
// Package filters provides helper function to parse and handle command line /*Package filters provides tools for encoding a mapping of keys to a set of
// filter, used for example in docker ps or docker images commands. multiple values.
*/
package filters package filters
import ( import (
@ -11,27 +12,34 @@ import (
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
) )
// Args stores filter arguments as map key:{map key: bool}. // Args stores a mapping of keys to a set of multiple values.
// It contains an aggregation of the map of arguments (which are in the form
// of -f 'key=value') based on the key, and stores values for the same key
// in a map with string keys and boolean values.
// e.g given -f 'label=label1=1' -f 'label=label2=2' -f 'image.name=ubuntu'
// the args will be {"image.name":{"ubuntu":true},"label":{"label1=1":true,"label2=2":true}}
type Args struct { type Args struct {
fields map[string]map[string]bool fields map[string]map[string]bool
} }
// NewArgs initializes a new Args struct. // KeyValuePair are used to initialize a new Args
func NewArgs() Args { type KeyValuePair struct {
return Args{fields: map[string]map[string]bool{}} Key string
Value string
} }
// ParseFlag parses the argument to the filter flag. Like // Arg creates a new KeyValuePair for initializing Args
func Arg(key, value string) KeyValuePair {
return KeyValuePair{Key: key, Value: value}
}
// NewArgs returns a new Args populated with the initial args
func NewArgs(initialArgs ...KeyValuePair) Args {
args := Args{fields: map[string]map[string]bool{}}
for _, arg := range initialArgs {
args.Add(arg.Key, arg.Value)
}
return args
}
// ParseFlag parses a key=value string and adds it to an Args.
// //
// `docker ps -f 'created=today' -f 'image.name=ubuntu*'` // Deprecated: Use Args.Add()
//
// If prev map is provided, then it is appended to, and returned. By default a new
// map is created.
func ParseFlag(arg string, prev Args) (Args, error) { func ParseFlag(arg string, prev Args) (Args, error) {
filters := prev filters := prev
if len(arg) == 0 { if len(arg) == 0 {
@ -52,74 +60,95 @@ func ParseFlag(arg string, prev Args) (Args, error) {
return filters, nil return filters, nil
} }
// ErrBadFormat is an error returned in case of bad format for a filter. // ErrBadFormat is an error returned when a filter is not in the form key=value
//
// Deprecated: this error will be removed in a future version
var ErrBadFormat = errors.New("bad format of filter (expected name=value)") var ErrBadFormat = errors.New("bad format of filter (expected name=value)")
// ToParam packs the Args into a string for easy transport from client to server. // ToParam encodes the Args as args JSON encoded string
//
// Deprecated: use ToJSON
func ToParam(a Args) (string, error) { func ToParam(a Args) (string, error) {
// this way we don't URL encode {}, just empty space return ToJSON(a)
}
// MarshalJSON returns a JSON byte representation of the Args
func (args Args) MarshalJSON() ([]byte, error) {
if len(args.fields) == 0 {
return []byte{}, nil
}
return json.Marshal(args.fields)
}
// ToJSON returns the Args as a JSON encoded string
func ToJSON(a Args) (string, error) {
if a.Len() == 0 { if a.Len() == 0 {
return "", nil return "", nil
} }
buf, err := json.Marshal(a)
buf, err := json.Marshal(a.fields) return string(buf), err
if err != nil {
return "", err
}
return string(buf), nil
} }
// ToParamWithVersion packs the Args into a string for easy transport from client to server. // ToParamWithVersion encodes Args as a JSON string. If version is less than 1.22
// The generated string will depend on the specified version (corresponding to the API version). // then the encoded format will use an older legacy format where the values are a
// list of strings, instead of a set.
//
// Deprecated: Use ToJSON
func ToParamWithVersion(version string, a Args) (string, error) { func ToParamWithVersion(version string, a Args) (string, error) {
// this way we don't URL encode {}, just empty space
if a.Len() == 0 { if a.Len() == 0 {
return "", nil return "", nil
} }
// for daemons older than v1.10, filter must be of the form map[string][]string
var buf []byte
var err error
if version != "" && versions.LessThan(version, "1.22") { if version != "" && versions.LessThan(version, "1.22") {
buf, err = json.Marshal(convertArgsToSlice(a.fields)) buf, err := json.Marshal(convertArgsToSlice(a.fields))
} else { return string(buf), err
buf, err = json.Marshal(a.fields)
} }
if err != nil {
return "", err return ToJSON(a)
}
return string(buf), nil
} }
// FromParam unpacks the filter Args. // FromParam decodes a JSON encoded string into Args
//
// Deprecated: use FromJSON
func FromParam(p string) (Args, error) { func FromParam(p string) (Args, error) {
if len(p) == 0 { return FromJSON(p)
return NewArgs(), nil
}
r := strings.NewReader(p)
d := json.NewDecoder(r)
m := map[string]map[string]bool{}
if err := d.Decode(&m); err != nil {
r.Seek(0, 0)
// Allow parsing old arguments in slice format.
// Because other libraries might be sending them in this format.
deprecated := map[string][]string{}
if deprecatedErr := d.Decode(&deprecated); deprecatedErr == nil {
m = deprecatedArgs(deprecated)
} else {
return NewArgs(), err
}
}
return Args{m}, nil
} }
// Get returns the list of values associates with a field. // FromJSON decodes a JSON encoded string into Args
// It returns a slice of strings to keep backwards compatibility with old code. func FromJSON(p string) (Args, error) {
func (filters Args) Get(field string) []string { args := NewArgs()
values := filters.fields[field]
if p == "" {
return args, nil
}
raw := []byte(p)
err := json.Unmarshal(raw, &args)
if err == nil {
return args, nil
}
// Fallback to parsing arguments in the legacy slice format
deprecated := map[string][]string{}
if legacyErr := json.Unmarshal(raw, &deprecated); legacyErr != nil {
return args, err
}
args.fields = deprecatedArgs(deprecated)
return args, nil
}
// UnmarshalJSON populates the Args from JSON encode bytes
func (args Args) UnmarshalJSON(raw []byte) error {
if len(raw) == 0 {
return nil
}
return json.Unmarshal(raw, &args.fields)
}
// Get returns the list of values associated with the key
func (args Args) Get(key string) []string {
values := args.fields[key]
if values == nil { if values == nil {
return make([]string, 0) return make([]string, 0)
} }
@ -130,37 +159,34 @@ func (filters Args) Get(field string) []string {
return slice return slice
} }
// Add adds a new value to a filter field. // Add a new value to the set of values
func (filters Args) Add(name, value string) { func (args Args) Add(key, value string) {
if _, ok := filters.fields[name]; ok { if _, ok := args.fields[key]; ok {
filters.fields[name][value] = true args.fields[key][value] = true
} else { } else {
filters.fields[name] = map[string]bool{value: true} args.fields[key] = map[string]bool{value: true}
} }
} }
// Del removes a value from a filter field. // Del removes a value from the set
func (filters Args) Del(name, value string) { func (args Args) Del(key, value string) {
if _, ok := filters.fields[name]; ok { if _, ok := args.fields[key]; ok {
delete(filters.fields[name], value) delete(args.fields[key], value)
if len(filters.fields[name]) == 0 { if len(args.fields[key]) == 0 {
delete(filters.fields, name) delete(args.fields, key)
} }
} }
} }
// Len returns the number of fields in the arguments. // Len returns the number of keys in the mapping
func (filters Args) Len() int { func (args Args) Len() int {
return len(filters.fields) return len(args.fields)
} }
// MatchKVList returns true if the values for the specified field matches the ones // MatchKVList returns true if all the pairs in sources exist as key=value
// from the sources. // pairs in the mapping at key, or if there are no values at key.
// e.g. given Args are {'label': {'label1=1','label2=1'}, 'image.name', {'ubuntu'}}, func (args Args) MatchKVList(key string, sources map[string]string) bool {
// field is 'label' and sources are {'label1': '1', 'label2': '2'} fieldValues := args.fields[key]
// it returns true.
func (filters Args) MatchKVList(field string, sources map[string]string) bool {
fieldValues := filters.fields[field]
//do not filter if there is no filter set or cannot determine filter //do not filter if there is no filter set or cannot determine filter
if len(fieldValues) == 0 { if len(fieldValues) == 0 {
@ -171,8 +197,8 @@ func (filters Args) MatchKVList(field string, sources map[string]string) bool {
return false return false
} }
for name2match := range fieldValues { for value := range fieldValues {
testKV := strings.SplitN(name2match, "=", 2) testKV := strings.SplitN(value, "=", 2)
v, ok := sources[testKV[0]] v, ok := sources[testKV[0]]
if !ok { if !ok {
@ -186,16 +212,13 @@ func (filters Args) MatchKVList(field string, sources map[string]string) bool {
return true return true
} }
// Match returns true if the values for the specified field matches the source string // Match returns true if any of the values at key match the source string
// e.g. given Args are {'label': {'label1=1','label2=1'}, 'image.name', {'ubuntu'}}, func (args Args) Match(field, source string) bool {
// field is 'image.name' and source is 'ubuntu' if args.ExactMatch(field, source) {
// it returns true.
func (filters Args) Match(field, source string) bool {
if filters.ExactMatch(field, source) {
return true return true
} }
fieldValues := filters.fields[field] fieldValues := args.fields[field]
for name2match := range fieldValues { for name2match := range fieldValues {
match, err := regexp.MatchString(name2match, source) match, err := regexp.MatchString(name2match, source)
if err != nil { if err != nil {
@ -208,9 +231,9 @@ func (filters Args) Match(field, source string) bool {
return false return false
} }
// ExactMatch returns true if the source matches exactly one of the filters. // ExactMatch returns true if the source matches exactly one of the values.
func (filters Args) ExactMatch(field, source string) bool { func (args Args) ExactMatch(key, source string) bool {
fieldValues, ok := filters.fields[field] fieldValues, ok := args.fields[key]
//do not filter if there is no filter set or cannot determine filter //do not filter if there is no filter set or cannot determine filter
if !ok || len(fieldValues) == 0 { if !ok || len(fieldValues) == 0 {
return true return true
@ -220,14 +243,15 @@ func (filters Args) ExactMatch(field, source string) bool {
return fieldValues[source] return fieldValues[source]
} }
// UniqueExactMatch returns true if there is only one filter and the source matches exactly this one. // UniqueExactMatch returns true if there is only one value and the source
func (filters Args) UniqueExactMatch(field, source string) bool { // matches exactly the value.
fieldValues := filters.fields[field] func (args Args) UniqueExactMatch(key, source string) bool {
fieldValues := args.fields[key]
//do not filter if there is no filter set or cannot determine filter //do not filter if there is no filter set or cannot determine filter
if len(fieldValues) == 0 { if len(fieldValues) == 0 {
return true return true
} }
if len(filters.fields[field]) != 1 { if len(args.fields[key]) != 1 {
return false return false
} }
@ -235,14 +259,14 @@ func (filters Args) UniqueExactMatch(field, source string) bool {
return fieldValues[source] return fieldValues[source]
} }
// FuzzyMatch returns true if the source matches exactly one of the filters, // FuzzyMatch returns true if the source matches exactly one value, or the
// or the source has one of the filters as a prefix. // source has one of the values as a prefix.
func (filters Args) FuzzyMatch(field, source string) bool { func (args Args) FuzzyMatch(key, source string) bool {
if filters.ExactMatch(field, source) { if args.ExactMatch(key, source) {
return true return true
} }
fieldValues := filters.fields[field] fieldValues := args.fields[key]
for prefix := range fieldValues { for prefix := range fieldValues {
if strings.HasPrefix(source, prefix) { if strings.HasPrefix(source, prefix) {
return true return true
@ -251,9 +275,17 @@ func (filters Args) FuzzyMatch(field, source string) bool {
return false return false
} }
// Include returns true if the name of the field to filter is in the filters. // Include returns true if the key exists in the mapping
func (filters Args) Include(field string) bool { //
_, ok := filters.fields[field] // Deprecated: use Contains
func (args Args) Include(field string) bool {
_, ok := args.fields[field]
return ok
}
// Contains returns true if the key exists in the mapping
func (args Args) Contains(field string) bool {
_, ok := args.fields[field]
return ok return ok
} }
@ -265,10 +297,10 @@ func (e invalidFilter) Error() string {
func (invalidFilter) InvalidParameter() {} func (invalidFilter) InvalidParameter() {}
// Validate ensures that all the fields in the filter are valid. // Validate compared the set of accepted keys against the keys in the mapping.
// It returns an error as soon as it finds an invalid field. // An error is returned if any mapping keys are not in the accepted set.
func (filters Args) Validate(accepted map[string]bool) error { func (args Args) Validate(accepted map[string]bool) error {
for name := range filters.fields { for name := range args.fields {
if !accepted[name] { if !accepted[name] {
return invalidFilter(name) return invalidFilter(name)
} }
@ -276,13 +308,14 @@ func (filters Args) Validate(accepted map[string]bool) error {
return nil return nil
} }
// WalkValues iterates over the list of filtered values for a field. // WalkValues iterates over the list of values for a key in the mapping and calls
// It stops the iteration if it finds an error and it returns that error. // op() for each value. If op returns an error the iteration stops and the
func (filters Args) WalkValues(field string, op func(value string) error) error { // error is returned.
if _, ok := filters.fields[field]; !ok { func (args Args) WalkValues(field string, op func(value string) error) error {
if _, ok := args.fields[field]; !ok {
return nil return nil
} }
for v := range filters.fields[field] { for v := range args.fields[field] {
if err := op(v); err != nil { if err := op(v); err != nil {
return err return err
} }

View File

@ -3,6 +3,9 @@ package filters
import ( import (
"errors" "errors"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestParseArgs(t *testing.T) { func TestParseArgs(t *testing.T) {
@ -16,23 +19,18 @@ func TestParseArgs(t *testing.T) {
args = NewArgs() args = NewArgs()
err error err error
) )
for i := range flagArgs { for i := range flagArgs {
args, err = ParseFlag(flagArgs[i], args) args, err = ParseFlag(flagArgs[i], args)
if err != nil { require.NoError(t, err)
t.Errorf("failed to parse %s: %s", flagArgs[i], err)
}
}
if len(args.Get("created")) != 1 {
t.Error("failed to set this arg")
}
if len(args.Get("image.name")) != 2 {
t.Error("the args should have collapsed")
} }
assert.Len(t, args.Get("created"), 1)
assert.Len(t, args.Get("image.name"), 2)
} }
func TestParseArgsEdgeCase(t *testing.T) { func TestParseArgsEdgeCase(t *testing.T) {
var filters Args var args Args
args, err := ParseFlag("", filters) args, err := ParseFlag("", args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -233,9 +231,8 @@ func TestArgsMatch(t *testing.T) {
} }
for args, field := range matches { for args, field := range matches {
if args.Match(field, source) != true { assert.True(t, args.Match(field, source),
t.Fatalf("Expected true for %v on %v, got false", source, args) "Expected field %s to match %s", field, source)
}
} }
differs := map[*Args]string{ differs := map[*Args]string{
@ -258,9 +255,8 @@ func TestArgsMatch(t *testing.T) {
} }
for args, field := range differs { for args, field := range differs {
if args.Match(field, source) != false { assert.False(t, args.Match(field, source),
t.Fatalf("Expected false for %v on %v, got true", source, args) "Expected field %s to not match %s", field, source)
}
} }
} }