commit b985c9ff0bfb1f050d425715a941a51e35f5d0c4 Author: jolheiser Date: Thu Mar 3 14:50:36 2022 -0600 Initial commit Signed-off-by: jolheiser diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62c8935 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ec2045e --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 John Olheiser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f0d528 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# opt + +`opt` is a tiny package that reduces boilerplate for functional options. + +Simply decide whether your options can return errors or not, then use the corresponding type. + +Without errors: +```go +type Foo struct { + Bar string +} + +func WithBar(b string) opt.Func[Foo] { + return func(f *Foo) { + f.Bar = b + } +} +``` + +With errors: +```go +type Foo struct { + Bar string +} + +func WithBar(b string) opt.ErrorFunc[Foo] { + return func(f *Foo) error { + f.Bar = b + return nil + } +} +``` + +## License + +[MIT](LICENSE) + +Credit to `@segfaultax` in the [Discord Gophers](https://discord.gg/golang) server for the idea and starting implementation. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..133d356 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module go.jolheiser.com/opt + +go 1.18 + +require github.com/matryer/is v1.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ddd6bbf --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= diff --git a/opt.go b/opt.go new file mode 100644 index 0000000..f11a4e4 --- /dev/null +++ b/opt.go @@ -0,0 +1,24 @@ +package opt + +// Func is a functional option for A +type Func[A any] func(*A) + +// Apply applies all options to A +func Apply[A any](a *A, opts ...Func[A]) { + for _, opt := range opts { + opt(a) + } +} + +// ErrorFunc is a functional option for A that can return an error +type ErrorFunc[A any] func(*A) error + +// ApplyError applies all options to A, returning the first error +func ApplyError[A any](a *A, opts ...ErrorFunc[A]) error { + for _, opt := range opts { + if err := opt(a); err != nil { + return err + } + } + return nil +} diff --git a/opt_example_test.go b/opt_example_test.go new file mode 100644 index 0000000..b7fead9 --- /dev/null +++ b/opt_example_test.go @@ -0,0 +1,30 @@ +package opt_test + +import ( + "fmt" + + "go.jolheiser.com/opt" +) + +func ExampleApply() { + f := &Foo{ + Bar: "default", + } + opt.Apply(f, WithBar("override")) + + fmt.Println(f.Bar) + // Output: + // override +} + +func ExampleApplyError() { + f := &Foo{ + Bar: "default", + } + err := opt.ApplyError(f, WithBarErr("uh oh!")) // WithBarErr always returns errors.New(b) + if err != nil { + fmt.Println(err) + } + // Output: + // uh oh! +} diff --git a/opt_test.go b/opt_test.go new file mode 100644 index 0000000..21a93c8 --- /dev/null +++ b/opt_test.go @@ -0,0 +1,69 @@ +package opt_test + +import ( + "errors" + "strconv" + "testing" + + "go.jolheiser.com/opt" + + "github.com/matryer/is" +) + +func TestOptions(t *testing.T) { + assert := is.New(t) + + f := NewFoo(WithBar("bar")) + assert.Equal(f.Bar, "bar") // Option should set bar + + opt.Apply(f, WithBaz(100)) + assert.Equal(f.Baz, 100) // Apply should set Baz + + _, err := NewErrorFoo(WithBarErr("bar")) + assert.True(err != nil) // ErrorOption should return error + assert.Equal(err.Error(), "bar") // ErrorOption error should be bar + + err = opt.ApplyError(f, WithBazErr(100)) + assert.True(err != nil) // ApplyError should return error + assert.Equal(err.Error(), "100") // ApplyError error should be 100 +} + +type Foo struct { + Bar string + Baz int +} + +func WithBar(b string) opt.Func[Foo] { + return func(f *Foo) { + f.Bar = b + } +} + +func WithBaz(b int) opt.Func[Foo] { + return func(f *Foo) { + f.Baz = b + } +} + +func NewFoo(opts ...opt.Func[Foo]) *Foo { + f := &Foo{ /** some defaults **/ } + opt.Apply(f, opts...) + return f +} + +func NewErrorFoo(opts ...opt.ErrorFunc[Foo]) (*Foo, error) { + f := &Foo{ /** some defaults **/ } + return f, opt.ApplyError(f, opts...) +} + +func WithBarErr(b string) opt.ErrorFunc[Foo] { + return func(f *Foo) error { + return errors.New(b) + } +} + +func WithBazErr(b int) opt.ErrorFunc[Foo] { + return func(f *Foo) error { + return errors.New(strconv.Itoa(b)) + } +}