1// Copyright 2014 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package proptools 16 17import ( 18 "fmt" 19 "reflect" 20 "sort" 21 "strconv" 22 "strings" 23 "text/scanner" 24 25 "github.com/google/blueprint/parser" 26) 27 28const maxUnpackErrors = 10 29 30type UnpackError struct { 31 Err error 32 Pos scanner.Position 33} 34 35func (e *UnpackError) Error() string { 36 return fmt.Sprintf("%s: %s", e.Pos, e.Err) 37} 38 39// packedProperty helps to track properties usage (`used` will be true) 40type packedProperty struct { 41 property *parser.Property 42 used bool 43} 44 45// unpackContext keeps compound names and their values in a map. It is initialized from 46// parsed properties. 47type unpackContext struct { 48 propertyMap map[string]*packedProperty 49 errs []error 50} 51 52// UnpackProperties populates the list of runtime values ("property structs") from the parsed properties. 53// If a property a.b.c has a value, a field with the matching name in each runtime value is initialized 54// from it. See PropertyNameForField for field and property name matching. 55// For instance, if the input contains 56// { foo: "abc", bar: {x: 1},} 57// and a runtime value being has been declared as 58// var v struct { Foo string; Bar int } 59// then v.Foo will be set to "abc" and v.Bar will be set to 1 60// (cf. unpack_test.go for further examples) 61// 62// The type of a receiving field has to match the property type, i.e., a bool/int/string field 63// can be set from a property with bool/int/string value, a struct can be set from a map (only the 64// matching fields are set), and an slice can be set from a list. 65// If a field of a runtime value has been already set prior to the UnpackProperties, the new value 66// is appended to it (see somewhat inappropriately named ExtendBasicType). 67// The same property can initialize fields in multiple runtime values. It is an error if any property 68// value was not used to initialize at least one field. 69func UnpackProperties(properties []*parser.Property, objects ...interface{}) (map[string]*parser.Property, []error) { 70 var unpackContext unpackContext 71 unpackContext.propertyMap = make(map[string]*packedProperty) 72 if !unpackContext.buildPropertyMap("", properties) { 73 return nil, unpackContext.errs 74 } 75 76 for _, obj := range objects { 77 valueObject := reflect.ValueOf(obj) 78 if !isStructPtr(valueObject.Type()) { 79 panic(fmt.Errorf("properties must be *struct, got %s", 80 valueObject.Type())) 81 } 82 unpackContext.unpackToStruct("", valueObject.Elem()) 83 if len(unpackContext.errs) >= maxUnpackErrors { 84 return nil, unpackContext.errs 85 } 86 } 87 88 // Gather property map, and collect any unused properties. 89 // Avoid reporting subproperties of unused properties. 90 result := make(map[string]*parser.Property) 91 var unusedNames []string 92 for name, v := range unpackContext.propertyMap { 93 if v.used { 94 result[name] = v.property 95 } else { 96 unusedNames = append(unusedNames, name) 97 } 98 } 99 if len(unusedNames) == 0 && len(unpackContext.errs) == 0 { 100 return result, nil 101 } 102 return nil, unpackContext.reportUnusedNames(unusedNames) 103} 104 105func (ctx *unpackContext) reportUnusedNames(unusedNames []string) []error { 106 sort.Strings(unusedNames) 107 var lastReported string 108 for _, name := range unusedNames { 109 // if 'foo' has been reported, ignore 'foo\..*' and 'foo\[.*' 110 if lastReported != "" { 111 trimmed := strings.TrimPrefix(name, lastReported) 112 if trimmed != name && (trimmed[0] == '.' || trimmed[0] == '[') { 113 continue 114 } 115 } 116 ctx.errs = append(ctx.errs, &UnpackError{ 117 fmt.Errorf("unrecognized property %q", name), 118 ctx.propertyMap[name].property.ColonPos}) 119 lastReported = name 120 } 121 return ctx.errs 122} 123 124func (ctx *unpackContext) buildPropertyMap(prefix string, properties []*parser.Property) bool { 125 nOldErrors := len(ctx.errs) 126 for _, property := range properties { 127 name := fieldPath(prefix, property.Name) 128 if first, present := ctx.propertyMap[name]; present { 129 ctx.addError( 130 &UnpackError{fmt.Errorf("property %q already defined", name), property.ColonPos}) 131 if ctx.addError( 132 &UnpackError{fmt.Errorf("<-- previous definition here"), first.property.ColonPos}) { 133 return false 134 } 135 continue 136 } 137 138 ctx.propertyMap[name] = &packedProperty{property, false} 139 switch propValue := property.Value.Eval().(type) { 140 case *parser.Map: 141 ctx.buildPropertyMap(name, propValue.Properties) 142 case *parser.List: 143 // If it is a list, unroll it unless its elements are of primitive type 144 // (no further mapping will be needed in that case, so we avoid cluttering 145 // the map). 146 if len(propValue.Values) == 0 { 147 continue 148 } 149 if t := propValue.Values[0].Type(); t == parser.StringType || t == parser.Int64Type || t == parser.BoolType { 150 continue 151 } 152 153 itemProperties := make([]*parser.Property, len(propValue.Values), len(propValue.Values)) 154 for i, expr := range propValue.Values { 155 itemProperties[i] = &parser.Property{ 156 Name: property.Name + "[" + strconv.Itoa(i) + "]", 157 NamePos: property.NamePos, 158 ColonPos: property.ColonPos, 159 Value: expr, 160 } 161 } 162 if !ctx.buildPropertyMap(prefix, itemProperties) { 163 return false 164 } 165 } 166 } 167 168 return len(ctx.errs) == nOldErrors 169} 170 171func fieldPath(prefix, fieldName string) string { 172 if prefix == "" { 173 return fieldName 174 } 175 return prefix + "." + fieldName 176} 177 178func (ctx *unpackContext) addError(e error) bool { 179 ctx.errs = append(ctx.errs, e) 180 return len(ctx.errs) < maxUnpackErrors 181} 182 183func (ctx *unpackContext) unpackToStruct(namePrefix string, structValue reflect.Value) { 184 structType := structValue.Type() 185 186 for i := 0; i < structValue.NumField(); i++ { 187 fieldValue := structValue.Field(i) 188 field := structType.Field(i) 189 190 // In Go 1.7, runtime-created structs are unexported, so it's not 191 // possible to create an exported anonymous field with a generated 192 // type. So workaround this by special-casing "BlueprintEmbed" to 193 // behave like an anonymous field for structure unpacking. 194 if field.Name == "BlueprintEmbed" { 195 field.Name = "" 196 field.Anonymous = true 197 } 198 199 if field.PkgPath != "" { 200 // This is an unexported field, so just skip it. 201 continue 202 } 203 204 propertyName := fieldPath(namePrefix, PropertyNameForField(field.Name)) 205 206 if !fieldValue.CanSet() { 207 panic(fmt.Errorf("field %s is not settable", propertyName)) 208 } 209 210 // Get the property value if it was specified. 211 packedProperty, propertyIsSet := ctx.propertyMap[propertyName] 212 213 origFieldValue := fieldValue 214 215 // To make testing easier we validate the struct field's type regardless 216 // of whether or not the property was specified in the parsed string. 217 // TODO(ccross): we don't validate types inside nil struct pointers 218 // Move type validation to a function that runs on each factory once 219 switch kind := fieldValue.Kind(); kind { 220 case reflect.Bool, reflect.String, reflect.Struct, reflect.Slice: 221 // Do nothing 222 case reflect.Interface: 223 if fieldValue.IsNil() { 224 panic(fmt.Errorf("field %s contains a nil interface", propertyName)) 225 } 226 fieldValue = fieldValue.Elem() 227 elemType := fieldValue.Type() 228 if elemType.Kind() != reflect.Ptr { 229 panic(fmt.Errorf("field %s contains a non-pointer interface", propertyName)) 230 } 231 fallthrough 232 case reflect.Ptr: 233 switch ptrKind := fieldValue.Type().Elem().Kind(); ptrKind { 234 case reflect.Struct: 235 if fieldValue.IsNil() && (propertyIsSet || field.Anonymous) { 236 // Instantiate nil struct pointers 237 // Set into origFieldValue in case it was an interface, in which case 238 // fieldValue points to the unsettable pointer inside the interface 239 fieldValue = reflect.New(fieldValue.Type().Elem()) 240 origFieldValue.Set(fieldValue) 241 } 242 fieldValue = fieldValue.Elem() 243 case reflect.Bool, reflect.Int64, reflect.String: 244 // Nothing 245 default: 246 panic(fmt.Errorf("field %s contains a pointer to %s", propertyName, ptrKind)) 247 } 248 249 case reflect.Int, reflect.Uint: 250 if !HasTag(field, "blueprint", "mutated") { 251 panic(fmt.Errorf(`int field %s must be tagged blueprint:"mutated"`, propertyName)) 252 } 253 254 default: 255 panic(fmt.Errorf("unsupported kind for field %s: %s", propertyName, kind)) 256 } 257 258 if field.Anonymous && isStruct(fieldValue.Type()) { 259 ctx.unpackToStruct(namePrefix, fieldValue) 260 continue 261 } 262 263 if !propertyIsSet { 264 // This property wasn't specified. 265 continue 266 } 267 268 packedProperty.used = true 269 property := packedProperty.property 270 271 if HasTag(field, "blueprint", "mutated") { 272 if !ctx.addError( 273 &UnpackError{ 274 fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName), 275 property.ColonPos, 276 }) { 277 return 278 } 279 continue 280 } 281 282 if isStruct(fieldValue.Type()) { 283 if property.Value.Eval().Type() != parser.MapType { 284 ctx.addError(&UnpackError{ 285 fmt.Errorf("can't assign %s value to map property %q", 286 property.Value.Type(), property.Name), 287 property.Value.Pos(), 288 }) 289 continue 290 } 291 ctx.unpackToStruct(propertyName, fieldValue) 292 if len(ctx.errs) >= maxUnpackErrors { 293 return 294 } 295 } else if isSlice(fieldValue.Type()) { 296 if unpackedValue, ok := ctx.unpackToSlice(propertyName, property, fieldValue.Type()); ok { 297 ExtendBasicType(fieldValue, unpackedValue, Append) 298 } 299 if len(ctx.errs) >= maxUnpackErrors { 300 return 301 } 302 303 } else { 304 unpackedValue, err := propertyToValue(fieldValue.Type(), property) 305 if err != nil && !ctx.addError(err) { 306 return 307 } 308 ExtendBasicType(fieldValue, unpackedValue, Append) 309 } 310 } 311} 312 313// unpackSlice creates a value of a given slice type from the property which should be a list 314func (ctx *unpackContext) unpackToSlice( 315 sliceName string, property *parser.Property, sliceType reflect.Type) (reflect.Value, bool) { 316 propValueAsList, ok := property.Value.Eval().(*parser.List) 317 if !ok { 318 ctx.addError(&UnpackError{ 319 fmt.Errorf("can't assign %s value to list property %q", 320 property.Value.Type(), property.Name), 321 property.Value.Pos(), 322 }) 323 return reflect.MakeSlice(sliceType, 0, 0), false 324 } 325 exprs := propValueAsList.Values 326 value := reflect.MakeSlice(sliceType, 0, len(exprs)) 327 if len(exprs) == 0 { 328 return value, true 329 } 330 331 // The function to construct an item value depends on the type of list elements. 332 var getItemFunc func(*parser.Property, reflect.Type) (reflect.Value, bool) 333 switch exprs[0].Type() { 334 case parser.BoolType, parser.StringType, parser.Int64Type: 335 getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) { 336 value, err := propertyToValue(t, property) 337 if err != nil { 338 ctx.addError(err) 339 return value, false 340 } 341 return value, true 342 } 343 case parser.ListType: 344 getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) { 345 return ctx.unpackToSlice(property.Name, property, t) 346 } 347 case parser.MapType: 348 getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) { 349 itemValue := reflect.New(t).Elem() 350 ctx.unpackToStruct(property.Name, itemValue) 351 return itemValue, true 352 } 353 case parser.NotEvaluatedType: 354 getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) { 355 return reflect.New(t), false 356 } 357 default: 358 panic(fmt.Errorf("bizarre property expression type: %v", exprs[0].Type())) 359 } 360 361 itemProperty := &parser.Property{NamePos: property.NamePos, ColonPos: property.ColonPos} 362 elemType := sliceType.Elem() 363 isPtr := elemType.Kind() == reflect.Ptr 364 365 for i, expr := range exprs { 366 itemProperty.Name = sliceName + "[" + strconv.Itoa(i) + "]" 367 itemProperty.Value = expr 368 if packedProperty, ok := ctx.propertyMap[itemProperty.Name]; ok { 369 packedProperty.used = true 370 } 371 if isPtr { 372 if itemValue, ok := getItemFunc(itemProperty, elemType.Elem()); ok { 373 ptrValue := reflect.New(itemValue.Type()) 374 ptrValue.Elem().Set(itemValue) 375 value = reflect.Append(value, ptrValue) 376 } 377 } else { 378 if itemValue, ok := getItemFunc(itemProperty, elemType); ok { 379 value = reflect.Append(value, itemValue) 380 } 381 } 382 } 383 return value, true 384} 385 386// propertyToValue creates a value of a given value type from the property. 387func propertyToValue(typ reflect.Type, property *parser.Property) (reflect.Value, error) { 388 var value reflect.Value 389 var baseType reflect.Type 390 isPtr := typ.Kind() == reflect.Ptr 391 if isPtr { 392 baseType = typ.Elem() 393 } else { 394 baseType = typ 395 } 396 397 switch kind := baseType.Kind(); kind { 398 case reflect.Bool: 399 b, ok := property.Value.Eval().(*parser.Bool) 400 if !ok { 401 return value, &UnpackError{ 402 fmt.Errorf("can't assign %s value to bool property %q", 403 property.Value.Type(), property.Name), 404 property.Value.Pos(), 405 } 406 } 407 value = reflect.ValueOf(b.Value) 408 409 case reflect.Int64: 410 b, ok := property.Value.Eval().(*parser.Int64) 411 if !ok { 412 return value, &UnpackError{ 413 fmt.Errorf("can't assign %s value to int64 property %q", 414 property.Value.Type(), property.Name), 415 property.Value.Pos(), 416 } 417 } 418 value = reflect.ValueOf(b.Value) 419 420 case reflect.String: 421 s, ok := property.Value.Eval().(*parser.String) 422 if !ok { 423 return value, &UnpackError{ 424 fmt.Errorf("can't assign %s value to string property %q", 425 property.Value.Type(), property.Name), 426 property.Value.Pos(), 427 } 428 } 429 value = reflect.ValueOf(s.Value) 430 431 default: 432 return value, &UnpackError{ 433 fmt.Errorf("cannot assign %s value %s to %s property %s", property.Value.Type(), property.Value, kind, typ), 434 property.NamePos} 435 } 436 437 if isPtr { 438 ptrValue := reflect.New(value.Type()) 439 ptrValue.Elem().Set(value) 440 return ptrValue, nil 441 } 442 return value, nil 443} 444