Skip to content

Commit d71214c

Browse files
committed
Add other::SourceMap header
1 parent d8c65ab commit d71214c

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

src/headers/constants.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ pub const SERVER: HeaderName = HeaderName::from_lowercase_str("server");
150150
/// The `Server` Header
151151
pub const SERVER_TIMING: HeaderName = HeaderName::from_lowercase_str("server-timing");
152152

153+
/// The `SourceMap` Header
154+
pub const SOURCE_MAP: HeaderName = HeaderName::from_lowercase_str("sourcemap");
155+
153156
/// The `Te` Header
154157
pub const TE: HeaderName = HeaderName::from_lowercase_str("te");
155158

src/other/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
33
mod date;
44
mod expect;
5+
mod source_map;
56

67
pub use date::Date;
78
pub use expect::Expect;
9+
pub use source_map::SourceMap;

src/other/source_map.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use crate::headers::{HeaderName, HeaderValue, Headers, SOURCE_MAP};
2+
use crate::{bail_status as bail, Status, Url};
3+
4+
use std::convert::TryInto;
5+
6+
/// Links to a file that maps transformed source to the original source.
7+
///
8+
/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/SourceMap)
9+
///
10+
/// # Specifications
11+
///
12+
/// - [Source Map Revision 3](https://sourcemaps.info/spec.html)
13+
///
14+
/// # Examples
15+
///
16+
/// ```
17+
/// # fn main() -> http_types::Result<()> {
18+
/// #
19+
/// use http_types::{Response, Url};
20+
/// use http_types::other::SourceMap;
21+
///
22+
/// let source_map = SourceMap::new(Url::parse("https://example.net/")?);
23+
///
24+
/// let mut res = Response::new(200);
25+
/// source_map.apply(&mut res);
26+
///
27+
/// let base_url = Url::parse("https://example.net/")?;
28+
/// let source_map = SourceMap::from_headers(base_url, res)?.unwrap();
29+
/// assert_eq!(source_map.location(), &Url::parse("https://example.net/")?);
30+
/// #
31+
/// # Ok(()) }
32+
/// ```
33+
#[derive(Debug)]
34+
pub struct SourceMap {
35+
location: Url,
36+
}
37+
38+
impl SourceMap {
39+
/// Create a new instance of `SourceMap` header.
40+
pub fn new(location: Url) -> Self {
41+
Self { location }
42+
}
43+
44+
/// Create a new instance from headers.
45+
pub fn from_headers<U>(base_url: U, headers: impl AsRef<Headers>) -> crate::Result<Option<Self>>
46+
where
47+
U: TryInto<Url>,
48+
U::Error: std::fmt::Debug,
49+
{
50+
let headers = match headers.as_ref().get(SOURCE_MAP) {
51+
Some(headers) => headers,
52+
None => return Ok(None),
53+
};
54+
55+
// If we successfully parsed the header then there's always at least one
56+
// entry. We want the last entry.
57+
let header_value = headers.iter().last().unwrap();
58+
59+
let url = match Url::parse(header_value.as_str()) {
60+
Ok(url) => url,
61+
Err(_) => match base_url.try_into() {
62+
Ok(base_url) => base_url.join(header_value.as_str().trim()).status(400)?,
63+
Err(_) => bail!(400, "Invalid base url provided"),
64+
},
65+
};
66+
67+
Ok(Some(Self { location: url }))
68+
}
69+
70+
/// Sets the header.
71+
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
72+
headers.as_mut().insert(self.name(), self.value());
73+
}
74+
75+
/// Get the `HeaderName`.
76+
pub fn name(&self) -> HeaderName {
77+
SOURCE_MAP
78+
}
79+
80+
/// Get the `HeaderValue`.
81+
pub fn value(&self) -> HeaderValue {
82+
let output = self.location.to_string();
83+
84+
// SAFETY: the internal string is validated to be ASCII.
85+
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
86+
}
87+
88+
/// Get the url.
89+
pub fn location(&self) -> &Url {
90+
&self.location
91+
}
92+
93+
/// Set the url.
94+
pub fn set_location<U>(&mut self, location: U)
95+
where
96+
U: TryInto<Url>,
97+
U::Error: std::fmt::Debug,
98+
{
99+
self.location = location
100+
.try_into()
101+
.expect("Could not convert into valid URL")
102+
}
103+
}
104+
105+
#[cfg(test)]
106+
mod test {
107+
use super::*;
108+
use crate::headers::Headers;
109+
110+
#[test]
111+
fn smoke() -> crate::Result<()> {
112+
let source_map = SourceMap::new(Url::parse("https://example.net/test.json")?);
113+
114+
let mut headers = Headers::new();
115+
source_map.apply(&mut headers);
116+
117+
let base_url = Url::parse("https://example.net/")?;
118+
let source_map = SourceMap::from_headers(base_url, headers)?.unwrap();
119+
assert_eq!(
120+
source_map.location(),
121+
&Url::parse("https://example.net/test.json")?
122+
);
123+
Ok(())
124+
}
125+
126+
#[test]
127+
fn bad_request_on_parse_error() -> crate::Result<()> {
128+
let mut headers = Headers::new();
129+
headers.insert(SOURCE_MAP, "htt://<nori ate the tag. yum.>");
130+
let err = SourceMap::from_headers(Url::parse("https://example.net").unwrap(), headers)
131+
.unwrap_err();
132+
assert_eq!(err.status(), 400);
133+
Ok(())
134+
}
135+
136+
#[test]
137+
fn fallback_works() -> crate::Result<()> {
138+
let mut headers = Headers::new();
139+
headers.insert(SOURCE_MAP, "/test.json");
140+
141+
let base_url = Url::parse("https://fallback.net/")?;
142+
let source_map = SourceMap::from_headers(base_url, headers)?.unwrap();
143+
assert_eq!(
144+
source_map.location(),
145+
&Url::parse("https://fallback.net/test.json")?
146+
);
147+
148+
let mut headers = Headers::new();
149+
headers.insert(SOURCE_MAP, "https://example.com/test.json");
150+
151+
let base_url = Url::parse("https://fallback.net/")?;
152+
let source_map = SourceMap::from_headers(base_url, headers)?.unwrap();
153+
assert_eq!(
154+
source_map.location(),
155+
&Url::parse("https://example.com/test.json")?
156+
);
157+
Ok(())
158+
}
159+
}

0 commit comments

Comments
 (0)