// Package routing implements MyDay's pure routing model. // // It turns a request path plus the two routing fields (`do` / `action`) into a // single Resolution for BOTH doors — ROUTER (path) and HANDLER (actions), per // D-039 and SPEC-002. // // The model is deliberately PURE: // // - it imports no HTTP stack and no database driver; // - it never touches storage — identity (GUID -> found/enrolled) and the // module registry are INJECTED interfaces (§3.1); // - so the whole cascade is exhaustively unit-testable in isolation (§5). // // Governing decisions: D-001 · D-004 · D-017 · D-018 · D-032 · D-039. // Spec: docs/specs/spec-002-routing-model.md. package routing import ( "fmt" "strings" ) // Params is the exploded path: the path split on "/" with EMPTY SEGMENTS // PRESERVED (SPEC-002 §3.4). An empty segment is meaningful — a trailing empty // is a trailing slash (→ default), while an interior empty ("//") is a mismatch // (→ 404). type Params []string // Parse strips a single leading "/", splits on "/", and PRESERVES empty // segments (SPEC-002 §3.4): // // "/" -> {} // "/home" -> {"home"} // "/home/" -> {"home", ""} // "/{guid}/request/pto" -> {"{guid}", "request", "pto"} // "/a//b" -> {"a", "", "b"} func Parse(path string) Params { trimmed := strings.TrimPrefix(path, "/") if trimmed == "" { return Params{} } return Params(strings.Split(trimmed, "/")) } // Kind is the classification of a Resolution (SPEC-002 §3.2). type Kind int const ( // KindRoute is a path-mode resolution -> (module, function, params). KindRoute Kind = iota // KindAction is an action-mode resolution, reached via `do`/`action`. KindAction // KindRedirect is the ONE canonical redirect ("/" -> "/home"). KindRedirect // KindNotFound is a 404. KindNotFound // KindEnroll is the enrollment screen (GUID found but not enrolled). KindEnroll // KindLogin is the login screen. The PURE model never returns this: it // returns KindRoute and the door upgrades it when no session exists // (SPEC-002 §6). KindLogin // KindSetup is the setup wizard (setup mode only). KindSetup ) // String renders a Kind for logs and test failures. func (k Kind) String() string { switch k { case KindRoute: return "Route" case KindAction: return "Action" case KindRedirect: return "Redirect" case KindNotFound: return "NotFound" case KindEnroll: return "Enroll" case KindLogin: return "Login" case KindSetup: return "Setup" default: return fmt.Sprintf("Kind(%d)", int(k)) } } // Resolution is the single outcome of the model (SPEC-002 §3.2). One request // resolves to one Resolution; the door then gates and dispatches it. type Resolution struct { // Kind classifies the outcome. Kind Kind // Workflow is the PARAM1 classification: a reserved workflow ("home", // "access", "setup", "api", …) or the GUID itself; "" for the redirect. Workflow string // Module is the resolved registry module (PARAM2 / `do`). Empty for // reserved workflows — those are core-owned and carry no module. Module string // Function is the resolved module function (PARAM3 / `action`). Function string // Params is the remainder handed to the function. Params Params // Redirect is the target path when Kind == KindRedirect. Redirect string } // ReservedWorkflows is the ONE source for the reserved level-1 names (SPEC-002 // §4, D-004). A GUID may never equal one of these; validate on create AND // update with ValidateWorkflowName. // // NOTE: kept as an exported slice because the spec declares it as a constant // source of truth; treat it as read-only. Use IsReservedWorkflow for checks. var ReservedWorkflows = []string{ "home", "access", "setup", "api", "static", "media", "ws", "dev", } // IsReservedWorkflow reports whether name is a reserved level-1 workflow // (exact, case-sensitive match — SPEC-002 §4). func IsReservedWorkflow(name string) bool { for _, w := range ReservedWorkflows { if w == name { return true } } return false } // ValidateWorkflowName rejects a name that collides with a reserved workflow. // It is the single guard used when a GUID (or any workflow name) is created or // updated (SPEC-002 §4, test row 22). func ValidateWorkflowName(name string) error { if IsReservedWorkflow(name) { return fmt.Errorf("routing: %q is a reserved workflow name and cannot be a GUID", name) } return nil } // IsNumeric reports whether s is a non-empty ASCII digit string. It is the // built-in classifier modules use for unknown path values such as numeric ids // (SPEC-002 §3.7); e.g. `/{GUID}/requests/42` -> function "detail", params {42}. func IsNumeric(s string) bool { if s == "" { return false } for i := 0; i < len(s); i++ { if s[i] < '0' || s[i] > '9' { return false } } return true }