//! 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, 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, /// Owning app. Always equals `scripts.app_id` for the bound script. /// Carried on the route row so the orchestrator can partition the /// route table without joining back to scripts on every refresh. pub app_id: AppId, 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, pub created_at: DateTime, }