ssdp_client/
search_target.rs

1use crate::error::{ParseSearchTargetError, ParseURNError};
2use std::{borrow::Cow, fmt};
3
4#[derive(Debug, Eq, PartialEq, Clone)]
5/// Specify what SSDP control points to search for
6pub enum SearchTarget {
7    /// Search for all devices and services.
8    All,
9    /// Search for root devices only.
10    RootDevice,
11    /// unique identifier for a device
12    UUID(String),
13    /// e.g. schemas-upnp-org:device:ZonePlayer:1
14    /// or schemas-sonos-com:service:Queue:1
15    URN(URN),
16    /// e.g. roku:ecp
17    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)]
57/// Uniform Resource Name
58///
59/// e.g. `urn:schemas-upnp-org:service:RenderingControl:1`
60pub 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    /// Creates an instance of a device URN
79    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    /// Creates an instance of a service URN
83    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    /// Extracts the `schemas-upnp-org` part of the
88    /// `urn:schemas-upnp-org:service:RenderingControl:1`
89    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    /// Extracts the `RenderingControl` part of the
97    /// `urn:schemas-upnp-org:service:RenderingControl:1`
98    pub fn typ(&self) -> &str {
99        match self {
100            URN::Device(_, typ, _) => typ,
101            URN::Service(_, typ, _) => typ,
102        }
103    }
104
105    /// Extracts the `1` part of the
106    /// `urn:schemas-upnp-org:service:RenderingControl:1`
107    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}