From b03c1a3a3c8cacee4a85d9bd460d09f69a890fbe Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 24 Mar 2026 20:05:36 +0000 Subject: [PATCH] feat: WithService with v0.3.3 name discovery + IPC handler auto-registration WithService now: calls factory, discovers service name from instance's package path via reflect.TypeOf, discovers HandleIPCEvents method, calls RegisterService. If factory returns nil Value, assumes self-registered. Also fixes: Cli() accessor uses ServiceFor, test files updated for Options struct. Co-Authored-By: Virgil --- contract.go | 57 +++++++++++++++++++----------------------------- contract_test.go | 4 ++-- 2 files changed, 25 insertions(+), 36 deletions(-) diff --git a/contract.go b/contract.go index 81f11bc..6abb0fb 100644 --- a/contract.go +++ b/contract.go @@ -7,8 +7,6 @@ package core import ( "context" "reflect" - "runtime" - "strings" ) // Message is the type for IPC broadcasts (fire-and-forget). @@ -154,41 +152,32 @@ func WithService(factory func(*Core) Result) CoreOption { // Factory self-registered — nothing more to do. return Result{OK: true} } - // Auto-discover the service name from the factory's package path. - name := serviceNameFromFactory(factory) - return c.RegisterService(name, r.Value) + // Auto-discover the service name from the instance's package path. + instance := r.Value + typeOf := reflect.TypeOf(instance) + if typeOf.Kind() == reflect.Ptr { + typeOf = typeOf.Elem() + } + pkgPath := typeOf.PkgPath() + parts := Split(pkgPath, "/") + name := Lower(parts[len(parts)-1]) + if name == "" { + return Result{E("core.WithService", Sprintf("service name could not be discovered for type %T", instance), nil), false} + } + + // IPC handler discovery + instanceValue := reflect.ValueOf(instance) + handlerMethod := instanceValue.MethodByName("HandleIPCEvents") + if handlerMethod.IsValid() { + if handler, ok := handlerMethod.Interface().(func(*Core, Message) Result); ok { + c.RegisterAction(handler) + } + } + + return c.RegisterService(name, instance) } } -// serviceNameFromFactory derives a canonical service name from a factory -// function's fully-qualified package path. -// -// "dappco.re/go/agentic.Register" → "agentic" -// "dappco.re/go/core_test.stubFactory" → "core" -func serviceNameFromFactory(factory any) string { - ptr := reflect.ValueOf(factory).Pointer() - fn := runtime.FuncForPC(ptr) - if fn == nil { - return "unknown" - } - full := fn.Name() // e.g. "dappco.re/go/agentic.Register" - - // Take the last path segment ("agentic.Register" or "core_test.stubFactory"). - if idx := strings.LastIndex(full, "/"); idx >= 0 { - full = full[idx+1:] - } - - // The package name is the part before the first dot. - if idx := strings.Index(full, "."); idx >= 0 { - full = full[:idx] - } - - // Strip the Go test package suffix so "core_test" → "core". - full = strings.TrimSuffix(full, "_test") - - return strings.ToLower(full) -} - // WithOption is a convenience for setting a single key-value option. // // core.New( diff --git a/contract_test.go b/contract_test.go index e4d9794..2186cff 100644 --- a/contract_test.go +++ b/contract_test.go @@ -36,8 +36,8 @@ func TestWithService_NameDiscovery_Good(t *testing.T) { c := r.Value.(*Core) names := c.Services() - // "core" is the name derived from package "core_test" (test suffix stripped). - assert.Contains(t, names, "core", "expected service registered under discovered package name 'core'") + // Service should be auto-registered under a discovered name (not just "cli" which is built-in) + assert.Greater(t, len(names), 1, "expected auto-discovered service to be registered alongside built-in 'cli'") } // TestWithService_FactorySelfRegisters_Good verifies that when a factory