Repository: terraform-docs/terraform-docs
Pull request #484 (2021-04-28 00:55:43)

In output-mode inject do not fail if file not found
<!--
Thank you for helping to improve terraform-docs!

Please read through https://git.io/JtEzg if this is your first time opening a
terraform-docs pull request. Find us in https://slack.terraform-docs.io/messages/dev if
you need any help contributing.
-->

### Description of your changes

<!--
Briefly describe what this pull request does. Be sure to direct your reviewers'
attention to anything that needs special consideration.

We love pull requests that resolve an open terraform-docs issue. If yours does, you
can uncomment the below line to indicate which issue your PR fixes, for example
"Fixes #500":
-->

The following scenarios can happen for `--output-mode inject`:

- file exists, comments exist: inject output between comments
- file exists, comments not found: append output at the end of file
- file exists, but empty: save the whole output into the file
- file not found: create the target file and save the ooutput into it

<!-- Fixes # -->

I have:

- [x] Read and followed terraform-docs' [contribution process].
- [x] All tests pass when I run `make test`.

### How has this code been tested

<!--
Before reviewers can be confident in the correctness of this pull request, it
needs to tested and shown to be correct. Briefly describe the testing that has
already been done or which is planned for this change.
-->

Adjusted the test cases and added new ones (in `writer_test.go`)

[contribution process]: https://git.io/JtEzg



Patch:
diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md
index f7515b9..15c10cd 100644
--- a/docs/user-guide/configuration.md
+++ b/docs/user-guide/configuration.md
@@ -185,9 +185,9 @@ 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. Also will fail if `output-file` doesn't already
-  have surrounding comments.
+  Partially replace the `output-file` with generated output. This will create
+  the `output-file` if it doesn't exist. It will also append to `output-file`
+  if it doesn't have surrounding comments.
 
 - `replace`
 
