feat(gui): core:// scheme handler + 7 route dispatches
display.Service now exposes a SchemeHandler interface; new scheme_handler.go implements the core:// dispatcher covering the 7 RFC routes: - settings, store, network, models → core.QUERY dispatch - agent, wallet, identity → core.ACTION dispatch Validates URL shape (rejects paths beyond the scheme host, malformed URLs), unknown routes return a named error. Good/Bad/Ugly tests + godoc example. AssetServer startup wiring deferred — no tracked Wails bootstrap (application.New / wails.Run / AssetServer config) exists in this worktree yet; handler is ready for wiring when that lands. Closes tasks.lthn.sh/view.php?id=15 Co-authored-by: Codex <noreply@openai.com> Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
016c2bd9c6
commit
d3858dcd26
4 changed files with 302 additions and 1 deletions
|
|
@ -1,7 +1,26 @@
|
|||
// pkg/display/interfaces.go
|
||||
package display
|
||||
|
||||
import "github.com/wailsapp/wails/v3/pkg/application"
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// RouteSchemeHandler dispatches a parsed route URL through Core.
|
||||
//
|
||||
// result := handler.Handle(parsedURL)
|
||||
type RouteSchemeHandler interface {
|
||||
Handle(url *url.URL) core.Result
|
||||
}
|
||||
|
||||
// SchemeHandlerProvider exposes the active route scheme handler.
|
||||
//
|
||||
// handler := svc.SchemeHandler()
|
||||
type SchemeHandlerProvider interface {
|
||||
SchemeHandler() RouteSchemeHandler
|
||||
}
|
||||
|
||||
// App abstracts the Wails application for the orchestrator.
|
||||
// After Spec D cleanup, only Quit() and Logger() remain —
|
||||
|
|
|
|||
133
pkg/display/scheme_handler.go
Normal file
133
pkg/display/scheme_handler.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package display
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
type routeDispatchKind uint8
|
||||
|
||||
const (
|
||||
routeDispatchQuery routeDispatchKind = iota
|
||||
routeDispatchAction
|
||||
)
|
||||
|
||||
var coreRouteDispatch = map[string]routeDispatchKind{
|
||||
"settings": routeDispatchQuery,
|
||||
"store": routeDispatchQuery,
|
||||
"network": routeDispatchQuery,
|
||||
"models": routeDispatchQuery,
|
||||
"agent": routeDispatchAction,
|
||||
"wallet": routeDispatchAction,
|
||||
"identity": routeDispatchAction,
|
||||
}
|
||||
|
||||
type coreSchemeHandler struct {
|
||||
core *core.Core
|
||||
}
|
||||
|
||||
// NewCoreSchemeHandler returns the RFC route dispatcher for `core://` URLs.
|
||||
//
|
||||
// handler := display.NewCoreSchemeHandler(c)
|
||||
func NewCoreSchemeHandler(c *core.Core) RouteSchemeHandler {
|
||||
return coreSchemeHandler{core: c}
|
||||
}
|
||||
|
||||
// SchemeHandler exposes the RFC route dispatcher for the active display service.
|
||||
//
|
||||
// handler := svc.SchemeHandler()
|
||||
func (s *Service) SchemeHandler() RouteSchemeHandler {
|
||||
if s == nil || s.ServiceRuntime == nil {
|
||||
return coreSchemeHandler{}
|
||||
}
|
||||
return NewCoreSchemeHandler(s.Core())
|
||||
}
|
||||
|
||||
func (h coreSchemeHandler) Handle(rawURL *url.URL) core.Result {
|
||||
if h.core == nil {
|
||||
return core.Result{
|
||||
Value: coreerr.E("display.coreSchemeHandler.Handle", "core runtime unavailable", nil),
|
||||
OK: false,
|
||||
}
|
||||
}
|
||||
|
||||
route, dispatch, result := resolveCoreSchemeRoute(rawURL)
|
||||
if !result.OK {
|
||||
return result
|
||||
}
|
||||
|
||||
target := "core." + route
|
||||
switch dispatch {
|
||||
case routeDispatchAction:
|
||||
return h.core.Action(target).Run(context.Background(), core.NewOptions())
|
||||
case routeDispatchQuery:
|
||||
result = h.core.Query(target)
|
||||
if result.OK {
|
||||
return result
|
||||
}
|
||||
return core.Result{
|
||||
Value: coreerr.E("display.coreSchemeHandler.Handle", "query not handled: "+target, nil),
|
||||
OK: false,
|
||||
}
|
||||
default:
|
||||
return core.Result{
|
||||
Value: coreerr.E("display.coreSchemeHandler.Handle", "unsupported dispatch kind", nil),
|
||||
OK: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveCoreSchemeRoute(rawURL *url.URL) (string, routeDispatchKind, core.Result) {
|
||||
if rawURL == nil {
|
||||
return "", routeDispatchQuery, core.Result{
|
||||
Value: coreerr.E("display.resolveCoreSchemeRoute", "scheme URL is required", nil),
|
||||
OK: false,
|
||||
}
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(rawURL.Scheme), "core") {
|
||||
return "", routeDispatchQuery, core.Result{
|
||||
Value: coreerr.E("display.resolveCoreSchemeRoute", "unsupported scheme: "+rawURL.Scheme, nil),
|
||||
OK: false,
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(rawURL.Opaque) != "" {
|
||||
return "", routeDispatchQuery, core.Result{
|
||||
Value: coreerr.E("display.resolveCoreSchemeRoute", "malformed core URL", nil),
|
||||
OK: false,
|
||||
}
|
||||
}
|
||||
if rawURL.User != nil || strings.TrimSpace(rawURL.Fragment) != "" || rawURL.Port() != "" {
|
||||
return "", routeDispatchQuery, core.Result{
|
||||
Value: coreerr.E("display.resolveCoreSchemeRoute", "malformed core URL", nil),
|
||||
OK: false,
|
||||
}
|
||||
}
|
||||
if path := strings.TrimSpace(rawURL.Path); path != "" && path != "/" {
|
||||
return "", routeDispatchQuery, core.Result{
|
||||
Value: coreerr.E("display.resolveCoreSchemeRoute", "malformed core URL", nil),
|
||||
OK: false,
|
||||
}
|
||||
}
|
||||
|
||||
route := strings.ToLower(strings.TrimSpace(rawURL.Hostname()))
|
||||
if route == "" {
|
||||
return "", routeDispatchQuery, core.Result{
|
||||
Value: coreerr.E("display.resolveCoreSchemeRoute", "malformed core URL", nil),
|
||||
OK: false,
|
||||
}
|
||||
}
|
||||
|
||||
dispatch, ok := coreRouteDispatch[route]
|
||||
if !ok {
|
||||
return "", routeDispatchQuery, core.Result{
|
||||
Value: coreerr.E("display.resolveCoreSchemeRoute", "unknown core route: "+route, nil),
|
||||
OK: false,
|
||||
}
|
||||
}
|
||||
|
||||
return route, dispatch, core.Result{OK: true}
|
||||
}
|
||||
25
pkg/display/scheme_handler_example_test.go
Normal file
25
pkg/display/scheme_handler_example_test.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
func ExampleNewCoreSchemeHandler() {
|
||||
c := core.New()
|
||||
c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
|
||||
name, ok := q.(string)
|
||||
if !ok || name != "core.settings" {
|
||||
return core.Result{}
|
||||
}
|
||||
return core.Result{Value: "settings-query", OK: true}
|
||||
})
|
||||
|
||||
parsedURL, _ := url.Parse("core://settings")
|
||||
result := NewCoreSchemeHandler(c).Handle(parsedURL)
|
||||
|
||||
fmt.Println(result.OK, result.Value)
|
||||
// Output: true settings-query
|
||||
}
|
||||
124
pkg/display/scheme_handler_test.go
Normal file
124
pkg/display/scheme_handler_test.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package display
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type schemeDispatchRecorder struct {
|
||||
queries []string
|
||||
actions []string
|
||||
}
|
||||
|
||||
func newTestCoreSchemeHandler(t *testing.T) (RouteSchemeHandler, *schemeDispatchRecorder) {
|
||||
t.Helper()
|
||||
|
||||
c := core.New(
|
||||
core.WithService(Register(nil)),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.True(t, c.ServiceStartup(context.Background(), nil).OK)
|
||||
|
||||
recorder := &schemeDispatchRecorder{}
|
||||
c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
|
||||
name, ok := q.(string)
|
||||
if !ok {
|
||||
return core.Result{}
|
||||
}
|
||||
|
||||
recorder.queries = append(recorder.queries, name)
|
||||
switch name {
|
||||
case "core.settings":
|
||||
return core.Result{Value: "settings-query", OK: true}
|
||||
case "core.store":
|
||||
return core.Result{Value: "store-query", OK: true}
|
||||
case "core.network":
|
||||
return core.Result{Value: "network-query", OK: true}
|
||||
case "core.models":
|
||||
return core.Result{Value: "models-query", OK: true}
|
||||
default:
|
||||
return core.Result{}
|
||||
}
|
||||
})
|
||||
|
||||
c.Action("core.agent", func(_ context.Context, _ core.Options) core.Result {
|
||||
recorder.actions = append(recorder.actions, "core.agent")
|
||||
return core.Result{Value: "agent-action", OK: true}
|
||||
})
|
||||
c.Action("core.wallet", func(_ context.Context, _ core.Options) core.Result {
|
||||
recorder.actions = append(recorder.actions, "core.wallet")
|
||||
return core.Result{Value: "wallet-action", OK: true}
|
||||
})
|
||||
c.Action("core.identity", func(_ context.Context, _ core.Options) core.Result {
|
||||
recorder.actions = append(recorder.actions, "core.identity")
|
||||
return core.Result{Value: "identity-action", OK: true}
|
||||
})
|
||||
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
return svc.SchemeHandler(), recorder
|
||||
}
|
||||
|
||||
func TestSchemeHandler_Handle_Good(t *testing.T) {
|
||||
handler, recorder := newTestCoreSchemeHandler(t)
|
||||
|
||||
tests := []struct {
|
||||
rawURL string
|
||||
value string
|
||||
}{
|
||||
{rawURL: "core://settings", value: "settings-query"},
|
||||
{rawURL: "core://store", value: "store-query"},
|
||||
{rawURL: "core://network", value: "network-query"},
|
||||
{rawURL: "core://models", value: "models-query"},
|
||||
{rawURL: "core://agent", value: "agent-action"},
|
||||
{rawURL: "core://wallet", value: "wallet-action"},
|
||||
{rawURL: "core://identity", value: "identity-action"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
parsedURL, err := url.Parse(test.rawURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := handler.Handle(parsedURL)
|
||||
require.True(t, result.OK, test.rawURL)
|
||||
assert.Equal(t, test.value, result.Value)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"core.settings",
|
||||
"core.store",
|
||||
"core.network",
|
||||
"core.models",
|
||||
}, recorder.queries)
|
||||
assert.Equal(t, []string{
|
||||
"core.agent",
|
||||
"core.wallet",
|
||||
"core.identity",
|
||||
}, recorder.actions)
|
||||
}
|
||||
|
||||
func TestSchemeHandler_Handle_Bad(t *testing.T) {
|
||||
handler, _ := newTestCoreSchemeHandler(t)
|
||||
|
||||
parsedURL, err := url.Parse("core://missing")
|
||||
require.NoError(t, err)
|
||||
|
||||
result := handler.Handle(parsedURL)
|
||||
require.False(t, result.OK)
|
||||
assert.ErrorContains(t, result.Value.(error), "unknown core route: missing")
|
||||
}
|
||||
|
||||
func TestSchemeHandler_Handle_Ugly(t *testing.T) {
|
||||
handler, _ := newTestCoreSchemeHandler(t)
|
||||
|
||||
parsedURL, err := url.Parse("core://settings/profile")
|
||||
require.NoError(t, err)
|
||||
|
||||
result := handler.Handle(parsedURL)
|
||||
require.False(t, result.OK)
|
||||
assert.ErrorContains(t, result.Value.(error), "malformed core URL")
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue