1use std::fmt;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5pub enum Scheme {
6 Ca,
7 Pva,
8 Loc,
9 Sim,
10 Formula,
12 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
33pub struct PvName {
34 pub scheme: Scheme,
35 pub pv: String,
37}
38
39impl PvName {
40 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 pub fn base(&self) -> &str {
82 let end = self.pv.find(['<', '(']).unwrap_or(self.pv.len());
83 self.pv[..end].trim()
84 }
85
86 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 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 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
120fn 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
127pub(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 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}