Skip to content

Instantly share code, notes, and snippets.

@listochkin
Last active June 29, 2023 10:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save listochkin/d07e50389ad20b8fe21b6d7e1a7b8dd9 to your computer and use it in GitHub Desktop.
Save listochkin/d07e50389ad20b8fe21b6d7e1a7b8dd9 to your computer and use it in GitHub Desktop.
Exercise: custom data for a plugin system

Custom Data for a Plugin System

This exercise is inspired by a real-world scenario encountered by one of Ferrous Systems' customers.

Imagine that we are building the next version of the best code editor out there, and for it we want to introduce a system of plugins that require configuration. Some configuration options are common: every plugin has a name, for example. Other settings can be absolutely unique.

We want to design a type that would allow us to store arbitrary custom data. It should play nicely with the rest of our code base. We want to be able to:

  • Debug-print our plugin configuration
  • compare different configs
  • Clone them
  • move them between threads (maybe we're doing some async-await, who knows?)

Thankfully, we can make certain assumptions about the types for custom config settings. They all are Debug + PartialEq + Clone + Send + Sync + 'static. Ideally we don't want our plugins to implement any other traits, but if we have to we can ask them to add impl Unknown for Type {} for them. That's what we did in the code below.

What are our goals for the API?

  1. No generics in PluginConfig struct.
  2. Overall list of available plugins is unknown. We can't create an enum listing them all.
  3. We should be able to construct a Vec<PluginConfig>.
  4. PluginConfig should implement Debug, PartialEq, Clone
  5. PluginConfig should be Send + Sync + 'static
  6. Unknown trait should implementable without requiring plugin authors to write any extra methods. Adding dependencies to other traits, for example: Unknown: Debug + ..., or having default methods implemented is fine.
use custom_data::{CustomData, Unknown};

#[derive(Debug, Clone)]
struct PluginConfig {
    name: String,
    custom_options: CustomData,
}

impl PluginConfig {
    fn new<T: Unknown>(name: &str, custom_options: T) -> PluginConfig {
        PluginConfig {
            name: name.to_string(),
            custom_options: CustomData::new(custom_options),
        }
    }
}

// Plugin authors provide us these types
#[derive(Debug, PartialEq, Clone)]
struct RustAnalyzerOptions {
    lsp_path: String,
}
impl Unknown for RustAnalyzerOptions {}

#[derive(Debug, PartialEq, Clone)]
struct SpellCheckerOptions {
    oxford_spelling: bool,
    check_capitalized_words: bool,
}
impl Unknown for SpellCheckerOptions {}

fn main() {
    let spell_checker = PluginConfig::new(
        "SpellCheck",
        SpellCheckerOptions {
            oxford_spelling: true,
            check_capitalized_words: false,
        },
    );
    let rust_analyzer = PluginConfig::new(
        "Rust Analyzer",
        RustAnalyzerOptions {
            lsp_path: "/usr/local/bin/rust_analyzer".to_string(),
        },
    );

    // Requirement tests.
    // Tip: uncomment them one by one as you work on a solution

    // 1. Configs can be cloned and Debug-printed
    // let plugins = vec![spell_checker.clone(), rust_analyzer.clone()];
    // println!("{:#?}", plugins);

    // 2. There should be a way to downcast from `CustomData` to a specific
    //    type for read-only access
    // if let Ok(ra_options) = rust_analyzer
    //     .custom_options
    //     .downcast::<RustAnalyzerOptions>()
    // {
    //     println!("R-A Path: {}", ra_options.lsp_path);
    // }

    // 3. Configs implement `PartialEq`
    // Generally, this is the hardest part
    // assert_eq!(spell_checker, spell_checker);
    // assert_ne!(spell_checker, rust_analyzer);

    // 4. Finally, we should be able to move configs between threads
    // std::thread::scope(|s| {
    //     s.spawn(|| {
    //         let plugins = vec![spell_checker, rust_analyzer];
    //         println!("{:#?}", plugins);
    //     });
    // });
}

mod custom_data {
    // You would be working in this module.
    // Generally, you won't need to change anything in code above this line,
    // apart from sprinkling some derives
    // Here, however, no code is sacred. Go to town!

    pub struct DowncastingError {}

    #[derive(Debug, Clone)]
    pub struct CustomData {
        // This should store an `Unknown`
    }

    impl CustomData {
        pub fn new<T: Unknown>(data: T) -> Self {
            CustomData {}
        }

        pub fn downcast<T: Unknown>(&self) -> Result<&T, DowncastingError> {
            todo!()
        }
    }

    pub trait Unknown {}

    // The word "downcast" above is a hint. The solution involves
    // - `std::any::Any`
    // - trait objects (dyn Trait)
    // - blanket implementations
    // - careful generic programming
    // - be mindful of intentional *and* unintentional `Deref` conversions!
    //
    // The solution I've come up with:
    // - doesn't have any `unsafe` code
    // - doesn't use any third-party crates
    // - doesn't use macros
    //
    // You solution can be entirely different!
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment