specfold_core/model/
id.rs1use serde::{Serialize, Serializer};
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
7pub struct DocId {
8 pub ty: String,
9 pub owner: String,
10 pub key: String,
11}
12
13impl DocId {
14 pub fn new(ty: &str, owner: &str, key: &str) -> Self {
15 DocId { ty: ty.into(), owner: owner.into(), key: key.into() }
16 }
17
18 pub fn parse(s: &str) -> Option<Self> {
20 let (ty, rest) = s.split_once(':')?;
21 let (owner, key) = rest.rsplit_once('/')?;
22 let valid = |p: &str| !p.is_empty() && p.bytes().all(|b| b.is_ascii_alphanumeric() || b"-._".contains(&b));
23 if !valid(ty) || !valid(owner) || !valid(key) {
24 return None;
25 }
26 Some(DocId::new(ty, owner, key))
27 }
28}
29
30impl fmt::Display for DocId {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 write!(f, "{}:{}/{}", self.ty, self.owner, self.key)
33 }
34}
35
36impl Serialize for DocId {
37 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
38 s.serialize_str(&self.to_string())
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct RefText {
46 pub target: String,
47 pub anchor: Option<String>,
48}
49
50impl RefText {
51 pub fn parse(s: &str) -> Self {
52 let s = s.trim();
53 match s.split_once('#') {
54 Some((t, a)) if !a.is_empty() => RefText { target: t.to_string(), anchor: Some(a.to_string()) },
55 _ => RefText { target: s.trim_end_matches('#').to_string(), anchor: None },
56 }
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn parse_ids() {
66 let id = DocId::parse("adr:core.infra.proxy/20261027073000").unwrap();
67 assert_eq!(id.owner, "core.infra.proxy");
68 assert_eq!(id.to_string(), "adr:core.infra.proxy/20261027073000");
69 assert!(DocId::parse("ADR-7").is_none());
70 assert!(DocId::parse("plan:/x").is_none());
71 let r = RefText::parse("sot:shop/checkout#CO-102");
72 assert_eq!(r.anchor.as_deref(), Some("CO-102"));
73 }
74}