Add property support for pointers to bools and strings
The only append semantics for bool that result in a no-op when the zero
value is appended is to OR the two values together, but that is rarely
the desired semantics. Add support for *bool and *string as property
types, where appending a nil pointer is a no-op. For *bool, appending a
non-nil pointer replaces the destination with the value. For *string,
appending a non-nil pointer appends the value.
This also provides a more reliable replacement for
ModuleContext.ContainsProperty, as the build logic can tell that the
property was set, even if it was set by a mutator and not by the
blueprints file, by testing against nil.
[]string already provides these semantics for lists.
Setting a *bool or *string property from a blueprints file is the same
syntax as setting a bool or a string property.
diff --git a/proptools/clone.go b/proptools/clone.go
index d08a9db..7496584 100644
--- a/proptools/clone.go
+++ b/proptools/clone.go
@@ -91,27 +91,34 @@
}
fallthrough
case reflect.Ptr:
- if srcFieldValue.Type().Elem().Kind() != reflect.Struct {
- panic(fmt.Errorf("can't clone field %q: points to a non-struct",
- field.Name))
- }
-
if srcFieldValue.IsNil() {
dstFieldValue.Set(srcFieldValue)
break
}
- if !dstFieldValue.IsNil() {
- // Re-use the existing allocation.
- CopyProperties(dstFieldValue.Elem(), srcFieldValue.Elem())
- break
- } else {
- newValue := CloneProperties(srcFieldValue.Elem())
- if dstFieldInterfaceValue.IsValid() {
- dstFieldInterfaceValue.Set(newValue)
+ srcFieldValue := srcFieldValue.Elem()
+
+ switch srcFieldValue.Kind() {
+ case reflect.Struct:
+ if !dstFieldValue.IsNil() {
+ // Re-use the existing allocation.
+ CopyProperties(dstFieldValue.Elem(), srcFieldValue)
+ break
} else {
- dstFieldValue.Set(newValue)
+ newValue := CloneProperties(srcFieldValue)
+ if dstFieldInterfaceValue.IsValid() {
+ dstFieldInterfaceValue.Set(newValue)
+ } else {
+ dstFieldValue.Set(newValue)
+ }
}
+ case reflect.Bool, reflect.String:
+ newValue := reflect.New(srcFieldValue.Type())
+ newValue.Elem().Set(srcFieldValue)
+ dstFieldValue.Set(newValue)
+ default:
+ panic(fmt.Errorf("can't clone field %q: points to a %s",
+ field.Name, srcFieldValue.Kind()))
}
default:
panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
@@ -153,18 +160,19 @@
}
fallthrough
case reflect.Ptr:
- // We leave the pointer intact and zero out the struct that's
- // pointed to.
- if fieldValue.Type().Elem().Kind() != reflect.Struct {
- panic(fmt.Errorf("can't zero field %q: points to a non-struct",
- field.Name))
+ switch fieldValue.Type().Elem().Kind() {
+ case reflect.Struct:
+ if fieldValue.IsNil() {
+ break
+ }
+ ZeroProperties(fieldValue.Elem())
+ case reflect.Bool, reflect.String:
+ fieldValue.Set(reflect.Zero(fieldValue.Type()))
+ default:
+ panic(fmt.Errorf("can't zero field %q: points to a %s",
+ field.Name, fieldValue.Elem().Kind()))
}
- if fieldValue.IsNil() {
- break
- }
-
- ZeroProperties(fieldValue.Elem())
default:
panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
field.Name, fieldValue.Kind()))
@@ -217,21 +225,24 @@
dstFieldValue = newValue
fallthrough
case reflect.Ptr:
- if srcFieldValue.Type().Elem().Kind() != reflect.Struct {
- panic(fmt.Errorf("can't clone field %q: points to a non-struct",
- field.Name))
+ switch srcFieldValue.Type().Elem().Kind() {
+ case reflect.Struct:
+ if srcFieldValue.IsNil() {
+ break
+ }
+ newValue := CloneEmptyProperties(srcFieldValue.Elem())
+ if dstFieldInterfaceValue.IsValid() {
+ dstFieldInterfaceValue.Set(newValue)
+ } else {
+ dstFieldValue.Set(newValue)
+ }
+ case reflect.Bool, reflect.String:
+ // Nothing
+ default:
+ panic(fmt.Errorf("can't clone empty field %q: points to a %s",
+ field.Name, srcFieldValue.Elem().Kind()))
}
- if srcFieldValue.IsNil() {
- break
- }
-
- newValue := CloneEmptyProperties(srcFieldValue.Elem())
- if dstFieldInterfaceValue.IsValid() {
- dstFieldInterfaceValue.Set(newValue)
- } else {
- dstFieldValue.Set(newValue)
- }
default:
panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
field.Name, srcFieldValue.Kind()))
diff --git a/proptools/clone_test.go b/proptools/clone_test.go
index b082543..f7460cf 100644
--- a/proptools/clone_test.go
+++ b/proptools/clone_test.go
@@ -71,6 +71,25 @@
out: &struct{ S []string }{},
},
{
+ // Clone pointer to bool
+ in: &struct{ B1, B2 *bool }{
+ B1: BoolPtr(true),
+ B2: BoolPtr(false),
+ },
+ out: &struct{ B1, B2 *bool }{
+ B1: BoolPtr(true),
+ B2: BoolPtr(false),
+ },
+ },
+ {
+ // Clone pointer to string
+ in: &struct{ S *string }{
+ S: StringPtr("string1"),
+ },
+ out: &struct{ S *string }{
+ S: StringPtr("string1"),
+ },
+ },
{
// Clone struct
in: &struct{ S struct{ S string } }{
@@ -201,6 +220,20 @@
out: &struct{ S []string }{},
},
{
+ // Clone pointer to bool
+ in: &struct{ B1, B2 *bool }{
+ B1: BoolPtr(true),
+ B2: BoolPtr(false),
+ },
+ out: &struct{ B1, B2 *bool }{},
+ },
+ {
+ // Clone pointer to string
+ in: &struct{ S *string }{
+ S: StringPtr("string1"),
+ },
+ out: &struct{ S *string }{},
+ },
{
// Clone struct
in: &struct{ S struct{ S string } }{
diff --git a/proptools/extend.go b/proptools/extend.go
index 241643b..d356419 100644
--- a/proptools/extend.go
+++ b/proptools/extend.go
@@ -29,8 +29,9 @@
// An error returned by AppendProperties that applies to a specific property will be an
// *ExtendPropertyError, and can have the property name and error extracted from it.
//
-// The append operation is defined as appending string and slices of strings normally, OR-ing
-// bool values, and recursing into embedded structs, pointers to structs, and interfaces containing
+// The append operation is defined as appending strings, pointers to strings, and slices of
+// strings normally, OR-ing bool values, replacing non-nil pointers to booleans, and recursing into
+// embedded structs, pointers to structs, and interfaces containing
// pointers to structs. Appending the zero value of a property will always be a no-op.
func AppendProperties(dst interface{}, src interface{}, filter ExtendPropertyFilterFunc) error {
return extendProperties(dst, src, filter, false)
@@ -46,8 +47,9 @@
// An error returned by PrependProperties that applies to a specific property will be an
// *ExtendPropertyError, and can have the property name and error extracted from it.
//
-// The prepend operation is defined as prepending string and slices of strings normally, OR-ing
-// bool values, and recursing into embedded structs, pointers to structs, and interfaces containing
+// The prepend operation is defined as prepending strings, pointers to strings, and slices of
+// strings normally, OR-ing bool values, replacing non-nil pointers to booleans, and recursing into
+// embedded structs, pointers to structs, and interfaces containing
// pointers to structs. Prepending the zero value of a property will always be a no-op.
func PrependProperties(dst interface{}, src interface{}, filter ExtendPropertyFilterFunc) error {
return extendProperties(dst, src, filter, true)
@@ -65,8 +67,9 @@
// An error returned by AppendMatchingProperties that applies to a specific property will be an
// *ExtendPropertyError, and can have the property name and error extracted from it.
//
-// The append operation is defined as appending string and slices of strings normally, OR-ing
-// bool values, and recursing into embedded structs, pointers to structs, and interfaces containing
+// The append operation is defined as appending strings, pointers to strings, and slices of
+// strings normally, OR-ing bool values, replacing non-nil pointers to booleans, and recursing into
+// embedded structs, pointers to structs, and interfaces containing
// pointers to structs. Appending the zero value of a property will always be a no-op.
func AppendMatchingProperties(dst []interface{}, src interface{},
filter ExtendPropertyFilterFunc) error {
@@ -85,8 +88,9 @@
// An error returned by PrependProperties that applies to a specific property will be an
// *ExtendPropertyError, and can have the property name and error extracted from it.
//
-// The prepend operation is defined as prepending string and slices of strings normally, OR-ing
-// bool values, and recursing into embedded structs, pointers to structs, and interfaces containing
+// The prepend operation is defined as prepending strings, pointers to strings, and slices of
+// strings normally, OR-ing bool values, replacing non-nil pointers to booleans, and recursing into
+// embedded structs, pointers to structs, and interfaces containing
// pointers to structs. Prepending the zero value of a property will always be a no-op.
func PrependMatchingProperties(dst []interface{}, src interface{},
filter ExtendPropertyFilterFunc) error {
@@ -213,6 +217,18 @@
fallthrough
case reflect.Ptr:
+ ptrKind := srcFieldValue.Type().Elem().Kind()
+ if ptrKind == reflect.Bool || ptrKind == reflect.String {
+ if srcFieldValue.Type() != dstFieldValue.Type() {
+ return extendPropertyErrorf(propertyName, "mismatched pointer types %s and %s",
+ dstFieldValue.Type(), srcFieldValue.Type())
+ }
+ break
+ } else if ptrKind != reflect.Struct {
+ return extendPropertyErrorf(propertyName, "pointer is a %s", ptrKind)
+ }
+
+ // Pointer to a struct
if dstFieldValue.IsNil() != srcFieldValue.IsNil() {
return extendPropertyErrorf(propertyName, "nilitude mismatch")
}
@@ -223,10 +239,6 @@
dstFieldValue = dstFieldValue.Elem()
srcFieldValue = srcFieldValue.Elem()
- if srcFieldValue.Kind() != reflect.Struct || dstFieldValue.Kind() != reflect.Struct {
- return extendPropertyErrorf(propertyName, "pointer not to a struct")
- }
-
fallthrough
case reflect.Struct:
if sameTypes && dstFieldValue.Type() != srcFieldValue.Type() {
@@ -293,6 +305,34 @@
newSlice = reflect.AppendSlice(newSlice, srcFieldValue)
}
dstFieldValue.Set(newSlice)
+ case reflect.Ptr:
+ if srcFieldValue.IsNil() {
+ break
+ }
+
+ switch ptrKind := srcFieldValue.Type().Elem().Kind(); ptrKind {
+ case reflect.Bool:
+ if prepend {
+ if dstFieldValue.IsNil() {
+ dstFieldValue.Set(reflect.ValueOf(BoolPtr(srcFieldValue.Elem().Bool())))
+ }
+ } else {
+ // For append, replace the original value.
+ dstFieldValue.Set(reflect.ValueOf(BoolPtr(srcFieldValue.Elem().Bool())))
+ }
+ case reflect.String:
+ dstStr := ""
+ if !dstFieldValue.IsNil() {
+ dstStr = dstFieldValue.Elem().String()
+ }
+ if prepend {
+ dstFieldValue.Set(reflect.ValueOf(StringPtr(srcFieldValue.Elem().String() + dstStr)))
+ } else {
+ dstFieldValue.Set(reflect.ValueOf(StringPtr(dstStr + srcFieldValue.Elem().String())))
+ }
+ default:
+ panic(fmt.Errorf("unexpected pointer kind %s", ptrKind))
+ }
}
}
if !found {
diff --git a/proptools/extend_test.go b/proptools/extend_test.go
index a2482cc..35a1816 100644
--- a/proptools/extend_test.go
+++ b/proptools/extend_test.go
@@ -101,6 +101,114 @@
prepend: true,
},
{
+ // Append pointer to bool
+ in1: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+ B1: BoolPtr(true),
+ B2: BoolPtr(false),
+ B3: nil,
+ B4: BoolPtr(true),
+ B5: BoolPtr(false),
+ B6: nil,
+ B7: BoolPtr(true),
+ B8: BoolPtr(false),
+ B9: nil,
+ },
+ in2: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+ B1: nil,
+ B2: nil,
+ B3: nil,
+ B4: BoolPtr(true),
+ B5: BoolPtr(true),
+ B6: BoolPtr(true),
+ B7: BoolPtr(false),
+ B8: BoolPtr(false),
+ B9: BoolPtr(false),
+ },
+ out: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+ B1: BoolPtr(true),
+ B2: BoolPtr(false),
+ B3: nil,
+ B4: BoolPtr(true),
+ B5: BoolPtr(true),
+ B6: BoolPtr(true),
+ B7: BoolPtr(false),
+ B8: BoolPtr(false),
+ B9: BoolPtr(false),
+ },
+ },
+ {
+ // Prepend pointer to bool
+ in1: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+ B1: BoolPtr(true),
+ B2: BoolPtr(false),
+ B3: nil,
+ B4: BoolPtr(true),
+ B5: BoolPtr(false),
+ B6: nil,
+ B7: BoolPtr(true),
+ B8: BoolPtr(false),
+ B9: nil,
+ },
+ in2: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+ B1: nil,
+ B2: nil,
+ B3: nil,
+ B4: BoolPtr(true),
+ B5: BoolPtr(true),
+ B6: BoolPtr(true),
+ B7: BoolPtr(false),
+ B8: BoolPtr(false),
+ B9: BoolPtr(false),
+ },
+ out: &struct{ B1, B2, B3, B4, B5, B6, B7, B8, B9 *bool }{
+ B1: BoolPtr(true),
+ B2: BoolPtr(false),
+ B3: nil,
+ B4: BoolPtr(true),
+ B5: BoolPtr(false),
+ B6: BoolPtr(true),
+ B7: BoolPtr(true),
+ B8: BoolPtr(false),
+ B9: BoolPtr(false),
+ },
+ prepend: true,
+ },
+ {
+ // Append pointer to strings
+ in1: &struct{ S1, S2, S3, S4 *string }{
+ S1: StringPtr("string1"),
+ S2: StringPtr("string2"),
+ },
+ in2: &struct{ S1, S2, S3, S4 *string }{
+ S1: StringPtr("string3"),
+ S3: StringPtr("string4"),
+ },
+ out: &struct{ S1, S2, S3, S4 *string }{
+ S1: StringPtr("string1string3"),
+ S2: StringPtr("string2"),
+ S3: StringPtr("string4"),
+ S4: nil,
+ },
+ },
+ {
+ // Prepend pointer to strings
+ in1: &struct{ S1, S2, S3, S4 *string }{
+ S1: StringPtr("string1"),
+ S2: StringPtr("string2"),
+ },
+ in2: &struct{ S1, S2, S3, S4 *string }{
+ S1: StringPtr("string3"),
+ S3: StringPtr("string4"),
+ },
+ out: &struct{ S1, S2, S3, S4 *string }{
+ S1: StringPtr("string3string1"),
+ S2: StringPtr("string2"),
+ S3: StringPtr("string4"),
+ S4: nil,
+ },
+ prepend: true,
+ },
+ {
// Append slice
in1: &struct{ S []string }{
S: []string{"string1"},
@@ -439,7 +547,7 @@
out: &struct{ S *[]string }{
S: &[]string{"string1"},
},
- err: extendPropertyErrorf("s", "pointer not to a struct"),
+ err: extendPropertyErrorf("s", "pointer is a slice"),
},
{
// Error in nested struct
diff --git a/proptools/proptools.go b/proptools/proptools.go
index 79c4f6d..690d384 100644
--- a/proptools/proptools.go
+++ b/proptools/proptools.go
@@ -49,3 +49,31 @@
return false
}
+
+// BoolPtr returns a pointer to a new bool containing the given value.
+func BoolPtr(b bool) *bool {
+ return &b
+}
+
+// StringPtr returns a pointer to a new string containing the given value.
+func StringPtr(s string) *string {
+ return &s
+}
+
+// Bool takes a pointer to a bool and returns true iff the pointer is non-nil and points to a true
+// value.
+func Bool(b *bool) bool {
+ if b != nil {
+ return *b
+ }
+ return false
+}
+
+// String takes a pointer to a string and returns the value of the string if the pointer is non-nil,
+// or an empty string.
+func String(s *string) string {
+ if s != nil {
+ return *s
+ }
+ return ""
+}
diff --git a/unpack.go b/unpack.go
index 445a8c6..64c65e4 100644
--- a/unpack.go
+++ b/unpack.go
@@ -161,15 +161,18 @@
}
fallthrough
case reflect.Ptr:
- if fieldValue.IsNil() {
- panic(fmt.Errorf("field %s contains a nil pointer",
- field.Name))
- }
- fieldValue = fieldValue.Elem()
- elemType := fieldValue.Type()
- if elemType.Kind() != reflect.Struct {
- panic(fmt.Errorf("field %s contains a non-struct pointer",
- field.Name))
+ switch ptrKind := fieldValue.Type().Elem().Kind(); ptrKind {
+ case reflect.Struct:
+ if fieldValue.IsNil() {
+ panic(fmt.Errorf("field %s contains a nil pointer",
+ field.Name))
+ }
+ fieldValue = fieldValue.Elem()
+ case reflect.Bool, reflect.String:
+ // Nothing
+ default:
+ panic(fmt.Errorf("field %s contains a pointer to %s",
+ field.Name, ptrKind))
}
case reflect.Int, reflect.Uint:
@@ -225,9 +228,19 @@
newErrs = unpackString(fieldValue, packedProperty.property)
case reflect.Slice:
newErrs = unpackSlice(fieldValue, packedProperty.property)
- case reflect.Ptr, reflect.Interface:
- fieldValue = fieldValue.Elem()
- fallthrough
+ case reflect.Ptr:
+ switch ptrKind := fieldValue.Type().Elem().Kind(); ptrKind {
+ case reflect.Bool:
+ newValue := reflect.New(fieldValue.Type().Elem())
+ newErrs = unpackBool(newValue.Elem(), packedProperty.property)
+ fieldValue.Set(newValue)
+ case reflect.String:
+ newValue := reflect.New(fieldValue.Type().Elem())
+ newErrs = unpackString(newValue.Elem(), packedProperty.property)
+ fieldValue.Set(newValue)
+ default:
+ panic(fmt.Errorf("unexpected pointer kind %s", ptrKind))
+ }
case reflect.Struct:
localFilterKey, localFilterValue := filterKey, filterValue
if k, v, err := HasFilter(field.Tag); err != nil {
@@ -248,6 +261,8 @@
}
newErrs = unpackStruct(propertyName+".", fieldValue,
packedProperty.property, propertyMap, localFilterKey, localFilterValue)
+ default:
+ panic(fmt.Errorf("unexpected kind %s", kind))
}
errs = append(errs, newErrs...)
if len(errs) >= maxErrors {
diff --git a/unpack_test.go b/unpack_test.go
index 0b31efc..77a57ec 100644
--- a/unpack_test.go
+++ b/unpack_test.go
@@ -33,6 +33,24 @@
{`
m {
name: "abc",
+ blank: "",
+ }
+ `,
+ struct {
+ Name *string
+ Blank *string
+ Unset *string
+ }{
+ Name: proptools.StringPtr("abc"),
+ Blank: proptools.StringPtr(""),
+ Unset: nil,
+ },
+ nil,
+ },
+
+ {`
+ m {
+ name: "abc",
}
`,
struct {
@@ -58,6 +76,24 @@
{`
m {
+ isGood: true,
+ isBad: false,
+ }
+ `,
+ struct {
+ IsGood *bool
+ IsBad *bool
+ IsUgly *bool
+ }{
+ IsGood: proptools.BoolPtr(true),
+ IsBad: proptools.BoolPtr(false),
+ IsUgly: nil,
+ },
+ nil,
+ },
+
+ {`
+ m {
stuff: ["asdf", "jkl;", "qwert",
"uiop", "bnm,"],
empty: []