use crate::domain::{DomainError, DomainResult, FileId, LookupResult, MasterPort};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResizeMode {
Fit,
Fill,
}
#[derive(Debug, Clone, Default)]
pub struct ImageParams {
pub width: Option<u32>,
pub height: Option<u32>,
pub mode: Option<ResizeMode>,
}
impl ImageParams {
#[must_use]
pub const fn width(width: u32) -> Self {
Self {
width: Some(width),
height: None,
mode: None,
}
}
#[must_use]
pub const fn height(height: u32) -> Self {
Self {
width: None,
height: Some(height),
mode: None,
}
}
#[must_use]
pub const fn dimensions(width: u32, height: u32) -> Self {
Self {
width: Some(width),
height: Some(height),
mode: None,
}
}
#[must_use]
pub const fn with_mode(mut self, mode: ResizeMode) -> Self {
self.mode = Some(mode);
self
}
#[must_use]
pub fn render(&self) -> String {
let mut params = Vec::new();
if let Some(w) = self.width {
params.push(format!("width={w}"));
}
if let Some(h) = self.height {
params.push(format!("height={h}"));
}
if let Some(mode) = self.mode {
let mode_str = match mode {
ResizeMode::Fit => "fit",
ResizeMode::Fill => "fill",
};
params.push(format!("mode={mode_str}"));
}
if params.is_empty() {
String::new()
} else {
format!("?{}", params.join("&"))
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PublicUrlOptions {
pub image_params: Option<ImageParams>,
pub prefer_public: bool,
}
pub struct PublicUrlBuilder<M> {
master: M,
}
impl<M> PublicUrlBuilder<M>
where
M: MasterPort,
{
pub const fn new(master: M) -> Self {
Self { master }
}
pub async fn build(
&self,
file_id: &FileId,
options: Option<PublicUrlOptions>,
) -> DomainResult<String> {
let opts = options.unwrap_or_default();
let lookup = self.master.lookup(file_id.volume_id()).await?;
if lookup.locations.is_empty() {
return Err(DomainError::NoReplicasAvailable {
volume_id: file_id.volume_id(),
});
}
let location = &lookup.locations[0];
let base_url = if opts.prefer_public {
location
.public_url
.as_ref()
.unwrap_or(&location.url)
.clone()
} else {
location.url.clone()
};
let fid_str = file_id.render();
let image_suffix = opts
.image_params
.as_ref()
.map(ImageParams::render)
.unwrap_or_default();
Ok(format!("{base_url}/{fid_str}{image_suffix}"))
}
pub fn build_from_lookup(
file_id: &FileId,
lookup: &LookupResult,
options: Option<PublicUrlOptions>,
) -> DomainResult<String> {
let opts = options.unwrap_or_default();
if lookup.locations.is_empty() {
return Err(DomainError::NoReplicasAvailable {
volume_id: file_id.volume_id(),
});
}
let location = &lookup.locations[0];
let base_url = if opts.prefer_public {
location
.public_url
.as_ref()
.unwrap_or(&location.url)
.clone()
} else {
location.url.clone()
};
let fid_str = file_id.render();
let image_suffix = opts
.image_params
.as_ref()
.map(ImageParams::render)
.unwrap_or_default();
Ok(format!("{base_url}/{fid_str}{image_suffix}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_image_params_width() {
let params = ImageParams::width(100);
assert_eq!(params.width, Some(100));
assert_eq!(params.height, None);
}
#[test]
fn test_image_params_dimensions() {
let params = ImageParams::dimensions(100, 200);
assert_eq!(params.width, Some(100));
assert_eq!(params.height, Some(200));
}
#[test]
fn test_image_params_with_mode() {
let params = ImageParams::dimensions(100, 200).with_mode(ResizeMode::Fill);
assert_eq!(params.mode, Some(ResizeMode::Fill));
}
#[test]
fn test_image_params_render_empty() {
let params = ImageParams::default();
assert_eq!(params.render(), "");
}
#[test]
fn test_image_params_render_width_only() {
let params = ImageParams::width(100);
assert_eq!(params.render(), "?width=100");
}
#[test]
fn test_image_params_render_full() {
let params = ImageParams::dimensions(100, 200).with_mode(ResizeMode::Fit);
let rendered = params.render();
assert!(rendered.contains("width=100"));
assert!(rendered.contains("height=200"));
assert!(rendered.contains("mode=fit"));
}
}