use percent_encoding::{AsciiSet, CONTROLS, NON_ALPHANUMERIC, utf8_percent_encode};
const UNRESERVED: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'!')
.add(b'"')
.add(b'#')
.add(b'$')
.add(b'%')
.add(b'&')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b'+')
.add(b',')
.add(b'/')
.add(b':')
.add(b';')
.add(b'<')
.add(b'=')
.add(b'>')
.add(b'?')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
pub fn percent_encode_unreserved(s: &str) -> String {
utf8_percent_encode(s, UNRESERVED).to_string()
}
const PATH_SEGMENT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'-').remove(b'_').remove(b'.');
pub fn percent_encode_path_segment(s: &str) -> String {
utf8_percent_encode(s, PATH_SEGMENT).to_string()
}
pub fn join(base: &str, path: &str) -> String {
let b = base.trim_end_matches('/');
let p = path.trim_start_matches('/');
if p.is_empty() {
b.to_string()
} else {
format!("{}/{}", b, p)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unreserved_passes_through() {
assert_eq!(percent_encode_unreserved("hello"), "hello");
assert_eq!(percent_encode_unreserved("A-Za-z0-9-_.~"), "A-Za-z0-9-_.~");
}
#[test]
fn space_and_specials_encoded() {
assert_eq!(percent_encode_unreserved("hello world"), "hello%20world");
assert_eq!(percent_encode_unreserved("a=b&c=d"), "a%3Db%26c%3Dd");
}
#[test]
fn slashes_encoded() {
assert_eq!(percent_encode_unreserved("a/b/c"), "a%2Fb%2Fc");
}
#[test]
fn utf8_encoded_per_byte() {
assert_eq!(percent_encode_unreserved("café"), "caf%C3%A9");
}
}