Skip to main content

qrcode_core/
plugin.rs

1//! Explicit plugin registry and object-safe extension points.
2//!
3//! The registry is intentionally local state: callers create a
4//! [`PluginRegistry`], register plugins into it, and pass it to facade or
5//! application code. This keeps plugin behavior deterministic and avoids hidden
6//! global mutation.
7
8use crate::{Color, ModuleSource, ModuleStorage};
9use alloc::boxed::Box;
10use alloc::collections::BTreeMap;
11use alloc::string::String;
12use alloc::vec::Vec;
13use core::fmt;
14
15/// Error type used by object-safe plugin entry points.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum PluginError {
18    /// A named renderer was not present in the registry.
19    RendererNotFound(String),
20
21    /// A named encoder was not present in the registry.
22    EncoderNotFound(String),
23
24    /// The plugin configuration was invalid.
25    InvalidConfig(String),
26
27    /// A module grid shape was invalid.
28    InvalidModuleGrid,
29
30    /// A renderer failed.
31    RenderFailed(String),
32
33    /// An encoder failed.
34    EncodeFailed(String),
35
36    /// A postprocessor failed.
37    PostProcessFailed(String),
38}
39
40impl fmt::Display for PluginError {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Self::RendererNotFound(name) => write!(f, "renderer plugin not found: {name}"),
44            Self::EncoderNotFound(name) => write!(f, "encoder plugin not found: {name}"),
45            Self::InvalidConfig(message) => write!(f, "invalid plugin config: {message}"),
46            Self::InvalidModuleGrid => f.write_str("invalid module grid"),
47            Self::RenderFailed(message) => write!(f, "renderer plugin failed: {message}"),
48            Self::EncodeFailed(message) => write!(f, "encoder plugin failed: {message}"),
49            Self::PostProcessFailed(message) => write!(f, "postprocessor plugin failed: {message}"),
50        }
51    }
52}
53
54#[cfg(feature = "std")]
55impl std::error::Error for PluginError {}
56
57/// Runtime renderer configuration passed to renderer factories.
58#[derive(Clone, Debug, Default, PartialEq, Eq)]
59pub struct RenderConfig {
60    format: Option<String>,
61    options: BTreeMap<String, String>,
62}
63
64impl RenderConfig {
65    /// Creates an empty render configuration.
66    #[must_use]
67    pub const fn new() -> Self {
68        Self { format: None, options: BTreeMap::new() }
69    }
70
71    /// Sets the requested output format.
72    #[must_use]
73    pub fn with_format(mut self, format: impl Into<String>) -> Self {
74        self.format = Some(format.into());
75        self
76    }
77
78    /// Adds or replaces an arbitrary string option.
79    #[must_use]
80    pub fn with_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
81        self.options.insert(key.into(), value.into());
82        self
83    }
84
85    /// Returns the requested output format, if one was configured.
86    #[must_use]
87    pub fn format(&self) -> Option<&str> {
88        self.format.as_deref()
89    }
90
91    /// Returns a string option by key.
92    #[must_use]
93    pub fn option(&self, key: &str) -> Option<&str> {
94        self.options.get(key).map(String::as_str)
95    }
96}
97
98/// Runtime encoder configuration passed to encoder factories.
99#[derive(Clone, Debug, Default, PartialEq, Eq)]
100pub struct EncodeConfig {
101    options: BTreeMap<String, String>,
102}
103
104impl EncodeConfig {
105    /// Creates an empty encode configuration.
106    #[must_use]
107    pub const fn new() -> Self {
108        Self { options: BTreeMap::new() }
109    }
110
111    /// Adds or replaces an arbitrary string option.
112    #[must_use]
113    pub fn with_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
114        self.options.insert(key.into(), value.into());
115        self
116    }
117
118    /// Returns a string option by key.
119    #[must_use]
120    pub fn option(&self, key: &str) -> Option<&str> {
121        self.options.get(key).map(String::as_str)
122    }
123}
124
125/// Type-erased render output returned by dynamic renderers.
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub enum RenderOutput {
128    /// Text output such as SVG, HTML, ANSI, or plain strings.
129    Text(String),
130
131    /// Binary output such as PNG, PDF, or other encoded bytes.
132    Bytes(Vec<u8>),
133
134    /// A module-grid output for plugins that transform but do not serialize.
135    Modules(ModuleGrid),
136}
137
138/// Type-erased encode output returned by dynamic encoders.
139#[derive(Clone, Debug, PartialEq, Eq)]
140pub enum EncodedOutput {
141    /// Encoded QR modules.
142    Modules(ModuleGrid),
143
144    /// Opaque encoded bytes.
145    Bytes(Vec<u8>),
146}
147
148/// Owned mutable module grid used by plugin postprocessors.
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct ModuleGrid {
151    modules: Vec<Color>,
152    width: usize,
153    height: usize,
154}
155
156impl ModuleGrid {
157    /// Creates a module grid from row-major modules.
158    ///
159    /// # Errors
160    ///
161    /// Returns [`PluginError::InvalidModuleGrid`] when the dimensions are zero
162    /// or `modules.len() != width * height`.
163    pub fn new(modules: Vec<Color>, width: usize, height: usize) -> Result<Self, PluginError> {
164        let Some(expected_len) = width.checked_mul(height) else {
165            return Err(PluginError::InvalidModuleGrid);
166        };
167        if width == 0 || height == 0 || modules.len() != expected_len {
168            return Err(PluginError::InvalidModuleGrid);
169        }
170        Ok(Self { modules, width, height })
171    }
172
173    /// Returns the grid modules as a mutable row-major slice.
174    #[must_use]
175    pub fn modules_mut(&mut self) -> &mut [Color] {
176        &mut self.modules
177    }
178}
179
180impl ModuleStorage for ModuleGrid {
181    fn get(&self, x: usize, y: usize) -> Color {
182        assert!(x < self.width && y < self.height, "module coordinates out of bounds");
183        self.modules[y * self.width + x]
184    }
185
186    fn set(&mut self, x: usize, y: usize, color: Color) {
187        assert!(x < self.width && y < self.height, "module coordinates out of bounds");
188        self.modules[y * self.width + x] = color;
189    }
190
191    fn width(&self) -> usize {
192        self.width
193    }
194
195    fn height(&self) -> usize {
196        self.height
197    }
198
199    fn modules(&self) -> &[Color] {
200        &self.modules
201    }
202}
203
204/// Object-safe renderer used by [`RendererFactory`].
205pub trait DynRenderer {
206    /// Renders a module source.
207    ///
208    /// # Errors
209    ///
210    /// Returns [`PluginError`] when the renderer cannot produce output.
211    fn render(&self, code: &dyn ModuleSource) -> Result<RenderOutput, PluginError>;
212}
213
214/// Factory for object-safe renderers.
215pub trait RendererFactory {
216    /// Builds a renderer from `config`.
217    fn build(&self, config: &RenderConfig) -> Box<dyn DynRenderer>;
218
219    /// Validates a renderer configuration before building it.
220    ///
221    /// The default implementation accepts every configuration, preserving
222    /// compatibility with existing plugin factories. Factories with
223    /// renderer-specific options can override this hook so
224    /// [`PluginRegistry::build_renderer`] reports invalid input before a
225    /// renderer is constructed.
226    fn validate_config(&self, _config: &RenderConfig) -> Result<(), PluginError> {
227        Ok(())
228    }
229}
230
231/// Object-safe encoder used by [`EncoderFactory`].
232pub trait DynEncoder {
233    /// Encodes raw input.
234    ///
235    /// # Errors
236    ///
237    /// Returns [`PluginError`] when the encoder cannot produce output.
238    fn encode(&self, input: &[u8]) -> Result<EncodedOutput, PluginError>;
239}
240
241/// Factory for object-safe encoders.
242pub trait EncoderFactory {
243    /// Builds an encoder from `config`.
244    fn build(&self, config: &EncodeConfig) -> Box<dyn DynEncoder>;
245}
246
247/// Object-safe postprocessor for in-place module-grid transforms.
248pub trait PostProcessor {
249    /// Processes `modules` in place.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`PluginError`] when processing fails.
254    fn process(&self, modules: &mut dyn ModuleStorage) -> Result<(), PluginError>;
255}
256
257/// A plugin that registers one or more extension points.
258pub trait QrPlugin {
259    /// Stable plugin name.
260    fn name(&self) -> &str;
261
262    /// Plugin version string.
263    fn version(&self) -> &str;
264
265    /// Registers this plugin's extension points into `registry`.
266    fn register(&self, registry: &mut PluginRegistry);
267}
268
269/// Explicit plugin registry.
270#[derive(Default)]
271pub struct PluginRegistry {
272    plugins: BTreeMap<String, String>,
273    renderers: BTreeMap<String, Box<dyn RendererFactory>>,
274    encoders: BTreeMap<String, Box<dyn EncoderFactory>>,
275    postprocessors: Vec<Box<dyn PostProcessor>>,
276}
277
278impl PluginRegistry {
279    /// Creates an empty registry.
280    #[must_use]
281    pub const fn new() -> Self {
282        Self {
283            plugins: BTreeMap::new(),
284            renderers: BTreeMap::new(),
285            encoders: BTreeMap::new(),
286            postprocessors: Vec::new(),
287        }
288    }
289
290    /// Registers all extension points provided by `plugin`.
291    pub fn register_plugin<P: QrPlugin + ?Sized>(&mut self, plugin: &P) {
292        self.plugins.insert(String::from(plugin.name()), String::from(plugin.version()));
293        plugin.register(self);
294    }
295
296    /// Returns the registered version for a plugin name.
297    #[must_use]
298    pub fn plugin_version(&self, name: &str) -> Option<&str> {
299        self.plugins.get(name).map(String::as_str)
300    }
301
302    /// Iterates registered plugin names in deterministic order.
303    pub fn plugin_names(&self) -> impl Iterator<Item = &str> {
304        self.plugins.keys().map(String::as_str)
305    }
306
307    /// Registers or replaces a renderer factory by name.
308    pub fn register_renderer(
309        &mut self,
310        name: impl Into<String>,
311        factory: Box<dyn RendererFactory>,
312    ) -> Option<Box<dyn RendererFactory>> {
313        self.renderers.insert(name.into(), factory)
314    }
315
316    /// Registers or replaces an encoder factory by name.
317    pub fn register_encoder(
318        &mut self,
319        name: impl Into<String>,
320        factory: Box<dyn EncoderFactory>,
321    ) -> Option<Box<dyn EncoderFactory>> {
322        self.encoders.insert(name.into(), factory)
323    }
324
325    /// Appends a postprocessor to the registry.
326    pub fn register_postprocessor(&mut self, postprocessor: Box<dyn PostProcessor>) {
327        self.postprocessors.push(postprocessor);
328    }
329
330    /// Returns a renderer factory by name.
331    #[must_use]
332    pub fn renderer(&self, name: &str) -> Option<&dyn RendererFactory> {
333        self.renderers.get(name).map(Box::as_ref)
334    }
335
336    /// Builds a renderer by name.
337    ///
338    /// # Errors
339    ///
340    /// Returns [`PluginError::RendererNotFound`] when no renderer factory is
341    /// registered with `name`.
342    pub fn build_renderer(&self, name: &str, config: &RenderConfig) -> Result<Box<dyn DynRenderer>, PluginError> {
343        let factory = self.renderer(name).ok_or_else(|| PluginError::RendererNotFound(String::from(name)))?;
344        factory.validate_config(config)?;
345        Ok(factory.build(config))
346    }
347
348    /// Returns an encoder factory by name.
349    #[must_use]
350    pub fn encoder(&self, name: &str) -> Option<&dyn EncoderFactory> {
351        self.encoders.get(name).map(Box::as_ref)
352    }
353
354    /// Builds an encoder by name.
355    ///
356    /// # Errors
357    ///
358    /// Returns [`PluginError::EncoderNotFound`] when no encoder factory is
359    /// registered with `name`.
360    pub fn build_encoder(&self, name: &str, config: &EncodeConfig) -> Result<Box<dyn DynEncoder>, PluginError> {
361        let factory = self.encoder(name).ok_or_else(|| PluginError::EncoderNotFound(String::from(name)))?;
362        Ok(factory.build(config))
363    }
364
365    /// Returns all postprocessors in registration order.
366    #[must_use]
367    pub fn postprocessors(&self) -> &[Box<dyn PostProcessor>] {
368        &self.postprocessors
369    }
370
371    /// Applies all registered postprocessors in registration order.
372    ///
373    /// # Errors
374    ///
375    /// Returns the first [`PluginError`] reported by a postprocessor.
376    pub fn process_modules(&self, modules: &mut dyn ModuleStorage) -> Result<(), PluginError> {
377        for postprocessor in &self.postprocessors {
378            postprocessor.process(modules)?;
379        }
380        Ok(())
381    }
382
383    /// Iterates renderer names in deterministic order.
384    pub fn renderer_names(&self) -> impl Iterator<Item = &str> {
385        self.renderers.keys().map(String::as_str)
386    }
387
388    /// Iterates encoder names in deterministic order.
389    pub fn encoder_names(&self) -> impl Iterator<Item = &str> {
390        self.encoders.keys().map(String::as_str)
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::{
397        DynEncoder, DynRenderer, EncodeConfig, EncodedOutput, EncoderFactory, ModuleGrid, PluginRegistry,
398        PostProcessor, QrPlugin, RenderConfig, RenderOutput, RendererFactory,
399    };
400    use crate::{Color, ModuleSource, ModuleStorage};
401    use alloc::boxed::Box;
402    use alloc::string::ToString;
403    use std::panic::{AssertUnwindSafe, catch_unwind};
404
405    struct TextRenderer {
406        dark: char,
407    }
408
409    impl DynRenderer for TextRenderer {
410        fn render(&self, code: &dyn ModuleSource) -> Result<RenderOutput, super::PluginError> {
411            let mut out = String::new();
412            for y in 0..code.height() {
413                for x in 0..code.width() {
414                    out.push(if code.get(x, y) == Color::Dark { self.dark } else { '.' });
415                }
416            }
417            Ok(RenderOutput::Text(out))
418        }
419    }
420
421    struct TextRendererFactory;
422
423    impl RendererFactory for TextRendererFactory {
424        fn build(&self, config: &RenderConfig) -> Box<dyn DynRenderer> {
425            let dark = config.option("dark").and_then(|s| s.chars().next()).unwrap_or('#');
426            Box::new(TextRenderer { dark })
427        }
428    }
429
430    struct LengthEncoder;
431
432    impl DynEncoder for LengthEncoder {
433        fn encode(&self, input: &[u8]) -> Result<EncodedOutput, super::PluginError> {
434            Ok(EncodedOutput::Bytes(input.len().to_string().into_bytes()))
435        }
436    }
437
438    struct LengthEncoderFactory;
439
440    impl EncoderFactory for LengthEncoderFactory {
441        fn build(&self, _config: &EncodeConfig) -> Box<dyn DynEncoder> {
442            Box::new(LengthEncoder)
443        }
444    }
445
446    struct FlipFirst;
447
448    impl PostProcessor for FlipFirst {
449        fn process(&self, modules: &mut dyn ModuleStorage) -> Result<(), super::PluginError> {
450            modules.set(0, 0, Color::Dark);
451            Ok(())
452        }
453    }
454
455    struct FailPostprocessor;
456
457    impl PostProcessor for FailPostprocessor {
458        fn process(&self, _modules: &mut dyn ModuleStorage) -> Result<(), super::PluginError> {
459            Err(super::PluginError::PostProcessFailed("boom".into()))
460        }
461    }
462
463    struct DemoPlugin;
464
465    impl QrPlugin for DemoPlugin {
466        fn name(&self) -> &str {
467            "demo"
468        }
469
470        fn version(&self) -> &str {
471            "0.1.0"
472        }
473
474        fn register(&self, registry: &mut PluginRegistry) {
475            registry.register_renderer("text", Box::new(TextRendererFactory));
476            registry.register_encoder("length", Box::new(LengthEncoderFactory));
477            registry.register_postprocessor(Box::new(FlipFirst));
478        }
479    }
480
481    #[test]
482    fn registry_registers_and_uses_plugin_extension_points() {
483        let mut registry = PluginRegistry::new();
484        registry.register_plugin(&DemoPlugin);
485
486        let grid = ModuleGrid::new(alloc::vec![Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 2).unwrap();
487        let config = RenderConfig::new().with_option("dark", "X");
488        let renderer = registry.build_renderer("text", &config).unwrap();
489        assert_eq!(renderer.render(&grid).unwrap(), RenderOutput::Text("X..X".into()));
490
491        let encoder = registry.build_encoder("length", &EncodeConfig::new()).unwrap();
492        assert_eq!(encoder.encode(b"abcd").unwrap(), EncodedOutput::Bytes(b"4".to_vec()));
493        assert_eq!(registry.plugin_version("demo"), Some("0.1.0"));
494        assert_eq!(registry.plugin_names().collect::<Vec<_>>(), ["demo"]);
495    }
496
497    #[test]
498    fn build_renderer_reports_missing_renderer_name() {
499        let registry = PluginRegistry::new();
500
501        assert!(matches!(
502            registry.build_renderer("missing", &RenderConfig::new()),
503            Err(super::PluginError::RendererNotFound(name)) if name == "missing"
504        ));
505    }
506
507    #[test]
508    fn build_encoder_reports_missing_encoder_name() {
509        let registry = PluginRegistry::new();
510
511        assert!(matches!(
512            registry.build_encoder("missing", &EncodeConfig::new()),
513            Err(super::PluginError::EncoderNotFound(name)) if name == "missing"
514        ));
515    }
516
517    #[test]
518    fn registry_keeps_names_deterministic() {
519        let mut registry = PluginRegistry::new();
520        registry.register_renderer("zeta", Box::new(TextRendererFactory));
521        registry.register_renderer("alpha", Box::new(TextRendererFactory));
522
523        let names = registry.renderer_names().collect::<Vec<_>>();
524        assert_eq!(names, ["alpha", "zeta"]);
525    }
526
527    #[test]
528    fn postprocessors_mutate_module_storage_in_order() {
529        let mut registry = PluginRegistry::new();
530        registry.register_postprocessor(Box::new(FlipFirst));
531        let mut grid = ModuleGrid::new(alloc::vec![Color::Light; 4], 2, 2).unwrap();
532
533        registry.process_modules(&mut grid).unwrap();
534
535        assert_eq!(ModuleSource::get(&grid, 0, 0), Color::Dark);
536    }
537
538    #[test]
539    fn process_modules_stops_on_first_postprocessor_error() {
540        let mut registry = PluginRegistry::new();
541        registry.register_postprocessor(Box::new(FailPostprocessor));
542        let mut grid = ModuleGrid::new(alloc::vec![Color::Light; 4], 2, 2).unwrap();
543
544        assert!(matches!(
545            registry.process_modules(&mut grid),
546            Err(super::PluginError::PostProcessFailed(message)) if message == "boom"
547        ));
548    }
549
550    #[test]
551    fn module_grid_rejects_dimension_multiplication_overflow() {
552        assert_eq!(ModuleGrid::new(alloc::vec![], usize::MAX, 2), Err(super::PluginError::InvalidModuleGrid));
553    }
554
555    #[test]
556    fn rectangular_module_grid_reads_and_writes_each_coordinate() {
557        let mut grid = ModuleGrid::new(alloc::vec![Color::Light; 6], 3, 2).unwrap();
558        for y in 0_usize..2 {
559            for x in 0_usize..3 {
560                let color = if (x + y).is_multiple_of(2) { Color::Dark } else { Color::Light };
561                ModuleStorage::set(&mut grid, x, y, color);
562                assert_eq!(ModuleStorage::get(&grid, x, y), color);
563                assert_eq!(ModuleSource::get(&grid, x, y), color);
564            }
565        }
566        assert_eq!(ModuleSource::width(&grid), 3);
567        assert_eq!(ModuleSource::height(&grid), 2);
568        assert_eq!(ModuleSource::row(&grid, 1), &[Color::Light, Color::Dark, Color::Light]);
569    }
570
571    #[test]
572    fn module_grid_rejects_invalid_coordinates_before_mutating_a_different_row() {
573        let mut grid = ModuleGrid::new(alloc::vec![Color::Light; 6], 3, 2).unwrap();
574        for (x, y) in [(3, 0), (0, 2), (usize::MAX, 0), (0, usize::MAX), (usize::MAX, usize::MAX)] {
575            assert!(catch_unwind(|| ModuleStorage::get(&grid, x, y)).is_err());
576            assert!(catch_unwind(|| ModuleSource::get(&grid, x, y)).is_err());
577            assert!(catch_unwind(AssertUnwindSafe(|| ModuleStorage::set(&mut grid, x, y, Color::Dark))).is_err());
578            assert_eq!(ModuleSource::modules(&grid), &[Color::Light; 6]);
579        }
580    }
581}