diff --git a/kadai2/kynefuk/.gitignore b/kadai2/kynefuk/.gitignore new file mode 100644 index 0000000..05ecac4 --- /dev/null +++ b/kadai2/kynefuk/.gitignore @@ -0,0 +1,27 @@ +# Created by https://www.toptal.com/developers/gitignore/api/go +# Edit at https://www.toptal.com/developers/gitignore?templates=go + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +### Go Patch ### +/vendor/ +/Godeps/ + +img + +# End of https://www.toptal.com/developers/gitign \ No newline at end of file diff --git a/kadai2/kynefuk/README.md b/kadai2/kynefuk/README.md new file mode 100644 index 0000000..2c9bb7b --- /dev/null +++ b/kadai2/kynefuk/README.md @@ -0,0 +1,40 @@ +# <課題 2> io.Reader と io.Writer + +### io.Reader とは + +一連のデータ(バイト列)の読み込み処理を抽象化したインターフェース。 +読み込まれる対象データとしてバイト型の配列を受け取り、読み込んだバイト数とエラーを戻り値とする。 + +``` +type Reader interface { + Read(p []byte) (n int, err error) +} +``` + +### io.Writer とは + +一連のデータ(バイト列)の書き込み処理を抽象化したインターフェース +書き込まれる対象データとしてバイト型の配列を受け取り、書き込んだバイト数とエラーを戻り値とする。 + +``` +type Writer interface { + Write(p []byte) (n int, err error) +} +``` + +## 標準パッケージでどのように使われているか + +データの入出力を表現する様々なパッケージで io.Reader や io.Writer が実装している。 +(標準入力を表す`os.Stdin`、標準出力を表す`os.Stdout`、ファイルの入出力を表現する`os.File`や、通信の入出力を表現する`net.Conn`など) + +## io.Reader と io.Writer があることでどういう利点があるのか具体例を挙げて考えてみる + +入力処理と出力処理の仕様をインターフェースとして定義しておくことで、様々な入出力処理を統一的に扱うことができ、プログラムの柔軟性が高くなる。 +例えば、何らかの読み込み処理を行う関数の引数の型を`io.Reader`とすることで、`io.Reader`を実装している型であれば何でも受けることができる。 +コマンドラインのの処理内容を出力する標準出力や、エラーを表示する標準エラー出力のフィールドの型として io.Writer を指定することで、テスト時に、`bytes.Buffer`などに切り替えることができ、テスト容易性を高めることができる。 + +``` +type Command struct { + OutStream, ErrStream io.Writer +} +``` diff --git a/kadai2/kynefuk/cli/args.go b/kadai2/kynefuk/cli/args.go new file mode 100644 index 0000000..5a9dcbb --- /dev/null +++ b/kadai2/kynefuk/cli/args.go @@ -0,0 +1,44 @@ +package cli + +import ( + "fmt" +) + +// Args represents CLI's arguments object +type Args struct { + DirecTory, From, To string +} + +// Validate is a validation method +func (args *Args) Validate() error { + fileExtMap := createFileExtMap() + if _, ok := fileExtMap[args.From]; !ok { + return fmt.Errorf("argument of \"-f, --From\" is not valid file format. invalid format: %s", args.From) + } + if _, ok := fileExtMap[args.To]; !ok { + return fmt.Errorf("argument of \"-t, --To\" is not valid file format. invalid format: %s", args.To) + } + + return nil +} + +func createFileExtMap() map[string]string { + fileExtMap := make(map[string]string) + list := []string{ + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "tiff", + } + for _, v := range list { + fileExtMap[v] = "" + } + return fileExtMap +} + +// NewArgs is a construcTor of Args +func NewArgs(DirecTory, From, To string) *Args { + return &Args{DirecTory: DirecTory, From: From, To: To} +} diff --git a/kadai2/kynefuk/cli/args_test.go b/kadai2/kynefuk/cli/args_test.go new file mode 100644 index 0000000..c4220f7 --- /dev/null +++ b/kadai2/kynefuk/cli/args_test.go @@ -0,0 +1,31 @@ +package cli + +import ( + "testing" + + "github.com/gopherdojo/dojo8/kadai2/kynefuk/helper" +) + +const dummyDir = "dummy" + +func TestArgs(t *testing.T) { + cases := []struct{ name, fromFormat, toFormat, expectedMessage string }{ + {name: "valid arguments", fromFormat: "jpeg", toFormat: "png", expectedMessage: ""}, + {name: "invalid from format", fromFormat: "hoge", toFormat: "jpg", expectedMessage: "argument of \"-f, --From\" is not valid file format. invalid format: hoge"}, + {name: "invalid to format", fromFormat: "gif", toFormat: "hage", expectedMessage: "argument of \"-t, --To\" is not valid file format. invalid format: hage"}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + args := NewArgs(dummyDir, c.fromFormat, c.toFormat) + actual := args.Validate() + if actual == nil { + if c.expectedMessage != "" { + helper.ErrorHelper(t, actual, c.expectedMessage) + } + } else if actual.Error() != c.expectedMessage { + helper.ErrorHelper(t, actual, c.expectedMessage) + } + }) + } +} diff --git a/kadai2/kynefuk/cli/cli.go b/kadai2/kynefuk/cli/cli.go new file mode 100644 index 0000000..de244d1 --- /dev/null +++ b/kadai2/kynefuk/cli/cli.go @@ -0,0 +1,45 @@ +package cli + +import ( + "fmt" + "io" + + "github.com/gopherdojo/dojo8/kadai2/kynefuk/converter" + "github.com/gopherdojo/dojo8/kadai2/kynefuk/walker" +) + +// it represents Exit Code +const ( + ExitCodeOK = iota + ExitCodeError +) + +// Command represents CLI object +type Command struct { + OutStream, ErrStream io.Writer +} + +// Run invoke cli main logic +func (cli *Command) Run(directory, from, to string) int { + dirWalker := walker.NewWalker(from) + files, err := dirWalker.Dirwalk(directory) + if err != nil { + fmt.Fprintf(cli.ErrStream, "failed to read directory: %s, err: %s\n", directory, err) + return ExitCodeError + } + + imgConverter := converter.NewConverter(from, to) + for _, file := range files { + dstPath := converter.ConvertExt(file, from, to) + if err := imgConverter.ConvertFormat(file, dstPath); err != nil { + fmt.Fprintf(cli.ErrStream, "failed to convert img, err: %s\n", err) + return ExitCodeError + } + } + return ExitCodeOK +} + +// NewCommand is a constructor of CLI +func NewCommand(outStream, errStream io.Writer) *Command { + return &Command{OutStream: outStream, ErrStream: errStream} +} diff --git a/kadai2/kynefuk/cli/cli_test.go b/kadai2/kynefuk/cli/cli_test.go new file mode 100644 index 0000000..5ded4c4 --- /dev/null +++ b/kadai2/kynefuk/cli/cli_test.go @@ -0,0 +1,63 @@ +package cli + +import ( + "bytes" + "os" + "strings" + "testing" + + "github.com/gopherdojo/dojo8/kadai2/kynefuk/helper" +) + +var fileExtList = []string{ + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "tiff", +} + +func ConvertExt(filepath, to string) string { + return strings.Replace(filepath, ".png", to, 1) +} + +func TestCLI(t *testing.T) { + testDir := helper.CreateTmpDir() + defer os.RemoveAll(testDir) + outStream, errStream := new(bytes.Buffer), new(bytes.Buffer) + command := NewCommand(outStream, errStream) + + cases := []struct { + name string + directory string + fromFormat string + toFormat string + outStream string + errStream string + exitCode int + }{ + {name: "success", directory: testDir, fromFormat: "png", toFormat: "jpg", outStream: "", errStream: "", exitCode: 0}, + {name: "invalid directory", directory: "dummyDir", fromFormat: "jpg", toFormat: "png", outStream: "", errStream: "failed to read directory: dummyDir, err: open dummyDir: no such file or directory", exitCode: 1}, + {name: "invalid to format", directory: testDir, fromFormat: "jpg", toFormat: "hoge", outStream: "", errStream: "failed to convert img, err: unknown format type", exitCode: 1}, + {name: "invalid from format", directory: testDir, fromFormat: "hoge", toFormat: "png", outStream: "", errStream: "", exitCode: 1}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + tmpFile := helper.CreateTmpFile(testDir, c.fromFormat) + defer os.Remove(tmpFile.Name()) + status := command.Run(c.directory, c.fromFormat, c.toFormat) + + if status != c.exitCode { + helper.ErrorHelper(t, c.exitCode, status) + } + if !strings.Contains(outStream.String(), c.outStream) { + helper.ErrorHelper(t, c.outStream, outStream.String()) + } + if !strings.Contains(errStream.String(), c.errStream) { + helper.ErrorHelper(t, c.errStream, errStream.String()) + } + }) + } +} diff --git a/kadai2/kynefuk/cli/example130541744/example.859397199.jpg b/kadai2/kynefuk/cli/example130541744/example.859397199.jpg new file mode 100644 index 0000000..02a5c44 Binary files /dev/null and b/kadai2/kynefuk/cli/example130541744/example.859397199.jpg differ diff --git a/kadai2/kynefuk/cli/example979633815/example.209719562.jpg b/kadai2/kynefuk/cli/example979633815/example.209719562.jpg new file mode 100644 index 0000000..02a5c44 Binary files /dev/null and b/kadai2/kynefuk/cli/example979633815/example.209719562.jpg differ diff --git a/kadai2/kynefuk/converter/converter.go b/kadai2/kynefuk/converter/converter.go new file mode 100644 index 0000000..11c71ab --- /dev/null +++ b/kadai2/kynefuk/converter/converter.go @@ -0,0 +1,66 @@ +package converter + +import ( + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "os" + "strings" + + "golang.org/x/image/bmp" + "golang.org/x/image/tiff" +) + +// Converter converts image file +type Converter struct { + From, To string +} + +// ConvertFormat converts file format +func (converter *Converter) ConvertFormat(filepath, dst string) error { + file, err := os.Open(filepath) + if err != nil { + return fmt.Errorf("failed to open file. file: %s", filepath) + } + defer file.Close() + + img, _, err := image.Decode(file) + if err != nil { + return fmt.Errorf("failed to decode file. file: %s", filepath) + } + + out, err := os.Create(dst) + if err != nil { + return fmt.Errorf("failed to create output file. file: %s", dst) + } + defer out.Close() + + switch converter.To { + case "jpg", "jpeg": + err = jpeg.Encode(out, img, &jpeg.Options{Quality: 100}) + case "png": + err = png.Encode(out, img) + case "gif": + err = gif.Encode(out, img, &gif.Options{NumColors: 256, Quantizer: nil, Drawer: nil}) + case "bmp": + err = bmp.Encode(out, img) + case "tiff": + err = tiff.Encode(out, img, nil) + default: + return fmt.Errorf("unknown format type") + } + + return err +} + +// ConvertExt converts file format extention +func ConvertExt(filepath, from, to string) string { + return strings.Replace(filepath, from, to, 1) +} + +// NewConverter is a Constructor of Converter +func NewConverter(from, to string) *Converter { + return &Converter{From: from, To: to} +} diff --git a/kadai2/kynefuk/converter/converter_test.go b/kadai2/kynefuk/converter/converter_test.go new file mode 100644 index 0000000..0b08992 --- /dev/null +++ b/kadai2/kynefuk/converter/converter_test.go @@ -0,0 +1,42 @@ +package converter + +import ( + "os" + + "github.com/gopherdojo/dojo8/kadai2/kynefuk/helper" + + "testing" +) + +var fileExtList = []string{ + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "tiff", +} + +const testDirPath = "../testdata" + +func TestConverter(t *testing.T) { + tmpDir := helper.CreateTmpDir() + defer os.RemoveAll(tmpDir) + + // from→toに画像変換できているかテスト + for _, from := range helper.FileExtList { + for _, to := range helper.FileExtList { + tmpFile := helper.CreateTmpFile(tmpDir, from) + defer os.Remove(tmpFile.Name()) + imgConverter := NewConverter(from, to) + err := imgConverter.ConvertFormat(tmpFile.Name(), ConvertExt(tmpFile.Name(), from, to)) + if err != nil { + t.Errorf("failed to test. error: %s", err) + } + exists := helper.Exists(ConvertExt(tmpFile.Name(), from, to)) + if !exists { + t.Errorf("want true, but got %v", exists) + } + } + } +} diff --git a/kadai2/kynefuk/go.mod b/kadai2/kynefuk/go.mod new file mode 100644 index 0000000..8720123 --- /dev/null +++ b/kadai2/kynefuk/go.mod @@ -0,0 +1,5 @@ +module github.com/gopherdojo/dojo8/kadai2/kynefuk + +go 1.14 + +require golang.org/x/image v0.0.0-20200618115811-c13761719519 diff --git a/kadai2/kynefuk/go.sum b/kadai2/kynefuk/go.sum new file mode 100644 index 0000000..83da957 --- /dev/null +++ b/kadai2/kynefuk/go.sum @@ -0,0 +1,4 @@ +github.com/gopherdojo/dojo8 v0.0.0-20200703052727-6a79d18126bf h1:lpYevjFQMxI5VNBc3WXV6Z5pDDrdppdDKwmeBoyt5BE= +golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/kadai2/kynefuk/helper/helper.go b/kadai2/kynefuk/helper/helper.go new file mode 100644 index 0000000..232830f --- /dev/null +++ b/kadai2/kynefuk/helper/helper.go @@ -0,0 +1,104 @@ +package helper + +import ( + "image" + "image/gif" + "image/jpeg" + "image/png" + "io/ioutil" + "log" + "os" + "testing" + + "golang.org/x/image/bmp" + "golang.org/x/image/tiff" +) + +var FileExtList = []string{ + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "tiff", +} + +func CreateTmpDir() string { + dir, err := ioutil.TempDir("./", "example") + if err != nil { + log.Fatalf("failed to create test dir. error: %s", err) + } + return dir +} + +func CreateJPG(f *os.File, img *image.RGBA) error { + return jpeg.Encode(f, img, &jpeg.Options{Quality: 100}) +} + +func CreatePNG(f *os.File, img *image.RGBA) error { + return png.Encode(f, img) +} + +func CreateGIF(f *os.File, img *image.RGBA) error { + return gif.Encode(f, img, &gif.Options{NumColors: 256, Quantizer: nil, Drawer: nil}) +} + +func CreateBMP(f *os.File, img *image.RGBA) error { + return bmp.Encode(f, img) +} + +func CreateTIFF(f *os.File, img *image.RGBA) error { + return tiff.Encode(f, img, nil) +} + +func CreateTmpFile(dir, fileExt string) *os.File { + + img := image.NewRGBA(image.Rect(0, 0, 100, 50)) + + f, err := ioutil.TempFile(dir, "example.*."+fileExt) + if err != nil { + log.Fatalf("failed to create tmp file. error: %s", err) + } + defer f.Close() + + switch fileExt { + case "jpg", "jpeg": + err = CreateJPG(f, img) + case "png": + err = CreatePNG(f, img) + case "gif": + err = CreateGIF(f, img) + case "bmp": + err = CreateBMP(f, img) + case "tiff": + err = CreateTIFF(f, img) + } + + if err != nil { + log.Fatalf("failed to create test file. fileExt: %s", fileExt) + } + + return f +} + +func CreateTmpFiles(dir string) []string { + var tmpFilePaths []string + for _, v := range FileExtList { + f, err := ioutil.TempFile(dir, "example.*."+v) + if err != nil { + log.Fatalf("failed to create tmp file. error: %s", err) + } + tmpFilePaths = append(tmpFilePaths, f.Name()) + } + return tmpFilePaths +} + +func Exists(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} + +func ErrorHelper(tb testing.TB, want, got interface{}) { + tb.Helper() + tb.Errorf("want = %v, got = %v", want, got) +} diff --git a/kadai2/kynefuk/main.go b/kadai2/kynefuk/main.go new file mode 100644 index 0000000..998d8d3 --- /dev/null +++ b/kadai2/kynefuk/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/gopherdojo/dojo8/kadai2/kynefuk/cli" +) + +func main() { + var directory string + var from string + var to string + flag.StringVar(&directory, "directory", "./", "directory") + flag.StringVar(&directory, "d", "./", "directory(short)") + flag.StringVar(&from, "from", "jpg", "from format") + flag.StringVar(&from, "f", "jpg", "from format(short)") + flag.StringVar(&to, "to", "png", "to format") + flag.StringVar(&to, "t", "png", "to format(short)") + flag.Parse() + + command := cli.NewCommand(os.Stdout, os.Stderr) + + args := cli.NewArgs(directory, from, to) + if err := args.Validate(); err != nil { + fmt.Fprintf(command.ErrStream, "error: %s\n", err) + os.Exit(cli.ExitCodeError) + } + + os.Exit(command.Run(args.DirecTory, args.From, args.To)) +} diff --git a/kadai2/kynefuk/walker/walker.go b/kadai2/kynefuk/walker/walker.go new file mode 100644 index 0000000..c2c069c --- /dev/null +++ b/kadai2/kynefuk/walker/walker.go @@ -0,0 +1,41 @@ +package walker + +import ( + "io/ioutil" + "path/filepath" + "strings" +) + +// Walker walk directory to extract files +type Walker struct { + TargetExt string +} + +// Dirwalk walk directory and collect files +func (walker *Walker) Dirwalk(directory string) ([]string, error) { + items, err := ioutil.ReadDir(directory) + if err != nil { + return nil, err + } + + var paths []string + for _, item := range items { + if item.IsDir() { + items, err := walker.Dirwalk(filepath.Join(directory, item.Name())) + if err != nil { + return nil, err + } + paths = append(paths, items...) + continue + } + if strings.HasSuffix(item.Name(), walker.TargetExt) { + paths = append(paths, filepath.Join(directory, item.Name())) + } + } + return paths, nil +} + +// NewWalker is a constructor of Walker +func NewWalker(targetExt string) *Walker { + return &Walker{TargetExt: targetExt} +} diff --git a/kadai2/kynefuk/walker/walker_test.go b/kadai2/kynefuk/walker/walker_test.go new file mode 100644 index 0000000..ae356a7 --- /dev/null +++ b/kadai2/kynefuk/walker/walker_test.go @@ -0,0 +1,50 @@ +package walker + +import ( + "fmt" + "os" + "reflect" + "testing" + + "github.com/gopherdojo/dojo8/kadai2/kynefuk/helper" +) + +func TestWalker(t *testing.T) { + tmpDir := helper.CreateTmpDir() + tmpFilePaths := helper.CreateTmpFiles(tmpDir) + defer os.RemoveAll(tmpDir) + + type TestCase struct { + name, directory, fromFmt string + files []string + err error + } + + type TestCases []struct { + name, directory, fromFmt string + files []string + err error + } + + var testCases TestCases + for _, fileFmt := range helper.FileExtList { + name := fmt.Sprintf("walker collects %s file", fileFmt) + tests := TestCase{ + name: name, directory: tmpDir, fromFmt: fileFmt, files: tmpFilePaths, err: nil, + } + testCases = append(testCases, tests) + } + + for _, c := range testCases { + t.Run(c.name, func(t *testing.T) { + dirWalker := NewWalker(c.fromFmt) + files, err := dirWalker.Dirwalk(c.directory) + if err != c.err { + t.Errorf("test failed. error: %s", err) + } + if reflect.DeepEqual(c.files, files) { + helper.ErrorHelper(t, c.files, files) + } + }) + } +}