-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmodule.go
More file actions
304 lines (273 loc) · 8.22 KB
/
Copy pathmodule.go
File metadata and controls
304 lines (273 loc) · 8.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
package godi
import (
"bytes"
"fmt"
"reflect"
"strings"
)
// ModuleOption represents a registration action within a module.
type ModuleOption func(Collection) error
// NewModule creates a new module with the given name and builders.
// Modules are a way to group related service registrations together.
//
// Example:
//
// var DatabaseModule = godi.NewModule("database",
// godi.AddSingleton(NewDatabaseConnection),
// godi.AddScoped(NewUserRepository),
// godi.AddScoped(NewOrderRepository),
// )
//
// var CacheModule = godi.NewModule("cache",
// godi.AddSingleton(cache.New[any]),
// godi.AddSingleton(NewCacheMetrics),
// )
//
// var AppModule = godi.NewModule("app",
// DatabaseModule,
// CacheModule,
// godi.AddScoped(NewService1),
// godi.AddScoped(NewService1, godi.Name("service1")),
// godi.AddScoped(NewService1, godi.Name("service2")),
// )
func NewModule(name string, builders ...ModuleOption) ModuleOption {
return func(s Collection) error {
// Attribute registration errors recorded by the builders (whose Add*
// calls defer errors to Build) to this module by name.
if c, ok := s.(*collection); ok {
c.pushModule(name)
defer c.popModule()
}
// Execute all builders in order
for _, builder := range builders {
if builder == nil {
continue
}
if err := builder(s); err != nil {
return &ModuleError{Module: name, Cause: err}
}
}
return nil
}
}
// AddSingleton creates a ModuleBuilder for adding a singleton service.
// Registration errors are recorded on the collection and reported by Build.
func AddSingleton(service any, opts ...AddOption) ModuleOption {
return func(s Collection) error {
s.AddSingleton(service, opts...)
return nil
}
}
// AddScoped creates a ModuleBuilder for adding a scoped service.
// Registration errors are recorded on the collection and reported by Build.
func AddScoped(service any, opts ...AddOption) ModuleOption {
return func(s Collection) error {
s.AddScoped(service, opts...)
return nil
}
}
// AddTransient creates a ModuleBuilder for adding a transient service.
// Registration errors are recorded on the collection and reported by Build.
func AddTransient(service any, opts ...AddOption) ModuleOption {
return func(s Collection) error {
s.AddTransient(service, opts...)
return nil
}
}
// An AddOption modifies the default behavior of AddSingleton, AddScoped, and AddTransient.
type AddOption interface {
applyAddOption(*addOptions)
}
type addOptions struct {
Name string
Group string
As []any
}
func (o *addOptions) Validate() error {
if o.Group != "" {
if o.Name != "" {
return &ValidationError{
ServiceType: nil,
Cause: fmt.Errorf("cannot use both godi.Name and godi.Group: name:%q provided with group:%q", o.Name, o.Group),
}
}
}
// Names must be representable inside a backquoted string. The only
// limitation for raw string literals as per
// https://golang.org/ref/spec#raw_string_lit is that they cannot contain
// backquotes.
if strings.ContainsRune(o.Name, '`') {
return &ValidationError{
ServiceType: nil,
Cause: fmt.Errorf("invalid godi.Name(%q): names cannot contain backquotes", o.Name),
}
}
if strings.ContainsRune(o.Group, '`') {
return &ValidationError{
ServiceType: nil,
Cause: fmt.Errorf("invalid godi.Group(%q): group names cannot contain backquotes", o.Group),
}
}
for _, i := range o.As {
t := reflect.TypeOf(i)
if t == nil {
return &ValidationError{
ServiceType: nil,
Cause: fmt.Errorf("invalid godi.As(nil): argument must be a pointer to an interface"),
}
}
if t.Kind() != reflect.Pointer {
return &ValidationError{
ServiceType: nil,
Cause: fmt.Errorf("invalid godi.As(%v): argument must be a pointer to an interface", t),
}
}
pointingTo := t.Elem()
if pointingTo.Kind() != reflect.Interface {
return &ValidationError{
ServiceType: nil,
Cause: fmt.Errorf("invalid godi.As(*%v): argument must be a pointer to an interface", pointingTo),
}
}
}
return nil
}
// Name is an AddOption that specifies that all values produced by a
// constructor should have the given name. See also the package documentation
// about Named Values.
//
// Given,
//
// func NewReadOnlyConnection(...) (*Connection, error)
// func NewReadWriteConnection(...) (*Connection, error)
//
// The following will provide two connections to the container: one under the
// name "ro" and the other under the name "rw".
//
// c.AddSingleton(NewReadOnlyConnection, godi.Name("ro"))
// c.AddSingleton(NewReadWriteConnection, godi.Name("rw"))
//
// This option cannot be provided for constructors which produce result
// objects.
func Name(name string) AddOption {
return addNameOption(name)
}
type addNameOption string
func (o addNameOption) String() string {
return fmt.Sprintf("Name(%q)", string(o))
}
func (o addNameOption) applyAddOption(opt *addOptions) {
opt.Name = string(o)
}
// Group is an AddOption that specifies that all values produced by a
// constructor should be added to the specified group. See also the package
// documentation about Value Groups.
//
// This option cannot be provided for constructors which produce result
// objects.
func Group(group string) AddOption {
return addGroupOption(group)
}
type addGroupOption string
func (o addGroupOption) String() string {
return fmt.Sprintf("Group(%q)", string(o))
}
func (o addGroupOption) applyAddOption(opt *addOptions) {
opt.Group = string(o)
}
// As is an AddOption that specifies that the value produced by the
// constructor implements the interface T and is provided to the container
// as that interface.
//
// The value will then be available in the container as an implementation of
// T, but not as its concrete type. Pass As multiple times to register the
// value under several interfaces.
//
// For example, the following will make io.Reader and io.Writer available
// in the container, but not the concrete buffer type.
//
// c.AddSingleton(newBuffer, godi.As[io.Reader](), godi.As[io.Writer]())
//
// That is, the above is equivalent to the following.
//
// c.AddSingleton(func(...) (io.Reader, io.Writer) {
// b := newBuffer(...)
// return b, b
// })
//
// If used with godi.Name, the types specified with godi.As will all use the
// same name. For example,
//
// c.AddSingleton(newFile, godi.As[io.Reader](), godi.Name("temp"))
//
// The above is equivalent to the following.
//
// type Result struct {
// godi.Out
//
// Reader io.Reader `name:"temp"`
// }
//
// c.AddSingleton(func(...) Result {
// f := newFile(...)
// return Result{
// Reader: f,
// }
// })
//
// This option cannot be provided for constructors which produce result
// objects or have multiple non-error return values, and reserved types
// (context.Context, godi.Provider, godi.Scope) cannot be registered this way.
func As[T any]() AddOption {
return addAsOption{new(T)}
}
type addAsOption []any
func (o addAsOption) String() string {
buf := bytes.NewBufferString("As(")
for i, iface := range o {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(reflect.TypeOf(iface).Elem().String())
}
buf.WriteString(")")
return buf.String()
}
func (o addAsOption) applyAddOption(opts *addOptions) {
opts.As = append(opts.As, o...)
}
// Remove creates a ModuleOption for removing all services of type T.
// This is useful for testing scenarios where you need to replace a service
// with a mock implementation.
//
// Example:
//
// c.AddModules(
// godi.Remove[posthog.Client](),
// godi.AddSingleton(infrastructure.NewPostHogClientMock),
// // ... other modules
// )
// // Any registration errors surface from c.Build().
func Remove[T any]() ModuleOption {
return func(c Collection) error {
c.Remove(reflect.TypeFor[T]())
return nil
}
}
// RemoveKeyed creates a ModuleOption for removing a specific keyed service of type T.
// This allows you to remove only services registered with a specific key.
//
// Example:
//
// c.AddModules(
// godi.RemoveKeyed[database.Connection]("primary"),
// godi.AddSingleton(NewMockConnection, godi.Name("primary")),
// // ... other modules
// )
// // Any registration errors surface from c.Build().
func RemoveKeyed[T any](key any) ModuleOption {
return func(c Collection) error {
c.RemoveKeyed(reflect.TypeFor[T](), key)
return nil
}
}