use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
const URL_COMPONENT: &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'@');
pub fn assemble_rest_url(
override_base: Option<&str>,
real_base: &str,
path: &str,
query: &str,
) -> String {
match override_base {
Some(ov) if ov.contains("{url}") => {
let target = format!("{real_base}{path}{query}");
let encoded = utf8_percent_encode(&target, URL_COMPONENT).to_string();
ov.replace("{url}", &encoded)
}
Some(ov) => format!("{ov}{path}{query}"),
None => format!("{real_base}{path}{query}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
const REAL_BASE: &str = "https://api.binance.com";
const PATH: &str = "/api/v3/klines";
const QUERY: &str = "?symbol=BTCUSDT&interval=1m&limit=1";
#[test]
fn no_override_returns_real_url() {
let url = assemble_rest_url(None, REAL_BASE, PATH, QUERY);
assert_eq!(url, "https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1m&limit=1");
}
#[test]
fn no_override_empty_path_query() {
let url = assemble_rest_url(None, REAL_BASE, "", "");
assert_eq!(url, "https://api.binance.com");
}
#[test]
fn prefix_mode_prepends_override() {
let ov = "https://corsproxy.io/?https%3A%2F%2Fapi.binance.com";
let url = assemble_rest_url(Some(ov), REAL_BASE, PATH, QUERY);
assert_eq!(
url,
"https://corsproxy.io/?https%3A%2F%2Fapi.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1m&limit=1"
);
}
#[test]
fn prefix_mode_empty_query() {
let ov = "https://my-proxy.example.com";
let url = assemble_rest_url(Some(ov), REAL_BASE, "/api/v3/time", "");
assert_eq!(url, "https://my-proxy.example.com/api/v3/time");
}
#[test]
fn encoded_proxy_encodes_full_target() {
let ov = "https://api.allorigins.win/raw?url={url}";
let url = assemble_rest_url(Some(ov), REAL_BASE, PATH, QUERY);
assert!(url.starts_with("https://api.allorigins.win/raw?url=https%3A%2F%2F"), "unexpected: {url}");
assert!(url.contains("%2Fapi%2Fv3%2Fklines"), "path not encoded: {url}");
assert!(url.contains("%3Fsymbol%3DBTCUSDT"), "query not encoded: {url}");
assert!(url.contains("%26interval%3D1m"), "& not encoded: {url}");
}
#[test]
fn encoded_proxy_no_query() {
let ov = "https://api.allorigins.win/raw?url={url}";
let url = assemble_rest_url(Some(ov), REAL_BASE, "/api/v3/time", "");
assert_eq!(
url,
"https://api.allorigins.win/raw?url=https%3A%2F%2Fapi.binance.com%2Fapi%2Fv3%2Ftime"
);
}
#[test]
fn encoded_proxy_placeholder_replaced_not_doubled() {
let ov = "https://proxy.example/{url}";
let url = assemble_rest_url(Some(ov), "https://ex.com", "/path", "");
assert!(!url.contains("{url}"), "literal {{url}} must be replaced");
assert!(url.contains("https%3A%2F%2Fex.com"), "encoded base must appear");
}
#[test]
fn colon_and_slash_are_encoded() {
let ov = "https://p.test?url={url}";
let url = assemble_rest_url(Some(ov), "https://api.ex.com", "/v1/data", "?a=1&b=2");
assert!(url.contains("%3A"), "colon must be encoded");
assert!(url.contains("%2F"), "slash must be encoded");
assert!(url.contains("%3F"), "? must be encoded");
assert!(url.contains("%26"), "& must be encoded");
assert!(url.contains("%3D"), "= must be encoded");
}
}