From a6ab158855179b95bbb76fcb391b5b8d7e23435e Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 27 Aug 2026 23:41:12 +0800 Subject: [PATCH] feat(core): implement context service hub and generic ioc container --- core/container.go | 189 +++++++++++++++++ core/context.go | 268 ++++++++++++++++++++++++ core/context_test.go | 475 +++++++++++++++++++++++++++++++++++++++++++ core/manifest.go | 35 ++++ core/types.go | 71 +++++++ 5 files changed, 1038 insertions(+) create mode 100644 core/container.go create mode 100644 core/context.go create mode 100644 core/context_test.go create mode 100644 core/manifest.go create mode 100644 core/types.go diff --git a/core/container.go b/core/container.go new file mode 100644 index 00000000..ff9311ee --- /dev/null +++ b/core/container.go @@ -0,0 +1,189 @@ +package core + +import ( + "fmt" + "reflect" + "sync" +) + +// Container manages service registration and resolution using Go reflection and generics. +type Container struct { + mu sync.RWMutex + parent *Container + services map[reflect.Type]any + listeners map[reflect.Type][]func(any) +} + +// NewContainer creates a new IoC container instance with an optional parent container. +func NewContainer(parent *Container) *Container { + return &Container{ + parent: parent, + services: make(map[reflect.Type]any), + listeners: make(map[reflect.Type][]func(any)), + } +} + +func isNil(i any) bool { + if i == nil { + return true + } + v := reflect.ValueOf(i) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice: + return v.IsNil() + default: + return false + } +} + +// Provide registers a typed service implementation into the Context's IoC container. +func Provide[T any](ctx *Context, service T) { + if ctx == nil { + panic("core: nil context provided to Provide") + } + if isNil(service) { + panic("core: cannot provide nil service") + } + + targetType := reflect.TypeFor[T]() + ctx.Container().provide(targetType, service) +} + +func (c *Container) provide(targetType reflect.Type, service any) { + c.mu.Lock() + c.services[targetType] = service + + // Collect any matching listeners to invoke outside the lock + var callbacks []func(any) + svcType := reflect.TypeOf(service) + for lType, cbs := range c.listeners { + if lType == targetType || (lType.Kind() == reflect.Interface && svcType.Implements(lType)) { + callbacks = append(callbacks, cbs...) + } + } + c.mu.Unlock() + + for _, cb := range callbacks { + cb(service) + } +} + +// Inject resolves a registered service of type T from the Context. +func Inject[T any](ctx *Context) (T, error) { + var zero T + if ctx == nil { + return zero, ErrNilContext + } + + targetType := reflect.TypeFor[T]() + val, err := ctx.Container().resolve(targetType) + if err != nil { + return zero, err + } + + typedVal, ok := val.(T) + if !ok { + return zero, fmt.Errorf("%w: cannot cast %T to %v", ErrServiceNotFound, val, targetType) + } + return typedVal, nil +} + +func (c *Container) resolve(targetType reflect.Type) (any, error) { + c.mu.RLock() + // 1. Direct type match + if val, ok := c.services[targetType]; ok { + c.mu.RUnlock() + return val, nil + } + + // 2. Interface assignment scan + if targetType.Kind() == reflect.Interface { + for _, val := range c.services { + if reflect.TypeOf(val).Implements(targetType) { + c.mu.RUnlock() + return val, nil + } + } + } + c.mu.RUnlock() + + // 3. Fallback to parent container + if c.parent != nil { + return c.parent.resolve(targetType) + } + + return nil, fmt.Errorf("%w: %v", ErrServiceNotFound, targetType) +} + +// MustInject resolves a service of type T or panics if the service is not found. +func MustInject[T any](ctx *Context) T { + s, err := Inject[T](ctx) + if err != nil { + panic(fmt.Sprintf("core: failed to inject service %v: %v", reflect.TypeFor[T](), err)) + } + return s +} + +// Has returns true if a service of type T is registered and resolvable in the Context. +func Has[T any](ctx *Context) bool { + _, err := Inject[T](ctx) + return err == nil +} + +// Using executes the given function synchronously if the required dependency is ready. +func Using[T1 any](ctx *Context, fn func(s1 T1)) error { + s1, err := Inject[T1](ctx) + if err != nil { + return fmt.Errorf("%w: %w", ErrServiceNotReady, err) + } + fn(s1) + return nil +} + +// Using2 executes the given function synchronously if both required dependencies are ready. +func Using2[T1, T2 any](ctx *Context, fn func(s1 T1, s2 T2)) error { + s1, err1 := Inject[T1](ctx) + s2, err2 := Inject[T2](ctx) + if err1 != nil || err2 != nil { + return fmt.Errorf("%w: (dep1: %v, dep2: %v)", ErrServiceNotReady, err1, err2) + } + fn(s1, s2) + return nil +} + +// Using3 executes the given function synchronously if all 3 required dependencies are ready. +func Using3[T1, T2, T3 any](ctx *Context, fn func(s1 T1, s2 T2, s3 T3)) error { + s1, err1 := Inject[T1](ctx) + s2, err2 := Inject[T2](ctx) + s3, err3 := Inject[T3](ctx) + if err1 != nil || err2 != nil || err3 != nil { + return fmt.Errorf("%w: (dep1: %v, dep2: %v, dep3: %v)", ErrServiceNotReady, err1, err2, err3) + } + fn(s1, s2, s3) + return nil +} + +// When registers a reactive hook that is called immediately if T is already provided, +// or called as soon as T is provided in the future. +func When[T any](ctx *Context, fn func(s T)) { + if ctx == nil { + panic("core: nil context provided to When") + } + + targetType := reflect.TypeFor[T]() + c := ctx.Container() + + // If already ready, execute immediately + if s, err := Inject[T](ctx); err == nil { + fn(s) + } + + // Also register listener for future calls / updates + c.mu.Lock() + defer c.mu.Unlock() + c.listeners[targetType] = append(c.listeners[targetType], func(val any) { + if typed, ok := val.(T); ok { + fn(typed) + } + }) +} diff --git a/core/context.go b/core/context.go new file mode 100644 index 00000000..980521d9 --- /dev/null +++ b/core/context.go @@ -0,0 +1,268 @@ +package core + +import ( + "context" + "errors" + "fmt" + "sync" + "time" +) + +// Context is the central micro-kernel service bus and runtime lifecycle container. +// It embeds Go standard context.Context compatibility, hierarchical scoping, +// service resolution, and LIFO disposer teardown. +type Context struct { + goCtx context.Context + cancel context.CancelFunc + parent *Context + container *Container + + mu sync.RWMutex + children []*Context + disposers []Disposer + drivers []Driver + values map[any]any + disposed bool +} + +// NewContext creates a new root Context wrapping a standard Go context. +// If base is nil, context.Background() is used by default. +func NewContext(base context.Context) *Context { + if base == nil { + base = context.Background() + } + ctx, cancel := context.WithCancel(base) + + return &Context{ + goCtx: ctx, + cancel: cancel, + container: NewContainer(nil), + values: make(map[any]any), + } +} + +// Deadline returns the time when work done on behalf of this context should be canceled. +func (c *Context) Deadline() (deadline time.Time, ok bool) { + return c.goCtx.Deadline() +} + +// Done returns a channel that's closed when work done on behalf of this context should be canceled. +func (c *Context) Done() <-chan struct{} { + return c.goCtx.Done() +} + +// Err returns a non-nil error value after Done is closed. +func (c *Context) Err() error { + return c.goCtx.Err() +} + +// Value returns the value associated with key, searching the local values map, +// the underlying Go context, and fallback parent Contexts. +func (c *Context) Value(key any) any { + c.mu.RLock() + if v, ok := c.values[key]; ok { + c.mu.RUnlock() + return v + } + c.mu.RUnlock() + + if v := c.goCtx.Value(key); v != nil { + return v + } + + if c.parent != nil { + return c.parent.Value(key) + } + + return nil +} + +// GoContext returns the underlying standard Go context.Context. +func (c *Context) GoContext() context.Context { + return c.goCtx +} + +// Set stores an arbitrary key-value pair in this Context's local storage. +func (c *Context) Set(key any, val any) { + c.mu.Lock() + defer c.mu.Unlock() + if c.values == nil { + c.values = make(map[any]any) + } + c.values[key] = val +} + +// Get retrieves a key-value pair from this Context's local storage. +func (c *Context) Get(key any) (any, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.values == nil { + return nil, false + } + v, ok := c.values[key] + return v, ok +} + +// Container returns the underlying IoC container for this Context. +func (c *Context) Container() *Container { + return c.container +} + +// Parent returns the parent Context, or nil if this is a root Context. +func (c *Context) Parent() *Context { + return c.parent +} + +// Fork creates a child Context with its own scoped IoC container and values, +// linked to this Context for hierarchical fallback resolution and cascading teardown. +func (c *Context) Fork() *Context { + return c.ForkWithContext(c.goCtx) +} + +// ForkWithContext creates a child Context using a specific standard Go context. +func (c *Context) ForkWithContext(base context.Context) *Context { + if base == nil { + base = c.goCtx + } + ctx, cancel := context.WithCancel(base) + + child := &Context{ + goCtx: ctx, + cancel: cancel, + parent: c, + container: NewContainer(c.container), + values: make(map[any]any), + } + + c.mu.Lock() + c.children = append(c.children, child) + c.mu.Unlock() + + return child +} + +// OnDispose registers a cleanup callback function to be executed when this Context is disposed. +// It accepts func() error, func(), or Disposer. +func (c *Context) OnDispose(fn any) { + if fn == nil { + return + } + + var d Disposer + switch f := fn.(type) { + case Disposer: + d = f + case func() error: + d = f + case func(): + d = func() error { + f() + return nil + } + default: + panic(fmt.Sprintf("core: OnDispose expects func() error or func(), got %T", fn)) + } + + c.mu.Lock() + defer c.mu.Unlock() + c.disposers = append(c.disposers, d) +} + +// Dispose shuts down this Context and all child Contexts, running registered disposers in LIFO order. +func (c *Context) Dispose() error { + c.mu.Lock() + if c.disposed { + c.mu.Unlock() + return nil + } + c.disposed = true + + // Copy children and disposers under lock + children := make([]*Context, len(c.children)) + copy(children, c.children) + + disposers := make([]Disposer, len(c.disposers)) + copy(disposers, c.disposers) + c.mu.Unlock() + + var errs []error + + // 1. Dispose all child contexts in reverse order + for i := len(children) - 1; i >= 0; i-- { + if err := children[i].Dispose(); err != nil { + errs = append(errs, err) + } + } + + // 2. Run local disposers in LIFO order + for i := len(disposers) - 1; i >= 0; i-- { + if err := disposers[i](); err != nil { + errs = append(errs, err) + } + } + + // 3. Cancel the Go context + if c.cancel != nil { + c.cancel() + } + + // 4. Detach from parent + if c.parent != nil { + c.parent.removeChild(c) + } + + return errors.Join(errs...) +} + +func (c *Context) removeChild(target *Context) { + c.mu.Lock() + defer c.mu.Unlock() + for i, child := range c.children { + if child == target { + c.children = append(c.children[:i], c.children[i+1:]...) + break + } + } +} + +// IsDisposed returns true if this Context has been disposed. +func (c *Context) IsDisposed() bool { + c.mu.RLock() + defer c.mu.RUnlock() + return c.disposed +} + +// RegisterDriver registers a runtime driver engine on this Context. +func (c *Context) RegisterDriver(d Driver) error { + if d == nil { + return ErrNilService + } + + c.mu.Lock() + defer c.mu.Unlock() + c.drivers = append(c.drivers, d) + return nil +} + +// Drivers returns a copy of all drivers registered on this Context. +func (c *Context) Drivers() []Driver { + c.mu.RLock() + defer c.mu.RUnlock() + + result := make([]Driver, len(c.drivers)) + copy(result, c.drivers) + return result +} + +// Driver looks up a registered driver by its driver type. +func (c *Context) Driver(driverType DriverType) (Driver, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + + for _, d := range c.drivers { + if d.Type() == driverType { + return d, true + } + } + return nil, false +} diff --git a/core/context_test.go b/core/context_test.go new file mode 100644 index 00000000..dc9d1282 --- /dev/null +++ b/core/context_test.go @@ -0,0 +1,475 @@ +package core_test + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Rain-kl/Wavelet/core" +) + +// Sample services for testing +type SampleService interface { + Greet(name string) string +} + +type sampleServiceImpl struct { + prefix string +} + +func (s *sampleServiceImpl) Greet(name string) string { + if s.prefix != "" { + return s.prefix + " " + name + } + return "Hello, " + name +} + +type LogService interface { + Log(msg string) +} + +type logServiceImpl struct { + logs []string +} + +func (l *logServiceImpl) Log(msg string) { + l.logs = append(l.logs, msg) +} + +type ConfigService interface { + Get(key string) string +} + +type configServiceImpl struct { + data map[string]string +} + +func (c *configServiceImpl) Get(key string) string { + return c.data[key] +} + +// Sample plugin for testing +type samplePlugin struct { + name string +} + +func (p *samplePlugin) Name() string { + return p.name +} + +func (p *samplePlugin) Apply(ctx *core.Context) error { + core.Provide[SampleService](ctx, &sampleServiceImpl{prefix: "Plugin:"}) + return nil +} + +func (p *samplePlugin) Manifest() core.Manifest { + return core.Manifest{ + Name: p.name, + Version: "1.0.0", + Description: "Sample plugin", + } +} + +// Sample driver for testing +type mockDriver struct { + driverType core.DriverType + started bool + stopped bool +} + +func (m *mockDriver) Type() core.DriverType { + return m.driverType +} + +func (m *mockDriver) Start(ctx context.Context) error { + m.started = true + return nil +} + +func (m *mockDriver) Stop(ctx context.Context) error { + m.stopped = true + return nil +} + +func TestContextProvideAndInject(t *testing.T) { + ctx := core.NewContext(context.Background()) + + // Before providing, Inject should fail + _, err := core.Inject[SampleService](ctx) + require.Error(t, err) + assert.True(t, errors.Is(err, core.ErrServiceNotFound)) + assert.False(t, core.Has[SampleService](ctx)) + + // MustInject should panic + assert.Panics(t, func() { + core.MustInject[SampleService](ctx) + }) + + // Provide service + svcImpl := &sampleServiceImpl{prefix: "Hello,"} + core.Provide[SampleService](ctx, svcImpl) + + // Inject should succeed + assert.True(t, core.Has[SampleService](ctx)) + svc, err := core.Inject[SampleService](ctx) + require.NoError(t, err) + assert.Equal(t, "Hello, Wavelet", svc.Greet("Wavelet")) + + // MustInject should succeed + mustSvc := core.MustInject[SampleService](ctx) + assert.Equal(t, "Hello, Cordis", mustSvc.Greet("Cordis")) +} + +func TestContextProvideNilPanics(t *testing.T) { + ctx := core.NewContext(context.Background()) + + assert.Panics(t, func() { + core.Provide[SampleService](nil, &sampleServiceImpl{}) + }) + + assert.Panics(t, func() { + var nilSvc SampleService + core.Provide[SampleService](ctx, nilSvc) + }) + + assert.Panics(t, func() { + var nilImpl *sampleServiceImpl + core.Provide[*sampleServiceImpl](ctx, nilImpl) + }) + + // Inject with nil context + var nilCtx *core.Context + _, err := core.Inject[SampleService](nilCtx) + assert.ErrorIs(t, err, core.ErrNilContext) +} + +func TestContextUsing(t *testing.T) { + ctx := core.NewContext(context.Background()) + var called bool + + // Using when service not ready should return ErrServiceNotReady + err := core.Using(ctx, func(s SampleService) { + called = true + assert.Equal(t, "Hello, Cordis", s.Greet("Cordis")) + }) + assert.Error(t, err) + assert.True(t, errors.Is(err, core.ErrServiceNotReady)) + assert.False(t, called) + + // Provide service and try Using again + core.Provide[SampleService](ctx, &sampleServiceImpl{}) + err = core.Using(ctx, func(s SampleService) { + called = true + assert.Equal(t, "Hello, Cordis", s.Greet("Cordis")) + }) + assert.NoError(t, err) + assert.True(t, called) +} + +func TestContextUsingMultiple(t *testing.T) { + ctx := core.NewContext(context.Background()) + + // Using2 with missing dependencies + var called2 bool + err := core.Using2(ctx, func(s SampleService, l LogService) { + called2 = true + }) + assert.Error(t, err) + assert.False(t, called2) + + // Provide 1 of 2 + core.Provide[SampleService](ctx, &sampleServiceImpl{}) + err = core.Using2(ctx, func(s SampleService, l LogService) { + called2 = true + }) + assert.Error(t, err) + assert.False(t, called2) + + // Provide 2 of 2 + logSvc := &logServiceImpl{} + core.Provide[LogService](ctx, logSvc) + err = core.Using2(ctx, func(s SampleService, l LogService) { + called2 = true + l.Log(s.Greet("World")) + }) + assert.NoError(t, err) + assert.True(t, called2) + assert.Equal(t, []string{"Hello, World"}, logSvc.logs) + + // Using3 test - error condition + err = core.Using3(ctx, func(s SampleService, l LogService, c ConfigService) {}) + assert.Error(t, err) + + // Using3 test - success condition + var called3 bool + cfgSvc := &configServiceImpl{data: map[string]string{"env": "test"}} + core.Provide[ConfigService](ctx, cfgSvc) + + err = core.Using3(ctx, func(s SampleService, l LogService, c ConfigService) { + called3 = true + assert.Equal(t, "test", c.Get("env")) + }) + assert.NoError(t, err) + assert.True(t, called3) +} + +func TestContextHierarchyAndFork(t *testing.T) { + parent := core.NewContext(nil) // nil base context test + core.Provide[SampleService](parent, &sampleServiceImpl{prefix: "Parent:"}) + + child := parent.ForkWithContext(nil) // nil child context test + require.NotNil(t, child) + assert.Equal(t, parent, child.Parent()) + + // Child can resolve service from parent + svc, err := core.Inject[SampleService](child) + require.NoError(t, err) + assert.Equal(t, "Parent: Ryan", svc.Greet("Ryan")) + + // Child provides LogService + childLog := &logServiceImpl{} + core.Provide[LogService](child, childLog) + + // Child has LogService, parent does not + assert.True(t, core.Has[LogService](child)) + assert.False(t, core.Has[LogService](parent)) + + // Child overrides SampleService + core.Provide[SampleService](child, &sampleServiceImpl{prefix: "Child:"}) + childSvc, err := core.Inject[SampleService](child) + require.NoError(t, err) + assert.Equal(t, "Child: Ryan", childSvc.Greet("Ryan")) + + parentSvc, err := core.Inject[SampleService](parent) + require.NoError(t, err) + assert.Equal(t, "Parent: Ryan", parentSvc.Greet("Ryan")) +} + +func TestContextReactiveWhen(t *testing.T) { + ctx := core.NewContext(context.Background()) + + assert.Panics(t, func() { + core.When[SampleService](nil, func(s SampleService) {}) + }) + + var whenCalled atomic.Bool + var greeted string + + // Register When before service is provided + core.When[SampleService](ctx, func(s SampleService) { + whenCalled.Store(true) + greeted = s.Greet("Reactive") + }) + + assert.False(t, whenCalled.Load()) + + // Now Provide the service - listener should trigger + core.Provide[SampleService](ctx, &sampleServiceImpl{}) + + assert.True(t, whenCalled.Load()) + assert.Equal(t, "Hello, Reactive", greeted) + + // Register another When after service is already provided - should trigger immediately + var immediateCalled bool + core.When[SampleService](ctx, func(s SampleService) { + immediateCalled = true + }) + assert.True(t, immediateCalled) +} + +func TestContextDisposerLifecycle(t *testing.T) { + parent := core.NewContext(context.Background()) + child := parent.Fork() + + var order []string + + // Test nil disposer + parent.OnDispose(nil) + + // Test Disposer type + var customDisposer core.Disposer = func() error { + order = append(order, "parent-custom") + return nil + } + parent.OnDispose(customDisposer) + + parent.OnDispose(func() error { + order = append(order, "parent-1") + return nil + }) + parent.OnDispose(func() { + order = append(order, "parent-2") + }) + + child.OnDispose(func() error { + order = append(order, "child-1") + return errors.New("child-1 error") + }) + child.OnDispose(func() { + order = append(order, "child-2") + }) + + assert.Panics(t, func() { + parent.OnDispose("invalid-func") + }) + + assert.False(t, parent.IsDisposed()) + assert.False(t, child.IsDisposed()) + + // Disposing parent should cascade to children first, and execute disposers in LIFO order + err := parent.Dispose() + assert.Error(t, err) // child-1 error should be joined + assert.Contains(t, err.Error(), "child-1 error") + + assert.True(t, parent.IsDisposed()) + assert.True(t, child.IsDisposed()) + + // Child disposers run in LIFO: child-2, child-1 + // Parent disposers run in LIFO: parent-2, parent-1, parent-custom + expected := []string{"child-2", "child-1", "parent-2", "parent-1", "parent-custom"} + assert.Equal(t, expected, order) + + // Disposing again should be idempotent and return nil + err = parent.Dispose() + assert.NoError(t, err) +} + +func TestContextStandardGoContext(t *testing.T) { + baseCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer cancel() + + parentCtx := core.NewContext(baseCtx) + parentCtx.Set("parent_key", "parent_val") + + childCtx := parentCtx.Fork() + + // Deadline + dl, ok := childCtx.Deadline() + assert.True(t, ok) + assert.False(t, dl.IsZero()) + + // Value fallback: child has no key, falls back to parentCtx + assert.Equal(t, "parent_val", childCtx.Value("parent_key")) + + // GoContext getter + assert.NotNil(t, childCtx.GoContext()) + + // Value not found in either + assert.Nil(t, childCtx.Value("non_existent_key")) + + // Cancellation propagation + select { + case <-childCtx.Done(): + t.Fatal("ctx should not be done yet") + default: + } + + cancel() + + select { + case <-childCtx.Done(): + assert.Equal(t, context.Canceled, childCtx.Err()) + case <-time.After(100 * time.Millisecond): + t.Fatal("ctx should be cancelled") + } +} + +func TestManifestValidation(t *testing.T) { + mValid := core.Manifest{ + Name: "auth", + Version: "1.0.0", + Description: "Auth plugin", + } + assert.NoError(t, mValid.Validate()) + + mInvalid := core.Manifest{ + Version: "1.0.0", + } + assert.Error(t, mInvalid.Validate()) +} + +func TestDriverRegistration(t *testing.T) { + ctx := core.NewContext(context.Background()) + + // Register nil driver returns error + assert.ErrorIs(t, ctx.RegisterDriver(nil), core.ErrNilService) + + dHTTP := &mockDriver{driverType: core.DriverTypeHTTP} + dWorker := &mockDriver{driverType: core.DriverTypeWorker} + + require.NoError(t, ctx.RegisterDriver(dHTTP)) + require.NoError(t, ctx.RegisterDriver(dWorker)) + + drivers := ctx.Drivers() + assert.Len(t, drivers, 2) + + foundHTTP, ok := ctx.Driver(core.DriverTypeHTTP) + assert.True(t, ok) + assert.Equal(t, dHTTP, foundHTTP) + + foundWorker, ok := ctx.Driver(core.DriverTypeWorker) + assert.True(t, ok) + assert.Equal(t, dWorker, foundWorker) + + _, ok = ctx.Driver(core.DriverTypeScheduler) + assert.False(t, ok) +} + +func TestPluginInterfaces(t *testing.T) { + ctx := core.NewContext(context.Background()) + var p core.Plugin = &samplePlugin{name: "sample"} + assert.Equal(t, "sample", p.Name()) + require.NoError(t, p.Apply(ctx)) + + svc, err := core.Inject[SampleService](ctx) + require.NoError(t, err) + assert.Equal(t, "Plugin: Ryan", svc.Greet("Ryan")) + + var pwm core.PluginWithManifest = &samplePlugin{name: "sample"} + manifest := pwm.Manifest() + assert.Equal(t, "sample", manifest.Name) + assert.Equal(t, "1.0.0", manifest.Version) +} + +func TestConcurrentAccess(t *testing.T) { + ctx := core.NewContext(context.Background()) + var wg sync.WaitGroup + + // Concurrently provide, inject, fork, set, and get + for i := 0; i < 50; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + ctx.Set(fmt.Sprintf("key-%d", idx), idx) + _, _ = ctx.Get(fmt.Sprintf("key-%d", idx)) + + child := ctx.Fork() + child.Set("child_key", idx) + }(i) + } + + core.Provide[SampleService](ctx, &sampleServiceImpl{}) + + for i := 0; i < 50; i++ { + wg.Add(1) + go func() { + defer wg.Done() + svc, err := core.Inject[SampleService](ctx) + if err == nil { + _ = svc.Greet("Concurrency") + } + _ = core.Using(ctx, func(s SampleService) { + _ = s.Greet("Safe") + }) + }() + } + + wg.Wait() +} diff --git a/core/manifest.go b/core/manifest.go new file mode 100644 index 00000000..f696e8d8 --- /dev/null +++ b/core/manifest.go @@ -0,0 +1,35 @@ +package core + +import ( + "fmt" + "strings" +) + +// Manifest defines the metadata and dependency declarations for a plugin. +type Manifest struct { + // Name is the unique identifier for the plugin (e.g. "auth", "user", "order"). + Name string `json:"name" yaml:"name"` + + // Version is the semantic version string of the plugin (e.g. "1.0.0"). + Version string `json:"version,omitempty" yaml:"version,omitempty"` + + // Description gives a brief summary of the plugin capabilities. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + + // Author specifies the author or maintainer of the plugin. + Author string `json:"author,omitempty" yaml:"author,omitempty"` + + // Dependencies lists the plugin names that this plugin depends on. + Dependencies []string `json:"dependencies,omitempty" yaml:"dependencies,omitempty"` + + // Metadata holds arbitrary plugin-specific metadata. + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// Validate checks whether the manifest satisfies basic integrity requirements. +func (m Manifest) Validate() error { + if strings.TrimSpace(m.Name) == "" { + return fmt.Errorf("%w: %w", ErrInvalidManifest, ErrInvalidManifestName) + } + return nil +} diff --git a/core/types.go b/core/types.go new file mode 100644 index 00000000..2014a39b --- /dev/null +++ b/core/types.go @@ -0,0 +1,71 @@ +package core + +import ( + "context" + "errors" +) + +// Standard sentinel errors returned by core operations. +var ( + // ErrServiceNotFound is returned when a requested service is not registered in the IoC container. + ErrServiceNotFound = errors.New("core: service not found") + + // ErrServiceNotReady is returned when one or more required services are not ready in Using/UsingN. + ErrServiceNotReady = errors.New("core: service not ready") + + // ErrNilContext is returned when a nil Context is passed to an operation requiring a valid Context. + ErrNilContext = errors.New("core: context is nil") + + // ErrNilService is returned when attempting to provide a nil service implementation. + ErrNilService = errors.New("core: service is nil") + + // ErrInvalidManifest is returned when a plugin manifest fails validation. + ErrInvalidManifest = errors.New("core: invalid manifest") + + // ErrInvalidManifestName is returned when a plugin manifest has an empty name. + ErrInvalidManifestName = errors.New("core: manifest name is required") + + // ErrDriverNotFound is returned when a requested driver type is not registered. + ErrDriverNotFound = errors.New("core: driver not found") +) + +// Plugin is the unified contract for all core and downstream plugins. +type Plugin interface { + // Name returns the globally unique identifier of the plugin (e.g. "auth", "database"). + Name() string + // Apply is the core mounting entrypoint: provides services, registers routes, tasks, and event listeners. + Apply(ctx *Context) error +} + +// PluginWithManifest is an optional extension interface for plugins that declare metadata. +type PluginWithManifest interface { + Plugin + Manifest() Manifest +} + +// DriverType identifies the category of a runtime driver engine. +type DriverType string + +const ( + // DriverTypeHTTP represents HTTP web server drivers (e.g. Gin). + DriverTypeHTTP DriverType = "http" + + // DriverTypeWorker represents asynchronous background worker drivers (e.g. Asynq worker server). + DriverTypeWorker DriverType = "worker" + + // DriverTypeScheduler represents cron and timer schedule drivers (e.g. Asynq scheduler). + DriverTypeScheduler DriverType = "schedule" +) + +// Driver is a runtime engine that manages an event loop or listening port. +type Driver interface { + // Type returns the category of this driver engine. + Type() DriverType + // Start starts the driver lifecycle loop. + Start(ctx context.Context) error + // Stop gracefully shuts down the driver. + Stop(ctx context.Context) error +} + +// Disposer is a cleanup function executed when a Context is disposed. +type Disposer func() error