From ebf906ea51d1925df955a75f7b0d4ed1f1d398a7 Mon Sep 17 00:00:00 2001 From: ppom Date: Fri, 31 Oct 2025 12:00:00 +0100 Subject: [PATCH] Better doc and errors --- plugins/reaction-plugin/src/lib.rs | 106 ++++++++++++++++----------- src/daemon/plugin/mod.rs | 39 ++++++++-- tests/test-conf/test-cluster.jsonnet | 4 +- 3 files changed, 97 insertions(+), 52 deletions(-) diff --git a/plugins/reaction-plugin/src/lib.rs b/plugins/reaction-plugin/src/lib.rs index ccc9355..32d3ca5 100644 --- a/plugins/reaction-plugin/src/lib.rs +++ b/plugins/reaction-plugin/src/lib.rs @@ -1,10 +1,67 @@ //! This crate defines the API between reaction's core and plugins. //! +//! Plugins must be written in Rust. +//! +//! This documentation assumes the reader has some knowledge of Rust. +//! However, if you find that something is unclear, don't hesitate to +//! [ask for help](https://framagit.org/ppom/reaction/#help), even if you're new to Rust. +//! //! To implement a plugin, one has to provide an implementation of [`PluginInfo`], that provides //! the entrypoint for a plugin. -//! It permits to define 0 to n (stream, filter, action) custom types. +//! It permits to define `0` to `n` custom stream and action types. +//! +//! ## Naming & calling conventions +//! +//! Your plugin should be named `reaction-plugin-$NAME`, eg. `reaction-plugin-postgresql`. +//! It will be invoked with one positional argument "serve". +//! ``` +//! reaction-plugin-$NAME serve +//! ``` +//! This can be useful if you want to provide CLI functionnality to your users, +//! so you can distinguish between a human user and reaction. +//! +//! It will be executed in its own directory, in which it should have write access. +//! The directory is `$reaction_state_directory/plugin_data/$NAME`. +//! reaction's [state_directory](https://reaction.ppom.me/reference.html#state_directory) +//! defaults to its working directory. +//! +//! ## Communication +//! +//! Communication between the plugin and reaction is based on [`remoc`], which permits to multiplex channels and remote objects/functions/trait +//! calls over a single transport channel. +//! The channels is made of stdin and stdout, so don't use them for something else. +//! +//! [`remoc`] build upon [`tokio`], so you'll need to use tokio too. +//! +//! ### Errors +//! +//! Errors can be printed to stderr. +//! They'll be captured line by line and re-printed by reaction, with the plugin name prepended. +//! +//! A line can start with `DEBUG `, `INFO `, `WARN `, `ERROR `. +//! If the starts with none of the above, the line is assumed to be an error. +//! +//! Example: +//! Those lines: +//! ```log +//! WARN This is an official warning from the plugin +//! Freeeee errrooooorrr +//! ``` +//! Will become: +//! ```log +//! WARN plugin test: This is an official warning from the plugin +//! ERROR plugin test: Freeeee errrooooorrr +//! ``` +//! +//! ## Starting template +//! +//! ```bash +//! cargo new reaction-plugin-$NAME +//! cd reaction-plugin-$NAME +//! cargo add reaction-plugin tokio +//! vim src/main.rs +//! ``` //! -//! Minimal example: //! `src/main.rs` //! ```rust //! use reaction_plugin::PluginInfo; @@ -20,50 +77,13 @@ //! //! impl PluginInfo for Plugin { //! // ... +//! // Your IDE should propose to implement missing members of the `Plugin` trait +//! // ... //! } //! ``` //! -//! ## Naming & calling conventions -//! -//! Your plugin should be named `reaction-plugin-$NAME`, eg. `reaction-plugin-postgresql`. -//! It will be invoked with one positional argument "serve". -//! ``` -//! reaction-plugin-$NAME serve -//! ``` -//! This can be useful if you want to provide CLI functionnality to your users. -//! -//! It will be run in its own directory, in which it should have write access. -//! -//! ## Communication -//! -//! Communication between the plugin and reaction is based on [`remoc`], which permits to multiplex channels and remote objects/functions/trait -//! calls over a single transport channel. -//! The channels used are stdin and stdout, so you can't use them for something else. -//! -//! ### Errors -//! -//! Errors can be printed to stderr. -//! They'll be captured line by line and re-printed by reaction, with the plugin name prepended. -//! -//! A line can start with `DEBUG `, `INFO `, `WARN `, `ERROR `. -//! If the starts with none of the above, the line is assumed to be an error. -//! -//! Examples: -//! ```log -//! WARN This is an official warning from the plugin -//! # will become: -//! WARN plugin test: This is an official warning from the plugin -//! -//! Freeeee errrooooorrr -//! # will become: -//! ERROR plugin test: Freeeee errrooooorrr -//! ``` -//! -//! ## Starting template -//! -//! Core plugins can be found here: -//! The "virtual" plugin is the simplest and can serve as a template. -//! You'll have to adjust dependencies versions in `Cargo.toml`. +//! Core plugins can be found here: . +//! The "virtual" plugin is the simplest and can serve as a good complete example. use std::{collections::BTreeSet, error::Error, fmt::Display, process::exit}; diff --git a/src/daemon/plugin/mod.rs b/src/daemon/plugin/mod.rs index a332c7a..340e2c1 100644 --- a/src/daemon/plugin/mod.rs +++ b/src/daemon/plugin/mod.rs @@ -12,7 +12,7 @@ use tokio::{ process::{Child, ChildStderr}, time::sleep, }; -use tracing::error; +use tracing::{error, info}; use crate::{ concepts::{Config, Plugin}, @@ -225,9 +225,15 @@ impl Plugins { stream_type: String, config: Value, ) -> Result { - let plugin_name = self.streams.get(&stream_type).ok_or(format!( - "No plugin provided the stream type '{stream_type}'" - ))?; + let plugin_name = match self.streams.get(&stream_type) { + Some(name) => name, + None => { + display_plugin_exposed_types(&self.streams, "stream"); + return Err(format!( + "No plugin provided the stream type '{stream_type}'" + )); + } + }; let plugin = self.plugins.get_mut(plugin_name).unwrap(); @@ -246,9 +252,15 @@ impl Plugins { config: Value, patterns: Vec, ) -> Result { - let plugin_name = self.actions.get(&action_type).ok_or(format!( - "No plugin provided the action type '{action_type}'" - ))?; + let plugin_name = match self.actions.get(&action_type) { + Some(name) => name, + None => { + display_plugin_exposed_types(&self.actions, "action"); + return Err(format!( + "No plugin provided the action type '{action_type}'" + )); + } + }; let plugin = self.plugins.get_mut(plugin_name).unwrap(); @@ -289,3 +301,16 @@ impl Plugins { } } } + +fn display_plugin_exposed_types(type_to_plugin: &BTreeMap, name: &str) { + let mut plugin_to_types: BTreeMap<&str, Vec<&str>> = BTreeMap::new(); + for (type_, plugin) in type_to_plugin { + plugin_to_types.entry(plugin).or_default().push(type_); + } + for (plugin, types) in plugin_to_types { + info!( + "Plugin {plugin} exposes those {name} types: '{}'", + types.join("', '") + ); + } +} diff --git a/tests/test-conf/test-cluster.jsonnet b/tests/test-conf/test-cluster.jsonnet index e73d89f..a635c73 100644 --- a/tests/test-conf/test-cluster.jsonnet +++ b/tests/test-conf/test-cluster.jsonnet @@ -26,14 +26,14 @@ regex: ['^$'], actions: { a0: { - type: 'virtual', + type: 'cluster_send', options: { send: 'a0 ', to: 's1', }, }, b0: { - type: 'virtual', + type: 'cluster_send', options: { send: 'b0 ', to: 's1',