github.com/hashicorp/hcl/v2@v2.20.0/cmd/hcldec/spec.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package main
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/ext/userfunc"
    11  	"github.com/hashicorp/hcl/v2/gohcl"
    12  	"github.com/hashicorp/hcl/v2/hcldec"
    13  	"github.com/zclconf/go-cty/cty"
    14  	"github.com/zclconf/go-cty/cty/function"
    15  )
    16  
    17  type specFileContent struct {
    18  	Variables map[string]cty.Value
    19  	Functions map[string]function.Function
    20  	RootSpec  hcldec.Spec
    21  }
    22  
    23  var specCtx = &hcl.EvalContext{
    24  	Functions: specFuncs,
    25  }
    26  
    27  func loadSpecFile(filename string) (specFileContent, hcl.Diagnostics) {
    28  	file, diags := parser.ParseHCLFile(filename)
    29  	if diags.HasErrors() {
    30  		return specFileContent{RootSpec: errSpec}, diags
    31  	}
    32  
    33  	vars, funcs, specBody, declDiags := decodeSpecDecls(file.Body)
    34  	diags = append(diags, declDiags...)
    35  
    36  	spec, specDiags := decodeSpecRoot(specBody)
    37  	diags = append(diags, specDiags...)
    38  
    39  	return specFileContent{
    40  		Variables: vars,
    41  		Functions: funcs,
    42  		RootSpec:  spec,
    43  	}, diags
    44  }
    45  
    46  func decodeSpecDecls(body hcl.Body) (map[string]cty.Value, map[string]function.Function, hcl.Body, hcl.Diagnostics) {
    47  	funcs, body, diags := userfunc.DecodeUserFunctions(body, "function", func() *hcl.EvalContext {
    48  		return specCtx
    49  	})
    50  
    51  	content, body, moreDiags := body.PartialContent(&hcl.BodySchema{
    52  		Blocks: []hcl.BlockHeaderSchema{
    53  			{
    54  				Type: "variables",
    55  			},
    56  		},
    57  	})
    58  	diags = append(diags, moreDiags...)
    59  
    60  	vars := make(map[string]cty.Value)
    61  	for _, block := range content.Blocks {
    62  		// We only have one block type in our schema, so we can assume all
    63  		// blocks are of that type.
    64  		attrs, moreDiags := block.Body.JustAttributes()
    65  		diags = append(diags, moreDiags...)
    66  
    67  		for name, attr := range attrs {
    68  			val, moreDiags := attr.Expr.Value(specCtx)
    69  			diags = append(diags, moreDiags...)
    70  			vars[name] = val
    71  		}
    72  	}
    73  
    74  	return vars, funcs, body, diags
    75  }
    76  
    77  func decodeSpecRoot(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
    78  	content, diags := body.Content(specSchemaUnlabelled)
    79  
    80  	if len(content.Blocks) == 0 {
    81  		if diags.HasErrors() {
    82  			// If we already have errors then they probably explain
    83  			// why we have no blocks, so we'll skip our additional
    84  			// error message added below.
    85  			return errSpec, diags
    86  		}
    87  
    88  		diags = append(diags, &hcl.Diagnostic{
    89  			Severity: hcl.DiagError,
    90  			Summary:  "Missing spec block",
    91  			Detail:   "A spec file must have exactly one root block specifying how to map to a JSON value.",
    92  			Subject:  body.MissingItemRange().Ptr(),
    93  		})
    94  		return errSpec, diags
    95  	}
    96  
    97  	if len(content.Blocks) > 1 {
    98  		diags = append(diags, &hcl.Diagnostic{
    99  			Severity: hcl.DiagError,
   100  			Summary:  "Extraneous spec block",
   101  			Detail:   "A spec file must have exactly one root block specifying how to map to a JSON value.",
   102  			Subject:  &content.Blocks[1].DefRange,
   103  		})
   104  		return errSpec, diags
   105  	}
   106  
   107  	spec, specDiags := decodeSpecBlock(content.Blocks[0])
   108  	diags = append(diags, specDiags...)
   109  	return spec, diags
   110  }
   111  
   112  func decodeSpecBlock(block *hcl.Block) (hcldec.Spec, hcl.Diagnostics) {
   113  	var impliedName string
   114  	if len(block.Labels) > 0 {
   115  		impliedName = block.Labels[0]
   116  	}
   117  
   118  	switch block.Type {
   119  
   120  	case "object":
   121  		return decodeObjectSpec(block.Body)
   122  
   123  	case "array":
   124  		return decodeArraySpec(block.Body)
   125  
   126  	case "attr":
   127  		return decodeAttrSpec(block.Body, impliedName)
   128  
   129  	case "block":
   130  		return decodeBlockSpec(block.Body, impliedName)
   131  
   132  	case "block_list":
   133  		return decodeBlockListSpec(block.Body, impliedName)
   134  
   135  	case "block_set":
   136  		return decodeBlockSetSpec(block.Body, impliedName)
   137  
   138  	case "block_map":
   139  		return decodeBlockMapSpec(block.Body, impliedName)
   140  
   141  	case "block_attrs":
   142  		return decodeBlockAttrsSpec(block.Body, impliedName)
   143  
   144  	case "default":
   145  		return decodeDefaultSpec(block.Body)
   146  
   147  	case "transform":
   148  		return decodeTransformSpec(block.Body)
   149  
   150  	case "literal":
   151  		return decodeLiteralSpec(block.Body)
   152  
   153  	default:
   154  		// Should never happen, because the above cases should be exhaustive
   155  		// for our schema.
   156  		var diags hcl.Diagnostics
   157  		diags = append(diags, &hcl.Diagnostic{
   158  			Severity: hcl.DiagError,
   159  			Summary:  "Invalid spec block",
   160  			Detail:   fmt.Sprintf("Blocks of type %q are not expected here.", block.Type),
   161  			Subject:  &block.TypeRange,
   162  		})
   163  		return errSpec, diags
   164  	}
   165  }
   166  
   167  func decodeObjectSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
   168  	content, diags := body.Content(specSchemaLabelled)
   169  
   170  	spec := make(hcldec.ObjectSpec)
   171  	for _, block := range content.Blocks {
   172  		propSpec, propDiags := decodeSpecBlock(block)
   173  		diags = append(diags, propDiags...)
   174  		spec[block.Labels[0]] = propSpec
   175  	}
   176  
   177  	return spec, diags
   178  }
   179  
   180  func decodeArraySpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
   181  	content, diags := body.Content(specSchemaUnlabelled)
   182  
   183  	spec := make(hcldec.TupleSpec, 0, len(content.Blocks))
   184  	for _, block := range content.Blocks {
   185  		elemSpec, elemDiags := decodeSpecBlock(block)
   186  		diags = append(diags, elemDiags...)
   187  		spec = append(spec, elemSpec)
   188  	}
   189  
   190  	return spec, diags
   191  }
   192  
   193  func decodeAttrSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
   194  	type content struct {
   195  		Name     *string        `hcl:"name"`
   196  		Type     hcl.Expression `hcl:"type"`
   197  		Required *bool          `hcl:"required"`
   198  	}
   199  
   200  	var args content
   201  	diags := gohcl.DecodeBody(body, nil, &args)
   202  	if diags.HasErrors() {
   203  		return errSpec, diags
   204  	}
   205  
   206  	spec := &hcldec.AttrSpec{
   207  		Name: impliedName,
   208  	}
   209  
   210  	if args.Required != nil {
   211  		spec.Required = *args.Required
   212  	}
   213  	if args.Name != nil {
   214  		spec.Name = *args.Name
   215  	}
   216  
   217  	var typeDiags hcl.Diagnostics
   218  	spec.Type, typeDiags = evalTypeExpr(args.Type)
   219  	diags = append(diags, typeDiags...)
   220  
   221  	if spec.Name == "" {
   222  		diags = append(diags, &hcl.Diagnostic{
   223  			Severity: hcl.DiagError,
   224  			Summary:  "Missing name in attribute spec",
   225  			Detail:   "The name attribute is required, to specify the attribute name that is expected in an input HCL file.",
   226  			Subject:  body.MissingItemRange().Ptr(),
   227  		})
   228  		return errSpec, diags
   229  	}
   230  
   231  	return spec, diags
   232  }
   233  
   234  func decodeBlockSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
   235  	type content struct {
   236  		TypeName *string  `hcl:"block_type"`
   237  		Required *bool    `hcl:"required"`
   238  		Nested   hcl.Body `hcl:",remain"`
   239  	}
   240  
   241  	var args content
   242  	diags := gohcl.DecodeBody(body, nil, &args)
   243  	if diags.HasErrors() {
   244  		return errSpec, diags
   245  	}
   246  
   247  	spec := &hcldec.BlockSpec{
   248  		TypeName: impliedName,
   249  	}
   250  
   251  	if args.Required != nil {
   252  		spec.Required = *args.Required
   253  	}
   254  	if args.TypeName != nil {
   255  		spec.TypeName = *args.TypeName
   256  	}
   257  
   258  	nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
   259  	diags = append(diags, nestedDiags...)
   260  	spec.Nested = nested
   261  
   262  	return spec, diags
   263  }
   264  
   265  func decodeBlockListSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
   266  	type content struct {
   267  		TypeName *string  `hcl:"block_type"`
   268  		MinItems *int     `hcl:"min_items"`
   269  		MaxItems *int     `hcl:"max_items"`
   270  		Nested   hcl.Body `hcl:",remain"`
   271  	}
   272  
   273  	var args content
   274  	diags := gohcl.DecodeBody(body, nil, &args)
   275  	if diags.HasErrors() {
   276  		return errSpec, diags
   277  	}
   278  
   279  	spec := &hcldec.BlockListSpec{
   280  		TypeName: impliedName,
   281  	}
   282  
   283  	if args.MinItems != nil {
   284  		spec.MinItems = *args.MinItems
   285  	}
   286  	if args.MaxItems != nil {
   287  		spec.MaxItems = *args.MaxItems
   288  	}
   289  	if args.TypeName != nil {
   290  		spec.TypeName = *args.TypeName
   291  	}
   292  
   293  	nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
   294  	diags = append(diags, nestedDiags...)
   295  	spec.Nested = nested
   296  
   297  	if spec.TypeName == "" {
   298  		diags = append(diags, &hcl.Diagnostic{
   299  			Severity: hcl.DiagError,
   300  			Summary:  "Missing block_type in block_list spec",
   301  			Detail:   "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.",
   302  			Subject:  body.MissingItemRange().Ptr(),
   303  		})
   304  		return errSpec, diags
   305  	}
   306  
   307  	return spec, diags
   308  }
   309  
   310  func decodeBlockSetSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
   311  	type content struct {
   312  		TypeName *string  `hcl:"block_type"`
   313  		MinItems *int     `hcl:"min_items"`
   314  		MaxItems *int     `hcl:"max_items"`
   315  		Nested   hcl.Body `hcl:",remain"`
   316  	}
   317  
   318  	var args content
   319  	diags := gohcl.DecodeBody(body, nil, &args)
   320  	if diags.HasErrors() {
   321  		return errSpec, diags
   322  	}
   323  
   324  	spec := &hcldec.BlockSetSpec{
   325  		TypeName: impliedName,
   326  	}
   327  
   328  	if args.MinItems != nil {
   329  		spec.MinItems = *args.MinItems
   330  	}
   331  	if args.MaxItems != nil {
   332  		spec.MaxItems = *args.MaxItems
   333  	}
   334  	if args.TypeName != nil {
   335  		spec.TypeName = *args.TypeName
   336  	}
   337  
   338  	nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
   339  	diags = append(diags, nestedDiags...)
   340  	spec.Nested = nested
   341  
   342  	if spec.TypeName == "" {
   343  		diags = append(diags, &hcl.Diagnostic{
   344  			Severity: hcl.DiagError,
   345  			Summary:  "Missing block_type in block_set spec",
   346  			Detail:   "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.",
   347  			Subject:  body.MissingItemRange().Ptr(),
   348  		})
   349  		return errSpec, diags
   350  	}
   351  
   352  	return spec, diags
   353  }
   354  
   355  func decodeBlockMapSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
   356  	type content struct {
   357  		TypeName *string  `hcl:"block_type"`
   358  		Labels   []string `hcl:"labels"`
   359  		Nested   hcl.Body `hcl:",remain"`
   360  	}
   361  
   362  	var args content
   363  	diags := gohcl.DecodeBody(body, nil, &args)
   364  	if diags.HasErrors() {
   365  		return errSpec, diags
   366  	}
   367  
   368  	spec := &hcldec.BlockMapSpec{
   369  		TypeName: impliedName,
   370  	}
   371  
   372  	if args.TypeName != nil {
   373  		spec.TypeName = *args.TypeName
   374  	}
   375  	spec.LabelNames = args.Labels
   376  
   377  	nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
   378  	diags = append(diags, nestedDiags...)
   379  	spec.Nested = nested
   380  
   381  	if spec.TypeName == "" {
   382  		diags = append(diags, &hcl.Diagnostic{
   383  			Severity: hcl.DiagError,
   384  			Summary:  "Missing block_type in block_map spec",
   385  			Detail:   "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.",
   386  			Subject:  body.MissingItemRange().Ptr(),
   387  		})
   388  		return errSpec, diags
   389  	}
   390  	if len(spec.LabelNames) < 1 {
   391  		diags = append(diags, &hcl.Diagnostic{
   392  			Severity: hcl.DiagError,
   393  			Summary:  "Invalid block label name list",
   394  			Detail:   "A block_map must have at least one label specified.",
   395  			Subject:  body.MissingItemRange().Ptr(),
   396  		})
   397  		return errSpec, diags
   398  	}
   399  
   400  	if hcldec.ImpliedType(spec).HasDynamicTypes() {
   401  		diags = append(diags, &hcl.Diagnostic{
   402  			Severity: hcl.DiagError,
   403  			Summary:  "Invalid block_map spec",
   404  			Detail:   "A block_map spec may not contain attributes with type 'any'.",
   405  			Subject:  body.MissingItemRange().Ptr(),
   406  		})
   407  	}
   408  
   409  	return spec, diags
   410  }
   411  
   412  func decodeBlockNestedSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
   413  	content, diags := body.Content(specSchemaUnlabelled)
   414  
   415  	if len(content.Blocks) == 0 {
   416  		if diags.HasErrors() {
   417  			// If we already have errors then they probably explain
   418  			// why we have no blocks, so we'll skip our additional
   419  			// error message added below.
   420  			return errSpec, diags
   421  		}
   422  
   423  		diags = append(diags, &hcl.Diagnostic{
   424  			Severity: hcl.DiagError,
   425  			Summary:  "Missing spec block",
   426  			Detail:   "A block spec must have exactly one child spec specifying how to decode block contents.",
   427  			Subject:  body.MissingItemRange().Ptr(),
   428  		})
   429  		return errSpec, diags
   430  	}
   431  
   432  	if len(content.Blocks) > 1 {
   433  		diags = append(diags, &hcl.Diagnostic{
   434  			Severity: hcl.DiagError,
   435  			Summary:  "Extraneous spec block",
   436  			Detail:   "A block spec must have exactly one child spec specifying how to decode block contents.",
   437  			Subject:  &content.Blocks[1].DefRange,
   438  		})
   439  		return errSpec, diags
   440  	}
   441  
   442  	spec, specDiags := decodeSpecBlock(content.Blocks[0])
   443  	diags = append(diags, specDiags...)
   444  	return spec, diags
   445  }
   446  
   447  func decodeBlockAttrsSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
   448  	type content struct {
   449  		TypeName    *string        `hcl:"block_type"`
   450  		ElementType hcl.Expression `hcl:"element_type"`
   451  		Required    *bool          `hcl:"required"`
   452  	}
   453  
   454  	var args content
   455  	diags := gohcl.DecodeBody(body, nil, &args)
   456  	if diags.HasErrors() {
   457  		return errSpec, diags
   458  	}
   459  
   460  	spec := &hcldec.BlockAttrsSpec{
   461  		TypeName: impliedName,
   462  	}
   463  
   464  	if args.Required != nil {
   465  		spec.Required = *args.Required
   466  	}
   467  	if args.TypeName != nil {
   468  		spec.TypeName = *args.TypeName
   469  	}
   470  
   471  	var typeDiags hcl.Diagnostics
   472  	spec.ElementType, typeDiags = evalTypeExpr(args.ElementType)
   473  	diags = append(diags, typeDiags...)
   474  
   475  	if spec.TypeName == "" {
   476  		diags = append(diags, &hcl.Diagnostic{
   477  			Severity: hcl.DiagError,
   478  			Summary:  "Missing block_type in block_attrs spec",
   479  			Detail:   "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.",
   480  			Subject:  body.MissingItemRange().Ptr(),
   481  		})
   482  		return errSpec, diags
   483  	}
   484  
   485  	return spec, diags
   486  }
   487  
   488  func decodeLiteralSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
   489  	type content struct {
   490  		Value cty.Value `hcl:"value"`
   491  	}
   492  
   493  	var args content
   494  	diags := gohcl.DecodeBody(body, specCtx, &args)
   495  	if diags.HasErrors() {
   496  		return errSpec, diags
   497  	}
   498  
   499  	return &hcldec.LiteralSpec{
   500  		Value: args.Value,
   501  	}, diags
   502  }
   503  
   504  func decodeDefaultSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
   505  	content, diags := body.Content(specSchemaUnlabelled)
   506  
   507  	if len(content.Blocks) == 0 {
   508  		if diags.HasErrors() {
   509  			// If we already have errors then they probably explain
   510  			// why we have no blocks, so we'll skip our additional
   511  			// error message added below.
   512  			return errSpec, diags
   513  		}
   514  
   515  		diags = append(diags, &hcl.Diagnostic{
   516  			Severity: hcl.DiagError,
   517  			Summary:  "Missing spec block",
   518  			Detail:   "A default block must have at least one nested spec, each specifying a possible outcome.",
   519  			Subject:  body.MissingItemRange().Ptr(),
   520  		})
   521  		return errSpec, diags
   522  	}
   523  
   524  	if len(content.Blocks) == 1 && !diags.HasErrors() {
   525  		diags = append(diags, &hcl.Diagnostic{
   526  			Severity: hcl.DiagWarning,
   527  			Summary:  "Useless default block",
   528  			Detail:   "A default block with only one spec is equivalent to using that spec alone.",
   529  			Subject:  &content.Blocks[1].DefRange,
   530  		})
   531  	}
   532  
   533  	var spec hcldec.Spec
   534  	for _, block := range content.Blocks {
   535  		candidateSpec, candidateDiags := decodeSpecBlock(block)
   536  		diags = append(diags, candidateDiags...)
   537  		if candidateDiags.HasErrors() {
   538  			continue
   539  		}
   540  
   541  		if spec == nil {
   542  			spec = candidateSpec
   543  		} else {
   544  			spec = &hcldec.DefaultSpec{
   545  				Primary: spec,
   546  				Default: candidateSpec,
   547  			}
   548  		}
   549  	}
   550  
   551  	return spec, diags
   552  }
   553  
   554  func decodeTransformSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
   555  	type content struct {
   556  		Result hcl.Expression `hcl:"result"`
   557  		Nested hcl.Body       `hcl:",remain"`
   558  	}
   559  
   560  	var args content
   561  	diags := gohcl.DecodeBody(body, nil, &args)
   562  	if diags.HasErrors() {
   563  		return errSpec, diags
   564  	}
   565  
   566  	spec := &hcldec.TransformExprSpec{
   567  		Expr:         args.Result,
   568  		VarName:      "nested",
   569  		TransformCtx: specCtx,
   570  	}
   571  
   572  	nestedContent, nestedDiags := args.Nested.Content(specSchemaUnlabelled)
   573  	diags = append(diags, nestedDiags...)
   574  
   575  	if len(nestedContent.Blocks) != 1 {
   576  		if nestedDiags.HasErrors() {
   577  			// If we already have errors then they probably explain
   578  			// why we have the wrong number of blocks, so we'll skip our
   579  			// additional error message added below.
   580  			return errSpec, diags
   581  		}
   582  
   583  		diags = append(diags, &hcl.Diagnostic{
   584  			Severity: hcl.DiagError,
   585  			Summary:  "Invalid transform spec",
   586  			Detail:   "A transform spec block must have exactly one nested spec block.",
   587  			Subject:  body.MissingItemRange().Ptr(),
   588  		})
   589  		return errSpec, diags
   590  	}
   591  
   592  	nestedSpec, nestedDiags := decodeSpecBlock(nestedContent.Blocks[0])
   593  	diags = append(diags, nestedDiags...)
   594  	spec.Wrapped = nestedSpec
   595  
   596  	return spec, diags
   597  }
   598  
   599  var errSpec = &hcldec.LiteralSpec{
   600  	Value: cty.NullVal(cty.DynamicPseudoType),
   601  }
   602  
   603  var specBlockTypes = []string{
   604  	"object",
   605  	"array",
   606  
   607  	"literal",
   608  
   609  	"attr",
   610  
   611  	"block",
   612  	"block_list",
   613  	"block_map",
   614  	"block_set",
   615  
   616  	"default",
   617  	"transform",
   618  }
   619  
   620  var specSchemaUnlabelled *hcl.BodySchema
   621  var specSchemaLabelled *hcl.BodySchema
   622  
   623  var specSchemaLabelledLabels = []string{"key"}
   624  
   625  func init() {
   626  	specSchemaLabelled = &hcl.BodySchema{
   627  		Blocks: make([]hcl.BlockHeaderSchema, 0, len(specBlockTypes)),
   628  	}
   629  	specSchemaUnlabelled = &hcl.BodySchema{
   630  		Blocks: make([]hcl.BlockHeaderSchema, 0, len(specBlockTypes)),
   631  	}
   632  
   633  	for _, name := range specBlockTypes {
   634  		specSchemaLabelled.Blocks = append(
   635  			specSchemaLabelled.Blocks,
   636  			hcl.BlockHeaderSchema{
   637  				Type:       name,
   638  				LabelNames: specSchemaLabelledLabels,
   639  			},
   640  		)
   641  		specSchemaUnlabelled.Blocks = append(
   642  			specSchemaUnlabelled.Blocks,
   643  			hcl.BlockHeaderSchema{
   644  				Type: name,
   645  			},
   646  		)
   647  	}
   648  }