Skip to main content

ntex/web/
rmap.rs

1use std::{cell::RefCell, rc::Rc};
2
3use urly::{Url, quoting::Component, quoting::quote};
4
5use crate::router::ResourceDef;
6use crate::util::HashMap;
7use crate::web::httprequest::HttpRequest;
8
9/// Map of registered resources, used for URL generation.
10///
11/// The map is built from the application's resource definitions and is used
12/// by [`HttpRequest::url_for()`](crate::web::HttpRequest::url_for) to resolve
13/// named and external resources.
14#[derive(Clone, Debug)]
15pub struct ResourceMap {
16    #[allow(dead_code)]
17    root: ResourceDef,
18    parent: RefCell<Option<Rc<ResourceMap>>>,
19    named: HashMap<String, ResourceDef>,
20    patterns: Vec<(ResourceDef, Option<Rc<ResourceMap>>)>,
21}
22
23impl ResourceMap {
24    /// Create an empty resource map for the `root` resource.
25    pub fn new(root: ResourceDef) -> Self {
26        ResourceMap {
27            root,
28            parent: RefCell::new(None),
29            named: HashMap::default(),
30            patterns: Vec::new(),
31        }
32    }
33
34    /// Add a resource pattern, optionally with the resource map of a nested
35    /// scope.
36    ///
37    /// Assigns the pattern its id within this map. Named patterns can be used
38    /// for URL generation.
39    pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option<Rc<ResourceMap>>) {
40        pattern.set_id(self.patterns.len() as u16);
41        self.patterns.push((pattern.clone(), nested));
42        if !pattern.name().is_empty() {
43            self.named
44                .insert(pattern.name().to_string(), pattern.clone());
45        }
46    }
47
48    pub(crate) fn build(&self, current: &Rc<ResourceMap>) {
49        for (_, nested) in &self.patterns {
50            if let Some(nested) = nested {
51                *nested.parent.borrow_mut() = Some(current.clone());
52                nested.build(nested);
53            }
54        }
55    }
56}
57
58impl ResourceMap {
59    /// Generate url for named resource
60    ///
61    /// Check [`HttpRequest::url_for()`](crate::web::HttpRequest::url_for) for detailed information.
62    pub fn url_for<U, I>(
63        &self,
64        req: &HttpRequest,
65        name: &str,
66        elements: U,
67    ) -> Result<Url, super::error::UrlGenerationError>
68    where
69        U: IntoIterator<Item = I>,
70        I: AsRef<str>,
71    {
72        let mut path = String::new();
73        let mut elements = elements
74            .into_iter()
75            .map(|element| quote(element.as_ref(), Component::Opaque).into_owned());
76
77        if self.patterns_for(name, &mut path, &mut elements)?.is_some() {
78            if path.starts_with('/') {
79                let conn = req.connection_info();
80                let (scheme, host) = (conn.scheme(), conn.host());
81                let mut url = String::with_capacity(scheme.len() + host.len() + path.len() + 3);
82                url.push_str(scheme);
83                url.push_str("://");
84                url.push_str(host);
85                url.push_str(&path);
86                path = url;
87            }
88            Ok(Url::try_from(path)?)
89        } else {
90            Err(super::error::UrlGenerationError::ResourceNotFound)
91        }
92    }
93
94    // pub fn has_resource(&self, path: &str) -> bool {
95    // let _path = if path.is_empty() { "/" } else { path };
96
97    // for (pattern, rmap) in &self.patterns {
98    //     if let Some(ref rmap) = rmap {
99    //         if let Some(plen) = pattern.is_prefix_match(path) {
100    //             return rmap.has_resource(&path[plen..]);
101    //         }
102    //     } else if pattern.is_match(path) {
103    //         return true;
104    //     }
105    // }
106    // false
107    // }
108
109    fn patterns_for<U, I>(
110        &self,
111        name: &str,
112        path: &mut String,
113        elements: &mut U,
114    ) -> Result<Option<()>, super::error::UrlGenerationError>
115    where
116        U: Iterator<Item = I>,
117        I: AsRef<str>,
118    {
119        if self.pattern_for(name, path, elements)?.is_some() {
120            Ok(Some(()))
121        } else {
122            self.parent_pattern_for(name, path, elements)
123        }
124    }
125
126    fn pattern_for<U, I>(
127        &self,
128        name: &str,
129        path: &mut String,
130        elements: &mut U,
131    ) -> Result<Option<()>, super::error::UrlGenerationError>
132    where
133        U: Iterator<Item = I>,
134        I: AsRef<str>,
135    {
136        if let Some(pattern) = self.named.get(name) {
137            if pattern.pattern().starts_with('/') {
138                self.fill_root(path, elements)?;
139            }
140            if pattern.build_path(path, elements) {
141                Ok(Some(()))
142            } else {
143                Err(super::error::UrlGenerationError::NotEnoughElements)
144            }
145        } else {
146            for (_, rmap) in &self.patterns {
147                if let Some(rmap) = rmap
148                    && rmap.pattern_for(name, path, elements)?.is_some()
149                {
150                    return Ok(Some(()));
151                }
152            }
153            Ok(None)
154        }
155    }
156
157    fn fill_root<U, I>(
158        &self,
159        path: &mut String,
160        elements: &mut U,
161    ) -> Result<(), super::error::UrlGenerationError>
162    where
163        U: Iterator<Item = I>,
164        I: AsRef<str>,
165    {
166        if let Some(ref parent) = *self.parent.borrow() {
167            parent.fill_root(path, elements)?;
168        }
169        if self.root.build_path(path, elements) {
170            Ok(())
171        } else {
172            Err(super::error::UrlGenerationError::NotEnoughElements)
173        }
174    }
175
176    fn parent_pattern_for<U, I>(
177        &self,
178        name: &str,
179        path: &mut String,
180        elements: &mut U,
181    ) -> Result<Option<()>, super::error::UrlGenerationError>
182    where
183        U: Iterator<Item = I>,
184        I: AsRef<str>,
185    {
186        if let Some(ref parent) = *self.parent.borrow() {
187            parent.patterns_for(name, path, elements)
188        } else {
189            Ok(None)
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::web::test::TestRequest;
198
199    #[test]
200    fn url_for_parent() {
201        // regression: names of the parent map were prefixed with the nested root
202        let mut root = ResourceMap::new(ResourceDef::new(""));
203        let mut index = ResourceDef::new("/index/{id}");
204        index.set_name("index");
205        root.add(&mut index, None);
206        let mut ext = ResourceDef::new("https://youtube.com/watch/{id}");
207        ext.set_name("youtube");
208        root.add(&mut ext, None);
209
210        let mut nested = ResourceMap::new(ResourceDef::root_prefix("/a"));
211        let mut res = ResourceDef::new("/{id}");
212        res.set_name("nested");
213        nested.add(&mut res, None);
214        let nested = Rc::new(nested);
215        root.add(&mut ResourceDef::root_prefix("/a"), Some(nested.clone()));
216
217        let mut sibling = ResourceMap::new(ResourceDef::root_prefix("/b"));
218        let mut res = ResourceDef::new("/{id}");
219        res.set_name("sibling");
220        sibling.add(&mut res, None);
221        root.add(&mut ResourceDef::root_prefix("/b"), Some(Rc::new(sibling)));
222
223        let root = Rc::new(root);
224        root.build(&root);
225
226        let req = TestRequest::default().to_http_request();
227        for rmap in [&root, &nested] {
228            assert_eq!(
229                rmap.url_for(&req, "index", ["1"]).unwrap().as_str(),
230                "http://localhost:8080/index/1"
231            );
232            assert_eq!(
233                rmap.url_for(&req, "youtube", ["2"]).unwrap().as_str(),
234                "https://youtube.com/watch/2"
235            );
236            assert_eq!(
237                rmap.url_for(&req, "nested", ["3"]).unwrap().as_str(),
238                "http://localhost:8080/a/3"
239            );
240            assert_eq!(
241                rmap.url_for(&req, "sibling", ["4"]).unwrap().as_str(),
242                "http://localhost:8080/b/4"
243            );
244            assert_eq!(
245                rmap.url_for(&req, "index", ["a/b?c#d%"]).unwrap().as_str(),
246                "http://localhost:8080/index/a%2Fb%3Fc%23d%25"
247            );
248            assert_eq!(
249                rmap.url_for(&req, "youtube", ["a/b?c#d%"])
250                    .unwrap()
251                    .as_str(),
252                "https://youtube.com/watch/a%2Fb%3Fc%23d%25"
253            );
254            assert!(rmap.url_for(&req, "index", [""; 0]).is_err());
255            assert!(rmap.url_for(&req, "unknown", [""; 0]).is_err());
256        }
257    }
258}