use url::Url;
use crate::client::{HttpMethod, Rerout};
use crate::error::{ReroutError, Result};
use crate::models::QrOptions;
#[derive(Debug, Clone)]
pub struct Qr<'a> {
client: &'a Rerout,
}
impl<'a> Qr<'a> {
pub(crate) fn new(client: &'a Rerout) -> Self {
Self { client }
}
pub fn url(&self, code: &str, options: &QrOptions) -> Result<String> {
build_qr_url(self.client.base_url(), code, options)
}
pub async fn svg(&self, code: &str, options: &QrOptions) -> Result<String> {
let path = format!("/v1/links/{}/qr", percent_encode_code(code));
let pairs = options.to_query_pairs();
let query: Option<&[(&str, String)]> = if pairs.is_empty() { None } else { Some(&pairs) };
self.client
.request_text::<()>(HttpMethod::Get, &path, query, None)
.await
}
}
pub fn build_qr_url(base_url: &str, code: &str, options: &QrOptions) -> Result<String> {
let trimmed = base_url.trim_end_matches('/');
let raw = format!("{}/v1/links/{}/qr", trimmed, percent_encode_code(code));
let mut url = Url::parse(&raw).map_err(|e| ReroutError::Config {
code: "invalid_base_url".to_string(),
message: format!("Could not build QR URL: {e}"),
})?;
let pairs = options.to_query_pairs();
if !pairs.is_empty() {
let mut q = url.query_pairs_mut();
for (k, v) in &pairs {
q.append_pair(k, v);
}
drop(q);
}
Ok(url.to_string())
}
fn percent_encode_code(code: &str) -> String {
let mut out = String::with_capacity(code.len());
for byte in code.bytes() {
if matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~') {
out.push(byte as char);
} else {
out.push('%');
out.push(hex_upper(byte >> 4));
out.push(hex_upper(byte & 0x0f));
}
}
out
}
#[inline]
fn hex_upper(nibble: u8) -> char {
match nibble {
0..=9 => (b'0' + nibble) as char,
10..=15 => (b'A' + (nibble - 10)) as char,
_ => unreachable!("nibble out of range"),
}
}