package routing // Resolver answers the one identity question the model needs: does a GUID name // a known identity, and is that identity enrolled? It is INJECTED — the pure // model never touches a database (SPEC-002 §3.1, §5). type Resolver interface { // GUID reports whether guid names a known identity and, if so, whether // that identity is enrolled (spec §3.5: found · not enrolled -> ENROLL; // found · enrolled -> continue to the module). GUID(guid string) (found, enrolled bool) } // Module is one entry of the module registry: its PARAM2 name plus its own // function whitelist and its classifier for unknown values (SPEC-002 §3.7). // The core never parses a module's internals — it asks the module. type Module interface { // Name is the PARAM2 segment — the registry key ("requests", "time", …). Name() string // Route resolves a path-mode function (§3.7). function is the PARAM3 // segment ("" means absent/trailing — the module default); tail is the // remaining segments (PARAM4+). It returns the resolved function name, // the params that function receives, and ok=false when the value is // neither whitelisted nor classified (-> 404). Route(function string, tail Params) (fn string, params Params, ok bool) // Action resolves an action-mode function — the `action` routing field. // ok=false -> 404. Action(action string) (fn string, ok bool) } // Registry is the module whitelist: PARAM2 in path mode and `do` in action mode // (SPEC-002 §3.6/§3.7). SPEC-008 owns the concrete registry; the model depends // only on this interface. type Registry interface { // Module returns the registered module by name. Unknown OR disabled // modules are reported as not found (-> 404). Module(name string) (Module, bool) } // DefaultModule is the module a resolved GUID falls back to when PARAM2 is // absent/trailing — the dashboard (SPEC-002 §3.5, §4.4 level 2: "" -> home). const DefaultModule = "home" // Dispatcher is the pure model's entry point. It holds the injected identity // resolver and module registry and resolves a request into a Resolution. type Dispatcher struct { Resolver Resolver Registry Registry } // New builds a Dispatcher from the injected dependencies. func New(res Resolver, reg Registry) *Dispatcher { return &Dispatcher{Resolver: res, Registry: reg} } // Dispatch is the level-1 dispatch (SPEC-002 §3.3, D-039): keyed on the routing // fields, never the HTTP method. // // if do != "" || action != "" -> Handler(do, action, path) // else -> Router(path) func (d *Dispatcher) Dispatch(path, do, action string) Resolution { if do != "" || action != "" { return d.Handler(do, action, path) } return d.Router(path) } // Dispatch is a convenience wrapper around (*Dispatcher).Dispatch for a one-shot // resolution with explicit dependencies. func Dispatch(res Resolver, reg Registry, path, do, action string) Resolution { return (&Dispatcher{Resolver: res, Registry: reg}).Dispatch(path, do, action) } // Router resolves a path request (SPEC-002 §3.5): the RESERVED switch first (so // reserved names never touch identity), then the GUID classifier, then the // module cascade. func (d *Dispatcher) Router(path string) Resolution { p := Parse(path) // Absent PARAM1 ("" / "/") -> the ONE canonical redirect. if len(p) == 0 { return Resolution{Kind: KindRedirect, Redirect: "/home"} } // An interior empty segment ("//") is a mismatch at any level -> 404. if hasInteriorEmpty(p) { return Resolution{Kind: KindNotFound, Workflow: p[0]} } // Reserved cases are checked BEFORE the resolver (§3.5). switch p[0] { case "setup": return Resolution{Kind: KindSetup, Workflow: "setup"} case "home", "access", "api", "static", "media", "ws", "dev": return reserved(p) } return d.guid(p) } // Handler resolves an action request (SPEC-002 §3.6). The GUID still rides the // path (identity is never a form field); `do` selects the module and `action` // the function — both whitelisted against the registry. func (d *Dispatcher) Handler(do, action, path string) Resolution { p := Parse(path) wf := "" if len(p) > 0 { wf = p[0] } if d.Registry == nil { return Resolution{Kind: KindNotFound, Workflow: wf} } m, ok := d.Registry.Module(do) if !ok { return Resolution{Kind: KindNotFound, Workflow: wf} } fn, ok := m.Action(action) if !ok { return Resolution{Kind: KindNotFound, Workflow: wf} } return Resolution{ Kind: KindAction, Workflow: wf, Module: m.Name(), Function: fn, Params: tailFrom(p, 2), // the path remainder after the GUID } } // reserved classifies a reserved level-1 workflow. These are core-owned, carry // no registry module, and hand the raw path remainder to the core workflow. func reserved(p Params) Resolution { return Resolution{ Kind: KindRoute, Workflow: p[0], Params: tailFrom(p, 1), } } // guid classifies a non-reserved PARAM1 via the injected resolver. func (d *Dispatcher) guid(p Params) Resolution { guid := p[0] if d.Resolver == nil { return Resolution{Kind: KindNotFound, Workflow: guid} } found, enrolled := d.Resolver.GUID(guid) if !found { return Resolution{Kind: KindNotFound, Workflow: guid} } if !enrolled { return Resolution{Kind: KindEnroll, Workflow: guid} } return d.module(guid, p) } // module resolves PARAM2 (the module) and PARAM3+ (the module's own cascade) // for a resolved, enrolled GUID (SPEC-002 §3.5/§3.7). func (d *Dispatcher) module(guid string, p Params) Resolution { // PARAM2 — the module (absent/trailing empty -> the dashboard default). name, absent := segment(p, 1) if absent { name = DefaultModule } if d.Registry == nil { return Resolution{Kind: KindNotFound, Workflow: guid} } m, ok := d.Registry.Module(name) if !ok { return Resolution{Kind: KindNotFound, Workflow: guid} } // PARAM3 — the function (absent/trailing empty -> the module default). fn, absent := segment(p, 2) if absent { fn = "" } resolved, params, ok := m.Route(fn, tailFrom(p, 3)) if !ok { return Resolution{Kind: KindNotFound, Workflow: guid} } return Resolution{ Kind: KindRoute, Workflow: guid, Module: m.Name(), Function: resolved, Params: params, } } // segment applies the presence rules for p[i] (SPEC-002 §3.4). Interior empties // were already rejected by hasInteriorEmpty, so a "" reaching here is always a // trailing slash — which is "absent". Callers substitute their default. func segment(p Params, i int) (value string, absent bool) { if i >= len(p) || p[i] == "" { return "", true } return p[i], false } // hasInteriorEmpty reports whether any non-final segment is empty ("//"). func hasInteriorEmpty(p Params) bool { for i := 0; i < len(p)-1; i++ { if p[i] == "" { return true } } return false } // tailFrom returns the remainder p[i:] for a resolved function (SPEC-002 §3.4). // A trailing empty — a trailing slash — is "absent" and is dropped, so the // function receives no phantom empty param. Interior empties never reach here: // hasInteriorEmpty already rejected them. func tailFrom(p Params, i int) Params { if i >= len(p) { return nil } rest := p[i:] if n := len(rest); n > 0 && rest[n-1] == "" { rest = rest[:n-1] } return rest }