Add support for unpacking properties into nested structs.
Change-Id: Idb67dcb9cc7f857258b9b3409db68199d42a8001
This commit is contained in:
parent
b9cbdae701
commit
8762292240
5 changed files with 161 additions and 71 deletions
|
@ -1,6 +1,7 @@
|
|||
bootstrap_go_package {
|
||||
name: "blueprint",
|
||||
deps: ["blueprint-parser"],
|
||||
deps: ["blueprint-parser",
|
||||
"blueprint-proptools"],
|
||||
pkgPath: "blueprint",
|
||||
srcs: ["blueprint/context.go",
|
||||
"blueprint/live_tracker.go",
|
||||
|
|
|
@ -531,17 +531,20 @@ func (c *Context) processModuleDef(moduleDef *parser.Module,
|
|||
relBlueprintsFile: relBlueprintsFile,
|
||||
}
|
||||
|
||||
properties = append(properties, &info.properties)
|
||||
props := []interface{}{
|
||||
&info.properties,
|
||||
}
|
||||
properties = append(props, properties...)
|
||||
|
||||
errs := unpackProperties(moduleDef.Properties, properties...)
|
||||
propertyMap, errs := unpackProperties(moduleDef.Properties, properties...)
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
info.pos = moduleDef.Pos
|
||||
info.propertyPos = make(map[string]scanner.Position)
|
||||
for _, propertyDef := range moduleDef.Properties {
|
||||
info.propertyPos[propertyDef.Name] = propertyDef.Pos
|
||||
for name, propertyDef := range propertyMap {
|
||||
info.propertyPos[name] = propertyDef.Pos
|
||||
}
|
||||
|
||||
name := info.properties.Name
|
||||
|
|
|
@ -2,10 +2,9 @@ package blueprint
|
|||
|
||||
import (
|
||||
"blueprint/parser"
|
||||
"blueprint/proptools"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type packedProperty struct {
|
||||
|
@ -14,12 +13,65 @@ type packedProperty struct {
|
|||
}
|
||||
|
||||
func unpackProperties(propertyDefs []*parser.Property,
|
||||
propertiesStructs ...interface{}) (errs []error) {
|
||||
propertiesStructs ...interface{}) (map[string]*parser.Property, []error) {
|
||||
|
||||
propertyMap := make(map[string]*packedProperty)
|
||||
errs := buildPropertyMap("", propertyDefs, propertyMap)
|
||||
if len(errs) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
for _, properties := range propertiesStructs {
|
||||
propertiesValue := reflect.ValueOf(properties)
|
||||
if propertiesValue.Kind() != reflect.Ptr {
|
||||
panic("properties must be a pointer to a struct")
|
||||
}
|
||||
|
||||
propertiesValue = propertiesValue.Elem()
|
||||
if propertiesValue.Kind() != reflect.Struct {
|
||||
panic("properties must be a pointer to a struct")
|
||||
}
|
||||
|
||||
newErrs := unpackStructValue("", propertiesValue, propertyMap)
|
||||
errs = append(errs, newErrs...)
|
||||
|
||||
if len(errs) >= maxErrors {
|
||||
return nil, errs
|
||||
}
|
||||
}
|
||||
|
||||
// Report any properties that didn't have corresponding struct fields as
|
||||
// errors.
|
||||
result := make(map[string]*parser.Property)
|
||||
for name, packedProperty := range propertyMap {
|
||||
result[name] = packedProperty.property
|
||||
if !packedProperty.unpacked {
|
||||
err := &Error{
|
||||
Err: fmt.Errorf("unrecognized property %q", name),
|
||||
Pos: packedProperty.property.Pos,
|
||||
}
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func buildPropertyMap(namePrefix string, propertyDefs []*parser.Property,
|
||||
propertyMap map[string]*packedProperty) (errs []error) {
|
||||
|
||||
for _, propertyDef := range propertyDefs {
|
||||
name := propertyDef.Name
|
||||
name := namePrefix + propertyDef.Name
|
||||
if first, present := propertyMap[name]; present {
|
||||
if first.property == propertyDef {
|
||||
// We've already added this property.
|
||||
continue
|
||||
}
|
||||
|
||||
errs = append(errs, &Error{
|
||||
Err: fmt.Errorf("property %q already defined", name),
|
||||
Pos: propertyDef.Pos,
|
||||
|
@ -38,43 +90,19 @@ func unpackProperties(propertyDefs []*parser.Property,
|
|||
property: propertyDef,
|
||||
unpacked: false,
|
||||
}
|
||||
|
||||
// We intentionally do not rescursively add MapValue properties to the
|
||||
// property map here. Instead we add them when we encounter a struct
|
||||
// into which they can be unpacked. We do this so that if we never
|
||||
// encounter such a struct then the "unrecognized property" error will
|
||||
// be reported only once for the map property and not for each of its
|
||||
// sub-properties.
|
||||
}
|
||||
|
||||
for _, properties := range propertiesStructs {
|
||||
propertiesValue := reflect.ValueOf(properties)
|
||||
if propertiesValue.Kind() != reflect.Ptr {
|
||||
panic("properties must be a pointer to a struct")
|
||||
}
|
||||
|
||||
propertiesValue = propertiesValue.Elem()
|
||||
if propertiesValue.Kind() != reflect.Struct {
|
||||
panic("properties must be a pointer to a struct")
|
||||
}
|
||||
|
||||
newErrs := unpackStruct(propertiesValue, propertyMap)
|
||||
errs = append(errs, newErrs...)
|
||||
|
||||
if len(errs) >= maxErrors {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
|
||||
// Report any properties that didn't have corresponding struct fields as
|
||||
// errors.
|
||||
for name, packedProperty := range propertyMap {
|
||||
if !packedProperty.unpacked {
|
||||
err := &Error{
|
||||
Err: fmt.Errorf("unrecognized property %q", name),
|
||||
Pos: packedProperty.property.Pos,
|
||||
}
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
return
|
||||
}
|
||||
|
||||
func unpackStruct(structValue reflect.Value,
|
||||
func unpackStructValue(namePrefix string, structValue reflect.Value,
|
||||
propertyMap map[string]*packedProperty) []error {
|
||||
|
||||
structType := structValue.Type()
|
||||
|
@ -96,27 +124,43 @@ func unpackStruct(structValue reflect.Value,
|
|||
// To make testing easier we validate the struct field's type regardless
|
||||
// of whether or not the property was specified in the parsed string.
|
||||
switch kind := fieldValue.Kind(); kind {
|
||||
case reflect.Bool, reflect.String:
|
||||
case reflect.Bool, reflect.String, reflect.Struct:
|
||||
// Do nothing
|
||||
case reflect.Slice:
|
||||
elemType := field.Type.Elem()
|
||||
if elemType.Kind() != reflect.String {
|
||||
panic(fmt.Errorf("field %s is a non-string slice", field.Name))
|
||||
}
|
||||
case reflect.Struct:
|
||||
newErrs := unpackStruct(fieldValue, propertyMap)
|
||||
errs = append(errs, newErrs...)
|
||||
if len(errs) >= maxErrors {
|
||||
return errs
|
||||
case reflect.Interface:
|
||||
if fieldValue.IsNil() {
|
||||
panic(fmt.Errorf("field %s contains a nil interface",
|
||||
field.Name))
|
||||
}
|
||||
fieldValue = fieldValue.Elem()
|
||||
elemType := fieldValue.Type()
|
||||
if elemType.Kind() != reflect.Ptr {
|
||||
panic(fmt.Errorf("field %s contains a non-pointer interface",
|
||||
field.Name))
|
||||
}
|
||||
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))
|
||||
}
|
||||
continue // This field doesn't correspond to a specific property.
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported kind for field %s: %s",
|
||||
field.Name, kind))
|
||||
}
|
||||
|
||||
// Get the property value if it was specified.
|
||||
propertyName := propertyNameForField(field)
|
||||
propertyName := namePrefix + proptools.PropertyNameForField(field.Name)
|
||||
packedProperty, ok := propertyMap[propertyName]
|
||||
if !ok {
|
||||
// This property wasn't specified.
|
||||
|
@ -133,6 +177,13 @@ func unpackStruct(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)
|
||||
}
|
||||
errs = append(errs, newErrs...)
|
||||
if len(errs) >= maxErrors {
|
||||
|
@ -191,11 +242,22 @@ func unpackSlice(sliceValue reflect.Value, property *parser.Property) []error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func propertyNameForField(field reflect.StructField) string {
|
||||
r, size := utf8.DecodeRuneInString(field.Name)
|
||||
propertyName := string(unicode.ToLower(r))
|
||||
if len(field.Name) > size {
|
||||
propertyName += field.Name[size:]
|
||||
func unpackStruct(namePrefix string, structValue reflect.Value,
|
||||
property *parser.Property,
|
||||
propertyMap map[string]*packedProperty) []error {
|
||||
|
||||
if property.Value.Type != parser.Map {
|
||||
return []error{
|
||||
fmt.Errorf("%s: can't assign %s value to %s property %q",
|
||||
property.Value.Pos, property.Value.Type, parser.Map,
|
||||
property.Name),
|
||||
}
|
||||
}
|
||||
return propertyName
|
||||
|
||||
errs := buildPropertyMap(namePrefix, property.Value.MapValue, propertyMap)
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return unpackStructValue(namePrefix, structValue, propertyMap)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package blueprint
|
|||
|
||||
import (
|
||||
"blueprint/parser"
|
||||
"blueprint/proptools"
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
@ -50,7 +51,9 @@ var validUnpackTestCases = []struct {
|
|||
|
||||
{`
|
||||
m {
|
||||
name: "abc",
|
||||
nested: {
|
||||
name: "abc",
|
||||
}
|
||||
}
|
||||
`,
|
||||
struct {
|
||||
|
@ -66,7 +69,25 @@ var validUnpackTestCases = []struct {
|
|||
|
||||
{`
|
||||
m {
|
||||
foo: "abc",
|
||||
nested: {
|
||||
name: "def",
|
||||
}
|
||||
}
|
||||
`,
|
||||
struct {
|
||||
Nested interface{}
|
||||
}{
|
||||
Nested: &struct{ Name string }{
|
||||
Name: "def",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{`
|
||||
m {
|
||||
nested: {
|
||||
foo: "abc",
|
||||
},
|
||||
bar: false,
|
||||
baz: ["def", "ghi"],
|
||||
}
|
||||
|
@ -101,9 +122,9 @@ func TestUnpackProperties(t *testing.T) {
|
|||
}
|
||||
|
||||
module := defs[0].(*parser.Module)
|
||||
propertiesType := reflect.TypeOf(testCase.output)
|
||||
properties := reflect.New(propertiesType)
|
||||
errs = unpackProperties(module.Properties, properties.Interface())
|
||||
properties := proptools.CloneProperties(reflect.ValueOf(testCase.output))
|
||||
proptools.ZeroProperties(properties.Elem())
|
||||
_, errs = unpackProperties(module.Properties, properties.Interface())
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("test case: %s", testCase.input)
|
||||
t.Errorf("unexpected unpack errors:")
|
||||
|
|
|
@ -72,9 +72,10 @@ build .bootstrap/blueprint/obj/_go_.${g.bootstrap.GoChar}: g.bootstrap.gc $
|
|||
${g.bootstrap.SrcDir}/blueprint/scope.go $
|
||||
${g.bootstrap.SrcDir}/blueprint/singleton_ctx.go $
|
||||
${g.bootstrap.SrcDir}/blueprint/unpack.go | $
|
||||
.bootstrap/blueprint-parser/pkg/blueprint/parser.a
|
||||
.bootstrap/blueprint-parser/pkg/blueprint/parser.a $
|
||||
.bootstrap/blueprint-proptools/pkg/blueprint/proptools.a
|
||||
pkgPath = blueprint
|
||||
incFlags = -I .bootstrap/blueprint-parser/pkg
|
||||
incFlags = -I .bootstrap/blueprint-parser/pkg -I .bootstrap/blueprint-proptools/pkg
|
||||
|
||||
build .bootstrap/blueprint/pkg/blueprint.a: g.bootstrap.pack $
|
||||
.bootstrap/blueprint/obj/_go_.${g.bootstrap.GoChar}
|
||||
|
@ -84,7 +85,7 @@ build .bootstrap/blueprint/pkg/blueprint.a: g.bootstrap.pack $
|
|||
# Module: blueprint-bootstrap
|
||||
# Type: bootstrap_go_package
|
||||
# Factory: blueprint/bootstrap.newGoPackageModule
|
||||
# Defined: Blueprints:36:1
|
||||
# Defined: Blueprints:37:1
|
||||
|
||||
build .bootstrap/blueprint-bootstrap/obj/_go_.${g.bootstrap.GoChar}: $
|
||||
g.bootstrap.gc ${g.bootstrap.SrcDir}/blueprint/bootstrap/bootstrap.go $
|
||||
|
@ -92,10 +93,11 @@ build .bootstrap/blueprint-bootstrap/obj/_go_.${g.bootstrap.GoChar}: $
|
|||
${g.bootstrap.SrcDir}/blueprint/bootstrap/config.go $
|
||||
${g.bootstrap.SrcDir}/blueprint/bootstrap/doc.go | $
|
||||
.bootstrap/blueprint-parser/pkg/blueprint/parser.a $
|
||||
.bootstrap/blueprint-proptools/pkg/blueprint/proptools.a $
|
||||
.bootstrap/blueprint/pkg/blueprint.a $
|
||||
.bootstrap/blueprint-pathtools/pkg/blueprint/pathtools.a
|
||||
pkgPath = blueprint/bootstrap
|
||||
incFlags = -I .bootstrap/blueprint-parser/pkg -I .bootstrap/blueprint/pkg -I .bootstrap/blueprint-pathtools/pkg
|
||||
incFlags = -I .bootstrap/blueprint-parser/pkg -I .bootstrap/blueprint-proptools/pkg -I .bootstrap/blueprint/pkg -I .bootstrap/blueprint-pathtools/pkg
|
||||
|
||||
build .bootstrap/blueprint-bootstrap/pkg/blueprint/bootstrap.a: $
|
||||
g.bootstrap.pack $
|
||||
|
@ -106,7 +108,7 @@ build .bootstrap/blueprint-bootstrap/pkg/blueprint/bootstrap.a: $
|
|||
# Module: blueprint-parser
|
||||
# Type: bootstrap_go_package
|
||||
# Factory: blueprint/bootstrap.newGoPackageModule
|
||||
# Defined: Blueprints:18:1
|
||||
# Defined: Blueprints:19:1
|
||||
|
||||
build .bootstrap/blueprint-parser/obj/_go_.${g.bootstrap.GoChar}: $
|
||||
g.bootstrap.gc ${g.bootstrap.SrcDir}/blueprint/parser/parser.go
|
||||
|
@ -120,7 +122,7 @@ build .bootstrap/blueprint-parser/pkg/blueprint/parser.a: g.bootstrap.pack $
|
|||
# Module: blueprint-pathtools
|
||||
# Type: bootstrap_go_package
|
||||
# Factory: blueprint/bootstrap.newGoPackageModule
|
||||
# Defined: Blueprints:24:1
|
||||
# Defined: Blueprints:25:1
|
||||
|
||||
build .bootstrap/blueprint-pathtools/obj/_go_.${g.bootstrap.GoChar}: $
|
||||
g.bootstrap.gc ${g.bootstrap.SrcDir}/blueprint/pathtools/lists.go
|
||||
|
@ -135,7 +137,7 @@ build .bootstrap/blueprint-pathtools/pkg/blueprint/pathtools.a: $
|
|||
# Module: blueprint-proptools
|
||||
# Type: bootstrap_go_package
|
||||
# Factory: blueprint/bootstrap.newGoPackageModule
|
||||
# Defined: Blueprints:30:1
|
||||
# Defined: Blueprints:31:1
|
||||
|
||||
build .bootstrap/blueprint-proptools/obj/_go_.${g.bootstrap.GoChar}: $
|
||||
g.bootstrap.gc ${g.bootstrap.SrcDir}/blueprint/proptools/proptools.go
|
||||
|
@ -150,16 +152,17 @@ build .bootstrap/blueprint-proptools/pkg/blueprint/proptools.a: $
|
|||
# Module: minibp
|
||||
# Type: bootstrap_go_binary
|
||||
# Factory: blueprint/bootstrap.newGoBinaryModule
|
||||
# Defined: Blueprints:47:1
|
||||
# Defined: Blueprints:48:1
|
||||
|
||||
build .bootstrap/minibp/obj/_go_.${g.bootstrap.GoChar}: g.bootstrap.gc $
|
||||
${g.bootstrap.SrcDir}/blueprint/bootstrap/minibp/main.go | $
|
||||
.bootstrap/blueprint-parser/pkg/blueprint/parser.a $
|
||||
.bootstrap/blueprint-proptools/pkg/blueprint/proptools.a $
|
||||
.bootstrap/blueprint/pkg/blueprint.a $
|
||||
.bootstrap/blueprint-pathtools/pkg/blueprint/pathtools.a $
|
||||
.bootstrap/blueprint-bootstrap/pkg/blueprint/bootstrap.a
|
||||
pkgPath = minibp
|
||||
incFlags = -I .bootstrap/blueprint-parser/pkg -I .bootstrap/blueprint/pkg -I .bootstrap/blueprint-pathtools/pkg -I .bootstrap/blueprint-bootstrap/pkg
|
||||
incFlags = -I .bootstrap/blueprint-parser/pkg -I .bootstrap/blueprint-proptools/pkg -I .bootstrap/blueprint/pkg -I .bootstrap/blueprint-pathtools/pkg -I .bootstrap/blueprint-bootstrap/pkg
|
||||
|
||||
build .bootstrap/minibp/obj/minibp.a: g.bootstrap.pack $
|
||||
.bootstrap/minibp/obj/_go_.${g.bootstrap.GoChar}
|
||||
|
@ -167,7 +170,7 @@ build .bootstrap/minibp/obj/minibp.a: g.bootstrap.pack $
|
|||
|
||||
build .bootstrap/minibp/obj/a.out: g.bootstrap.link $
|
||||
.bootstrap/minibp/obj/minibp.a
|
||||
libDirFlags = -L .bootstrap/blueprint-parser/pkg -L .bootstrap/blueprint/pkg -L .bootstrap/blueprint-pathtools/pkg -L .bootstrap/blueprint-bootstrap/pkg
|
||||
libDirFlags = -L .bootstrap/blueprint-parser/pkg -L .bootstrap/blueprint-proptools/pkg -L .bootstrap/blueprint/pkg -L .bootstrap/blueprint-pathtools/pkg -L .bootstrap/blueprint-bootstrap/pkg
|
||||
|
||||
build .bootstrap/bin/minibp: g.bootstrap.cp .bootstrap/minibp/obj/a.out
|
||||
|
||||
|
|
Loading…
Reference in a new issue