diff --git a/kadai2/shuheiktgw/converter-cli/.gitignore b/kadai2/shuheiktgw/converter-cli/.gitignore new file mode 100644 index 0000000..15ba9dd --- /dev/null +++ b/kadai2/shuheiktgw/converter-cli/.gitignore @@ -0,0 +1,2 @@ +vendor +converter-cli \ No newline at end of file diff --git a/kadai2/shuheiktgw/converter-cli/README.md b/kadai2/shuheiktgw/converter-cli/README.md new file mode 100644 index 0000000..6503e4c --- /dev/null +++ b/kadai2/shuheiktgw/converter-cli/README.md @@ -0,0 +1,15 @@ +converter-cli +=== + +converter-cli is a command line tool to convert image extension. + +## Usage + +``` +converter-cli [options...] PATH + +OPTIONS: + --from value, -f value specifies a image extension converted from (default: .jpg) + --to value, -t value specifies a image extension converted to (default: .png) + --help, -h prints out help +``` \ No newline at end of file diff --git a/kadai2/shuheiktgw/converter-cli/cli.go b/kadai2/shuheiktgw/converter-cli/cli.go new file mode 100644 index 0000000..fd3892f --- /dev/null +++ b/kadai2/shuheiktgw/converter-cli/cli.go @@ -0,0 +1,112 @@ +package main + +import ( + "flag" + "fmt" + "io" + + "github.com/shuheiktgw/dojo4/kadai1/shuheiktgw/converter-cli/converter" +) + +const ( + ExitCodeOK = iota + ExitCodeExpectedError + ExitCodeUnexpectedError + ExitCodeBadArgs + ExitCodeParseFlagsError + ExitCodeInvalidFlagError +) + +const Name = "converter-cli" + +var extensions = [4]string{".gif", ".jpeg", ".jpg", ".png"} + +// CLI represents CLI interface for converter +type CLI struct { + outStream, errStream io.Writer +} + +// Run executes `converter-cli` command and converts images's extension +func (cli *CLI) Run(args []string) int { + var ( + from string + to string + ) + + flags := flag.NewFlagSet(Name, flag.ContinueOnError) + flags.Usage = func() { + fmt.Fprint(cli.outStream, usage) + } + + flags.StringVar(&from, "from", ".jpg", "") + flags.StringVar(&from, "f", ".jpg", "") + + flags.StringVar(&to, "to", ".png", "") + flags.StringVar(&to, "t", ".png", "") + + if err := flags.Parse(args[1:]); err != nil { + return ExitCodeParseFlagsError + } + + if !validateExtensions(from) { + fmt.Fprintf(cli.errStream, "Failed to set up converter-cli: invalid extension `%s` is given for --from flag\n"+ + "Please choose an extension from one of those: %v\n\n", from, extensions) + return ExitCodeInvalidFlagError + } + + if !validateExtensions(to) { + fmt.Fprintf(cli.errStream, "Failed to set up converter-cli: invalid extension `%s` is given for --to flag\n"+ + "Please choose an extension from one of those: %v\n\n", to, extensions) + return ExitCodeInvalidFlagError + } + + path := flags.Args() + if len(path) != 1 { + fmt.Fprintf(cli.errStream, "Failed to set up converter-cli: invalid argument\n"+ + "Please specify the exact one path to a directly or a file\n\n") + return ExitCodeBadArgs + } + + files, err := converter.Convert(from, to, path[0]) + if err != nil { + if _, ok := err.(converter.Handled); ok { + fmt.Fprintf(cli.errStream, "Failed to execute converter-cli\n"+ + "%s\n\n", err) + return ExitCodeExpectedError + } + + fmt.Fprintf(cli.errStream, `converter-cli failed because of the following error. + +%s + +You might encounter a bug with converter-cli, so please report it to https://github.com/xxx/xxxx + +`, err) + return ExitCodeUnexpectedError + } + + fmt.Fprintf(cli.outStream, "converter-cli successfully converted following files to `%s`.\n", to) + fmt.Fprintf(cli.outStream, "%s\n\n", files) + return ExitCodeOK +} + +func validateExtensions(ext string) bool { + for _, e := range extensions { + if ext == e { + return true + } + } + + return false +} + +var usage = `Usage: converter-cli [options...] PATH + +converter-cli is a command line tool to convert image extension + +OPTIONS: + --from value, -f value specifies a image extension converted from (default: .jpg) + --to value, -t value specifies a image extension converted to (default: .png) + --help, -h prints out help + +` diff --git a/kadai2/shuheiktgw/converter-cli/cli_test.go b/kadai2/shuheiktgw/converter-cli/cli_test.go new file mode 100644 index 0000000..6c8ec9a --- /dev/null +++ b/kadai2/shuheiktgw/converter-cli/cli_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCLI_Run(t *testing.T) { + cases := []struct { + command string + expectedOutStream string + expectedErrStream string + expectedExitCode int + }{ + { + command: "converter-cli", + expectedOutStream: "", + expectedErrStream: "Failed to set up converter-cli: invalid argument\nPlease specify the exact one path to a directly or a file\n\n", + expectedExitCode: ExitCodeBadArgs, + }, + { + command: "converter-cli testdata testdata2", + expectedOutStream: "", + expectedErrStream: "Failed to set up converter-cli: invalid argument\nPlease specify the exact one path to a directly or a file\n\n", + expectedExitCode: ExitCodeBadArgs, + }, + { + command: "converter-cli --from .svg", + expectedOutStream: "", + expectedErrStream: "Failed to set up converter-cli: invalid extension `.svg` is given for --from flag\nPlease choose an extension from one of those: [.gif .jpeg .jpg .png]\n\n", + expectedExitCode: ExitCodeInvalidFlagError, + }, + { + command: "converter-cli --to .svg", + expectedOutStream: "", + expectedErrStream: "Failed to set up converter-cli: invalid extension `.svg` is given for --to flag\nPlease choose an extension from one of those: [.gif .jpeg .jpg .png]\n\n", + expectedExitCode: ExitCodeInvalidFlagError, + }, + { + command: "converter-cli testdata", + expectedOutStream: "", + expectedErrStream: "Failed to execute converter-cli\ncould not find files with the specified extension. path: testdata, extension: .jpg\n\n", + expectedExitCode: ExitCodeExpectedError, + }, + { + command: "converter-cli testdata/unknown.jpg", + expectedOutStream: "", + expectedErrStream: "Failed to execute converter-cli\nlstat testdata/unknown.jpg: no such file or directory\n\n", + expectedExitCode: ExitCodeExpectedError, + }, + { + command: "converter-cli --from .jpeg testdata", + expectedOutStream: "converter-cli successfully converted following files to `.png`.\n[testdata/jpeg-image.jpeg]\n\n", + expectedErrStream: "", + expectedExitCode: ExitCodeOK, + }, + } + + for i, tc := range cases { + outStream := new(bytes.Buffer) + errStream := new(bytes.Buffer) + + cli := CLI{outStream: outStream, errStream: errStream} + args := strings.Split(tc.command, " ") + + if got := cli.Run(args); got != tc.expectedExitCode { + t.Errorf("#%d %q exits with %d, want %d", i, tc.command, got, tc.expectedExitCode) + } + + if got := outStream.String(); got != tc.expectedOutStream { + t.Errorf("#%d Unexpected outStream has returned: want: %s, got: %s", i, tc.expectedOutStream, got) + } + + if got := errStream.String(); got != tc.expectedErrStream { + t.Errorf("#%d Unexpected errStream has returned: want: %s, got: %s", i, tc.expectedErrStream, got) + } + + cleanup(t) + } +} + +func cleanup(t *testing.T) { + t.Helper() + + err := filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if path != "testdata/jpeg-image.jpeg" && path != "testdata" { + return os.Remove(path) + } + + return nil + }) + + if err != nil { + t.Errorf("failed to cleanup testdata: %s", err) + } +} diff --git a/kadai2/shuheiktgw/converter-cli/converter/converter.go b/kadai2/shuheiktgw/converter-cli/converter/converter.go new file mode 100644 index 0000000..9a2f312 --- /dev/null +++ b/kadai2/shuheiktgw/converter-cli/converter/converter.go @@ -0,0 +1,116 @@ +// Package converter provides a functionality to convert image extension +package converter + +import ( + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "os" + "path/filepath" +) + +// Handled interface represents the error is properly handled and expected +type Handled interface { + Handled() bool +} + +// HandedError represents the error is properly handled and expected +type HandedError struct { + Message string +} + +// Error returns error message for HandedError +func (r *HandedError) Error() string { + return r.Message +} + +// Handled suggests that HandedError implemented Handled interface +func (r *HandedError) Handled() bool { + return true +} + +// Convert converts file extension in the specified path recursively +func Convert(from, to, path string) ([]string, error) { + filePaths, err := findFilePaths(from, path) + if err != nil { + return nil, err + } + + for _, filePath := range filePaths { + if err := convert(to, filePath); err != nil { + return nil, err + } + } + + return filePaths, nil +} + +func findFilePaths(from, path string) ([]string, error) { + var filePaths []string + + err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if _, ok := err.(*os.PathError); ok { + return &HandedError{Message: err.Error()} + } + + if err != nil { + return err + } + + if filepath.Ext(path) == from { + filePaths = append(filePaths, path) + } + + return nil + }) + + if err != nil { + return nil, err + } + + if len(filePaths) == 0 { + return nil, &HandedError{Message: fmt.Sprintf("could not find files with the specified extension. path: %s, extension: %s", path, from)} + } + + return filePaths, nil +} + +func convert(to, filePath string) error { + file, err := os.Open(filePath) + defer file.Close() + + if err != nil { + return err + } + + img, _, err := image.Decode(file) + + if err != nil { + return err + } + + out, err := os.Create(newFilePath(to, filePath)) + defer out.Close() + + if err != nil { + return err + } + + switch to { + case ".gif": + return gif.Encode(out, img, nil) + case ".jpeg", ".jpg": + return jpeg.Encode(out, img, nil) + case ".png": + return png.Encode(out, img) + default: + return fmt.Errorf("unsupported extension is specified: %s", to) + } +} + +func newFilePath(to, filePath string) string { + ext := filepath.Ext(filePath) + return filePath[:len(filePath)-len(ext)] + to +} diff --git a/kadai2/shuheiktgw/converter-cli/converter/converter_test.go b/kadai2/shuheiktgw/converter-cli/converter/converter_test.go new file mode 100644 index 0000000..33cf0f6 --- /dev/null +++ b/kadai2/shuheiktgw/converter-cli/converter/converter_test.go @@ -0,0 +1,146 @@ +package converter + +import ( + "os" + "path/filepath" + "testing" +) + +var originalFile = []string{ + "testdata", + "testdata/empty", + "testdata/gif-image.gif", + "testdata/gif-image1.gif", + "testdata/jpeg-image.jpeg", + "testdata/jpeg-image1.jpeg", + "testdata/jpg-image.jpg", + "testdata/jpg-image1.jpg", + "testdata/png-image.png", + "testdata/png-image1.png", +} + +func TestConvert_Fail(t *testing.T) { + cases := []struct { + from string + to string + path string + handled bool + }{ + {from: ".jpeg", to: ".png", path: "./unknown", handled: true}, + {from: ".jpeg", to: ".png", path: "./testdata/empty", handled: true}, + {from: ".svg", to: ".png", path: "./testdata", handled: true}, + {from: ".jpeg", to: ".png", path: "./unknown.jpeg", handled: true}, + } + + for i, tc := range cases { + _, err := Convert(tc.from, tc.to, tc.path) + + if err == nil { + t.Fatalf("#%d error is not supposed to be nil", i) + } + + if _, ok := err.(Handled); ok != tc.handled { + t.Fatalf("#%d Handled is suppoed to be %v. error: %s", i, tc.handled, err) + } + } +} + +func TestConvert_Success(t *testing.T) { + cases := []struct { + from string + to string + path string + expectedFiles []string + expectedOutputs []string + }{ + { + from: ".jpeg", + to: ".png", + path: "./testdata", + expectedFiles: []string{"testdata/jpeg-image.jpeg", "testdata/jpeg-image1.jpeg"}, + expectedOutputs: []string{"testdata/jpeg-image.png", "testdata/jpeg-image1.png"}, + }, + { + from: ".jpeg", + to: ".gif", + path: "./testdata", + expectedFiles: []string{"testdata/jpeg-image.jpeg", "testdata/jpeg-image1.jpeg"}, + expectedOutputs: []string{"testdata/jpeg-image.gif", "testdata/jpeg-image1.gif"}, + }, + { + from: ".png", + to: ".jpeg", + path: "./testdata", + expectedFiles: []string{"testdata/png-image.png", "testdata/png-image1.png"}, + expectedOutputs: []string{"testdata/png-image.jpeg", "testdata/png-image1.jpeg"}, + }, + { + from: ".gif", + to: ".jpeg", + path: "./testdata", + expectedFiles: []string{"testdata/gif-image.gif", "testdata/gif-image1.gif"}, + expectedOutputs: []string{"testdata/gif-image.jpeg", "testdata/gif-image1.jpeg"}, + }, + } + + for i, tc := range cases { + files, err := Convert(tc.from, tc.to, tc.path) + + if err != nil { + t.Fatalf("#%d converter.Convert returned enexpected error: %s", i, err) + } + + if got, want := len(files), len(tc.expectedFiles); got != want { + t.Errorf("#%d converter.Convert returnede unexpected number of files: want: %d, got: %d", i, want, got) + } + + for _, file := range files { + + ok := false + for _, expected := range tc.expectedFiles { + if file == expected { + ok = true + } + } + + if !ok { + t.Errorf("#%d converter.Convert returned unexpected file: %s", i, file) + } + } + + for _, expected := range tc.expectedOutputs { + if _, err := os.Stat(expected); err != nil { + t.Errorf("#%d converter.Convert did not output expected file: %s", i, expected) + } + } + + cleanup(t) + } +} + +func cleanup(t *testing.T) { + t.Helper() + + err := filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + deletable := true + for _, original := range originalFile { + if path == original { + deletable = false + } + } + + if deletable { + return os.Remove(path) + } + + return nil + }) + + if err != nil { + t.Errorf("failed to cleanup testdata") + } +} diff --git a/kadai2/shuheiktgw/converter-cli/converter/testdata/gif-image.gif b/kadai2/shuheiktgw/converter-cli/converter/testdata/gif-image.gif new file mode 100644 index 0000000..98a696e Binary files /dev/null and b/kadai2/shuheiktgw/converter-cli/converter/testdata/gif-image.gif differ diff --git a/kadai2/shuheiktgw/converter-cli/converter/testdata/gif-image1.gif b/kadai2/shuheiktgw/converter-cli/converter/testdata/gif-image1.gif new file mode 100644 index 0000000..98a696e Binary files /dev/null and b/kadai2/shuheiktgw/converter-cli/converter/testdata/gif-image1.gif differ diff --git a/kadai2/shuheiktgw/converter-cli/converter/testdata/jpeg-image.jpeg b/kadai2/shuheiktgw/converter-cli/converter/testdata/jpeg-image.jpeg new file mode 100644 index 0000000..2da5e0d Binary files /dev/null and b/kadai2/shuheiktgw/converter-cli/converter/testdata/jpeg-image.jpeg differ diff --git a/kadai2/shuheiktgw/converter-cli/converter/testdata/jpeg-image1.jpeg b/kadai2/shuheiktgw/converter-cli/converter/testdata/jpeg-image1.jpeg new file mode 100644 index 0000000..2da5e0d Binary files /dev/null and b/kadai2/shuheiktgw/converter-cli/converter/testdata/jpeg-image1.jpeg differ diff --git a/kadai2/shuheiktgw/converter-cli/converter/testdata/jpg-image.jpg b/kadai2/shuheiktgw/converter-cli/converter/testdata/jpg-image.jpg new file mode 100644 index 0000000..2da5e0d Binary files /dev/null and b/kadai2/shuheiktgw/converter-cli/converter/testdata/jpg-image.jpg differ diff --git a/kadai2/shuheiktgw/converter-cli/converter/testdata/jpg-image1.jpg b/kadai2/shuheiktgw/converter-cli/converter/testdata/jpg-image1.jpg new file mode 100644 index 0000000..2da5e0d Binary files /dev/null and b/kadai2/shuheiktgw/converter-cli/converter/testdata/jpg-image1.jpg differ diff --git a/kadai2/shuheiktgw/converter-cli/converter/testdata/png-image.png b/kadai2/shuheiktgw/converter-cli/converter/testdata/png-image.png new file mode 100644 index 0000000..5dc5b32 Binary files /dev/null and b/kadai2/shuheiktgw/converter-cli/converter/testdata/png-image.png differ diff --git a/kadai2/shuheiktgw/converter-cli/converter/testdata/png-image1.png b/kadai2/shuheiktgw/converter-cli/converter/testdata/png-image1.png new file mode 100644 index 0000000..5dc5b32 Binary files /dev/null and b/kadai2/shuheiktgw/converter-cli/converter/testdata/png-image1.png differ diff --git a/kadai2/shuheiktgw/converter-cli/main.go b/kadai2/shuheiktgw/converter-cli/main.go new file mode 100644 index 0000000..94ac970 --- /dev/null +++ b/kadai2/shuheiktgw/converter-cli/main.go @@ -0,0 +1,18 @@ +// Package main provides CLI interface for converter package +// +// Usage: converter-cli [options...] PATH +// +// OPTIONS: +// --from value, -f value specifies a image extension converted from (default: .jpg) +// --to value, -t value specifies a image extension converted to (default: .png) +// --help, -h prints out help +package main + +import ( + "os" +) + +func main() { + cli := &CLI{outStream: os.Stdout, errStream: os.Stderr} + os.Exit(cli.Run(os.Args)) +} diff --git a/kadai2/shuheiktgw/converter-cli/testdata/jpeg-image.jpeg b/kadai2/shuheiktgw/converter-cli/testdata/jpeg-image.jpeg new file mode 100644 index 0000000..2da5e0d Binary files /dev/null and b/kadai2/shuheiktgw/converter-cli/testdata/jpeg-image.jpeg differ