1use crate::error::{ParseSearchTargetError, ParseURNError};
2use std::{borrow::Cow, fmt};
3
4#[derive(Debug, Eq, PartialEq, Clone)]
5pub enum SearchTarget {
7 All,
9 RootDevice,
11 UUID(String),
13 URN(URN),
16 Custom(String, String),
18}
19impl fmt::Display for SearchTarget {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 SearchTarget::All => write!(f, "ssdp:all"),
23 SearchTarget::RootDevice => write!(f, "upnp:rootdevice"),
24 SearchTarget::UUID(uuid) => write!(f, "uuid:{}", uuid),
25 SearchTarget::URN(urn) => write!(f, "{}", urn),
26 SearchTarget::Custom(key, value) => write!(f, "{}:{}", key, value),
27 }
28 }
29}
30
31impl std::str::FromStr for SearchTarget {
32 type Err = ParseSearchTargetError;
33
34 fn from_str(s: &str) -> Result<Self, Self::Err> {
35 Ok(match s {
36 "ssdp:all" => SearchTarget::All,
37 "upnp:rootdevice" => SearchTarget::RootDevice,
38 s if s.starts_with("uuid") => {
39 SearchTarget::UUID(s.trim_start_matches("uuid:").to_string())
40 }
41 s if s.starts_with("urn") => URN::from_str(s)
42 .map(SearchTarget::URN)
43 .map_err(ParseSearchTargetError::URN)?,
44 s => {
45 let split: Vec<&str> = s.split(":").collect();
46 if split.len() != 2 {
47 return Err(ParseSearchTargetError::ST);
48 }
49 SearchTarget::Custom(split[0].into(), split[1].into())
50 }
51 })
52 }
53}
54
55#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
56#[allow(missing_docs)]
57pub enum URN {
61 Device(Cow<'static, str>, Cow<'static, str>, u32),
62 Service(Cow<'static, str>, Cow<'static, str>, u32),
63}
64impl fmt::Display for URN {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 URN::Device(domain, typ, version) => {
68 write!(f, "urn:{}:device:{}:{}", domain, typ, version)
69 }
70 URN::Service(domain, typ, version) => {
71 write!(f, "urn:{}:service:{}:{}", domain, typ, version)
72 }
73 }
74 }
75}
76
77impl URN {
78 pub const fn device(domain: &'static str, typ: &'static str, version: u32) -> Self {
80 URN::Device(Cow::Borrowed(domain), Cow::Borrowed(typ), version)
81 }
82 pub const fn service(domain: &'static str, typ: &'static str, version: u32) -> Self {
84 URN::Service(Cow::Borrowed(domain), Cow::Borrowed(typ), version)
85 }
86
87 pub fn domain_name(&self) -> &str {
90 match self {
91 URN::Device(domain_name, _, _) => domain_name,
92 URN::Service(domain_name, _, _) => domain_name,
93 }
94 }
95
96 pub fn typ(&self) -> &str {
99 match self {
100 URN::Device(_, typ, _) => typ,
101 URN::Service(_, typ, _) => typ,
102 }
103 }
104
105 pub fn version(&self) -> u32 {
108 match self {
109 URN::Device(_, _, v) => *v,
110 URN::Service(_, _, v) => *v,
111 }
112 }
113}
114
115impl Into<SearchTarget> for URN {
116 fn into(self) -> SearchTarget {
117 SearchTarget::URN(self)
118 }
119}
120
121impl std::str::FromStr for URN {
122 type Err = ParseURNError;
123 fn from_str(str: &str) -> Result<Self, Self::Err> {
124 let mut iter = str.split(':');
125 if iter.next() != Some("urn") {
126 return Err(ParseURNError);
127 }
128
129 let domain = iter.next().ok_or(ParseURNError)?.to_string().into();
130 let urn_type = &iter.next().ok_or(ParseURNError)?;
131 let typ = iter.next().ok_or(ParseURNError)?.to_string().into();
132 let version = iter
133 .next()
134 .ok_or(ParseURNError)?
135 .parse::<u32>()
136 .map_err(|_| ParseURNError)?;
137
138 if iter.next() != None {
139 return Err(ParseURNError);
140 }
141
142 if urn_type.eq_ignore_ascii_case("service") {
143 Ok(URN::Service(domain, typ, version))
144 } else if urn_type.eq_ignore_ascii_case("device") {
145 Ok(URN::Device(domain, typ, version))
146 } else {
147 Err(ParseURNError)
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::{SearchTarget, URN};
155
156 #[test]
157 fn parse_search_target() {
158 assert_eq!("ssdp:all".parse(), Ok(SearchTarget::All));
159 assert_eq!("upnp:rootdevice".parse(), Ok(SearchTarget::RootDevice));
160
161 assert_eq!(
162 "uuid:some-uuid".parse(),
163 Ok(SearchTarget::UUID("some-uuid".to_string()))
164 );
165
166 assert_eq!(
167 "urn:schemas-upnp-org:device:ZonePlayer:1".parse(),
168 Ok(SearchTarget::URN(URN::Device(
169 "schemas-upnp-org".into(),
170 "ZonePlayer".into(),
171 1
172 )))
173 );
174 assert_eq!(
175 "urn:schemas-sonos-com:service:Queue:2".parse(),
176 Ok(SearchTarget::URN(URN::Service(
177 "schemas-sonos-com".into(),
178 "Queue".into(),
179 2
180 )))
181 );
182 assert_eq!(
183 "roku:ecp".parse(),
184 Ok(SearchTarget::Custom("roku".into(), "ecp".into()))
185 );
186 }
187}