Skip to main content

oxi/internal_urls/
rule_handler.rs

1//! `rule://` protocol handler — resolves TTSR rule names to their content.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use oxi_sdk::SdkError;
7use oxi_sdk::ports::{ProtocolHandler, ResolveContext, ResolvedUrl, RuleRegistry};
8
9/// Protocol handler for `rule://` URLs backed by the SDK's `RuleRegistry` port.
10pub struct RuleProtocolHandler {
11    registry: Arc<dyn RuleRegistry>,
12}
13
14impl RuleProtocolHandler {
15    /// Create a new handler backed by the given rule registry.
16    pub fn new(registry: Arc<dyn RuleRegistry>) -> Self {
17        Self { registry }
18    }
19}
20
21#[async_trait]
22impl ProtocolHandler for RuleProtocolHandler {
23    fn scheme(&self) -> &str {
24        "rule"
25    }
26    fn immutable(&self) -> bool {
27        true
28    }
29
30    async fn resolve(
31        &self,
32        url: &str,
33        _selector: Option<&str>,
34        _ctx: &ResolveContext,
35    ) -> Result<ResolvedUrl, SdkError> {
36        let rule_name = url
37            .strip_prefix("rule://")
38            .unwrap_or(url)
39            .trim_end_matches('/');
40        if rule_name.is_empty() {
41            return Err(SdkError::ExecutionFailed {
42                reason: "rule:// URL requires a rule name".into(),
43            });
44        }
45
46        let rules = self.registry.rules().await;
47        let rule = rules.iter().find(|r| r.name == rule_name).ok_or_else(|| {
48            let available: Vec<String> = rules.iter().map(|r| r.name.clone()).collect();
49            SdkError::ExecutionFailed {
50                reason: format!(
51                    "Unknown rule: '{rule_name}'. Available: {}",
52                    if available.is_empty() {
53                        "none".to_string()
54                    } else {
55                        available.join(", ")
56                    }
57                ),
58            }
59        })?;
60
61        Ok(ResolvedUrl {
62            url: format!("rule://{rule_name}"),
63            content: rule.content.clone(),
64            content_type: "text/markdown".into(),
65            size: None,
66            source_path: None,
67            notes: vec![],
68            immutable: true,
69        })
70    }
71}