use crate::processing::options::ProcessingOption;
use std::collections::HashMap;
use thiserror::Error;
use tracing::debug;
const PRESET: &str = "preset";
const PRESET_SHORT: &str = "pr";
#[derive(Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum PresetError {
#[error("preset option requires a preset name")]
MissingName,
#[error("unknown preset: {name}")]
Unknown { name: String },
#[error("only preset references are allowed in only_presets mode, found: {option}")]
NonPresetOption { option: String },
#[error("only preset references are allowed in only_presets mode")]
PresetRequired,
#[error("invalid preset option: {option}")]
InvalidOption { option: String },
}
pub fn expand_presets(
options: Vec<ProcessingOption>,
presets: &HashMap<String, Vec<ProcessingOption>>,
only_presets: bool,
) -> Result<Vec<ProcessingOption>, PresetError> {
let mut expanded = Vec::new();
let mut has_preset_reference = false;
if let Some(default_options) = presets.get("default") {
debug!("Applying default preset with {} options", default_options.len());
expanded.extend(default_options.iter().cloned());
}
for option in options {
if option.name == PRESET || option.name == PRESET_SHORT {
has_preset_reference = true;
if option.args.is_empty() {
return Err(PresetError::MissingName);
}
let preset_name = &option.args[0];
let preset_options = presets.get(preset_name).ok_or_else(|| PresetError::Unknown {
name: preset_name.clone(),
})?;
debug!(
"Expanding preset '{}' with {} options",
preset_name,
preset_options.len()
);
expanded.extend(preset_options.iter().cloned());
} else if only_presets {
return Err(PresetError::NonPresetOption { option: option.name });
} else {
expanded.push(option);
}
}
if only_presets && !has_preset_reference && !presets.contains_key("default") && !expanded.is_empty() {
return Err(PresetError::PresetRequired);
}
Ok(expanded)
}
pub fn parse_options_string(options_str: &str) -> Result<Vec<ProcessingOption>, PresetError> {
let mut options = Vec::new();
for part in options_str.split('/') {
let part = part.trim();
if part.is_empty() {
continue;
}
let mut segments = part.split(':');
let name = segments.next().unwrap_or_default();
if name.is_empty() {
return Err(PresetError::InvalidOption {
option: part.to_string(),
});
}
let name = name.to_string();
let args: Vec<String> = segments.map(|s| s.to_string()).collect();
options.push(ProcessingOption { name, args });
}
Ok(options)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_options_string() {
let options_str = "resize:fit:300:300/dpr:3/quality:85";
let options = parse_options_string(options_str).unwrap();
assert_eq!(options.len(), 3);
assert_eq!(options[0].name, "resize");
assert_eq!(options[0].args, vec!["fit", "300", "300"]);
assert_eq!(options[1].name, "dpr");
assert_eq!(options[1].args, vec!["3"]);
assert_eq!(options[2].name, "quality");
assert_eq!(options[2].args, vec!["85"]);
}
#[test]
fn test_parse_options_string_empty() {
let options = parse_options_string("").unwrap();
assert_eq!(options.len(), 0);
}
#[test]
fn test_parse_options_string_trailing_slash() {
let options_str = "resize:fit:300:300/quality:85/";
let options = parse_options_string(options_str).unwrap();
assert_eq!(options.len(), 2);
}
#[test]
fn test_expand_presets_simple() {
let mut presets = HashMap::new();
presets.insert(
"thumbnail".to_string(),
parse_options_string("resize:fit:150:150/quality:80").unwrap(),
);
let options = vec![ProcessingOption {
name: "preset".to_string(),
args: vec!["thumbnail".to_string()],
}];
let expanded = expand_presets(options, &presets, false).unwrap();
assert_eq!(expanded.len(), 2);
assert_eq!(expanded[0].name, "resize");
assert_eq!(expanded[1].name, "quality");
}
#[test]
fn test_expand_presets_with_default() {
let mut presets = HashMap::new();
presets.insert("default".to_string(), parse_options_string("quality:90").unwrap());
presets.insert(
"thumbnail".to_string(),
parse_options_string("resize:fit:150:150").unwrap(),
);
let options = vec![ProcessingOption {
name: "preset".to_string(),
args: vec!["thumbnail".to_string()],
}];
let expanded = expand_presets(options, &presets, false).unwrap();
assert_eq!(expanded.len(), 2);
assert_eq!(expanded[0].name, "quality");
assert_eq!(expanded[1].name, "resize");
}
#[test]
fn test_expand_presets_default_only() {
let mut presets = HashMap::new();
presets.insert("default".to_string(), parse_options_string("quality:90/dpr:2").unwrap());
let options = vec![ProcessingOption {
name: "blur".to_string(),
args: vec!["5".to_string()],
}];
let expanded = expand_presets(options, &presets, false).unwrap();
assert_eq!(expanded.len(), 3);
assert_eq!(expanded[0].name, "quality");
assert_eq!(expanded[1].name, "dpr");
assert_eq!(expanded[2].name, "blur");
}
#[test]
fn test_expand_presets_unknown_preset() {
let presets: HashMap<String, Vec<ProcessingOption>> = HashMap::new();
let options = vec![ProcessingOption {
name: "preset".to_string(),
args: vec!["unknown".to_string()],
}];
let result = expand_presets(options, &presets, false);
assert_eq!(
result.unwrap_err(),
PresetError::Unknown {
name: "unknown".to_string()
}
);
}
#[test]
fn test_expand_presets_only_presets_mode_allows_presets() {
let mut presets = HashMap::new();
presets.insert(
"thumbnail".to_string(),
parse_options_string("resize:fit:150:150").unwrap(),
);
let options = vec![ProcessingOption {
name: "preset".to_string(),
args: vec!["thumbnail".to_string()],
}];
let expanded = expand_presets(options, &presets, true).unwrap();
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0].name, "resize");
}
#[test]
fn test_expand_presets_only_presets_mode_rejects_non_presets() {
let presets: HashMap<String, Vec<ProcessingOption>> = HashMap::new();
let options = vec![ProcessingOption {
name: "blur".to_string(),
args: vec!["5".to_string()],
}];
let result = expand_presets(options, &presets, true);
assert_eq!(
result.unwrap_err(),
PresetError::NonPresetOption {
option: "blur".to_string()
}
);
}
#[test]
fn test_expand_presets_only_presets_mode_allows_default() {
let mut presets = HashMap::new();
presets.insert("default".to_string(), parse_options_string("quality:90").unwrap());
let options = vec![];
let expanded = expand_presets(options, &presets, true).unwrap();
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0].name, "quality");
}
#[test]
fn test_expand_presets_preset_short() {
let mut presets = HashMap::new();
presets.insert("thumb".to_string(), parse_options_string("resize:fit:100:100").unwrap());
let options = vec![ProcessingOption {
name: "pr".to_string(),
args: vec!["thumb".to_string()],
}];
let expanded = expand_presets(options, &presets, false).unwrap();
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0].name, "resize");
}
#[test]
fn test_expand_presets_empty_preset_name() {
let presets: HashMap<String, Vec<ProcessingOption>> = HashMap::new();
let options = vec![ProcessingOption {
name: "preset".to_string(),
args: vec![],
}];
let result = expand_presets(options, &presets, false);
assert_eq!(result.unwrap_err(), PresetError::MissingName);
}
}