From 249fa8675ae4765f630b56b1c2816fd937465f3d Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Mon, 7 Feb 2022 11:00:08 +0100 Subject: [PATCH] cln-plugin: Add options to the `getmanifest` call --- plugins/examples/cln-plugin-startup.rs | 11 ++- plugins/src/lib.rs | 37 +++++++- plugins/src/messages.rs | 7 +- plugins/src/options.rs | 122 +++++++++++++++++++++++++ tests/test_cln_rs.py | 24 +++-- 5 files changed, 183 insertions(+), 18 deletions(-) create mode 100644 plugins/src/options.rs diff --git a/plugins/examples/cln-plugin-startup.rs b/plugins/examples/cln-plugin-startup.rs index 828605e3c..df71ae39c 100644 --- a/plugins/examples/cln-plugin-startup.rs +++ b/plugins/examples/cln-plugin-startup.rs @@ -1,12 +1,19 @@ //! This is a test plugin used to verify that we can compile and run //! plugins using the Rust API against c-lightning. -use cln_plugin::Builder; +use cln_plugin::{options, Builder}; use tokio; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { - let (plugin, stdin) = Builder::new((), tokio::io::stdin(), tokio::io::stdout()).build(); + let (plugin, stdin) = Builder::new((), tokio::io::stdin(), tokio::io::stdout()) + .option(options::ConfigOption::new( + "test-option", + options::Value::Integer(42), + "a test-option with default 42", + )) + .build(); + tokio::spawn(async { tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; log::info!("Hello world"); diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs index 4568baccd..feb1833f7 100644 --- a/plugins/src/lib.rs +++ b/plugins/src/lib.rs @@ -18,6 +18,10 @@ mod messages; #[macro_use] extern crate serde_json; +pub mod options; + +use options::ConfigOption; + /// Builder for a new plugin. pub struct Builder where @@ -35,6 +39,8 @@ where #[allow(dead_code)] subscriptions: Subscriptions, + + options: Vec, } impl Builder @@ -50,9 +56,15 @@ where output, hooks: Hooks::default(), subscriptions: Subscriptions::default(), + options: vec![], } } + pub fn option(mut self, opt: options::ConfigOption) -> Builder { + self.options.push(opt); + self + } + pub fn build(self) -> (Plugin, I) { let output = Arc::new(Mutex::new(FramedWrite::new( self.output, @@ -67,6 +79,7 @@ where state: Arc::new(Mutex::new(self.state)), output, input_type: PhantomData, + options: self.options, }, self.input, ) @@ -85,7 +98,20 @@ where /// The state gets cloned for each request state: Arc>, input_type: PhantomData, + options: Vec, } + +impl Plugin +where + S: Clone + Send, + I: AsyncRead + Send + Unpin, + O: Send + AsyncWrite + Unpin, +{ + pub fn options(&self) -> Vec { + self.options.clone() + } +} + impl Plugin where S: Clone + Send, @@ -130,8 +156,7 @@ where let state = self.state.clone(); let res: serde_json::Value = match request { messages::Request::Getmanifest(c) => { - serde_json::to_value(Plugin::::handle_get_manifest(c, state).await?) - .unwrap() + serde_json::to_value(self.handle_get_manifest(c, state).await?).unwrap() } messages::Request::Init(c) => { serde_json::to_value(Plugin::::handle_init(c, state).await?).unwrap() @@ -160,10 +185,14 @@ where } async fn handle_get_manifest( + &mut self, _call: messages::GetManifestCall, _state: Arc>, ) -> Result { - Ok(messages::GetManifestResponse::default()) + Ok(messages::GetManifestResponse { + options: self.options.clone(), + rpcmethods: vec![], + }) } async fn handle_init( @@ -192,6 +221,6 @@ mod test { #[test] fn init() { let builder = Builder::new((), tokio::io::stdin(), tokio::io::stdout()); - let plugin = builder.build(); + builder.build(); } } diff --git a/plugins/src/messages.rs b/plugins/src/messages.rs index fcba6ceae..540019b6d 100644 --- a/plugins/src/messages.rs +++ b/plugins/src/messages.rs @@ -1,3 +1,4 @@ +use crate::options::ConfigOption; use serde::de::{self, Deserializer}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -149,9 +150,9 @@ where } #[derive(Serialize, Default, Debug)] -pub struct GetManifestResponse { - options: Vec<()>, - rpcmethods: Vec<()>, +pub(crate) struct GetManifestResponse { + pub(crate) options: Vec, + pub(crate) rpcmethods: Vec<()>, } #[derive(Serialize, Default, Debug)] diff --git a/plugins/src/options.rs b/plugins/src/options.rs new file mode 100644 index 000000000..04e11509f --- /dev/null +++ b/plugins/src/options.rs @@ -0,0 +1,122 @@ +use serde::ser::{SerializeStruct, Serializer}; +use serde::{Serialize}; + +#[derive(Clone, Debug)] +pub enum Value { + String(String), + Integer(i64), + Boolean(bool), +} + +/// An stringly typed option that is passed to +#[derive(Clone, Debug)] +pub struct ConfigOption { + name: String, + pub(crate) value: Option, + default: Value, + description: String, +} + +impl ConfigOption { + pub fn name(&self) -> &str { + &self.name + } + pub fn default(&self) -> &Value { + &self.default + } +} + +// When we serialize we don't add the value. This is because we only +// ever serialize when we pass the option back to lightningd during +// the getmanifest call. +impl Serialize for ConfigOption { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut s = serializer.serialize_struct("ConfigOption", 4)?; + s.serialize_field("name", &self.name)?; + match &self.default { + Value::String(ss) => { + s.serialize_field("type", "string")?; + s.serialize_field("default", ss)?; + } + Value::Integer(i) => { + s.serialize_field("type", "int")?; + s.serialize_field("default", i)?; + } + + Value::Boolean(b) => { + s.serialize_field("type", "bool")?; + s.serialize_field("default", b)?; + } + } + + s.serialize_field("description", &self.description)?; + s.end() + } +} +impl ConfigOption { + pub fn new(name: &str, default: Value, description: &str) -> Self { + Self { + name: name.to_string(), + default, + description: description.to_string(), + value: None, + } + } + + pub fn value(&self) -> Value { + match &self.value { + None => self.default.clone(), + Some(v) => v.clone(), + } + } + + pub fn description(&self) -> String { + self.description.clone() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_option_serialize() { + let tests = vec![ + ( + ConfigOption::new("name", Value::String("default".to_string()), "description"), + json!({ + "name": "name", + "description":"description", + "default": "default", + "type": "string", + }), + ), + ( + ConfigOption::new("name", Value::Integer(42), "description"), + json!({ + "name": "name", + "description":"description", + "default": 42, + "type": "int", + }), + ), + ( + ConfigOption::new("name", Value::Boolean(true), "description" + json!({ + "name": "name", + "description":"description", + "default": true, + "type": "booltes", + }), + )), + ]; + + for (input, expected) in tests.iter() { + let res = serde_json::to_value(input).unwrap(); + assert_eq!(&res, expected); + } + } +} diff --git a/tests/test_cln_rs.py b/tests/test_cln_rs.py index ccccca15a..86fc02259 100644 --- a/tests/test_cln_rs.py +++ b/tests/test_cln_rs.py @@ -21,14 +21,20 @@ def test_rpc_client(node_factory): def test_plugin_start(node_factory): - """Start a minimal plugin and ensure it is well-behaved - """ - bin_path = Path.cwd() / "target" / "debug" / "examples" / "cln-plugin-startup" - l1 = node_factory.get_node(options={"plugin": str(bin_path)}) + """Start a minimal plugin and ensure it is well-behaved + """ + bin_path = Path.cwd() / "target" / "debug" / "examples" / "cln-plugin-startup" + l1 = node_factory.get_node(options={"plugin": str(bin_path)}) - # The plugin should be in the list of active plugins - plugins = l1.rpc.plugin('list')['plugins'] - assert len([p for p in plugins if 'cln-plugin-startup' in p['name'] and p['active']]) == 1 + cfg = l1.rpc.listconfigs() + p = cfg['plugins'][0] + p['path'] = None # The path is host-specific, so blank it. + expected = { + 'name': 'cln-plugin-startup', + 'options': { + 'test-option': 42 + }, + 'path': None + } + assert expected == p - # Logging should also work through the log integration - l1.daemon.wait_for_log(r'Hello world')