github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/lang/funcs/defaults.go (about)

     1  package funcs
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/iaas-resource-provision/iaas-rpc/internal/tfdiags"
     7  	"github.com/zclconf/go-cty/cty"
     8  	"github.com/zclconf/go-cty/cty/convert"
     9  	"github.com/zclconf/go-cty/cty/function"
    10  )
    11  
    12  // DefaultsFunc is a helper function for substituting default values in
    13  // place of null values in a given data structure.
    14  //
    15  // See the documentation for function Defaults for more information.
    16  var DefaultsFunc = function.New(&function.Spec{
    17  	Params: []function.Parameter{
    18  		{
    19  			Name:      "input",
    20  			Type:      cty.DynamicPseudoType,
    21  			AllowNull: true,
    22  		},
    23  		{
    24  			Name: "defaults",
    25  			Type: cty.DynamicPseudoType,
    26  		},
    27  	},
    28  	Type: func(args []cty.Value) (cty.Type, error) {
    29  		// The result type is guaranteed to be the same as the input type,
    30  		// since all we're doing is replacing null values with non-null
    31  		// values of the same type.
    32  		retType := args[0].Type()
    33  		defaultsType := args[1].Type()
    34  
    35  		// This function is aimed at filling in object types or collections
    36  		// of object types where some of the attributes might be null, so
    37  		// it doesn't make sense to use a primitive type directly with it.
    38  		// (The "coalesce" function may be appropriate for such cases.)
    39  		if retType.IsPrimitiveType() {
    40  			// This error message is a bit of a fib because we can actually
    41  			// apply defaults to tuples too, but we expect that to be so
    42  			// unusual as to not be worth mentioning here, because mentioning
    43  			// it would require using some less-well-known Terraform language
    44  			// terminology in the message (tuple types, structural types).
    45  			return cty.DynamicPseudoType, function.NewArgErrorf(1, "only object types and collections of object types can have defaults applied")
    46  		}
    47  
    48  		defaultsPath := make(cty.Path, 0, 4) // some capacity so that most structures won't reallocate
    49  		if err := defaultsAssertSuitableFallback(retType, defaultsType, defaultsPath); err != nil {
    50  			errMsg := tfdiags.FormatError(err) // add attribute path prefix
    51  			return cty.DynamicPseudoType, function.NewArgErrorf(1, "%s", errMsg)
    52  		}
    53  
    54  		return retType, nil
    55  	},
    56  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
    57  		if args[0].Type().HasDynamicTypes() {
    58  			// If the types our input object aren't known yet for some reason
    59  			// then we'll defer all of our work here, because our
    60  			// interpretation of the defaults depends on the types in
    61  			// the input.
    62  			return cty.UnknownVal(retType), nil
    63  		}
    64  
    65  		v := defaultsApply(args[0], args[1])
    66  		return v, nil
    67  	},
    68  })
    69  
    70  func defaultsApply(input, fallback cty.Value) cty.Value {
    71  	wantTy := input.Type()
    72  	if !(input.IsKnown() && fallback.IsKnown()) {
    73  		return cty.UnknownVal(wantTy)
    74  	}
    75  
    76  	// For the rest of this function we're assuming that the given defaults
    77  	// will always be valid, because we expect to have caught any problems
    78  	// during the type checking phase. Any inconsistencies that reach here are
    79  	// therefore considered to be implementation bugs, and so will panic.
    80  
    81  	// Our strategy depends on the kind of type we're working with.
    82  	switch {
    83  	case wantTy.IsPrimitiveType():
    84  		// For leaf primitive values the rule is relatively simple: use the
    85  		// input if it's non-null, or fallback if input is null.
    86  		if !input.IsNull() {
    87  			return input
    88  		}
    89  		v, err := convert.Convert(fallback, wantTy)
    90  		if err != nil {
    91  			// Should not happen because we checked in defaultsAssertSuitableFallback
    92  			panic(err.Error())
    93  		}
    94  		return v
    95  
    96  	case wantTy.IsObjectType():
    97  		// For structural types, a null input value must be passed through. We
    98  		// do not apply default values for missing optional structural values,
    99  		// only their contents.
   100  		//
   101  		// We also pass through the input if the fallback value is null. This
   102  		// can happen if the given defaults do not include a value for this
   103  		// attribute.
   104  		if input.IsNull() || fallback.IsNull() {
   105  			return input
   106  		}
   107  		atys := wantTy.AttributeTypes()
   108  		ret := map[string]cty.Value{}
   109  		for attr, aty := range atys {
   110  			inputSub := input.GetAttr(attr)
   111  			fallbackSub := cty.NullVal(aty)
   112  			if fallback.Type().HasAttribute(attr) {
   113  				fallbackSub = fallback.GetAttr(attr)
   114  			}
   115  			ret[attr] = defaultsApply(inputSub, fallbackSub)
   116  		}
   117  		return cty.ObjectVal(ret)
   118  
   119  	case wantTy.IsTupleType():
   120  		// For structural types, a null input value must be passed through. We
   121  		// do not apply default values for missing optional structural values,
   122  		// only their contents.
   123  		//
   124  		// We also pass through the input if the fallback value is null. This
   125  		// can happen if the given defaults do not include a value for this
   126  		// attribute.
   127  		if input.IsNull() || fallback.IsNull() {
   128  			return input
   129  		}
   130  
   131  		l := wantTy.Length()
   132  		ret := make([]cty.Value, l)
   133  		for i := 0; i < l; i++ {
   134  			inputSub := input.Index(cty.NumberIntVal(int64(i)))
   135  			fallbackSub := fallback.Index(cty.NumberIntVal(int64(i)))
   136  			ret[i] = defaultsApply(inputSub, fallbackSub)
   137  		}
   138  		return cty.TupleVal(ret)
   139  
   140  	case wantTy.IsCollectionType():
   141  		// For collection types we apply a single fallback value to each
   142  		// element of the input collection, because in the situations this
   143  		// function is intended for we assume that the number of elements
   144  		// is the caller's decision, and so we'll just apply the same defaults
   145  		// to all of the elements.
   146  		ety := wantTy.ElementType()
   147  		switch {
   148  		case wantTy.IsMapType():
   149  			newVals := map[string]cty.Value{}
   150  
   151  			if !input.IsNull() {
   152  				for it := input.ElementIterator(); it.Next(); {
   153  					k, v := it.Element()
   154  					newVals[k.AsString()] = defaultsApply(v, fallback)
   155  				}
   156  			}
   157  
   158  			if len(newVals) == 0 {
   159  				return cty.MapValEmpty(ety)
   160  			}
   161  			return cty.MapVal(newVals)
   162  		case wantTy.IsListType(), wantTy.IsSetType():
   163  			var newVals []cty.Value
   164  
   165  			if !input.IsNull() {
   166  				for it := input.ElementIterator(); it.Next(); {
   167  					_, v := it.Element()
   168  					newV := defaultsApply(v, fallback)
   169  					newVals = append(newVals, newV)
   170  				}
   171  			}
   172  
   173  			if len(newVals) == 0 {
   174  				if wantTy.IsSetType() {
   175  					return cty.SetValEmpty(ety)
   176  				}
   177  				return cty.ListValEmpty(ety)
   178  			}
   179  			if wantTy.IsSetType() {
   180  				return cty.SetVal(newVals)
   181  			}
   182  			return cty.ListVal(newVals)
   183  		default:
   184  			// There are no other collection types, so this should not happen
   185  			panic(fmt.Sprintf("invalid collection type %#v", wantTy))
   186  		}
   187  	default:
   188  		// We should've caught anything else in defaultsAssertSuitableFallback,
   189  		// so this should not happen.
   190  		panic(fmt.Sprintf("invalid target type %#v", wantTy))
   191  	}
   192  }
   193  
   194  func defaultsAssertSuitableFallback(wantTy, fallbackTy cty.Type, fallbackPath cty.Path) error {
   195  	// If the type we want is a collection type then we need to keep peeling
   196  	// away collection type wrappers until we find the non-collection-type
   197  	// that's underneath, which is what the fallback will actually be applied
   198  	// to.
   199  	inCollection := false
   200  	for wantTy.IsCollectionType() {
   201  		wantTy = wantTy.ElementType()
   202  		inCollection = true
   203  	}
   204  
   205  	switch {
   206  	case wantTy.IsPrimitiveType():
   207  		// The fallback is valid if it's equal to or convertible to what we want.
   208  		if fallbackTy.Equals(wantTy) {
   209  			return nil
   210  		}
   211  		conversion := convert.GetConversion(fallbackTy, wantTy)
   212  		if conversion == nil {
   213  			msg := convert.MismatchMessage(fallbackTy, wantTy)
   214  			return fallbackPath.NewErrorf("invalid default value for %s: %s", wantTy.FriendlyName(), msg)
   215  		}
   216  		return nil
   217  	case wantTy.IsObjectType():
   218  		if !fallbackTy.IsObjectType() {
   219  			if inCollection {
   220  				return fallbackPath.NewErrorf("the default value for a collection of an object type must itself be an object type, not %s", fallbackTy.FriendlyName())
   221  			}
   222  			return fallbackPath.NewErrorf("the default value for an object type must itself be an object type, not %s", fallbackTy.FriendlyName())
   223  		}
   224  		for attr, wantAty := range wantTy.AttributeTypes() {
   225  			if !fallbackTy.HasAttribute(attr) {
   226  				continue // it's always okay to not have a default value
   227  			}
   228  			fallbackSubpath := fallbackPath.GetAttr(attr)
   229  			fallbackSubTy := fallbackTy.AttributeType(attr)
   230  			err := defaultsAssertSuitableFallback(wantAty, fallbackSubTy, fallbackSubpath)
   231  			if err != nil {
   232  				return err
   233  			}
   234  		}
   235  		for attr := range fallbackTy.AttributeTypes() {
   236  			if !wantTy.HasAttribute(attr) {
   237  				fallbackSubpath := fallbackPath.GetAttr(attr)
   238  				return fallbackSubpath.NewErrorf("target type does not expect an attribute named %q", attr)
   239  			}
   240  		}
   241  		return nil
   242  	case wantTy.IsTupleType():
   243  		if !fallbackTy.IsTupleType() {
   244  			if inCollection {
   245  				return fallbackPath.NewErrorf("the default value for a collection of a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName())
   246  			}
   247  			return fallbackPath.NewErrorf("the default value for a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName())
   248  		}
   249  		wantEtys := wantTy.TupleElementTypes()
   250  		fallbackEtys := fallbackTy.TupleElementTypes()
   251  		if got, want := len(wantEtys), len(fallbackEtys); got != want {
   252  			return fallbackPath.NewErrorf("the default value for a tuple type of length %d must also have length %d, not %d", want, want, got)
   253  		}
   254  		for i := 0; i < len(wantEtys); i++ {
   255  			fallbackSubpath := fallbackPath.IndexInt(i)
   256  			wantSubTy := wantEtys[i]
   257  			fallbackSubTy := fallbackEtys[i]
   258  			err := defaultsAssertSuitableFallback(wantSubTy, fallbackSubTy, fallbackSubpath)
   259  			if err != nil {
   260  				return err
   261  			}
   262  		}
   263  		return nil
   264  	default:
   265  		// No other types are supported right now.
   266  		return fallbackPath.NewErrorf("cannot apply defaults to %s", wantTy.FriendlyName())
   267  	}
   268  }
   269  
   270  // Defaults is a helper function for substituting default values in
   271  // place of null values in a given data structure.
   272  //
   273  // This is primarily intended for use with a module input variable that
   274  // has an object type constraint (or a collection thereof) that has optional
   275  // attributes, so that the receiver of a value that omits those attributes
   276  // can insert non-null default values in place of the null values caused by
   277  // omitting the attributes.
   278  func Defaults(input, defaults cty.Value) (cty.Value, error) {
   279  	return DefaultsFunc.Call([]cty.Value{input, defaults})
   280  }