Browse Source

api/types/filters: Add GetBoolOrDefault

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
Paweł Gronowski 2 years ago
parent
commit
0d68591c8e
3 changed files with 187 additions and 11 deletions
  1. 37 0
      api/types/filters/errors.go
  2. 34 11
      api/types/filters/parse.go
  3. 116 0
      api/types/filters/parse_test.go

+ 37 - 0
api/types/filters/errors.go

@@ -0,0 +1,37 @@
+package filters
+
+import "fmt"
+
+// invalidFilter indicates that the provided filter or its value is invalid
+type invalidFilter struct {
+	Filter string
+	Value  []string
+}
+
+func (e invalidFilter) Error() string {
+	msg := "invalid filter"
+	if e.Filter != "" {
+		msg += " '" + e.Filter
+		if e.Value != nil {
+			msg = fmt.Sprintf("%s=%s", msg, e.Value)
+		}
+		msg += "'"
+	}
+	return msg
+}
+
+// InvalidParameter marks this error as ErrInvalidParameter
+func (e invalidFilter) InvalidParameter() {}
+
+// unreachableCode is an error indicating that the code path was not expected to be reached.
+type unreachableCode struct {
+	Filter string
+	Value  []string
+}
+
+// System marks this error as ErrSystem
+func (e unreachableCode) System() {}
+
+func (e unreachableCode) Error() string {
+	return fmt.Sprintf("unreachable code reached for filter: %q with values: %s", e.Filter, e.Value)
+}

+ 34 - 11
api/types/filters/parse.go

@@ -10,7 +10,6 @@ import (
 	"strings"
 	"strings"
 
 
 	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/api/types/versions"
-	"github.com/pkg/errors"
 )
 )
 
 
 // Args stores a mapping of keys to a set of multiple values.
 // Args stores a mapping of keys to a set of multiple values.
@@ -99,7 +98,7 @@ func FromJSON(p string) (Args, error) {
 	// Fallback to parsing arguments in the legacy slice format
 	// Fallback to parsing arguments in the legacy slice format
 	deprecated := map[string][]string{}
 	deprecated := map[string][]string{}
 	if legacyErr := json.Unmarshal(raw, &deprecated); legacyErr != nil {
 	if legacyErr := json.Unmarshal(raw, &deprecated); legacyErr != nil {
-		return args, invalidFilter{errors.Wrap(err, "invalid filter")}
+		return args, invalidFilter{}
 	}
 	}
 
 
 	args.fields = deprecatedArgs(deprecated)
 	args.fields = deprecatedArgs(deprecated)
@@ -196,6 +195,38 @@ func (args Args) Match(field, source string) bool {
 	return false
 	return false
 }
 }
 
 
