Skip to content

Commit

Permalink
[Ch4140] add output block (#29)
Browse files Browse the repository at this point in the history
* [ch4440] fix: skip newlines on template indentation function

* [ch4140] feat: support `output` blocks

* write outputs to markdown!

* add resource type to expr eval context

* differentiate between var/args and output ctx functions
  • Loading branch information
Nathan Thiesen authored Jan 18, 2022
1 parent 5c0d66c commit 1c0dcb6
Show file tree
Hide file tree
Showing 21 changed files with 511 additions and 64 deletions.
11 changes: 11 additions & 0 deletions internal/entities/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package entities

// Output represents an `output` block from the input file.
type Output struct {
// Name as defined in the `output` block label.
Name string `json:"name"`
// Type is a type definition for the output
Type Type `json:"type_definition"`
// Description is an optional output description
Description string `json:"description,omitempty"`
}
2 changes: 2 additions & 0 deletions internal/entities/section.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type Section struct {
Content string `json:"content,omitempty"`
// Variables is a collection of variable definitions contained in the section block.
Variables []Variable `json:"variables,omitempty"`
// Ouputs is a collection of output definitions contained in the section block.
Outputs []Output `json:"outputs,omitempty"`
// SubSections is a collection of nested sections contained in the section block.
SubSections []Section `json:"subsections,omitempty"`
// Level is the nesting of this section
Expand Down
4 changes: 2 additions & 2 deletions internal/parser/hclparser/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ func createAttributeFromHCLAttributes(attrs hcl.Attributes, name string, level i
// type definition
readmeType := getAttribute(attrs, readmeTypeAttributeName)
if !readmeType.isNil() {
attr.Type, err = readmeType.TypeFromString()
attr.Type, err = readmeType.VarTypeFromString()
} else {
attr.Type, err = getAttribute(attrs, typeAttributeName).Type()
attr.Type, err = getAttribute(attrs, typeAttributeName).VarType()
}

if err != nil {
Expand Down
16 changes: 12 additions & 4 deletions internal/parser/hclparser/hclattribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,23 @@ func (a *hclAttribute) RawJSON() (json.RawMessage, error) {
return json.RawMessage(src), nil
}

func (a *hclAttribute) Type() (entities.Type, error) {
func (a *hclAttribute) VarType() (entities.Type, error) {
if a.isNil() {
return entities.Type{}, nil
}

return getTypeFromExpression(a.Expr)
return getTypeFromExpression(a.Expr, varFunctions)
}

func (a *hclAttribute) TypeFromString() (entities.Type, error) {
func (a *hclAttribute) OutputType() (entities.Type, error) {
if a.isNil() {
return entities.Type{}, nil
}

return getTypeFromExpression(a.Expr, outputFunctions)
}

func (a *hclAttribute) VarTypeFromString() (entities.Type, error) {
if a.isNil() {
return entities.Type{}, nil
}
Expand All @@ -102,7 +110,7 @@ func (a *hclAttribute) TypeFromString() (entities.Type, error) {
return entities.Type{}, fmt.Errorf("could not fetch type string value for %q: %v", a.Name, diags.Errs())
}

return getTypeFromString(val.AsString())
return getTypeFromString(val.AsString(), varFunctions)
}

func getRawVariables(expr hcl.Expression) json.RawMessage {
Expand Down
4 changes: 2 additions & 2 deletions internal/parser/hclparser/hclattribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func TestAttributeToTypeValidPrimaryType(t *testing.T) {
t.Run(tt.desc, func(t *testing.T) {
attr := newTypeAttribute(tt.exprValue, tt.exprValue)

res, err := attr.Type()
res, err := attr.VarType()
assert.NoError(t, err)

assert.EqualInts(t, int(tt.expectedTerraformType), int(res.TFType))
Expand Down Expand Up @@ -197,7 +197,7 @@ func TestAttributeToTypeInvalidTypes(t *testing.T) {
t.Run(tt.desc, func(t *testing.T) {
attr := newTypeAttribute(tt.exprValue, tt.exprValue)

res, err := attr.Type()
res, err := attr.VarType()
assert.Error(t, err)

if !strings.Contains(err.Error(), tt.expectedErrorMSG) {
Expand Down
1 change: 1 addition & 0 deletions internal/parser/hclparser/hclparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
refBlockName = "ref"
headerBlockName = "header"
badgeBlockName = "badge"
outputBlockName = "output"
)

// Parse reads the content of a io.Reader and returns a Definition entity from its parsed values
Expand Down
83 changes: 83 additions & 0 deletions internal/parser/hclparser/hclparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,44 @@ Section contents support anything markdown and allow us to make references like
},
},
},
{
Level: 3,
Title: "Outputs!",
Outputs: []entities.Output{
{
Name: "obj_output",
Description: "an example object",
Type: entities.Type{
TFType: types.TerraformObject,
TFTypeLabel: "an_object_label",
},
},
{
Name: "string_output",
Description: "a string",
Type: entities.Type{
TFType: types.TerraformString,
},
},
{
Name: "list_output",
Description: "a list of example objects",
Type: entities.Type{
TFType: types.TerraformList,
NestedTFType: types.TerraformObject,
NestedTFTypeLabel: "example",
},
},
{
Name: "resource_output",
Description: "a resource output",
Type: entities.Type{
TFType: types.TerraformResource,
TFTypeLabel: "google_xxxx",
},
},
},
},
},
},
},
Expand Down Expand Up @@ -301,6 +339,7 @@ func assertSectionEquals(t *testing.T, want, got entities.Section) {
assert.EqualInts(t, want.Level, got.Level)

assertEqualVariables(t, want.Variables, got.Variables)
assertEqualOutputs(t, want.Outputs, got.Outputs)
assertEqualSections(t, want.SubSections, got.SubSections)
}

Expand Down Expand Up @@ -396,3 +435,47 @@ func assertAttributeEquals(t *testing.T, want, got entities.Attribute) {

assertEqualAttributes(t, want.Attributes, got.Attributes)
}

func assertEqualOutputs(t *testing.T, want, got []entities.Output) {
t.Helper()

assert.EqualInts(t, len(want), len(got))

if len(got) == 0 {
return
}

for _, output := range want {
assertContainsOutput(t, got, output)
}
}

func assertContainsOutput(t *testing.T, outputsList []entities.Output, want entities.Output) {
t.Helper()

var found bool
for _, output := range outputsList {
if output.Name == want.Name {
found = true

assertOutputEquals(t, want, output)
}
}

if !found {
t.Errorf("Expected outputs list to contain %q but didn't find one", want.Name)
}
}

func assertOutputEquals(t *testing.T, want, got entities.Output) {
t.Helper()

// redundant since we're finding the output by name
assert.EqualStrings(t, want.Name, got.Name)
assert.EqualStrings(t, want.Description, got.Description)
assert.EqualStrings(t, want.Type.TFType.String(), got.Type.TFType.String())
assert.EqualStrings(t, want.Type.TFTypeLabel, got.Type.TFTypeLabel)

assert.EqualStrings(t, want.Type.NestedTFType.String(), got.Type.NestedTFType.String())
assert.EqualStrings(t, want.Type.NestedTFTypeLabel, got.Type.NestedTFTypeLabel)
}
19 changes: 19 additions & 0 deletions internal/parser/hclparser/hclschema/hclschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ func SectionSchema() *hcl.BodySchema {
Type: "variable",
LabelNames: []string{"name"},
},
{
Type: "output",
LabelNames: []string{"name"},
},
},
}
}
Expand Down Expand Up @@ -153,6 +157,21 @@ func VariableSchema() *hcl.BodySchema {
}
}

func OutputSchema() *hcl.BodySchema {
return &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "type",
Required: true,
},
{
Name: "description",
Required: false,
},
},
}
}

