| 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) |
| } |
| } |
| } |
| } |