1use 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#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum PluginError {
18 RendererNotFound(String),
20
21 EncoderNotFound(String),
23
24 InvalidConfig(String),
26
27 InvalidModuleGrid,
29
30 RenderFailed(String),
32
33 EncodeFailed(String),
35
36 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
59pub struct RenderConfig {
60 format: Option<String>,
61 options: BTreeMap<String, String>,
62}
63
64impl RenderConfig {
65 #[must_use]
67 pub const fn new() -> Self {
68 Self { format: None, options: BTreeMap::new() }
69 }
70
71 #[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 #[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 #[must_use]
87 pub fn format(&self) -> Option<&str> {
88 self.format.as_deref()
89 }
90
91 #[must_use]
93 pub fn option(&self, key: &str) -> Option<&str> {
94 self.options.get(key).map(String::as_str)
95 }
96}
97
98#[derive(Clone, Debug, Default, PartialEq, Eq)]
100pub struct EncodeConfig {
101 options: BTreeMap<String, String>,
102}
103
104impl EncodeConfig {
105 #[must_use]
107 pub const fn new() -> Self {
108 Self { options: BTreeMap::new() }
109 }
110
111 #[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 #[must_use]
120 pub fn option(&self, key: &str) -> Option<&str> {
121 self.options.get(key).map(String::as_str)
122 }
123}
124
125#[derive(Clone, Debug, PartialEq, Eq)]
127pub enum RenderOutput {
128 Text(String),
130
131 Bytes(Vec<u8>),
133
134 Modules(ModuleGrid),
136}
137
138#[derive(Clone, Debug, PartialEq, Eq)]
140pub enum EncodedOutput {
141 Modules(ModuleGrid),
143
144 Bytes(Vec<u8>),
146}
147
148#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct ModuleGrid {
151 modules: Vec<Color>,
152 width: usize,
153 height: usize,
154}
155
156impl ModuleGrid {
157 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 #[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
204pub trait DynRenderer {
206 fn render(&self, code: &dyn ModuleSource) -> Result<RenderOutput, PluginError>;
212}
213
214pub trait RendererFactory {
216 fn build(&self, config: &RenderConfig) -> Box<dyn DynRenderer>;
218
219 fn validate_config(&self, _config: &RenderConfig) -> Result<(), PluginError> {
227 Ok(())
228 }
229}
230
231pub trait DynEncoder {
233 fn encode(&self, input: &[u8]) -> Result<EncodedOutput, PluginError>;
239}
240
241pub trait EncoderFactory {
243 fn build(&self, config: &EncodeConfig) -> Box<dyn DynEncoder>;
245}
246
247pub trait PostProcessor {
249 fn process(&self, modules: &mut dyn ModuleStorage) -> Result<(), PluginError>;
255}
256
257pub trait QrPlugin {
259 fn name(&self) -> &str;
261
262 fn version(&self) -> &str;
264
265 fn register(&self, registry: &mut PluginRegistry);
267}
268
269#[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 #[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 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 #[must_use]
298 pub fn plugin_version(&self, name: &str) -> Option<&str> {
299 self.plugins.get(name).map(String::as_str)
300 }
301
302 pub fn plugin_names(&self) -> impl Iterator<Item = &str> {
304 self.plugins.keys().map(String::as_str)
305 }
306
307 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 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 pub fn register_postprocessor(&mut self, postprocessor: Box<dyn PostProcessor>) {
327 self.postprocessors.push(postprocessor);
328 }
329
330 #[must_use]
332 pub fn renderer(&self, name: &str) -> Option<&dyn RendererFactory> {
333 self.renderers.get(name).map(Box::as_ref)
334 }
335
336 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 #[must_use]
350 pub fn encoder(&self, name: &str) -> Option<&dyn EncoderFactory> {
351 self.encoders.get(name).map(Box::as_ref)
352 }
353
354 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 #[must_use]
367 pub fn postprocessors(&self) -> &[Box<dyn PostProcessor>] {
368 &self.postprocessors
369 }
370
371 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 pub fn renderer_names(&self) -> impl Iterator<Item = &str> {
385 self.renderers.keys().map(String::as_str)
386 }
387
388 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}