github.com/opentofu/opentofu@v1.7.1/internal/gohcl/decode.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  
    10  	"github.com/zclconf/go-cty/cty"
    11  
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/zclconf/go-cty/cty/convert"
    14  	"github.com/zclconf/go-cty/cty/gocty"
    15  )
    16  
    17  // DecodeBody extracts the configuration within the given body into the given
    18  // value. This value must be a non-nil pointer to either a struct or
    19  // a map, where in the former case the configuration will be decoded using
    20  // struct tags and in the latter case only attributes are allowed and their
    21  // values are decoded into the map.
    22  //
    23  // The given EvalContext is used to resolve any variables or functions in
    24  // expressions encountered while decoding. This may be nil to require only
    25  // constant values, for simple applications that do not support variables or
    26  // functions.
    27  //
    28  // The returned diagnostics should be inspected with its HasErrors method to
    29  // determine if the populated value is valid and complete. If error diagnostics
    30  // are returned then the given value may have been partially-populated but
    31  // may still be accessed by a careful caller for static analysis and editor
    32  // integration use-cases.
    33  func DecodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
    34  	rv := reflect.ValueOf(val)
    35  	if rv.Kind() != reflect.Ptr {
    36  		panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String()))
    37  	}
    38  
    39  	return decodeBodyToValue(body, ctx, rv.Elem())
    40  }
    41  
    42  func decodeBodyToValue(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
    43  	et := val.Type()
    44  	switch et.Kind() {
    45  	case reflect.Struct:
    46  		return decodeBodyToStruct(body, ctx, val)
    47  	case reflect.Map:
    48  		return decodeBodyToMap(body, ctx, val)
    49  	default:
    50  		panic(fmt.Sprintf("target value must be pointer to struct or map, not %s", et.String()))
    51  	}
    52  }
    53  
    54  func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
    55  	schema, partial := ImpliedBodySchema(val.Interface())
    56  
    57  	var content *hcl.BodyContent
    58  	var leftovers hcl.Body
    59  	var diags hcl.Diagnostics
    60  	if partial {
    61  		content, leftovers, diags = body.PartialContent(schema)
    62  	} else {
    63  		content, diags = body.Content(schema)
    64  	}
    65  	if content == nil {
    66  		return diags
    67  	}
    68  
    69  	tags := getFieldTags(val.Type())
    70  
    71  	if tags.Body != nil {
    72  		fieldIdx := *tags.Body
    73  		field := val.Type().Field(fieldIdx)
    74  		fieldV := val.Field(fieldIdx)
    75  		switch {
    76  		case bodyType.AssignableTo(field.Type):
    77  			fieldV.Set(reflect.ValueOf(body))
    78  
    79  		default:
    80  			diags = append(diags, decodeBodyToValue(body, ctx, fieldV)...)
    81  		}
    82  	}
    83  
    84  	if tags.Remain != nil {
    85  		fieldIdx := *tags.Remain
    86  		field := val.Type().Field(fieldIdx)
    87  		fieldV := val.Field(fieldIdx)
    88  		switch {
    89  		case bodyType.AssignableTo(field.Type):
    90  			fieldV.Set(reflect.ValueOf(leftovers))
    91  		case attrsType.AssignableTo(field.Type):
    92  			attrs, attrsDiags := leftovers.JustAttributes()
    93  			if len(attrsDiags) > 0 {
    94  				diags = append(diags, attrsDiags...)
    95  			}
    96  			fieldV.Set(reflect.ValueOf(attrs))
    97  		default:
    98  			diags = append(diags, decodeBodyToValue(leftovers, ctx, fieldV)...)
    99  		}
   100  	}
   101  
   102  	for name, fieldIdx := range tags.Attributes {
   103  		attr := content.Attributes[name]
   104  		field := val.Type().Field(fieldIdx)
   105  		fieldV := val.Field(fieldIdx)
   106  
   107  		if attr == nil {
   108  			if !exprType.AssignableTo(field.Type) {
   109  				continue
   110  			}
   111  
   112  			// As a special case, if the target is of type hcl.Expression then
   113  			// we'll assign an actual expression that evalues to a cty null,
   114  			// so the caller can deal with it within the cty realm rather
   115  			// than within the Go realm.
   116  			synthExpr := hcl.StaticExpr(cty.NullVal(cty.DynamicPseudoType), body.MissingItemRange())
   117  			fieldV.Set(reflect.ValueOf(synthExpr))
   118  			continue
   119  		}
   120  
   121  		switch {
   122  		case attrType.AssignableTo(field.Type):
   123  			fieldV.Set(reflect.ValueOf(attr))
   124  		case exprType.AssignableTo(field.Type):
   125  			fieldV.Set(reflect.ValueOf(attr.Expr))
   126  		case field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct:
   127  			// TODO might want to check for nil here
   128  			rn := reflect.New(field.Type.Elem())
   129  			fieldV.Set(rn)
   130  			diags = append(diags, DecodeExpression(
   131  				attr.Expr, ctx, fieldV.Interface(),
   132  			)...)
   133  		default:
   134  			diags = append(diags, DecodeExpression(
   135  				attr.Expr, ctx, fieldV.Addr().Interface(),
   136  			)...)
   137  		}
   138  	}
   139  
   140  	blocksByType := content.Blocks.ByType()
   141  
   142  	for typeName, fieldIdx := range tags.Blocks {
   143  		blocks := blocksByType[typeName]
   144  		field := val.Type().Field(fieldIdx)
   145  
   146  		ty := field.Type
   147  		isSlice := false
   148  		isPtr := false
   149  		if ty.Kind() == reflect.Slice {
   150  			isSlice = true
   151  			ty = ty.Elem()
   152  		}
   153  		if ty.Kind() == reflect.Ptr {
   154  			isPtr = true
   155  			ty = ty.Elem()
   156  		}
   157  
   158  		if len(blocks) > 1 && !isSlice {
   159  			diags = append(diags, &hcl.Diagnostic{
   160  				Severity: hcl.DiagError,
   161  				Summary:  fmt.Sprintf("Duplicate %s block", typeName),
   162  				Detail: fmt.Sprintf(
   163  					"Only one %s block is allowed. Another was defined at %s.",
   164  					typeName, blocks[0].DefRange.String(),
   165  				),
   166  				Subject: &blocks[1].DefRange,
   167  			})
   168  			continue
   169  		}
   170  
   171  		if len(blocks) == 0 {
   172  			if isSlice || isPtr {
   173  				if val.Field(fieldIdx).IsNil() {
   174  					val.Field(fieldIdx).Set(reflect.Zero(field.Type))
   175  				}
   176  			} else {
   177  				diags = append(diags, &hcl.Diagnostic{
   178  					Severity: hcl.DiagError,
   179  					Summary:  fmt.Sprintf("Missing %s block", typeName),
   180  					Detail:   fmt.Sprintf("A %s block is required.", typeName),
   181  					Subject:  body.MissingItemRange().Ptr(),
   182  				})
   183  			}
   184  			continue
   185  		}
   186  
   187  		switch {
   188  
   189  		case isSlice:
   190  			elemType := ty
   191  			if isPtr {
   192  				elemType = reflect.PtrTo(ty)
   193  			}
   194  			sli := val.Field(fieldIdx)
   195  			if sli.IsNil() {
   196  				sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks))
   197  			}
   198  
   199  			for i, block := range blocks {
   200  				if isPtr {
   201  					if i >= sli.Len() {
   202  						sli = reflect.Append(sli, reflect.New(ty))
   203  					}
   204  					v := sli.Index(i)
   205  					if v.IsNil() {
   206  						v = reflect.New(ty)
   207  					}
   208  					diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...)
   209  					sli.Index(i).Set(v)
   210  				} else {
   211  					if i >= sli.Len() {
   212  						sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty)))
   213  					}
   214  					diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...)
   215  				}
   216  			}
   217  
   218  			if sli.Len() > len(blocks) {
   219  				sli.SetLen(len(blocks))
   220  			}
   221  
   222  			val.Field(fieldIdx).Set(sli)
   223  
   224  		default:
   225  			block := blocks[0]
   226  			if isPtr {
   227  				v := val.Field(fieldIdx)
   228  				if v.IsNil() {
   229  					v = reflect.New(ty)
   230  				}
   231  				diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...)
   232  				val.Field(fieldIdx).Set(v)
   233  			} else {
   234  				diags = append(diags, decodeBlockToValue(block, ctx, val.Field(fieldIdx))...)
   235  			}
   236  
   237  		}
   238  
   239  	}
   240  
   241  	return diags
   242  }
   243  
   244  func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
   245  	attrs, diags := body.JustAttributes()
   246  	if attrs == nil {
   247  		return diags
   248  	}
   249  
   250  	mv := reflect.MakeMap(v.Type())
   251  
   252  	for k, attr := range attrs {
   253  		switch {
   254  		case attrType.AssignableTo(v.Type().Elem()):
   255  			mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr))
   256  		case exprType.AssignableTo(v.Type().Elem()):
   257  			mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr.Expr))
   258  		default:
   259  			ev := reflect.New(v.Type().Elem())
   260  			diags = append(diags, DecodeExpression(attr.Expr, ctx, ev.Interface())...)
   261  			mv.SetMapIndex(reflect.ValueOf(k), ev.Elem())
   262  		}
   263  	}
   264  
   265  	v.Set(mv)
   266  
   267  	return diags
   268  }
   269  
   270  func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
   271  	diags := decodeBodyToValue(block.Body, ctx, v)
   272  
   273  	if len(block.Labels) > 0 {
   274  		blockTags := getFieldTags(v.Type())
   275  		for li, lv := range block.Labels {
   276  			lfieldIdx := blockTags.Labels[li].FieldIndex
   277  			v.Field(lfieldIdx).Set(reflect.ValueOf(lv))
   278  		}
   279  	}
   280  
   281  	return diags
   282  }
   283  
   284  // DecodeExpression extracts the value of the given expression into the given
   285  // value. This value must be something that gocty is able to decode into,
   286  // since the final decoding is delegated to that package.  If a reference to
   287  // a struct is provided which contains gohcl tags, it will be decoded using
   288  // the attr and optional tags.
   289  //
   290  // The given EvalContext is used to resolve any variables or functions in
   291  // expressions encountered while decoding. This may be nil to require only
   292  // constant values, for simple applications that do not support variables or
   293  // functions.
   294  //
   295  // The returned diagnostics should be inspected with its HasErrors method to
   296  // determine if the populated value is valid and complete. If error diagnostics
   297  // are returned then the given value may have been partially-populated but
   298  // may still be accessed by a careful caller for static analysis and editor
   299  // integration use-cases.
   300  func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
   301  	srcVal, diags := expr.Value(ctx)
   302  	if diags.HasErrors() {
   303  		return diags
   304  	}
   305  
   306  	return append(diags, DecodeValue(srcVal, expr.StartRange(), expr.Range(), val)...)
   307  }
   308  
   309  // DecodeValue extracts the given value into the provided target.
   310  // This value must be something that gocty is able to decode into,
   311  // since the final decoding is delegated to that package.  If a reference to
   312  // a struct is provided which contains gohcl tags, it will be decoded using
   313  // the attr and optional tags.
   314  //
   315  // The returned diagnostics should be inspected with its HasErrors method to
   316  // determine if the populated value is valid and complete. If error diagnostics
   317  // are returned then the given value may have been partially-populated but
   318  // may still be accessed by a careful caller for static analysis and editor
   319  // integration use-cases.
   320  func DecodeValue(srcVal cty.Value, subject hcl.Range, context hcl.Range, val interface{}) hcl.Diagnostics {
   321  	rv := reflect.ValueOf(val)
   322  	if rv.Type().Kind() == reflect.Ptr && rv.Type().Elem().Kind() == reflect.Struct && hasFieldTags(rv.Elem().Type()) {
   323  		attrs := make(hcl.Attributes)
   324  		for k, v := range srcVal.AsValueMap() {
   325  			attrs[k] = &hcl.Attribute{
   326  				Name:  k,
   327  				Expr:  hcl.StaticExpr(v, context),
   328  				Range: subject,
   329  			}
   330  
   331  		}
   332  		return decodeBodyToStruct(synthBody{
   333  			attrs:   attrs,
   334  			subject: subject,
   335  			context: context,
   336  		}, nil, rv.Elem())
   337  
   338  	}
   339  
   340  	convTy, err := gocty.ImpliedType(val)
   341  	if err != nil {
   342  		panic(fmt.Sprintf("unsuitable DecodeExpression target: %s", err))
   343  	}
   344  
   345  	var diags hcl.Diagnostics
   346  
   347  	srcVal, err = convert.Convert(srcVal, convTy)
   348  	if err != nil {
   349  		diags = append(diags, &hcl.Diagnostic{
   350  			Severity: hcl.DiagError,
   351  			Summary:  "Unsuitable value type",
   352  			Detail:   fmt.Sprintf("Unsuitable value: %s", err.Error()),
   353  			Subject:  subject.Ptr(),
   354  			Context:  context.Ptr(),
   355  		})
   356  		return diags
   357  	}
   358  
   359  	err = gocty.FromCtyValue(srcVal, val)
   360  	if err != nil {
   361  		diags = append(diags, &hcl.Diagnostic{
   362  			Severity: hcl.DiagError,
   363  			Summary:  "Unsuitable value type",
   364  			Detail:   fmt.Sprintf("Unsuitable value: %s", err.Error()),
   365  			Subject:  subject.Ptr(),
   366  			Context:  context.Ptr(),
   367  		})
   368  	}
   369  
   370  	return diags
   371  }
   372  
   373  type synthBody struct {
   374  	attrs   hcl.Attributes
   375  	subject hcl.Range
   376  	context hcl.Range
   377  }
   378  
   379  func (s synthBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
   380  	body, partial, diags := s.PartialContent(schema)
   381  
   382  	attrs, _ := partial.JustAttributes()
   383  	for name := range attrs {
   384  		diags = append(diags, &hcl.Diagnostic{
   385  			Severity: hcl.DiagError,
   386  			Summary:  "Unsupported argument",
   387  			Detail:   fmt.Sprintf("An argument named %q is not expected here.", name),
   388  			Subject:  s.subject.Ptr(),
   389  			Context:  s.context.Ptr(),
   390  		})
   391  	}
   392  
   393  	return body, diags
   394  }
   395  
   396  func (s synthBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
   397  	var diags hcl.Diagnostics
   398  
   399  	for _, block := range schema.Blocks {
   400  		panic("hcl block tags are not allowed in attribute structs: " + block.Type)
   401  	}
   402  
   403  	attrs := make(hcl.Attributes)
   404  	remainder := make(hcl.Attributes)
   405  
   406  	for _, attr := range schema.Attributes {
   407  		v, ok := s.attrs[attr.Name]
   408  		if !ok {
   409  			if attr.Required {
   410  				diags = append(diags, &hcl.Diagnostic{
   411  					Severity: hcl.DiagError,
   412  					Summary:  "Missing required argument",
   413  					Detail:   fmt.Sprintf("The argument %q is required, but no definition was found.", attr.Name),
   414  					Subject:  s.subject.Ptr(),
   415  					Context:  s.context.Ptr(),
   416  				})
   417  			}
   418  			continue
   419  		}
   420  
   421  		attrs[attr.Name] = v
   422  	}
   423  
   424  	for k, v := range s.attrs {
   425  		if _, ok := attrs[k]; !ok {
   426  			remainder[k] = v
   427  		}
   428  	}
   429  
   430  	return &hcl.BodyContent{
   431  		Attributes:       attrs,
   432  		MissingItemRange: s.context,
   433  	}, synthBody{attrs: remainder}, diags
   434  }
   435  
   436  func (s synthBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
   437  	return s.attrs, nil
   438  }
   439  func (s synthBody) MissingItemRange() hcl.Range {
   440  	return s.context
   441  }