From 9c8596aaa7eb98dbedf3a52450f5e478de5b1156 Mon Sep 17 00:00:00 2001 From: s14t284 Date: Sat, 15 Jan 2022 11:54:52 +0900 Subject: [PATCH 1/7] :sparkles: add 'afoc' commandline interface --- cmd/afoc.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 cmd/afoc.go diff --git a/cmd/afoc.go b/cmd/afoc.go new file mode 100644 index 0000000..7f33edb --- /dev/null +++ b/cmd/afoc.go @@ -0,0 +1,70 @@ +/* +Copyright © 2022 s14t284 rikeda71@gmail.com + +*/ +package cmd + +import ( + "io" + "os" + "path" + + "github.com/s14t284/foggo/internal/generator" + "github.com/s14t284/foggo/internal/logger" + "github.com/s14t284/foggo/internal/parser" + "github.com/s14t284/foggo/internal/writer" + "github.com/spf13/cobra" +) + +func initializeAfocCommand() *cobra.Command { + // afocCmd represents the afoc command + return &cobra.Command{ + Use: "afoc", + Short: "command to generate 'Applicable Functional Option Pattern' code of golang", + Long: `'afoc' is the command to command to generate 'Applicable Functional Option Pattern' code of golang. +ref. +- https://github.com/uber-go/guide/blob/master/style.md#functional-options +- https://ww24.jp/2019/07/go-option-pattern(in Japanese) +`, + RunE: func(_ *cobra.Command, _ []string) error { + out := os.Stdout + return generateAFOC(out) + }, + } +} + +// generateAFOC generate functional option pattern code +func generateAFOC(out io.Writer) error { + l := logger.InitializeLogger(out, "[AFOC Generator] ") + g := generator.InitializeGenerator() + w, err := writer.InitializeWriter(l) + if err != nil { + return err + } + + p := Args.Package + if p != "." { + p = "./" + path.Clean(Args.Package) + } + pkg, err := parser.ParsePackageInfo(p) + if err != nil { + return err + } + + fields, i, err := parser.CollectFields(Args.Struct, pkg.AstFiles) + if err != nil { + return err + } + + code, err := g.GenerateFOP(pkg.Name, Args.Struct, fields) + if err != nil { + return err + } + + err = w.Write(code, pkg.Paths[i]) + if err != nil { + return err + } + + return nil +} From 35960ac7f7e999a5dd7053b5d1364eb05249f3aa Mon Sep 17 00:00:00 2001 From: s14t284 Date: Sat, 15 Jan 2022 12:22:17 +0900 Subject: [PATCH 2/7] add afoc template and the method to generate afoc code --- internal/generator/generator.go | 15 +++- internal/generator/generator_test.go | 83 +++++++++++++++++++ .../applicable_functional_option_pattern.go | 24 ++++++ 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 internal/generator/templates/applicable_functional_option_pattern.go diff --git a/internal/generator/generator.go b/internal/generator/generator.go index cde2ec4..b836b63 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -21,9 +21,20 @@ func InitializeGenerator() *Generator { } } -// GenerateFOP is the function to generate code of functional option pattern from struct +// GenerateFOP is the function to generate code of Functional Option Pattern from struct func (g *Generator) GenerateFOP(pkgName string, structName string, sts []*StructField) (string, error) { - tpl := template.Must(template.New("a").Parse(templates.FOPTemplate)) + tpl := template.Must(template.New("t").Parse(templates.FOPTemplate)) + return g.generateInternal(pkgName, structName, sts, tpl) +} + +// GenerateAFOP is the function to generate code of Applicable Functional Option Pattern from struct +func (g *Generator) GenerateAFOP(pkgName string, structName string, sts []*StructField) (string, error) { + tpl := template.Must(template.New("t").Parse(templates.AFOPTemplate)) + return g.generateInternal(pkgName, structName, sts, tpl) +} + +// generateInternal is the function of the internal logic to generate Functional Option Pattern code +func (g *Generator) generateInternal(pkgName string, structName string, sts []*StructField, tpl *template.Template) (string, error) { if !g.checkStructFieldFormat(sts) { return "", fmt.Errorf("%s have same name fields", structName) } diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go index 17e447b..4fe4160 100644 --- a/internal/generator/generator_test.go +++ b/internal/generator/generator_test.go @@ -51,6 +51,52 @@ func NewTestData(options ...TestDataOption) *TestData { return s } +` + expectedAFOPMaximumStr = expectedTemplateBaseStr + ` +type TestDataOption interface { + apply(*TestData) +} + +type AOption struct { + A string +} + +func (o AOption) apply(s TestData) { + s.A = o.A +} + +type BOption struct { + B int +} + +func (o BOption) apply(s TestData) { + s.B = o.B +} + +func NewTestData(options ...TestDataOption) *TestData { + s := &TestData{} + + for _, option := range options { + option.apply(s) + } + + return s +} +` + expectedAFOPMinimumStr = expectedTemplateBaseStr + ` +type TestDataOption interface { + apply(*TestData) +} + +func NewTestData(options ...TestDataOption) *TestData { + s := &TestData{} + + for _, option := range options { + option.apply(s) + } + + return s +} ` ) @@ -91,6 +137,43 @@ func TestGenerator_GenerateFOP(t *testing.T) { } } +func TestGenerator_GenerateAFOP(t *testing.T) { + type fields struct { + goimports bool + } + type args struct { + pkgName string + structName string + sts []*StructField + } + tests := []struct { + name string + fields fields + args args + want string + wantErr assert.ErrorAssertionFunc + }{ + {"nominal: maximum", fields{false}, args{"testdata", "TestData", []*StructField{{Name: "A", Type: "string", Ignore: false}, {Name: "B", Type: "int", Ignore: false}, {Name: "C", Type: "int", Ignore: true}}}, expectedAFOPMaximumStr, assert.NoError}, + {"nominal: maximum with goimports", fields{true}, args{"testdata", "TestData", []*StructField{{Name: "A", Type: "string", Ignore: false}, {Name: "B", Type: "int", Ignore: false}, {Name: "C", Type: "int", Ignore: true}}}, expectedAFOPMaximumStr, assert.NoError}, + {"nominal: minimum", fields{false}, args{"testdata", "TestData", []*StructField{}}, expectedAFOPMinimumStr, assert.NoError}, + {"nominal: minimum with goimports", fields{true}, args{"testdata", "TestData", []*StructField{}}, expectedAFOPMinimumStr, assert.NoError}, + {"non_nominal: have same name fields", fields{false}, args{"testdata", "TestData", []*StructField{{Name: "A"}, {Name: "a"}}}, "", assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := assert.New(t) + g := &Generator{ + goimports: tt.fields.goimports, + } + got, err := g.GenerateAFOP(tt.args.pkgName, tt.args.structName, tt.args.sts) + if !tt.wantErr(t, err, fmt.Sprintf("Generator.GenerateFOP(%v)", tt.args)) { + return + } + a.Equal(tt.want, got) + }) + } +} + func TestGenerator_checkStructFieldFormat(t *testing.T) { type fields struct { goimports bool diff --git a/internal/generator/templates/applicable_functional_option_pattern.go b/internal/generator/templates/applicable_functional_option_pattern.go new file mode 100644 index 0000000..2bb1b73 --- /dev/null +++ b/internal/generator/templates/applicable_functional_option_pattern.go @@ -0,0 +1,24 @@ +package templates + +const AFOPTemplate = TemplateBase + ` +type {{ .structName }}Option interface { + apply(*{{ .structName }}) +} +{{ range .fields }}{{ if ne .Ignore true}} +type {{ .Name }}Option struct { + {{ .Name }} {{ .Type }} +} + +func (o {{ .Name }}Option) apply(s {{ $.structName }}) { + s.{{ .Name }} = o.{{ .Name }} +} +{{ end }}{{ end }} +func New{{ .structName }}(options ...{{ .structName }}Option) *{{ .structName }} { + s := &{{ .structName }}{} + + for _, option := range options { + option.apply(s) + } + + return s +}` From a14632c53d002077d20087a98912b6390390539e Mon Sep 17 00:00:00 2001 From: s14t284 Date: Sat, 15 Jan 2022 12:24:18 +0900 Subject: [PATCH 3/7] available to afoc command --- cmd/afoc_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 1 + 2 files changed, 58 insertions(+) create mode 100644 cmd/afoc_test.go diff --git a/cmd/afoc_test.go b/cmd/afoc_test.go new file mode 100644 index 0000000..3a69e01 --- /dev/null +++ b/cmd/afoc_test.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_initializeAfocCommand(t *testing.T) { + // assert initialization and check required parameters + a := assert.New(t) + cmd := initializeAfocCommand() + a.NotEqual("", cmd.Use) + a.NotEqual("", cmd.Short) + a.NotEqual("", cmd.Long) + a.NotNil(cmd.RunE) +} + +func Test_generateAFOC(t *testing.T) { + tests := []struct { + name string + struct_ string + package_ string + wantOut string + wantErr assert.ErrorAssertionFunc + }{ + {"nominal: Data1", "Data1", "../testdata", "success to write functional option pattern code to", assert.NoError}, + {"nominal: Data2", "Data2", "../testdata", "success to write functional option pattern code to", assert.NoError}, + {"non_nominal: parse package error", "Data2", "./", "", assert.Error}, + {"non_nominal: collect fields from struct type error", "Data3", "../testdata", "", assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Args.Struct = tt.struct_ + Args.Package = tt.package_ + out := &bytes.Buffer{} + err := generateAFOC(out) + if !tt.wantErr(t, err, fmt.Sprintf("generateFOC(%v)", out)) { + return + } + assert.Containsf(t, out.String(), tt.wantOut, "generateFOC(%v)", out) + + // remove generated files + files, err := filepath.Glob(fmt.Sprintf("%s/*_gen.go", tt.package_)) + assert.NoError(t, err) + for _, f := range files { + fmt.Println(f) + err = os.Remove(f) + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/root.go b/cmd/root.go index b127543..46c2c43 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -60,6 +60,7 @@ $ foggo foc --struct ${STRUCT_TYPE_NAME} --package ${PACKAGE_PATH} // set sub commands rootCmd.AddCommand(initializeFocCommand()) + rootCmd.AddCommand(initializeAfocCommand()) // set version format rootCmd.SetVersionTemplate("{{ .Version }}") From 26f79da293c395edfb18a01efeec6b1ed456a310 Mon Sep 17 00:00:00 2001 From: s14t284 Date: Sat, 15 Jan 2022 12:27:28 +0900 Subject: [PATCH 4/7] :white_check_mark: fix test --- .../templates/applicable_functional_option_pattern.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/generator/templates/applicable_functional_option_pattern.go b/internal/generator/templates/applicable_functional_option_pattern.go index 2bb1b73..841c1eb 100644 --- a/internal/generator/templates/applicable_functional_option_pattern.go +++ b/internal/generator/templates/applicable_functional_option_pattern.go @@ -21,4 +21,5 @@ func New{{ .structName }}(options ...{{ .structName }}Option) *{{ .structName }} } return s -}` +} +` From 3fb214b94edb82c8bcb5c81465775edaff0ca3fb Mon Sep 17 00:00:00 2001 From: s14t284 Date: Sat, 15 Jan 2022 13:07:33 +0900 Subject: [PATCH 5/7] :truck: rename 'fop' to 'afop' --- cmd/{afoc.go => afop.go} | 17 ++++--- cmd/{afoc_test.go => afop_test.go} | 12 ++--- cmd/{foc.go => fop.go} | 15 +++---- cmd/{foc_test.go => fop_test.go} | 10 ++--- cmd/root.go | 11 +++-- internal/generator/generator_test.go | 4 +- .../applicable_functional_option_pattern.go | 2 +- testdata/examples/image.go | 2 +- testdata/examples/image_gen.go | 44 +++++++++++-------- 9 files changed, 63 insertions(+), 54 deletions(-) rename cmd/{afoc.go => afop.go} (73%) rename cmd/{afoc_test.go => afop_test.go} (81%) rename cmd/{foc.go => fop.go} (78%) rename cmd/{foc_test.go => fop_test.go} (84%) diff --git a/cmd/afoc.go b/cmd/afop.go similarity index 73% rename from cmd/afoc.go rename to cmd/afop.go index 7f33edb..d0bbc9a 100644 --- a/cmd/afoc.go +++ b/cmd/afop.go @@ -16,26 +16,25 @@ import ( "github.com/spf13/cobra" ) -func initializeAfocCommand() *cobra.Command { - // afocCmd represents the afoc command +func initializeAfopCommand() *cobra.Command { return &cobra.Command{ - Use: "afoc", + Use: "afop", Short: "command to generate 'Applicable Functional Option Pattern' code of golang", - Long: `'afoc' is the command to command to generate 'Applicable Functional Option Pattern' code of golang. + Long: `'afop' is the command to command to generate 'Applicable Functional Option Pattern' code of golang. ref. - https://github.com/uber-go/guide/blob/master/style.md#functional-options - https://ww24.jp/2019/07/go-option-pattern(in Japanese) `, RunE: func(_ *cobra.Command, _ []string) error { out := os.Stdout - return generateAFOC(out) + return generateAFOP(out) }, } } -// generateAFOC generate functional option pattern code -func generateAFOC(out io.Writer) error { - l := logger.InitializeLogger(out, "[AFOC Generator] ") +// generateAFOP generate functional option pattern code +func generateAFOP(out io.Writer) error { + l := logger.InitializeLogger(out, "[AFOP Generator] ") g := generator.InitializeGenerator() w, err := writer.InitializeWriter(l) if err != nil { @@ -56,7 +55,7 @@ func generateAFOC(out io.Writer) error { return err } - code, err := g.GenerateFOP(pkg.Name, Args.Struct, fields) + code, err := g.GenerateAFOP(pkg.Name, Args.Struct, fields) if err != nil { return err } diff --git a/cmd/afoc_test.go b/cmd/afop_test.go similarity index 81% rename from cmd/afoc_test.go rename to cmd/afop_test.go index 3a69e01..74d0a17 100644 --- a/cmd/afoc_test.go +++ b/cmd/afop_test.go @@ -10,17 +10,17 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_initializeAfocCommand(t *testing.T) { +func Test_initializeAfopCommand(t *testing.T) { // assert initialization and check required parameters a := assert.New(t) - cmd := initializeAfocCommand() + cmd := initializeAfopCommand() a.NotEqual("", cmd.Use) a.NotEqual("", cmd.Short) a.NotEqual("", cmd.Long) a.NotNil(cmd.RunE) } -func Test_generateAFOC(t *testing.T) { +func Test_generateAFOP(t *testing.T) { tests := []struct { name string struct_ string @@ -38,11 +38,11 @@ func Test_generateAFOC(t *testing.T) { Args.Struct = tt.struct_ Args.Package = tt.package_ out := &bytes.Buffer{} - err := generateAFOC(out) - if !tt.wantErr(t, err, fmt.Sprintf("generateFOC(%v)", out)) { + err := generateAFOP(out) + if !tt.wantErr(t, err, fmt.Sprintf("generateFOP(%v)", out)) { return } - assert.Containsf(t, out.String(), tt.wantOut, "generateFOC(%v)", out) + assert.Containsf(t, out.String(), tt.wantOut, "generateAFOP(%v)", out) // remove generated files files, err := filepath.Glob(fmt.Sprintf("%s/*_gen.go", tt.package_)) diff --git a/cmd/foc.go b/cmd/fop.go similarity index 78% rename from cmd/foc.go rename to cmd/fop.go index e27c8dc..5b5714c 100644 --- a/cmd/foc.go +++ b/cmd/fop.go @@ -16,25 +16,24 @@ import ( "github.com/spf13/cobra" ) -func initializeFocCommand() *cobra.Command { - // focCmd represents the foc command +func initializeFopCommand() *cobra.Command { return &cobra.Command{ - Use: "foc", + Use: "fop", Short: "command to generate 'Functional Option Pattern' code of golang", - Long: `'foc' is the command to command to generate 'Functional Option Pattern' code of golang. + Long: `'fop' is the command to command to generate 'Functional Option Pattern' code of golang. ref. - https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html - https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis`, RunE: func(_ *cobra.Command, _ []string) error { out := os.Stdout - return generateFOC(out) + return generateFOP(out) }, } } -// generateFOC generate functional option pattern code -func generateFOC(out io.Writer) error { - l := logger.InitializeLogger(out, "[FOC Generator] ") +// generateFOP generate functional option pattern code +func generateFOP(out io.Writer) error { + l := logger.InitializeLogger(out, "[FOP Generator] ") g := generator.InitializeGenerator() w, err := writer.InitializeWriter(l) if err != nil { diff --git a/cmd/foc_test.go b/cmd/fop_test.go similarity index 84% rename from cmd/foc_test.go rename to cmd/fop_test.go index 55d7a14..73edff6 100644 --- a/cmd/foc_test.go +++ b/cmd/fop_test.go @@ -10,10 +10,10 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_initializeFocCommand(t *testing.T) { +func Test_initializeFopCommand(t *testing.T) { // assert initialization and check required parameters a := assert.New(t) - cmd := initializeFocCommand() + cmd := initializeFopCommand() a.NotEqual("", cmd.Use) a.NotEqual("", cmd.Short) a.NotEqual("", cmd.Long) @@ -38,11 +38,11 @@ func Test_generateFOC(t *testing.T) { Args.Struct = tt.struct_ Args.Package = tt.package_ out := &bytes.Buffer{} - err := generateFOC(out) - if !tt.wantErr(t, err, fmt.Sprintf("generateFOC(%v)", out)) { + err := generateFOP(out) + if !tt.wantErr(t, err, fmt.Sprintf("generateFOP(%v)", out)) { return } - assert.Containsf(t, out.String(), tt.wantOut, "generateFOC(%v)", out) + assert.Containsf(t, out.String(), tt.wantOut, "generateFOP(%v)", out) // remove generated files files, err := filepath.Glob(fmt.Sprintf("%s/*_gen.go", tt.package_)) diff --git a/cmd/root.go b/cmd/root.go index 46c2c43..6b2fe37 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -37,9 +37,12 @@ func initializeRootCmd() (*cobra.Command, error) { # Example: ## Generate 'Functional Option Pattern' code -$ foggo foc --struct ${STRUCT_TYPE_NAME} --package ${PACKAGE_PATH} +$ foggo fop --struct ${STRUCT_TYPE_NAME} --package ${PACKAGE_PATH} + +## Generate 'Applicable Functional Option Pattern' code +$ foggo afop --struct ${STRUCT_TYPE_NAME} --package ${PACKAGE_PATH} `, - Version: "0.0.4", + Version: "0.0.5", } // set arguments @@ -59,8 +62,8 @@ $ foggo foc --struct ${STRUCT_TYPE_NAME} --package ${PACKAGE_PATH} } // set sub commands - rootCmd.AddCommand(initializeFocCommand()) - rootCmd.AddCommand(initializeAfocCommand()) + rootCmd.AddCommand(initializeFopCommand()) + rootCmd.AddCommand(initializeAfopCommand()) // set version format rootCmd.SetVersionTemplate("{{ .Version }}") diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go index 4fe4160..701af0b 100644 --- a/internal/generator/generator_test.go +++ b/internal/generator/generator_test.go @@ -61,7 +61,7 @@ type AOption struct { A string } -func (o AOption) apply(s TestData) { +func (o AOption) apply(s *TestData) { s.A = o.A } @@ -69,7 +69,7 @@ type BOption struct { B int } -func (o BOption) apply(s TestData) { +func (o BOption) apply(s *TestData) { s.B = o.B } diff --git a/internal/generator/templates/applicable_functional_option_pattern.go b/internal/generator/templates/applicable_functional_option_pattern.go index 841c1eb..4b9fbbf 100644 --- a/internal/generator/templates/applicable_functional_option_pattern.go +++ b/internal/generator/templates/applicable_functional_option_pattern.go @@ -9,7 +9,7 @@ type {{ .Name }}Option struct { {{ .Name }} {{ .Type }} } -func (o {{ .Name }}Option) apply(s {{ $.structName }}) { +func (o {{ .Name }}Option) apply(s *{{ $.structName }}) { s.{{ .Name }} = o.{{ .Name }} } {{ end }}{{ end }} diff --git a/testdata/examples/image.go b/testdata/examples/image.go index 317d460..d923f98 100644 --- a/testdata/examples/image.go +++ b/testdata/examples/image.go @@ -1,6 +1,6 @@ package examples -//go:generate foggo foc --struct Image +//go:generate foggo fop --struct Image type Image struct { Width int Height int diff --git a/testdata/examples/image_gen.go b/testdata/examples/image_gen.go index c94d544..ffe33a8 100644 --- a/testdata/examples/image_gen.go +++ b/testdata/examples/image_gen.go @@ -2,32 +2,40 @@ package examples -type ImageOption func(*Image) +type ImageOption interface { + apply(*Image) +} -func NewImage(options ...ImageOption) *Image { - s := &Image{} +type WidthOption struct { + Width int +} - for _, option := range options { - option(s) - } +func (o WidthOption) apply(s *Image) { + s.Width = o.Width +} - return s +type HeightOption struct { + Height int } -func WithWidth(Width int) ImageOption { - return func(args *Image) { - args.Width = Width - } +func (o HeightOption) apply(s *Image) { + s.Height = o.Height } -func WithHeight(Height int) ImageOption { - return func(args *Image) { - args.Height = Height - } +type AltOption struct { + Alt string +} + +func (o AltOption) apply(s *Image) { + s.Alt = o.Alt } -func WithAlt(Alt string) ImageOption { - return func(args *Image) { - args.Alt = Alt +func NewImage(options ...ImageOption) *Image { + s := &Image{} + + for _, option := range options { + option.apply(s) } + + return s } From 5e48e8ca581fc5f80fa52e4718387747ab941f96 Mon Sep 17 00:00:00 2001 From: s14t284 Date: Sat, 15 Jan 2022 13:07:55 +0900 Subject: [PATCH 6/7] :memo: update readme --- README.ja.md | 119 ++++++++++++++++++++++++++++++++++++++++++++++--- README.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 226 insertions(+), 16 deletions(-) diff --git a/README.ja.md b/README.ja.md index 1421928..e3888ae 100644 --- a/README.ja.md +++ b/README.ja.md @@ -16,12 +16,11 @@ $ go install github.com/s14t284/foggo@latest ## Usage -__foggo__ では `foc` サブコマンドを提供しています。 -https://ww24.jp/2019/07/go-option-pattern で提案されているような mock を用いたテストでも利用しやすいオプションを自動生成する `afoc` サブコマンドも提供予定です。 +__foggo__ では `fop` と `afop` サブコマンドを提供しています。 ```shell Usage: - foggo foc [flags] + foggo (fop|afop) [flags] Flags: -h, --help help for foc @@ -93,7 +92,7 @@ Global Flags: } ``` -4. あとは`Functional Option Pattern` を使って実装するだけです。 +4. あとは `Functional Option Pattern` を使って実装するだけです。 ```go package main @@ -135,13 +134,104 @@ Global Flags: $ go generate ./... ``` -3. `go:generate foggo` コメントが付与されている全ての構造体に対して `Functional Option Pattern` コードが自動生成されます。 +3. `go:generate foggo` コメントが付与されている全ての構造体に対して `Functional Option Pattern` のコードが自動生成されます。 + +### Generate with `afop` command + +`afop` は `Applicable Functional Option Pattern` のコードを自動生成するコマンドです。 + +1. `go:generate` コメントを付与して構造体を定義します。サブコマンドとして `afop` コマンドを指定します + + ```go + // ./image/image.go + package image + + //go:generate foggo afop --struct Image + type Image struct { + Width int + Height int + // don't want to create option, specify `foggo:"-"` as the structure tag + Src string `foggo:"-"` + Alt string + } + ``` + +2. `go generate ./...` コマンドを実行してください。 + + ```shell + $ go generate ./... + ``` + +3. `go:generate foggo` コメントが付与されている全ての構造体に対して `Applicable Functional Option Pattern` のコードが自動生成されます。 + + ```go + // Code generated by foggo; DO NOT EDIT. + + package image + + type ImageOption interface { + apply(*Image) + } + + type WidthOption struct { + Width int + } + + func (o WidthOption) apply(s *Image) { + s.Width = o.Width + } + + type HeightOption struct { + Height int + } + + func (o HeightOption) apply(s *Image) { + s.Height = o.Height + } + + type AltOption struct { + Alt string + } + + func (o AltOption) apply(s *Image) { + s.Alt = o.Alt + } + + func NewImage(options ...ImageOption) *Image { + s := &Image{} + + for _, option := range options { + option.apply(s) + } + + return s + } + ``` + +4. あとは `Applicable Functional Option Pattern` を使って実装するだけです。 + + ```go + package main + + import "github.com/user/project/image" + + func main() { + image := NewImage( + WidthOption(1280), + HeightOption(720), + AltOption("alt title"), + ) + image.Src = "./image.png" + ... + } + ``` + ## Functional Option Pattern ? -`Functional Option Pattern` は Golang でよく使われるデザインパターンの一種です。 +`Functional Option Pattern`(`FOP`) は Golang でよく使われるデザインパターンの一種です。 Golang では python や ruby で利用できるキーワード引数のようなオプション引数を提供していません。 -`Functional Option Pattern` を使うことで、オプション引数を再現します。 +`FOP` を使うことで、オプション引数を再現します。 以下の記事が詳しいです。 @@ -150,6 +240,21 @@ Golang では python や ruby で利用できるキーワード引数のよう - https://commandcenter.blogspot.jp/2014/01/self-referential-functions-and-design.html - https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis +### Applicable Functional Option Pattern ? + +`Applicable Functional Option Pattern`(`AFOP`) は生成されるオプションがテスト可能な `FOP` です。 +`FOP` ではオプションを関数として定義します。 +そのため、同一引数を持つオプション関数同士を比較しても等しくないと判定されてしまいます。 + +`AFOP` ではオプションを 単一のパラメータを持ち、`apply` メソッドを実装した構造体として定義します。 +Go言語では構造体は比較可能であるため、同一引数を持つオプション同士を比較することができます。(すなわちテスト可能です) + +`AFOP` については以下の記事が詳しいです。 + +- [Functional Options Pattern に次ぐ、オプション引数を実現する方法](https://ww24.jp/2019/07/go-option-pattern) + - `Applicable Functional Option Pattern` はこの記事で命名されているため名称を拝借しました +- https://github.com/uber-go/guide/blob/master/style.md#functional-options + ## References - https://github.com/moznion/gonstructor diff --git a/README.md b/README.md index 53fc2bd..a6131d0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [日本語版 README](./README.ja.md) -__foggo__ is generator of `Functional Option Pattern` from struct field in Golang code. +__foggo__ is generator of `Functional Option Pattern` And `Applicable Functional Option Pattern` from struct field in Golang code. ## Installation @@ -17,14 +17,14 @@ $ go install github.com/s14t284/foggo@latest ## Usage -__foggo__ provides `foc` subcommand. (`afoc` subcommand will be provided in the future) +__foggo__ provides `fop` and `afop` subcommand. ```shell Usage: - foggo foc [flags] + foggo (fop|afop) [flags] Flags: - -h, --help help for foc + -h, --help help for fop Global Flags: -p, --package string Package name having target struct (default ".") @@ -48,12 +48,12 @@ Global Flags: } ``` -2. execute `foggo foc` command. +2. execute `foggo fop` command. ```shell # struct must be set struct type name # package must be package path - $ foggo foc --struct Image --package image + $ foggo fop --struct Image --package image ``` 3. then `foggo` generates Functional Option Pattern code to `./image/image_gen.go`. @@ -120,7 +120,7 @@ Global Flags: // ./image/image.go package image - //go:generate foggo foc --struct Image + //go:generate foggo fop --struct Image type Image struct { Width int Height int @@ -138,17 +138,122 @@ Global Flags: 3. the `foggo` generate Functional Option Pattern code to all files written `go:generate`. +### Generate with `afop` command + +`afop` is the method to generate `Applicable Functional Option Pattern` code. + +1. prepare a struct type with `go:generate`. (use `afop` subcommand) + + ```go + // ./image/image.go + package image + + //go:generate foggo afop --struct Image + type Image struct { + Width int + Height int + // don't want to create option, specify `foggo:"-"` as the structure tag + Src string `foggo:"-"` + Alt string + } + ``` + +2. execute `go generate ./...` command. + + ```shell + $ go generate ./... + ``` + +3. the `foggo` generate Applicable Functional Option Pattern code to all files written `go:generate`. + + ```go + // Code generated by foggo; DO NOT EDIT. + + package image + + type ImageOption interface { + apply(*Image) + } + + type WidthOption struct { + Width int + } + + func (o WidthOption) apply(s *Image) { + s.Width = o.Width + } + + type HeightOption struct { + Height int + } + + func (o HeightOption) apply(s *Image) { + s.Height = o.Height + } + + type AltOption struct { + Alt string + } + + func (o AltOption) apply(s *Image) { + s.Alt = o.Alt + } + + func NewImage(options ...ImageOption) *Image { + s := &Image{} + + for _, option := range options { + option.apply(s) + } + + return s + } + ``` + +4. write Golang code using `Applicable Functional Option Parameter` + + ```go + package main + + import "github.com/user/project/image" + + func main() { + image := NewImage( + WidthOption(1280), + HeightOption(720), + AltOption("alt title"), + ) + image.Src = "./image.png" + ... + } + ``` + + ## Functional Option Pattern ? -`Functional Option Pattern` is one of the most common design patterns used in Golang code. +`Functional Option Pattern`(`FOP`) is one of the most common design patterns used in Golang code. Golang cannot provide optional arguments such as keyword arguments (available in python, ruby, ...). -`Functional Option Pattern` is the technique for achieving optional arguments. +`FOP` is the technique for achieving optional arguments. For more information, please refer to the following articles. - https://commandcenter.blogspot.jp/2014/01/self-referential-functions-and-design.html - https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis +### Applicable Functional Option Pattern ? + +`Applicable Functional Option Pattern`(`AFOP`) is __testable__ `FOP`. +`FOP` express options to function. +For that reason, comparing to option function with same arguments fails (not testable). + +`AFOP` express options to struct type and options have a parameter and `apply` method. +Struct type is comparable in Golang, options followed `AFOP` are testable. + +`AFOP` proposed by following articles. + +- https://github.com/uber-go/guide/blob/master/style.md#functional-options +- https://ww24.jp/2019/07/go-option-pattern (in Japanese) + ## References - https://github.com/moznion/gonstructor From 637f64be28e2c1ae77145b3e2d216e003973e354 Mon Sep 17 00:00:00 2001 From: s14t284 Date: Sat, 15 Jan 2022 13:13:01 +0900 Subject: [PATCH 7/7] :pencil2: fix typos --- README.ja.md | 8 ++++---- cmd/afop.go | 2 +- cmd/fop_test.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.ja.md b/README.ja.md index e3888ae..1008dec 100644 --- a/README.ja.md +++ b/README.ja.md @@ -23,7 +23,7 @@ Usage: foggo (fop|afop) [flags] Flags: - -h, --help help for foc + -h, --help help for fop Global Flags: -p, --package string Package name having target struct (default ".") @@ -46,12 +46,12 @@ Global Flags: } ``` -2. `foggo foc` コマンドを実行。 +2. `foggo fop` コマンドを実行。 ```shell # struct パラメータには構造体名を指定します # package パラメータには構造体が配置されている相対パスを指定します - $ foggo foc --struct Image --package image + $ foggo fop --struct Image --package image ``` 3. `foggo` コマンドにより、以下のような Functional Option Pattern のコードが `./image/image_gen.go` に自動生成されます。 @@ -118,7 +118,7 @@ Global Flags: // ./image/image.go package image - //go:generate foggo foc --struct Image + //go:generate foggo fop --struct Image type Image struct { Width int Height int diff --git a/cmd/afop.go b/cmd/afop.go index d0bbc9a..88a16e5 100644 --- a/cmd/afop.go +++ b/cmd/afop.go @@ -32,7 +32,7 @@ ref. } } -// generateAFOP generate functional option pattern code +// generateAFOP generate Applicable Functional Option Pattern code func generateAFOP(out io.Writer) error { l := logger.InitializeLogger(out, "[AFOP Generator] ") g := generator.InitializeGenerator() diff --git a/cmd/fop_test.go b/cmd/fop_test.go index 73edff6..8e276c0 100644 --- a/cmd/fop_test.go +++ b/cmd/fop_test.go @@ -20,7 +20,7 @@ func Test_initializeFopCommand(t *testing.T) { a.NotNil(cmd.RunE) } -func Test_generateFOC(t *testing.T) { +func Test_generateFOP(t *testing.T) { tests := []struct { name string struct_ string