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}