func AttributeSchema() *hcl.BodySchema {
return &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
Expand Down
46 changes: 30 additions & 16 deletions internal/parser/hclparser/hcltype.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ var typeObj = map[string]cty.Type{
"nestedTypeLabel": cty.String,
}

var varFunctions, outputFunctions map[string]function.Function

func nestedTypeFunc(tfType types.TerraformType) function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
Expand Down Expand Up @@ -78,8 +80,8 @@ func complexTypeFunc(tfType types.TerraformType) function.Function {
})
}

func getComplexType(expr hcl.Expression) (entities.Type, error) {
got, exprDiags := expr.Value(getEvalContextForExpr(expr))
func getComplexType(expr hcl.Expression, ctxFunctions map[string]function.Function) (entities.Type, error) {
got, exprDiags := expr.Value(getEvalContextForExpr(expr, ctxFunctions))
if exprDiags.HasErrors() {
return entities.Type{}, fmt.Errorf("getting expression value: %v", exprDiags.Errs())
}
Expand Down Expand Up @@ -115,7 +117,7 @@ func getComplexType(expr hcl.Expression) (entities.Type, error) {
}, nil
}

func getTypeFromExpression(expr hcl.Expression) (entities.Type, error) {
func getTypeFromExpression(expr hcl.Expression, ctxFunctions map[string]function.Function) (entities.Type, error) {
kw := hcl.ExprAsKeyword(expr)

switch kw {
Expand All @@ -134,39 +136,51 @@ func getTypeFromExpression(expr hcl.Expression) (entities.Type, error) {
return entities.Type{}, fmt.Errorf("type %q is invalid", kw)
}

return getComplexType(expr)
return getComplexType(expr, ctxFunctions)
}

// this function exists to make it possible to parse `type` attribute expressions and `readme_type`
// attribute strings in the same way, so they are compatible even though they have different types
func getTypeFromString(str string) (entities.Type, error) {
func getTypeFromString(str string, ctxFunctions map[string]function.Function) (entities.Type, error) {
expr, parseDiags := hclsyntax.ParseExpression([]byte(str), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})
if parseDiags.HasErrors() {
return entities.Type{}, fmt.Errorf("parsing type string expression: %v", parseDiags.Errs())
}

return getTypeFromExpression(expr)
return getTypeFromExpression(expr, ctxFunctions)
}

func getVariablesMap(expr hcl.Expression) map[string]cty.Value {
myMap := make(map[string]cty.Value)
varMap := make(map[string]cty.Value)
for _, variable := range expr.Variables() {
name := variable.RootName()

myMap[name] = cty.StringVal(name)
varMap[name] = cty.StringVal(name)
}

return myMap
return varMap
}

func getEvalContextForExpr(expr hcl.Expression) *hcl.EvalContext {
func getEvalContextForExpr(expr hcl.Expression, ctxFunctions map[string]function.Function) *hcl.EvalContext {
return &hcl.EvalContext{
Functions: map[string]function.Function{
"object": complexTypeFunc(types.TerraformObject),
"map": nestedTypeFunc(types.TerraformMap),
"list": nestedTypeFunc(types.TerraformList),
"set": nestedTypeFunc(types.TerraformSet),
},
Functions: ctxFunctions,
Variables: getVariablesMap(expr),
}
}

func init() {
varFunctions = map[string]function.Function{
"object": complexTypeFunc(types.TerraformObject),
"map": nestedTypeFunc(types.TerraformMap),
"list": nestedTypeFunc(types.TerraformList),
"set": nestedTypeFunc(types.TerraformSet),
}

outputFunctions = map[string]function.Function{
"resource": complexTypeFunc(types.TerraformResource),
"object": complexTypeFunc(types.TerraformObject),
"map": nestedTypeFunc(types.TerraformMap),
"list": nestedTypeFunc(types.TerraformList),
"set": nestedTypeFunc(types.TerraformSet),
}
}
Loading

0 comments on commit 1c0dcb6

Please sign in to comment.