Support filtering nested structures by tag
Allow tagging a nested property structure with
`blueprint:"filter(key:\"value\")"`, which will only allow property
assignments to properites in the nested structure that are tagged
with `key:"value"`. Moving the filter into Blueprint instead of
the project build logic allows more reliable and consistent error
messages.
Change-Id: I06bc673dde647776fc5552673bdc0cdcd7216462
diff --git a/unpack.go b/unpack.go
index 37bc158..3e9fe00 100644
--- a/unpack.go
+++ b/unpack.go
@@ -16,10 +16,12 @@
import (
"fmt"
+ "reflect"
+ "strconv"
+ "strings"
+
"github.com/google/blueprint/parser"
"github.com/google/blueprint/proptools"
- "reflect"
- "strings"
)
type packedProperty struct {
@@ -47,7 +49,7 @@
panic("properties must be a pointer to a struct")
}
- newErrs := unpackStructValue("", propertiesValue, propertyMap)
+ newErrs := unpackStructValue("", propertiesValue, propertyMap, "", "")
errs = append(errs, newErrs...)
if len(errs) >= maxErrors {
@@ -118,7 +120,7 @@
}
func unpackStructValue(namePrefix string, structValue reflect.Value,
- propertyMap map[string]*packedProperty) []error {
+ propertyMap map[string]*packedProperty, filterKey, filterValue string) []error {
structType := structValue.Type()
@@ -169,6 +171,7 @@
panic(fmt.Errorf("field %s contains a non-struct pointer",
field.Name))
}
+
case reflect.Int, reflect.Uint:
if !hasTag(field, "blueprint", "mutated") {
panic(fmt.Errorf(`int field %s must be tagged blueprint:"mutated"`, field.Name))
@@ -187,18 +190,33 @@
continue
}
- var newErrs []error
+ packedProperty.unpacked = true
if hasTag(field, "blueprint", "mutated") {
errs = append(errs,
- fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName))
+ &Error{
+ Err: fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName),
+ Pos: packedProperty.property.Pos,
+ })
if len(errs) >= maxErrors {
return errs
}
continue
}
- packedProperty.unpacked = true
+ if filterKey != "" && !hasTag(field, filterKey, filterValue) {
+ errs = append(errs,
+ &Error{
+ Err: fmt.Errorf("filtered field %s cannot be set in a Blueprint file", propertyName),
+ Pos: packedProperty.property.Pos,
+ })
+ if len(errs) >= maxErrors {
+ return errs
+ }
+ continue
+ }
+
+ var newErrs []error
switch kind := fieldValue.Kind(); kind {
case reflect.Bool:
@@ -207,13 +225,29 @@
newErrs = unpackString(fieldValue, packedProperty.property)
case reflect.Slice:
newErrs = unpackSlice(fieldValue, packedProperty.property)
- case reflect.Struct:
- newErrs = unpackStruct(propertyName+".", fieldValue,
- packedProperty.property, propertyMap)
case reflect.Ptr, reflect.Interface:
- structValue := fieldValue.Elem()
- newErrs = unpackStruct(propertyName+".", structValue,
- packedProperty.property, propertyMap)
+ fieldValue = fieldValue.Elem()
+ fallthrough
+ case reflect.Struct:
+ localFilterKey, localFilterValue := filterKey, filterValue
+ if k, v, err := hasFilter(field); err != nil {
+ errs = append(errs, err)
+ if len(errs) >= maxErrors {
+ return errs
+ }
+ } else if k != "" {
+ if filterKey != "" {
+ errs = append(errs, fmt.Errorf("nested filter tag not supported on field %q",
+ field.Name))
+ if len(errs) >= maxErrors {
+ return errs
+ }
+ } else {
+ localFilterKey, localFilterValue = k, v
+ }
+ }
+ newErrs = unpackStruct(propertyName+".", fieldValue,
+ packedProperty.property, propertyMap, localFilterKey, localFilterValue)
}
errs = append(errs, newErrs...)
if len(errs) >= maxErrors {
@@ -273,8 +307,8 @@
}
func unpackStruct(namePrefix string, structValue reflect.Value,
- property *parser.Property,
- propertyMap map[string]*packedProperty) []error {
+ property *parser.Property, propertyMap map[string]*packedProperty,
+ filterKey, filterValue string) []error {
if property.Value.Type != parser.Map {
return []error{
@@ -289,7 +323,7 @@
return errs
}
- return unpackStructValue(namePrefix, structValue, propertyMap)
+ return unpackStructValue(namePrefix, structValue, propertyMap, filterKey, filterValue)
}
func hasTag(field reflect.StructField, name, value string) bool {
@@ -302,3 +336,29 @@
return false
}
+
+func hasFilter(field reflect.StructField) (k, v string, err error) {
+ tag := field.Tag.Get("blueprint")
+ for _, entry := range strings.Split(tag, ",") {
+ if strings.HasPrefix(entry, "filter") {
+ if !strings.HasPrefix(entry, "filter(") || !strings.HasSuffix(entry, ")") {
+ return "", "", fmt.Errorf("unexpected format for filter %q: missing ()", entry)
+ }
+ entry = strings.TrimPrefix(entry, "filter(")
+ entry = strings.TrimSuffix(entry, ")")
+
+ s := strings.Split(entry, ":")
+ if len(s) != 2 {
+ return "", "", fmt.Errorf("unexpected format for filter %q: expected single ':'", entry)
+ }
+ k = s[0]
+ v, err = strconv.Unquote(s[1])
+ if err != nil {
+ return "", "", fmt.Errorf("unexpected format for filter %q: %s", entry, err.Error())
+ }
+ return k, v, nil
+ }
+ }
+
+ return "", "", nil
+}
diff --git a/unpack_test.go b/unpack_test.go
index bd4dd8f..393631c 100644
--- a/unpack_test.go
+++ b/unpack_test.go
@@ -16,15 +16,19 @@
import (
"bytes"
- "github.com/google/blueprint/parser"
- "github.com/google/blueprint/proptools"
+ "fmt"
"reflect"
"testing"
+ "text/scanner"
+
+ "github.com/google/blueprint/parser"
+ "github.com/google/blueprint/proptools"
)
var validUnpackTestCases = []struct {
input string
output interface{}
+ errs []error
}{
{`
m {
@@ -36,6 +40,7 @@
}{
Name: "abc",
},
+ nil,
},
{`
@@ -48,6 +53,7 @@
}{
IsGood: true,
},
+ nil,
},
{`
@@ -61,6 +67,7 @@
}{
Stuff: []string{"asdf", "jkl;", "qwert", "uiop", "bnm,"},
},
+ nil,
},
{`
@@ -79,6 +86,7 @@
Name: "abc",
},
},
+ nil,
},
{`
@@ -95,6 +103,7 @@
Name: "def",
},
},
+ nil,
},
{`
@@ -119,6 +128,64 @@
Bar: false,
Baz: []string{"def", "ghi"},
},
+ nil,
+ },
+
+ {`
+ m {
+ nested: {
+ foo: "abc",
+ },
+ bar: false,
+ baz: ["def", "ghi"],
+ }
+ `,
+ struct {
+ Nested struct {
+ Foo string `allowNested:"true"`
+ } `blueprint:"filter(allowNested:\"true\")"`
+ Bar bool
+ Baz []string
+ }{
+ Nested: struct {
+ Foo string `allowNested:"true"`
+ }{
+ Foo: "abc",
+ },
+ Bar: false,
+ Baz: []string{"def", "ghi"},
+ },
+ nil,
+ },
+
+ {`
+ m {
+ nested: {
+ foo: "abc",
+ },
+ bar: false,
+ baz: ["def", "ghi"],
+ }
+ `,
+ struct {
+ Nested struct {
+ Foo string
+ } `blueprint:"filter(allowNested:\"true\")"`
+ Bar bool
+ Baz []string
+ }{
+ Nested: struct{ Foo string }{
+ Foo: "",
+ },
+ Bar: false,
+ Baz: []string{"def", "ghi"},
+ },
+ []error{
+ &Error{
+ Err: fmt.Errorf("filtered field nested.foo cannot be set in a Blueprint file"),
+ Pos: scanner.Position{"", 27, 4, 8},
+ },
+ },
},
}
@@ -139,13 +206,18 @@
properties := proptools.CloneProperties(reflect.ValueOf(testCase.output))
proptools.ZeroProperties(properties.Elem())
_, errs = unpackProperties(module.Properties, properties.Interface())
- if len(errs) != 0 {
+ if len(errs) != 0 && len(testCase.errs) == 0 {
t.Errorf("test case: %s", testCase.input)
t.Errorf("unexpected unpack errors:")
for _, err := range errs {
t.Errorf(" %s", err)
}
t.FailNow()
+ } else if !reflect.DeepEqual(errs, testCase.errs) {
+ t.Errorf("test case: %s", testCase.input)
+ t.Errorf("incorrect errors:")
+ t.Errorf(" expected: %+v", testCase.errs)
+ t.Errorf(" got: %+v", errs)
}
output := properties.Elem().Interface()