Bootstrap UCXL Core: Implementation of UCXLAddress and TemporalAxis in Rust
This commit is contained in:
192
UCXL/src/lib.rs
Normal file
192
UCXL/src/lib.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
// UCXL Core Data Structures
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Represents the temporal axis in a UCXL address.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum TemporalAxis {
|
||||
/// Present ("#")
|
||||
Present,
|
||||
/// Past ("~~")
|
||||
Past,
|
||||
/// Future ("^^")
|
||||
Future,
|
||||
}
|
||||
|
||||
impl FromStr for TemporalAxis {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"#" => Ok(TemporalAxis::Present),
|
||||
"~~" => Ok(TemporalAxis::Past),
|
||||
"^^" => Ok(TemporalAxis::Future),
|
||||
_ => Err(format!("Invalid temporal axis: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TemporalAxis {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
TemporalAxis::Present => "#",
|
||||
TemporalAxis::Past => "~~",
|
||||
TemporalAxis::Future => "^^",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a parsed UCXL address.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct UCXLAddress {
|
||||
pub agent: String,
|
||||
pub role: Option<String>,
|
||||
pub project: String,
|
||||
pub task: String,
|
||||
pub temporal: TemporalAxis,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl FromStr for UCXLAddress {
|
||||
type Err = String;
|
||||
fn from_str(address: &str) -> Result<Self, Self::Err> {
|
||||
// Ensure the scheme is correct
|
||||
let scheme_split: Vec<&str> = address.splitn(2, "://").collect();
|
||||
if scheme_split.len() != 2 || scheme_split[0] != "ucxl" {
|
||||
return Err("Address must start with 'ucxl://'".into());
|
||||
}
|
||||
let remainder = scheme_split[1];
|
||||
// Split at the first '@' to separate agent/role from project/task
|
||||
let parts: Vec<&str> = remainder.splitn(2, '@').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("Missing '@' separating agent and project".into());
|
||||
}
|
||||
// Agent and optional role
|
||||
let agent_part = parts[0];
|
||||
let mut agent_iter = agent_part.splitn(2, ':');
|
||||
let agent = agent_iter.next().unwrap().to_string();
|
||||
let role = agent_iter.next().map(|s| s.to_string());
|
||||
// Project and task
|
||||
let project_task_part = parts[1];
|
||||
// Find the first '/' that starts the temporal segment and path
|
||||
let slash_idx = project_task_part
|
||||
.find('/')
|
||||
.ok_or("Missing '/' before temporal segment and path")?;
|
||||
let (proj_task, after_slash) = project_task_part.split_at(slash_idx);
|
||||
let mut proj_task_iter = proj_task.splitn(2, ':');
|
||||
let project = proj_task_iter.next().ok_or("Missing project")?.to_string();
|
||||
let task = proj_task_iter.next().ok_or("Missing task")?.to_string();
|
||||
// after_slash starts with '/', remove it
|
||||
let after = &after_slash[1..];
|
||||
// Temporal segment is up to the next '/' if present
|
||||
let temporal_end = after
|
||||
.find('/')
|
||||
.ok_or("Missing '/' after temporal segment")?;
|
||||
let temporal_str = &after[..temporal_end];
|
||||
let temporal = TemporalAxis::from_str(temporal_str)?;
|
||||
// The rest is the resource path
|
||||
let path = after[temporal_end + 1..].to_string();
|
||||
Ok(UCXLAddress {
|
||||
agent,
|
||||
role,
|
||||
project,
|
||||
task,
|
||||
temporal,
|
||||
path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UCXLAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let role_part = if let Some(r) = &self.role {
|
||||
format!(":{}", r)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"ucxl://{}{}@{}:{}/{}{}",
|
||||
self.agent,
|
||||
role_part,
|
||||
self.project,
|
||||
self.task,
|
||||
self.temporal,
|
||||
if self.path.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("/{}", self.path)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple in‑memory metadata store mapping a file path to a metadata string.
|
||||
pub trait MetadataStore {
|
||||
fn get(&self, path: &str) -> Option<&String>;
|
||||
fn set(&mut self, path: &str, metadata: String);
|
||||
fn remove(&mut self, path: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A concrete in‑memory implementation using a HashMap.
|
||||
pub struct InMemoryMetadataStore {
|
||||
map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl InMemoryMetadataStore {
|
||||
pub fn new() -> Self {
|
||||
InMemoryMetadataStore {
|
||||
map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataStore for InMemoryMetadataStore {
|
||||
fn get(&self, path: &str) -> Option<&String> {
|
||||
self.map.get(path)
|
||||
}
|
||||
fn set(&mut self, path: &str, metadata: String) {
|
||||
self.map.insert(path.to_string(), metadata);
|
||||
}
|
||||
fn remove(&mut self, path: &str) -> Option<String> {
|
||||
self.map.remove(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_temporal_from_str() {
|
||||
assert_eq!(TemporalAxis::from_str("#").unwrap(), TemporalAxis::Present);
|
||||
assert_eq!(TemporalAxis::from_str("~~").unwrap(), TemporalAxis::Past);
|
||||
assert_eq!(TemporalAxis::from_str("^^").unwrap(), TemporalAxis::Future);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ucxl_address_parsing() {
|
||||
let addr_str = "ucxl://alice:admin@myproj:task1/#/docs/readme.md";
|
||||
let addr = UCXLAddress::from_str(addr_str).unwrap();
|
||||
assert_eq!(addr.agent, "alice");
|
||||
assert_eq!(addr.role, Some("admin".to_string()));
|
||||
assert_eq!(addr.project, "myproj");
|
||||
assert_eq!(addr.task, "task1");
|
||||
assert_eq!(addr.temporal, TemporalAxis::Present);
|
||||
assert_eq!(addr.path, "docs/readme.md");
|
||||
assert_eq!(addr.to_string(), addr_str);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metadata_store() {
|
||||
let mut store = InMemoryMetadataStore::new();
|
||||
store.set("/foo.txt", "meta".into());
|
||||
assert_eq!(store.get("/foo.txt"), Some(&"meta".to_string()));
|
||||
store.remove("/foo.txt");
|
||||
assert!(store.get("/foo.txt").is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user