Browse code

Make filtering a linear operation.

Improves the current filtering implementation complixity.
Currently, the best case is O(N) and worst case O(N^2) for key-value filtering.
In the new implementation, the best case is O(1) and worst case O(N), again for key-value filtering.

Signed-off-by: David Calavera <david.calavera@gmail.com>

David Calavera authored on 2015/11/26 10:27:11
Showing 13 changed files
... ...
@@ -26,7 +26,7 @@ func (cli *DockerCli) CmdEvents(args ...string) error {
26 26
 
27 27
 	var (
28 28
 		v               = url.Values{}
29
-		eventFilterArgs = filters.Args{}
29
+		eventFilterArgs = filters.NewArgs()
30 30
 	)
31 31
 
32 32
 	// Consolidate all filter flags, and sanity check them early.
... ...
@@ -53,7 +53,7 @@ func (cli *DockerCli) CmdEvents(args ...string) error {
53 53
 		}
54 54
 		v.Set("until", ts)
55 55
 	}
56
-	if len(eventFilterArgs) > 0 {
56
+	if eventFilterArgs.Len() > 0 {
57 57
 		filterJSON, err := filters.ToParam(eventFilterArgs)
58 58
 		if err != nil {
59 59
 			return err
... ...
@@ -36,7 +36,7 @@ func (cli *DockerCli) CmdImages(args ...string) error {
36 36
 
37 37
 	// Consolidate all filter flags, and sanity check them early.
38 38
 	// They'll get process in the daemon/server.
39
-	imageFilterArgs := filters.Args{}
39
+	imageFilterArgs := filters.NewArgs()
40 40
 	for _, f := range flFilter.GetAll() {
41 41
 		var err error
42 42
 		imageFilterArgs, err = filters.ParseFlag(f, imageFilterArgs)
... ...
@@ -47,7 +47,7 @@ func (cli *DockerCli) CmdImages(args ...string) error {
47 47
 
48 48
 	matchName := cmd.Arg(0)
49 49
 	v := url.Values{}
50
-	if len(imageFilterArgs) > 0 {
50
+	if imageFilterArgs.Len() > 0 {
51 51
 		filterJSON, err := filters.ToParam(imageFilterArgs)
52 52
 		if err != nil {
53 53
 			return err
... ...
@@ -20,7 +20,7 @@ func (cli *DockerCli) CmdPs(args ...string) error {
20 20
 	var (
21 21
 		err error
22 22
 
23
-		psFilterArgs = filters.Args{}
23
+		psFilterArgs = filters.NewArgs()
24 24
 		v            = url.Values{}
25 25
 
26 26
 		cmd      = Cli.Subcmd("ps", nil, Cli.DockerCommands["ps"].Description, true)
... ...
@@ -72,7 +72,7 @@ func (cli *DockerCli) CmdPs(args ...string) error {
72 72
 		}
73 73
 	}
74 74
 
75
-	if len(psFilterArgs) > 0 {
75
+	if psFilterArgs.Len() > 0 {
76 76
 		filterJSON, err := filters.ToParam(psFilterArgs)
77 77
 		if err != nil {
78 78
 			return err
... ...
@@ -54,7 +54,7 @@ func (cli *DockerCli) CmdVolumeLs(args ...string) error {
54 54
 	cmd.Require(flag.Exact, 0)
55 55
 	cmd.ParseFlags(args, true)
56 56
 
57
-	volFilterArgs := filters.Args{}
57
+	volFilterArgs := filters.NewArgs()
58 58
 	for _, f := range flFilter.GetAll() {
59 59
 		var err error
60 60
 		volFilterArgs, err = filters.ParseFlag(f, volFilterArgs)
... ...
@@ -64,7 +64,7 @@ func (cli *DockerCli) CmdVolumeLs(args ...string) error {
64 64
 	}
65 65
 
66 66
 	v := url.Values{}
67
-	if len(volFilterArgs) > 0 {
67
+	if volFilterArgs.Len() > 0 {
68 68
 		filterJSON, err := filters.ToParam(volFilterArgs)
69 69
 		if err != nil {
70 70
 			return err
... ...
@@ -29,27 +29,23 @@ func (n *networkRouter) getNetworksList(ctx context.Context, w http.ResponseWrit
29 29
 	}
30 30
 
31 31
 	list := []*types.NetworkResource{}
32
-	var nameFilter, idFilter bool
33
-	var names, ids []string
34
-	if names, nameFilter = netFilters["name"]; nameFilter {
35
-		for _, name := range names {
36
-			if nw, err := n.backend.GetNetwork(name, daemon.NetworkByName); err == nil {
37
-				list = append(list, buildNetworkResource(nw))
38
-			} else {
39
-				logrus.Errorf("failed to get network for filter=%s : %v", name, err)
40
-			}
32
+	netFilters.WalkValues("name", func(name string) error {
33
+		if nw, err := n.backend.GetNetwork(name, daemon.NetworkByName); err == nil {
34
+			list = append(list, buildNetworkResource(nw))
35
+		} else {
36
+			logrus.Errorf("failed to get network for filter=%s : %v", name, err)
41 37
 		}
42
-	}
38
+		return nil
39
+	})
43 40
 
44
-	if ids, idFilter = netFilters["id"]; idFilter {
45
-		for _, id := range ids {
46
-			for _, nw := range n.backend.GetNetworksByID(id) {
47
-				list = append(list, buildNetworkResource(nw))
48
-			}
41
+	netFilters.WalkValues("id", func(id string) error {
42
+		for _, nw := range n.backend.GetNetworksByID(id) {
43
+			list = append(list, buildNetworkResource(nw))
49 44
 		}
50
-	}
45
+		return nil
46
+	})
51 47
 
52
-	if !nameFilter && !idFilter {
48
+	if !netFilters.Include("name") && !netFilters.Include("id") {
53 49
 		nwList := n.backend.GetNetworksByID("")
54 50
 		for _, nw := range nwList {
55 51
 			list = append(list, buildNetworkResource(nw))
... ...
@@ -536,12 +536,11 @@ func (daemon *Daemon) GetByName(name string) (*Container, error) {
536 536
 func (daemon *Daemon) GetEventFilter(filter filters.Args) *events.Filter {
537 537
 	// incoming container filter can be name, id or partial id, convert to
538 538
 	// a full container id
539
-	for i, cn := range filter["container"] {
539
+	for _, cn := range filter.Get("container") {
540 540
 		c, err := daemon.Get(cn)
541
-		if err != nil {
542
-			filter["container"][i] = ""
543
-		} else {
544
-			filter["container"][i] = c.ID
541
+		filter.Del("container", cn)
542
+		if err == nil {
543
+			filter.Add("container", c.ID)
545 544
 		}
546 545
 	}
547 546
 	return events.NewFilter(filter, daemon.GetLabels)
... ...
@@ -19,14 +19,14 @@ func NewFilter(filter filters.Args, getLabels func(id string) map[string]string)
19 19
 
20 20
 // Include returns true when the event ev is included by the filters
21 21
 func (ef *Filter) Include(ev *jsonmessage.JSONMessage) bool {
22
-	return isFieldIncluded(ev.Status, ef.filter["event"]) &&
23
-		isFieldIncluded(ev.ID, ef.filter["container"]) &&
22
+	return ef.filter.ExactMatch("event", ev.Status) &&
23
+		ef.filter.ExactMatch("container", ev.ID) &&
24 24
 		ef.isImageIncluded(ev.ID, ev.From) &&
25 25
 		ef.isLabelFieldIncluded(ev.ID)
26 26
 }
27 27
 
28 28
 func (ef *Filter) isLabelFieldIncluded(id string) bool {
29
-	if _, ok := ef.filter["label"]; !ok {
29
+	if !ef.filter.Include("label") {
30 30
 		return true
31 31
 	}
32 32
 	return ef.filter.MatchKVList("label", ef.getLabels(id))
... ...
@@ -37,31 +37,16 @@ func (ef *Filter) isLabelFieldIncluded(id string) bool {
37 37
 // from an image will be included in the image events. Also compare both
38 38
 // against the stripped repo name without any tags.
39 39
 func (ef *Filter) isImageIncluded(eventID string, eventFrom string) bool {
40
-	stripTag := func(image string) string {
41
-		ref, err := reference.ParseNamed(image)
42
-		if err != nil {
43
-			return image
44
-		}
45
-		return ref.Name()
46
-	}
47
-
48
-	return isFieldIncluded(eventID, ef.filter["image"]) ||
49
-		isFieldIncluded(eventFrom, ef.filter["image"]) ||
50
-		isFieldIncluded(stripTag(eventID), ef.filter["image"]) ||
51
-		isFieldIncluded(stripTag(eventFrom), ef.filter["image"])
40
+	return ef.filter.ExactMatch("image", eventID) ||
41
+		ef.filter.ExactMatch("image", eventFrom) ||
42
+		ef.filter.ExactMatch("image", stripTag(eventID)) ||
43
+		ef.filter.ExactMatch("image", stripTag(eventFrom))
52 44
 }
53 45
 
54
-func isFieldIncluded(field string, filter []string) bool {
55
-	if len(field) == 0 {
56
-		return true
57
-	}
58
-	if len(filter) == 0 {
59
-		return true
60
-	}
61
-	for _, v := range filter {
62
-		if v == field {
63
-			return true
64
-		}
46
+func stripTag(image string) string {
47
+	ref, err := reference.ParseNamed(image)
48
+	if err != nil {
49
+		return image
65 50
 	}
66
-	return false
51
+	return ref.Name()
67 52
 }
... ...
@@ -4,7 +4,6 @@ import (
4 4
 	"fmt"
5 5
 	"path"
6 6
 	"sort"
7
-	"strings"
8 7
 
9 8
 	"github.com/docker/distribution/reference"
10 9
 	"github.com/docker/docker/api/types"
... ...
@@ -13,9 +12,9 @@ import (
13 13
 	"github.com/docker/docker/pkg/parsers/filters"
14 14
 )
15 15
 
16
-var acceptedImageFilterTags = map[string]struct{}{
17
-	"dangling": {},
18
-	"label":    {},
16
+var acceptedImageFilterTags = map[string]bool{
17
+	"dangling": true,
18
+	"label":    true,
19 19
 }
20 20
 
21 21
 // byCreated is a temporary type used to sort a list of images by creation
... ...
@@ -47,19 +46,15 @@ func (daemon *Daemon) Images(filterArgs, filter string, all bool) ([]*types.Imag
47 47
 	if err != nil {
48 48
 		return nil, err
49 49
 	}
50
-	for name := range imageFilters {
51
-		if _, ok := acceptedImageFilterTags[name]; !ok {
52
-			return nil, fmt.Errorf("Invalid filter '%s'", name)
53
-		}
50
+	if err := imageFilters.Validate(acceptedImageFilterTags); err != nil {
51
+		return nil, err
54 52
 	}
55 53
 
56
-	if i, ok := imageFilters["dangling"]; ok {
57
-		for _, value := range i {
58
-			if v := strings.ToLower(value); v == "true" {
59
-				danglingOnly = true
60
-			} else if v != "false" {
61
-				return nil, fmt.Errorf("Invalid filter 'dangling=%s'", v)
62
-			}
54
+	if imageFilters.Include("dangling") {
55
+		if imageFilters.ExactMatch("dangling", "true") {
56
+			danglingOnly = true
57
+		} else if !imageFilters.ExactMatch("dangling", "false") {
58
+			return nil, fmt.Errorf("Invalid filter 'dangling=%s'", imageFilters.Get("dangling"))
63 59
 		}
64 60
 	}
65 61
 
... ...
@@ -82,9 +77,9 @@ func (daemon *Daemon) Images(filterArgs, filter string, all bool) ([]*types.Imag
82 82
 	}
83 83
 
84 84
 	for id, img := range allImages {
85
-		if _, ok := imageFilters["label"]; ok {
85
+		if imageFilters.Include("label") {
86
+			// Very old image that do not have image.Config (or even labels)
86 87
 			if img.Config == nil {
87
-				// Very old image that do not have image.Config (or even labels)
88 88
 				continue
89 89
 			}
90 90
 			// We are now sure image.Config is not nil
... ...
@@ -8,7 +8,6 @@ import (
8 8
 
9 9
 	"github.com/Sirupsen/logrus"
10 10
 	"github.com/docker/docker/api/types"
11
-	derr "github.com/docker/docker/errors"
12 11
 	"github.com/docker/docker/image"
13 12
 	"github.com/docker/docker/pkg/graphdb"
14 13
 	"github.com/docker/docker/pkg/nat"
... ...
@@ -136,64 +135,65 @@ func (daemon *Daemon) foldFilter(config *ContainersConfig) (*listContext, error)
136 136
 	}
137 137
 
138 138
 	var filtExited []int
139
-	if i, ok := psFilters["exited"]; ok {
140
-		for _, value := range i {
141
-			code, err := strconv.Atoi(value)
142
-			if err != nil {
143
-				return nil, err
144
-			}
145
-			filtExited = append(filtExited, code)
139
+	err = psFilters.WalkValues("exited", func(value string) error {
140
+		code, err := strconv.Atoi(value)
141
+		if err != nil {
142
+			return err
146 143
 		}
144
+		filtExited = append(filtExited, code)
145
+		return nil
146
+	})
147
+	if err != nil {
148
+		return nil, err
147 149
 	}
148 150
 
149
-	if i, ok := psFilters["status"]; ok {
150
-		for _, value := range i {
151
-			if !isValidStateString(value) {
152
-				return nil, errors.New("Unrecognised filter value for status")
153
-			}
154
-
155
-			config.All = true
151
+	err = psFilters.WalkValues("status", func(value string) error {
152
+		if !isValidStateString(value) {
153
+			return fmt.Errorf("Unrecognised filter value for status: %s", value)
156 154
 		}
155
+
156
+		config.All = true
157
+		return nil
158
+	})
159
+	if err != nil {
160
+		return nil, err
157 161
 	}
158 162
 
159 163
 	var beforeContFilter, sinceContFilter *Container
160
-	if i, ok := psFilters["before"]; ok {
161
-		for _, value := range i {
162
-			beforeContFilter, err = daemon.Get(value)
163
-			if err != nil {
164
-				return nil, err
165
-			}
166
-		}
164
+	err = psFilters.WalkValues("before", func(value string) error {
165
+		beforeContFilter, err = daemon.Get(value)
166
+		return err
167
+	})
168
+	if err != nil {
169
+		return nil, err
167 170
 	}
168 171
 
169
-	if i, ok := psFilters["since"]; ok {
170
-		for _, value := range i {
171
-			sinceContFilter, err = daemon.Get(value)
172
-			if err != nil {
173
-				return nil, err
174
-			}
175
-		}
172
+	err = psFilters.WalkValues("since", func(value string) error {
173
+		sinceContFilter, err = daemon.Get(value)
174
+		return err
175
+	})
176
+	if err != nil {
177
+		return nil, err
176 178
 	}
177 179
 
178 180
 	imagesFilter := map[image.ID]bool{}
179 181
 	var ancestorFilter bool
180
-	if ancestors, ok := psFilters["ancestor"]; ok {
182
+	if psFilters.Include("ancestor") {
181 183
 		ancestorFilter = true
182
-		// The idea is to walk the graph down the most "efficient" way.
183
-		for _, ancestor := range ancestors {
184
-			// First, get the imageId of the ancestor filter (yay)
184
+		psFilters.WalkValues("ancestor", func(ancestor string) error {
185 185
 			id, err := daemon.GetImageID(ancestor)
186 186
 			if err != nil {
187 187
 				logrus.Warnf("Error while looking up for image %v", ancestor)
188
-				continue
188
+				return nil
189 189
 			}
190 190
 			if imagesFilter[id] {
191 191
 				// Already seen this ancestor, skip it
192
-				continue
192
+				return nil
193 193
 			}
194 194
 			// Then walk down the graph and put the imageIds in imagesFilter
195 195
 			populateImageFilterByParents(imagesFilter, id, daemon.imageStore.Children)
196
-		}
196
+			return nil
197
+		})
197 198
 	}
198 199
 
199 200
 	names := make(map[string][]string)
... ...
@@ -202,14 +202,14 @@ func (daemon *Daemon) foldFilter(config *ContainersConfig) (*listContext, error)
202 202
 		return nil
203 203
 	}, 1)
204 204
 
205
-	if config.Before != "" {
205
+	if config.Before != "" && beforeContFilter == nil {
206 206
 		beforeContFilter, err = daemon.Get(config.Before)
207 207
 		if err != nil {
208 208
 			return nil, err
209 209
 		}
210 210
 	}
211 211
 
212
-	if config.Since != "" {
212
+	if config.Since != "" && sinceContFilter == nil {
213 213
 		sinceContFilter, err = daemon.Get(config.Since)
214 214
 		if err != nil {
215 215
 			return nil, err
... ...
@@ -397,17 +397,8 @@ func (daemon *Daemon) Volumes(filter string) ([]*types.Volume, error) {
397 397
 		return nil, err
398 398
 	}
399 399
 
400
-	filterUsed := false
401
-	if i, ok := volFilters["dangling"]; ok {
402
-		if len(i) > 1 {
403
-			return nil, derr.ErrorCodeDanglingOne
404
-		}
405
-
406
-		filterValue := i[0]
407
-		if strings.ToLower(filterValue) == "true" || filterValue == "1" {
408
-			filterUsed = true
409
-		}
410
-	}
400
+	filterUsed := volFilters.Include("dangling") &&
401
+		(volFilters.ExactMatch("dangling", "true") || volFilters.ExactMatch("dangling", "1"))
411 402
 
412 403
 	volumes := daemon.volumes.List()
413 404
 	for _, v := range volumes {
... ...
@@ -230,9 +230,9 @@ func isNetworkAvailable(c *check.C, name string) bool {
230 230
 func getNetworkIDByName(c *check.C, name string) string {
231 231
 	var (
232 232
 		v          = url.Values{}
233
-		filterArgs = filters.Args{}
233
+		filterArgs = filters.NewArgs()
234 234
 	)
235
-	filterArgs["name"] = []string{name}
235
+	filterArgs.Add("name", name)
236 236
 	filterJSON, err := filters.ToParam(filterArgs)
237 237
 	c.Assert(err, checker.IsNil)
238 238
 	v.Set("filters", filterJSON)
... ...
@@ -74,7 +74,7 @@ func (s *DockerSuite) TestImagesErrorWithInvalidFilterNameTest(c *check.C) {
74 74
 	c.Assert(out, checker.Contains, "Invalid filter")
75 75
 }
76 76
 
77
-func (s *DockerSuite) TestImagesFilterLabel(c *check.C) {
77
+func (s *DockerSuite) TestImagesFilterLabelMatch(c *check.C) {
78 78
 	testRequires(c, DaemonIsLinux)
79 79
 	imageName1 := "images_filter_test1"
80 80
 	imageName2 := "images_filter_test2"
... ...
@@ -5,6 +5,7 @@ package filters
5 5
 import (
6 6
 	"encoding/json"
7 7
 	"errors"
8
+	"fmt"
8 9
 	"regexp"
9 10
 	"strings"
10 11
 )
... ...
@@ -15,7 +16,14 @@ import (
15 15
 // in an slice.
16 16
 // e.g given -f 'label=label1=1' -f 'label=label2=2' -f 'image.name=ubuntu'
17 17
 // the args will be {'label': {'label1=1','label2=2'}, 'image.name', {'ubuntu'}}
18
-type Args map[string][]string
18
+type Args struct {
19
+	fields map[string]map[string]bool
20
+}
21
+
22
+// NewArgs initializes a new Args struct.
23
+func NewArgs() Args {
24
+	return Args{fields: map[string]map[string]bool{}}
25
+}
19 26
 
20 27
 // ParseFlag parses the argument to the filter flag. Like
21 28
 //
... ...
@@ -25,9 +33,6 @@ type Args map[string][]string
25 25
 // map is created.
26 26
 func ParseFlag(arg string, prev Args) (Args, error) {
27 27
 	filters := prev
28
-	if prev == nil {
29
-		filters = Args{}
30
-	}
31 28
 	if len(arg) == 0 {
32 29
 		return filters, nil
33 30
 	}
... ...
@@ -37,9 +42,11 @@ func ParseFlag(arg string, prev Args) (Args, error) {
37 37
 	}
38 38
 
39 39
 	f := strings.SplitN(arg, "=", 2)
40
+
40 41
 	name := strings.ToLower(strings.TrimSpace(f[0]))
41 42
 	value := strings.TrimSpace(f[1])
42
-	filters[name] = append(filters[name], value)
43
+
44
+	filters.Add(name, value)
43 45
 
44 46
 	return filters, nil
45 47
 }
... ...
@@ -50,11 +57,11 @@ var ErrBadFormat = errors.New("bad format of filter (expected name=value)")
50 50
 // ToParam packs the Args into an string for easy transport from client to server.
51 51
 func ToParam(a Args) (string, error) {
52 52
 	// this way we don't URL encode {}, just empty space
53
-	if len(a) == 0 {
53
+	if a.Len() == 0 {
54 54
 		return "", nil
55 55
 	}
56 56
 
57
-	buf, err := json.Marshal(a)
57
+	buf, err := json.Marshal(a.fields)
58 58
 	if err != nil {
59 59
 		return "", err
60 60
 	}
... ...
@@ -63,23 +70,71 @@ func ToParam(a Args) (string, error) {
63 63
 
64 64
 // FromParam unpacks the filter Args.
65 65
 func FromParam(p string) (Args, error) {
66
-	args := Args{}
67 66
 	if len(p) == 0 {
68
-		return args, nil
67
+		return NewArgs(), nil
68
+	}
69
+
70
+	r := strings.NewReader(p)
71
+	d := json.NewDecoder(r)
72
+
73
+	m := map[string]map[string]bool{}
74
+	if err := d.Decode(&m); err != nil {
75
+		r.Seek(0, 0)
76
+
77
+		// Allow parsing old arguments in slice format.
78
+		// Because other libraries might be sending them in this format.
79
+		deprecated := map[string][]string{}
80
+		if deprecatedErr := d.Decode(&deprecated); deprecatedErr == nil {
81
+			m = deprecatedArgs(deprecated)
82
+		} else {
83
+			return NewArgs(), err
84
+		}
85
+	}
86
+	return Args{m}, nil
87
+}
88
+
89
+// Get returns the list of values associates with a field.
90
+// It returns a slice of strings to keep backwards compatibility with old code.
91
+func (filters Args) Get(field string) []string {
92
+	values := filters.fields[field]
93
+	if values == nil {
94
+		return make([]string, 0)
69 95
 	}
70
-	if err := json.NewDecoder(strings.NewReader(p)).Decode(&args); err != nil {
71
-		return nil, err
96
+	slice := make([]string, 0, len(values))
97
+	for key := range values {
98
+		slice = append(slice, key)
72 99
 	}
73
-	return args, nil
100
+	return slice
101
+}
102
+
103
+// Add adds a new value to a filter field.
104
+func (filters Args) Add(name, value string) {
105
+	if _, ok := filters.fields[name]; ok {
106
+		filters.fields[name][value] = true
107
+	} else {
108
+		filters.fields[name] = map[string]bool{value: true}
109
+	}
110
+}
111
+
112
+// Del removes a value from a filter field.
113
+func (filters Args) Del(name, value string) {
114
+	if _, ok := filters.fields[name]; ok {
115
+		delete(filters.fields[name], value)
116
+	}
117
+}
118
+
119
+// Len returns the number of fields in the arguments.
120
+func (filters Args) Len() int {
121
+	return len(filters.fields)
74 122
 }
75 123
 
76 124
 // MatchKVList returns true if the values for the specified field maches the ones
77 125
 // from the sources.
78 126
 // e.g. given Args are {'label': {'label1=1','label2=1'}, 'image.name', {'ubuntu'}},
79
-//      field is 'label' and sources are {'label':{'label1=1','label2=2','label3=3'}}
127
+//      field is 'label' and sources are {'label1': '1', 'label2': '2'}
80 128
 //      it returns true.
81 129
 func (filters Args) MatchKVList(field string, sources map[string]string) bool {
82
-	fieldValues := filters[field]
130
+	fieldValues := filters.fields[field]
83 131
 
84 132
 	//do not filter if there is no filter set or cannot determine filter
85 133
 	if len(fieldValues) == 0 {
... ...
@@ -90,21 +145,16 @@ func (filters Args) MatchKVList(field string, sources map[string]string) bool {
90 90
 		return false
91 91
 	}
92 92
 
93
-outer:
94
-	for _, name2match := range fieldValues {
93
+	for name2match := range fieldValues {
95 94
 		testKV := strings.SplitN(name2match, "=", 2)
96 95
 
97
-		for k, v := range sources {
98
-			if len(testKV) == 1 {
99
-				if k == testKV[0] {
100
-					continue outer
101
-				}
102
-			} else if k == testKV[0] && v == testKV[1] {
103
-				continue outer
104
-			}
96
+		v, ok := sources[testKV[0]]
97
+		if !ok {
98
+			return false
99
+		}
100
+		if len(testKV) == 2 && testKV[1] != v {
101
+			return false
105 102
 		}
106
-
107
-		return false
108 103
 	}
109 104
 
110 105
 	return true
... ...
@@ -115,13 +165,12 @@ outer:
115 115
 //      field is 'image.name' and source is 'ubuntu'
116 116
 //      it returns true.
117 117
 func (filters Args) Match(field, source string) bool {
118
-	fieldValues := filters[field]
119
-
120
-	//do not filter if there is no filter set or cannot determine filter
121
-	if len(fieldValues) == 0 {
118
+	if filters.ExactMatch(field, source) {
122 119
 		return true
123 120
 	}
124
-	for _, name2match := range fieldValues {
121
+
122
+	fieldValues := filters.fields[field]
123
+	for name2match := range fieldValues {
125 124
 		match, err := regexp.MatchString(name2match, source)
126 125
 		if err != nil {
127 126
 			continue
... ...
@@ -132,3 +181,61 @@ func (filters Args) Match(field, source string) bool {
132 132
 	}
133 133
 	return false
134 134
 }
135
+
136
+// ExactMatch returns true if the source matches exactly one of the filters.
137
+func (filters Args) ExactMatch(field, source string) bool {
138
+	fieldValues, ok := filters.fields[field]
139
+	//do not filter if there is no filter set or cannot determine filter
140
+	if !ok || len(fieldValues) == 0 {
141
+		return true
142
+	}
143
+
144
+	// try to march full name value to avoid O(N) regular expression matching
145
+	if fieldValues[source] {
146
+		return true
147
+	}
148
+	return false
149
+}
150
+
151
+// Include returns true if the name of the field to filter is in the filters.
152
+func (filters Args) Include(field string) bool {
153
+	_, ok := filters.fields[field]
154
+	return ok
155
+}
156
+
157
+// Validate ensures that all the fields in the filter are valid.
158
+// It returns an error as soon as it finds an invalid field.
159
+func (filters Args) Validate(accepted map[string]bool) error {
160
+	for name := range filters.fields {
161
+		if !accepted[name] {
162
+			return fmt.Errorf("Invalid filter '%s'", name)
163
+		}
164
+	}
165
+	return nil
166
+}
167
+
168
+// WalkValues iterates over the list of filtered values for a field.
169
+// It stops the iteration if it finds an error and it returns that error.
170
+func (filters Args) WalkValues(field string, op func(value string) error) error {
171
+	if _, ok := filters.fields[field]; !ok {
172
+		return nil
173
+	}
174
+	for v := range filters.fields[field] {
175
+		if err := op(v); err != nil {
176
+			return err
177
+		}
178
+	}
179
+	return nil
180
+}
181
+
182
+func deprecatedArgs(d map[string][]string) map[string]map[string]bool {
183
+	m := map[string]map[string]bool{}
184
+	for k, v := range d {
185
+		values := map[string]bool{}
186
+		for _, vv := range v {
187
+			values[vv] = true
188
+		}
189
+		m[k] = values
190
+	}
191
+	return m
192
+}
... ...
@@ -1,7 +1,7 @@
1 1
 package filters
2 2
 
3 3
 import (
4
-	"sort"
4
+	"fmt"
5 5
 	"testing"
6 6
 )
7 7
 
... ...
@@ -13,7 +13,7 @@ func TestParseArgs(t *testing.T) {
13 13
 		"image.name=*untu",
14 14
 	}
15 15
 	var (
16
-		args = Args{}
16
+		args = NewArgs()
17 17
 		err  error
18 18
 	)
19 19
 	for i := range flagArgs {
... ...
@@ -22,10 +22,10 @@ func TestParseArgs(t *testing.T) {
22 22
 			t.Errorf("failed to parse %s: %s", flagArgs[i], err)
23 23
 		}
24 24
 	}
25
-	if len(args["created"]) != 1 {
25
+	if len(args.Get("created")) != 1 {
26 26
 		t.Errorf("failed to set this arg")
27 27
 	}
28
-	if len(args["image.name"]) != 2 {
28
+	if len(args.Get("image.name")) != 2 {
29 29
 		t.Errorf("the args should have collapsed")
30 30
 	}
31 31
 }
... ...
@@ -36,7 +36,7 @@ func TestParseArgsEdgeCase(t *testing.T) {
36 36
 	if err != nil {
37 37
 		t.Fatal(err)
38 38
 	}
39
-	if args == nil || len(args) != 0 {
39
+	if args.Len() != 0 {
40 40
 		t.Fatalf("Expected an empty Args (map), got %v", args)
41 41
 	}
42 42
 	if args, err = ParseFlag("anything", args); err == nil || err != ErrBadFormat {
... ...
@@ -45,10 +45,11 @@ func TestParseArgsEdgeCase(t *testing.T) {
45 45
 }
46 46
 
47 47
 func TestToParam(t *testing.T) {
48
-	a := Args{
49
-		"created":    []string{"today"},
50
-		"image.name": []string{"ubuntu*", "*untu"},
48
+	fields := map[string]map[string]bool{
49
+		"created":    {"today": true},
50
+		"image.name": {"ubuntu*": true, "*untu": true},
51 51
 	}
52
+	a := Args{fields: fields}
52 53
 
53 54
 	_, err := ToParam(a)
54 55
 	if err != nil {
... ...
@@ -63,42 +64,48 @@ func TestFromParam(t *testing.T) {
63 63
 		"{'key': 'value'}",
64 64
 		`{"key": "value"}`,
65 65
 	}
66
-	valids := map[string]Args{
67
-		`{"key": ["value"]}`: {
68
-			"key": {"value"},
66
+	valid := map[*Args][]string{
67
+		&Args{fields: map[string]map[string]bool{"key": {"value": true}}}: {
68
+			`{"key": ["value"]}`,
69
+			`{"key": {"value": true}}`,
69 70
 		},
70
-		`{"key": ["value1", "value2"]}`: {
71
-			"key": {"value1", "value2"},
71
+		&Args{fields: map[string]map[string]bool{"key": {"value1": true, "value2": true}}}: {
72
+			`{"key": ["value1", "value2"]}`,
73
+			`{"key": {"value1": true, "value2": true}}`,
72 74
 		},
73
-		`{"key1": ["value1"], "key2": ["value2"]}`: {
74
-			"key1": {"value1"},
75
-			"key2": {"value2"},
75
+		&Args{fields: map[string]map[string]bool{"key1": {"value1": true}, "key2": {"value2": true}}}: {
76
+			`{"key1": ["value1"], "key2": ["value2"]}`,
77
+			`{"key1": {"value1": true}, "key2": {"value2": true}}`,
76 78
 		},
77 79
 	}
80
+
78 81
 	for _, invalid := range invalids {
79 82
 		if _, err := FromParam(invalid); err == nil {
80 83
 			t.Fatalf("Expected an error with %v, got nothing", invalid)
81 84
 		}
82 85
 	}
83
-	for json, expectedArgs := range valids {
84
-		args, err := FromParam(json)
85
-		if err != nil {
86
-			t.Fatal(err)
87
-		}
88
-		if len(args) != len(expectedArgs) {
89
-			t.Fatalf("Expected %v, go %v", expectedArgs, args)
90
-		}
91
-		for key, expectedValues := range expectedArgs {
92
-			values := args[key]
93
-			sort.Strings(values)
94
-			sort.Strings(expectedValues)
95
-			if len(values) != len(expectedValues) {
86
+
87
+	for expectedArgs, matchers := range valid {
88
+		for _, json := range matchers {
89
+			args, err := FromParam(json)
90
+			if err != nil {
91
+				t.Fatal(err)
92
+			}
93
+			if args.Len() != expectedArgs.Len() {
96 94
 				t.Fatalf("Expected %v, go %v", expectedArgs, args)
97 95
 			}
98
-			for index, expectedValue := range expectedValues {
99
-				if values[index] != expectedValue {
96
+			for key, expectedValues := range expectedArgs.fields {
97
+				values := args.Get(key)
98
+
99
+				if len(values) != len(expectedValues) {
100 100
 					t.Fatalf("Expected %v, go %v", expectedArgs, args)
101 101
 				}
102
+
103
+				for _, v := range values {
104
+					if !expectedValues[v] {
105
+						t.Fatalf("Expected %v, go %v", expectedArgs, args)
106
+					}
107
+				}
102 108
 			}
103 109
 		}
104 110
 	}
... ...
@@ -114,54 +121,63 @@ func TestEmpty(t *testing.T) {
114 114
 	if err != nil {
115 115
 		t.Errorf("%s", err)
116 116
 	}
117
-	if len(a) != len(v1) {
117
+	if a.Len() != v1.Len() {
118 118
 		t.Errorf("these should both be empty sets")
119 119
 	}
120 120
 }
121 121
 
122
-func TestArgsMatchKVList(t *testing.T) {
123
-	// empty sources
124
-	args := Args{
125
-		"created": []string{"today"},
122
+func TestArgsMatchKVListEmptySources(t *testing.T) {
123
+	args := NewArgs()
124
+	if !args.MatchKVList("created", map[string]string{}) {
125
+		t.Fatalf("Expected true for (%v,created), got true", args)
126 126
 	}
127
+
128
+	args = Args{map[string]map[string]bool{"created": {"today": true}}}
127 129
 	if args.MatchKVList("created", map[string]string{}) {
128 130
 		t.Fatalf("Expected false for (%v,created), got true", args)
129 131
 	}
132
+}
133
+
134
+func TestArgsMatchKVList(t *testing.T) {
130 135
 	// Not empty sources
131 136
 	sources := map[string]string{
132 137
 		"key1": "value1",
133 138
 		"key2": "value2",
134 139
 		"key3": "value3",
135 140
 	}
141
+
136 142
 	matches := map[*Args]string{
137 143
 		&Args{}: "field",
138
-		&Args{
139
-			"created": []string{"today"},
140
-			"labels":  []string{"key1"},
144
+		&Args{map[string]map[string]bool{
145
+			"created": map[string]bool{"today": true},
146
+			"labels":  map[string]bool{"key1": true}},
141 147
 		}: "labels",
142
-		&Args{
143
-			"created": []string{"today"},
144
-			"labels":  []string{"key1=value1"},
145
-		}: "labels",
146
-	}
147
-	differs := map[*Args]string{
148
-		&Args{
149
-			"created": []string{"today"},
150
-		}: "created",
151
-		&Args{
152
-			"created": []string{"today"},
153
-			"labels":  []string{"key4"},
154
-		}: "labels",
155
-		&Args{
156
-			"created": []string{"today"},
157
-			"labels":  []string{"key1=value3"},
148
+		&Args{map[string]map[string]bool{
149
+			"created": map[string]bool{"today": true},
150
+			"labels":  map[string]bool{"key1=value1": true}},
158 151
 		}: "labels",
159 152
 	}
153
+
160 154
 	for args, field := range matches {
161 155
 		if args.MatchKVList(field, sources) != true {
162 156
 			t.Fatalf("Expected true for %v on %v, got false", sources, args)
163 157
 		}
164 158
 	}
159
+
160
+	differs := map[*Args]string{
161
+		&Args{map[string]map[string]bool{
162
+			"created": map[string]bool{"today": true}},
163
+		}: "created",
164
+		&Args{map[string]map[string]bool{
165
+			"created": map[string]bool{"today": true},
166
+			"labels":  map[string]bool{"key4": true}},
167
+		}: "labels",
168
+		&Args{map[string]map[string]bool{
169
+			"created": map[string]bool{"today": true},
170
+			"labels":  map[string]bool{"key1=value3": true}},
171
+		}: "labels",
172
+	}
173
+
165 174
 	for args, field := range differs {
166 175
 		if args.MatchKVList(field, sources) != false {
167 176
 			t.Fatalf("Expected false for %v on %v, got true", sources, args)
... ...
@@ -171,48 +187,165 @@ func TestArgsMatchKVList(t *testing.T) {
171 171
 
172 172
 func TestArgsMatch(t *testing.T) {
173 173
 	source := "today"
174
+
174 175
 	matches := map[*Args]string{
175 176
 		&Args{}: "field",
176
-		&Args{
177
-			"created": []string{"today"},
178
-			"labels":  []string{"key1"},
177
+		&Args{map[string]map[string]bool{
178
+			"created": map[string]bool{"today": true}},
179 179
 		}: "today",
180
-		&Args{
181
-			"created": []string{"to*"},
180
+		&Args{map[string]map[string]bool{
181
+			"created": map[string]bool{"to*": true}},
182 182
 		}: "created",
183
-		&Args{
184
-			"created": []string{"to(.*)"},
183
+		&Args{map[string]map[string]bool{
184
+			"created": map[string]bool{"to(.*)": true}},
185 185
 		}: "created",
186
-		&Args{
187
-			"created": []string{"tod"},
186
+		&Args{map[string]map[string]bool{
187
+			"created": map[string]bool{"tod": true}},
188 188
 		}: "created",
189
-		&Args{
190
-			"created": []string{"anything", "to*"},
189
+		&Args{map[string]map[string]bool{
190
+			"created": map[string]bool{"anyting": true, "to*": true}},
191 191
 		}: "created",
192 192
 	}
193
+
194
+	for args, field := range matches {
195
+		if args.Match(field, source) != true {
196
+			t.Fatalf("Expected true for %v on %v, got false", source, args)
197
+		}
198
+	}
199
+
193 200
 	differs := map[*Args]string{
194
-		&Args{
195
-			"created": []string{"tomorrow"},
201
+		&Args{map[string]map[string]bool{
202
+			"created": map[string]bool{"tomorrow": true}},
196 203
 		}: "created",
197
-		&Args{
198
-			"created": []string{"to(day"},
204
+		&Args{map[string]map[string]bool{
205
+			"created": map[string]bool{"to(day": true}},
199 206
 		}: "created",
200
-		&Args{
201
-			"created": []string{"tom(.*)"},
207
+		&Args{map[string]map[string]bool{
208
+			"created": map[string]bool{"tom(.*)": true}},
202 209
 		}: "created",
203
-		&Args{
204
-			"created": []string{"today1"},
205
-			"labels":  []string{"today"},
210
+		&Args{map[string]map[string]bool{
211
+			"created": map[string]bool{"tom": true}},
212
+		}: "created",
213
+		&Args{map[string]map[string]bool{
214
+			"created": map[string]bool{"today1": true},
215
+			"labels":  map[string]bool{"today": true}},
206 216
 		}: "created",
207 217
 	}
208
-	for args, field := range matches {
209
-		if args.Match(field, source) != true {
210
-			t.Fatalf("Expected true for %v on %v, got false", source, args)
211
-		}
212
-	}
218
+
213 219
 	for args, field := range differs {
214 220
 		if args.Match(field, source) != false {
215 221
 			t.Fatalf("Expected false for %v on %v, got true", source, args)
216 222
 		}
217 223
 	}
218 224
 }
225
+
226
+func TestAdd(t *testing.T) {
227
+	f := NewArgs()
228
+	f.Add("status", "running")
229
+	v := f.fields["status"]
230
+	if len(v) != 1 || !v["running"] {
231
+		t.Fatalf("Expected to include a running status, got %v", v)
232
+	}
233
+
234
+	f.Add("status", "paused")
235
+	if len(v) != 2 || !v["paused"] {
236
+		t.Fatalf("Expected to include a paused status, got %v", v)
237
+	}
238
+}
239
+
240
+func TestDel(t *testing.T) {
241
+	f := NewArgs()
242
+	f.Add("status", "running")
243
+	f.Del("status", "running")
244
+	v := f.fields["status"]
245
+	if v["running"] {
246
+		t.Fatalf("Expected to not include a running status filter, got true")
247
+	}
248
+}
249
+
250
+func TestLen(t *testing.T) {
251
+	f := NewArgs()
252
+	if f.Len() != 0 {
253
+		t.Fatalf("Expected to not include any field")
254
+	}
255
+	f.Add("status", "running")
256
+	if f.Len() != 1 {
257
+		t.Fatalf("Expected to include one field")
258
+	}
259
+}
260
+
261
+func TestExactMatch(t *testing.T) {
262
+	f := NewArgs()
263
+
264
+	if !f.ExactMatch("status", "running") {
265
+		t.Fatalf("Expected to match `running` when there are no filters, got false")
266
+	}
267
+
268
+	f.Add("status", "running")
269
+	f.Add("status", "pause*")
270
+
271
+	if !f.ExactMatch("status", "running") {
272
+		t.Fatalf("Expected to match `running` with one of the filters, got false")
273
+	}
274
+
275
+	if f.ExactMatch("status", "paused") {
276
+		t.Fatalf("Expected to not match `paused` with one of the filters, got true")
277
+	}
278
+}
279
+
280
+func TestInclude(t *testing.T) {
281
+	f := NewArgs()
282
+	if f.Include("status") {
283
+		t.Fatalf("Expected to not include a status key, got true")
284
+	}
285
+	f.Add("status", "running")
286
+	if !f.Include("status") {
287
+		t.Fatalf("Expected to include a status key, got false")
288
+	}
289
+}
290
+
291
+func TestValidate(t *testing.T) {
292
+	f := NewArgs()
293
+	f.Add("status", "running")
294
+
295
+	valid := map[string]bool{
296
+		"status":   true,
297
+		"dangling": true,
298
+	}
299
+
300
+	if err := f.Validate(valid); err != nil {
301
+		t.Fatal(err)
302
+	}
303
+
304
+	f.Add("bogus", "running")
305
+	if err := f.Validate(valid); err == nil {
306
+		t.Fatalf("Expected to return an error, got nil")
307
+	}
308
+}
309
+
310
+func TestWalkValues(t *testing.T) {
311
+	f := NewArgs()
312
+	f.Add("status", "running")
313
+	f.Add("status", "paused")
314
+
315
+	f.WalkValues("status", func(value string) error {
316
+		if value != "running" && value != "paused" {
317
+			t.Fatalf("Unexpected value %s", value)
318
+		}
319
+		return nil
320
+	})
321
+
322
+	err := f.WalkValues("status", func(value string) error {
323
+		return fmt.Errorf("return")
324
+	})
325
+	if err == nil {
326
+		t.Fatalf("Expected to get an error, got nil")
327
+	}
328
+
329
+	err = f.WalkValues("foo", func(value string) error {
330
+		return fmt.Errorf("return")
331
+	})
332
+	if err != nil {
333
+		t.Fatalf("Expected to not iterate when the field doesn't exist, got %v", err)
334
+	}
335
+}