github.com/mineiros-io/terradoc@v0.0.9-0.20220711062319-018bd4ae81f5/internal/parsers/validationparser/validationparser.go (about)

     1  package validationparser
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/hclparse"
    11  	"github.com/mineiros-io/terradoc/internal/entities"
    12  	"github.com/mineiros-io/terradoc/internal/parsers/hclparser"
    13  	"github.com/mineiros-io/terradoc/internal/schemas/validationschema"
    14  	"github.com/mineiros-io/terradoc/internal/schemas/varsschema"
    15  )
    16  
    17  func Parse(r io.Reader, filename string, variablesEnabled bool, outputsEnabled bool) (entities.ValidationContents, error) {
    18  	src, err := io.ReadAll(r)
    19  	if err != nil {
    20  		return entities.ValidationContents{}, err
    21  	}
    22  
    23  	return parseContentHCL(src, filename, variablesEnabled, outputsEnabled)
    24  }
    25  
    26  func parseContentHCL(src []byte, filename string, variablesEnabled bool, outputsEnabled bool) (entities.ValidationContents, error) {
    27  	p := hclparse.NewParser()
    28  	validationContents := entities.ValidationContents{}
    29  
    30  	f, diags := p.ParseHCL(src, filename)
    31  	if diags.HasErrors() {
    32  		// Only return errors relevant to parsing of variables or outputs
    33  		var errors []error
    34  		for _, e := range diags.Errs() {
    35  			if variablesEnabled && strings.Contains(e.Error(), "variable") {
    36  				errors = append(errors, e)
    37  			}
    38  
    39  			if outputsEnabled && strings.Contains(e.Error(), "output") {
    40  				errors = append(errors, e)
    41  			}
    42  		}
    43  
    44  		if len(errors) > 0 {
    45  			return entities.ValidationContents{}, fmt.Errorf("parsing HCL: %v", errors)
    46  		}
    47  
    48  	}
    49  
    50  	// Ignore errors, only focus on content to ignore non outputs/variables
    51  	content, _ := f.Body.Content(validationschema.RootSchema())
    52  
    53  	if variablesEnabled {
    54  		variables, err := parseVariables(content.Blocks.OfType("variable"))
    55  		if err != nil {
    56  			return entities.ValidationContents{}, fmt.Errorf("parsing variables: %v", err)
    57  		}
    58  		validationContents.Variables = variables
    59  	}
    60  
    61  	if outputsEnabled {
    62  		outputs, err := parseOutputs(content.Blocks.OfType("output"))
    63  		if err != nil {
    64  			return entities.ValidationContents{}, fmt.Errorf("parsing outputs: %v", err)
    65  		}
    66  		validationContents.Outputs = outputs
    67  	}
    68  
    69  	return validationContents, nil
    70  }
    71  
    72  func parseVariables(variableBlocks hcl.Blocks) (variables []entities.Variable, err error) {
    73  	for _, varBlk := range variableBlocks {
    74  		variable, err := parseVariable(varBlk)
    75  		if err != nil {
    76  			return nil, fmt.Errorf("parsing variable: %s", err)
    77  		}
    78  
    79  		variables = append(variables, variable)
    80  	}
    81  
    82  	return variables, nil
    83  }
    84  
    85  func parseVariable(variableBlock *hcl.Block) (entities.Variable, error) {
    86  	if len(variableBlock.Labels) != 1 {
    87  		return entities.Variable{}, errors.New("variable block must have a single label")
    88  	}
    89  
    90  	variableContent, diags := variableBlock.Body.Content(varsschema.VariableSchema())
    91  	if diags.HasErrors() && len(variableContent.Attributes) != 1 {
    92  		var errors []error
    93  		for _, e := range diags.Errs() {
    94  			// Only return if parsing error is relevant to `type`
    95  			if strings.Contains(e.Error(), "type") {
    96  				errors = append(errors, e)
    97  			}
    98  		}
    99  
   100  		if len(errors) > 0 {
   101  			return entities.Variable{}, fmt.Errorf("parsing variable: %v", errors)
   102  		}
   103  	}
   104  
   105  	// variable blocks are required to have a label as defined in the schema
   106  	name := variableBlock.Labels[0]
   107  	variable, err := createVariableFromHCLAttributes(variableContent.Attributes, name)
   108  	if err != nil {
   109  		return entities.Variable{}, fmt.Errorf("parsing variable: %s", err)
   110  	}
   111  
   112  	return variable, nil
   113  }
   114  
   115  func createVariableFromHCLAttributes(attrs hcl.Attributes, name string) (entities.Variable, error) {
   116  	var err error
   117  
   118  	variable := entities.Variable{Name: name}
   119  
   120  	variable.Default, err = hclparser.GetAttribute(attrs, "default").RawJSON()
   121  	if err != nil {
   122  		return entities.Variable{}, err
   123  	}
   124  
   125  	// type definition
   126  	variable.Type, err = hclparser.GetAttribute(attrs, "type").VarType()
   127  	if err != nil {
   128  		return entities.Variable{}, err
   129  	}
   130  
   131  	return variable, nil
   132  }
   133  
   134  func parseOutputs(outputBlocks hcl.Blocks) (outputs []entities.Output, err error) {
   135  	for _, outBlk := range outputBlocks {
   136  		output, err := parseOutput(outBlk)
   137  		if err != nil {
   138  			return nil, fmt.Errorf("parsing output: %s", err)
   139  		}
   140  
   141  		outputs = append(outputs, output)
   142  	}
   143  
   144  	return outputs, nil
   145  }
   146  
   147  func parseOutput(outputBlock *hcl.Block) (entities.Output, error) {
   148  	if len(outputBlock.Labels) != 1 {
   149  		return entities.Output{}, errors.New("output block must have a single label")
   150  	}
   151  
   152  	// output blocks are required to have a label as defined in the schema
   153  	name := outputBlock.Labels[0]
   154  	output := entities.Output{Name: name}
   155  
   156  	return output, nil
   157  }