Skip to main content

pv/
name.rs

1use std::fmt;
2
3/// Which backend serves a PV.
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5pub enum Scheme {
6    Ca,
7    Pva,
8    Loc,
9    Sim,
10    /// A formula over other PVs: `=expr` as in BOY, or `eq://expr`.
11    Formula,
12    /// A kind of PV bobview does not serve, such as `sys://` or an unknown
13    /// `scheme://` prefix: it never connects, rather than being looked up
14    /// as a Channel Access name.
15    Unsupported,
16}
17
18impl Scheme {
19    fn prefix(self) -> &'static str {
20        match self {
21            Scheme::Ca => "ca",
22            Scheme::Pva => "pva",
23            Scheme::Loc => "loc",
24            Scheme::Sim => "sim",
25            Scheme::Formula => "eq",
26            Scheme::Unsupported => "",
27        }
28    }
29}
30
31/// A parsed PV name such as `ca://X`, `loc://x<VDouble>(3)` or `sim://sine(0, 10, 0.1)`.
32#[derive(Clone, Debug, PartialEq, Eq, Hash)]
33pub struct PvName {
34    pub scheme: Scheme,
35    /// Everything after the `scheme://` prefix.
36    pub pv: String,
37}
38
39impl PvName {
40    /// Parse a name; one without a prefix is Channel Access, as in Phoebus,
41    /// and one starting `=` is a formula, as in BOY. An unknown `scheme://`
42    /// prefix is [`Scheme::Unsupported`].
43    pub fn parse(name: &str) -> Self {
44        let name = name.trim();
45        if let Some(expr) = name.strip_prefix('=') {
46            return Self {
47                scheme: Scheme::Formula,
48                pv: expr.trim().to_string(),
49            };
50        }
51        for scheme in [
52            Scheme::Ca,
53            Scheme::Pva,
54            Scheme::Loc,
55            Scheme::Sim,
56            Scheme::Formula,
57        ] {
58            if let Some(rest) = name
59                .strip_prefix(scheme.prefix())
60                .and_then(|r| r.strip_prefix("://"))
61            {
62                return Self {
63                    scheme,
64                    pv: rest.trim().to_string(),
65                };
66            }
67        }
68        if name.split_once("://").is_some_and(|(p, _)| is_scheme(p)) {
69            return Self {
70                scheme: Scheme::Unsupported,
71                pv: name.to_string(),
72            };
73        }
74        Self {
75            scheme: Scheme::Ca,
76            pv: name.to_string(),
77        }
78    }
79
80    /// The part before any `<type>` or `(arguments)`.
81    pub fn base(&self) -> &str {
82        let end = self.pv.find(['<', '(']).unwrap_or(self.pv.len());
83        self.pv[..end].trim()
84    }
85
86    /// The `<VType>` of a local PV, if given.
87    pub fn vtype(&self) -> Option<&str> {
88        let start = self.pv.find('<')?;
89        let end = self.pv[start..].find('>')? + start;
90        Some(self.pv[start + 1..end].trim())
91    }
92
93    /// The raw text inside the outer parentheses, if any.
94    pub fn args(&self) -> Option<&str> {
95        let start = self.pv.find('(')?;
96        let end = self.pv.rfind(')')?;
97        (end > start).then(|| self.pv[start + 1..end].trim())
98    }
99
100    /// Identifies a subscription: local PVs share state by base name alone.
101    pub fn key(&self) -> String {
102        match self.scheme {
103            Scheme::Loc => format!("loc://{}", self.base()),
104            Scheme::Formula | Scheme::Unsupported => self.to_string(),
105            _ => format!("{}://{}", self.scheme.prefix(), self.pv),
106        }
107    }
108}
109
110impl fmt::Display for PvName {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self.scheme {
113            Scheme::Formula => write!(f, "={}", self.pv),
114            Scheme::Unsupported => f.write_str(&self.pv),
115            _ => write!(f, "{}://{}", self.scheme.prefix(), self.pv),
116        }
117    }
118}
119
120/// An RFC 3986 scheme: a letter, then letters, digits, `+`, `-` or `.`.
121fn is_scheme(p: &str) -> bool {
122    p.starts_with(|c: char| c.is_ascii_alphabetic())
123        && p.chars()
124            .all(|c| c.is_ascii_alphanumeric() || "+-.".contains(c))
125}
126
127/// Split an argument list on commas that are outside double quotes.
128pub(crate) fn split_args(args: &str) -> Vec<String> {
129    let mut out = Vec::new();
130    let mut cur = String::new();
131    let mut quoted = false;
132    for c in args.chars() {
133        match c {
134            '"' => {
135                quoted = !quoted;
136                cur.push(c);
137            }
138            ',' if !quoted => out.push(std::mem::take(&mut cur).trim().to_string()),
139            _ => cur.push(c),
140        }
141    }
142    if !cur.trim().is_empty() || !out.is_empty() {
143        out.push(cur.trim().to_string());
144    }
145    out
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn default_scheme_is_ca() {
154        let n = PvName::parse("bl01t:SUM");
155        assert_eq!(n.scheme, Scheme::Ca);
156        assert_eq!(n.key(), PvName::parse("ca://bl01t:SUM").key());
157    }
158
159    #[test]
160    fn local_parts() {
161        let n = PvName::parse("loc://x<VEnum>(1, \"a\", \"b,c\")");
162        assert_eq!(n.scheme, Scheme::Loc);
163        assert_eq!(n.base(), "x");
164        assert_eq!(n.vtype(), Some("VEnum"));
165        assert_eq!(split_args(n.args().unwrap()), vec!["1", "\"a\"", "\"b,c\""]);
166        assert_eq!(n.key(), "loc://x");
167    }
168
169    #[test]
170    fn sim_args() {
171        let n = PvName::parse("sim://sine(0, 10, 0.5)");
172        assert_eq!(n.base(), "sine");
173        assert_eq!(split_args(n.args().unwrap()), vec!["0", "10", "0.5"]);
174        assert!(PvName::parse("sim://ramp").args().is_none());
175    }
176
177    #[test]
178    fn formulas() {
179        let n = PvName::parse(" =(pv(\"X\")*1000)");
180        assert_eq!(n.scheme, Scheme::Formula);
181        assert_eq!(n.pv, "(pv(\"X\")*1000)");
182        assert_eq!(n.to_string(), "=(pv(\"X\")*1000)");
183        assert_eq!(n.key(), PvName::parse("eq://(pv(\"X\")*1000)").key());
184    }
185
186    #[test]
187    fn unknown_schemes_are_unsupported() {
188        for name in ["sys://time", "mqtt://x/y", "mqtt+ssl://x"] {
189            let n = PvName::parse(name);
190            assert_eq!(n.scheme, Scheme::Unsupported, "{name}");
191            assert_eq!(n.to_string(), name);
192        }
193        // A colon in a CA name is not a scheme.
194        assert_eq!(PvName::parse("BL01:A://B").scheme, Scheme::Ca);
195        assert_eq!(PvName::parse("BL01T-MO-01:X").scheme, Scheme::Ca);
196        assert_eq!(PvName::parse("pva://X").scheme, Scheme::Pva);
197        assert_eq!(PvName::parse("eq://1").scheme, Scheme::Formula);
198    }
199}