Skip to main content

supercode_interchange/orchestration/
route.rs

1//! Routes: which profile answers a surface (ยง2.3, Hermes `gateway.profile_routes`).
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::ontology::{Residue, SurfaceKey};
7
8/// What a route matches; most specific wins.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
10pub struct RouteMatch {
11    /// The platform.
12    pub platform: String,
13    /// Discord-style guild id.
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub guild_id: Option<String>,
16    /// The chat.
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub chat_id: Option<String>,
19    /// The thread.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub thread_id: Option<String>,
22}
23
24/// One route.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
26pub struct Route {
27    /// Optional name.
28    #[serde(default)]
29    pub name: Option<String>,
30    /// What it matches.
31    #[serde(rename = "match")]
32    pub matches: RouteMatch,
33    /// The profile that answers.
34    pub profile: String,
35    /// Unmodeled fields of the route, verbatim.
36    #[serde(default)]
37    pub residue: Residue,
38}
39
40/// Most-specific-first: thread > chat > guild > platform.
41pub fn route_specificity(route: &Route) -> u8 {
42    let m = &route.matches;
43    (if m.thread_id.is_some() { 8 } else { 0 })
44        + (if m.chat_id.is_some() { 4 } else { 0 })
45        + (if m.guild_id.is_some() { 2 } else { 0 })
46        + 1
47}
48
49/// The profile a surface routes to, or `None` for the adapter's own profile.
50/// Ties are broken by declaration order, as Hermes does.
51pub fn resolve_route<'a>(
52    routes: &'a [Route],
53    key: &SurfaceKey,
54    guild_id: Option<&str>,
55) -> Option<&'a str> {
56    let mut best: Option<&Route> = None;
57    for route in routes {
58        let m = &route.matches;
59        if Some(m.platform.as_str()) != key.platform.as_deref() {
60            continue;
61        }
62        if m.guild_id.is_some() && m.guild_id.as_deref() != guild_id {
63            continue;
64        }
65        if m.chat_id.is_some() && m.chat_id != key.chat_id {
66            continue;
67        }
68        if m.thread_id.is_some() && m.thread_id != key.thread_id {
69            continue;
70        }
71        if best.is_none_or(|b| route_specificity(route) > route_specificity(b)) {
72            best = Some(route);
73        }
74    }
75    best.map(|r| r.profile.as_str())
76}