I'm about to rewrite a highly modular CMS in Rust, so my question is if it's even possible to have the "core" application set up extension points (actions/hooks), which other plugins / crates is able to "tab" into.
Something like this would suffice, but how would you do this in Rust? The architecture above uses a plugin registry and initiates each plugin's main method from the core by iterating over each of them. However in Rust, since you can't have a global "modules" variable in e.g. a plugin_registry lib crate, I guess this is not the correct thinking in Rust.
Is there a better and more flexible way to make "plugins" integrate seamlessly with a core application? For example, something like an event dispatcher like WordPress uses?
As Shepmaster said, this is a very general question; hence there are many ways to do what you want. And as already mentioned, too, iron
is a great example of a modular framework.
However, I'll try to give a useful example of how one could implement such a plugin system. For the example I will assume, that there is some kind of main-crate that can load the plugins and "configure" the CMS. This means that the plugins aren't loaded dynamically!
First, lets say we have four crates:
rustpress
: the big main crate with all WordPress-like functionalityrustpress-plugin
: needs to be used by plugin authors (is an own crate in order to avoid using a huge crate like rustpress
for every plugin)rustpress-signature
: here we create our plugin which will add a signature to each postmy-blog
: this will be the main executable that configures our blog and will run as a web server laterThe way to go in Rust are trait
s. You can compare them to interfaces from other languages. We will now design the trait
for plugins which lives in rustpress-plugin
:
pub trait Plugin {
/// Returns the name of the plugin
fn name(&self) -> &str;
/// Hook to change the title of a post
fn filter_title(&self, title: &mut String) {}
/// Hook to change the body of a post
fn filter_body(&self, body: &mut String) {}
}
Note that the filter_*
methods already have a default implementation that does nothing ({}
). This means that plugins don't have to override all methods if they only want to use one hook.
As I said we want to write a plugin that adds our signature to each posts body. To do that we will impl
the trait for our own type (in rustpress-signature
):
extern crate rustpress_plugin;
use rustpress_plugin::Plugin;
pub struct Signature {
pub text: String,
}
impl Plugin for Signature {
fn name(&self) -> &str {
"Signature Plugin v0.1 by ferris"
}
fn filter_body(&self, body: &mut String) {
body.push_str("\n-------\n"); // add visual seperator
body.push_str(&self.text);
}
}
We created a simple type Signature
for which we implement the trait Plugin
. We have to implement the name()
method and we also override the filter_body()
method. In our implementation we just add text to the post body. We did not override filter_title()
because we don't need to.
The CMS has to manage all plugins. I assume that the CMS has a main type RustPress
that will handle everything. It could look like this (in rustpress
):
extern crate rustpress_plugin;
use rustpress_plugin::Plugin;
pub struct RustPress {
// ...
plugins: Vec<Box<Plugin>>,
}
impl RustPress {
pub fn new() -> RustPress {
RustPress {
// ...
plugins: Vec::new(),
}
}
/// Adds a plugin to the stack
pub fn add_plugin<P: Plugin + 'static>(&mut self, plugin: P) {
self.plugins.push(Box::new(plugin));
}
/// Internal function that prepares a post
fn serve_post(&self) {
let mut title = "dummy".to_string();
let mut body = "dummy body".to_string();
for p in &self.plugins {
p.filter_title(&mut title);
p.filter_body(&mut body);
}
// use the finalized title and body now ...
}
/// Starts the CMS ...
pub fn start(&self) {}
}
What we are doing here is storing a Vec
full of boxed plugins (we need to box them, because we want ownership, but traits are unsized). When the CMS then prepare a blog-post, it iterates through all plugins and calls all hooks.
Last step is adding the plugin and starting the CMS (putting it all together). We will do this in the my-blog
crate:
extern crate rustpress;
extern crate rustpress_plugin;
extern crate rustpress_signature;
use rustpress::RustPress;
use rustpress_plugin::Plugin;
use rustpress_signature::Signature;
fn main() {
let mut rustpress = RustPress::new();
// add plugin
let sig = Signature { text: "Ferris loves you <3".into() };
rustpress.add_plugin(sig);
rustpress.start();
}
You also need to add the dependencies to the Cargo.toml
files. I omitted that because it should be fairly easy.
And note again that this is one of many possibilities to create such a system. I hope this example is helpful. You can try it on playground, too.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With