Make terraform.Module available in content

Add one extra special variable the `content`:

- `{{ .Module }}`

As opposed to the other variables, which are generated sections based on
a selected formatter, the `{{ .Module }}` variable is just a `struct`
representing a Terraform module.

It can be used to build highly complex and highly customized content:

```yaml
content: |-
  ## Resources

  {{ range .Module.Resources }}
  - {{ .GetMode }}.{{ .Spec }} ({{ .Position.Filename }}#{{ .Position.Line }})
  {{- end }}
```

Signed-off-by: Khosrow Moossavi <khos2ow@gmail.com>
This commit is contained in:
Khosrow Moossavi
2021-10-04 15:17:41 -04:00
parent de684ce48c
commit 465dd14cff
34 changed files with 461 additions and 449 deletions

View File

@@ -312,11 +312,11 @@ func buildTerraformDocs(path string, tmpl string) (string, error) {
// // Note: if you don't intend to provide additional template for the generated // // Note: if you don't intend to provide additional template for the generated
// // content, or the target format doesn't provide templating (e.g. json, yaml, // // content, or the target format doesn't provide templating (e.g. json, yaml,
// // xml, or toml) you can use `Content()` function instead of `ExecuteTemplate()`. // // xml, or toml) you can use `Content()` function instead of `Render()`.
// // `Content()` returns all the sections combined with predefined order. // // `Content()` returns all the sections combined with predefined order.
// return formatter.Content(), nil // return formatter.Content(), nil
return formatter.ExecuteTemplate(tmpl) return formatter.Render(tmpl)
} }
``` ```

View File

@@ -29,19 +29,27 @@ will be ignored for other formatters.
- `{{ .Requirements }}` - `{{ .Requirements }}`
- `{{ .Resources }}` - `{{ .Resources }}`
and following functions:
- `{{ include "relative/path/to/file" }}`
These variables are the generated output of individual sections in the selected These variables are the generated output of individual sections in the selected
formatter. For example `{{ .Inputs }}` is Markdown Table representation of _inputs_ formatter. For example `{{ .Inputs }}` is Markdown Table representation of _inputs_
when formatter is set to `markdown table`. when formatter is set to `markdown table`.
{{< alert type="info" >}} {{< alert type="info" >}}
Sections visibility (i.e. `sections.show` and `sections.hide`) takes Sections visibility (i.e. `sections.show` and `sections.hide`) takes precedence
precedence over the `content`. over the `content`.
{{< /alert >}} {{< /alert >}}
`content` also has the following function:
- `{{ include "relative/path/to/file" }}`
Additionally there's also one extra special variable avaialble to the `content`:
- `{{ .Module }}`
As opposed to the other variables mentioned above, which are generated sections
based on a selected formatter, the `{{ .Module }}` variable is just a `struct`
representing a [Terraform module].
## Options ## Options
Available options with their default values. Available options with their default values.
@@ -93,7 +101,7 @@ content: |-
```` ````
In the following example, although `{{ .Providers }}` is defined it won't be In the following example, although `{{ .Providers }}` is defined it won't be
rendered because `providers` is not set to be shown in `sections.show`. rendered because `providers` is not set to be shown in `sections.show`:
```yaml ```yaml
sections: sections:
@@ -113,3 +121,16 @@ content: |-
{{ .Outputs }} {{ .Outputs }}
``` ```
Building highly complex and highly customized content using `{{ .Module }}` struct:
```yaml
content: |-
## Resources
{{ range .Module.Resources }}
- {{ .GetMode }}.{{ .Spec }} ({{ .Position.Filename }}#{{ .Position.Line }})
{{- end }}
```
[Terraform module]: https://pkg.go.dev/github.com/terraform-docs/terraform-docs/terraform#Module

View File

@@ -32,6 +32,11 @@ sections:
# #
# and even in between sections # and even in between sections
# #
# ## Resources
# {{ range .Module.Resources }}
# - {{ .GetMode }}.{{ .Spec }} ({{ .Position.Filename }}#{{ .Position.Line }})
# {{- end }}
#
# ## Examples # ## Examples
# #
# ```hcl # ```hcl

View File

