|
| 1 | +use super::{Format, TransformEdge}; |
| 2 | + |
| 3 | +/// A pluggable definition of a format-to-format transformation. |
| 4 | +/// |
| 5 | +/// Unlike [`TransformEdge`], which is a graph artifact used internally by |
| 6 | +/// [`TransformGraph`](super::TransformGraph), a `TransformDefinition` |
| 7 | +/// describes a concrete conversion capability that can be registered at runtime |
| 8 | +/// and later materialized into graph edges via |
| 9 | +/// [`TransformDefinitionRegistry::build_graph`](super::TransformDefinitionRegistry::build_graph). |
| 10 | +/// |
| 11 | +/// The optional `label` field identifies the underlying tool or method (e.g. |
| 12 | +/// `"pandoc"` or `"wkhtmltopdf"`), which helps with diagnostics and lets |
| 13 | +/// callers register multiple competing definitions for the same format pair. |
| 14 | +/// |
| 15 | +/// # Example |
| 16 | +/// |
| 17 | +/// ```rust |
| 18 | +/// use renderflow::graph::{Format, TransformDefinition}; |
| 19 | +/// |
| 20 | +/// let def = TransformDefinition::new(Format::Markdown, Format::Html, 0.5, 1.0, "pandoc"); |
| 21 | +/// assert_eq!(def.from, Format::Markdown); |
| 22 | +/// assert_eq!(def.to, Format::Html); |
| 23 | +/// assert_eq!(def.label, "pandoc"); |
| 24 | +/// ``` |
| 25 | +#[derive(Debug, Clone, PartialEq)] |
| 26 | +pub struct TransformDefinition { |
| 27 | + /// Source format for this definition. |
| 28 | + pub from: Format, |
| 29 | + /// Target format produced by this definition. |
| 30 | + pub to: Format, |
| 31 | + /// Relative cost of applying this transformation (lower is cheaper). |
| 32 | + pub cost: f32, |
| 33 | + /// Expected quality of the output on a 0.0–1.0 scale (higher is better). |
| 34 | + pub quality: f32, |
| 35 | + /// Human-readable label identifying the tool or method (e.g. `"pandoc"`). |
| 36 | + pub label: String, |
| 37 | +} |
| 38 | + |
| 39 | +impl TransformDefinition { |
| 40 | + /// Create a new `TransformDefinition`. |
| 41 | + /// |
| 42 | + /// # Parameters |
| 43 | + /// |
| 44 | + /// * `from` – source [`Format`] |
| 45 | + /// * `to` – target [`Format`] |
| 46 | + /// * `cost` – relative execution cost (lower is cheaper) |
| 47 | + /// * `quality` – expected output quality in the range `[0.0, 1.0]`; values |
| 48 | + /// outside this range are clamped automatically. |
| 49 | + /// * `label` – human-readable name identifying the conversion tool or method |
| 50 | + pub fn new(from: Format, to: Format, cost: f32, quality: f32, label: impl Into<String>) -> Self { |
| 51 | + Self { |
| 52 | + from, |
| 53 | + to, |
| 54 | + cost, |
| 55 | + quality: quality.clamp(0.0, 1.0), |
| 56 | + label: label.into(), |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + /// Convert this definition into a [`TransformEdge`] for use in a |
| 61 | + /// [`TransformGraph`](super::TransformGraph). |
| 62 | + pub fn to_edge(&self) -> TransformEdge { |
| 63 | + TransformEdge::new(self.from, self.to, self.cost, self.quality) |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +#[cfg(test)] |
| 68 | +mod tests { |
| 69 | + use super::*; |
| 70 | + |
| 71 | + // ── construction ────────────────────────────────────────────────────────── |
| 72 | + |
| 73 | + #[test] |
| 74 | + fn test_definition_fields() { |
| 75 | + let def = TransformDefinition::new(Format::Markdown, Format::Html, 0.5, 1.0, "pandoc"); |
| 76 | + assert_eq!(def.from, Format::Markdown); |
| 77 | + assert_eq!(def.to, Format::Html); |
| 78 | + assert_eq!(def.cost, 0.5); |
| 79 | + assert!((def.quality - 1.0).abs() < 1e-5); |
| 80 | + assert_eq!(def.label, "pandoc"); |
| 81 | + } |
| 82 | + |
| 83 | + #[test] |
| 84 | + fn test_quality_clamped_above_one() { |
| 85 | + let def = TransformDefinition::new(Format::Markdown, Format::Pdf, 1.0, 1.5, "tool"); |
| 86 | + assert!((def.quality - 1.0).abs() < 1e-5, "quality above 1.0 must be clamped"); |
| 87 | + } |
| 88 | + |
| 89 | + #[test] |
| 90 | + fn test_quality_clamped_below_zero() { |
| 91 | + let def = TransformDefinition::new(Format::Markdown, Format::Pdf, 1.0, -0.5, "tool"); |
| 92 | + assert!((def.quality - 0.0).abs() < 1e-5, "quality below 0.0 must be clamped"); |
| 93 | + } |
| 94 | + |
| 95 | + #[test] |
| 96 | + fn test_quality_at_boundary_zero_not_clamped() { |
| 97 | + let def = TransformDefinition::new(Format::Markdown, Format::Html, 1.0, 0.0, "tool"); |
| 98 | + assert!((def.quality - 0.0).abs() < 1e-5); |
| 99 | + } |
| 100 | + |
| 101 | + #[test] |
| 102 | + fn test_quality_at_boundary_one_not_clamped() { |
| 103 | + let def = TransformDefinition::new(Format::Markdown, Format::Html, 1.0, 1.0, "tool"); |
| 104 | + assert!((def.quality - 1.0).abs() < 1e-5); |
| 105 | + } |
| 106 | + |
| 107 | + // ── clone / equality ────────────────────────────────────────────────────── |
| 108 | + |
| 109 | + #[test] |
| 110 | + fn test_definition_clone() { |
| 111 | + let def = TransformDefinition::new(Format::Html, Format::Pdf, 0.8, 0.85, "wkhtmltopdf"); |
| 112 | + let cloned = def.clone(); |
| 113 | + assert_eq!(def, cloned); |
| 114 | + } |
| 115 | + |
| 116 | + #[test] |
| 117 | + fn test_definition_equality() { |
| 118 | + let a = TransformDefinition::new(Format::Markdown, Format::Html, 0.5, 1.0, "pandoc"); |
| 119 | + let b = TransformDefinition::new(Format::Markdown, Format::Html, 0.5, 1.0, "pandoc"); |
| 120 | + assert_eq!(a, b); |
| 121 | + } |
| 122 | + |
| 123 | + #[test] |
| 124 | + fn test_definition_inequality_by_label() { |
| 125 | + let a = TransformDefinition::new(Format::Markdown, Format::Html, 0.5, 1.0, "pandoc"); |
| 126 | + let b = TransformDefinition::new(Format::Markdown, Format::Html, 0.5, 1.0, "other"); |
| 127 | + assert_ne!(a, b); |
| 128 | + } |
| 129 | + |
| 130 | + #[test] |
| 131 | + fn test_definition_inequality_by_format() { |
| 132 | + let a = TransformDefinition::new(Format::Markdown, Format::Html, 0.5, 1.0, "pandoc"); |
| 133 | + let b = TransformDefinition::new(Format::Markdown, Format::Pdf, 0.5, 1.0, "pandoc"); |
| 134 | + assert_ne!(a, b); |
| 135 | + } |
| 136 | + |
| 137 | + // ── to_edge ─────────────────────────────────────────────────────────────── |
| 138 | + |
| 139 | + #[test] |
| 140 | + fn test_to_edge_preserves_from_to_cost_quality() { |
| 141 | + let def = TransformDefinition::new(Format::Html, Format::Pdf, 0.8, 0.85, "wkhtmltopdf"); |
| 142 | + let edge = def.to_edge(); |
| 143 | + assert_eq!(edge.from, Format::Html); |
| 144 | + assert_eq!(edge.to, Format::Pdf); |
| 145 | + assert!((edge.cost - 0.8).abs() < 1e-5); |
| 146 | + assert!((edge.quality - 0.85).abs() < 1e-5); |
| 147 | + } |
| 148 | + |
| 149 | + #[test] |
| 150 | + fn test_to_edge_from_definition_with_clamped_quality() { |
| 151 | + // Quality of 1.5 is clamped to 1.0 at construction time; edge must reflect that. |
| 152 | + let def = TransformDefinition::new(Format::Markdown, Format::Html, 0.5, 1.5, "tool"); |
| 153 | + let edge = def.to_edge(); |
| 154 | + assert!((edge.quality - 1.0).abs() < 1e-5); |
| 155 | + } |
| 156 | +} |
0 commit comments