Skip to content

Commit 1c0dcb6

Browse files
author
Nathan Thiesen
authored
[Ch4140] add output block (#29)
* [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
1 parent 5c0d66c commit 1c0dcb6

21 files changed

Lines changed: 511 additions & 64 deletions

internal/entities/output.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package entities
2+
3+
// Output represents an `output` block from the input file.
4+
type Output struct {
5+
// Name as defined in the `output` block label.
6+
Name string `json:"name"`
7+
// Type is a type definition for the output
8+
Type Type `json:"type_definition"`
9+
// Description is an optional output description
10+
Description string `json:"description,omitempty"`
11+
}

internal/entities/section.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ type Section struct {
88
Content string `json:"content,omitempty"`
99
// Variables is a collection of variable definitions contained in the section block.
1010
Variables []Variable `json:"variables,omitempty"`
11+
// Ouputs is a collection of output definitions contained in the section block.
12+
Outputs []Output `json:"outputs,omitempty"`
1113
// SubSections is a collection of nested sections contained in the section block.
1214
SubSections []Section `json:"subsections,omitempty"`
1315
// Level is the nesting of this section

internal/parser/hclparser/attributes.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ func createAttributeFromHCLAttributes(attrs hcl.Attributes, name string, level i
8181
// type definition
8282
readmeType := getAttribute(attrs, readmeTypeAttributeName)
8383
if !readmeType.isNil() {
84-
attr.Type, err = readmeType.TypeFromString()
84+
attr.Type, err = readmeType.VarTypeFromString()
8585
} else {
86-
attr.Type, err = getAttribute(attrs, typeAttributeName).Type()
86+
attr.Type, err = getAttribute(attrs, typeAttributeName).VarType()
8787
}
8888

8989
if err != nil {

internal/parser/hclparser/hclattribute.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,23 @@ func (a *hclAttribute) RawJSON() (json.RawMessage, error) {
8484
return json.RawMessage(src), nil
8585
}
8686

87-
func (a *hclAttribute) Type() (entities.Type, error) {
87+
func (a *hclAttribute) VarType() (entities.Type, error) {
8888
if a.isNil() {
8989
return entities.Type{}, nil
9090
}
9191

92-
return getTypeFromExpression(a.Expr)
92+
return getTypeFromExpression(a.Expr, varFunctions)
9393
}
9494

95-
func (a *hclAttribute) TypeFromString() (entities.Type, error) {
95+
func (a *hclAttribute) OutputType() (entities.Type, error) {
96+
if a.isNil() {
97+
return entities.Type{}, nil
98+
}
99+
100+
return getTypeFromExpression(a.Expr, outputFunctions)
101+
}
102+
103+
func (a *hclAttribute) VarTypeFromString() (entities.Type, error) {
96104
if a.isNil() {
97105
return entities.Type{}, nil
98106
}
@@ -102,7 +110,7 @@ func (a *hclAttribute) TypeFromString() (entities.Type, error) {
102110
return entities.Type{}, fmt.Errorf("could not fetch type string value for %q: %v", a.Name, diags.Errs())
103111
}
104112

105-
return getTypeFromString(val.AsString())
113+
return getTypeFromString(val.AsString(), varFunctions)
106114
}
107115

108116
func getRawVariables(expr hcl.Expression) json.RawMessage {

internal/parser/hclparser/hclattribute_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ func TestAttributeToTypeValidPrimaryType(t *testing.T) {
150150
t.Run(tt.desc, func(t *testing.T) {
151151
attr := newTypeAttribute(tt.exprValue, tt.exprValue)
152152

153-
res, err := attr.Type()
153+
res, err := attr.VarType()
154154
assert.NoError(t, err)
155155

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

200-
res, err := attr.Type()
200+
res, err := attr.VarType()
201201
assert.Error(t, err)
202202

203203
if !strings.Contains(err.Error(), tt.expectedErrorMSG) {

internal/parser/hclparser/hclparser.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const (
3333
refBlockName = "ref"
3434
headerBlockName = "header"
3535
badgeBlockName = "badge"
36+
outputBlockName = "output"
3637
)
3738

3839
// Parse reads the content of a io.Reader and returns a Definition entity from its parsed values

internal/parser/hclparser/hclparser_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,44 @@ Section contents support anything markdown and allow us to make references like
119119
},
120120
},
121121
},
122+
{
123+
Level: 3,
124+
Title: "Outputs!",
125+
Outputs: []entities.Output{
126+
{
127+
Name: "obj_output",
128+
Description: "an example object",
129+
Type: entities.Type{
130+
TFType: types.TerraformObject,
131+
TFTypeLabel: "an_object_label",
132+
},
133+
},
134+
{
135+
Name: "string_output",
136+
Description: "a string",
137+
Type: entities.Type{
138+
TFType: types.TerraformString,
139+
},
140+
},
141+
{
142+
Name: "list_output",
143+
Description: "a list of example objects",
144+
Type: entities.Type{
145+
TFType: types.TerraformList,
146+
NestedTFType: types.TerraformObject,
147+
NestedTFTypeLabel: "example",
148+
},
149+
},
150+
{
151+
Name: "resource_output",
152+
Description: "a resource output",
153+
Type: entities.Type{
154+
TFType: types.TerraformResource,
155+
TFTypeLabel: "google_xxxx",
156+
},
157+
},
158+
},
159+
},
122160
},
123161
},
124162
},
@@ -301,6 +339,7 @@ func assertSectionEquals(t *testing.T, want, got entities.Section) {
301339
assert.EqualInts(t, want.Level, got.Level)
302340

303341
assertEqualVariables(t, want.Variables, got.Variables)
342+
assertEqualOutputs(t, want.Outputs, got.Outputs)
304343
assertEqualSections(t, want.SubSections, got.SubSections)
305344
}
306345

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

397436
assertEqualAttributes(t, want.Attributes, got.Attributes)
398437
}
438+
439+
func assertEqualOutputs(t *testing.T, want, got []entities.Output) {
440+
t.Helper()
441+
442+
assert.EqualInts(t, len(want), len(got))
443+
444+
if len(got) == 0 {
445+
return
446+
}
447+
448+
for _, output := range want {
449+
assertContainsOutput(t, got, output)
450+
}
451+
}
452+
453+
func assertContainsOutput(t *testing.T, outputsList []entities.Output, want entities.Output) {
454+
t.Helper()
455+
456+
var found bool
457+
for _, output := range outputsList {
458+
if output.Name == want.Name {
459+
found = true
460+
461+
assertOutputEquals(t, want, output)
462+
}
463+
}
464+
465+
if !found {
466+
t.Errorf("Expected outputs list to contain %q but didn't find one", want.Name)
467+
}
468+
}
469+
470+
func assertOutputEquals(t *testing.T, want, got entities.Output) {
471+
t.Helper()
472+
473+
// redundant since we're finding the output by name
474+
assert.EqualStrings(t, want.Name, got.Name)
475+
assert.EqualStrings(t, want.Description, got.Description)
476+
assert.EqualStrings(t, want.Type.TFType.String(), got.Type.TFType.String())
477+
assert.EqualStrings(t, want.Type.TFTypeLabel, got.Type.TFTypeLabel)
478+
479+
assert.EqualStrings(t, want.Type.NestedTFType.String(), got.Type.NestedTFType.String())
480+
assert.EqualStrings(t, want.Type.NestedTFTypeLabel, got.Type.NestedTFTypeLabel)
481+
}

internal/parser/hclparser/hclschema/hclschema.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ func SectionSchema() *hcl.BodySchema {
108108
Type: "variable",
109109
LabelNames: []string{"name"},
110110
},
111+
{
112+
Type: "output",
113+
LabelNames: []string{"name"},
114+
},
111115
},
112116
}
113117
}
@@ -153,6 +157,21 @@ func VariableSchema() *hcl.BodySchema {
153157
}
154158
}
155159

160+
func OutputSchema() *hcl.BodySchema {
161+
return &hcl.BodySchema{
162+
Attributes: []hcl.AttributeSchema{
163+
{
164+
Name: "type",
165+
Required: true,
166+
},
167+
{
168+
Name: "description",
169+
Required: false,
170+
},
171+
},
172+
}
173+
}
174+
156175
func AttributeSchema() *hcl.BodySchema {
157176
return &hcl.BodySchema{
158177
Attributes: []hcl.AttributeSchema{

internal/parser/hclparser/hcltype.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ var typeObj = map[string]cty.Type{
2020
"nestedTypeLabel": cty.String,
2121
}
2222

23+
var varFunctions, outputFunctions map[string]function.Function
24+
2325
func nestedTypeFunc(tfType types.TerraformType) function.Function {
2426
return function.New(&function.Spec{
2527
Params: []function.Parameter{
@@ -78,8 +80,8 @@ func complexTypeFunc(tfType types.TerraformType) function.Function {
7880
})
7981
}
8082

81-
func getComplexType(expr hcl.Expression) (entities.Type, error) {
82-
got, exprDiags := expr.Value(getEvalContextForExpr(expr))
83+
func getComplexType(expr hcl.Expression, ctxFunctions map[string]function.Function) (entities.Type, error) {
84+
got, exprDiags := expr.Value(getEvalContextForExpr(expr, ctxFunctions))
8385
if exprDiags.HasErrors() {
8486
return entities.Type{}, fmt.Errorf("getting expression value: %v", exprDiags.Errs())
8587
}
@@ -115,7 +117,7 @@ func getComplexType(expr hcl.Expression) (entities.Type, error) {
115117
}, nil
116118
}
117119

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

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

137-
return getComplexType(expr)
139+
return getComplexType(expr, ctxFunctions)
138140
}
139141

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

148-
return getTypeFromExpression(expr)
150+
return getTypeFromExpression(expr, ctxFunctions)
149151
}
150152

151153
func getVariablesMap(expr hcl.Expression) map[string]cty.Value {
152-
myMap := make(map[string]cty.Value)
154+
varMap := make(map[string]cty.Value)
153155
for _, variable := range expr.Variables() {
154156
name := variable.RootName()
155157

156-
myMap[name] = cty.StringVal(name)
158+
varMap[name] = cty.StringVal(name)
157159
}
158160

159-
return myMap
161+
return varMap
160162
}
161163

162-
func getEvalContextForExpr(expr hcl.Expression) *hcl.EvalContext {
164+
func getEvalContextForExpr(expr hcl.Expression, ctxFunctions map[string]function.Function) *hcl.EvalContext {
163165
return &hcl.EvalContext{
164-
Functions: map[string]function.Function{
165-
"object": complexTypeFunc(types.TerraformObject),
166-
"map": nestedTypeFunc(types.TerraformMap),
167-
"list": nestedTypeFunc(types.TerraformList),
168-
"set": nestedTypeFunc(types.TerraformSet),
169-
},
166+
Functions: ctxFunctions,
170167
Variables: getVariablesMap(expr),
171168
}
172169
}
170+
171+
func init() {
172+
varFunctions = map[string]function.Function{
173+
"object": complexTypeFunc(types.TerraformObject),
174+
"map": nestedTypeFunc(types.TerraformMap),
175+
"list": nestedTypeFunc(types.TerraformList),
176+
"set": nestedTypeFunc(types.TerraformSet),
177+
}
178+
179+
outputFunctions = map[string]function.Function{
180+
"resource": complexTypeFunc(types.TerraformResource),
181+
"object": complexTypeFunc(types.TerraformObject),
182+
"map": nestedTypeFunc(types.TerraformMap),
183+
"list": nestedTypeFunc(types.TerraformList),
184+
"set": nestedTypeFunc(types.TerraformSet),
185+
}
186+
}

0 commit comments

Comments
 (0)