github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/hclext/schema.go (about)

     1  package hclext
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"sort"
     7  	"strings"
     8  )
     9  
    10  // SchemaMode controls how the body's schema is declared.
    11  //
    12  //go:generate stringer -type=SchemaMode
    13  type SchemaMode int32
    14  
    15  const (
    16  	// SchemaDefaultMode is a mode for explicitly declaring the structure of attributes and blocks.
    17  	SchemaDefaultMode SchemaMode = iota
    18  	// SchemaJustAttributesMode is the mode to extract body as attributes.
    19  	// In this mode you don't need to declare schema for attributes or blocks.
    20  	SchemaJustAttributesMode
    21  )
    22  
    23  // BodySchema represents the desired body.
    24  // This structure is designed to have attributes similar to hcl.BodySchema.
    25  type BodySchema struct {
    26  	Mode       SchemaMode
    27  	Attributes []AttributeSchema
    28  	Blocks     []BlockSchema
    29  }
    30  
    31  // AttributeSchema represents the desired attribute.
    32  // This structure is designed to have attributes similar to hcl.AttributeSchema.
    33  type AttributeSchema struct {
    34  	Name     string
    35  	Required bool
    36  }
    37  
    38  // BlockSchema represents the desired block header and body schema.
    39  // Unlike hcl.BlockHeaderSchema, this can set nested body schema.
    40  // Instead, hclext.Block can't handle abstract values like hcl.Body,
    41  // so you need to specify all nested schemas at once.
    42  type BlockSchema struct {
    43  	Type       string
    44  	LabelNames []string
    45  
    46  	Body *BodySchema
    47  }
    48  
    49  // ImpliedBodySchema is a derivative of gohcl.ImpliedBodySchema that produces hclext.BodySchema instead of hcl.BodySchema.
    50  // Unlike gohcl.ImpliedBodySchema, it produces nested schemas.
    51  // This method differs from gohcl.DecodeBody in several ways:
    52  //
    53  // - Does not support `body` and `remain` tags.
    54  // - Does not support partial schema.
    55  //
    56  // @see https://github.com/hashicorp/hcl/blob/v2.11.1/gohcl/schema.go
    57  func ImpliedBodySchema(val interface{}) *BodySchema {
    58  	return impliedBodySchema(reflect.TypeOf(val))
    59  }
    60  
    61  func impliedBodySchema(ty reflect.Type) *BodySchema {
    62  	if ty.Kind() == reflect.Ptr {
    63  		ty = ty.Elem()
    64  	}
    65  
    66  	if ty.Kind() != reflect.Struct {
    67  		panic(fmt.Sprintf("given type must be struct, not %s", ty.Name()))
    68  	}
    69  
    70  	var attrSchemas []AttributeSchema
    71  	var blockSchemas []BlockSchema
    72  
    73  	tags := getFieldTags(ty)
    74  
    75  	attrNames := make([]string, 0, len(tags.Attributes))
    76  	for n := range tags.Attributes {
    77  		attrNames = append(attrNames, n)
    78  	}
    79  	sort.Strings(attrNames)
    80  	for _, n := range attrNames {
    81  		idx := tags.Attributes[n]
    82  		optional := tags.Optional[n]
    83  		field := ty.Field(idx)
    84  
    85  		var required bool
    86  
    87  		switch {
    88  		case field.Type.Kind() != reflect.Ptr && !optional:
    89  			required = true
    90  		default:
    91  			required = false
    92  		}
    93  
    94  		attrSchemas = append(attrSchemas, AttributeSchema{
    95  			Name:     n,
    96  			Required: required,
    97  		})
    98  	}
    99  
   100  	blockNames := make([]string, 0, len(tags.Blocks))
   101  	for n := range tags.Blocks {
   102  		blockNames = append(blockNames, n)
   103  	}
   104  	sort.Strings(blockNames)
   105  	for _, n := range blockNames {
   106  		idx := tags.Blocks[n]
   107  		field := ty.Field(idx)
   108  		fty := field.Type
   109  		if fty.Kind() == reflect.Slice {
   110  			fty = fty.Elem()
   111  		}
   112  		if fty.Kind() == reflect.Ptr {
   113  			fty = fty.Elem()
   114  		}
   115  		if fty.Kind() != reflect.Struct {
   116  			panic(fmt.Sprintf(
   117  				"schema 'block' tag kind cannot be applied to %s field %s: struct required", field.Type.String(), field.Name,
   118  			))
   119  		}
   120  		ftags := getFieldTags(fty)
   121  		var labelNames []string
   122  		if len(ftags.Labels) > 0 {
   123  			labelNames = make([]string, len(ftags.Labels))
   124  			for i, l := range ftags.Labels {
   125  				labelNames[i] = l.Name
   126  			}
   127  		}
   128  
   129  		blockSchemas = append(blockSchemas, BlockSchema{
   130  			Type:       n,
   131  			LabelNames: labelNames,
   132  			Body:       impliedBodySchema(fty),
   133  		})
   134  	}
   135  
   136  	return &BodySchema{
   137  		Attributes: attrSchemas,
   138  		Blocks:     blockSchemas,
   139  	}
   140  }
   141  
   142  type fieldTags struct {
   143  	Attributes map[string]int
   144  	Blocks     map[string]int
   145  	Labels     []labelField
   146  	Optional   map[string]bool
   147  }
   148  
   149  type labelField struct {
   150  	FieldIndex int
   151  	Name       string
   152  }
   153  
   154  func getFieldTags(ty reflect.Type) *fieldTags {
   155  	ret := &fieldTags{
   156  		Attributes: map[string]int{},
   157  		Blocks:     map[string]int{},
   158  		Optional:   map[string]bool{},
   159  	}
   160  
   161  	ct := ty.NumField()
   162  	for i := 0; i < ct; i++ {
   163  		field := ty.Field(i)
   164  		tag := field.Tag.Get("hclext")
   165  		if tag == "" {
   166  			continue
   167  		}
   168  
   169  		comma := strings.Index(tag, ",")
   170  		var name, kind string
   171  		if comma != -1 {
   172  			name = tag[:comma]
   173  			kind = tag[comma+1:]
   174  		} else {
   175  			name = tag
   176  			kind = "attr"
   177  		}
   178  
   179  		switch kind {
   180  		case "attr":
   181  			ret.Attributes[name] = i
   182  		case "block":
   183  			ret.Blocks[name] = i
   184  		case "label":
   185  			ret.Labels = append(ret.Labels, labelField{
   186  				FieldIndex: i,
   187  				Name:       name,
   188  			})
   189  		case "optional":
   190  			ret.Attributes[name] = i
   191  			ret.Optional[name] = true
   192  		case "remain", "body":
   193  			panic(fmt.Sprintf("'%s' tag is permitted in HCL, but not permitted in schema", kind))
   194  		default:
   195  			panic(fmt.Sprintf("invalid schema field tag kind %q on %s %q", kind, field.Type.String(), field.Name))
   196  		}
   197  	}
   198  
   199  	return ret
   200  }