use std::str::FromStr;
use url::{ParseError, Url};
#[derive(Clone, Debug)]
pub(crate) struct UrlBuilder {
value: Url
}
impl UrlBuilder {
pub fn from_str(value: &str) -> Result<Self, ParseError> {
Ok(Self {
value: Url::from_str(value)?
})
}
pub fn join(&self, path: &str) -> Result<Self, ParseError> {
let s = self.value.to_string();
if s.ends_with('/') {
Ok(Self {
value: self.value.join(path)?
})
}
else {
Ok(Self {
value: Url::from_str(&format!("{}/", s))?
.join(path)?
})
}
}
pub fn query(&self) -> UrlQueryBuilder {
UrlQueryBuilder {
value: self.value.clone()
}
}
}
impl ToString for UrlBuilder {
fn to_string(&self) -> String {
self.value.to_string()
}
}
#[derive(Clone, Debug)]
pub(crate) struct UrlQueryBuilder {
value: Url
}
impl UrlQueryBuilder {
pub fn append<V: ToString>(mut self, name: &str, value: V) -> Self {
self.value.query_pairs_mut().append_pair(name, &value.to_string());
self
}
pub fn append_all<V: ToString>(self, name: &str, values: Vec<V>) -> Self {
let mut this = self;
for v in values {
this = this.append(name, v);
}
this
}
pub fn option<V: ToString>(self, name: &str, value: Option<V>) -> Self {
match value {
None => self,
Some(v) => self.append(name, v)
}
}
}
impl ToString for UrlQueryBuilder {
fn to_string(&self) -> String {
self.value.to_string()
}
}
#[cfg(test)]
pub mod test_url_builder {
use url::ParseError;
use crate::imp::url::UrlBuilder;
#[test]
pub fn appends_path_to_base_with_trailing_slash() {
let base = UrlBuilder::from_str("http://a/")
.unwrap();
let sub = base.join("b")
.unwrap();
assert_eq!("http://a/b", &sub.to_string());
}
#[test]
pub fn appends_path_to_base_without_trailing_slash() {
let base = UrlBuilder::from_str("http://a")
.unwrap();
let sub = base.join("b")
.unwrap();
assert_eq!("http://a/b", &sub.to_string());
}
#[test]
pub fn appends_path_to_path_with_trailing_slash() {
let base = UrlBuilder::from_str("http://a/b/")
.unwrap();
let sub = base.join("c")
.unwrap();
assert_eq!("http://a/b/c", &sub.to_string());
}
#[test]
pub fn appends_path_to_path_without_trailing_slash() {
let base = UrlBuilder::from_str("http://a/b")
.unwrap();
let sub = base.join("c")
.unwrap();
assert_eq!("http://a/b/c", &sub.to_string());
}
#[test]
pub fn builds_path_and_query() -> Result<(), ParseError> {
let actual = UrlBuilder::from_str("https://testuri.org:123")?
.join("images")?
.join("foo/bar")?
.join("push")?
.query()
.append("repo", "qux")
.append("tag", "baz")
.to_string();
assert_eq!("https://testuri.org:123/images/foo/bar/push?repo=qux&tag=baz", &actual);
Ok(())
}
#[test]
pub fn builds_path_and_optional_query() -> Result<(), ParseError> {
let actual = UrlBuilder::from_str("unix://some-fd:0")?
.join("path")?
.query()
.append("one", 1)
.option("two", Some(2))
.option("three", Option::<usize>::None)
.append("maybe", true)
.to_string();
assert_eq!("unix://some-fd:0/path?one=1&two=2&maybe=true", &actual);
Ok(())
}
#[test]
pub fn encodes_path() {
let sub = UrlBuilder::from_str("http://a")
.unwrap()
.join("Hello, World")
.unwrap();
assert_eq!("http://a/Hello,%20World", &sub.to_string());
}
#[test]
pub fn encodes_query_value() {
let sub = UrlBuilder::from_str("http://a")
.unwrap()
.query()
.append("greeting", "Hello, world.");
assert_eq!("http://a/?greeting=Hello%2C+world.", &sub.to_string());
}
#[test]
pub fn errors_from_empty_string() {
let error = UrlBuilder::from_str("")
.unwrap_err();
match error {
ParseError::RelativeUrlWithoutBase => {}
_ => panic!("Unexpected error: {:?}", error)
}
}
#[test]
pub fn errors_from_invalid_url() {
let error = UrlBuilder::from_str("https:://a")
.unwrap_err();
match error {
ParseError::EmptyHost => {}
_ => panic!("Unexpected error: {:?}", error)
}
}
#[test]
pub fn appends_all_with_same_name() {
let actual = UrlBuilder::from_str("http://a")
.unwrap()
.query()
.append_all("b", vec!["c", "d"])
.to_string();
assert_eq!("http://a/?b=c&b=d", actual);
}
#[test]
pub fn appends_none_with_same_name() {
let empty: Vec<String> = Vec::new();
let actual = UrlBuilder::from_str("http://a")
.unwrap()
.query()
.append_all("b", empty)
.to_string();
assert_eq!("http://a/", actual);
}
}