- WithService now calls factory, discovers service name from package path via
reflect/runtime (last path segment, _test suffix stripped, lowercased), and
calls RegisterService — which handles Startable/Stoppable/HandleIPCEvents
- If factory returns nil Value (self-registered), WithService returns OK without
a second registration
- Add contract_test.go with _Good/_Bad tests covering all three code paths
- Fix core.go Cli() accessor: use ServiceFor[*Cli](c, "cli") (was cli.New())
- Fix pre-existing })) → }}) syntax errors in command_test, service_test, lock_test
- Fix pre-existing Options{...} → NewOptions(...) in core_test, data_test,
drive_test, i18n_test (Options is a struct, not a slice)
Co-Authored-By: Virgil <virgil@lethean.io>
226 lines
5.8 KiB
Go
226 lines
5.8 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
// Contracts, options, and type definitions for the Core framework.
|
|
|
|
package core
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// Message is the type for IPC broadcasts (fire-and-forget).
|
|
type Message any
|
|
|
|
// Query is the type for read-only IPC requests.
|
|
type Query any
|
|
|
|
// Task is the type for IPC requests that perform side effects.
|
|
type Task any
|
|
|
|
// TaskWithIdentifier is an optional interface for tasks that need to know their assigned identifier.
|
|
type TaskWithIdentifier interface {
|
|
Task
|
|
SetTaskIdentifier(id string)
|
|
GetTaskIdentifier() string
|
|
}
|
|
|
|
// QueryHandler handles Query requests. Returns Result{Value, OK}.
|
|
type QueryHandler func(*Core, Query) Result
|
|
|
|
// TaskHandler handles Task requests. Returns Result{Value, OK}.
|
|
type TaskHandler func(*Core, Task) Result
|
|
|
|
// Startable is implemented by services that need startup initialisation.
|
|
type Startable interface {
|
|
OnStartup(ctx context.Context) error
|
|
}
|
|
|
|
// Stoppable is implemented by services that need shutdown cleanup.
|
|
type Stoppable interface {
|
|
OnShutdown(ctx context.Context) error
|
|
}
|
|
|
|
// --- Action Messages ---
|
|
|
|
type ActionServiceStartup struct{}
|
|
type ActionServiceShutdown struct{}
|
|
|
|
type ActionTaskStarted struct {
|
|
TaskIdentifier string
|
|
Task Task
|
|
}
|
|
|
|
type ActionTaskProgress struct {
|
|
TaskIdentifier string
|
|
Task Task
|
|
Progress float64
|
|
Message string
|
|
}
|
|
|
|
type ActionTaskCompleted struct {
|
|
TaskIdentifier string
|
|
Task Task
|
|
Result any
|
|
Error error
|
|
}
|
|
|
|
// --- Constructor ---
|
|
|
|
// CoreOption is a functional option applied during Core construction.
|
|
// Returns Result — if !OK, New() stops and returns the error.
|
|
//
|
|
// core.New(
|
|
// core.WithService(agentic.Register),
|
|
// core.WithService(monitor.Register),
|
|
// core.WithServiceLock(),
|
|
// )
|
|
type CoreOption func(*Core) Result
|
|
|
|
// New initialises a Core instance by applying options in order.
|
|
// Services registered here form the application conclave — they share
|
|
// IPC access and participate in the lifecycle (ServiceStartup/ServiceShutdown).
|
|
//
|
|
// r := core.New(
|
|
// core.WithOptions(core.Options{{Key: "name", Value: "myapp"}}),
|
|
// core.WithService(auth.Register),
|
|
// core.WithServiceLock(),
|
|
// )
|
|
// if !r.OK { log.Fatal(r.Value) }
|
|
// c := r.Value.(*Core)
|
|
func New(opts ...CoreOption) Result {
|
|
c := &Core{
|
|
app: &App{},
|
|
data: &Data{},
|
|
drive: &Drive{},
|
|
fs: &Fs{root: "/"},
|
|
config: &Config{ConfigOptions: &ConfigOptions{}},
|
|
error: &ErrorPanic{},
|
|
log: &ErrorLog{log: Default()},
|
|
lock: &Lock{},
|
|
ipc: &Ipc{},
|
|
info: systemInfo,
|
|
i18n: &I18n{},
|
|
services: &serviceRegistry{services: make(map[string]*Service)},
|
|
commands: &commandRegistry{commands: make(map[string]*Command)},
|
|
}
|
|
c.context, c.cancel = context.WithCancel(context.Background())
|
|
|
|
// Core services
|
|
CliRegister(c)
|
|
|
|
for _, opt := range opts {
|
|
if r := opt(c); !r.OK {
|
|
return r
|
|
}
|
|
}
|
|
|
|
return Result{c, true}
|
|
}
|
|
|
|
// WithOptions applies key-value configuration to Core.
|
|
//
|
|
// core.WithOptions(core.Options{{Key: "name", Value: "myapp"}})
|
|
func WithOptions(opts Options) CoreOption {
|
|
return func(c *Core) Result {
|
|
c.options = &opts
|
|
if name := opts.String("name"); name != "" {
|
|
c.app.Name = name
|
|
}
|
|
return Result{OK: true}
|
|
}
|
|
}
|
|
|
|
// WithService registers a service via its factory function.
|
|
// If the factory returns a non-nil Value, WithService auto-discovers the
|
|
// service name from the factory's package path (last path segment, lowercase,
|
|
// with any "_test" suffix stripped) and calls RegisterService on the instance.
|
|
// IPC handler auto-registration is handled by RegisterService.
|
|
//
|
|
// If the factory returns nil Value (it registered itself), WithService
|
|
// returns success without a second registration.
|
|
//
|
|
// core.WithService(agentic.Register)
|
|
// core.WithService(display.Register(nil))
|
|
func WithService(factory func(*Core) Result) CoreOption {
|
|
return func(c *Core) Result {
|
|
r := factory(c)
|
|
if !r.OK {
|
|
return r
|
|
}
|
|
if r.Value == nil {
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// 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(
|
|
// core.WithOption("name", "myapp"),
|
|
// core.WithOption("port", 8080),
|
|
// )
|
|
func WithOption(key string, value any) CoreOption {
|
|
return func(c *Core) Result {
|
|
if c.options == nil {
|
|
opts := NewOptions()
|
|
c.options = &opts
|
|
}
|
|
c.options.Set(key, value)
|
|
if key == "name" {
|
|
if s, ok := value.(string); ok {
|
|
c.app.Name = s
|
|
}
|
|
}
|
|
return Result{OK: true}
|
|
}
|
|
}
|
|
|
|
// WithServiceLock prevents further service registration after construction.
|
|
//
|
|
// core.New(
|
|
// core.WithService(auth.Register),
|
|
// core.WithServiceLock(),
|
|
// )
|
|
func WithServiceLock() CoreOption {
|
|
return func(c *Core) Result {
|
|
c.LockEnable()
|
|
c.LockApply()
|
|
return Result{OK: true}
|
|
}
|
|
}
|