Skip to content

Commit

Permalink
In output-mode inject do not fail if file not found
Browse files Browse the repository at this point in the history
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

Signed-off-by: Khosrow Moossavi <khos2ow@gmail.com>
  • Loading branch information
khos2ow committed Apr 28, 2021
1 parent f957813 commit 4be2223
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 56 deletions.
6 changes: 3 additions & 3 deletions docs/user-guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
3 changes: 3 additions & 0 deletions internal/cli/testdata/writer/mode-inject-empty-file.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- BEGIN_TF_DOCS -->
Lorem ipsum dolor sit amet, consectetur adipiscing elit
<!-- END_TF_DOCS -->
3 changes: 3 additions & 0 deletions internal/cli/testdata/writer/mode-inject-file-missing.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- BEGIN_TF_DOCS -->
Lorem ipsum dolor sit amet, consectetur adipiscing elit
<!-- END_TF_DOCS -->
22 changes: 22 additions & 0 deletions internal/cli/testdata/writer/mode-inject-no-comment.golden
Original file line number Diff line number Diff line change
@@ -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 -->
18 changes: 18 additions & 0 deletions internal/cli/testdata/writer/mode-inject-no-comment.md
Original file line number Diff line number Diff line change
@@ -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.
93 changes: 64 additions & 29 deletions internal/cli/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"))
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
84 changes: 60 additions & 24 deletions internal/cli/writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down

0 comments on commit 4be2223

Please sign in to comment.