code refactor
Some checks failed
CI / Check / Test (push) Has been cancelled

Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
2026-10-26 09:36:23 +00:00
parent 2ce7db12c2
commit 017b58d6fa
804 changed files with 33423 additions and 28955 deletions

View File

@@ -1,19 +1,20 @@
[package]
name = "image-converter"
version = "0.1.0"
edition = "2021"
edition.workspace = true
[dependencies]
config = { workspace = true }
domain = { workspace = true }
async-trait = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] }
ravif = { version = "0.11", default-features = false }
webp = "0.3"
tokio = { workspace = true, features = ["rt"] }
image = { workspace = true, features = ["jpeg", "png", "webp"] }
ravif = { workspace = true }
webp = { workspace = true }
[dev-dependencies]
object-storage = { workspace = true }
object_store = "0.11"
uuid = { workspace = true }
object_store = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

View File

@@ -49,7 +49,3 @@ impl PeriodicJob for ConversionBackfillJob {
Ok(())
}
}
#[cfg(test)]
#[path = "tests/backfill.rs"]
mod tests;

View File

@@ -18,34 +18,22 @@ pub struct ConversionConfig {
}
impl ConversionConfig {
pub fn from_env() -> anyhow::Result<Option<Self>> {
Self::from_vars(
std::env::var("IMAGE_CONVERSION_ENABLED").ok().as_deref(),
std::env::var("IMAGE_CONVERSION_FORMAT").ok().as_deref(),
)
}
fn from_vars(enabled: Option<&str>, format: Option<&str>) -> anyhow::Result<Option<Self>> {
if enabled != Some("true") {
pub fn from_config(config: &::config::ImageConversionConfig) -> anyhow::Result<Option<Self>> {
if !config.enabled {
return Ok(None);
}
let format_str = format.ok_or_else(|| {
anyhow::anyhow!("IMAGE_CONVERSION_FORMAT required when IMAGE_CONVERSION_ENABLED=true")
})?;
let format = match format_str {
"avif" => Format::Avif,
"webp" => Format::Webp,
other => anyhow::bail!(
let format = match config.format.as_deref() {
Some("avif") => Format::Avif,
Some("webp") => Format::Webp,
Some(other) => anyhow::bail!(
"Unknown IMAGE_CONVERSION_FORMAT: {other:?}. Valid values: avif, webp"
),
None => {
anyhow::bail!("IMAGE_CONVERSION_FORMAT required when IMAGE_CONVERSION_ENABLED=true")
}
};
Ok(Some(Self { format }))
}
}
#[cfg(test)]
#[path = "tests/config.rs"]
mod tests;

View File

@@ -31,6 +31,10 @@ impl ImageConversionHandler {
#[async_trait]
impl EventHandler for ImageConversionHandler {
fn name(&self) -> &'static str {
"image_conversion"
}
async fn handle(&self, event: &DomainEvent) -> Result<(), DomainError> {
let key = match event {
DomainEvent::ImageStored { key } => key.clone(),
@@ -41,6 +45,10 @@ impl EventHandler for ImageConversionHandler {
return Ok(());
}
if !self.storage.exists(&key).await? {
tracing::warn!(%key, "original image already gone, treating conversion as done");
return Ok(());
}
let bytes = self.storage.get(&key).await?;
let format = self.format;
@@ -99,7 +107,3 @@ fn convert(bytes: Vec<u8>, format: Format) -> Result<Vec<u8>, String> {
}
}
}
#[cfg(test)]
#[path = "tests/handler.rs"]
mod tests;

View File

@@ -14,12 +14,13 @@ use std::sync::Arc;
type ConversionPair = (Arc<dyn EventHandler>, Arc<dyn PeriodicJob>);
pub fn build(
config: &::config::ImageConversionConfig,
object_storage: Arc<dyn ObjectStorage>,
image_ref_command: Arc<dyn ImageRefCommand>,
image_ref_query: Arc<dyn ImageRefQuery>,
event_publisher: Arc<dyn EventPublisher>,
) -> anyhow::Result<Option<ConversionPair>> {
let config = match ConversionConfig::from_env()? {
let config = match ConversionConfig::from_config(config)? {
Some(c) => c,
None => return Ok(None),
};

View File

@@ -1,45 +0,0 @@
use super::*;
#[test]
fn disabled_by_default() {
assert!(ConversionConfig::from_vars(None, None).unwrap().is_none());
assert!(ConversionConfig::from_vars(Some("false"), None)
.unwrap()
.is_none());
}
#[test]
fn enabled_avif() {
let cfg = ConversionConfig::from_vars(Some("true"), Some("avif"))
.unwrap()
.unwrap();
assert_eq!(cfg.format, Format::Avif);
}
#[test]
fn enabled_webp() {
let cfg = ConversionConfig::from_vars(Some("true"), Some("webp"))
.unwrap()
.unwrap();
assert_eq!(cfg.format, Format::Webp);
}
#[test]
fn unknown_format_is_error() {
assert!(ConversionConfig::from_vars(Some("true"), Some("gif")).is_err());
}
#[test]
fn missing_format_when_enabled_is_error() {
assert!(ConversionConfig::from_vars(Some("true"), None).is_err());
}
#[test]
fn avif_extension() {
assert_eq!(Format::Avif.extension(), ".avif");
}
#[test]
fn webp_extension() {
assert_eq!(Format::Webp.extension(), ".webp");
}

View File

@@ -1,5 +1,11 @@
use super::*;
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
use domain::{
errors::DomainError,
events::DomainEvent,
ports::{EventPublisher, ImageRefQuery, PeriodicJob},
};
use image_converter::ConversionBackfillJob;
struct MockImageRef {
keys: Vec<String>,

View File

@@ -0,0 +1,54 @@
use image_converter::{ConversionConfig, Format};
fn section(enabled: bool, format: Option<&str>) -> config::ImageConversionConfig {
config::ImageConversionConfig {
enabled,
format: format.map(str::to_owned),
}
}
fn convert(enabled: bool, format: Option<&str>) -> anyhow::Result<Option<ConversionConfig>> {
ConversionConfig::from_config(&section(enabled, format))
}
#[test]
fn is_disabled_by_default() {
assert!(
ConversionConfig::from_config(&config::ImageConversionConfig::default())
.unwrap()
.is_none()
);
assert!(convert(false, Some("webp")).unwrap().is_none());
}
#[test]
fn enabled_with_avif_format() {
let cfg = convert(true, Some("avif")).unwrap().unwrap();
assert_eq!(cfg.format, Format::Avif);
}
#[test]
fn enabled_with_webp_format() {
let cfg = convert(true, Some("webp")).unwrap().unwrap();
assert_eq!(cfg.format, Format::Webp);
}
#[test]
fn unknown_format_is_rejected() {
assert!(convert(true, Some("gif")).is_err());
}
#[test]
fn missing_format_when_enabled_is_rejected() {
assert!(convert(true, None).is_err());
}
#[test]
fn avif_extension_is_dot_avif() {
assert_eq!(Format::Avif.extension(), ".avif");
}
#[test]
fn webp_extension_is_dot_webp() {
assert_eq!(Format::Webp.extension(), ".webp");
}

View File

@@ -1,7 +1,13 @@
use super::*;
use std::sync::{Arc, Mutex};
use domain::{
errors::DomainError,
events::DomainEvent,
ports::{EventHandler, ImageRefCommand, ObjectStorage},
};
use image_converter::{Format, ImageConversionHandler};
use object_storage::ObjectStorageAdapter;
use object_store::memory::InMemory;
use std::sync::Mutex;
struct MockImageRef {
swaps: Mutex<Vec<(String, String)>>,
@@ -156,3 +162,32 @@ async fn converts_jpeg_to_webp_and_swaps_key() {
);
assert!(storage.get("avatars/u1.webp").await.is_ok());
}
#[tokio::test]
async fn image_conversion_is_idempotent_when_run_twice() {
for format in [Format::Webp, Format::Avif] {
let storage = in_memory_storage();
storage.store("posters/m1", &tiny_jpeg()).await.unwrap();
let image_ref = MockImageRef::new();
let handler = ImageConversionHandler::new(
Arc::clone(&storage) as Arc<dyn ObjectStorage>,
Arc::clone(&image_ref) as Arc<dyn ImageRefCommand>,
format,
);
let event = DomainEvent::ImageStored {
key: "posters/m1".into(),
};
let new_key = format!("posters/m1{}", format.extension());
handler.handle(&event).await.unwrap();
let converted = storage.get(&new_key).await.unwrap();
handler.handle(&event).await.unwrap();
assert_eq!(image_ref.swaps().len(), 1, "one swap");
assert_eq!(
storage.get(&new_key).await.unwrap(),
converted,
"converted bytes are not re-encoded"
);
}
}