ucxl/
lib.rs

1//! UCXL core data structures and utilities.
2//!
3//! This module provides the fundamental types used throughout the CHORUS
4//! system for addressing resources (UCXL addresses), handling temporal axes,
5//! and storing lightweight metadata. The implementation is deliberately
6//! lightweight and in‑memory to keep the core fast and dependency‑free.
7
8pub mod watcher;
9
10use std::collections::HashMap;
11use std::fmt;
12use std::str::FromStr;
13
14/// Represents the temporal axis in a UCXL address.
15///
16/// **What**: An enumeration of the three supported temporal positions –
17/// present, past, and future – each represented by a symbolic string in the
18/// address format.
19///
20/// **How**: The enum derives `Debug`, `PartialEq`, `Eq`, `Clone`, and `Copy`
21/// for ergonomic usage. Conversions to and from strings are provided via the
22/// `FromStr` and `fmt::Display` implementations.
23///
24/// **Why**: Temporal axes enable UCXL to refer to data at different points in
25/// time (e.g. versioned resources). The simple three‑state model matches the
26/// CHURUS architectural decision to keep addressing lightweight while still
27/// supporting historical and speculative queries.
28#[derive(Debug, PartialEq, Eq, Clone, Copy)]
29pub enum TemporalAxis {
30    /// Present ("#") – the current version of a resource.
31    Present,
32    /// Past ("~~") – a historical snapshot of a resource.
33    Past,
34    /// Future ("^^") – a speculative or planned version of a resource.
35    Future,
36}
37
38impl FromStr for TemporalAxis {
39    type Err = String;
40    /// Parses a temporal axis token from its textual representation.
41    ///
42    /// **What**: Accepts "#", "~~" or "^^" and maps them to the corresponding
43    /// enum variant.
44    ///
45    /// **How**: A simple `match` statement is used; an error string is
46    /// returned for any unrecognised token.
47    ///
48    /// **Why**: Centralises validation of temporal markers used throughout the
49    /// address parsing logic, ensuring consistency.
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        match s {
52            "#" => Ok(TemporalAxis::Present),
53            "~~" => Ok(TemporalAxis::Past),
54            "^^" => Ok(TemporalAxis::Future),
55            _ => Err(format!("Invalid temporal axis: {}", s)),
56        }
57    }
58}
59
60impl fmt::Display for TemporalAxis {
61    /// Formats the temporal axis back to its string token.
62    ///
63    /// **What**: Returns "#", "~~" or "^^" depending on the variant.
64    ///
65    /// **How**: Matches on `self` and writes the corresponding string to the
66    /// formatter.
67    ///
68    /// **Why**: Required for serialising a `UCXLAddress` back to its textual
69    /// representation.
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        let s = match self {
72            TemporalAxis::Present => "#",
73            TemporalAxis::Past => "~~",
74            TemporalAxis::Future => "^^",
75        };
76        write!(f, "{}", s)
77    }
78}
79
80/// Represents a parsed UCXL address.
81///
82/// **What**: Holds the components extracted from a UCXL URI – the agent, an
83/// optional role, the project identifier, task name, temporal axis, and the
84/// resource path within the project.
85///
86/// **How**: The struct is constructed via the `FromStr` implementation which
87/// validates the scheme, splits the address into its constituent parts and
88/// populates the fields. The `Display` implementation performs the inverse
89/// operation.
90///
91/// **Why**: UCXL addresses are the primary routing mechanism inside CHORUS.
92/// Encapsulating them in a dedicated type provides type‑safety and makes it
93/// easy to work with address components in the rest of the codebase.
94#[derive(Debug, PartialEq, Eq, Clone)]
95pub struct UCXLAddress {
96    /// The identifier of the agent (e.g., a user or system component).
97    pub agent: String,
98    /// Optional role associated with the agent (e.g., "admin").
99    pub role: Option<String>,
100    /// The project namespace this address belongs to.
101    pub project: String,
102    /// The specific task within the project.
103    pub task: String,
104    /// Temporal axis indicating present, past or future.
105    pub temporal: TemporalAxis,
106    /// Path to the resource relative to the project root.
107    pub path: String,
108}
109
110impl FromStr for UCXLAddress {
111    type Err = String;
112    /// Parses a full UCXL address string into a `UCXLAddress` value.
113    ///
114    /// **What**: Validates the scheme (`ucxl://`), extracts the agent, optional
115    /// role, project, task, temporal axis and the trailing resource path.
116    ///
117    /// **How**: The implementation performs a series of `split` operations,
118    /// handling optional components and converting the temporal token via
119    /// `TemporalAxis::from_str`. Errors are surfaced as descriptive strings.
120    ///
121    /// **Why**: Centralises address parsing logic, ensuring that all parts of
122    /// the system interpret UCXL URIs consistently.
123    fn from_str(address: &str) -> Result<Self, Self::Err> {
124        // Ensure the scheme is correct
125        let scheme_split: Vec<&str> = address.splitn(2, "://").collect();
126        if scheme_split.len() != 2 || scheme_split[0] != "ucxl" {
127            return Err("Address must start with 'ucxl://'".into());
128        }
129        let remainder = scheme_split[1];
130        // Split at the first '@' to separate agent/role from project/task
131        let parts: Vec<&str> = remainder.splitn(2, '@').collect();
132        if parts.len() != 2 {
133            return Err("Missing '@' separating agent and project".into());
134        }
135        // Agent and optional role
136        let agent_part = parts[0];
137        let mut agent_iter = agent_part.splitn(2, ':');
138        let agent = agent_iter.next().unwrap().to_string();
139        let role = agent_iter.next().map(|s| s.to_string());
140        // Project and task
141        let project_task_part = parts[1];
142        // Find the first '/' that starts the temporal segment and path
143        let slash_idx = project_task_part
144            .find('/')
145            .ok_or("Missing '/' before temporal segment and path")?;
146        let (proj_task, after_slash) = project_task_part.split_at(slash_idx);
147        let mut proj_task_iter = proj_task.splitn(2, ':');
148        let project = proj_task_iter.next().ok_or("Missing project")?.to_string();
149        let task = proj_task_iter.next().ok_or("Missing task")?.to_string();
150        // after_slash starts with '/', remove it
151        let after = &after_slash[1..];
152        // Temporal segment is up to the next '/' if present
153        let temporal_end = after
154            .find('/')
155            .ok_or("Missing '/' after temporal segment")?;
156        let temporal_str = &after[..temporal_end];
157        let temporal = TemporalAxis::from_str(temporal_str)?;
158        // The rest is the resource path
159        let path = after[temporal_end + 1..].to_string();
160        Ok(UCXLAddress {
161            agent,
162            role,
163            project,
164            task,
165            temporal,
166            path,
167        })
168    }
169}
170
171impl fmt::Display for UCXLAddress {
172    /// Serialises the address back to its canonical string form.
173    ///
174    /// **What**: Constructs a `ucxl://` URI including optional role and path.
175    ///
176    /// **How**: Conditionally inserts the role component, then formats the
177    /// project, task, temporal token and optional path using standard `write!`
178    /// semantics.
179    ///
180    /// **Why**: Needed when emitting addresses (e.g., logging events or
181    /// generating links) so that external tools can consume them.
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        let role_part = if let Some(r) = &self.role {
184            format!(":{}", r)
185        } else {
186            "".to_string()
187        };
188        write!(
189            f,
190            "ucxl://{}{}@{}:{}/{}{}",
191            self.agent,
192            role_part,
193            self.project,
194            self.task,
195            self.temporal,
196            if self.path.is_empty() {
197                "".to_string()
198            } else {
199                format!("/{}", self.path)
200            }
201        )
202    }
203}
204
205/// Trait defining a simple key‑value metadata store.
206///
207/// **What**: Provides read, write and removal operations for associating a
208/// string of metadata with a file‑system path.
209///
210/// **How**: The trait abstracts over concrete storage implementations –
211/// currently an in‑memory `HashMap` – allowing callers to depend on the trait
212/// rather than a specific type.
213///
214/// **Why**: CHORUS needs a lightweight way to attach auxiliary information to
215/// files without persisting to a database; the trait makes it easy to swap in a
216/// persistent backend later if required.
217pub trait MetadataStore {
218    /// Retrieves the metadata for `path` if it exists.
219    fn get(&self, path: &str) -> Option<&String>;
220    /// Stores `metadata` for `path`, overwriting any existing value.
221    fn set(&mut self, path: &str, metadata: String);
222    /// Removes the metadata entry for `path`, returning the old value if any.
223    fn remove(&mut self, path: &str) -> Option<String> {
224        None
225    }
226}
227
228/// In‑memory implementation of `MetadataStore` backed by a `HashMap`.
229///
230/// **What**: Holds metadata in a hash map where the key is the file path.
231///
232/// **How**: Provides a `new` constructor and implements the `MetadataStore`
233/// trait methods by delegating to the underlying map.
234///
235/// **Why**: Offers a zero‑cost, dependency‑free store suitable for unit tests
236/// and simple scenarios. It can be replaced with a persistent store without
237/// changing callers.
238pub struct InMemoryMetadataStore {
239    map: HashMap<String, String>,
240}
241
242impl InMemoryMetadataStore {
243    /// Creates a fresh, empty `InMemoryMetadataStore`.
244    ///
245    /// **What**: Returns a struct with an empty internal map.
246    ///
247    /// **How**: Calls `HashMap::new`.
248    ///
249    /// **Why**: Convenience constructor for callers.
250    pub fn new() -> Self {
251        InMemoryMetadataStore {
252            map: HashMap::new(),
253        }
254    }
255}
256
257impl MetadataStore for InMemoryMetadataStore {
258    fn get(&self, path: &str) -> Option<&String> {
259        self.map.get(path)
260    }
261    fn set(&mut self, path: &str, metadata: String) {
262        self.map.insert(path.to_string(), metadata);
263    }
264    fn remove(&mut self, path: &str) -> Option<String> {
265        self.map.remove(path)
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_temporal_from_str() {
275        assert_eq!(TemporalAxis::from_str("#").unwrap(), TemporalAxis::Present);
276        assert_eq!(TemporalAxis::from_str("~~").unwrap(), TemporalAxis::Past);
277        assert_eq!(TemporalAxis::from_str("^^").unwrap(), TemporalAxis::Future);
278    }
279
280    #[test]
281    fn test_ucxl_address_parsing() {
282        let addr_str = "ucxl://alice:admin@myproj:task1/#/docs/readme.md";
283        let addr = UCXLAddress::from_str(addr_str).unwrap();
284        assert_eq!(addr.agent, "alice");
285        assert_eq!(addr.role, Some("admin".to_string()));
286        assert_eq!(addr.project, "myproj");
287        assert_eq!(addr.task, "task1");
288        assert_eq!(addr.temporal, TemporalAxis::Present);
289        assert_eq!(addr.path, "docs/readme.md");
290        assert_eq!(addr.to_string(), addr_str);
291    }
292
293    #[test]
294    fn test_metadata_store() {
295        let mut store = InMemoryMetadataStore::new();
296        store.set("/foo.txt", "meta".into());
297        assert_eq!(store.get("/foo.txt"), Some(&"meta".to_string()));
298        store.remove("/foo.txt");
299        assert!(store.get("/foo.txt").is_none());
300    }
301}