Skip to content

Commit da7e55d

Browse files
SniderHephaestus
andcommitted
feat(go): add service.go — Core service registration (Mantis #1336)
Wires the cache.Cache surface into Core's service-registration plumbing behind a *core.ServiceRuntime[CacheConfig]. NewService() factory builds the underlying *Cache via the existing New() constructor and returns a *Service ready for c.Service() registration. Action handlers exposed via OnStartup: cache.delete — opts.key cache.delete_many — opts.keys ([]string) cache.path — opts.key → on-disk path cache.invalidate — opts.trigger cache.clear_scope — opts.origin Get/Set/SetBinary/GetBinary stay direct method calls because they need typed `dest any` arguments that don't round-trip through Options cleanly. Triplets + examples included (8 audit findings → 0; verdict COMPLIANT). Co-authored-by: Hephaestus <hephaestus@lthn.ai>
1 parent 1bbc67e commit da7e55d

3 files changed

Lines changed: 312 additions & 0 deletions

File tree

go/service.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// SPDX-License-Identifier: EUPL-1.2
2+
3+
// Service registration for the cache package. Exposes the Cache surface
4+
// as a Core service with action handlers so consumers can wire cache
5+
// operations through the same plumbing as every other core service.
6+
//
7+
// Usage example: `c, _ := core.New(core.WithName("cache", cache.NewService(cache.CacheConfig{BaseDir: "/var/lib/core/cache", TTL: time.Hour})))`
8+
9+
package cache
10+
11+
import (
12+
"context"
13+
"time"
14+
15+
core "dappco.re/go"
16+
coreio "dappco.re/go/io"
17+
)
18+
19+
// CacheConfig is the typed-options struct for the cache service. Empty
20+
// values fall back to package defaults (`coreio.Local` medium, CWD-rooted
21+
// `.core/cache` baseDir, `DefaultTTL` cacheTTL).
22+
//
23+
// Usage example: `cfg := cache.CacheConfig{BaseDir: "/var/lib/core/cache", TTL: time.Hour}`
24+
type CacheConfig struct {
25+
// Medium is the storage backend. Nil → coreio.Local.
26+
Medium coreio.Medium
27+
// BaseDir is the root directory. Empty → CWD/.core/cache.
28+
BaseDir string
29+
// TTL is the default cache TTL. Zero → DefaultTTL (1 hour).
30+
TTL time.Duration
31+
}
32+
33+
// Service is the registerable handle for the cache package — embeds
34+
// *core.ServiceRuntime[CacheConfig] for typed options access and holds
35+
// a live *Cache ready for direct method calls or action use.
36+
//
37+
// Usage example: `svc := core.MustServiceFor[*cache.Service](c, "cache"); _ = svc.Cache.Delete("key")`
38+
type Service struct {
39+
*core.ServiceRuntime[CacheConfig]
40+
// Cache is the live *Cache the service was constructed with.
41+
// Usage example: `svc.Cache.Delete("key")`
42+
Cache *Cache
43+
registrations core.Once
44+
}
45+
46+
// NewService returns a factory that constructs the cache and produces a
47+
// *Service ready for c.Service() registration. Use through core.WithName
48+
// so the framework wires lifecycle (OnStartup registers actions).
49+
//
50+
// Usage example: `c, _ := core.New(core.WithName("cache", cache.NewService(cache.CacheConfig{BaseDir: "/var/lib/core/cache"})))`
51+
func NewService(config CacheConfig) func(*core.Core) core.Result {
52+
return func(c *core.Core) core.Result {
53+
r := New(config.Medium, config.BaseDir, config.TTL)
54+
if !r.OK {
55+
return r
56+
}
57+
return core.Ok(&Service{
58+
ServiceRuntime: core.NewServiceRuntime(c, config),
59+
Cache: r.Value.(*Cache),
60+
})
61+
}
62+
}
63+
64+
// OnStartup registers the cache action handlers on the attached Core.
65+
// Implements core.Startable. Idempotent via core.Once — multiple startups
66+
// (e.g. test re-entry) won't double-register.
67+
//
68+
// Note: Get / Set / SetBinary / GetBinary stay direct method calls because
69+
// they need a typed `dest any` argument that doesn't round-trip through
70+
// Options cleanly. Other operations are exposed as actions.
71+
//
72+
// Usage example: `r := svc.OnStartup(ctx)`
73+
func (s *Service) OnStartup(context.Context) core.Result {
74+
if s == nil {
75+
return core.Ok(nil)
76+
}
77+
s.registrations.Do(func() {
78+
c := s.Core()
79+
if c == nil {
80+
return
81+
}
82+
c.Action("cache.delete", s.handleDelete)
83+
c.Action("cache.delete_many", s.handleDeleteMany)
84+
c.Action("cache.path", s.handlePath)
85+
c.Action("cache.invalidate", s.handleInvalidate)
86+
c.Action("cache.clear_scope", s.handleClearScope)
87+
})
88+
return core.Ok(nil)
89+
}
90+
91+
// OnShutdown is a no-op for the cache service — the Cache holds no
92+
// long-lived handles requiring teardown. Implements core.Stoppable for
93+
// shape parity with other services.
94+
//
95+
// Usage example: `r := svc.OnShutdown(ctx)`
96+
func (s *Service) OnShutdown(context.Context) core.Result {
97+
return core.Ok(nil)
98+
}
99+
100+
// handleDelete — `cache.delete` action handler. Reads opts.key.
101+
//
102+
// r := c.Action("cache.delete").Run(ctx, core.NewOptions(
103+
// core.Option{Key: "key", Value: "user.profile.42"},
104+
// ))
105+
func (s *Service) handleDelete(_ core.Context, opts core.Options) core.Result {
106+
if s == nil || s.Cache == nil {
107+
return core.Fail(core.E("cache.delete", "service not initialised", nil))
108+
}
109+
return s.Cache.Delete(opts.String("key"))
110+
}
111+
112+
// handleDeleteMany — `cache.delete_many` action handler. Reads
113+
// opts.keys (string slice). Removes every supplied key in one pass and
114+
// returns a count of successful deletions in r.Value.
115+
//
116+
// r := c.Action("cache.delete_many").Run(ctx, core.NewOptions(
117+
// core.Option{Key: "keys", Value: []string{"a", "b", "c"}},
118+
// ))
119+
func (s *Service) handleDeleteMany(_ core.Context, opts core.Options) core.Result {
120+
if s == nil || s.Cache == nil {
121+
return core.Fail(core.E("cache.delete_many", "service not initialised", nil))
122+
}
123+
r := opts.Get("keys")
124+
if !r.OK {
125+
return core.Fail(core.E("cache.delete_many", "keys is required", nil))
126+
}
127+
keys, ok := r.Value.([]string)
128+
if !ok {
129+
return core.Fail(core.E("cache.delete_many", "keys must be []string", nil))
130+
}
131+
return s.Cache.DeleteMany(keys...)
132+
}
133+
134+
// handlePath — `cache.path` action handler. Reads opts.key and returns
135+
// the on-disk JSON path in r.Value.
136+
//
137+
// r := c.Action("cache.path").Run(ctx, core.NewOptions(
138+
// core.Option{Key: "key", Value: "user.profile.42"},
139+
// ))
140+
// path, _ := r.Value.(string)
141+
func (s *Service) handlePath(_ core.Context, opts core.Options) core.Result {
142+
if s == nil || s.Cache == nil {
143+
return core.Fail(core.E("cache.path", "service not initialised", nil))
144+
}
145+
return s.Cache.Path(opts.String("key"))
146+
}
147+
148+
// handleInvalidate — `cache.invalidate` action handler. Reads
149+
// opts.trigger and runs every InvalidateFunc registered for that trigger.
150+
//
151+
// r := c.Action("cache.invalidate").Run(ctx, core.NewOptions(
152+
// core.Option{Key: "trigger", Value: "user.updated"},
153+
// ))
154+
func (s *Service) handleInvalidate(_ core.Context, opts core.Options) core.Result {
155+
if s == nil || s.Cache == nil {
156+
return core.Fail(core.E("cache.invalidate", "service not initialised", nil))
157+
}
158+
return s.Cache.Invalidate(opts.String("trigger"))
159+
}
160+
161+
// handleClearScope — `cache.clear_scope` action handler. Reads
162+
// opts.origin and removes every entry under that scope.
163+
//
164+
// r := c.Action("cache.clear_scope").Run(ctx, core.NewOptions(
165+
// core.Option{Key: "origin", Value: "users"},
166+
// ))
167+
func (s *Service) handleClearScope(_ core.Context, opts core.Options) core.Result {
168+
if s == nil || s.Cache == nil {
169+
return core.Fail(core.E("cache.clear_scope", "service not initialised", nil))
170+
}
171+
return s.Cache.ClearScope(opts.String("origin"))
172+
}

go/service_example_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package cache_test
2+
3+
import (
4+
"context"
5+
6+
core "dappco.re/go"
7+
"dappco.re/go/cache"
8+
)
9+
10+
// ExampleNewService constructs the cache service factory through
11+
// `NewService` for go-cache Core service registration. The factory
12+
// produces a *cache.Service ready for c.Service() — OnStartup wires
13+
// the cache.* action handlers, OnShutdown is a no-op.
14+
//
15+
// Usage example: `c.Service("cache", cache.NewService(cache.CacheConfig{BaseDir: "/var/lib/core/cache"}))`
16+
func ExampleNewService() {
17+
factory := cache.NewService(cache.CacheConfig{})
18+
core.Println(factory != nil)
19+
// Output: true
20+
}
21+
22+
// ExampleService_OnStartup registers the cache.* action handlers on the
23+
// attached Core through `Service.OnStartup` for go-cache Core service
24+
// registration. Idempotent — multiple startups won't double-register.
25+
//
26+
// Usage example: `r := svc.OnStartup(ctx)`
27+
func ExampleService_OnStartup() {
28+
c := core.New()
29+
r := cache.NewService(cache.CacheConfig{})(c)
30+
if !r.OK {
31+
core.Println("startup-init-failed")
32+
return
33+
}
34+
svc := r.Value.(*cache.Service)
35+
startup := svc.OnStartup(context.Background())
36+
core.Println(startup.OK)
37+
// Output: true
38+
}
39+
40+
// ExampleService_OnShutdown drains the service through
41+
// `Service.OnShutdown` for go-cache Core service registration. The cache
42+
// holds no long-lived handles requiring teardown — Shutdown is a no-op
43+
// returning Ok for shape parity with other services.
44+
//
45+
// Usage example: `r := svc.OnShutdown(ctx)`
46+
func ExampleService_OnShutdown() {
47+
c := core.New()
48+
r := cache.NewService(cache.CacheConfig{})(c)
49+
if !r.OK {
50+
core.Println("startup-init-failed")
51+
return
52+
}
53+
svc := r.Value.(*cache.Service)
54+
shutdown := svc.OnShutdown(context.Background())
55+
core.Println(shutdown.OK)
56+
// Output: true
57+
}

go/service_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
6+
core "dappco.re/go"
7+
)
8+
9+
// --- AX-7 compliance triplets ---
10+
11+
func TestService_NewService_Good(t *core.T) {
12+
cfg := CacheConfig{BaseDir: t.TempDir()}
13+
factory := NewService(cfg)
14+
core.AssertNotNil(t, factory)
15+
}
16+
17+
func TestService_NewService_Bad(t *core.T) {
18+
// NewService alone is a factory; resolution happens in c.Service().
19+
// Empty config falls back to package defaults.
20+
cfg := CacheConfig{}
21+
factory := NewService(cfg)
22+
core.AssertNotNil(t, factory)
23+
}
24+
25+
func TestService_NewService_Ugly(t *core.T) {
26+
a := NewService(CacheConfig{BaseDir: t.TempDir()})
27+
b := NewService(CacheConfig{BaseDir: t.TempDir()})
28+
core.AssertNotNil(t, a)
29+
core.AssertNotNil(t, b)
30+
}
31+
32+
// serviceForTest builds a *Service directly, mirroring the canonical
33+
// pattern used in config/go/service_test.go: construct via factory then
34+
// resolve through *Core.
35+
func serviceForTest(t *core.T) *Service {
36+
t.Helper()
37+
c := core.New()
38+
r := NewService(CacheConfig{BaseDir: t.TempDir()})(c)
39+
core.RequireTrue(t, r.OK)
40+
return r.Value.(*Service)
41+
}
42+
43+
func TestService_Service_OnStartup_Good(t *core.T) {
44+
svc := serviceForTest(t)
45+
startup := svc.OnStartup(context.Background())
46+
core.AssertTrue(t, startup.OK)
47+
}
48+
49+
func TestService_Service_OnStartup_Bad(t *core.T) {
50+
var s *Service
51+
r := s.OnStartup(context.Background())
52+
core.AssertTrue(t, r.OK)
53+
}
54+
55+
func TestService_Service_OnStartup_Ugly(t *core.T) {
56+
svc := serviceForTest(t)
57+
// Idempotent — second OnStartup is a no-op via core.Once.
58+
svc.OnStartup(context.Background())
59+
again := svc.OnStartup(context.Background())
60+
core.AssertTrue(t, again.OK)
61+
}
62+
63+
func TestService_Service_OnShutdown_Good(t *core.T) {
64+
svc := serviceForTest(t)
65+
shutdown := svc.OnShutdown(context.Background())
66+
core.AssertTrue(t, shutdown.OK)
67+
}
68+
69+
func TestService_Service_OnShutdown_Bad(t *core.T) {
70+
var s *Service
71+
r := s.OnShutdown(context.Background())
72+
core.AssertTrue(t, r.OK)
73+
}
74+
75+
func TestService_Service_OnShutdown_Ugly(t *core.T) {
76+
svc := serviceForTest(t)
77+
// Multiple shutdowns return Ok cleanly.
78+
svc.OnShutdown(context.Background())
79+
again := svc.OnShutdown(context.Background())
80+
core.AssertTrue(t, again.OK)
81+
}
82+
83+
// --- end AX-7 compliance triplets ---

0 commit comments

Comments
 (0)