+// GetBoolOrDefault returns a boolean value of the key if the key is present
+// and is intepretable as a boolean value. Otherwise the default value is returned.
+// Error is not nil only if the filter values are not valid boolean or are conflicting.
+func (args Args) GetBoolOrDefault(key string, defaultValue bool) (bool, error) {
+	fieldValues, ok := args.fields[key]
+
+	if !ok {
+		return defaultValue, nil
+	}
+
+	if len(fieldValues) == 0 {
+		return defaultValue, invalidFilter{key, nil}
+	}
+
+	isFalse := fieldValues["0"] || fieldValues["false"]
+	isTrue := fieldValues["1"] || fieldValues["true"]
+
+	conflicting := isFalse && isTrue
+	invalid := !isFalse && !isTrue
+
+	if conflicting || invalid {
+		return defaultValue, invalidFilter{key, args.Get(key)}
+	} else if isFalse {
+		return false, nil
+	} else if isTrue {
+		return true, nil
+	}
+
+	// This code shouldn't be reached.
+	return defaultValue, unreachableCode{Filter: key, Value: args.Get(key)}
+}
+
 // ExactMatch returns true if the source matches exactly one of the values.
 // ExactMatch returns true if the source matches exactly one of the values.
 func (args Args) ExactMatch(key, source string) bool {
 func (args Args) ExactMatch(key, source string) bool {
 	fieldValues, ok := args.fields[key]
 	fieldValues, ok := args.fields[key]
@@ -246,20 +277,12 @@ func (args Args) Contains(field string) bool {
 	return ok
 	return ok
 }
 }
 
 
-type invalidFilter struct{ error }
-
-func (e invalidFilter) Error() string {
-	return e.error.Error()
-}
-
-func (invalidFilter) InvalidParameter() {}
-
 // Validate compared the set of accepted keys against the keys in the mapping.
 // Validate compared the set of accepted keys against the keys in the mapping.
 // An error is returned if any mapping keys are not in the accepted set.
 // An error is returned if any mapping keys are not in the accepted set.
 func (args Args) Validate(accepted map[string]bool) error {
 func (args Args) Validate(accepted map[string]bool) error {
 	for name := range args.fields {
 	for name := range args.fields {
 		if !accepted[name] {
 		if !accepted[name] {
-			return invalidFilter{errors.New("invalid filter '" + name + "'")}
+			return invalidFilter{name, nil}
 		}
 		}
 	}
 	}
 	return nil
 	return nil

+ 116 - 0
api/types/filters/parse_test.go

@@ -3,6 +3,7 @@ package filters // import "github.com/docker/docker/api/types/filters"
 import (
 import (
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
+	"sort"
 	"testing"
 	"testing"
 
 
 	"gotest.tools/v3/assert"
 	"gotest.tools/v3/assert"
@@ -418,3 +419,118 @@ func TestClone(t *testing.T) {
 	f2.Add("baz", "qux")
 	f2.Add("baz", "qux")
 	assert.Check(t, is.Len(f.Get("baz"), 0))
 	assert.Check(t, is.Len(f.Get("baz"), 0))
 }
 }
+
+func TestGetBoolOrDefault(t *testing.T) {
+	for _, tC := range []struct {
+		name          string
+		args          map[string][]string
+		defValue      bool
+		expectedErr   error
+		expectedValue bool
+	}{
+		{
+			name: "single true",
+			args: map[string][]string{
+				"dangling": {"true"},
+			},
+			defValue:      false,
+			expectedErr:   nil,
+			expectedValue: true,
+		},
+		{
+			name: "single false",
+			args: map[string][]string{
+				"dangling": {"false"},
+			},
+			defValue:      true,
+			expectedErr:   nil,
+			expectedValue: false,
+		},
+		{
+			name: "single bad value",
+			args: map[string][]string{
+				"dangling": {"potato"},
+			},
+			defValue:      true,
+			expectedErr:   invalidFilter{Filter: "dangling", Value: []string{"potato"}},
+			expectedValue: true,
+		},
+		{
+			name: "two bad values",
+			args: map[string][]string{
+				"dangling": {"banana", "potato"},
+			},
+			defValue:      true,
+			expectedErr:   invalidFilter{Filter: "dangling", Value: []string{"banana", "potato"}},
+			expectedValue: true,
+		},
+		{
+			name: "two conflicting values",
+			args: map[string][]string{
+				"dangling": {"false", "true"},
+			},
+			defValue:      false,
+			expectedErr:   invalidFilter{Filter: "dangling", Value: []string{"false", "true"}},
+			expectedValue: false,
+		},
+		{
+			name: "multiple conflicting values",
+			args: map[string][]string{
+				"dangling": {"false", "true", "1"},
+			},
+			defValue:      true,
+			expectedErr:   invalidFilter{Filter: "dangling", Value: []string{"false", "true", "1"}},
+			expectedValue: true,
+		},
+		{
+			name: "1 means true",
+			args: map[string][]string{
+				"dangling": {"1"},
+			},
+			defValue:      false,
+			expectedErr:   nil,
+			expectedValue: true,
+		},
+		{
+			name: "0 means false",
+			args: map[string][]string{
+				"dangling": {"0"},
+			},
+			defValue:      true,
+			expectedErr:   nil,
+			expectedValue: false,
+		},
+	} {
+		tC := tC
+		t.Run(tC.name, func(t *testing.T) {
+			a := NewArgs()
+
+			for key, values := range tC.args {
+				for _, value := range values {
+					a.Add(key, value)
+				}
+			}
+
+			value, err := a.GetBoolOrDefault("dangling", tC.defValue)
+
+			if tC.expectedErr == nil {
+				assert.Check(t, is.Nil(err))
+			} else {
+				assert.Check(t, is.ErrorType(err, tC.expectedErr))
+
+				// Check if error is the same.
+				expected := tC.expectedErr.(invalidFilter)
+				actual := err.(invalidFilter)
+
+				assert.Check(t, is.Equal(expected.Filter, actual.Filter))
+
+				sort.Strings(expected.Value)
+				sort.Strings(actual.Value)
+				assert.Check(t, is.DeepEqual(expected.Value, actual.Value))
+			}
+
+			assert.Check(t, is.Equal(tC.expectedValue, value))
+		})
+	}
+
+}