Skip to content

Commit ef5d1c8

Browse files
authored
Merge pull request #278 from http-rs/sourcemap
Add `other::SourceMap` header
2 parents 9bafdd6 + eb40bac commit ef5d1c8

File tree

3 files changed

+163
-0
lines changed

3 files changed

+163
-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: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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!(500, "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) -> Result<(), U::Error>
95+
where
96+
U: TryInto<Url>,
97+
U::Error: std::fmt::Debug,
98+
{
99+
self.location = location.try_into()?;
100+
Ok(())
101+
}
102+
}
103+
104+
#[cfg(test)]
105+
mod test {
106+
use super::*;
107+
use crate::headers::Headers;
108+
109+
#[test]
110+
fn smoke() -> crate::Result<()> {
111+
let source_map = SourceMap::new(Url::parse("https://example.net/test.json")?);
112+
113+
let mut headers = Headers::new();
114+
source_map.apply(&mut headers);
115+
116+
let base_url = Url::parse("https://example.net/")?;
117+
let source_map = SourceMap::from_headers(base_url, headers)?.unwrap();
118+
assert_eq!(
119+
source_map.location(),
120+
&Url::parse("https://example.net/test.json")?
121+
);
122+
Ok(())
123+
}
124+
125+
#[test]
126+
fn bad_request_on_parse_error() -> crate::Result<()> {
127+
let mut headers = Headers::new();
128+
headers.insert(SOURCE_MAP, "htt://<nori ate the tag. yum.>");
129+
let err = SourceMap::from_headers(Url::parse("https://example.net").unwrap(), headers)
130+
.unwrap_err();
131+
assert_eq!(err.status(), 400);
132+
Ok(())
133+
}
134+
135+
#[test]
136+
fn fallback_works() -> crate::Result<()> {
137+
let mut headers = Headers::new();
138+
headers.insert(SOURCE_MAP, "/test.json");
139+
140+
let base_url = Url::parse("https://fallback.net/")?;
141+
let source_map = SourceMap::from_headers(base_url, headers)?.unwrap();
142+
assert_eq!(
143+
source_map.location(),
144+
&Url::parse("https://fallback.net/test.json")?
145+
);
146+
147+
let mut headers = Headers::new();
148+
headers.insert(SOURCE_MAP, "https://example.com/test.json");
149+
150+
let base_url = Url::parse("https://fallback.net/")?;
151+
let source_map = SourceMap::from_headers(base_url, headers)?.unwrap();
152+
assert_eq!(
153+
source_map.location(),
154+
&Url::parse("https://example.com/test.json")?
155+
);
156+
Ok(())
157+
}
158+
}

0 commit comments

Comments
 (0)