package taskenv import ( "errors" "fmt" "strings" "github.com/zclconf/go-cty/cty" ) var ( // ErrInvalidObjectPath is returned when a key cannot be converted into // a nested object path like "foo...bar", ".foo", or "foo." ErrInvalidObjectPath = errors.New("invalid object path") ) // addNestedKey expands keys into their nested form: // // k="foo.bar", v="quux" -> {"foo": {"bar": "quux"}} // // Existing keys are overwritten. Map values take precedence over primitives. // // If the key has dots but cannot be converted to a valid nested data structure // (eg "foo...bar", "foo.", or non-object value exists for key), an error is // returned. func addNestedKey(dst map[string]interface{}, k, v string) error { // createdParent and Key capture the parent object of the first created // object and the first created object's key respectively. The cleanup // func deletes them to prevent side-effects when returning errors. var createdParent map[string]interface{} var createdKey string cleanup := func() { if createdParent != nil { delete(createdParent, createdKey) } } segments := strings.Split(k, ".") for _, newKey := range segments[:len(segments)-1] { if newKey == "" { // String either begins with a dot (.foo) or has at // least two consecutive dots (foo..bar); either way // it's an invalid object path. cleanup() return ErrInvalidObjectPath } var target map[string]interface{} if existingI, ok := dst[newKey]; ok { if existing, ok := existingI.(map[string]interface{}); ok { // Target already exists target = existing } else { // Existing value is not a map. Maps should // take precedence over primitive values (eg // overwrite attr.driver.qemu = "1" with // attr.driver.qemu.version = "...") target = make(map[string]interface{}) dst[newKey] = target } } else { // Does not exist, create target = make(map[string]interface{}) dst[newKey] = target // If this is the first created key, capture it for // cleanup if there is an error later. if createdParent == nil { createdParent = dst createdKey = newKey } } // Descend into new m dst = target } // See if the final segment is a valid key newKey := segments[len(segments)-1] if newKey == "" { // String ends in a dot cleanup() return ErrInvalidObjectPath } if existingI, ok := dst[newKey]; ok { if _, ok := existingI.(map[string]interface{}); ok { // Existing value is a map which takes precedence over // a primitive value. Drop primitive. return nil } } dst[newKey] = v return nil } // ctyify converts nested map[string]interfaces to a map[string]cty.Value. An // error is returned if an unsupported type is encountered. // // Currently only strings, cty.Values, and nested maps are supported. func ctyify(src map[string]interface{}) (map[string]cty.Value, error) { dst := make(map[string]cty.Value, len(src)) for k, vI := range src { switch v := vI.(type) { case string: dst[k] = cty.StringVal(v) case cty.Value: dst[k] = v case map[string]interface{}: o, err := ctyify(v) if err != nil { return nil, err } dst[k] = cty.ObjectVal(o) default: return nil, fmt.Errorf("key %q has invalid type %T", k, v) } } return dst, nil }