From 8bf5b787b8413fb654b6d479a9958f9b143c6dcd Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 26 Aug 2025 11:50:34 -0700 Subject: [PATCH] Add explicit Optional types --- di.go | 30 +++++++++++++++---- di_test.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/di.go b/di.go index 04b0ec0c761f170c5c1304dc7b3a05aa59d6f995..83caa311de10b5c2a06740fd4382520f9de94dee 100644 --- a/di.go +++ b/di.go @@ -39,6 +39,26 @@ type Provide[Out any] struct { fOrV any } +// Optional is the canonical way of specifying a field is optional. The library +// does not handle this value in a special way. +type Optional[T any] struct { + Val T + IsSome bool +} + +// Unwrap returns the optional value T or panics if not present +func (o Optional[T]) Unwrap() T { + if !o.IsSome { + panic(fmt.Sprintf("expected optional value of type %T to be present", o.Val)) + } + return o.Val +} + +// Some returns a new Optional type supplied with the value. +func Some[T any](val T) Optional[T] { + return Optional[T]{Val: val, IsSome: true} +} + type provideI interface { diOutType() reflect.Type diPayload() any @@ -203,13 +223,14 @@ func Build[C any, R any](config C, result R) error { } // Can we just resolve the direct result type? - if v, err := c.resolve(resV.Elem().Type()); err == nil { + v, err := c.resolve(resV.Elem().Type()) + if err == nil { resV.Elem().Set(v) return nil } if resV.Elem().Kind() != reflect.Struct { - return fmt.Errorf("couldn't build result direct, and can not fill result as it is not a struct") + return fmt.Errorf("couldn't build result directly (%w), and can not fill result as it is not a struct", err) } // Populate result fields of the struct @@ -370,10 +391,7 @@ func (c *collector) collect(v reflect.Value, path string) error { } default: - // preprovided singular instance (non-zero only) - if !fv.IsZero() { - c.values[fv.Type()] = fv - } + c.values[fv.Type()] = fv } } return nil diff --git a/di_test.go b/di_test.go index 1688837c502f01d616b96280e7c58cc92181ad2a..1e5d0893541cf969e667cc00763c9fffa6015ebc 100644 --- a/di_test.go +++ b/di_test.go @@ -1,6 +1,7 @@ package di import ( + "crypto/tls" "errors" "fmt" "net/http" @@ -64,6 +65,40 @@ func ExampleNew() { // Output: Hello, Alice. You've been around the sun 42 times! } +func ExampleOptional() { + type Config struct { + // A *tls.Config type or Provide[*tls.Config] also works, but using the + // Optional wrapper lets us convey the optionality explicitly + TLSConfig Optional[*tls.Config] + Server Provide[*http.Server] + } + + cfg := Config{ + Server: MustProvide[*http.Server](func( + tlsConf Optional[*tls.Config], + ) *http.Server { + s := &http.Server{ + Addr: ":8080", + } + if tlsConf.IsSome { + s.TLSConfig = tlsConf.Val + } + return s + }), + } + + server, err := New[*http.Server](cfg) + if err != nil { + fmt.Println(err) + return + } + if server.TLSConfig == nil { + fmt.Println("No TLS configuration was provided") + } + + // Output: No TLS configuration was provided +} + func ExampleSideEffect() { type Config struct { Server *http.Server @@ -231,6 +266,57 @@ func TestBuildSuccess(t *testing.T) { } }, }, + { + name: "pre-supplied nil values", + config: struct { + A *A + }{ + A: nil, + }, + result: &struct { + A *A + }{}, + verify: func(t *testing.T, result any) { + res := result.(*struct { + A *A + }) + if res.A != nil { + t.Fatalf("expected nil A, got %+v", res.A) + } + }, + }, + { + name: "Explicit Optional Value", + config: struct { + A Optional[*A] + }{}, + result: &struct { + A Optional[*A] + }{}, + verify: func(t *testing.T, result any) { + res := result.(*struct { + A Optional[*A] + }) + if res.A.IsSome { + t.Fatalf("expected none") + } + }, + }, + { + name: "Explicit Provided Optional Value", + config: struct { + A Optional[*A] + }{A: Some(&A{})}, + result: &struct { + A Optional[*A] + }{}, + verify: func(t *testing.T, result any) { + res := result.(*struct { + A Optional[*A] + }) + _ = res.A.Unwrap() + }, + }, { name: "type aliases", config: struct {