package gohcl import ( "fmt" "reflect" "sort" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/zclconf/go-cty/cty/gocty" ) // EncodeIntoBody replaces the contents of the given hclwrite Body with // attributes and blocks derived from the given value, which must be a // struct value or a pointer to a struct value with the struct tags defined // in this package. // // This function can work only with fully-decoded data. It will ignore any // fields tagged as "remain", any fields that decode attributes into either // hcl.Attribute or hcl.Expression values, and any fields that decode blocks // into hcl.Attributes values. This function does not have enough information // to complete the decoding of these types. // // Any fields tagged as "label" are ignored by this function. Use EncodeAsBlock // to produce a whole hclwrite.Block including block labels. // // As long as a suitable value is given to encode and the destination body // is non-nil, this function will always complete. It will panic in case of // any errors in the calling program, such as passing an inappropriate type // or a nil body. // // The layout of the resulting HCL source is derived from the ordering of // the struct fields, with blank lines around nested blocks of different types. // Fields representing attributes should usually precede those representing // blocks so that the attributes can group togather in the result. For more // control, use the hclwrite API directly. func EncodeIntoBody(val interface{}, dst *hclwrite.Body) { rv := reflect.ValueOf(val) ty := rv.Type() if ty.Kind() == reflect.Ptr { rv = rv.Elem() ty = rv.Type() } if ty.Kind() != reflect.Struct { panic(fmt.Sprintf("value is %s, not struct", ty.Kind())) } tags := getFieldTags(ty) populateBody(rv, ty, tags, dst) } // EncodeAsBlock creates a new hclwrite.Block populated with the data from // the given value, which must be a struct or pointer to struct with the // struct tags defined in this package. // // If the given struct type has fields tagged with "label" tags then they // will be used in order to annotate the created block with labels. // // This function has the same constraints as EncodeIntoBody and will panic // if they are violated. func EncodeAsBlock(val interface{}, blockType string) *hclwrite.Block { rv := reflect.ValueOf(val) ty := rv.Type() if ty.Kind() == reflect.Ptr { rv = rv.Elem() ty = rv.Type() } if ty.Kind() != reflect.Struct { panic(fmt.Sprintf("value is %s, not struct", ty.Kind())) } tags := getFieldTags(ty) labels := make([]string, len(tags.Labels)) for i, lf := range tags.Labels { lv := rv.Field(lf.FieldIndex) // We just stringify whatever we find. It should always be a string // but if not then we'll still do something reasonable. labels[i] = fmt.Sprintf("%s", lv.Interface()) } block := hclwrite.NewBlock(blockType, labels) populateBody(rv, ty, tags, block.Body()) return block } func populateBody(rv reflect.Value, ty reflect.Type, tags *fieldTags, dst *hclwrite.Body) { nameIdxs := make(map[string]int, len(tags.Attributes)+len(tags.Blocks)) namesOrder := make([]string, 0, len(tags.Attributes)+len(tags.Blocks)) for n, i := range tags.Attributes { nameIdxs[n] = i namesOrder = append(namesOrder, n) } for n, i := range tags.Blocks { nameIdxs[n] = i namesOrder = append(namesOrder, n) } sort.SliceStable(namesOrder, func(i, j int) bool { ni, nj := namesOrder[i], namesOrder[j] return nameIdxs[ni] < nameIdxs[nj] }) dst.Clear() prevWasBlock := false for _, name := range namesOrder { fieldIdx := nameIdxs[name] field := ty.Field(fieldIdx) fieldTy := field.Type fieldVal := rv.Field(fieldIdx) if fieldTy.Kind() == reflect.Ptr { fieldTy = fieldTy.Elem() fieldVal = fieldVal.Elem() } if _, isAttr := tags.Attributes[name]; isAttr { if exprType.AssignableTo(fieldTy) || attrType.AssignableTo(fieldTy) { continue // ignore undecoded fields } if !fieldVal.IsValid() { continue // ignore (field value is nil pointer) } if fieldTy.Kind() == reflect.Ptr && fieldVal.IsNil() { continue // ignore } if prevWasBlock { dst.AppendNewline() prevWasBlock = false } valTy, err := gocty.ImpliedType(fieldVal.Interface()) if err != nil { panic(fmt.Sprintf("cannot encode %T as HCL expression: %s", fieldVal.Interface(), err)) } val, err := gocty.ToCtyValue(fieldVal.Interface(), valTy) if err != nil { // This should never happen, since we should always be able // to decode into the implied type. panic(fmt.Sprintf("failed to encode %T as %#v: %s", fieldVal.Interface(), valTy, err)) } dst.SetAttributeValue(name, val) } else { // must be a block, then elemTy := fieldTy isSeq := false if elemTy.Kind() == reflect.Slice || elemTy.Kind() == reflect.Array { isSeq = true elemTy = elemTy.Elem() } if bodyType.AssignableTo(elemTy) || attrsType.AssignableTo(elemTy) { continue // ignore undecoded fields } prevWasBlock = false if isSeq { l := fieldVal.Len() for i := 0; i < l; i++ { elemVal := fieldVal.Index(i) if !elemVal.IsValid() { continue // ignore (elem value is nil pointer) } if elemTy.Kind() == reflect.Ptr && elemVal.IsNil() { continue // ignore } block := EncodeAsBlock(elemVal.Interface(), name) if !prevWasBlock { dst.AppendNewline() prevWasBlock = true } dst.AppendBlock(block) } } else { if !fieldVal.IsValid() { continue // ignore (field value is nil pointer) } if elemTy.Kind() == reflect.Ptr && fieldVal.IsNil() { continue // ignore } block := EncodeAsBlock(fieldVal.Interface(), name) if !prevWasBlock { dst.AppendNewline() prevWasBlock = true } dst.AppendBlock(block) } } } }