From c94b43ab80d11962ca73d613a3411441127392fb Mon Sep 17 00:00:00 2001 From: Khosrow Moossavi Date: Tue, 16 Mar 2021 19:17:56 -0400 Subject: [PATCH] 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 --- docs/user-guide/configuration.md | 37 ++++++- docs/user-guide/how-to.md | 2 +- internal/cli/config.go | 21 +++- .../cli/testdata/writer/mode-inject.golden | 19 ++++ internal/cli/testdata/writer/mode-inject.md | 20 ++++ .../writer/mode-replace-with-comment.golden | 3 + .../mode-replace-without-comment.golden | 1 + .../mode-replace-without-template.golden | 1 + internal/cli/testdata/writer/mode-replace.md | 20 ++++ internal/cli/writer.go | 78 ++++++++------ internal/cli/writer_test.go | 100 +++++++++++++++++- 11 files changed, 260 insertions(+), 42 deletions(-) create mode 100644 internal/cli/testdata/writer/mode-inject.golden create mode 100644 internal/cli/testdata/writer/mode-inject.md create mode 100644 internal/cli/testdata/writer/mode-replace-with-comment.golden create mode 100644 internal/cli/testdata/writer/mode-replace-without-comment.golden create mode 100644 internal/cli/testdata/writer/mode-replace-without-template.golden create mode 100644 internal/cli/testdata/writer/mode-replace.md diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index 0c48423..73bddaf 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -44,6 +44,7 @@ corresponding default values (if applicable). ```yaml formatter: + 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: ``` -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 and + # which will break the integrity yaml file. + + {{ .Content }} +``` diff --git a/docs/user-guide/how-to.md b/docs/user-guide/how-to.md index 9a4afda..af46d50 100644 --- a/docs/user-guide/how-to.md +++ b/docs/user-guide/how-to.md @@ -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. diff --git a/internal/cli/config.go b/internal/cli/config.go index 4948643..4997958 100644 --- a/internal/cli/config.go +++ b/internal/cli/config.go @@ -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)") diff --git a/internal/cli/testdata/writer/mode-inject.golden b/internal/cli/testdata/writer/mode-inject.golden new file mode 100644 index 0000000..2add952 --- /dev/null +++ b/internal/cli/testdata/writer/mode-inject.golden @@ -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 + + +Lorem ipsum dolor sit amet, consectetur adipiscing elit + + +## Baz + +esse cillum dolore eu fugiat nulla pariatur. diff --git a/internal/cli/testdata/writer/mode-inject.md b/internal/cli/testdata/writer/mode-inject.md new file mode 100644 index 0000000..d979fbb --- /dev/null +++ b/internal/cli/testdata/writer/mode-inject.md @@ -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 + + +ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit + + +## Baz + +esse cillum dolore eu fugiat nulla pariatur. diff --git a/internal/cli/testdata/writer/mode-replace-with-comment.golden b/internal/cli/testdata/writer/mode-replace-with-comment.golden new file mode 100644 index 0000000..21f6ef3 --- /dev/null +++ b/internal/cli/testdata/writer/mode-replace-with-comment.golden @@ -0,0 +1,3 @@ + +Lorem ipsum dolor sit amet, consectetur adipiscing elit + \ No newline at end of file diff --git a/internal/cli/testdata/writer/mode-replace-without-comment.golden b/internal/cli/testdata/writer/mode-replace-without-comment.golden new file mode 100644 index 0000000..df46cce --- /dev/null +++ b/internal/cli/testdata/writer/mode-replace-without-comment.golden @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit \ No newline at end of file diff --git a/internal/cli/testdata/writer/mode-replace-without-template.golden b/internal/cli/testdata/writer/mode-replace-without-template.golden new file mode 100644 index 0000000..df46cce --- /dev/null +++ b/internal/cli/testdata/writer/mode-replace-without-template.golden @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit \ No newline at end of file diff --git a/internal/cli/testdata/writer/mode-replace.md b/internal/cli/testdata/writer/mode-replace.md new file mode 100644 index 0000000..d979fbb --- /dev/null +++ b/internal/cli/testdata/writer/mode-replace.md @@ -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 + + +ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit + + +## Baz + +esse cillum dolore eu fugiat nulla pariatur. diff --git a/internal/cli/writer.go b/internal/cli/writer.go index 4a67d1f..37de9ef 100644 --- a/internal/cli/writer.go +++ b/internal/cli/writer.go @@ -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) } diff --git a/internal/cli/writer_test.go b/internal/cli/writer_test.go index 391250d..ac33417 100644 --- a/internal/cli/writer_test.go +++ b/internal/cli/writer_test.go @@ -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()) + } }) } }