use crate::{Color, ModuleSource, ModuleStorage};
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PluginError {
RendererNotFound(String),
EncoderNotFound(String),
InvalidConfig(String),
InvalidModuleGrid,
RenderFailed(String),
EncodeFailed(String),
PostProcessFailed(String),
}
impl fmt::Display for PluginError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RendererNotFound(name) => write!(f, "renderer plugin not found: {name}"),
Self::EncoderNotFound(name) => write!(f, "encoder plugin not found: {name}"),
Self::InvalidConfig(message) => write!(f, "invalid plugin config: {message}"),
Self::InvalidModuleGrid => f.write_str("invalid module grid"),
Self::RenderFailed(message) => write!(f, "renderer plugin failed: {message}"),
Self::EncodeFailed(message) => write!(f, "encoder plugin failed: {message}"),
Self::PostProcessFailed(message) => write!(f, "postprocessor plugin failed: {message}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for PluginError {}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RenderConfig {
format: Option<String>,
options: BTreeMap<String, String>,
}
impl RenderConfig {
#[must_use]
pub const fn new() -> Self {
Self { format: None, options: BTreeMap::new() }
}
#[must_use]
pub fn with_format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
#[must_use]
pub fn with_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert(key.into(), value.into());
self
}
#[must_use]
pub fn format(&self) -> Option<&str> {
self.format.as_deref()
}
#[must_use]
pub fn option(&self, key: &str) -> Option<&str> {
self.options.get(key).map(String::as_str)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EncodeConfig {
options: BTreeMap<String, String>,
}
impl EncodeConfig {
#[must_use]
pub const fn new() -> Self {
Self { options: BTreeMap::new() }
}
#[must_use]
pub fn with_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert(key.into(), value.into());
self
}
#[must_use]
pub fn option(&self, key: &str) -> Option<&str> {
self.options.get(key).map(String::as_str)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RenderOutput {
Text(String),
Bytes(Vec<u8>),
Modules(ModuleGrid),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EncodedOutput {
Modules(ModuleGrid),
Bytes(Vec<u8>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModuleGrid {
modules: Vec<Color>,
width: usize,
height: usize,
}
impl ModuleGrid {
pub fn new(modules: Vec<Color>, width: usize, height: usize) -> Result<Self, PluginError> {
if width == 0 || height == 0 || modules.len() != width * height {
return Err(PluginError::InvalidModuleGrid);
}
Ok(Self { modules, width, height })
}
#[must_use]
pub fn modules_mut(&mut self) -> &mut [Color] {
&mut self.modules
}
}
impl ModuleStorage for ModuleGrid {
fn get(&self, x: usize, y: usize) -> Color {
self.modules[y * self.width + x]
}
fn set(&mut self, x: usize, y: usize, color: Color) {
self.modules[y * self.width + x] = color;
}
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
fn modules(&self) -> &[Color] {
&self.modules
}
}
pub trait DynRenderer {
fn render(&self, code: &dyn ModuleSource) -> Result<RenderOutput, PluginError>;
}
pub trait RendererFactory {
fn build(&self, config: &RenderConfig) -> Box<dyn DynRenderer>;
}
pub trait DynEncoder {
fn encode(&self, input: &[u8]) -> Result<EncodedOutput, PluginError>;
}
pub trait EncoderFactory {
fn build(&self, config: &EncodeConfig) -> Box<dyn DynEncoder>;
}
pub trait PostProcessor {
fn process(&self, modules: &mut dyn ModuleStorage) -> Result<(), PluginError>;
}
pub trait QrPlugin {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn register(&self, registry: &mut PluginRegistry);
}
#[derive(Default)]
pub struct PluginRegistry {
renderers: BTreeMap<String, Box<dyn RendererFactory>>,
encoders: BTreeMap<String, Box<dyn EncoderFactory>>,
postprocessors: Vec<Box<dyn PostProcessor>>,
}
impl PluginRegistry {
#[must_use]
pub const fn new() -> Self {
Self { renderers: BTreeMap::new(), encoders: BTreeMap::new(), postprocessors: Vec::new() }
}
pub fn register_plugin<P: QrPlugin + ?Sized>(&mut self, plugin: &P) {
plugin.register(self);
}
pub fn register_renderer(
&mut self,
name: impl Into<String>,
factory: Box<dyn RendererFactory>,
) -> Option<Box<dyn RendererFactory>> {
self.renderers.insert(name.into(), factory)
}
pub fn register_encoder(
&mut self,
name: impl Into<String>,
factory: Box<dyn EncoderFactory>,
) -> Option<Box<dyn EncoderFactory>> {
self.encoders.insert(name.into(), factory)
}
pub fn register_postprocessor(&mut self, postprocessor: Box<dyn PostProcessor>) {
self.postprocessors.push(postprocessor);
}
#[must_use]
pub fn renderer(&self, name: &str) -> Option<&dyn RendererFactory> {
self.renderers.get(name).map(Box::as_ref)
}
pub fn build_renderer(&self, name: &str, config: &RenderConfig) -> Result<Box<dyn DynRenderer>, PluginError> {
let factory = self.renderer(name).ok_or_else(|| PluginError::RendererNotFound(String::from(name)))?;
Ok(factory.build(config))
}
#[must_use]
pub fn encoder(&self, name: &str) -> Option<&dyn EncoderFactory> {
self.encoders.get(name).map(Box::as_ref)
}
pub fn build_encoder(&self, name: &str, config: &EncodeConfig) -> Result<Box<dyn DynEncoder>, PluginError> {
let factory = self.encoder(name).ok_or_else(|| PluginError::EncoderNotFound(String::from(name)))?;
Ok(factory.build(config))
}
#[must_use]
pub fn postprocessors(&self) -> &[Box<dyn PostProcessor>] {
&self.postprocessors
}
pub fn process_modules(&self, modules: &mut dyn ModuleStorage) -> Result<(), PluginError> {
for postprocessor in &self.postprocessors {
postprocessor.process(modules)?;
}
Ok(())
}
pub fn renderer_names(&self) -> impl Iterator<Item = &str> {
self.renderers.keys().map(String::as_str)
}
pub fn encoder_names(&self) -> impl Iterator<Item = &str> {
self.encoders.keys().map(String::as_str)
}
}
#[cfg(test)]
mod tests {
use super::{
DynEncoder, DynRenderer, EncodeConfig, EncodedOutput, EncoderFactory, ModuleGrid, PluginRegistry,
PostProcessor, QrPlugin, RenderConfig, RenderOutput, RendererFactory,
};
use crate::{Color, ModuleSource, ModuleStorage};
use alloc::boxed::Box;
use alloc::string::ToString;
struct TextRenderer {
dark: char,
}
impl DynRenderer for TextRenderer {
fn render(&self, code: &dyn ModuleSource) -> Result<RenderOutput, super::PluginError> {
let mut out = String::new();
for y in 0..code.height() {
for x in 0..code.width() {
out.push(if code.get(x, y) == Color::Dark { self.dark } else { '.' });
}
}
Ok(RenderOutput::Text(out))
}
}
struct TextRendererFactory;
impl RendererFactory for TextRendererFactory {
fn build(&self, config: &RenderConfig) -> Box<dyn DynRenderer> {
let dark = config.option("dark").and_then(|s| s.chars().next()).unwrap_or('#');
Box::new(TextRenderer { dark })
}
}
struct LengthEncoder;
impl DynEncoder for LengthEncoder {
fn encode(&self, input: &[u8]) -> Result<EncodedOutput, super::PluginError> {
Ok(EncodedOutput::Bytes(input.len().to_string().into_bytes()))
}
}
struct LengthEncoderFactory;
impl EncoderFactory for LengthEncoderFactory {
fn build(&self, _config: &EncodeConfig) -> Box<dyn DynEncoder> {
Box::new(LengthEncoder)
}
}
struct FlipFirst;
impl PostProcessor for FlipFirst {
fn process(&self, modules: &mut dyn ModuleStorage) -> Result<(), super::PluginError> {
modules.set(0, 0, Color::Dark);
Ok(())
}
}
struct FailPostprocessor;
impl PostProcessor for FailPostprocessor {
fn process(&self, _modules: &mut dyn ModuleStorage) -> Result<(), super::PluginError> {
Err(super::PluginError::PostProcessFailed("boom".into()))
}
}
struct DemoPlugin;
impl QrPlugin for DemoPlugin {
fn name(&self) -> &str {
"demo"
}
fn version(&self) -> &str {
"0.1.0"
}
fn register(&self, registry: &mut PluginRegistry) {
registry.register_renderer("text", Box::new(TextRendererFactory));
registry.register_encoder("length", Box::new(LengthEncoderFactory));
registry.register_postprocessor(Box::new(FlipFirst));
}
}
#[test]
fn registry_registers_and_uses_plugin_extension_points() {
let mut registry = PluginRegistry::new();
registry.register_plugin(&DemoPlugin);
let grid = ModuleGrid::new(alloc::vec![Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 2).unwrap();
let config = RenderConfig::new().with_option("dark", "X");
let renderer = registry.build_renderer("text", &config).unwrap();
assert_eq!(renderer.render(&grid).unwrap(), RenderOutput::Text("X..X".into()));
let encoder = registry.build_encoder("length", &EncodeConfig::new()).unwrap();
assert_eq!(encoder.encode(b"abcd").unwrap(), EncodedOutput::Bytes(b"4".to_vec()));
}
#[test]
fn build_renderer_reports_missing_renderer_name() {
let registry = PluginRegistry::new();
assert!(matches!(
registry.build_renderer("missing", &RenderConfig::new()),
Err(super::PluginError::RendererNotFound(name)) if name == "missing"
));
}
#[test]
fn build_encoder_reports_missing_encoder_name() {
let registry = PluginRegistry::new();
assert!(matches!(
registry.build_encoder("missing", &EncodeConfig::new()),
Err(super::PluginError::EncoderNotFound(name)) if name == "missing"
));
}
#[test]
fn registry_keeps_names_deterministic() {
let mut registry = PluginRegistry::new();
registry.register_renderer("zeta", Box::new(TextRendererFactory));
registry.register_renderer("alpha", Box::new(TextRendererFactory));
let names = registry.renderer_names().collect::<Vec<_>>();
assert_eq!(names, ["alpha", "zeta"]);
}
#[test]
fn postprocessors_mutate_module_storage_in_order() {
let mut registry = PluginRegistry::new();
registry.register_postprocessor(Box::new(FlipFirst));
let mut grid = ModuleGrid::new(alloc::vec![Color::Light; 4], 2, 2).unwrap();
registry.process_modules(&mut grid).unwrap();
assert_eq!(ModuleSource::get(&grid, 0, 0), Color::Dark);
}
#[test]
fn process_modules_stops_on_first_postprocessor_error() {
let mut registry = PluginRegistry::new();
registry.register_postprocessor(Box::new(FailPostprocessor));
let mut grid = ModuleGrid::new(alloc::vec![Color::Light; 4], 2, 2).unwrap();
assert!(matches!(
registry.process_modules(&mut grid),
Err(super::PluginError::PostProcessFailed(message)) if message == "boom"
));
}
}