use std::fmt::{Display, Formatter};
use std::str::FromStr;
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct BagRef {
pub namespace: Option<String>,
pub name: String,
pub tags: Vec<String>,
pub attachment: Option<String>,
}
impl BagRef {
pub fn cache_key(&self) -> String {
let mut key = String::new();
if let Some(ns) = &self.namespace {
key.push_str(ns);
key.push('_');
}
key.push_str(&self.name);
if !self.tags.is_empty() {
key.push('_');
key.push_str(&self.tags.join("_"));
}
if let Some(att) = &self.attachment {
key.push('_');
key.push_str(att);
}
key.replace('/', "_")
}
pub fn object_path(&self) -> String {
let mut parts = Vec::new();
if let Some(ns) = &self.namespace {
parts.push(ns.clone());
}
parts.push(self.name.clone());
for t in &self.tags {
parts.push(t.clone());
}
parts.join("/")
}
pub fn with_attachment(&self, attachment: Option<String>) -> Self {
let mut next = self.clone();
next.attachment = attachment;
next
}
pub fn without_attachment(&self) -> Self {
let mut next = self.clone();
next.attachment = None;
next
}
}
impl Display for BagRef {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(ns) = &self.namespace {
write!(f, "{ns}/")?;
}
write!(f, "{}", self.name)?;
for tag in &self.tags {
write!(f, ":{tag}")?;
}
if let Some(att) = &self.attachment {
write!(f, "[{att}]")?;
}
Ok(())
}
}
impl FromStr for BagRef {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let (main, attachment) = if let Some(idx) = s.find('[') {
let close = s
.rfind(']')
.ok_or_else(|| anyhow!("attachment syntax is invalid, missing ']'"))?;
if close < idx {
return Err(anyhow!("attachment syntax is invalid"));
}
let att = s[idx + 1..close].trim();
(
s[..idx].trim(),
if att.is_empty() {
None
} else {
Some(att.to_string())
},
)
} else {
(s.trim(), None)
};
if main.is_empty() {
return Err(anyhow!("bag reference cannot be empty"));
}
let slash_idx = main.find('/');
let (namespace, tail) = match slash_idx {
Some(i) => {
let ns = main[..i].trim();
if ns.is_empty() {
return Err(anyhow!("namespace cannot be empty"));
}
(Some(ns.to_string()), main[i + 1..].trim())
}
None => (None, main),
};
let mut parts = tail.split(':');
let name = parts
.next()
.ok_or_else(|| anyhow!("bag name cannot be empty"))?
.trim();
if name.is_empty() {
return Err(anyhow!("bag name cannot be empty"));
}
let tags = parts
.map(str::trim)
.filter(|v| !v.is_empty())
.map(ToString::to_string)
.collect::<Vec<_>>();
Ok(Self {
namespace,
name: name.to_string(),
tags,
attachment,
})
}
}
#[cfg(test)]
mod tests {
use super::BagRef;
use std::str::FromStr;
#[test]
fn parse_with_namespace_tags_and_attachment() {
let bag = BagRef::from_str("stelzo/dlg_cut:ouster:1min[traj.txt]").unwrap();
assert_eq!(bag.namespace.as_deref(), Some("stelzo"));
assert_eq!(bag.name, "dlg_cut");
assert_eq!(bag.tags, vec!["ouster".to_string(), "1min".to_string()]);
assert_eq!(bag.attachment.as_deref(), Some("traj.txt"));
}
#[test]
fn parse_plain_name() {
let bag = BagRef::from_str("dlg_cut").unwrap();
assert_eq!(bag.namespace, None);
assert_eq!(bag.name, "dlg_cut");
assert!(bag.tags.is_empty());
assert!(bag.attachment.is_none());
}
}