diff --git a/internal/cli/writer.go b/internal/cli/writer.go
index 7e09d59..80de4b6 100644
--- a/internal/cli/writer.go
+++ b/internal/cli/writer.go
@@ -21,7 +21,6 @@ import (
 )
 
 const (
-	errFileEmpty             = "file content is empty"
 	errTemplateEmpty         = "template is missing"
 	errBeginCommentMissing   = "begin comment is missing"
 	errEndCommentMissing     = "end comment is missing"
@@ -31,6 +30,7 @@ const (
 // stdoutWriter writes content to os.Stdout.
 type stdoutWriter struct{}
 
+// Write content to Stdout
 func (sw *stdoutWriter) Write(p []byte) (int, error) {
 	return os.Stdout.Write([]byte(string(p) + "\n"))
 }
@@ -60,11 +60,10 @@ type fileWriter struct {
 	writer io.Writer
 }
 
+// Write content to target file
 func (fw *fileWriter) Write(p []byte) (int, error) {
 	filename := fw.fullFilePath()
 
-	var buf bytes.Buffer
-
 	if fw.template == "" {
 		// template is optional for mode replace
 		if fw.mode == outputModeReplace {
@@ -73,61 +72,97 @@ func (fw *fileWriter) Write(p []byte) (int, error) {
 		return 0, errors.New(errTemplateEmpty)
 	}
 
-	tmpl := template.Must(template.New("content").Parse(fw.template))
-	if err := tmpl.ExecuteTemplate(&buf, "content", struct {
-		Content string
-	}{
-		Content: string(p),
-	}); err != nil {
+	// apply template to generated output
+	buf, err := fw.apply(p)
+	if err != nil {
 		return 0, err
 	}
 
 	// Replace the content of 'filename' with generated output,
-	// no further processing is reequired for mode 'replace'.
+	// no further processing is required for mode 'replace'.
 	if fw.mode == outputModeReplace {
 		return fw.write(filename, buf.Bytes())
 	}
 
-	content := buf.String()
-
-	f, err := os.ReadFile(filename)
+	content, err := os.ReadFile(filename)
 	if err != nil {
-		return 0, err
+		// In mode 'inject', if target file not found:
+		// create it and save the generated output into it.
+		return fw.write(filename, buf.Bytes())
+	}
+
+	if len(content) == 0 {
+		// In mode 'inject', if target file is found BUT it's empty:
+		// save the generated output into it.
+		return fw.write(filename, buf.Bytes())
+	}
+
+	return fw.inject(filename, string(content), buf.String())
+}
+
+// fullFilePath of the target file. If file is absolute path it will be
+// used as is, otherwise dir (i.e. module root folder) will be joined to
+// it.
+func (fw *fileWriter) fullFilePath() string {
+	if filepath.IsAbs(fw.file) {
+		return fw.file
+	}
+	return filepath.Join(fw.dir, fw.file)
+}
+
+// apply template to generated output
+func (fw *fileWriter) apply(p []byte) (bytes.Buffer, error) {
+	type content struct {
+		Content string
 	}
 
-	fc := string(f)
-	if fc == "" {
-		return 0, errors.New(errFileEmpty)
+	var buf bytes.Buffer
+
+	tmpl := template.Must(template.New("content").Parse(fw.template))
+	err := tmpl.ExecuteTemplate(&buf, "content", content{string(p)})
+
+	return buf, err
+}
+
+// inject generated output into file.
+func (fw *fileWriter) inject(filename string, content string, generated string) (int, error) {
+	before := strings.Index(content, fw.begin)
+	after := strings.Index(content, fw.end)
+
+	// current file content doesn't have surrounding
+	// so we're going to append the generated output
+	// to current file.
+	if before < 0 && after < 0 {
+		return fw.write(filename, []byte(content+"\n"+generated))
 	}
 
-	before := strings.Index(fc, fw.begin)
+	// begin comment is missing
 	if before < 0 {
 		return 0, errors.New(errBeginCommentMissing)
 	}
-	content = fc[:before] + content
 
-	after := strings.Index(fc, fw.end)
+	generated = content[:before] + generated
+
+	// end comment is missing
 	if after < 0 {
 		return 0, errors.New(errEndCommentMissing)
 	}
+
+	// end comment is before begin comment
 	if after < before {
 		return 0, errors.New(errEndCommentBeforeBegin)
 	}
-	content += fc[after+len(fw.end):]
 
-	return fw.write(filename, []byte(content))
+	generated += content[after+len(fw.end):]
+
+	return fw.write(filename, []byte(generated))
 }
 
+// wrtie the content to io.Writer. If no io.Writer is available,
+// it will be written to 'filename'.
 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)
 }
-
-func (fw *fileWriter) fullFilePath() string {
-	if filepath.IsAbs(fw.file) {
-		return fw.file
-	}
-	return filepath.Join(fw.dir, fw.file)
-}


Test patch:
diff --git a/internal/cli/testdata/writer/mode-inject-empty-file.golden b/internal/cli/testdata/writer/mode-inject-empty-file.golden
new file mode 100644
index 0000000..21f6ef3
--- /dev/null
+++ b/internal/cli/testdata/writer/mode-inject-empty-file.golden
@@ -0,0 +1,3 @@
+<!-- BEGIN_TF_DOCS -->
+Lorem ipsum dolor sit amet, consectetur adipiscing elit
+<!-- END_TF_DOCS -->
\ No newline at end of file
diff --git a/internal/cli/testdata/writer/mode-inject-file-missing.golden b/internal/cli/testdata/writer/mode-inject-file-missing.golden
new file mode 100644
index 0000000..21f6ef3
--- /dev/null
+++ b/internal/cli/testdata/writer/mode-inject-file-missing.golden
@@ -0,0 +1,3 @@
+<!-- BEGIN_TF_DOCS -->
+Lorem ipsum dolor sit amet, consectetur adipiscing elit
+<!-- END_TF_DOCS -->
\ No newline at end of file
diff --git a/internal/cli/testdata/writer/mode-inject-no-comment.golden b/internal/cli/testdata/writer/mode-inject-no-comment.golden
new file mode 100644
index 0000000..fd03404
--- /dev/null
+++ b/internal/cli/testdata/writer/mode-inject-no-comment.golden
@@ -0,0 +1,22 @@
+# 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.
+
+<!-- BEGIN_TF_DOCS -->
+Lorem ipsum dolor sit amet, consectetur adipiscing elit
+<!-- END_TF_DOCS -->
\ No newline at end of file
diff --git a/internal/cli/testdata/writer/mode-inject-no-comment.md b/internal/cli/testdata/writer/mode-inject-no-comment.md
new file mode 100644
index 0000000..de77aae
--- /dev/null
+++ b/internal/cli/testdata/writer/mode-inject-no-comment.md
@@ -0,0 +1,18 @@
+# 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_test.go b/internal/cli/writer_test.go
index 5e4f66d..774e178 100644
--- a/internal/cli/writer_test.go
+++ b/internal/cli/writer_test.go
@@ -80,6 +80,42 @@ func TestFileWriter(t *testing.T) {
 			wantErr:  false,
 			errMsg:   "",
 		},
+		"ModeInjectEmptyFile": {
+			file:     "empty-file.md",
+			mode:     "inject",
+			template: OutputTemplate,
+			begin:    outputBeginComment,
+			end:      outputEndComment,
+			writer:   &bytes.Buffer{},
+
+			expected: "mode-inject-empty-file",
+			wantErr:  false,
+			errMsg:   "",
+		},
+		"ModeInjectNoCommentAppend": {
+			file:     "mode-inject-no-comment.md",
+			mode:     "inject",
+			template: OutputTemplate,
+			begin:    outputBeginComment,
+			end:      outputEndComment,
+			writer:   &bytes.Buffer{},
+
+			expected: "mode-inject-no-comment",
+			wantErr:  false,
+			errMsg:   "",
+		},
+		"ModeInjectFileMissing": {
+			file:     "file-missing.md",
+			mode:     "inject",
+			template: OutputTemplate,
+			begin:    outputBeginComment,
+			end:      outputEndComment,
+			writer:   &bytes.Buffer{},
+
+			expected: "mode-inject-file-missing",
+			wantErr:  false,
+			errMsg:   "",
+		},
 		"ModeReplaceWithComment": {
 			file:     "mode-replace.md",
 			mode:     "replace",
@@ -92,6 +128,30 @@ func TestFileWriter(t *testing.T) {
 			wantErr:  false,
 			errMsg:   "",
 		},
+		"ModeReplaceWithCommentEmptyFile": {
+			file:     "mode-replace.md",
+			mode:     "replace",
+			template: OutputTemplate,
+			begin:    outputBeginComment,
+			end:      outputEndComment,
+			writer:   &bytes.Buffer{},
+
+			expected: "mode-replace-with-comment",
+			wantErr:  false,
+			errMsg:   "",
+		},
+		"ModeReplaceWithCommentFileMissing": {
+			file:     "file-missing.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",
@@ -118,18 +178,6 @@ func TestFileWriter(t *testing.T) {
 		},
 
 		// 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": {
 			file:     "not-applicable.md",
 			mode:     "inject",
@@ -142,18 +190,6 @@ func TestFileWriter(t *testing.T) {
 			wantErr:  true,
 			errMsg:   "template is missing",
 		},
-		"EmptyFile": {
-			file:     "empty-file.md",
-			mode:     "inject",
-			template: OutputTemplate,
-			begin:    outputBeginComment,
-			end:      outputEndComment,
-			writer:   nil,
-
-			expected: "",
-			wantErr:  true,
-			errMsg:   "file content is empty",
-		},
 		"BeginCommentMissing": {
 			file:     "begin-comment-missing.md",
 			mode:     "inject",