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 <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-24 20:05:36 +00:00
parent f1ed1f0ac5
commit 0342089b0e
2 changed files with 25 additions and 36 deletions

View file

@ -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(

View file

@ -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