Skip to main content

specfold_core/model/
id.rs

1//! Document identifiers: `<type>:<owner>/<key>`, optionally followed by `#<anchor>`.
2
3use 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    /// Parses `type:owner/key`. The owner may contain dots; the key may not contain `/`.
19    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/// A reference as written in a document or on the command line: an id, an id with an anchor,
43/// an alias, or a path.
44#[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}