-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcollection.go
More file actions
1213 lines (1056 loc) · 36.5 KB
/
Copy pathcollection.go
File metadata and controls
1213 lines (1056 loc) · 36.5 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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package godi
import (
"context"
"errors"
"fmt"
"reflect"
"strconv"
"sync"
"sync/atomic"
"github.com/junioryono/godi/v5/internal/graph"
"github.com/junioryono/godi/v5/internal/reflection"
)
// Global atomic counter for fast ID generation (replaces UUID)
var providerIDCounter atomic.Uint64
// Collection represents a collection of service descriptors that define
// the services available in the dependency injection container.
//
// Collection follows a builder pattern where services are registered
// with their lifetimes and dependencies, then built into a Provider.
//
// Collection is NOT thread-safe. It should be configured in a single
// goroutine before building the Provider.
//
// Example:
//
// collection := godi.NewCollection()
// collection.AddSingleton(NewLogger)
// collection.AddScoped(NewDatabase)
//
// provider, err := collection.Build()
// if err != nil {
// log.Fatal(err)
// }
// defer provider.Close()
type Collection interface {
// Build creates a Provider from the registered services
// using default options.
Build() (Provider, error)
// BuildWithContext creates a Provider with the given context.
// Eager constructors can depend on context.Context and cooperate with
// cancellation; the context is also checked throughout construction.
BuildWithContext(ctx context.Context) (Provider, error)
// BuildWithOptions creates a Provider with custom options
// for validation and behavior configuration.
BuildWithOptions(options *ProviderOptions) (Provider, error)
// AddModules applies one or more module configurations to the service collection.
// Modules provide a way to group related service registrations.
// Registration errors are recorded and reported by Build (or Err).
AddModules(modules ...ModuleOption)
// AddSingleton registers a service with singleton lifetime.
// Only one instance is created and shared across all resolutions.
// Registration errors are recorded and reported by Build (or Err).
AddSingleton(service any, opts ...AddOption)
// AddScoped registers a service with scoped lifetime.
// One instance is created per scope and shared within that scope.
// The service must be a constructor, not a pre-built instance.
// Registration errors are recorded and reported by Build (or Err).
AddScoped(service any, opts ...AddOption)
// AddTransient registers a service with transient lifetime.
// A new instance is created every time the service is resolved.
// The service must be a constructor that returns a service value.
// Registration errors are recorded and reported by Build (or Err).
AddTransient(service any, opts ...AddOption)
// Err returns all registration errors recorded so far, joined into a
// single error, or nil if every registration succeeded. Build returns
// the same errors, so checking Err is only needed when inspecting the
// collection before building.
Err() error
// Contains checks if a service exists for the type.
Contains(serviceType reflect.Type) bool
// ContainsKeyed checks if a keyed service exists.
ContainsKeyed(serviceType reflect.Type, key any) bool
// Remove removes all services for a given service type.
Remove(serviceType reflect.Type)
// RemoveKeyed removes a specific keyed service.
RemoveKeyed(serviceType reflect.Type, key any)
// ToSlice returns a read-only snapshot of all registered services for
// inspection and debugging.
ToSlice() []ServiceInfo
// Count returns the number of registered services.
Count() int
}
// Collection is the core service registry that manages services.
type collection struct {
mu sync.RWMutex
// services stores all non-keyed services by type
services map[TypeKey]*descriptor
// groups stores services that belong to groups
groups map[GroupKey][]*descriptor
// allDescriptors tracks all unique descriptors for efficient iteration
allDescriptors []*descriptor
// analyzer is shared across all registrations for caching
analyzer *reflection.Analyzer
// errs accumulates registration errors so Build can report them all at
// once; the Add* methods do not return errors.
errs []error
// moduleStack tracks the modules currently being applied so that
// registration errors recorded inside a module carry the module's name.
moduleStack []string
}
// TypeKey uniquely identifies a keyed service
type TypeKey struct {
Type reflect.Type
Key any
}
// GroupKey uniquely identifies a group of services
type GroupKey struct {
Type reflect.Type
Group string
}
// ServiceInfo is a read-only description of a registered service, returned by
// Collection.ToSlice for inspection and debugging. It intentionally exposes
// only the stable identity of a registration, not godi's internal wiring.
type ServiceInfo struct {
// ServiceType is the type the service resolves as.
ServiceType reflect.Type
// Key is the name for keyed services, or nil.
Key any
// Group is the value-group name for grouped services, or "".
Group string
// Lifetime is the service's lifetime (Singleton, Scoped, or Transient).
Lifetime Lifetime
}
// NewCollection creates a new empty Collection instance.
//
// Example:
//
// collection := godi.NewCollection()
// collection.AddSingleton(NewLogger)
// provider, err := collection.Build()
func NewCollection() Collection {
return &collection{
services: make(map[TypeKey]*descriptor, 16), // Pre-size for typical usage
groups: make(map[GroupKey][]*descriptor, 4),
allDescriptors: make([]*descriptor, 0, 16),
analyzer: reflection.New(),
}
}
// Build creates a Provider from the registered services using default options.
func (sc *collection) Build() (Provider, error) {
return sc.BuildWithContext(context.Background())
}
// BuildWithContext creates a Provider with the given cooperative build context.
// The context is available to eager constructors that depend on context.Context
// and is checked throughout construction.
func (sc *collection) BuildWithContext(ctx context.Context) (Provider, error) {
if ctx == nil {
ctx = context.Background()
}
return sc.doBuild(ctx)
}
// BuildWithOptions creates a Provider with custom options for validation and behavior configuration.
func (sc *collection) BuildWithOptions(options *ProviderOptions) (Provider, error) {
ctx := context.Background()
// Handle build timeout if specified
if options != nil && options.BuildTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, options.BuildTimeout)
defer cancel()
}
return sc.doBuild(ctx)
}
func (sc *collection) doBuild(ctx context.Context) (Provider, error) {
// Check context before starting
select {
case <-ctx.Done():
return nil, &BuildError{
Phase: "initialization",
Details: "build cancelled before starting",
Cause: ctx.Err(),
}
default:
}
sc.mu.Lock()
defer sc.mu.Unlock()
// Surface every recorded registration error before doing any work:
// the Add* methods defer their errors to Build so callers can register
// services without per-call error checks.
if len(sc.errs) > 0 {
return nil, &BuildError{
Phase: "registration",
Details: "one or more service registrations failed",
Cause: errors.Join(sc.errs...),
}
}
// Build a provider-owned snapshot. Collections remain reusable after Build,
// so providers must never retain the collection's mutable maps, slices, or
// sibling links.
allDescriptors, services, groups := snapshotRegistrations(
sc.allDescriptors,
sc.services,
sc.groups,
)
// Phase 1: Build dependency graph (validates cycles as part of build)
select {
case <-ctx.Done():
return nil, &BuildError{
Phase: "graph",
Details: "build cancelled during graph construction",
Cause: ctx.Err(),
}
default:
}
g := graph.NewDependencyGraphWithCapacity(len(allDescriptors))
for _, descriptor := range allDescriptors {
if descriptor == nil {
continue
}
if err := g.AddProviderDeferred(descriptor); err != nil {
return nil, &BuildError{
Phase: "graph",
Details: fmt.Sprintf("failed to add provider %v", formatType(descriptor.Type)),
Cause: err,
}
}
}
// Phase 1.5: Resolve group dependencies
// Connect group consumers to actual group member nodes in the graph.
// Without this, group consumers depend on phantom nodes (Key=nil) that
// don't match the real group members (Key=1,2,...), causing incorrect
// topological ordering and ErrSingletonNotInitialized during build.
g.ResolveGroupDependencies()
// Phase 2: Validate graph (cycles detected here, not per-add)
if err := g.DetectCycles(); err != nil {
return nil, &BuildError{
Phase: "validation",
Details: "dependency graph validation failed",
Cause: err,
}
}
// Phase 3: Validate lifetimes
select {
case <-ctx.Done():
return nil, &BuildError{
Phase: "validation",
Details: "build cancelled during lifetime validation",
Cause: ctx.Err(),
}
default:
}
if err := sc.validateLifetimes(); err != nil {
return nil, &BuildError{
Phase: "validation",
Details: "lifetime validation failed",
Cause: err,
}
}
// Phase 4: Create provider with fast ID generation
// Count void-return scoped descriptors for pre-allocation
voidCount := 0
for _, d := range allDescriptors {
if d != nil && d.Lifetime == Scoped && d.VoidReturn {
voidCount++
}
}
p := &provider{
id: "p" + strconv.FormatUint(providerIDCounter.Add(1), 36),
services: services,
groups: groups,
graph: g,
analyzer: sc.analyzer, // Share analyzer from collection
singletonKeys: make([]instanceKey, 0, len(allDescriptors)),
voidReturnScopedDescriptors: make([]*descriptor, 0, voidCount),
disposables: make([]Disposable, 0, 4),
disposableSet: make(map[disposableIdentity]struct{}, 4),
scopes: make(map[*scope]struct{}, 4),
closeDone: make(chan struct{}),
}
for _, descriptor := range allDescriptors {
if descriptor != nil && descriptor.Lifetime == Scoped && descriptor.VoidReturn {
p.voidReturnScopedDescriptors = append(p.voidReturnScopedDescriptors, descriptor)
}
}
// Phase 5: Create root scope
select {
case <-ctx.Done():
return nil, &BuildError{
Phase: "scope-creation",
Details: "build cancelled during root scope creation",
Cause: ctx.Err(),
}
default:
}
var err error
rootCtx := context.Background()
p.rootScope, err = newUninitializedScope(p, nil, rootCtx, nil)
if err != nil {
return nil, &BuildError{
Phase: "scope-creation",
Details: "failed to create root scope",
Cause: err,
}
}
// Phase 6: Create singletons with context propagation. Decorate the build
// context so FromContext works inside eager constructors, then clear the
// atomic override before returning the provider.
buildCtx := context.WithValue(ctx, scopeContextKey{}, p.rootScope)
p.rootScope.constructionContext.Store(&scopeConstructionContext{context: buildCtx})
defer func() {
p.rootScope.constructionContext.Store(nil)
}()
if err := p.createAllSingletonsWithContext(ctx); err != nil {
buildErr := &BuildError{
Phase: "singleton-creation",
Details: "failed to initialize singletons",
Cause: err,
}
return nil, joinBuildCleanupError(buildErr, p.Close())
}
// Phase 7: Initialize root-scoped side-effect constructors only after all
// singletons exist. Request/child scopes still initialize them in newScope.
if err := p.rootScope.initializeScopedServices(); err != nil {
buildErr := &BuildError{
Phase: "scope-initialization",
Details: "failed to initialize root scoped services",
Cause: err,
}
return nil, joinBuildCleanupError(buildErr, p.Close())
}
if err := ctx.Err(); err != nil {
buildErr := &BuildError{
Phase: "scope-initialization",
Details: "build deadline expired after root scope initialization",
Cause: err,
}
return nil, joinBuildCleanupError(buildErr, p.Close())
}
return p, nil
}
func joinBuildCleanupError(buildErr, closeErr error) error {
if closeErr == nil {
return buildErr
}
return errors.Join(
buildErr,
&BuildError{
Phase: "cleanup",
Details: "failed to clean up partially created provider",
Cause: closeErr,
},
)
}
// AddModules applies one or more module configurations to the service collection.
// Errors returned by module functions are recorded and reported by Build.
func (sc *collection) AddModules(modules ...ModuleOption) {
for _, module := range modules {
if module == nil {
continue
}
if err := module(sc); err != nil {
sc.recordErr(err)
}
}
}
// AddSingleton adds a singleton service to the collection.
// Registration errors are recorded and reported by Build (or Err).
func (sc *collection) AddSingleton(service any, opts ...AddOption) {
sc.recordErr(sc.addService(service, Singleton, opts...))
}
// AddScoped adds a scoped service to the collection.
// Registration errors are recorded and reported by Build (or Err).
func (sc *collection) AddScoped(service any, opts ...AddOption) {
sc.recordErr(sc.addService(service, Scoped, opts...))
}
// AddTransient adds a transient service to the collection.
// Registration errors are recorded and reported by Build (or Err).
func (sc *collection) AddTransient(service any, opts ...AddOption) {
sc.recordErr(sc.addService(service, Transient, opts...))
}
// recordErr stores a registration error for Build to report, wrapping it
// with the names of the modules being applied (innermost last) so the
// failure is attributable.
func (sc *collection) recordErr(err error) {
if err == nil {
return
}
sc.mu.Lock()
defer sc.mu.Unlock()
for i := len(sc.moduleStack) - 1; i >= 0; i-- {
// Avoid double-wrapping: module functions may already return
// ModuleError for the innermost module.
var moduleErr *ModuleError
if errors.As(err, &moduleErr) && moduleErr.Module == sc.moduleStack[i] {
continue
}
err = &ModuleError{Module: sc.moduleStack[i], Cause: err}
}
sc.errs = append(sc.errs, err)
}
// Err returns all registration errors recorded so far, joined into a single
// error, or nil if every registration succeeded.
func (sc *collection) Err() error {
sc.mu.RLock()
defer sc.mu.RUnlock()
return errors.Join(sc.errs...)
}
// pushModule and popModule maintain the module attribution stack used by
// recordErr. They are invoked by NewModule via interface assertion.
func (sc *collection) pushModule(name string) {
sc.mu.Lock()
sc.moduleStack = append(sc.moduleStack, name)
sc.mu.Unlock()
}
func (sc *collection) popModule() {
sc.mu.Lock()
if len(sc.moduleStack) > 0 {
sc.moduleStack = sc.moduleStack[:len(sc.moduleStack)-1]
}
sc.mu.Unlock()
}
// Contains checks if a service exists for the type
func (r *collection) Contains(t reflect.Type) bool {
if t == nil {
return false
}
r.mu.RLock()
defer r.mu.RUnlock()
typeKey := TypeKey{Type: t}
_, ok := r.services[typeKey]
return ok
}
// ContainsKeyed checks if a keyed service exists
func (r *collection) ContainsKeyed(t reflect.Type, key any) bool {
if t == nil {
return false
}
// Value-level comparability: a comparable static type can still wrap a
// non-comparable value in an interface field and panic as a map key.
if key != nil && !reflect.ValueOf(key).Comparable() {
return false
}
r.mu.RLock()
defer r.mu.RUnlock()
typeKey := TypeKey{Type: t, Key: key}
_, ok := r.services[typeKey]
return ok
}
// HasGroup checks if a group has any services registered for the specified type and group name.
// Returns false if the type is nil, group name is empty, or no services are registered in the group.
func (r *collection) HasGroup(t reflect.Type, group string) bool {
if t == nil || group == "" {
return false
}
r.mu.RLock()
defer r.mu.RUnlock()
groupKey := GroupKey{Type: t, Group: group}
services, ok := r.groups[groupKey]
return ok && len(services) > 0
}
// Remove removes all services for a given type: the unkeyed registration,
// every keyed registration, and every group member of that type.
func (r *collection) Remove(t reflect.Type) {
if t == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
removed := make(map[*descriptor]struct{})
for key, descriptor := range r.services {
if key.Type == t {
removed[descriptor] = struct{}{}
delete(r.services, key)
}
}
for key, descriptors := range r.groups {
if key.Type == t {
for _, descriptor := range descriptors {
removed[descriptor] = struct{}{}
}
delete(r.groups, key)
}
}
r.pruneDescriptors(removed)
}
// RemoveKeyed removes a specific keyed service
func (r *collection) RemoveKeyed(t reflect.Type, key any) {
if t == nil {
return
}
// Value-level comparability: a comparable static type can still wrap a
// non-comparable value in an interface field and panic as a map key.
if key != nil && !reflect.ValueOf(key).Comparable() {
return
}
r.mu.Lock()
defer r.mu.Unlock()
typeKey := TypeKey{Type: t, Key: key}
d, ok := r.services[typeKey]
if !ok {
return
}
delete(r.services, typeKey)
r.pruneDescriptors(map[*descriptor]struct{}{d: {}})
}
// pruneDescriptors drops the given descriptors from allDescriptors so that
// Build, Count, and ToSlice no longer see them. Without this, removed
// singletons would still be constructed at build time.
func (r *collection) pruneDescriptors(removed map[*descriptor]struct{}) {
if len(removed) == 0 {
return
}
kept := r.allDescriptors[:0]
for _, d := range r.allDescriptors {
if _, ok := removed[d]; !ok {
kept = append(kept, d)
}
}
// Zero the tail so the backing array doesn't pin removed descriptors.
for i := len(kept); i < len(r.allDescriptors); i++ {
r.allDescriptors[i] = nil
}
r.allDescriptors = kept
// Unlink removed descriptors from survivors' sibling lists. Otherwise a
// surviving sibling's constructor invocation would still cache instances
// under the removed registration's keys, shadowing any replacement
// registered after the removal.
for _, d := range r.allDescriptors {
if len(d.siblings) == 0 {
continue
}
pruned := false
for _, sibling := range d.siblings {
if _, ok := removed[sibling]; ok {
pruned = true
break
}
}
if !pruned {
continue
}
surviving := make([]*descriptor, 0, len(d.siblings))
for _, sibling := range d.siblings {
if _, ok := removed[sibling]; !ok {
surviving = append(surviving, sibling)
}
}
for _, sibling := range surviving {
sibling.siblings = surviving
}
}
}
// ToSlice returns a copy of all registered service descriptors
func (r *collection) ToSlice() []ServiceInfo {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]ServiceInfo, 0, len(r.allDescriptors))
for _, d := range r.allDescriptors {
if d == nil {
continue
}
result = append(result, ServiceInfo{
ServiceType: d.Type,
Key: d.Key,
Group: d.Group,
Lifetime: d.Lifetime,
})
}
return result
}
// Count returns the number of registered services in the collection.
func (r *collection) Count() int {
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.allDescriptors)
}
var (
// Reserved types that are handled specially by the framework
reservedTypes = map[reflect.Type]struct{}{
reflect.TypeFor[context.Context](): {},
reflect.TypeFor[Provider](): {},
reflect.TypeFor[Scope](): {},
}
)
// addService registers a new service with the specified lifetime and options.
// It performs validation, creates descriptors, handles multi-return constructors,
// and manages interface registrations when using the As option.
func (r *collection) addService(service any, lifetime Lifetime, opts ...AddOption) error {
// Validate inputs
if service == nil {
return &ValidationError{
ServiceType: nil,
Cause: ErrConstructorNil,
}
}
// Create descriptor from constructor using shared analyzer
descriptor, err := newDescriptorWithAnalyzer(service, lifetime, r.analyzer, opts...)
if err != nil {
return &RegistrationError{
ServiceType: nil,
Operation: "create descriptor",
Cause: err,
}
}
// Validate the descriptor
if validationErr := descriptor.Validate(); validationErr != nil {
return &RegistrationError{
ServiceType: descriptor.Type,
Operation: "validate descriptor",
Cause: validationErr,
}
}
// Check if the service type is reserved
if _, isReserved := reservedTypes[descriptor.Type]; isReserved {
return &ValidationError{
ServiceType: descriptor.Type,
Cause: fmt.Errorf("service type %s is reserved and cannot be registered", formatType(descriptor.Type)),
}
}
r.mu.Lock()
defer r.mu.Unlock()
// newDescriptorWithAnalyzer already parsed options and validated them,
// and Analyze() was called on the way through. Re-parse the options
// locally so we can inspect them (Name/Group/As), but skip the second
// Analyze call and the second Validate by reading the cached info off
// the descriptor.
options := &addOptions{}
for _, opt := range opts {
if opt != nil {
opt.applyAddOption(options)
}
}
info := descriptor.info
if info == nil {
// Defensive fallback: a descriptor constructed outside the normal
// path won't have info stashed. Re-analyze in that case.
var err error
info, err = r.analyzer.Analyze(service)
if err != nil {
return &ReflectionAnalysisError{
Constructor: service,
Operation: "analyze",
Cause: err,
}
}
}
// Handle result objects (Out structs)
if info.IsResultObject {
if options.Name != "" || options.Group != "" {
return &RegistrationError{
ServiceType: descriptor.Type,
Operation: "register result object",
Cause: fmt.Errorf("godi.Name and godi.Group cannot be applied to a result object (godi.Out) constructor; put name or group tags on its fields"),
}
}
// godi.As is ambiguous for result objects: it's unclear which field
// the interface should bind to. Reject explicitly rather than
// silently dropping the option.
if len(options.As) > 0 {
return &RegistrationError{
ServiceType: descriptor.Type,
Operation: "register result object",
Cause: fmt.Errorf("godi.As cannot be combined with a result object (godi.Out) constructor; use a name or group tag on the field instead"),
}
}
return r.registerResultObjectFields(descriptor)
}
// Handle multiple return types (not Out structs)
if handled, err := r.registerMultiReturn(descriptor, info, options); handled {
return err
}
// Handle As option - register under interface types.
// If As is specified, we only register under interface types, not the concrete type.
if len(options.As) > 0 {
return r.registerAliases(descriptor, options)
}
// Register the descriptor normally
return r.registerDescriptor(descriptor)
}
// registerAliases registers a descriptor under each interface type in
// options.As instead of its concrete type. The aliases are linked as siblings
// so one constructor invocation caches every interface entry. Caller must hold
// r.mu.
func (r *collection) registerAliases(d *descriptor, options *addOptions) error {
// A void or error-only constructor produces no service value to bind
// to an interface. Reject rather than registering an empty struct
// placeholder under the interface type.
if d.VoidReturn {
return &RegistrationError{
ServiceType: d.Type,
Operation: "register as interface",
Cause: fmt.Errorf("godi.As cannot be combined with a constructor that returns no service value"),
}
}
// Validate every alias before committing any of them. A single Add call is
// transactional: either all requested interfaces are registered or none
// are.
interfaceDescriptors := make([]*descriptor, 0, len(options.As))
seenInterfaces := make(map[reflect.Type]struct{}, len(options.As))
for _, iface := range options.As {
interfaceType := reflect.TypeOf(iface).Elem()
if _, duplicate := seenInterfaces[interfaceType]; duplicate {
return &RegistrationError{
ServiceType: interfaceType,
Operation: "register as interface",
Cause: fmt.Errorf("interface %s was specified more than once", formatType(interfaceType)),
}
}
seenInterfaces[interfaceType] = struct{}{}
// Reserved types are special-cased by the resolver and cannot be
// registered, not even via As.
if _, isReserved := reservedTypes[interfaceType]; isReserved {
return &ValidationError{
ServiceType: interfaceType,
Cause: fmt.Errorf("service type %s is reserved and cannot be registered", formatType(interfaceType)),
}
}
// Validate that the service type implements the interface
if !d.Type.Implements(interfaceType) {
return &TypeMismatchError{
Expected: interfaceType,
Actual: d.Type,
Context: "interface implementation",
}
}
// Create a new descriptor for the interface type
interfaceDescriptor := d.clone()
interfaceDescriptor.Type = interfaceType
interfaceDescriptor.As = options.As
interfaceDescriptor.isAlias = true
interfaceDescriptors = append(interfaceDescriptors, interfaceDescriptor)
}
for _, interfaceDescriptor := range interfaceDescriptors {
interfaceDescriptor.siblings = interfaceDescriptors
}
registered := make([]*descriptor, 0, len(interfaceDescriptors))
for _, interfaceDescriptor := range interfaceDescriptors {
if err := r.registerDescriptor(interfaceDescriptor); err != nil {
r.unregisterDescriptors(registered)
return &RegistrationError{
ServiceType: interfaceDescriptor.Type,
Operation: "register as interface",
Cause: err,
}
}
registered = append(registered, interfaceDescriptor)
}
return nil
}
// snapshotRegistrations clones the mutable registration graph owned by a
// collection. Descriptor metadata and constructor analysis are immutable after
// registration, but descriptors and their sibling slices are rewritten by
// Remove, so those links must be remapped to provider-owned clones.
func snapshotRegistrations(
all []*descriptor,
services map[TypeKey]*descriptor,
groups map[GroupKey][]*descriptor,
) (
snapshotAll []*descriptor,
snapshotServices map[TypeKey]*descriptor,
snapshotGroups map[GroupKey][]*descriptor,
) {
clones := make(map[*descriptor]*descriptor, len(all))
snapshotAll = make([]*descriptor, 0, len(all))
for _, original := range all {
if original == nil {
continue
}
clone := *original
clone.siblings = nil
clone.As = append([]any(nil), original.As...)
clone.Dependencies = append([]*reflection.Dependency(nil), original.Dependencies...)
clone.resultFields = append([]reflection.ResultField(nil), original.resultFields...)
clone.paramFields = append([]reflection.ParamField(nil), original.paramFields...)
clones[original] = &clone
snapshotAll = append(snapshotAll, &clone)
}
for original, clone := range clones {
if len(original.siblings) == 0 {
continue
}
clone.siblings = make([]*descriptor, 0, len(original.siblings))
for _, sibling := range original.siblings {
if siblingClone, ok := clones[sibling]; ok {
clone.siblings = append(clone.siblings, siblingClone)
}
}
}
snapshotServices = make(map[TypeKey]*descriptor, len(services))
for key, original := range services {
if clone, ok := clones[original]; ok {
snapshotServices[key] = clone
}
}
snapshotGroups = make(map[GroupKey][]*descriptor, len(groups))
for key, originals := range groups {
members := make([]*descriptor, 0, len(originals))
for _, original := range originals {
if clone, ok := clones[original]; ok {
members = append(members, clone)
}
}
snapshotGroups[key] = members
}
return snapshotAll, snapshotServices, snapshotGroups
}
// registerResultObjectFields registers each exported field of a result
// object (Out struct) as its own service. The fields all share the same
// constructor and are linked as siblings so one invocation can cache every
// field under its own registration (key or group). The result object type
// itself is not registered. Caller must hold r.mu.
func (r *collection) registerResultObjectFields(d *descriptor) error {
// No fields to register
if len(d.resultFields) == 0 {
return nil
}
fieldDescriptors := make([]*descriptor, 0, len(d.resultFields))
for _, field := range d.resultFields {
// A field cannot be both keyed and grouped: the resolver caches and
// looks up under exactly one of the two, so accepting both would
// register a service that can never be resolved consistently.
if field.Key != nil && field.Group != "" {
return &RegistrationError{
ServiceType: field.Type,
Operation: "register result object field",
Cause: fmt.Errorf("field %s cannot have both name and group tags", field.Name),
}
}
fieldDescriptor := d.clone()
fieldDescriptor.Type = field.Type
fieldDescriptor.Key = field.Key
fieldDescriptor.Group = field.Group
fieldDescriptor.resultFieldIndex = field.Index
fieldDescriptors = append(fieldDescriptors, fieldDescriptor)
}
for _, fieldDescriptor := range fieldDescriptors {
fieldDescriptor.siblings = fieldDescriptors
}
registered := make([]*descriptor, 0, len(fieldDescriptors))
for _, fieldDescriptor := range fieldDescriptors {
if err := r.registerDescriptor(fieldDescriptor); err != nil {
// Roll back the fields registered so far: leaving them in place
// would keep sibling links to never-registered descriptors,
// corrupting primary detection and scoped caching for callers
// that ignore the Add error.
r.unregisterDescriptors(registered)
return &RegistrationError{
ServiceType: fieldDescriptor.Type,
Operation: "register result object field",
Cause: err,
}
}
registered = append(registered, fieldDescriptor)
}
return nil
}
// registerMultiReturn registers each non-error return of a multi-return
// constructor as its own service, linking the descriptors as siblings.
// Returns handled=false when the constructor has at most one non-error
// return, in which case the caller proceeds with normal registration.
// Caller must hold r.mu.
func (r *collection) registerMultiReturn(d *descriptor, info *reflection.ConstructorInfo, options *addOptions) (bool, error) {
if !info.IsFunc || len(info.Returns) <= 1 {
return false, nil
}
// Filter out error returns to get actual service types
nonErrorReturns := make([]reflection.ReturnInfo, 0)
for _, ret := range info.Returns {
if !ret.IsError {
nonErrorReturns = append(nonErrorReturns, ret)
}
}
if len(nonErrorReturns) <= 1 {
return false, nil
}
// godi.As is ambiguous for multi-return constructors: it's unclear which
// return value the interface should bind to. Reject explicitly rather
// than silently dropping the option.
if len(options.As) > 0 {
return true, &RegistrationError{
ServiceType: d.Type,
Operation: "register multi-return type",
Cause: fmt.Errorf("godi.As cannot be combined with a multi-return constructor; register a wrapper constructor that returns the desired interface"),