diff --git a/README.md b/README.md index b20d362..c9e1428 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,17 @@ In case of trouble: [Go Modules FAQ][mod-faq]. [mod-faq]: https://github.com/golang/go/wiki/Modules#why-does-installing-a-tool-via-go-get-fail-with-error-cannot-find-main-module +## Tools + +Go-toml provides one handy command line tool: + + * `tomljson`: Reads a TOML file and outputs its JSON representation. + + ``` + $ go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest + $ tomljson --help + ``` + ## Migrating from v1 This section describes the differences between v1 and v2, with some pointers on diff --git a/cmd/tomljson/main.go b/cmd/tomljson/main.go new file mode 100644 index 0000000..ff81a20 --- /dev/null +++ b/cmd/tomljson/main.go @@ -0,0 +1,79 @@ +// Tomljson reads TOML and converts to JSON. +// +// Usage: +// cat file.toml | tomljson > file.json +// tomljson file1.toml > file.json +package main + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "os" + + "github.com/pelletier/go-toml/v2" +) + +func usage() { + fmt.Fprint(os.Stderr, `tomljson can be used in two ways: +Reading from stdin: + cat file.toml | tomljson > file.json + +Reading from a file: + tomljson file.toml > file.json +`) +} + +func init() { + flag.Usage = usage +} + +func main() { + flag.Parse() + os.Exit(processMain(flag.Args(), os.Stdin, os.Stdout, os.Stderr)) +} + +func processMain(files []string, input io.Reader, output, error io.Writer) int { + err := run(files, input, output) + if err != nil { + var derr *toml.DecodeError + if errors.As(err, &derr) { + fmt.Fprintln(error, derr.String()) + row, col := derr.Position() + fmt.Fprintln(error, "error occurred at row", row, "column", col) + } else { + fmt.Fprintln(error, err.Error()) + } + return -1 + } + return 0 +} + +func run(files []string, input io.Reader, output io.Writer) error { + if len(files) > 0 { + f, err := os.Open(files[0]) + if err != nil { + return err + } + defer f.Close() + input = f + } + + return convert(input, output) +} + +func convert(r io.Reader, w io.Writer) error { + var v interface{} + + d := toml.NewDecoder(r) + err := d.Decode(&v) + if err != nil { + return err + } + + e := json.NewEncoder(w) + e.SetIndent("", " ") + return e.Encode(v) +} diff --git a/cmd/tomljson/main_test.go b/cmd/tomljson/main_test.go new file mode 100644 index 0000000..8c67383 --- /dev/null +++ b/cmd/tomljson/main_test.go @@ -0,0 +1,154 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "runtime" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func expectBufferEquality(t *testing.T, name string, buffer *bytes.Buffer, expected string) { + t.Helper() + output := buffer.String() + assert.Equal(t, expected, output, fmt.Sprintf("%s does not match", name)) +} + +func expectProcessMainResults(t *testing.T, input io.Reader, args []string, exitCode int, expectedOutput string, expectedError string) { + t.Helper() + outputBuffer := new(bytes.Buffer) + errorBuffer := new(bytes.Buffer) + + returnCode := processMain(args, input, outputBuffer, errorBuffer) + + expectBufferEquality(t, "stdout", outputBuffer, expectedOutput) + expectBufferEquality(t, "stderr", errorBuffer, expectedError) + + require.Equal(t, exitCode, returnCode, "exit codes should match") +} + +func expect(t *testing.T, input string, args []string, exitCode int, expectedOutput string, expectedError string) { + t.Helper() + r := strings.NewReader(input) + expectProcessMainResults(t, r, args, exitCode, expectedOutput, expectedError) +} + +func TestProcessMainReadFromStdin(t *testing.T) { + input := ` + [mytoml] + a = 42` + expectedOutput := `{ + "mytoml": { + "a": 42 + } +} +` + expect(t, input, []string{}, 0, expectedOutput, ``) +} + +func TestProcessMainReadInvalidTOML(t *testing.T) { + input := `bad = []]` + expectedError := `1| bad = []] + | ~ expected newline but got U+005D ']' +error occurred at row 1 column 9 +` + + expect(t, input, []string{}, -1, ``, expectedError) +} + +type badReader struct{} + +func (r *badReader) Read([]byte) (int, error) { + return 0, fmt.Errorf("reader failed on purpose") +} + +func TestProcessMainProblemReadingFile(t *testing.T) { + expectedError := `toml: reader failed on purpose +` + input := &badReader{} + + expectProcessMainResults(t, input, []string{}, -1, ``, expectedError) +} + +func TestProcessMainReadFromFile(t *testing.T) { + input := ` + [mytoml] + a = 42` + + tmpfile, err := ioutil.TempFile("", "example.toml") + if err != nil { + t.Fatal(err) + } + if _, err := tmpfile.Write([]byte(input)); err != nil { + t.Fatal(err) + } + + defer os.Remove(tmpfile.Name()) + + expectedOutput := `{ + "mytoml": { + "a": 42 + } +} +` + expectedError := `` + expectedExitCode := 0 + + expect(t, ``, []string{tmpfile.Name()}, expectedExitCode, expectedOutput, expectedError) +} + +func TestProcessMainReadFromMissingFile(t *testing.T) { + var expectedError string + if runtime.GOOS == "windows" { + expectedError = `open /this/file/does/not/exist: The system cannot find the path specified. +` + } else { + expectedError = `open /this/file/does/not/exist: no such file or directory +` + } + + expect(t, ``, []string{"/this/file/does/not/exist"}, -1, ``, expectedError) +} + +func TestMainUsage(t *testing.T) { + out := doAndCaptureStderr(usage) + require.NotEmpty(t, out) +} + +func doAndCaptureStderr(f func()) string { + orig := os.Stderr + defer func() { os.Stderr = orig }() + + r, w, err := os.Pipe() + if err != nil { + panic(err) + } + + b := new(bytes.Buffer) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + _, err := io.Copy(b, r) + if err != nil { + panic(err) + } + }() + + os.Stderr = w + + f() + + w.Close() + wg.Wait() + + return b.String() +}