rustrails-storage 0.1.2

File storage (ActiveStorage equivalent)
Documentation
//! Typed image transformation configuration helpers.

use std::collections::BTreeMap;

use serde_json::{Value, json};

/// Resize dimensions for an image variant.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResizeTransform {
    /// Target width in pixels.
    pub width: u32,
    /// Target height in pixels.
    pub height: u32,
}

impl ResizeTransform {
    /// Creates a resize transform.
    #[must_use]
    pub const fn new(width: u32, height: u32) -> Self {
        Self { width, height }
    }
}

/// Crop rectangle for an image variant.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CropTransform {
    /// Left offset in pixels.
    pub x: u32,
    /// Top offset in pixels.
    pub y: u32,
    /// Crop width in pixels.
    pub width: u32,
    /// Crop height in pixels.
    pub height: u32,
}

impl CropTransform {
    /// Creates a crop transform.
    #[must_use]
    pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
        Self {
            x,
            y,
            width,
            height,
        }
    }
}

/// Typed image transformation configuration that converts into variant options.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ImageTransformations {
    /// Resize instruction applied before upload.
    pub resize: Option<ResizeTransform>,
    /// Crop instruction applied before upload.
    pub crop: Option<CropTransform>,
    /// Optional output image format such as `png`.
    pub format: Option<String>,
}

impl ImageTransformations {
    /// Creates an empty transformation set.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Adds a resize transform.
    #[must_use]
    pub fn resize(mut self, width: u32, height: u32) -> Self {
        self.resize = Some(ResizeTransform::new(width, height));
        self
    }

    /// Adds a crop transform.
    #[must_use]
    pub fn crop(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
        self.crop = Some(CropTransform::new(x, y, width, height));
        self
    }

    /// Sets the output image format.
    #[must_use]
    pub fn format(mut self, format: impl Into<String>) -> Self {
        self.format = Some(format.into());
        self
    }

    /// Converts the transformation set into variant options.
    #[must_use]
    pub fn to_map(&self) -> BTreeMap<String, Value> {
        let mut transformations = BTreeMap::new();
        if let Some(resize) = self.resize {
            transformations.insert(
                "resize".to_owned(),
                json!({"width": resize.width, "height": resize.height}),
            );
        }
        if let Some(crop) = self.crop {
            transformations.insert(
                "crop".to_owned(),
                json!({
                    "x": crop.x,
                    "y": crop.y,
                    "width": crop.width,
                    "height": crop.height,
                }),
            );
        }
        if let Some(format) = &self.format {
            transformations.insert("format".to_owned(), Value::String(format.clone()));
        }
        transformations
    }
}

impl From<ImageTransformations> for BTreeMap<String, Value> {
    fn from(value: ImageTransformations) -> Self {
        value.to_map()
    }
}

impl From<&ImageTransformations> for BTreeMap<String, Value> {
    fn from(value: &ImageTransformations) -> Self {
        value.to_map()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn resize_and_crop_transformations_convert_to_variant_map() {
        let transformations = ImageTransformations::new()
            .resize(100, 200)
            .crop(10, 20, 80, 160)
            .format("png")
            .to_map();
        assert_eq!(
            transformations.get("resize"),
            Some(&json!({"width": 100, "height": 200})),
        );
        assert_eq!(
            transformations.get("crop"),
            Some(&json!({"x": 10, "y": 20, "width": 80, "height": 160})),
        );
        assert_eq!(
            transformations.get("format"),
            Some(&Value::String("png".to_owned()))
        );
    }

    #[test]
    fn empty_transformations_produce_empty_map() {
        assert!(ImageTransformations::new().to_map().is_empty());
    }
}