lib.rs

  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}