From 465dd14cff74409f74bc148c4f71411b1cf380d3 Mon Sep 17 00:00:00 2001 From: Khosrow Moossavi Date: Mon, 4 Oct 2021 15:17:41 -0400 Subject: [PATCH] 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 --- README.md | 4 +- docs/user-guide/configuration/content.md | 35 ++- examples/.terraform-docs.yml | 5 + format/asciidoc_document.go | 8 +- format/asciidoc_document_test.go | 5 +- format/asciidoc_table.go | 8 +- format/asciidoc_table_test.go | 5 +- format/doc.go | 6 +- format/generator.go | 267 ++++++++++++++++++ {print => format}/generator_test.go | 184 ++++++------ format/json.go | 6 +- format/json_test.go | 5 +- format/markdown_document.go | 8 +- format/markdown_document_test.go | 5 +- format/markdown_table.go | 8 +- format/markdown_table_test.go | 5 +- format/pretty.go | 7 +- format/pretty_test.go | 5 +- .../testdata/generator}/sample-file.txt | 0 format/tfvars_hcl.go | 6 +- format/tfvars_hcl_test.go | 5 +- format/tfvars_json.go | 7 +- format/tfvars_json_test.go | 5 +- format/toml.go | 6 +- format/toml_test.go | 5 +- format/type.go | 2 +- format/xml.go | 6 +- format/xml_test.go | 5 +- format/yaml.go | 6 +- format/yaml_test.go | 5 +- internal/cli/run.go | 2 +- print/doc.go | 5 +- print/generator.go | 248 ---------------- template/template.go | 21 +- 34 files changed, 461 insertions(+), 449 deletions(-) create mode 100644 format/generator.go rename {print => format}/generator_test.go (56%) rename {print/testdata => format/testdata/generator}/sample-file.txt (100%) delete mode 100644 print/generator.go diff --git a/README.md b/README.md index 82026e1..9cb0989 100644 --- a/README.md +++ b/README.md @@ -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 // // 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. // return formatter.Content(), nil - return formatter.ExecuteTemplate(tmpl) + return formatter.Render(tmpl) } ``` diff --git a/docs/user-guide/configuration/content.md b/docs/user-guide/configuration/content.md index b14cd1b..fff5f0d 100644 --- a/docs/user-guide/configuration/content.md +++ b/docs/user-guide/configuration/content.md @@ -29,19 +29,27 @@ will be ignored for other formatters. - `{{ .Requirements }}` - `{{ .Resources }}` -and following functions: - -- `{{ include "relative/path/to/file" }}` - These variables are the generated output of individual sections in the selected formatter. For example `{{ .Inputs }}` is Markdown Table representation of _inputs_ when formatter is set to `markdown table`. {{< alert type="info" >}} -Sections visibility (i.e. `sections.show` and `sections.hide`) takes -precedence over the `content`. +Sections visibility (i.e. `sections.show` and `sections.hide`) takes precedence +over the `content`. {{< /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 Available options with their default values. @@ -93,7 +101,7 @@ content: |- ```` 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 sections: @@ -113,3 +121,16 @@ content: |- {{ .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 \ No newline at end of file diff --git a/examples/.terraform-docs.yml b/examples/.terraform-docs.yml index 3c27dae..4da1441 100644 --- a/examples/.terraform-docs.yml +++ b/examples/.terraform-docs.yml @@ -32,6 +32,11 @@ sections: # # and even in between sections # +# ## Resources +# {{ range .Module.Resources }} +# - {{ .GetMode }}.{{ .Spec }} ({{ .Position.Filename }}#{{ .Position.Line }}) +# {{- end }} +# # ## Examples # # ```hcl diff --git a/format/asciidoc_document.go b/format/asciidoc_document.go index 8b789a6..260d833 100644 --- a/format/asciidoc_document.go +++ b/format/asciidoc_document.go @@ -24,7 +24,7 @@ var asciidocsDocumentFS embed.FS // asciidocDocument represents AsciiDoc Document format. type asciidocDocument struct { - *print.Generator + *generator config *print.Config template *template.Template @@ -61,7 +61,7 @@ func NewAsciidocDocument(config *print.Config) Type { }) return &asciidocDocument{ - Generator: print.NewGenerator("json", config.ModuleRoot), + generator: newGenerator(config, true), config: config, template: tt, } @@ -69,7 +69,7 @@ func NewAsciidocDocument(config *print.Config) Type { // Generate a Terraform module as AsciiDoc document. 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) if err != nil { return "", err @@ -77,6 +77,8 @@ func (d *asciidocDocument) Generate(module *terraform.Module) error { return sanitize(rendered), nil }) + d.generator.funcs(withModule(module)) + return err } diff --git a/format/asciidoc_document_test.go b/format/asciidoc_document_test.go index ac25c02..5aebdbe 100644 --- a/format/asciidoc_document_test.go +++ b/format/asciidoc_document_test.go @@ -153,10 +153,7 @@ func TestAsciidocDocument(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/format/asciidoc_table.go b/format/asciidoc_table.go index 8ab1759..c8cd675 100644 --- a/format/asciidoc_table.go +++ b/format/asciidoc_table.go @@ -24,7 +24,7 @@ var asciidocTableFS embed.FS // asciidocTable represents AsciiDoc Table format. type asciidocTable struct { - *print.Generator + *generator config *print.Config template *template.Template @@ -52,7 +52,7 @@ func NewAsciidocTable(config *print.Config) Type { }) return &asciidocTable{ - Generator: print.NewGenerator("json", config.ModuleRoot), + generator: newGenerator(config, true), config: config, template: tt, } @@ -60,7 +60,7 @@ func NewAsciidocTable(config *print.Config) Type { // Generate a Terraform module as AsciiDoc tables. 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) if err != nil { return "", err @@ -68,6 +68,8 @@ func (t *asciidocTable) Generate(module *terraform.Module) error { return sanitize(rendered), nil }) + t.generator.funcs(withModule(module)) + return err } diff --git a/format/asciidoc_table_test.go b/format/asciidoc_table_test.go index 5bb884e..dbaa9b6 100644 --- a/format/asciidoc_table_test.go +++ b/format/asciidoc_table_test.go @@ -153,10 +153,7 @@ func TestAsciidocTable(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/format/doc.go b/format/doc.go index a85442b..b1a1ec8 100644 --- a/format/doc.go +++ b/format/doc.go @@ -28,15 +28,15 @@ the root directory of this source tree. // return err // } // -// output, err := formatter.ExecuteTemplate("") +// output, err := formatter.Render"") // if err != nil { // return err // } // // 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, -// xml, or toml) you can use `Content()` function instead of `ExecuteTemplate()`. -// `Content()` returns all the sections combined with predefined order. +// xml, or toml) you can use `Content()` function instead of `Render)`. Note +// that `Content()` returns all the sections combined with predefined order. // // output := formatter.Content() // diff --git a/format/generator.go b/format/generator.go new file mode 100644 index 0000000..baf4c7b --- /dev/null +++ b/format/generator.go @@ -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 +} diff --git a/print/generator_test.go b/format/generator_test.go similarity index 56% rename from print/generator_test.go rename to format/generator_test.go index 10dbc4a..baa7af2 100644 --- a/print/generator_test.go +++ b/format/generator_test.go @@ -8,128 +8,80 @@ You may obtain a copy of the License at the LICENSE file in the root directory of this source tree. */ -package print +package format import ( + "io/ioutil" + "path/filepath" "testing" "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) { header := "this is the header" footer := "this is the footer" tests := map[string]struct { - name string + complex bool content string template string expected string wantErr bool }{ "Compatible without template": { - name: "markdown table", + complex: true, content: "this is the header\nthis is the footer", template: "", expected: "this is the header\nthis is the footer", wantErr: false, }, "Compatible with template not empty section": { - name: "markdown table", + complex: true, content: "this is the header\nthis is the footer", template: "{{ .Header }}", expected: "this is the header", wantErr: false, }, "Compatible with template empty section": { - name: "markdown table", + complex: true, content: "this is the header\nthis is the footer", template: "{{ .Inputs }}", expected: "", wantErr: false, }, "Compatible with template and unknown section": { - name: "markdown table", + complex: true, content: "this is the header\nthis is the footer", template: "{{ .Unknown }}", expected: "", wantErr: true, }, "Compatible with template include file": { - name: "markdown table", + complex: true, content: "this is the header\nthis is the footer", - template: "{{ include \"testdata/sample-file.txt\" }}", - expected: "Sample file to be included.\n", + template: "{{ include \"testdata/generator/sample-file.txt\" }}", + expected: "Sample file to be included.", wantErr: false, }, "Compatible with template include unknown file": { - name: "markdown table", + complex: true, content: "this is the header\nthis is the footer", template: "{{ include \"file-not-found\" }}", expected: "", wantErr: true, }, "Incompatible without template": { - name: "yaml", + complex: false, content: "header: \"this is the header\"\nfooter: \"this is the footer\"", template: "", expected: "header: \"this is the header\"\nfooter: \"this is the footer\"", wantErr: false, }, "Incompatible with template": { - name: "yaml", + complex: false, content: "header: \"this is the header\"\nfooter: \"this is the footer\"", template: "{{ .Header }}", 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) { assert := assert.New(t) - generator := NewGenerator(tt.name, "") + config := print.DefaultConfig() + + generator := newGenerator(config, tt.complex) generator.content = tt.content generator.header = header generator.footer = footer - actual, err := generator.ExecuteTemplate(tt.template) + actual, err := generator.Render(tt.template) if tt.wantErr { assert.NotNil(err) @@ -160,60 +114,92 @@ func TestExecuteTemplate(t *testing.T) { func TestGeneratorFunc(t *testing.T) { text := "foo" tests := map[string]struct { - fn func(string) GenerateFunc - actual func(*Generator) string + fn func(string) generateFunc + actual func(*generator) string }{ - "WithContent": { - fn: WithContent, - actual: func(r *Generator) string { return r.content }, + "withContent": { + fn: withContent, + actual: func(r *generator) string { return r.content }, }, - "WithHeader": { - fn: WithHeader, - actual: func(r *Generator) string { return r.header }, + "withHeader": { + fn: withHeader, + actual: func(r *generator) string { return r.header }, }, - "WithFooter": { - fn: WithFooter, - actual: func(r *Generator) string { return r.footer }, + "withFooter": { + fn: withFooter, + actual: func(r *generator) string { return r.footer }, }, - "WithInputs": { - fn: WithInputs, - actual: func(r *Generator) string { return r.inputs }, + "withInputs": { + fn: withInputs, + actual: func(r *generator) string { return r.inputs }, }, - "WithModules": { - fn: WithModules, - actual: func(r *Generator) string { return r.modules }, + "withModules": { + fn: withModules, + actual: func(r *generator) string { return r.modules }, }, - "WithOutputs": { - fn: WithOutputs, - actual: func(r *Generator) string { return r.outputs }, + "withOutputs": { + fn: withOutputs, + actual: func(r *generator) string { return r.outputs }, }, - "WithProviders": { - fn: WithProviders, - actual: func(r *Generator) string { return r.providers }, + "withProviders": { + fn: withProviders, + actual: func(r *generator) string { return r.providers }, }, - "WithRequirements": { - fn: WithRequirements, - actual: func(r *Generator) string { return r.requirements }, + "withRequirements": { + fn: withRequirements, + actual: func(r *generator) string { return r.requirements }, }, - "WithResources": { - fn: WithResources, - actual: func(r *Generator) string { return r.resources }, + "withResources": { + fn: withResources, + actual: func(r *generator) string { return r.resources }, }, } for name, tt := range tests { t.Run(name, func(t *testing.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)) }) } } +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) { - generator := NewGenerator("foo", "") - generator.ForEach(func(name string) (string, error) { + config := print.DefaultConfig() + + generator := newGenerator(config, false) + generator.forEach(func(name string) (string, error) { return name, nil }) diff --git a/format/json.go b/format/json.go index cc5f1a7..357ca9f 100644 --- a/format/json.go +++ b/format/json.go @@ -21,7 +21,7 @@ import ( // json represents JSON format. type json struct { - *print.Generator + *generator config *print.Config } @@ -29,7 +29,7 @@ type json struct { // NewJSON returns new instance of JSON. func NewJSON(config *print.Config) Type { return &json{ - Generator: print.NewGenerator("json", config.ModuleRoot), + generator: newGenerator(config, false), config: config, } } @@ -47,7 +47,7 @@ func (j *json) Generate(module *terraform.Module) error { return err } - j.Generator.Funcs(print.WithContent(strings.TrimSuffix(buffer.String(), "\n"))) + j.generator.funcs(withContent(strings.TrimSuffix(buffer.String(), "\n"))) return nil } diff --git a/format/json_test.go b/format/json_test.go index 3aa627e..2c401cf 100644 --- a/format/json_test.go +++ b/format/json_test.go @@ -105,10 +105,7 @@ func TestJson(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/format/markdown_document.go b/format/markdown_document.go index 977eaf8..23f8982 100644 --- a/format/markdown_document.go +++ b/format/markdown_document.go @@ -24,7 +24,7 @@ var markdownDocumentFS embed.FS // markdownDocument represents Markdown Document format. type markdownDocument struct { - *print.Generator + *generator config *print.Config template *template.Template @@ -59,7 +59,7 @@ func NewMarkdownDocument(config *print.Config) Type { }) return &markdownDocument{ - Generator: print.NewGenerator("json", config.ModuleRoot), + generator: newGenerator(config, true), config: config, template: tt, } @@ -67,7 +67,7 @@ func NewMarkdownDocument(config *print.Config) Type { // Generate a Terraform module as Markdown document. 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) if err != nil { return "", err @@ -75,6 +75,8 @@ func (d *markdownDocument) Generate(module *terraform.Module) error { return sanitize(rendered), nil }) + d.generator.funcs(withModule(module)) + return err } diff --git a/format/markdown_document_test.go b/format/markdown_document_test.go index 232c130..fc119a3 100644 --- a/format/markdown_document_test.go +++ b/format/markdown_document_test.go @@ -190,10 +190,7 @@ func TestMarkdownDocument(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/format/markdown_table.go b/format/markdown_table.go index 4ab1783..19214d6 100644 --- a/format/markdown_table.go +++ b/format/markdown_table.go @@ -24,7 +24,7 @@ var markdownTableFS embed.FS // markdownTable represents Markdown Table format. type markdownTable struct { - *print.Generator + *generator config *print.Config template *template.Template @@ -50,7 +50,7 @@ func NewMarkdownTable(config *print.Config) Type { }) return &markdownTable{ - Generator: print.NewGenerator("markdown table", config.ModuleRoot), + generator: newGenerator(config, true), config: config, template: tt, } @@ -58,7 +58,7 @@ func NewMarkdownTable(config *print.Config) Type { // Generate a Terraform module as Markdown tables. 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) if err != nil { return "", err @@ -66,6 +66,8 @@ func (t *markdownTable) Generate(module *terraform.Module) error { return sanitize(rendered), nil }) + t.generator.funcs(withModule(module)) + return err } diff --git a/format/markdown_table_test.go b/format/markdown_table_test.go index 0527370..2de24cf 100644 --- a/format/markdown_table_test.go +++ b/format/markdown_table_test.go @@ -190,10 +190,7 @@ func TestMarkdownTable(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/format/pretty.go b/format/pretty.go index 462f9c2..7776773 100644 --- a/format/pretty.go +++ b/format/pretty.go @@ -26,7 +26,7 @@ var prettyTpl []byte // pretty represents colorized pretty format. type pretty struct { - *print.Generator + *generator config *print.Config template *template.Template @@ -50,7 +50,7 @@ func NewPretty(config *print.Config) Type { }) return &pretty{ - Generator: print.NewGenerator("pretty", config.ModuleRoot), + generator: newGenerator(config, true), config: config, template: tt, } @@ -63,7 +63,8 @@ func (p *pretty) Generate(module *terraform.Module) error { 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 } diff --git a/format/pretty_test.go b/format/pretty_test.go index 7ce60ff..181e445 100644 --- a/format/pretty_test.go +++ b/format/pretty_test.go @@ -105,10 +105,7 @@ func TestPretty(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/print/testdata/sample-file.txt b/format/testdata/generator/sample-file.txt similarity index 100% rename from print/testdata/sample-file.txt rename to format/testdata/generator/sample-file.txt diff --git a/format/tfvars_hcl.go b/format/tfvars_hcl.go index c487e32..87f1f5b 100644 --- a/format/tfvars_hcl.go +++ b/format/tfvars_hcl.go @@ -27,7 +27,7 @@ var tfvarsHCLTpl []byte // tfvarsHCL represents Terraform tfvars HCL format. type tfvarsHCL struct { - *print.Generator + *generator config *print.Config template *template.Template @@ -60,7 +60,7 @@ func NewTfvarsHCL(config *print.Config) Type { }) return &tfvarsHCL{ - Generator: print.NewGenerator("tfvars hcl", config.ModuleRoot), + generator: newGenerator(config, false), config: config, template: tt, } @@ -75,7 +75,7 @@ func (h *tfvarsHCL) Generate(module *terraform.Module) error { return err } - h.Generator.Funcs(print.WithContent(strings.TrimSuffix(sanitize(rendered), "\n"))) + h.generator.funcs(withContent(strings.TrimSuffix(sanitize(rendered), "\n"))) return nil } diff --git a/format/tfvars_hcl_test.go b/format/tfvars_hcl_test.go index 8c84ee0..92c2691 100644 --- a/format/tfvars_hcl_test.go +++ b/format/tfvars_hcl_test.go @@ -97,10 +97,7 @@ func TestTfvarsHcl(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/format/tfvars_json.go b/format/tfvars_json.go index adaf252..c6203f8 100644 --- a/format/tfvars_json.go +++ b/format/tfvars_json.go @@ -23,7 +23,7 @@ import ( // tfvarsJSON represents Terraform tfvars JSON format. type tfvarsJSON struct { - *print.Generator + *generator config *print.Config } @@ -31,7 +31,7 @@ type tfvarsJSON struct { // NewTfvarsJSON returns new instance of TfvarsJSON. func NewTfvarsJSON(config *print.Config) Type { return &tfvarsJSON{ - Generator: print.NewGenerator("tfvars json", config.ModuleRoot), + generator: newGenerator(config, false), config: config, } } @@ -53,10 +53,9 @@ func (j *tfvarsJSON) Generate(module *terraform.Module) error { return err } - j.Generator.Funcs(print.WithContent(strings.TrimSuffix(buffer.String(), "\n"))) + j.generator.funcs(withContent(strings.TrimSuffix(buffer.String(), "\n"))) return nil - } func init() { diff --git a/format/tfvars_json_test.go b/format/tfvars_json_test.go index 2b255d1..b90f402 100644 --- a/format/tfvars_json_test.go +++ b/format/tfvars_json_test.go @@ -88,10 +88,7 @@ func TestTfvarsJson(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/format/toml.go b/format/toml.go index d9b44c3..ce46298 100644 --- a/format/toml.go +++ b/format/toml.go @@ -22,7 +22,7 @@ import ( // toml represents TOML format. type toml struct { - *print.Generator + *generator config *print.Config } @@ -30,7 +30,7 @@ type toml struct { // NewTOML returns new instance of TOML. func NewTOML(config *print.Config) Type { return &toml{ - Generator: print.NewGenerator("toml", config.ModuleRoot), + generator: newGenerator(config, false), config: config, } } @@ -46,7 +46,7 @@ func (t *toml) Generate(module *terraform.Module) error { return err } - t.Generator.Funcs(print.WithContent(strings.TrimSuffix(buffer.String(), "\n"))) + t.generator.funcs(withContent(strings.TrimSuffix(buffer.String(), "\n"))) return nil diff --git a/format/toml_test.go b/format/toml_test.go index 0a92515..3ee2464 100644 --- a/format/toml_test.go +++ b/format/toml_test.go @@ -98,10 +98,7 @@ func TestToml(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/format/type.go b/format/type.go index 19a1579..994b4ae 100644 --- a/format/type.go +++ b/format/type.go @@ -32,7 +32,7 @@ type Type interface { Requirements() string // requirements 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. diff --git a/format/xml.go b/format/xml.go index 7091a82..1093e94 100644 --- a/format/xml.go +++ b/format/xml.go @@ -20,7 +20,7 @@ import ( // xml represents XML format. type xml struct { - *print.Generator + *generator config *print.Config } @@ -28,7 +28,7 @@ type xml struct { // NewXML returns new instance of XML. func NewXML(config *print.Config) Type { return &xml{ - Generator: print.NewGenerator("xml", config.ModuleRoot), + generator: newGenerator(config, false), config: config, } } @@ -42,7 +42,7 @@ func (x *xml) Generate(module *terraform.Module) error { return err } - x.Generator.Funcs(print.WithContent(strings.TrimSuffix(string(out), "\n"))) + x.generator.funcs(withContent(strings.TrimSuffix(string(out), "\n"))) return nil } diff --git a/format/xml_test.go b/format/xml_test.go index 081291c..a8390ba 100644 --- a/format/xml_test.go +++ b/format/xml_test.go @@ -98,10 +98,7 @@ func TestXml(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/format/yaml.go b/format/yaml.go index 16245fb..a12a125 100644 --- a/format/yaml.go +++ b/format/yaml.go @@ -22,7 +22,7 @@ import ( // yaml represents YAML format. type yaml struct { - *print.Generator + *generator config *print.Config } @@ -30,7 +30,7 @@ type yaml struct { // NewYAML returns new instance of YAML. func NewYAML(config *print.Config) Type { return &yaml{ - Generator: print.NewGenerator("yaml", config.ModuleRoot), + generator: newGenerator(config, false), config: config, } } @@ -47,7 +47,7 @@ func (y *yaml) Generate(module *terraform.Module) error { return err } - y.Generator.Funcs(print.WithContent(strings.TrimSuffix(buffer.String(), "\n"))) + y.generator.funcs(withContent(strings.TrimSuffix(buffer.String(), "\n"))) return nil } diff --git a/format/yaml_test.go b/format/yaml_test.go index be6f642..2d9c7c3 100644 --- a/format/yaml_test.go +++ b/format/yaml_test.go @@ -98,10 +98,7 @@ func TestYaml(t *testing.T) { err = formatter.Generate(module) assert.Nil(err) - actual, err := formatter.ExecuteTemplate("") - - assert.Nil(err) - assert.Equal(expected, actual) + assert.Equal(expected, formatter.Content()) }) } } diff --git a/internal/cli/run.go b/internal/cli/run.go index f0cea1f..b48f6ea 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -360,7 +360,7 @@ func generateContent(config *print.Config) error { return err } - content, err := formatter.ExecuteTemplate(config.Content) + content, err := formatter.Render(config.Content) if err != nil { return err } diff --git a/print/doc.go b/print/doc.go index e5e3038..888fff9 100644 --- a/print/doc.go +++ b/print/doc.go @@ -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) // and also it renders all of them, in a predefined order, in `Content()`. // -// It also provides `ExecuteTemplate(string)` function to process and render the -// template to generate the final output content. Following variables and functions are -// available: +// It also provides `Render(string)` function to process and render the template to generate +// the final output content. Following variables and functions are available: // // • `{{ .Header }}` // • `{{ .Footer }}` diff --git a/print/generator.go b/print/generator.go deleted file mode 100644 index 70ab34f..0000000 --- a/print/generator.go +++ /dev/null @@ -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 -} diff --git a/template/template.go b/template/template.go index ee6d71c..04681e8 100644 --- a/template/template.go +++ b/template/template.go @@ -74,6 +74,19 @@ func (t *Template) applyCustomFunc() { // Render template with given Module struct. 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 { 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))) } - if err := tmpl.ExecuteTemplate(&buffer, item.Name, struct { - Module *terraform.Module - Config *print.Config - }{ - Module: module, - Config: t.config, - }); err != nil { + if err := tmpl.ExecuteTemplate(&buffer, item.Name, data); err != nil { return "", err }