Template should be optional for output-mode 'replace'

Template is optional when writing to --output-file and --output-mode is
'replace'. This is useful in particular when user wants to directly
output to a known file format, for example json, yaml, in which
sourrounding comments will break linters or functionality of those
formats.

Signed-off-by: Khosrow Moossavi <khos2ow@gmail.com>
This commit is contained in:
Khosrow Moossavi
2021-03-16 19:17:56 -04:00
parent 0993f62f16
commit c94b43ab80
11 changed files with 260 additions and 42 deletions

View File

@@ -44,6 +44,7 @@ corresponding default values (if applicable).
```yaml
formatter: <FORMATTER_NAME>
header-from: main.tf
footer-from: ""
@@ -145,7 +146,8 @@ flag) is not empty. Insersion behavior can be controlled by `output.mode` (or
- `inject` (default)
Partially replace the `output-file` with generated output. This will fail if
`output-file` doesn't exist.
`output-file` doesn't exist. Also will fail if `output-file` doesn't already
have surrounding comments.
- `replace`
@@ -153,8 +155,12 @@ flag) is not empty. Insersion behavior can be controlled by `output.mode` (or
the `output-file` if it doesn't exist.
The output generated by formatters (`markdown`, `asciidoc`, etc) will first be
inserted into a template before getting saved into the file. This template can
be customized with `output.template` or `--output-template string` CLI flag.
inserted into a template, if provided, before getting saved into the file. This
template can be customized with `output.template` or `--output-template string`
CLI flag.
**Note:** `output.template` is optional for mode `replace`.
The default template value is:
```go
@@ -163,8 +169,7 @@ The default template value is:
<!-- END_TF_DOCS -->
```
This template consists of three items, all of them are mandatory and have to be on
separate lines:
This template consists of at least three lines (all of which are mandatory):
- begin comment
- `{{ .Content }}` slug
@@ -172,3 +177,25 @@ separate lines:
You may change the wording of comment as you wish, but the comment must be present
in the template. Also note that `SPACE`s inside `{{ }}` are mandatory.
You may also add as many lines as you'd like before or after `{{ .Content }}` line.
**Note:** `{{ .Content }}` is mandatory if you want to customize template for mode
`replace`. For example if you wish to output to YAML file with trailing comment, the
following can be used:
```yaml
formatter: yaml
output:
file: output.yaml
mode: replace
template: |-
# Example trailing comments block which will be placed at the top of the
# 'output.yaml' file.
#
# Note that there's no <!-- BEGIN_TF_DOCS --> and <!-- END_TF_DOCS -->
# which will break the integrity yaml file.
{{ .Content }}
```

View File

@@ -71,7 +71,7 @@ configuration file.
## Insert Output To File
Since `v0.12.0` generated output can be insterted directly into the file. There
Since `v0.12.0`, generated output can be insterted directly into the file. There
are two modes of insersion: `inject` (default) or `replace`. Take a look at [output]
configuration for all the details.

View File

@@ -156,20 +156,35 @@ func defaultOutput() output {
}
}
func (o *output) validate() error {
func (o *output) validate() error { //nolint:gocyclo
// NOTE(khos2ow): this function is over our cyclomatic complexity goal.
// Be wary when adding branches, and look for functionality that could
// be reasonably moved into an injected dependency.
if o.File != "" {
if o.Mode == "" {
return fmt.Errorf("value of '--output-mode' can't be empty")
}
// Template is optional for mode 'replace'
if o.Mode == outputModeReplace && o.Template == "" {
return nil
}
if o.Template == "" {
return fmt.Errorf("value of '--output-template' can't be empty")
}
index := strings.Index(o.Template, outputContent)
if index < 0 {
if index := strings.Index(o.Template, outputContent); index < 0 {
return fmt.Errorf("value of '--output-template' doesn't have '{{ .Content }}' (note that spaces inside '{{ }}' are mandatory)")
}
// No extra validation is needed for mode 'replace',
// the followings only apply for every other modes.
if o.Mode == outputModeReplace {
return nil
}
lines := strings.Split(o.Template, "\n")
if len(lines) < 3 {
return fmt.Errorf("value of '--output-template' should contain at least 3 lines (begin comment, {{ .Content }}, and end comment)")

View File

@@ -0,0 +1,19 @@
# Foo
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
## Bar
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua.
- Ut enim ad minim veniam
- quis nostrud exercitation
<!-- BEGIN_TF_DOCS -->
Lorem ipsum dolor sit amet, consectetur adipiscing elit
<!-- END_TF_DOCS -->
## Baz
esse cillum dolore eu fugiat nulla pariatur.

View File

@@ -0,0 +1,20 @@
# Foo
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
## Bar
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua.
- Ut enim ad minim veniam
- quis nostrud exercitation
<!-- BEGIN_TF_DOCS -->
ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit
<!-- END_TF_DOCS -->
## Baz
esse cillum dolore eu fugiat nulla pariatur.

View File

@@ -0,0 +1,3 @@
<!-- BEGIN_TF_DOCS -->
Lorem ipsum dolor sit amet, consectetur adipiscing elit
<!-- END_TF_DOCS -->

View File

@@ -0,0 +1 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit

View File

@@ -0,0 +1 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit

View File

@@ -0,0 +1,20 @@
# Foo
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
## Bar
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua.
- Ut enim ad minim veniam
- quis nostrud exercitation
<!-- BEGIN_TF_DOCS -->
ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit
<!-- END_TF_DOCS -->
## Baz
esse cillum dolore eu fugiat nulla pariatur.

View File

@@ -13,6 +13,7 @@ package cli
import (
"bytes"
"errors"
"io"
"os"
"path/filepath"
"strings"
@@ -55,12 +56,20 @@ type fileWriter struct {
template string
begin string
end string
writer io.Writer
}
func (fw *fileWriter) Write(p []byte) (int, error) {
filename := filepath.Join(fw.dir, fw.file)
var buf bytes.Buffer
if fw.template == "" {
// template is optional for mode replace
if fw.mode == outputModeReplace {
return fw.write(filename, p)
}
return 0, errors.New(errTemplateEmpty)
}
@@ -73,38 +82,45 @@ func (fw *fileWriter) Write(p []byte) (int, error) {
return 0, err
}
content := buf.String()
filename := filepath.Join(fw.dir, fw.file)
if fw.mode == outputModeInject {
f, err := os.ReadFile(filename)
if err != nil {
return 0, err
}
fc := string(f)
if fc == "" {
return 0, errors.New(errFileEmpty)
}
before := strings.Index(fc, fw.begin)
if before < 0 {
return 0, errors.New(errBeginCommentMissing)
}
content = fc[:before] + content
after := strings.Index(fc, fw.end)
if after < 0 {
return 0, errors.New(errEndCommentMissing)
}
if after < before {
return 0, errors.New(errEndCommentBeforeBegin)
}
content += fc[after+len(fw.end):]
// Replace the content of 'filename' with generated output,
// no further processing is reequired for mode 'replace'.
if fw.mode == outputModeReplace {
return fw.write(filename, buf.Bytes())
}
n := len(content)
err := os.WriteFile(filename, []byte(content), 0644)
content := buf.String()
return n, err
f, err := os.ReadFile(filename)
if err != nil {
return 0, err
}
fc := string(f)
if fc == "" {
return 0, errors.New(errFileEmpty)
}
before := strings.Index(fc, fw.begin)
if before < 0 {
return 0, errors.New(errBeginCommentMissing)
}
content = fc[:before] + content
after := strings.Index(fc, fw.end)
if after < 0 {
return 0, errors.New(errEndCommentMissing)
}
if after < before {
return 0, errors.New(errEndCommentBeforeBegin)
}
content += fc[after+len(fw.end):]
return fw.write(filename, []byte(content))
}
func (fw *fileWriter) write(filename string, p []byte) (int, error) {
if fw.writer != nil {
return fw.writer.Write(p)
}
return len(p), os.WriteFile(filename, p, 0644)
}

View File

@@ -11,11 +11,14 @@ the root directory of this source tree.
package cli
import (
"bytes"
"io"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/terraform-docs/terraform-docs/internal/testutil"
)
func TestFileWriter(t *testing.T) {
@@ -26,14 +29,73 @@ func TestFileWriter(t *testing.T) {
template string
begin string
end string
writer io.Writer
expected string
wantErr bool
errMsg string
}{
// Successful writes
"ModeInject": {
file: "mode-inject.md",
mode: "inject",
template: OutputTemplate,
begin: outputBeginComment,
end: outputEndComment,
writer: &bytes.Buffer{},
expected: "mode-inject",
wantErr: false,
errMsg: "",
},
"ModeReplaceWithComment": {
file: "mode-replace.md",
mode: "replace",
template: OutputTemplate,
begin: outputBeginComment,
end: outputEndComment,
writer: &bytes.Buffer{},
expected: "mode-replace-with-comment",
wantErr: false,
errMsg: "",
},
"ModeReplaceWithoutComment": {
file: "mode-replace.md",
mode: "replace",
template: outputContent,
begin: "",
end: "",
writer: &bytes.Buffer{},
expected: "mode-replace-without-comment",
wantErr: false,
errMsg: "",
},
"ModeReplaceWithoutTemplate": {
file: "mode-replace.md",
mode: "replace",
template: "",
begin: "",
end: "",
writer: &bytes.Buffer{},
expected: "mode-replace-without-template",
wantErr: false,
errMsg: "",
},
// Error writes
"ModeInjectNoFile": {
file: "file-missing.md",
mode: "inject",
template: OutputTemplate,
begin: outputBeginComment,
end: outputEndComment,
writer: nil,
expected: "",
wantErr: true,
errMsg: "open testdata/writer/file-missing.md: no such file or directory",
},
"EmptyTemplate": {
@@ -42,6 +104,10 @@ func TestFileWriter(t *testing.T) {
template: "",
begin: outputBeginComment,
end: outputEndComment,
writer: nil,
expected: "",
wantErr: true,
errMsg: "template is missing",
},
"EmptyFile": {
@@ -50,6 +116,10 @@ func TestFileWriter(t *testing.T) {
template: OutputTemplate,
begin: outputBeginComment,
end: outputEndComment,
writer: nil,
expected: "",
wantErr: true,
errMsg: "file content is empty",
},
"BeginCommentMissing": {
@@ -58,6 +128,10 @@ func TestFileWriter(t *testing.T) {
template: OutputTemplate,
begin: outputBeginComment,
end: outputEndComment,
writer: nil,
expected: "",
wantErr: true,
errMsg: "begin comment is missing",
},
"EndCommentMissing": {
@@ -66,6 +140,10 @@ func TestFileWriter(t *testing.T) {
template: OutputTemplate,
begin: outputBeginComment,
end: outputEndComment,
writer: nil,
expected: "",
wantErr: true,
errMsg: "end comment is missing",
},
"EndCommentBeforeBegin": {
@@ -74,6 +152,10 @@ func TestFileWriter(t *testing.T) {
template: OutputTemplate,
begin: outputBeginComment,
end: outputEndComment,
writer: nil,
expected: "",
wantErr: true,
errMsg: "end comment is before begin comment",
},
}
@@ -90,12 +172,26 @@ func TestFileWriter(t *testing.T) {
template: tt.template,
begin: tt.begin,
end: tt.end,
writer: tt.writer,
}
_, err := io.WriteString(writer, content)
assert.NotNil(err)
assert.Equal(tt.errMsg, err.Error())
if tt.wantErr {
assert.NotNil(err)
assert.Equal(tt.errMsg, err.Error())
} else {
assert.Nil(err)
w, ok := tt.writer.(*bytes.Buffer)
assert.True(ok, "tt.writer is not a valid bytes.Buffer")
expected, err := testutil.GetExpected("writer", tt.expected)
assert.Nil(err)
assert.Equal(expected, w.String())
}
})
}
}