Skip to main content

bwipp/
options.rs

1//! Rendering options shared across symbologies.
2//!
3//! Each symbology accepts additional symbology-specific options; those go in
4//! [`Options::extras`] as `(key, value)` string pairs to mirror BWIPP's flag
5//! convention (`includecheck=true`, `eclevel=M`, etc.).
6
7/// Module-level rendering options.
8#[derive(Debug, Clone)]
9pub struct Options {
10    /// Pixel multiplier for raster output, or stroke unit for vector output.
11    /// Default: 4.
12    pub scale: u32,
13    /// Bar height in modules (only meaningful for 1D codes). Default: 50.
14    pub bar_height: u32,
15    /// Quiet zone in modules around the barcode. Default: 4.
16    pub quiet_zone: u32,
17    /// Whether to render human-readable text under 1D symbologies.
18    pub include_text: bool,
19    /// Foreground color as RGB. Default: black.
20    pub foreground: [u8; 3],
21    /// Background color as RGB. Default: white.
22    pub background: [u8; 3],
23    /// Symbology-specific options (e.g. `("eclevel", "M")`, `("type", "29")`).
24    pub extras: Vec<(String, String)>,
25}
26
27impl Default for Options {
28    fn default() -> Self {
29        Self {
30            scale: 4,
31            bar_height: 50,
32            quiet_zone: 4,
33            include_text: false,
34            foreground: [0, 0, 0],
35            background: [255, 255, 255],
36            extras: Vec::new(),
37        }
38    }
39}
40
41impl Options {
42    /// Look up an extra option by key (case-sensitive, like BWIPP).
43    ///
44    /// # Example
45    ///
46    /// ```
47    /// use bwipp::Options;
48    ///
49    /// let opts = Options::default().with("eclevel", "H");
50    /// assert_eq!(opts.get("eclevel"), Some("H"));
51    /// assert_eq!(opts.get("missing"), None);
52    /// ```
53    pub fn get(&self, key: &str) -> Option<&str> {
54        self.extras
55            .iter()
56            .find(|(k, _)| k == key)
57            .map(|(_, v)| v.as_str())
58    }
59
60    /// Add an option `key=value`. Designed to be chained as a builder.
61    ///
62    /// # Example
63    ///
64    /// ```
65    /// use bwipp::{Options, Symbology, render_svg};
66    ///
67    /// let opts = Options::default()
68    ///     .with("eclevel", "Q")
69    ///     .with("version", "5");
70    /// let svg = render_svg(Symbology::QrCode, "Hello", &opts).unwrap();
71    /// assert!(svg.starts_with("<svg"));
72    /// ```
73    #[must_use]
74    pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
75        self.extras.push((key.into(), value.into()));
76        self
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    /// Stage 11.A8c — pin `Options::default()` field values. Kills
85    /// any mutation that changes a default literal (e.g. scale=4 →
86    /// scale=0, quiet_zone=4 → quiet_zone=0).
87    #[test]
88    fn default_field_values() {
89        let opts = Options::default();
90        assert_eq!(opts.scale, 4);
91        assert_eq!(opts.bar_height, 50);
92        assert_eq!(opts.quiet_zone, 4);
93        assert!(!opts.include_text);
94        assert_eq!(opts.foreground, [0, 0, 0]);
95        assert_eq!(opts.background, [255, 255, 255]);
96        assert!(opts.extras.is_empty());
97    }
98
99    /// Stage 11.A8c — pin `Options::get` lookup semantics: returns
100    /// Some for known keys, None for unknown, case-sensitive (BWIPP
101    /// behavior). Kills `find` predicate mutations.
102    #[test]
103    fn get_lookup_semantics() {
104        let opts = Options::default().with("eclevel", "H").with("version", "5");
105        assert_eq!(opts.get("eclevel"), Some("H"));
106        assert_eq!(opts.get("version"), Some("5"));
107        assert_eq!(opts.get("missing"), None);
108        // Case-sensitive.
109        assert_eq!(opts.get("ECLEVEL"), None);
110        assert_eq!(opts.get("Eclevel"), None);
111        // Empty key.
112        assert_eq!(opts.get(""), None);
113    }
114
115    /// Stage 11.A8c — pin `Options::with` builder behavior. Kills
116    /// `push` order or value-swap mutations.
117    #[test]
118    fn with_builder_appends_and_returns() {
119        let opts = Options::default().with("a", "1").with("b", "2");
120        assert_eq!(opts.extras.len(), 2);
121        // Order preserved: first append at index 0, second at index 1.
122        assert_eq!(opts.extras[0], ("a".to_string(), "1".to_string()));
123        assert_eq!(opts.extras[1], ("b".to_string(), "2".to_string()));
124        // Subsequent .with for the same key appends a duplicate (BWIPP
125        // matches by key from front, so later wins for `get`? Let's
126        // verify both behaviors are pinned).
127        let opts = Options::default().with("k", "first").with("k", "second");
128        assert_eq!(opts.extras.len(), 2);
129        // get returns the FIRST match.
130        assert_eq!(opts.get("k"), Some("first"));
131    }
132}