urltemplate/
lib.rs

1//!# URLTemplate
2//! Utility that enables URLs with placeholders, i.e. `https://www.mozilla.org/?utm_source={source}&utm_medium={medium}
3//!# Usage
4//! ```
5//! extern crate urltemplate;
6//!
7//! use urltemplate::UrlTemplate;
8//! use std::collections::HashMap;
9//!
10//! let mut params = HashMap::new();
11//! params.insert("source".to_string(), "url-template-crate-❤".to_string());
12//! let url_with_placeholders = UrlTemplate::from("https://www.mozilla.org/?utm_source={source}");
13//! let url =  url_with_placeholders.substitute_str(&params).expect("valid url");
14//! assert_eq!(url, "https://www.mozilla.org/?utm_source=url-template-crate-❤")
15//! ```
16use url::Url;
17
18use std::fmt;
19
20use std::collections::{HashMap};
21use std::error::{self, Error};
22
23use std::ops::Add;
24
25
26
27#[derive(Debug, Clone)]
28pub struct UrlTemplate(pub String);
29
30
31#[derive(Debug)]
32#[derive(PartialEq)]
33pub enum UrlTemplateErrorKind {
34    /// provided String is not an URL
35    IsNotAnUrl,
36    /// provided URL scheme is differ from expected `http` or `https`
37    InvalidScheme,
38    /// provided pattern has incorrect syntax
39    InvalidPattern
40}
41
42
43#[derive(Debug)]
44#[derive(PartialEq)]
45pub struct UrlTemplateError {
46    position: usize,
47    kind: UrlTemplateErrorKind
48}
49
50impl From<(UrlTemplateErrorKind, usize)> for UrlTemplateError {
51    fn from((kind, position): (UrlTemplateErrorKind, usize)) -> UrlTemplateError {
52        UrlTemplateError {
53            kind: kind,
54            position: position
55        }
56    }
57}
58
59impl From<UrlTemplateErrorKind> for UrlTemplateError {
60    fn from(kind: UrlTemplateErrorKind) -> UrlTemplateError {
61        UrlTemplateError {
62            kind: kind,
63            position: 0
64        }
65    }
66}
67
68impl fmt::Display for UrlTemplateError {
69    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
70        match self.kind {
71            UrlTemplateErrorKind::IsNotAnUrl => {
72                write!(f, "Provided pattern is not a valid URL.")
73            }
74            UrlTemplateErrorKind::InvalidScheme => {
75                write!(f, "URL scheme is differ from expected `http` or `https`.")
76            }
77            UrlTemplateErrorKind::InvalidPattern => {
78                write!(f, "The pattern has invalid syntax.")
79            }
80        }
81    }
82}
83
84impl error::Error for UrlTemplateError {
85    fn cause(&self) -> Option<&Error> { None }
86    fn source(&self) -> Option<&(Error + 'static)> { None }
87}
88
89
90impl From<String> for UrlTemplate {
91    fn from(s: String) -> UrlTemplate {
92        UrlTemplate(s)
93    }
94}
95
96impl From<&str> for UrlTemplate {
97    fn from(s: &str) -> UrlTemplate {
98        UrlTemplate(String::from(s))
99    }
100}
101
102impl Into<String> for UrlTemplate {
103    fn into(self) -> String {
104        self.0.clone()
105    }
106}
107
108impl PartialEq for UrlTemplate {
109    fn eq(&self, other: &UrlTemplate) -> bool {
110        let s: String = other.into();
111        self.0 == s
112    }
113}
114
115impl ToString for UrlTemplate {
116    fn to_string(&self) -> String {
117        self.0.clone()
118    }
119}
120
121impl From<&UrlTemplate> for String {
122    fn from(tpl: &UrlTemplate) -> String { tpl.to_string() }
123}
124
125impl UrlTemplate {
126    pub fn substitute(&self, values: &HashMap<String, String>) -> Result<Url, UrlTemplateError> {
127        match self.substitute_str(values) {
128            Ok(url_string) => {
129                Ok(Url::parse(url_string.as_str()).expect("Valid URL string"))
130            }
131            Err(e) => {
132                Err(e)
133            }
134        }
135    }
136
137    pub fn substitute_str(&self, values: &HashMap<String, String>) -> Result<String, UrlTemplateError> {
138        // sanity check
139        match Url::parse(self.0.as_str()) {
140            Ok(parsed) => {
141                let scheme_valid = parsed.scheme() == "http" || parsed.scheme() == "https";
142                if !scheme_valid {
143                    return Err(UrlTemplateError::from(UrlTemplateErrorKind::InvalidScheme));
144                }
145            }
146            _ => {
147                return Err(UrlTemplateError::from(UrlTemplateErrorKind::IsNotAnUrl));
148            }
149        }
150
151        let mut chars = self.0.char_indices();
152        let mut out = String::new();
153
154        let mut current_placeholder = String::new();
155        let mut inside_placeholder = false;
156
157        loop{
158            match chars.next() {
159                None => {
160                    break
161                }
162                Some((charnum, '{')) => {
163                    if inside_placeholder {
164                        return Err(UrlTemplateError::from((UrlTemplateErrorKind::InvalidPattern, charnum)));
165                    }
166                    current_placeholder = String::new();
167                    inside_placeholder = true;
168                }
169                Some((charnum, '}')) => {
170                    if !inside_placeholder {
171                        return Err(UrlTemplateError::from((UrlTemplateErrorKind::InvalidPattern, charnum)));
172                    }
173
174                    match values.get(&current_placeholder) {
175                        Some(s) => {
176                            out = out.add(s);
177                        }
178                        None => {
179                            out = out.add("");
180                        }
181                    }
182                    inside_placeholder = false;
183                }
184                Some((_charnum, ch)) => {
185                    if inside_placeholder {
186                        current_placeholder.push(ch);
187                    } else {
188                        out.push(ch);
189                    }
190                }
191            }
192        }
193
194        if inside_placeholder {
195            return Err(UrlTemplateError::from((UrlTemplateErrorKind::InvalidPattern, self.0.len() - 1)));
196        }
197
198        Ok(out)
199
200    }
201}