@@ -24,7 +24,7 @@ var asciidocsDocumentFS embed.FS
// asciidocDocument represents AsciiDoc Document format. // asciidocDocument represents AsciiDoc Document format.
type asciidocDocument struct { type asciidocDocument struct {
*print.Generator *generator
config *print.Config config *print.Config
template *template.Template template *template.Template
@@ -61,7 +61,7 @@ func NewAsciidocDocument(config *print.Config) Type {
}) })
return &asciidocDocument{ return &asciidocDocument{
Generator: print.NewGenerator("json", config.ModuleRoot), generator: newGenerator(config, true),
config: config, config: config,
template: tt, template: tt,
} }
@@ -69,7 +69,7 @@ func NewAsciidocDocument(config *print.Config) Type {
// Generate a Terraform module as AsciiDoc document. // Generate a Terraform module as AsciiDoc document.
func (d *asciidocDocument) Generate(module *terraform.Module) error { func (d *asciidocDocument) Generate(module *terraform.Module) error {
err := d.Generator.ForEach(func(name string) (string, error) { err := d.generator.forEach(func(name string) (string, error) {
rendered, err := d.template.Render(name, module) rendered, err := d.template.Render(name, module)
if err != nil { if err != nil {
return "", err return "", err
@@ -77,6 +77,8 @@ func (d *asciidocDocument) Generate(module *terraform.Module) error {
return sanitize(rendered), nil return sanitize(rendered), nil
}) })
d.generator.funcs(withModule(module))
return err return err
} }

View File

@@ -153,10 +153,7 @@ func TestAsciidocDocument(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -24,7 +24,7 @@ var asciidocTableFS embed.FS
// asciidocTable represents AsciiDoc Table format. // asciidocTable represents AsciiDoc Table format.
type asciidocTable struct { type asciidocTable struct {
*print.Generator *generator
config *print.Config config *print.Config
template *template.Template template *template.Template
@@ -52,7 +52,7 @@ func NewAsciidocTable(config *print.Config) Type {
}) })
return &asciidocTable{ return &asciidocTable{
Generator: print.NewGenerator("json", config.ModuleRoot), generator: newGenerator(config, true),
config: config, config: config,
template: tt, template: tt,
} }
@@ -60,7 +60,7 @@ func NewAsciidocTable(config *print.Config) Type {
// Generate a Terraform module as AsciiDoc tables. // Generate a Terraform module as AsciiDoc tables.
func (t *asciidocTable) Generate(module *terraform.Module) error { func (t *asciidocTable) Generate(module *terraform.Module) error {
err := t.Generator.ForEach(func(name string) (string, error) { err := t.generator.forEach(func(name string) (string, error) {
rendered, err := t.template.Render(name, module) rendered, err := t.template.Render(name, module)
if err != nil { if err != nil {
return "", err return "", err
@@ -68,6 +68,8 @@ func (t *asciidocTable) Generate(module *terraform.Module) error {
return sanitize(rendered), nil return sanitize(rendered), nil
}) })
t.generator.funcs(withModule(module))
return err return err
} }

View File

@@ -153,10 +153,7 @@ func TestAsciidocTable(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -28,15 +28,15 @@ the root directory of this source tree.
// return err // return err
// } // }
// //
// output, err := formatter.ExecuteTemplate("") // output, err := formatter.Render"")
// if err != nil { // if err != nil {
// return err // return err
// } // }
// //
// Note: if you don't intend to provide additional template for the generated // Note: if you don't intend to provide additional template for the generated
// content, or the target format doesn't provide templating (e.g. json, yaml, // content, or the target format doesn't provide templating (e.g. json, yaml,
// xml, or toml) you can use `Content()` function instead of `ExecuteTemplate()`. // xml, or toml) you can use `Content()` function instead of `Render)`. Note
// `Content()` returns all the sections combined with predefined order. // that `Content()` returns all the sections combined with predefined order.
// //
// output := formatter.Content() // output := formatter.Content()
// //

267
format/generator.go Normal file
View File

@@ -0,0 +1,267 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package format
import (
"os"
"path/filepath"
"strings"
gotemplate "text/template"
"github.com/terraform-docs/terraform-docs/print"
"github.com/terraform-docs/terraform-docs/template"
"github.com/terraform-docs/terraform-docs/terraform"
)
// generateFunc configures generator.
type generateFunc func(*generator)
// withContent specifies how the generator should add content.
func withContent(content string) generateFunc {
return func(g *generator) {
g.content = content
}
}
// withHeader specifies how the generator should add Header.
func withHeader(header string) generateFunc {
return func(g *generator) {
g.header = header
}
}
// withFooter specifies how the generator should add Footer.
func withFooter(footer string) generateFunc {
return func(g *generator) {
g.footer = footer
}
}
// withInputs specifies how the generator should add Inputs.
func withInputs(inputs string) generateFunc {
return func(g *generator) {
g.inputs = inputs
}
}
// withModules specifies how the generator should add Modules.
func withModules(modules string) generateFunc {
return func(g *generator) {
g.modules = modules
}
}
// withOutputs specifies how the generator should add Outputs.
func withOutputs(outputs string) generateFunc {
return func(g *generator) {
g.outputs = outputs
}
}
// withProviders specifies how the generator should add Providers.
func withProviders(providers string) generateFunc {
return func(g *generator) {
g.providers = providers
}
}
// withRequirements specifies how the generator should add Requirements.
func withRequirements(requirements string) generateFunc {
return func(g *generator) {
g.requirements = requirements
}
}
// withResources specifies how the generator should add Resources.
func withResources(resources string) generateFunc {
return func(g *generator) {
g.resources = resources
}
}
// withModule specifies how the generator should add Resources.
func withModule(module *terraform.Module) generateFunc {
return func(g *generator) {
g.module = module
}
}
// generator represents all the sections that can be generated for a Terraform
// modules (e.g. header, footer, inputs, etc). All these sections are being
// generated individually and if no content template was passed they will be
// combined together with a predefined order.
//
// On the other hand these sections can individually be used in content template
// to form a custom format (and order).
//
// Note that the notion of custom content template will be ignored for incompatible
// formatters and custom plugins. Compatible formatters are:
//
// - asciidoc document
// - asciidoc table
// - markdown document
// - markdown table
type generator struct {
// all the content combined
content string
// individual sections
header string
footer string
inputs string
modules string
outputs string
providers string
requirements string
resources string
config *print.Config
module *terraform.Module
path string // module's path
fns []generateFunc // generator helper functions
canRender bool // indicates if the generator can render with custom template
}
// newGenerator returns a generator for specific formatter name and with
// provided sets of GeneratorFunc functions to build and add individual
// sections.
func newGenerator(config *print.Config, canRender bool, fns ...generateFunc) *generator {
g := &generator{
config: config,
path: config.ModuleRoot,
fns: []generateFunc{},
canRender: canRender,
}
g.funcs(fns...)
return g
}
// Content returns generted all the sections combined based on the underlying format.
func (g *generator) Content() string { return g.content }
// Header returns generted header section based on the underlying format.
func (g *generator) Header() string { return g.header }
// Footer returns generted footer section based on the underlying format.
func (g *generator) Footer() string { return g.footer }
// Inputs returns generted inputs section based on the underlying format.
func (g *generator) Inputs() string { return g.inputs }
// Modules returns generted modules section based on the underlying format.
func (g *generator) Modules() string { return g.modules }
// Outputs returns generted outputs section based on the underlying format.
func (g *generator) Outputs() string { return g.outputs }
// Providers returns generted providers section based on the underlying format.
func (g *generator) Providers() string { return g.providers }
// Requirements returns generted resources section based on the underlying format.
func (g *generator) Requirements() string { return g.requirements }
// Resources returns generted requirements section based on the underlying format.
func (g *generator) Resources() string { return g.resources }
// Module returns generted requirements section based on the underlying format.
func (g *generator) Module() *terraform.Module { return g.module }
// funcs adds GenerateFunc to the list of available functions, for further use
// if need be, and then runs them.
func (g *generator) funcs(fns ...generateFunc) {
for _, fn := range fns {
g.fns = append(g.fns, fn)
fn(g)
}
}
// Path set path of module's root directory.
func (g *generator) Path(root string) {
g.path = root
}
func (g *generator) Render(tpl string) (string, error) {
if !g.canRender {
return g.content, nil
}
if tpl == "" {
return g.content, nil
}
tt := template.New(g.config, &template.Item{
Name: "content",
Text: tpl,
})
tt.CustomFunc(gotemplate.FuncMap{
"include": func(s string) string {
content, err := os.ReadFile(filepath.Join(g.path, s))
if err != nil {
panic(err)
}
return strings.TrimSuffix(string(content), "\n")
},
})
data := struct {
*generator
Config *print.Config
Module *terraform.Module
}{
generator: g,
Config: g.config,
Module: g.module,
}
rendered, err := tt.RenderContent("content", data)
if err != nil {
return "", err
}
return strings.TrimSuffix(rendered, "\n"), nil
}
// generatorCallback renders a Terraform module and creates a GenerateFunc.
type generatorCallback func(string) generateFunc
// forEach section executes generatorCallback to render the content for that
// section and create corresponding GeneratorFunc. If there is any error in
// executing the template for the section forEach function immediately returns
// it and exits.
func (g *generator) forEach(render func(string) (string, error)) error {
mappings := map[string]generatorCallback{
"all": withContent,
"header": withHeader,
"footer": withFooter,
"inputs": withInputs,
"modules": withModules,
"outputs": withOutputs,
"providers": withProviders,
"requirements": withRequirements,
"resources": withResources,
}
for name, callback := range mappings {
result, err := render(name)
if err != nil {
return err
}
fn := callback(result)
g.fns = append(g.fns, fn)
fn(g)
}
return nil
}

View File

@@ -8,128 +8,80 @@ You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree. the root directory of this source tree.
*/ */
package print package format
import ( import (
"io/ioutil"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/terraform-docs/terraform-docs/print"
"github.com/terraform-docs/terraform-docs/terraform"
) )
func TestIsCompatible(t *testing.T) {
tests := map[string]struct {
expected bool
}{
"asciidoc document": {
expected: true,
},
"asciidoc table": {
expected: true,
},
"markdown document": {
expected: true,
},
"markdown table": {
expected: true,
},
"markdown": {
expected: false,
},
"markdown-table": {
expected: false,
},
"md": {
expected: false,
},
"md tbl": {
expected: false,
},
"md-tbl": {
expected: false,
},
"json": {
expected: false,
},
"yaml": {
expected: false,
},
"xml": {
expected: false,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
generator := NewGenerator(name, "")
actual := generator.isCompatible()
assert.Equal(tt.expected, actual)
})
}
}
func TestExecuteTemplate(t *testing.T) { func TestExecuteTemplate(t *testing.T) {
header := "this is the header" header := "this is the header"
footer := "this is the footer" footer := "this is the footer"
tests := map[string]struct { tests := map[string]struct {
name string complex bool
content string content string
template string template string
expected string expected string
wantErr bool wantErr bool
}{ }{
"Compatible without template": { "Compatible without template": {
name: "markdown table", complex: true,
content: "this is the header\nthis is the footer", content: "this is the header\nthis is the footer",
template: "", template: "",
expected: "this is the header\nthis is the footer", expected: "this is the header\nthis is the footer",
wantErr: false, wantErr: false,
}, },
"Compatible with template not empty section": { "Compatible with template not empty section": {
name: "markdown table", complex: true,
content: "this is the header\nthis is the footer", content: "this is the header\nthis is the footer",
template: "{{ .Header }}", template: "{{ .Header }}",
expected: "this is the header", expected: "this is the header",
wantErr: false, wantErr: false,
}, },
"Compatible with template empty section": { "Compatible with template empty section": {
name: "markdown table", complex: true,
content: "this is the header\nthis is the footer", content: "this is the header\nthis is the footer",
template: "{{ .Inputs }}", template: "{{ .Inputs }}",
expected: "", expected: "",
wantErr: false, wantErr: false,
}, },
"Compatible with template and unknown section": { "Compatible with template and unknown section": {
name: "markdown table", complex: true,
content: "this is the header\nthis is the footer", content: "this is the header\nthis is the footer",
template: "{{ .Unknown }}", template: "{{ .Unknown }}",
expected: "", expected: "",
wantErr: true, wantErr: true,
}, },
"Compatible with template include file": { "Compatible with template include file": {
name: "markdown table", complex: true,
content: "this is the header\nthis is the footer", content: "this is the header\nthis is the footer",
template: "{{ include \"testdata/sample-file.txt\" }}", template: "{{ include \"testdata/generator/sample-file.txt\" }}",
expected: "Sample file to be included.\n", expected: "Sample file to be included.",
wantErr: false, wantErr: false,
}, },
"Compatible with template include unknown file": { "Compatible with template include unknown file": {
name: "markdown table", complex: true,
content: "this is the header\nthis is the footer", content: "this is the header\nthis is the footer",
template: "{{ include \"file-not-found\" }}", template: "{{ include \"file-not-found\" }}",
expected: "", expected: "",
wantErr: true, wantErr: true,
}, },
"Incompatible without template": { "Incompatible without template": {
name: "yaml", complex: false,
content: "header: \"this is the header\"\nfooter: \"this is the footer\"", content: "header: \"this is the header\"\nfooter: \"this is the footer\"",
template: "", template: "",
expected: "header: \"this is the header\"\nfooter: \"this is the footer\"", expected: "header: \"this is the header\"\nfooter: \"this is the footer\"",
wantErr: false, wantErr: false,
}, },
"Incompatible with template": { "Incompatible with template": {
name: "yaml", complex: false,
content: "header: \"this is the header\"\nfooter: \"this is the footer\"", content: "header: \"this is the header\"\nfooter: \"this is the footer\"",
template: "{{ .Header }}", template: "{{ .Header }}",
expected: "header: \"this is the header\"\nfooter: \"this is the footer\"", expected: "header: \"this is the header\"\nfooter: \"this is the footer\"",
@@ -140,12 +92,14 @@ func TestExecuteTemplate(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
generator := NewGenerator(tt.name, "") config := print.DefaultConfig()
generator := newGenerator(config, tt.complex)
generator.content = tt.content generator.content = tt.content
generator.header = header generator.header = header
generator.footer = footer generator.footer = footer
actual, err := generator.ExecuteTemplate(tt.template) actual, err := generator.Render(tt.template)
if tt.wantErr { if tt.wantErr {
assert.NotNil(err) assert.NotNil(err)
@@ -160,60 +114,92 @@ func TestExecuteTemplate(t *testing.T) {
func TestGeneratorFunc(t *testing.T) { func TestGeneratorFunc(t *testing.T) {
text := "foo" text := "foo"
tests := map[string]struct { tests := map[string]struct {
fn func(string) GenerateFunc fn func(string) generateFunc
actual func(*Generator) string actual func(*generator) string
}{ }{
"WithContent": { "withContent": {
fn: WithContent, fn: withContent,
actual: func(r *Generator) string { return r.content }, actual: func(r *generator) string { return r.content },
}, },
"WithHeader": { "withHeader": {
fn: WithHeader, fn: withHeader,
actual: func(r *Generator) string { return r.header }, actual: func(r *generator) string { return r.header },
}, },
"WithFooter": { "withFooter": {
fn: WithFooter, fn: withFooter,
actual: func(r *Generator) string { return r.footer }, actual: func(r *generator) string { return r.footer },
}, },
"WithInputs": { "withInputs": {
fn: WithInputs, fn: withInputs,
actual: func(r *Generator) string { return r.inputs }, actual: func(r *generator) string { return r.inputs },
}, },
"WithModules": { "withModules": {
fn: WithModules, fn: withModules,
actual: func(r *Generator) string { return r.modules }, actual: func(r *generator) string { return r.modules },
}, },
"WithOutputs": { "withOutputs": {
fn: WithOutputs, fn: withOutputs,
actual: func(r *Generator) string { return r.outputs }, actual: func(r *generator) string { return r.outputs },
}, },
"WithProviders": { "withProviders": {
fn: WithProviders, fn: withProviders,
actual: func(r *Generator) string { return r.providers }, actual: func(r *generator) string { return r.providers },
}, },
"WithRequirements": { "withRequirements": {
fn: WithRequirements, fn: withRequirements,
actual: func(r *Generator) string { return r.requirements }, actual: func(r *generator) string { return r.requirements },
}, },
"WithResources": { "withResources": {
fn: WithResources, fn: withResources,
actual: func(r *Generator) string { return r.resources }, actual: func(r *generator) string { return r.resources },
}, },
} }
for name, tt := range tests { for name, tt := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
generator := NewGenerator(name, "", tt.fn(text)) config := print.DefaultConfig()
config.Sections.Footer = true
generator := newGenerator(config, false, tt.fn(text))
assert.Equal(text, tt.actual(generator)) assert.Equal(text, tt.actual(generator))
}) })
} }
} }
func TestGeneratorFuncModule(t *testing.T) {
t.Run("withModule", func(t *testing.T) {
assert := assert.New(t)
config := print.DefaultConfig()
config.ModuleRoot = filepath.Join("..", "terraform", "testdata", "full-example")
module, err := terraform.LoadWithOptions(config)
assert.Nil(err)
generator := newGenerator(config, true, withModule(module))
path := filepath.Join("..", "terraform", "testdata", "expected", "full-example-mainTf-Header.golden")
data, err := ioutil.ReadFile(path)
assert.Nil(err)
expected := string(data)
assert.Equal(expected, generator.module.Header)
assert.Equal("", generator.module.Footer)
assert.Equal(7, len(generator.module.Inputs))
assert.Equal(3, len(generator.module.Outputs))
})
}
func TestForEach(t *testing.T) { func TestForEach(t *testing.T) {
generator := NewGenerator("foo", "") config := print.DefaultConfig()
generator.ForEach(func(name string) (string, error) {
generator := newGenerator(config, false)
generator.forEach(func(name string) (string, error) {
return name, nil return name, nil
}) })

View File

@@ -21,7 +21,7 @@ import (
// json represents JSON format. // json represents JSON format.
type json struct { type json struct {
*print.Generator *generator
config *print.Config config *print.Config
} }
@@ -29,7 +29,7 @@ type json struct {
// NewJSON returns new instance of JSON. // NewJSON returns new instance of JSON.
func NewJSON(config *print.Config) Type { func NewJSON(config *print.Config) Type {
return &json{ return &json{
Generator: print.NewGenerator("json", config.ModuleRoot), generator: newGenerator(config, false),
config: config, config: config,
} }
} }
@@ -47,7 +47,7 @@ func (j *json) Generate(module *terraform.Module) error {
return err return err
} }
j.Generator.Funcs(print.WithContent(strings.TrimSuffix(buffer.String(), "\n"))) j.generator.funcs(withContent(strings.TrimSuffix(buffer.String(), "\n")))
return nil return nil
} }

View File

@@ -105,10 +105,7 @@ func TestJson(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -24,7 +24,7 @@ var markdownDocumentFS embed.FS
// markdownDocument represents Markdown Document format. // markdownDocument represents Markdown Document format.
type markdownDocument struct { type markdownDocument struct {
*print.Generator *generator
config *print.Config config *print.Config
template *template.Template template *template.Template
@@ -59,7 +59,7 @@ func NewMarkdownDocument(config *print.Config) Type {
}) })
return &markdownDocument{ return &markdownDocument{
Generator: print.NewGenerator("json", config.ModuleRoot), generator: newGenerator(config, true),
config: config, config: config,
template: tt, template: tt,
} }
@@ -67,7 +67,7 @@ func NewMarkdownDocument(config *print.Config) Type {
// Generate a Terraform module as Markdown document. // Generate a Terraform module as Markdown document.
func (d *markdownDocument) Generate(module *terraform.Module) error { func (d *markdownDocument) Generate(module *terraform.Module) error {
err := d.Generator.ForEach(func(name string) (string, error) { err := d.generator.forEach(func(name string) (string, error) {
rendered, err := d.template.Render(name, module) rendered, err := d.template.Render(name, module)
if err != nil { if err != nil {
return "", err return "", err
@@ -75,6 +75,8 @@ func (d *markdownDocument) Generate(module *terraform.Module) error {
return sanitize(rendered), nil return sanitize(rendered), nil
}) })
d.generator.funcs(withModule(module))
return err return err
} }

View File

@@ -190,10 +190,7 @@ func TestMarkdownDocument(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -24,7 +24,7 @@ var markdownTableFS embed.FS
// markdownTable represents Markdown Table format. // markdownTable represents Markdown Table format.
type markdownTable struct { type markdownTable struct {
*print.Generator *generator
config *print.Config config *print.Config
template *template.Template template *template.Template
@@ -50,7 +50,7 @@ func NewMarkdownTable(config *print.Config) Type {
}) })
return &markdownTable{ return &markdownTable{
Generator: print.NewGenerator("markdown table", config.ModuleRoot), generator: newGenerator(config, true),
config: config, config: config,
template: tt, template: tt,
} }
@@ -58,7 +58,7 @@ func NewMarkdownTable(config *print.Config) Type {
// Generate a Terraform module as Markdown tables. // Generate a Terraform module as Markdown tables.
func (t *markdownTable) Generate(module *terraform.Module) error { func (t *markdownTable) Generate(module *terraform.Module) error {
err := t.Generator.ForEach(func(name string) (string, error) { err := t.generator.forEach(func(name string) (string, error) {
rendered, err := t.template.Render(name, module) rendered, err := t.template.Render(name, module)
if err != nil { if err != nil {
return "", err return "", err
@@ -66,6 +66,8 @@ func (t *markdownTable) Generate(module *terraform.Module) error {
return sanitize(rendered), nil return sanitize(rendered), nil
}) })
t.generator.funcs(withModule(module))
return err return err
} }

View File

@@ -190,10 +190,7 @@ func TestMarkdownTable(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -26,7 +26,7 @@ var prettyTpl []byte
// pretty represents colorized pretty format. // pretty represents colorized pretty format.
type pretty struct { type pretty struct {
*print.Generator *generator
config *print.Config config *print.Config
template *template.Template template *template.Template
@@ -50,7 +50,7 @@ func NewPretty(config *print.Config) Type {
}) })
return &pretty{ return &pretty{
Generator: print.NewGenerator("pretty", config.ModuleRoot), generator: newGenerator(config, true),
config: config, config: config,
template: tt, template: tt,
} }
@@ -63,7 +63,8 @@ func (p *pretty) Generate(module *terraform.Module) error {
return err return err
} }
p.Generator.Funcs(print.WithContent(regexp.MustCompile(`(\r?\n)*$`).ReplaceAllString(rendered, ""))) p.generator.funcs(withContent(regexp.MustCompile(`(\r?\n)*$`).ReplaceAllString(rendered, "")))
p.generator.funcs(withModule(module))
return nil return nil
} }

View File

@@ -105,10 +105,7 @@ func TestPretty(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -27,7 +27,7 @@ var tfvarsHCLTpl []byte
// tfvarsHCL represents Terraform tfvars HCL format. // tfvarsHCL represents Terraform tfvars HCL format.
type tfvarsHCL struct { type tfvarsHCL struct {
*print.Generator *generator
config *print.Config config *print.Config
template *template.Template template *template.Template
@@ -60,7 +60,7 @@ func NewTfvarsHCL(config *print.Config) Type {
}) })
return &tfvarsHCL{ return &tfvarsHCL{
Generator: print.NewGenerator("tfvars hcl", config.ModuleRoot), generator: newGenerator(config, false),
config: config, config: config,
template: tt, template: tt,
} }
@@ -75,7 +75,7 @@ func (h *tfvarsHCL) Generate(module *terraform.Module) error {
return err return err
} }
h.Generator.Funcs(print.WithContent(strings.TrimSuffix(sanitize(rendered), "\n"))) h.generator.funcs(withContent(strings.TrimSuffix(sanitize(rendered), "\n")))
return nil return nil
} }

View File

@@ -97,10 +97,7 @@ func TestTfvarsHcl(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -23,7 +23,7 @@ import (
// tfvarsJSON represents Terraform tfvars JSON format. // tfvarsJSON represents Terraform tfvars JSON format.
type tfvarsJSON struct { type tfvarsJSON struct {
*print.Generator *generator
config *print.Config config *print.Config
} }
@@ -31,7 +31,7 @@ type tfvarsJSON struct {
// NewTfvarsJSON returns new instance of TfvarsJSON. // NewTfvarsJSON returns new instance of TfvarsJSON.
func NewTfvarsJSON(config *print.Config) Type { func NewTfvarsJSON(config *print.Config) Type {
return &tfvarsJSON{ return &tfvarsJSON{
Generator: print.NewGenerator("tfvars json", config.ModuleRoot), generator: newGenerator(config, false),
config: config, config: config,
} }
} }
@@ -53,10 +53,9 @@ func (j *tfvarsJSON) Generate(module *terraform.Module) error {
return err return err
} }
j.Generator.Funcs(print.WithContent(strings.TrimSuffix(buffer.String(), "\n"))) j.generator.funcs(withContent(strings.TrimSuffix(buffer.String(), "\n")))
return nil return nil
} }
func init() { func init() {

View File

@@ -88,10 +88,7 @@ func TestTfvarsJson(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -22,7 +22,7 @@ import (
// toml represents TOML format. // toml represents TOML format.
type toml struct { type toml struct {
*print.Generator *generator
config *print.Config config *print.Config
} }
@@ -30,7 +30,7 @@ type toml struct {
// NewTOML returns new instance of TOML. // NewTOML returns new instance of TOML.
func NewTOML(config *print.Config) Type { func NewTOML(config *print.Config) Type {
return &toml{ return &toml{
Generator: print.NewGenerator("toml", config.ModuleRoot), generator: newGenerator(config, false),
config: config, config: config,
} }
} }
@@ -46,7 +46,7 @@ func (t *toml) Generate(module *terraform.Module) error {
return err return err
} }
t.Generator.Funcs(print.WithContent(strings.TrimSuffix(buffer.String(), "\n"))) t.generator.funcs(withContent(strings.TrimSuffix(buffer.String(), "\n")))
return nil return nil

View File

@@ -98,10 +98,7 @@ func TestToml(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -32,7 +32,7 @@ type Type interface {
Requirements() string // requirements section based on the underlying format Requirements() string // requirements section based on the underlying format
Resources() string // resources section based on the underlying format Resources() string // resources section based on the underlying format
ExecuteTemplate(contentTmpl string) (string, error) Render(tmpl string) (string, error)
} }
// initializerFn returns a concrete implementation of an Engine. // initializerFn returns a concrete implementation of an Engine.

View File

@@ -20,7 +20,7 @@ import (
// xml represents XML format. // xml represents XML format.
type xml struct { type xml struct {
*print.Generator *generator
config *print.Config config *print.Config
} }
@@ -28,7 +28,7 @@ type xml struct {
// NewXML returns new instance of XML. // NewXML returns new instance of XML.
func NewXML(config *print.Config) Type { func NewXML(config *print.Config) Type {
return &xml{ return &xml{
Generator: print.NewGenerator("xml", config.ModuleRoot), generator: newGenerator(config, false),
config: config, config: config,
} }
} }
@@ -42,7 +42,7 @@ func (x *xml) Generate(module *terraform.Module) error {
return err return err
} }
x.Generator.Funcs(print.WithContent(strings.TrimSuffix(string(out), "\n"))) x.generator.funcs(withContent(strings.TrimSuffix(string(out), "\n")))
return nil return nil
} }

View File

@@ -98,10 +98,7 @@ func TestXml(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -22,7 +22,7 @@ import (
// yaml represents YAML format. // yaml represents YAML format.
type yaml struct { type yaml struct {
*print.Generator *generator
config *print.Config config *print.Config
} }
@@ -30,7 +30,7 @@ type yaml struct {
// NewYAML returns new instance of YAML. // NewYAML returns new instance of YAML.
func NewYAML(config *print.Config) Type { func NewYAML(config *print.Config) Type {
return &yaml{ return &yaml{
Generator: print.NewGenerator("yaml", config.ModuleRoot), generator: newGenerator(config, false),
config: config, config: config,
} }
} }
@@ -47,7 +47,7 @@ func (y *yaml) Generate(module *terraform.Module) error {
return err return err
} }
y.Generator.Funcs(print.WithContent(strings.TrimSuffix(buffer.String(), "\n"))) y.generator.funcs(withContent(strings.TrimSuffix(buffer.String(), "\n")))
return nil return nil
} }

View File

@@ -98,10 +98,7 @@ func TestYaml(t *testing.T) {
err = formatter.Generate(module) err = formatter.Generate(module)
assert.Nil(err) assert.Nil(err)
actual, err := formatter.ExecuteTemplate("") assert.Equal(expected, formatter.Content())
assert.Nil(err)
assert.Equal(expected, actual)
}) })
} }
} }

View File

@@ -360,7 +360,7 @@ func generateContent(config *print.Config) error {
return err return err
} }
content, err := formatter.ExecuteTemplate(config.Content) content, err := formatter.Render(config.Content)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -34,9 +34,8 @@ the root directory of this source tree.
// Generator holds a reference to all the sections (e.g. header, footer, inputs, etc) // Generator holds a reference to all the sections (e.g. header, footer, inputs, etc)
// and also it renders all of them, in a predefined order, in `Content()`. // and also it renders all of them, in a predefined order, in `Content()`.
// //
// It also provides `ExecuteTemplate(string)` function to process and render the // It also provides `Render(string)` function to process and render the template to generate
// template to generate the final output content. Following variables and functions are // the final output content. Following variables and functions are available:
// available:
// //
// • `{{ .Header }}` // • `{{ .Header }}`
// • `{{ .Footer }}` // • `{{ .Footer }}`

View File

@@ -1,248 +0,0 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package print
import (
"bytes"
"os"
"path/filepath"
"text/template"
)
// GenerateFunc configures Generator.
type GenerateFunc func(*Generator)
// WithContent specifies how the Generator should add content.
func WithContent(content string) GenerateFunc {
return func(g *Generator) {
g.content = content
}
}
// WithHeader specifies how the Generator should add Header.
func WithHeader(header string) GenerateFunc {
return func(g *Generator) {
g.header = header
}
}
// WithFooter specifies how the Generator should add Footer.
func WithFooter(footer string) GenerateFunc {
return func(g *Generator) {
g.footer = footer
}
}
// WithInputs specifies how the Generator should add Inputs.
func WithInputs(inputs string) GenerateFunc {
return func(g *Generator) {
g.inputs = inputs
}
}
// WithModules specifies how the Generator should add Modules.
func WithModules(modules string) GenerateFunc {
return func(g *Generator) {
g.modules = modules
}
}
// WithOutputs specifies how the Generator should add Outputs.
func WithOutputs(outputs string) GenerateFunc {
return func(g *Generator) {
g.outputs = outputs
}
}
// WithProviders specifies how the Generator should add Providers.
func WithProviders(providers string) GenerateFunc {
return func(g *Generator) {
g.providers = providers
}
}
// WithRequirements specifies how the Generator should add Requirements.
func WithRequirements(requirements string) GenerateFunc {
return func(g *Generator) {
g.requirements = requirements
}
}
// WithResources specifies how the Generator should add Resources.
func WithResources(resources string) GenerateFunc {
return func(g *Generator) {
g.resources = resources
}
}
// Generator represents all the sections that can be generated for a Terraform
// modules (e.g. header, footer, inputs, etc). All these sections are being
// generated individually and if no content template was passed they will be
// combined together with a predefined order.
//
// On the other hand these sections can individually be used in content template
// to form a custom format (and order).
//
// Note that the notion of custom content template will be ignored for incompatible
// formatters and custom plugins. Compatible formatters are:
//
// - asciidoc document
// - asciidoc table
// - markdown document
// - markdown table
type Generator struct {
// all the content combined
content string
// individual sections
header string
footer string
inputs string
modules string
outputs string
providers string
requirements string
resources string
path string // module's path
formatter string // formatter name
funcs []GenerateFunc
}
// NewGenerator returns a Generator for specific formatter name and with
// provided sets of GeneratorFunc functions to build and add individual
// sections.
func NewGenerator(name string, root string, fns ...GenerateFunc) *Generator {
g := &Generator{
path: root,
formatter: name,
funcs: []GenerateFunc{},
}
g.Funcs(fns...)
return g
}
// Content returns generted all the sections combined based on the underlying format.
func (g *Generator) Content() string { return g.content }
// Header returns generted header section based on the underlying format.
func (g *Generator) Header() string { return g.header }
// Footer returns generted footer section based on the underlying format.
func (g *Generator) Footer() string { return g.footer }
// Inputs returns generted inputs section based on the underlying format.
func (g *Generator) Inputs() string { return g.inputs }
// Modules returns generted modules section based on the underlying format.
func (g *Generator) Modules() string { return g.modules }
// Outputs returns generted outputs section based on the underlying format.
func (g *Generator) Outputs() string { return g.outputs }
// Providers returns generted providers section based on the underlying format.
func (g *Generator) Providers() string { return g.providers }
// Requirements returns generted resources section based on the underlying format.
func (g *Generator) Requirements() string { return g.requirements }
// Resources returns generted requirements section based on the underlying format.
func (g *Generator) Resources() string { return g.resources }
// Funcs adds GenerateFunc to the list of available functions, for further use
// if need be, and then runs them.
func (g *Generator) Funcs(fns ...GenerateFunc) {
for _, fn := range fns {
g.funcs = append(g.funcs, fn)
fn(g)
}
}
// Path set path of module's root directory.
func (g *Generator) Path(root string) {
g.path = root
}
// ExecuteTemplate applies the template with Renderer known items. If template
// is empty Renderer.content is returned as is. If template is not empty this
// still returns Renderer.content for incompatible formatters.
// func (g *Renderer) Render(contentTmpl string) (string, error) {
func (g *Generator) ExecuteTemplate(contentTmpl string) (string, error) {
if !g.isCompatible() {
return g.content, nil
}
if contentTmpl == "" {
return g.content, nil
}
var buf bytes.Buffer
tmpl := template.New("content")
tmpl.Funcs(template.FuncMap{
"include": func(s string) string {
content, err := os.ReadFile(filepath.Join(g.path, s))
if err != nil {
panic(err)
}
return string(content)
},
})
template.Must(tmpl.Parse(contentTmpl))
if err := tmpl.ExecuteTemplate(&buf, "content", g); err != nil {
return "", err
}
return buf.String(), nil
}
func (g *Generator) isCompatible() bool {
switch g.formatter {
case "asciidoc document", "asciidoc table", "markdown document", "markdown table":
return true
}
return false
}
// generatorCallback renders a Terraform module and creates a GenerateFunc.
type generatorCallback func(string) GenerateFunc
// ForEach section executes generatorCallback to render the content for that
// section and create corresponding GeneratorFunc. If there is any error in
// the executing the template for the section ForEach function immediately
// returns it and exit.
func (g *Generator) ForEach(render func(string) (string, error)) error {
mappings := map[string]generatorCallback{
"all": WithContent,
"header": WithHeader,
"footer": WithFooter,
"inputs": WithInputs,
"modules": WithModules,
"outputs": WithOutputs,
"providers": WithProviders,
"requirements": WithRequirements,
"resources": WithResources,
}
for name, callback := range mappings {
result, err := render(name)
if err != nil {
return err
}
fn := callback(result)
g.funcs = append(g.funcs, fn)
fn(g)
}
return nil
}

View File

@@ -74,6 +74,19 @@ func (t *Template) applyCustomFunc() {
// Render template with given Module struct. // Render template with given Module struct.
func (t *Template) Render(name string, module *terraform.Module) (string, error) { func (t *Template) Render(name string, module *terraform.Module) (string, error) {
data := struct {
Config *print.Config
Module *terraform.Module
}{
Config: t.config,
Module: module,
}
return t.RenderContent(name, data)
}
// RenderContent template with given data. It can contain anything but most
// probably it will only contain terraform.Module and print.generator.
func (t *Template) RenderContent(name string, data interface{}) (string, error) {
if len(t.items) < 1 { if len(t.items) < 1 {
return "", fmt.Errorf("base template not found") return "", fmt.Errorf("base template not found")
} }
@@ -95,13 +108,7 @@ func (t *Template) Render(name string, module *terraform.Module) (string, error)
gotemplate.Must(tt.Parse(normalize(ii.Text))) gotemplate.Must(tt.Parse(normalize(ii.Text)))
} }
if err := tmpl.ExecuteTemplate(&buffer, item.Name, struct { if err := tmpl.ExecuteTemplate(&buffer, item.Name, data); err != nil {
Module *terraform.Module
Config *print.Config
}{
Module: module,
Config: t.config,
}); err != nil {
return "", err return "", err
} }