github.com/opentofu/opentofu@v1.7.1/internal/gohcl/encode.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package gohcl
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"sort"
    10  
    11  	"github.com/hashicorp/hcl/v2/hclwrite"
    12  	"github.com/zclconf/go-cty/cty/gocty"
    13  )
    14  
    15  // EncodeIntoBody replaces the contents of the given hclwrite Body with
    16  // attributes and blocks derived from the given value, which must be a
    17  // struct value or a pointer to a struct value with the struct tags defined
    18  // in this package.
    19  //
    20  // This function can work only with fully-decoded data. It will ignore any
    21  // fields tagged as "remain", any fields that decode attributes into either
    22  // hcl.Attribute or hcl.Expression values, and any fields that decode blocks
    23  // into hcl.Attributes values. This function does not have enough information
    24  // to complete the decoding of these types.
    25  //
    26  // Any fields tagged as "label" are ignored by this function. Use EncodeAsBlock
    27  // to produce a whole hclwrite.Block including block labels.
    28  //
    29  // As long as a suitable value is given to encode and the destination body
    30  // is non-nil, this function will always complete. It will panic in case of
    31  // any errors in the calling program, such as passing an inappropriate type
    32  // or a nil body.
    33  //
    34  // The layout of the resulting HCL source is derived from the ordering of
    35  // the struct fields, with blank lines around nested blocks of different types.
    36  // Fields representing attributes should usually precede those representing
    37  // blocks so that the attributes can group togather in the result. For more
    38  // control, use the hclwrite API directly.
    39  func EncodeIntoBody(val interface{}, dst *hclwrite.Body) {
    40  	rv := reflect.ValueOf(val)
    41  	ty := rv.Type()
    42  	if ty.Kind() == reflect.Ptr {
    43  		rv = rv.Elem()
    44  		ty = rv.Type()
    45  	}
    46  	if ty.Kind() != reflect.Struct {
    47  		panic(fmt.Sprintf("value is %s, not struct", ty.Kind()))
    48  	}
    49  
    50  	tags := getFieldTags(ty)
    51  	populateBody(rv, ty, tags, dst)
    52  }
    53  
    54  // EncodeAsBlock creates a new hclwrite.Block populated with the data from
    55  // the given value, which must be a struct or pointer to struct with the
    56  // struct tags defined in this package.
    57  //
    58  // If the given struct type has fields tagged with "label" tags then they
    59  // will be used in order to annotate the created block with labels.
    60  //
    61  // This function has the same constraints as EncodeIntoBody and will panic
    62  // if they are violated.
    63  func EncodeAsBlock(val interface{}, blockType string) *hclwrite.Block {
    64  	rv := reflect.ValueOf(val)
    65  	ty := rv.Type()
    66  	if ty.Kind() == reflect.Ptr {
    67  		rv = rv.Elem()
    68  		ty = rv.Type()
    69  	}
    70  	if ty.Kind() != reflect.Struct {
    71  		panic(fmt.Sprintf("value is %s, not struct", ty.Kind()))
    72  	}
    73  
    74  	tags := getFieldTags(ty)
    75  	labels := make([]string, len(tags.Labels))
    76  	for i, lf := range tags.Labels {
    77  		lv := rv.Field(lf.FieldIndex)
    78  		// We just stringify whatever we find. It should always be a string
    79  		// but if not then we'll still do something reasonable.
    80  		labels[i] = fmt.Sprintf("%s", lv.Interface())
    81  	}
    82  
    83  	block := hclwrite.NewBlock(blockType, labels)
    84  	populateBody(rv, ty, tags, block.Body())
    85  	return block
    86  }
    87  
    88  func populateBody(rv reflect.Value, ty reflect.Type, tags *fieldTags, dst *hclwrite.Body) {
    89  	nameIdxs := make(map[string]int, len(tags.Attributes)+len(tags.Blocks))
    90  	namesOrder := make([]string, 0, len(tags.Attributes)+len(tags.Blocks))
    91  	for n, i := range tags.Attributes {
    92  		nameIdxs[n] = i
    93  		namesOrder = append(namesOrder, n)
    94  	}
    95  	for n, i := range tags.Blocks {
    96  		nameIdxs[n] = i
    97  		namesOrder = append(namesOrder, n)
    98  	}
    99  	sort.SliceStable(namesOrder, func(i, j int) bool {
   100  		ni, nj := namesOrder[i], namesOrder[j]
   101  		return nameIdxs[ni] < nameIdxs[nj]
   102  	})
   103  
   104  	dst.Clear()
   105  
   106  	prevWasBlock := false
   107  	for _, name := range namesOrder {
   108  		fieldIdx := nameIdxs[name]
   109  		field := ty.Field(fieldIdx)
   110  		fieldTy := field.Type
   111  		fieldVal := rv.Field(fieldIdx)
   112  
   113  		if fieldTy.Kind() == reflect.Ptr {
   114  			fieldTy = fieldTy.Elem()
   115  			fieldVal = fieldVal.Elem()
   116  		}
   117  
   118  		if _, isAttr := tags.Attributes[name]; isAttr {
   119  
   120  			if exprType.AssignableTo(fieldTy) || attrType.AssignableTo(fieldTy) {
   121  				continue // ignore undecoded fields
   122  			}
   123  			if !fieldVal.IsValid() {
   124  				continue // ignore (field value is nil pointer)
   125  			}
   126  			if fieldTy.Kind() == reflect.Ptr && fieldVal.IsNil() {
   127  				continue // ignore
   128  			}
   129  			if prevWasBlock {
   130  				dst.AppendNewline()
   131  				prevWasBlock = false
   132  			}
   133  
   134  			valTy, err := gocty.ImpliedType(fieldVal.Interface())
   135  			if err != nil {
   136  				panic(fmt.Sprintf("cannot encode %T as HCL expression: %s", fieldVal.Interface(), err))
   137  			}
   138  
   139  			val, err := gocty.ToCtyValue(fieldVal.Interface(), valTy)
   140  			if err != nil {
   141  				// This should never happen, since we should always be able
   142  				// to decode into the implied type.
   143  				panic(fmt.Sprintf("failed to encode %T as %#v: %s", fieldVal.Interface(), valTy, err))
   144  			}
   145  
   146  			dst.SetAttributeValue(name, val)
   147  
   148  		} else { // must be a block, then
   149  			elemTy := fieldTy
   150  			isSeq := false
   151  			if elemTy.Kind() == reflect.Slice || elemTy.Kind() == reflect.Array {
   152  				isSeq = true
   153  				elemTy = elemTy.Elem()
   154  			}
   155  
   156  			if bodyType.AssignableTo(elemTy) || attrsType.AssignableTo(elemTy) {
   157  				continue // ignore undecoded fields
   158  			}
   159  			prevWasBlock = false
   160  
   161  			if isSeq {
   162  				l := fieldVal.Len()
   163  				for i := 0; i < l; i++ {
   164  					elemVal := fieldVal.Index(i)
   165  					if !elemVal.IsValid() {
   166  						continue // ignore (elem value is nil pointer)
   167  					}
   168  					if elemTy.Kind() == reflect.Ptr && elemVal.IsNil() {
   169  						continue // ignore
   170  					}
   171  					block := EncodeAsBlock(elemVal.Interface(), name)
   172  					if !prevWasBlock {
   173  						dst.AppendNewline()
   174  						prevWasBlock = true
   175  					}
   176  					dst.AppendBlock(block)
   177  				}
   178  			} else {
   179  				if !fieldVal.IsValid() {
   180  					continue // ignore (field value is nil pointer)
   181  				}
   182  				if elemTy.Kind() == reflect.Ptr && fieldVal.IsNil() {
   183  					continue // ignore
   184  				}
   185  				block := EncodeAsBlock(fieldVal.Interface(), name)
   186  				if !prevWasBlock {
   187  					dst.AppendNewline()
   188  					prevWasBlock = true
   189  				}
   190  				dst.AppendBlock(block)
   191  			}
   192  		}
   193  	}
   194  }