1#![deny(missing_docs)]
2
3//! Provides a type for Jabber IDs.
4//!
5//! For usage, check the documentation on the `Jid` struct.
6
7use std::fmt;
8
9use std::convert::Into;
10
11use std::str::FromStr;
12
13/// An error that signifies that a `Jid` cannot be parsed from a string.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum JidParseError {
16 /// Happens when there is no domain, that is either the string is empty,
17 /// starts with a /, or contains the @/ sequence.
18 NoDomain,
19 /// Happens when the node is empty, that is the string starts with a @.
20 EmptyNode,
21 /// Happens when the resource is empty, that is the string ends with a /.
22 EmptyResource,
23}
24
25/// A struct representing a Jabber ID.
26///
27/// A Jabber ID is composed of 3 components, of which 2 are optional:
28///
29/// - A node/name, `node`, which is the optional part before the @.
30/// - A domain, `domain`, which is the mandatory part after the @ but before the /.
31/// - A resource, `resource`, which is the optional part after the /.
32#[derive(Clone, PartialEq, Eq, Hash)]
33pub struct Jid {
34 /// The node part of the Jabber ID, if it exists, else None.
35 pub node: Option<String>,
36 /// The domain of the Jabber ID.
37 pub domain: String,
38 /// The resource of the Jabber ID, if it exists, else None.
39 pub resource: Option<String>,
40}
41
42impl From<Jid> for String {
43 fn from(jid: Jid) -> String {
44 let mut string = String::new();
45 if let Some(ref node) = jid.node {
46 string.push_str(node);
47 string.push('@');
48 }
49 string.push_str(&jid.domain);
50 if let Some(ref resource) = jid.resource {
51 string.push('/');
52 string.push_str(resource);
53 }
54 string
55 }
56}
57
58impl fmt::Debug for Jid {
59 fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
60 write!(fmt, "JID({})", self)?;
61 Ok(())
62 }
63}
64
65impl fmt::Display for Jid {
66 fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
67 fmt.write_str(String::from(self.clone()).as_ref())?;
68 Ok(())
69 }
70}
71
72enum ParserState {
73 Node,
74 Domain,
75 Resource
76}
77
78impl FromStr for Jid {
79 type Err = JidParseError;
80
81 fn from_str(s: &str) -> Result<Jid, JidParseError> {
82 // TODO: very naive, may need to do it differently
83 let iter = s.chars();
84 let mut buf = String::with_capacity(s.len());
85 let mut state = ParserState::Node;
86 let mut node = None;
87 let mut domain = None;
88 let mut resource = None;
89 for c in iter {
90 match state {
91 ParserState::Node => {
92 match c {
93 '@' => {
94 if buf == "" {
95 return Err(JidParseError::EmptyNode);
96 }
97 state = ParserState::Domain;
98 node = Some(buf.clone()); // TODO: performance tweaks, do not need to copy it
99 buf.clear();
100 },
101 '/' => {
102 if buf == "" {
103 return Err(JidParseError::NoDomain);
104 }
105 state = ParserState::Resource;
106 domain = Some(buf.clone()); // TODO: performance tweaks
107 buf.clear();
108 },
109 c => {
110 buf.push(c);
111 },
112 }
113 },
114 ParserState::Domain => {
115 match c {
116 '/' => {
117 if buf == "" {
118 return Err(JidParseError::NoDomain);
119 }
120 state = ParserState::Resource;
121 domain = Some(buf.clone()); // TODO: performance tweaks
122 buf.clear();
123 },
124 c => {
125 buf.push(c);
126 },
127 }
128 },
129 ParserState::Resource => {
130 buf.push(c);
131 },
132 }
133 }
134 if !buf.is_empty() {
135 match state {
136 ParserState::Node => {
137 domain = Some(buf);
138 },
139 ParserState::Domain => {
140 domain = Some(buf);
141 },
142 ParserState::Resource => {
143 resource = Some(buf);
144 },
145 }
146 } else if let ParserState::Resource = state {
147 return Err(JidParseError::EmptyResource);
148 }
149 Ok(Jid {
150 node: node,
151 domain: domain.ok_or(JidParseError::NoDomain)?,
152 resource: resource,
153 })
154 }
155}
156
157impl Jid {
158 /// Constructs a Jabber ID containing all three components.
159 ///
160 /// This is of the form `node`@`domain`/`resource`.
161 ///
162 /// # Examples
163 ///
164 /// ```
165 /// use jid::Jid;
166 ///
167 /// let jid = Jid::full("node", "domain", "resource");
168 ///
169 /// assert_eq!(jid.node, Some("node".to_owned()));
170 /// assert_eq!(jid.domain, "domain".to_owned());
171 /// assert_eq!(jid.resource, Some("resource".to_owned()));
172 /// ```
173 pub fn full<NS, DS, RS>(node: NS, domain: DS, resource: RS) -> Jid
174 where NS: Into<String>
175 , DS: Into<String>
176 , RS: Into<String> {
177 Jid {
178 node: Some(node.into()),
179 domain: domain.into(),
180 resource: Some(resource.into()),
181 }
182 }
183
184 /// Constructs a Jabber ID containing only the `node` and `domain` components.
185 ///
186 /// This is of the form `node`@`domain`.
187 ///
188 /// # Examples
189 ///
190 /// ```
191 /// use jid::Jid;
192 ///
193 /// let jid = Jid::bare("node", "domain");
194 ///
195 /// assert_eq!(jid.node, Some("node".to_owned()));
196 /// assert_eq!(jid.domain, "domain".to_owned());
197 /// assert_eq!(jid.resource, None);
198 /// ```
199 pub fn bare<NS, DS>(node: NS, domain: DS) -> Jid
200 where NS: Into<String>
201 , DS: Into<String> {
202 Jid {
203 node: Some(node.into()),
204 domain: domain.into(),
205 resource: None,
206 }
207 }
208
209 /// Returns a new Jabber ID from the current one with only node and domain.
210 ///
211 /// This is of the form `node`@`domain`.
212 ///
213 /// # Examples
214 ///
215 /// ```
216 /// use jid::Jid;
217 ///
218 /// let jid = Jid::full("node", "domain", "resource").into_bare_jid();
219 ///
220 /// assert_eq!(jid.node, Some("node".to_owned()));
221 /// assert_eq!(jid.domain, "domain".to_owned());
222 /// assert_eq!(jid.resource, None);
223 /// ```
224 pub fn into_bare_jid(self) -> Jid {
225 Jid {
226 node: self.node,
227 domain: self.domain,
228 resource: None,
229 }
230 }
231
232 /// Constructs a Jabber ID containing only a `domain`.
233 ///
234 /// This is of the form `domain`.
235 ///
236 /// # Examples
237 ///
238 /// ```
239 /// use jid::Jid;
240 ///
241 /// let jid = Jid::domain("domain");
242 ///
243 /// assert_eq!(jid.node, None);
244 /// assert_eq!(jid.domain, "domain".to_owned());
245 /// assert_eq!(jid.resource, None);
246 /// ```
247 pub fn domain<DS>(domain: DS) -> Jid
248 where DS: Into<String> {
249 Jid {
250 node: None,
251 domain: domain.into(),
252 resource: None,
253 }
254 }
255
256 /// Returns a new Jabber ID from the current one with only domain.
257 ///
258 /// This is of the form `domain`.
259 ///
260 /// # Examples
261 ///
262 /// ```
263 /// use jid::Jid;
264 ///
265 /// let jid = Jid::full("node", "domain", "resource").into_domain_jid();
266 ///
267 /// assert_eq!(jid.node, None);
268 /// assert_eq!(jid.domain, "domain".to_owned());
269 /// assert_eq!(jid.resource, None);
270 /// ```
271 pub fn into_domain_jid(self) -> Jid {
272 Jid {
273 node: None,
274 domain: self.domain,
275 resource: None,
276 }
277 }
278
279 /// Constructs a Jabber ID containing the `domain` and `resource` components.
280 ///
281 /// This is of the form `domain`/`resource`.
282 ///
283 /// # Examples
284 ///
285 /// ```
286 /// use jid::Jid;
287 ///
288 /// let jid = Jid::domain_with_resource("domain", "resource");
289 ///
290 /// assert_eq!(jid.node, None);
291 /// assert_eq!(jid.domain, "domain".to_owned());
292 /// assert_eq!(jid.resource, Some("resource".to_owned()));
293 /// ```
294 pub fn domain_with_resource<DS, RS>(domain: DS, resource: RS) -> Jid
295 where DS: Into<String>
296 , RS: Into<String> {
297 Jid {
298 node: None,
299 domain: domain.into(),
300 resource: Some(resource.into()),
301 }
302 }
303
304 /// Constructs a new Jabber ID from an existing one, with the node swapped out with a new one.
305 ///
306 /// # Examples
307 ///
308 /// ```
309 /// use jid::Jid;
310 ///
311 /// let jid = Jid::domain("domain");
312 ///
313 /// assert_eq!(jid.node, None);
314 ///
315 /// let new_jid = jid.with_node("node");
316 ///
317 /// assert_eq!(new_jid.node, Some("node".to_owned()));
318 /// ```
319 pub fn with_node<S>(&self, node: S) -> Jid
320 where S: Into<String> {
321 Jid {
322 node: Some(node.into()),
323 domain: self.domain.clone(),
324 resource: self.resource.clone(),
325 }
326 }
327
328 /// Constructs a new Jabber ID from an existing one, with the domain swapped out with a new one.
329 ///
330 /// # Examples
331 ///
332 /// ```
333 /// use jid::Jid;
334 ///
335 /// let jid = Jid::domain("domain");
336 ///
337 /// assert_eq!(jid.domain, "domain");
338 ///
339 /// let new_jid = jid.with_domain("new_domain");
340 ///
341 /// assert_eq!(new_jid.domain, "new_domain");
342 /// ```
343 pub fn with_domain<S>(&self, domain: S) -> Jid
344 where S: Into<String> {
345 Jid {
346 node: self.node.clone(),
347 domain: domain.into(),
348 resource: self.resource.clone(),
349 }
350 }
351
352 /// Constructs a new Jabber ID from an existing one, with the resource swapped out with a new one.
353 ///
354 /// # Examples
355 ///
356 /// ```
357 /// use jid::Jid;
358 ///
359 /// let jid = Jid::domain("domain");
360 ///
361 /// assert_eq!(jid.resource, None);
362 ///
363 /// let new_jid = jid.with_resource("resource");
364 ///
365 /// assert_eq!(new_jid.resource, Some("resource".to_owned()));
366 /// ```
367 pub fn with_resource<S>(&self, resource: S) -> Jid
368 where S: Into<String> {
369 Jid {
370 node: self.node.clone(),
371 domain: self.domain.clone(),
372 resource: Some(resource.into()),
373 }
374 }
375
376}
377
378#[cfg(feature = "minidom")]
379extern crate minidom;
380
381#[cfg(feature = "minidom")]
382use minidom::{IntoAttributeValue, IntoElements, ElementEmitter};
383
384#[cfg(feature = "minidom")]
385impl IntoAttributeValue for Jid {
386 fn into_attribute_value(self) -> Option<String> {
387 Some(String::from(self))
388 }
389}
390
391#[cfg(feature = "minidom")]
392impl IntoElements for Jid {
393 fn into_elements(self, emitter: &mut ElementEmitter) {
394 emitter.append_text_node(String::from(self))
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 use std::str::FromStr;
403
404 #[test]
405 fn can_parse_jids() {
406 assert_eq!(Jid::from_str("a@b.c/d"), Ok(Jid::full("a", "b.c", "d")));
407 assert_eq!(Jid::from_str("a@b.c"), Ok(Jid::bare("a", "b.c")));
408 assert_eq!(Jid::from_str("b.c"), Ok(Jid::domain("b.c")));
409
410 assert_eq!(Jid::from_str(""), Err(JidParseError::NoDomain));
411
412 assert_eq!(Jid::from_str("a/b@c"), Ok(Jid::domain_with_resource("a", "b@c")));
413 }
414
415 #[test]
416 fn serialise() {
417 assert_eq!(String::from(Jid::full("a", "b", "c")), String::from("a@b/c"));
418 }
419
420 #[test]
421 fn invalid() {
422 match Jid::from_str("") {
423 Err(JidParseError::NoDomain) => (),
424 err => panic!("Invalid error: {:?}", err)
425 }
426
427 match Jid::from_str("a@/c") {
428 Err(JidParseError::NoDomain) => (),
429 err => panic!("Invalid error: {:?}", err)
430 }
431
432 match Jid::from_str("/c") {
433 Err(JidParseError::NoDomain) => (),
434 err => panic!("Invalid error: {:?}", err)
435 }
436
437 match Jid::from_str("@b") {
438 Err(JidParseError::EmptyNode) => (),
439 err => panic!("Invalid error: {:?}", err)
440 }
441
442 match Jid::from_str("b/") {
443 Err(JidParseError::EmptyResource) => (),
444 err => panic!("Invalid error: {:?}", err)
445 }
446 }
447
448 #[cfg(feature = "minidom")]
449 #[test]
450 fn minidom() {
451 let elem: minidom::Element = "<message from='a@b/c'/>".parse().unwrap();
452 let to: Jid = elem.attr("from").unwrap().parse().unwrap();
453 assert_eq!(to, Jid::full("a", "b", "c"));
454 }
455}