diff --git a/pkg/display/interfaces.go b/pkg/display/interfaces.go index 2ca6738d..14a319a3 100644 --- a/pkg/display/interfaces.go +++ b/pkg/display/interfaces.go @@ -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 — diff --git a/pkg/display/scheme_handler.go b/pkg/display/scheme_handler.go new file mode 100644 index 00000000..d1d628cc --- /dev/null +++ b/pkg/display/scheme_handler.go @@ -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} +} diff --git a/pkg/display/scheme_handler_example_test.go b/pkg/display/scheme_handler_example_test.go new file mode 100644 index 00000000..9efe999a --- /dev/null +++ b/pkg/display/scheme_handler_example_test.go @@ -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 +} diff --git a/pkg/display/scheme_handler_test.go b/pkg/display/scheme_handler_test.go new file mode 100644 index 00000000..fc491d59 --- /dev/null +++ b/pkg/display/scheme_handler_test.go @@ -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") +}