diff --git a/README.md b/README.md index c9e1428..788d0ae 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ In case of trouble: [Go Modules FAQ][mod-faq]. ## Tools -Go-toml provides one handy command line tool: +Go-toml provides two handy command line tools: * `tomljson`: Reads a TOML file and outputs its JSON representation. @@ -218,6 +218,13 @@ Go-toml provides one handy command line tool: $ tomljson --help ``` + * `jsontoml`: Reads a JSON file and outputs a TOML representation. + + ``` + $ go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest + $ jsontoml --help + ``` + ## Migrating from v1 This section describes the differences between v1 and v2, with some pointers on diff --git a/cmd/jsontoml/main.go b/cmd/jsontoml/main.go new file mode 100644 index 0000000..0470586 --- /dev/null +++ b/cmd/jsontoml/main.go @@ -0,0 +1,38 @@ +// Jsontoml reads JSON and converts to TOML. +// +// Usage: +// cat file.toml | jsontoml > file.json +// jsontoml file1.toml > file.json +package main + +import ( + "encoding/json" + "io" + + "github.com/pelletier/go-toml/v2" + "github.com/pelletier/go-toml/v2/internal/cli" +) + +func main() { + usage := `jsontoml can be used in two ways: +Reading from stdin: + cat file.json | jsontoml > file.toml + +Reading from a file: + jsontoml file.json > file.toml +` + cli.Execute(usage, convert) +} + +func convert(r io.Reader, w io.Writer) error { + var v interface{} + + d := json.NewDecoder(r) + err := d.Decode(&v) + if err != nil { + return err + } + + e := toml.NewEncoder(w) + return e.Encode(v) +} diff --git a/cmd/jsontoml/main_test.go b/cmd/jsontoml/main_test.go new file mode 100644 index 0000000..7f2455b --- /dev/null +++ b/cmd/jsontoml/main_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConvert(t *testing.T) { + examples := []struct { + name string + input string + expected string + errors bool + }{ + { + name: "valid json", + input: ` +{ + "mytoml": { + "a": 42 + } +}`, + expected: `[mytoml] +a = 42.0 + +`, + }, + { + name: "invalid json", + input: `{ foo`, + errors: true, + }, + } + + for _, e := range examples { + b := new(bytes.Buffer) + err := convert(strings.NewReader(e.input), b) + if e.errors { + require.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, e.expected, b.String()) + } + } +} diff --git a/cmd/tomljson/main.go b/cmd/tomljson/main.go index ff81a20..9627ae9 100644 --- a/cmd/tomljson/main.go +++ b/cmd/tomljson/main.go @@ -1,67 +1,29 @@ -// Tomljson reads TOML and converts to JSON. +// Package tomljson is a program that converts TOML to JSON. // // Usage: // cat file.toml | tomljson > file.json -// tomljson file1.toml > file.json +// tomljson file.toml > file.json package main import ( "encoding/json" "errors" - "flag" "fmt" "io" - "os" "github.com/pelletier/go-toml/v2" + "github.com/pelletier/go-toml/v2/internal/cli" ) -func usage() { - fmt.Fprint(os.Stderr, `tomljson can be used in two ways: +func main() { + usage := `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) +` + cli.Execute(usage, convert) } func convert(r io.Reader, w io.Writer) error { @@ -70,6 +32,11 @@ func convert(r io.Reader, w io.Writer) error { d := toml.NewDecoder(r) err := d.Decode(&v) if err != nil { + var derr *toml.DecodeError + if errors.As(err, &derr) { + row, col := derr.Position() + return fmt.Errorf("%s\nerror occurred at row %d column %d", derr.String(), row, col) + } return err } diff --git a/cmd/tomljson/main_test.go b/cmd/tomljson/main_test.go index 8c67383..8e78751 100644 --- a/cmd/tomljson/main_test.go +++ b/cmd/tomljson/main_test.go @@ -4,63 +4,54 @@ 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 := `{ +func TestConvert(t *testing.T) { + examples := []struct { + name string + input io.Reader + expected string + errors bool + }{ + { + name: "valid toml", + input: strings.NewReader(` +[mytoml] +a = 42`), + expected: `{ "mytoml": { "a": 42 } } -` - expect(t, input, []string{}, 0, expectedOutput, ``) -} +`, + }, + { + name: "invalid toml", + input: strings.NewReader(`bad = []]`), + errors: true, + }, + { + name: "bad reader", + input: &badReader{}, + errors: true, + }, + } -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) + for _, e := range examples { + b := new(bytes.Buffer) + err := convert(e.input, b) + if e.errors { + require.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, e.expected, b.String()) + } + } } type badReader struct{} @@ -68,87 +59,3 @@ 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() -} diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..86acece --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,37 @@ +package cli + +import ( + "flag" + "fmt" + "io" + "os" +) + +type ConvertFn func(r io.Reader, w io.Writer) error + +func Execute(usage string, fn ConvertFn) { + flag.Usage = func() { fmt.Fprintf(os.Stderr, usage) } + flag.Parse() + os.Exit(processMain(flag.Args(), os.Stdin, os.Stdout, os.Stderr, fn)) +} + +func processMain(files []string, input io.Reader, output, error io.Writer, f ConvertFn) int { + err := run(files, input, output, f) + if err != nil { + fmt.Fprintln(error, err.Error()) + return -1 + } + return 0 +} + +func run(files []string, input io.Reader, output io.Writer, convert ConvertFn) 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) +} diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go new file mode 100644 index 0000000..2eeef8b --- /dev/null +++ b/internal/cli/cli_test.go @@ -0,0 +1,74 @@ +package cli + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProcessMainStdin(t *testing.T) { + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + input := strings.NewReader("this is the input") + + exit := processMain([]string{}, input, stdout, stderr, func(r io.Reader, w io.Writer) error { + return nil + }) + + assert.Equal(t, 0, exit) + assert.Empty(t, stdout.String()) + assert.Empty(t, stderr.String()) +} + +func TestProcessMainStdinErr(t *testing.T) { + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + input := strings.NewReader("this is the input") + + exit := processMain([]string{}, input, stdout, stderr, func(r io.Reader, w io.Writer) error { + return fmt.Errorf("something bad") + }) + + assert.Equal(t, -1, exit) + assert.Empty(t, stdout.String()) + assert.NotEmpty(t, stderr.String()) +} + +func TestProcessMainFileExists(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "example") + require.NoError(t, err) + defer os.Remove(tmpfile.Name()) + _, err = tmpfile.Write([]byte(`some data`)) + require.NoError(t, err) + + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + + exit := processMain([]string{tmpfile.Name()}, nil, stdout, stderr, func(r io.Reader, w io.Writer) error { + return nil + }) + + assert.Equal(t, 0, exit) + assert.Empty(t, stdout.String()) + assert.Empty(t, stderr.String()) +} + +func TestProcessMainFileDoesNotExist(t *testing.T) { + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + + exit := processMain([]string{"/lets/hope/this/does/not/exist"}, nil, stdout, stderr, func(r io.Reader, w io.Writer) error { + return nil + }) + + assert.Equal(t, -1, exit) + assert.Empty(t, stdout.String()) + assert.NotEmpty(t, stderr.String()) +}