From 4adc8194905e72bcc240ef5bf5bbb2175c329c16 Mon Sep 17 00:00:00 2001 From: Colin Cross Date: Mon, 22 Jun 2015 13:38:45 -0700 Subject: [PATCH] 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 --- unpack.go | 92 +++++++++++++++++++++++++++++++++++++++++--------- unpack_test.go | 78 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 151 insertions(+), 19 deletions(-) diff --git a/unpack.go b/unpack.go index 37bc158..3e9fe00 100644 --- a/unpack.go +++ b/unpack.go @@ -16,10 +16,12 @@ package blueprint import ( "fmt" + "reflect" + "strconv" + "strings" + "github.com/google/blueprint/parser" "github.com/google/blueprint/proptools" - "reflect" - "strings" ) type packedProperty struct { @@ -47,7 +49,7 @@ func unpackProperties(propertyDefs []*parser.Property, 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 buildPropertyMap(namePrefix string, propertyDefs []*parser.Property, } 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 @@ func unpackStructValue(namePrefix string, structValue reflect.Value, 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 @@ func unpackStructValue(namePrefix string, structValue reflect.Value, 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 @@ func unpackStructValue(namePrefix string, structValue reflect.Value, 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 unpackSlice(sliceValue reflect.Value, property *parser.Property) []error { } 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 @@ func unpackStruct(namePrefix string, structValue reflect.Value, 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 @@ func hasTag(field reflect.StructField, name, value string) bool { 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 @@ package blueprint 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 @@ var validUnpackTestCases = []struct { }{ Name: "abc", }, + nil, }, {` @@ -48,6 +53,7 @@ var validUnpackTestCases = []struct { }{ IsGood: true, }, + nil, }, {` @@ -61,6 +67,7 @@ var validUnpackTestCases = []struct { }{ Stuff: []string{"asdf", "jkl;", "qwert", "uiop", "bnm,"}, }, + nil, }, {` @@ -79,6 +86,7 @@ var validUnpackTestCases = []struct { Name: "abc", }, }, + nil, }, {` @@ -95,6 +103,7 @@ var validUnpackTestCases = []struct { Name: "def", }, }, + nil, }, {` @@ -119,6 +128,64 @@ var validUnpackTestCases = []struct { 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 @@ func TestUnpackProperties(t *testing.T) { 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()