diff --git a/Cargo.toml b/Cargo.toml index c2c1e009a..f561dc580 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ test = false [dev-dependencies] rustc-test = "0.1" rustc-serialize = "0.3" +serde_json = ">=0.6.1, <0.9" [features] query_encoding = ["encoding"] diff --git a/Makefile b/Makefile index c9592e735..2042654f4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ test: - cargo test --features "query_encoding serde rustc-serialize" - [ x$$TRAVIS_RUST_VERSION != xnightly ] || cargo test --features heapsize + cargo test --features "query_encoding serde rustc-serialize heapsize" (cd idna && cargo test) .PHONY: test diff --git a/src/host.rs b/src/host.rs index ed67c527c..5d98323ef 100644 --- a/src/host.rs +++ b/src/host.rs @@ -27,6 +27,38 @@ pub enum HostInternal { #[cfg(feature = "heapsize")] known_heap_size!(0, HostInternal); +#[cfg(feature="serde")] +impl ::serde::Serialize for HostInternal { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: ::serde::Serializer { + // This doesn’t use `derive` because that involves + // large dependencies (that take a long time to build), and + // either Macros 1.1 which are not stable yet or a cumbersome build script. + // + // Implementing `Serializer` correctly for an enum is tricky, + // so let’s use existing enums that already do. + use std::net::IpAddr; + match *self { + HostInternal::None => None, + HostInternal::Domain => Some(None), + HostInternal::Ipv4(addr) => Some(Some(IpAddr::V4(addr))), + HostInternal::Ipv6(addr) => Some(Some(IpAddr::V6(addr))), + }.serialize(serializer) + } +} + +#[cfg(feature="serde")] +impl ::serde::Deserialize for HostInternal { + fn deserialize(deserializer: &mut D) -> Result where D: ::serde::Deserializer { + use std::net::IpAddr; + Ok(match try!(::serde::Deserialize::deserialize(deserializer)) { + None => HostInternal::None, + Some(None) => HostInternal::Domain, + Some(Some(IpAddr::V4(addr))) => HostInternal::Ipv4(addr), + Some(Some(IpAddr::V6(addr))) => HostInternal::Ipv6(addr), + }) + } +} + impl From> for HostInternal { fn from(host: Host) -> HostInternal { match host { diff --git a/src/lib.rs b/src/lib.rs index 0f9178c3e..4292b2f25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -348,11 +348,12 @@ impl Url { /// This checks each of these invariants and panic if one is not met. /// This is for testing rust-url itself. #[doc(hidden)] - pub fn assert_invariants(&self) { + pub fn check_invariants(&self) -> Result<(), String> { macro_rules! assert { ($x: expr) => { if !$x { - panic!("!( {} ) for URL {:?}", stringify!($x), self.serialization) + return Err(format!("!( {} ) for URL {:?}", + stringify!($x), self.serialization)) } } } @@ -363,8 +364,9 @@ impl Url { let a = $a; let b = $b; if a != b { - panic!("{:?} != {:?} ({} != {}) for URL {:?}", - a, b, stringify!($a), stringify!($b), self.serialization) + return Err(format!("{:?} != {:?} ({} != {}) for URL {:?}", + a, b, stringify!($a), stringify!($b), + self.serialization)) } } } @@ -445,6 +447,7 @@ impl Url { assert_eq!(self.path_start, other.path_start); assert_eq!(self.query_start, other.query_start); assert_eq!(self.fragment_start, other.fragment_start); + Ok(()) } /// Return the origin of this URL (https://url.spec.whatwg.org/#origin) @@ -1514,6 +1517,60 @@ impl Url { Ok(url) } + /// Serialize with Serde using the internal representation of the `Url` struct. + /// + /// The corresponding `deserialize_internal` method sacrifices some invariant-checking + /// for speed, compared to the `Deserialize` trait impl. + /// + /// This method is only available if the `serde` Cargo feature is enabled. + #[cfg(feature = "serde")] + #[deny(unused)] + pub fn serialize_internal(&self, serializer: &mut S) -> Result<(), S::Error> where S: serde::Serializer { + use serde::Serialize; + // Destructuring first lets us ensure that adding or removing fields forces this method + // to be updated + let Url { ref serialization, ref scheme_end, + ref username_end, ref host_start, + ref host_end, ref host, ref port, + ref path_start, ref query_start, + ref fragment_start} = *self; + (serialization, scheme_end, username_end, + host_start, host_end, host, port, path_start, + query_start, fragment_start).serialize(serializer) + } + + /// Serialize with Serde using the internal representation of the `Url` struct. + /// + /// The corresponding `deserialize_internal` method sacrifices some invariant-checking + /// for speed, compared to the `Deserialize` trait impl. + /// + /// This method is only available if the `serde` Cargo feature is enabled. + #[cfg(feature = "serde")] + #[deny(unused)] + pub fn deserialize_internal(deserializer: &mut D) -> Result where D: serde::Deserializer { + use serde::{Deserialize, Error}; + let (serialization, scheme_end, username_end, + host_start, host_end, host, port, path_start, + query_start, fragment_start) = try!(Deserialize::deserialize(deserializer)); + let url = Url { + serialization: serialization, + scheme_end: scheme_end, + username_end: username_end, + host_start: host_start, + host_end: host_end, + host: host, + port: port, + path_start: path_start, + query_start: query_start, + fragment_start: fragment_start + }; + if cfg!(debug_assertions) { + try!(url.check_invariants().map_err(|ref reason| Error::invalid_value(&reason))) + } + Ok(url) + } + + /// Assuming the URL is in the `file` scheme or similar, /// convert its path to an absolute `std::path::Path`. /// diff --git a/src/path_segments.rs b/src/path_segments.rs index 437a84ee7..c91c49198 100644 --- a/src/path_segments.rs +++ b/src/path_segments.rs @@ -181,7 +181,7 @@ impl<'a> PathSegmentsMut<'a> { /// For internal testing, not part of the public API. #[doc(hidden)] pub fn assert_url_invariants(&mut self) -> &mut Self { - self.url.assert_invariants(); + self.url.check_invariants(); self } } diff --git a/tests/data.rs b/tests/data.rs index b8945aa48..179d4f4df 100644 --- a/tests/data.rs +++ b/tests/data.rs @@ -15,6 +15,16 @@ extern crate url; use rustc_serialize::json::{self, Json}; use url::{Url, quirks}; +fn check_invariants(url: &Url) { + url.check_invariants().unwrap(); + #[cfg(feature="serde")] { + extern crate serde_json; + let bytes = serde_json::to_vec(url).unwrap(); + let new_url: Url = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(url, &new_url); + } +} + fn run_parsing(input: String, base: String, expected: Result) { let base = match Url::parse(&base) { @@ -28,7 +38,7 @@ fn run_parsing(input: String, base: String, expected: Result panic!("Expected a parse error for URL {:?}", input), }; - url.assert_invariants(); + check_invariants(&url); macro_rules! assert_eq { ($expected: expr, $got: expr) => { @@ -144,11 +154,11 @@ fn collect_setters(add_test: &mut F) where F: FnMut(String, test::TestFn) { let mut expected = test.take("expected").unwrap(); add_test(name, test::TestFn::dyn_test_fn(move || { let mut url = Url::parse(&href).unwrap(); - url.assert_invariants(); + check_invariants(&url); let _ = quirks::$setter(&mut url, &new_value); assert_attributes!(url, expected, href protocol username password host hostname port pathname search hash); - url.assert_invariants(); + check_invariants(&url); })) } }} diff --git a/tests/unit.rs b/tests/unit.rs index 9c22dd7f1..6739956fe 100644 --- a/tests/unit.rs +++ b/tests/unit.rs @@ -259,13 +259,13 @@ fn test_form_serialize() { fn issue_25() { let filename = if cfg!(windows) { r"C:\run\pg.sock" } else { "/run/pg.sock" }; let mut url = Url::from_file_path(filename).unwrap(); - url.assert_invariants(); + url.check_invariants().unwrap(); url.set_scheme("postgres").unwrap(); - url.assert_invariants(); + url.check_invariants().unwrap(); url.set_host(Some("")).unwrap(); - url.assert_invariants(); + url.check_invariants().unwrap(); url.set_username("me").unwrap(); - url.assert_invariants(); + url.check_invariants().unwrap(); let expected = format!("postgres://me@/{}run/pg.sock", if cfg!(windows) { "C:/" } else { "" }); assert_eq!(url.as_str(), expected); } @@ -277,7 +277,7 @@ fn issue_61() { url.set_scheme("https").unwrap(); assert_eq!(url.port(), None); assert_eq!(url.port_or_known_default(), Some(443)); - url.assert_invariants(); + url.check_invariants().unwrap(); } #[test] @@ -285,7 +285,7 @@ fn issue_61() { /// https://github.com/servo/rust-url/issues/197 fn issue_197() { let mut url = Url::from_file_path("/").expect("Failed to parse path"); - url.assert_invariants(); + url.check_invariants().unwrap(); assert_eq!(url, Url::parse("file:///").expect("Failed to parse path + protocol")); url.path_segments_mut().expect("path_segments_mut").pop_if_empty(); } @@ -299,9 +299,9 @@ fn issue_241() { /// https://github.com/servo/rust-url/issues/222 fn append_trailing_slash() { let mut url: Url = "http://localhost:6767/foo/bar?a=b".parse().unwrap(); - url.assert_invariants(); + url.check_invariants().unwrap(); url.path_segments_mut().unwrap().push(""); - url.assert_invariants(); + url.check_invariants().unwrap(); assert_eq!(url.to_string(), "http://localhost:6767/foo/bar/?a=b"); } @@ -310,10 +310,10 @@ fn append_trailing_slash() { fn extend_query_pairs_then_mutate() { let mut url: Url = "http://localhost:6767/foo/bar".parse().unwrap(); url.query_pairs_mut().extend_pairs(vec![ ("auth", "my-token") ].into_iter()); - url.assert_invariants(); + url.check_invariants().unwrap(); assert_eq!(url.to_string(), "http://localhost:6767/foo/bar?auth=my-token"); url.path_segments_mut().unwrap().push("some_other_path"); - url.assert_invariants(); + url.check_invariants().unwrap(); assert_eq!(url.to_string(), "http://localhost:6767/foo/bar/some_other_path?auth=my-token"); } @@ -321,9 +321,9 @@ fn extend_query_pairs_then_mutate() { /// https://github.com/servo/rust-url/issues/222 fn append_empty_segment_then_mutate() { let mut url: Url = "http://localhost:6767/foo/bar?a=b".parse().unwrap(); - url.assert_invariants(); + url.check_invariants().unwrap(); url.path_segments_mut().unwrap().push("").pop(); - url.assert_invariants(); + url.check_invariants().unwrap(); assert_eq!(url.to_string(), "http://localhost:6767/foo/bar?a=b"); }