//! Route binding: which `(host, method, path)` tuples invoke a given //! script. The storage shape (this file) is intentionally flat — the //! orchestrator parses these into typed `HostPattern` / `PathPattern` //! values for matching, and reconstructs strings for display. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{AppId, GroupId, ScriptId}; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum HostKind { /// Matches any host header. Any, /// Exact hostname match: `sub.example.com`. Strict, /// Wildcard suffix match: `*.example.com` matches any subdomain of /// `example.com`. Capture name is reserved for the future /// `{name}.example.com` syntax (currently always None on writes). Wildcard, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum PathKind { /// Literal string equality: `/greet` matches only `/greet`. Exact, /// Strict-subtree match. Stored as the prefix including the trailing /// slash; `/greet/*` is stored as path "/greet/" and matches /// `/greet/x` but not `/greet` itself (which would need its own /// exact route). Prefix, /// Named-parameter pattern: `/greet/:name` where each `:foo` consumes /// exactly one segment and captures it into `ctx.request.params.foo`. Param, } /// Per-route dispatch mode (v1.1.1). `Sync` = orchestrator awaits the /// executor and returns the response in the same HTTP request. `Async` /// = orchestrator writes the request to the trigger outbox, returns /// `202 Accepted` immediately, and the dispatcher runs the script in /// the background (with retries + dead-letter). #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(rename_all = "lowercase")] pub enum DispatchMode { #[default] Sync, Async, } impl DispatchMode { #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::Sync => "sync", Self::Async => "async", } } #[must_use] pub fn from_wire(s: &str) -> Option { match s { "sync" => Some(Self::Sync), "async" => Some(Self::Async), _ => None, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Route { pub id: Uuid, /// Polymorphic owner (§11 tail): an **app** owns a concrete route; a /// **group** owns a route TEMPLATE, expanded into every descendant app's /// in-memory slice at rebuild. Exactly one is set (DB CHECK). Mirrors /// `Trigger`. For an app route, `app_id` equals `scripts.app_id` for the /// bound script — carried here so the orchestrator can partition the route /// table without joining back to scripts on every refresh. pub app_id: Option, pub group_id: Option, pub script_id: ScriptId, pub host_kind: HostKind, /// For `Any`: empty string. For `Strict`: full hostname. For /// `Wildcard`: the suffix after the leading `*.` (e.g. `example.com`). pub host: String, pub host_param_name: Option, pub path_kind: PathKind, /// Raw path as the user typed it. For `Prefix`, normalized to end /// with `/` (the trailing `*` is dropped on write). pub path: String, /// `None` = any method. pub method: Option, /// v1.1.1: per-route dispatch mode. `Sync` (default) → orchestrator /// awaits the executor inline. `Async` → orchestrator writes to /// the outbox + returns `202 Accepted`; dispatcher fires the /// script in the background with retries. #[serde(default)] pub dispatch_mode: DispatchMode, /// Three-state lifecycle (§4.3): `false` means the route is deployed but /// inert — it is dropped from the compiled match table, so a request 404s /// indistinguishably from an absent route. Defaults `true`. #[serde(default = "crate::default_true")] pub enabled: bool, /// §11 tail: `true` for a sealed group route TEMPLATE — a descendant app's /// `[suppress]` cannot decline it, so the RouteTable rebuild keeps it in the /// app's slice even at a suppressed path. Always `false` for an app-owned /// route (sealing an un-inherited route is rejected at apply). Consulted /// only at rebuild via `list_effective`; the request hot path never reads it. #[serde(default)] pub sealed: bool, pub created_at: DateTime, }