package tftypes import ( "errors" "fmt" "strconv" "strings" ) var ( // ErrNotAttributePathStepper is returned when a type that doesn't full // the AttributePathStepper interface is passed to WalkAttributePath. ErrNotAttributePathStepper = errors.New("doesn't fill tftypes.AttributePathStepper interface") // ErrInvalidStep is returned when an AttributePath has the wrong kind // of AttributePathStep for the type that WalkAttributePath is // operating on. ErrInvalidStep = errors.New("step cannot be applied to this value") ) // AttributePath is a type that can point to a specific value within an // aggregate Terraform value. It consists of steps, each identifying one // element or attribute of the current value, and making that the current // value. This allows referring to arbitrarily precise values. type AttributePath struct { // Steps are the steps that must be followed from the root of the value // to obtain the value being indicated. steps []AttributePathStep } // NewAttributePath returns an empty AttributePath, ready to have steps added // to it using WithElementKeyString, WithElementKeyInt, WithElementKeyValue, or // WithAttributeName. func NewAttributePath() *AttributePath { return &AttributePath{} } // NewAttributePathWithSteps returns an AttributePath populated with the passed // AttributePathSteps. func NewAttributePathWithSteps(steps []AttributePathStep) *AttributePath { return &AttributePath{ steps: steps, } } // Steps returns the AttributePathSteps that make up an AttributePath. func (a *AttributePath) Steps() []AttributePathStep { if a == nil { return nil } steps := make([]AttributePathStep, len(a.steps)) copy(steps, a.steps) return steps } func (a *AttributePath) String() string { var res strings.Builder for pos, step := range a.Steps() { if pos != 0 { res.WriteString(".") } switch v := step.(type) { case AttributeName: res.WriteString(`AttributeName("` + string(v) + `")`) case ElementKeyString: res.WriteString(`ElementKeyString("` + string(v) + `")`) case ElementKeyInt: res.WriteString(`ElementKeyInt(` + strconv.FormatInt(int64(v), 10) + `)`) case ElementKeyValue: res.WriteString(`ElementKeyValue(` + Value(v).String() + `)`) } } return res.String() } // Equal returns true if two AttributePaths should be considered equal. // AttributePaths are considered equal if they have the same number of steps, // the steps are all the same types, and the steps have all the same values. func (a *AttributePath) Equal(o *AttributePath) bool { if len(a.Steps()) == 0 && len(o.Steps()) == 0 { return true } if len(a.Steps()) != len(o.Steps()) { return false } for pos, aStep := range a.Steps() { oStep := o.Steps()[pos] if !aStep.Equal(oStep) { return false } } return true } // NewErrorf returns an error associated with the value indicated by `a`. This // is equivalent to calling a.NewError(fmt.Errorf(f, args...)). func (a *AttributePath) NewErrorf(f string, args ...interface{}) error { return a.NewError(fmt.Errorf(f, args...)) } // NewError returns an error that associates `err` with the value indicated by // `a`. func (a *AttributePath) NewError(err error) error { var wrapped AttributePathError if errors.As(err, &wrapped) { // TODO: at some point we'll probably want to handle the // AttributePathError-within-AttributePathError situation, // either by de-duplicating the paths we're surfacing, or // privileging one, or something. For now, let's just do the // naive thing and not add our own path. return err } return AttributePathError{ Path: a, err: err, } } // LastStep returns the last step in the path. If the path was // empty, nil is returned. func (a *AttributePath) LastStep() AttributePathStep { steps := a.Steps() if len(steps) == 0 { return nil } return steps[len(steps)-1] } // WithAttributeName adds an AttributeName step to `a`, using `name` as the // attribute's name. `a` is copied, not modified. func (a *AttributePath) WithAttributeName(name string) *AttributePath { steps := a.Steps() return &AttributePath{ steps: append(steps, AttributeName(name)), } } // WithElementKeyString adds an ElementKeyString step to `a`, using `key` as // the element's key. `a` is copied, not modified. func (a *AttributePath) WithElementKeyString(key string) *AttributePath { steps := a.Steps() return &AttributePath{ steps: append(steps, ElementKeyString(key)), } } // WithElementKeyInt adds an ElementKeyInt step to `a`, using `key` as the // element's key. `a` is copied, not modified. func (a *AttributePath) WithElementKeyInt(key int) *AttributePath { steps := a.Steps() return &AttributePath{ steps: append(steps, ElementKeyInt(key)), } } // WithElementKeyValue adds an ElementKeyValue to `a`, using `key` as the // element's key. `a` is copied, not modified. func (a *AttributePath) WithElementKeyValue(key Value) *AttributePath { steps := a.Steps() return &AttributePath{ steps: append(steps, ElementKeyValue(key.Copy())), } } // WithoutLastStep removes the last step, whatever kind of step it was, from // `a`. `a` is copied, not modified. func (a *AttributePath) WithoutLastStep() *AttributePath { steps := a.Steps() if len(steps) == 0 { return nil } return &AttributePath{ steps: steps[:len(steps)-1], } } // AttributePathStep is an intentionally unimplementable interface that // functions as an enum, allowing us to use different strongly-typed step types // as a generic "step" type. // // An AttributePathStep is meant to indicate a single step in an AttributePath, // indicating a specific attribute or element that is the next value in the // path. type AttributePathStep interface { // Equal returns true if the AttributePathStep is equal to the other. Equal(AttributePathStep) bool unfulfillable() // make this interface fillable only by this package } var ( _ AttributePathStep = AttributeName("") _ AttributePathStep = ElementKeyString("") _ AttributePathStep = ElementKeyInt(0) ) // AttributeName is an AttributePathStep implementation that indicates the next // step in the AttributePath is to select an attribute. The value of the // AttributeName is the name of the attribute to be selected. type AttributeName string // Equal returns true if the other AttributePathStep is an AttributeName and // has the same value. func (a AttributeName) Equal(other AttributePathStep) bool { otherA, ok := other.(AttributeName) if !ok { return false } return string(a) == string(otherA) } func (a AttributeName) unfulfillable() {} // ElementKeyString is an AttributePathStep implementation that indicates the // next step in the AttributePath is to select an element using a string key. // The value of the ElementKeyString is the key of the element to select. type ElementKeyString string // Equal returns true if the other AttributePathStep is an ElementKeyString and // has the same value. func (e ElementKeyString) Equal(other AttributePathStep) bool { otherE, ok := other.(ElementKeyString) if !ok { return false } return string(e) == string(otherE) } func (e ElementKeyString) unfulfillable() {} // ElementKeyInt is an AttributePathStep implementation that indicates the next // step in the AttributePath is to select an element using an int64 key. The // value of the ElementKeyInt is the key of the element to select. type ElementKeyInt int64 // Equal returns true if the other AttributePathStep is an ElementKeyInt and // has the same value. func (e ElementKeyInt) Equal(other AttributePathStep) bool { otherE, ok := other.(ElementKeyInt) if !ok { return false } return int(e) == int(otherE) } func (e ElementKeyInt) unfulfillable() {} // ElementKeyValue is an AttributePathStep implementation that indicates the // next step in the AttributePath is to select an element using the element // itself as a key. The value of the ElementKeyValue is the key of the element // to select. type ElementKeyValue Value // Equal returns true if the other AttributePathStep is an ElementKeyValue and // has the same value. func (e ElementKeyValue) Equal(other AttributePathStep) bool { otherE, ok := other.(ElementKeyValue) if !ok { return false } return Value(e).Equal(Value(otherE)) } func (e ElementKeyValue) unfulfillable() {} // AttributePathStepper is an interface that types can implement to make them // traversable by WalkAttributePath, allowing providers to retrieve the // specific value an AttributePath is pointing to. type AttributePathStepper interface { // Return the attribute or element the AttributePathStep is referring // to, or an error if the AttributePathStep is referring to an // attribute or element that doesn't exist. ApplyTerraform5AttributePathStep(AttributePathStep) (interface{}, error) } // WalkAttributePath will return the Type or Value that `path` is pointing to, // using `in` as the root. If an error is returned, the AttributePath returned // will indicate the steps that remained to be applied when the error was // encountered. // // map[string]interface{} and []interface{} types have built-in support. Other // types need to use the AttributePathStepper interface to tell // WalkAttributePath how to traverse themselves. func WalkAttributePath(in interface{}, path *AttributePath) (interface{}, *AttributePath, error) { if len(path.Steps()) < 1 { return in, path, nil } stepper, ok := in.(AttributePathStepper) if !ok { stepper, ok = builtinAttributePathStepper(in) if !ok { return in, path, ErrNotAttributePathStepper } } next, err := stepper.ApplyTerraform5AttributePathStep(path.Steps()[0]) if err != nil { return in, path, err } return WalkAttributePath(next, &AttributePath{steps: path.Steps()[1:]}) } func builtinAttributePathStepper(in interface{}) (AttributePathStepper, bool) { switch v := in.(type) { case map[string]interface{}: return mapStringInterfaceAttributePathStepper(v), true case []interface{}: return interfaceSliceAttributePathStepper(v), true default: return nil, false } } type mapStringInterfaceAttributePathStepper map[string]interface{} func (m mapStringInterfaceAttributePathStepper) ApplyTerraform5AttributePathStep(step AttributePathStep) (interface{}, error) { _, isAttributeName := step.(AttributeName) _, isElementKeyString := step.(ElementKeyString) if !isAttributeName && !isElementKeyString { return nil, ErrInvalidStep } var stepValue string if isAttributeName { stepValue = string(step.(AttributeName)) } if isElementKeyString { stepValue = string(step.(ElementKeyString)) } v, ok := m[stepValue] if !ok { return nil, ErrInvalidStep } return v, nil } type interfaceSliceAttributePathStepper []interface{} func (i interfaceSliceAttributePathStepper) ApplyTerraform5AttributePathStep(step AttributePathStep) (interface{}, error) { eki, isElementKeyInt := step.(ElementKeyInt) if !isElementKeyInt { return nil, ErrInvalidStep } if eki < 0 { return nil, ErrInvalidStep } // slices can only have items up to the max value of int // but we get ElementKeyInt as an int64 // we keep ElementKeyInt as an int64 and cast the length of the slice // to int64 here because if ElementKeyInt is greater than the max value // of int, we will always (correctly) error out here. This lets us // confidently cast ElementKeyInt to an int below, knowing we're not // truncating data if int64(eki) >= int64(len(i)) { return nil, ErrInvalidStep } return i[int(eki)], nil }