diff --git a/plugins/examples/cln-plugin-startup.rs b/plugins/examples/cln-plugin-startup.rs index 300c7a790..a040f8c1d 100644 --- a/plugins/examples/cln-plugin-startup.rs +++ b/plugins/examples/cln-plugin-startup.rs @@ -3,10 +3,11 @@ #[macro_use] extern crate serde_json; use cln_plugin::options::{ - self, BooleanConfigOption, DefaultIntegerConfigOption, IntegerConfigOption, + self, BooleanConfigOption, DefaultIntegerArrayConfigOption, DefaultIntegerConfigOption, + DefaultStringArrayConfigOption, IntegerArrayConfigOption, IntegerConfigOption, + StringArrayConfigOption, }; use cln_plugin::{messages, Builder, Error, Plugin}; -use tokio; const TEST_NOTIF_TAG: &str = "test_custom_notification"; @@ -19,6 +20,32 @@ const TEST_OPTION: DefaultIntegerConfigOption = DefaultIntegerConfigOption::new_ const TEST_OPTION_NO_DEFAULT: IntegerConfigOption = IntegerConfigOption::new_i64_no_default("opt-option", "An option without a default"); +const TEST_MULTI_STR_OPTION: StringArrayConfigOption = + StringArrayConfigOption::new_str_arr_no_default( + "multi-str-option", + "An option that can have multiple string values", + ); + +const TEST_MULTI_STR_OPTION_DEFAULT: DefaultStringArrayConfigOption = + DefaultStringArrayConfigOption::new_str_arr_with_default( + "multi-str-option-default", + "Default1", + "An option that can have multiple string values with defaults", + ); + +const TEST_MULTI_I64_OPTION: IntegerArrayConfigOption = + IntegerArrayConfigOption::new_i64_arr_no_default( + "multi-i64-option", + "An option that can have multiple i64 values", + ); + +const TEST_MULTI_I64_OPTION_DEFAULT: DefaultIntegerArrayConfigOption = + DefaultIntegerArrayConfigOption::new_i64_arr_with_default( + "multi-i64-option-default", + -42, + "An option that can have multiple i64 values with defaults", + ); + #[tokio::main] async fn main() -> Result<(), anyhow::Error> { let state = (); @@ -33,6 +60,10 @@ async fn main() -> Result<(), anyhow::Error> { .option(TEST_OPTION) .option(TEST_OPTION_NO_DEFAULT) .option(test_dynamic_option) + .option(TEST_MULTI_STR_OPTION) + .option(TEST_MULTI_STR_OPTION_DEFAULT) + .option(TEST_MULTI_I64_OPTION) + .option(TEST_MULTI_I64_OPTION_DEFAULT) .setconfig_callback(setconfig_callback) .rpcmethod("testmethod", "This is a test", testmethod) .rpcmethod( @@ -79,10 +110,18 @@ async fn setconfig_callback( async fn testoptions(p: Plugin<()>, _v: serde_json::Value) -> Result { let test_option = p.option(&TEST_OPTION)?; let test_option_no_default = p.option(&TEST_OPTION_NO_DEFAULT)?; + let test_multi_str_option = p.option(&TEST_MULTI_STR_OPTION)?; + let test_multi_str_option_default = p.option(&TEST_MULTI_STR_OPTION_DEFAULT)?; + let test_multi_i64_option = p.option(&TEST_MULTI_I64_OPTION)?; + let test_multi_i64_option_default = p.option(&TEST_MULTI_I64_OPTION_DEFAULT)?; Ok(json!({ "test-option": test_option, - "opt-option" : test_option_no_default + "opt-option" : test_option_no_default, + "multi-str-option": test_multi_str_option, + "multi-str-option-default": test_multi_str_option_default, + "multi-i64-option": test_multi_i64_option, + "multi-i64-option-default": test_multi_i64_option_default, })) } diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs index 4603f7c31..1b1ddf268 100644 --- a/plugins/src/lib.rs +++ b/plugins/src/lib.rs @@ -437,6 +437,15 @@ where let option_value: Option = match (json_value, default_value) { (None, None) => None, (None, Some(default)) => Some(default.clone()), + (Some(JValue::Array(a)), _) => match a.first() { + Some(JValue::String(_)) => Some(OValue::StringArray( + a.iter().map(|x| x.as_str().unwrap().to_string()).collect(), + )), + Some(JValue::Number(_)) => Some(OValue::IntegerArray( + a.iter().map(|x| x.as_i64().unwrap()).collect(), + )), + _ => panic!("Array type not supported for option: {}", name), + }, (Some(JValue::String(s)), _) => Some(OValue::String(s.to_string())), (Some(JValue::Number(i)), _) => Some(OValue::Integer(i.as_i64().unwrap())), (Some(JValue::Bool(b)), _) => Some(OValue::Boolean(*b)), diff --git a/plugins/src/options.rs b/plugins/src/options.rs index 65839abef..6ede6f815 100644 --- a/plugins/src/options.rs +++ b/plugins/src/options.rs @@ -77,6 +77,7 @@ //! description : "A config option of type string that takes no default", //! deprecated : false, // Option is not deprecated //! dynamic: false, //Option is not dynamic +//! multi: false, //Option must not be multi, use StringArray instead //! }; //! ``` //! @@ -131,7 +132,7 @@ //! Ok(()) //! } //! ``` -use serde::ser::Serializer; +use serde::ser::{SerializeSeq, Serializer}; use serde::Serialize; pub mod config_type { @@ -140,10 +141,18 @@ pub mod config_type { #[derive(Clone, Debug)] pub struct DefaultInteger; #[derive(Clone, Debug)] + pub struct IntegerArray; + #[derive(Clone, Debug)] + pub struct DefaultIntegerArray; + #[derive(Clone, Debug)] pub struct String; #[derive(Clone, Debug)] pub struct DefaultString; #[derive(Clone, Debug)] + pub struct StringArray; + #[derive(Clone, Debug)] + pub struct DefaultStringArray; + #[derive(Clone, Debug)] pub struct Boolean; #[derive(Clone, Debug)] pub struct DefaultBoolean; @@ -153,14 +162,22 @@ pub mod config_type { /// Config values are represented as an i64. No default is used pub type IntegerConfigOption<'a> = ConfigOption<'a, config_type::Integer>; +// Config values are represented as a Vec. No default is used. +pub type IntegerArrayConfigOption<'a> = ConfigOption<'a, config_type::IntegerArray>; /// Config values are represented as a String. No default is used. pub type StringConfigOption<'a> = ConfigOption<'a, config_type::String>; +// Config values are represented as a Vec. No default is used. +pub type StringArrayConfigOption<'a> = ConfigOption<'a, config_type::StringArray>; /// Config values are represented as a boolean. No default is used. pub type BooleanConfigOption<'a> = ConfigOption<'a, config_type::Boolean>; /// Config values are repsentedas an i64. A default is used pub type DefaultIntegerConfigOption<'a> = ConfigOption<'a, config_type::DefaultInteger>; +// Config values are represented as a Vec. A default is used +pub type DefaultIntegerArrayConfigOption<'a> = ConfigOption<'a, config_type::DefaultIntegerArray>; /// Config values are repsentedas an String. A default is used pub type DefaultStringConfigOption<'a> = ConfigOption<'a, config_type::DefaultString>; +// Config values are represented as a Vec. A default is used +pub type DefaultStringArrayConfigOption<'a> = ConfigOption<'a, config_type::DefaultStringArray>; /// Config values are repsentedas an bool. A default is used pub type DefaultBooleanConfigOption<'a> = ConfigOption<'a, config_type::DefaultBoolean>; /// Config value is represented as a flag @@ -197,6 +214,26 @@ impl<'a> OptionType<'a> for config_type::DefaultString { } } +impl<'a> OptionType<'a> for config_type::DefaultStringArray { + type OutputValue = Vec; + type DefaultValue = &'a str; + + fn convert_default(value: &Self::DefaultValue) -> Option { + Some(Value::String(value.to_string())) + } + + fn from_value(value: &Option) -> Self::OutputValue { + match value { + Some(Value::StringArray(s)) => s.clone(), + _ => panic!("Type mismatch. Expected string-array but found {:?}", value), + } + } + + fn get_value_type() -> ValueType { + ValueType::String + } +} + impl<'a> OptionType<'a> for config_type::DefaultInteger { type OutputValue = i64; type DefaultValue = i64; @@ -205,7 +242,7 @@ impl<'a> OptionType<'a> for config_type::DefaultInteger { Some(Value::Integer(*value)) } - fn from_value(value: &Option) -> i64 { + fn from_value(value: &Option) -> Self::OutputValue { match value { Some(Value::Integer(i)) => *i, _ => panic!("Type mismatch. Expected Integer but found {:?}", value), @@ -217,6 +254,29 @@ impl<'a> OptionType<'a> for config_type::DefaultInteger { } } +impl<'a> OptionType<'a> for config_type::DefaultIntegerArray { + type OutputValue = Vec; + type DefaultValue = i64; + + fn convert_default(value: &Self::DefaultValue) -> Option { + Some(Value::Integer(*value)) + } + + fn from_value(value: &Option) -> Self::OutputValue { + match value { + Some(Value::IntegerArray(i)) => i.clone(), + _ => panic!( + "Type mismatch. Expected Integer-array but found {:?}", + value + ), + } + } + + fn get_value_type() -> ValueType { + ValueType::Integer + } +} + impl<'a> OptionType<'a> for config_type::DefaultBoolean { type OutputValue = bool; type DefaultValue = bool; @@ -224,7 +284,7 @@ impl<'a> OptionType<'a> for config_type::DefaultBoolean { fn convert_default(value: &bool) -> Option { Some(Value::Boolean(*value)) } - fn from_value(value: &Option) -> bool { + fn from_value(value: &Option) -> Self::OutputValue { match value { Some(Value::Boolean(b)) => *b, _ => panic!("Type mismatch. Expected Boolean but found {:?}", value), @@ -244,7 +304,7 @@ impl<'a> OptionType<'a> for config_type::Flag { Some(Value::Boolean(false)) } - fn from_value(value: &Option) -> bool { + fn from_value(value: &Option) -> Self::OutputValue { match value { Some(Value::Boolean(b)) => *b, _ => panic!("Type mismatch. Expected Boolean but found {:?}", value), @@ -264,7 +324,7 @@ impl<'a> OptionType<'a> for config_type::String { None } - fn from_value(value: &Option) -> Option { + fn from_value(value: &Option) -> Self::OutputValue { match value { Some(Value::String(s)) => Some(s.to_string()), None => None, @@ -280,6 +340,30 @@ impl<'a> OptionType<'a> for config_type::String { } } +impl<'a> OptionType<'a> for config_type::StringArray { + type OutputValue = Option>; + type DefaultValue = (); + + fn convert_default(_value: &()) -> Option { + None + } + + fn from_value(value: &Option) -> Self::OutputValue { + match value { + Some(Value::StringArray(s)) => Some(s.clone()), + None => None, + _ => panic!( + "Type mismatch. Expected Option> but found {:?}", + value + ), + } + } + + fn get_value_type() -> ValueType { + ValueType::String + } +} + impl<'a> OptionType<'a> for config_type::Integer { type OutputValue = Option; type DefaultValue = (); @@ -303,6 +387,31 @@ impl<'a> OptionType<'a> for config_type::Integer { ValueType::Integer } } + +impl<'a> OptionType<'a> for config_type::IntegerArray { + type OutputValue = Option>; + type DefaultValue = (); + + fn convert_default(_value: &()) -> Option { + None + } + + fn from_value(value: &Option) -> Self::OutputValue { + match value { + Some(Value::IntegerArray(i)) => Some(i.clone()), + None => None, + _ => panic!( + "Type mismatch. Expected Option> but found {:?}", + value + ), + } + } + + fn get_value_type() -> ValueType { + ValueType::Integer + } +} + impl<'a> OptionType<'a> for config_type::Boolean { type OutputValue = Option; type DefaultValue = (); @@ -343,6 +452,8 @@ pub enum Value { String(String), Integer(i64), Boolean(bool), + StringArray(Vec), + IntegerArray(Vec), } impl Serialize for Value { @@ -354,6 +465,20 @@ impl Serialize for Value { Value::String(s) => serializer.serialize_str(s), Value::Integer(i) => serializer.serialize_i64(*i), Value::Boolean(b) => serializer.serialize_bool(*b), + Value::StringArray(sa) => { + let mut seq = serializer.serialize_seq(Some(sa.len()))?; + for element in sa { + seq.serialize_element(element)?; + } + seq.end() + } + Value::IntegerArray(sa) => { + let mut seq = serializer.serialize_seq(Some(sa.len()))?; + for element in sa { + seq.serialize_element(element)?; + } + seq.end() + } } } } @@ -374,6 +499,8 @@ impl Value { Value::String(s) => Some(&s), Value::Integer(_) => None, Value::Boolean(_) => None, + Value::StringArray(_) => None, + Value::IntegerArray(_) => None, } } @@ -411,6 +538,40 @@ impl Value { _ => None, } } + + /// Returns true if the `Value` is a Vec. Returns false otherwise. + /// + /// For any Value on which `is_str_arr` returns true, `as_str_arr` is + /// guaranteed to return the Vec value. + pub fn is_str_arr(&self) -> bool { + self.as_str_arr().is_some() + } + + /// If the `Value` is a Vec, returns the associated Vec. + /// Returns None otherwise. + pub fn as_str_arr(&self) -> Option<&Vec> { + match self { + Value::StringArray(sa) => Some(sa), + _ => None, + } + } + + /// Returns true if the `Value` is a Vec. Returns false otherwise. + /// + /// For any Value on which `is_i64_arr` returns true, `as_i64_arr` is + /// guaranteed to return the Vec value. + pub fn is_i64_arr(&self) -> bool { + self.as_i64_arr().is_some() + } + + /// If the `Value` is a Vec, returns the associated Vec. + /// Returns None otherwise. + pub fn as_i64_arr(&self) -> Option<&Vec> { + match self { + Value::IntegerArray(sa) => Some(sa), + _ => None, + } + } } #[derive(Clone, Debug)] @@ -422,6 +583,7 @@ pub struct ConfigOption<'a, V: OptionType<'a>> { pub description: &'a str, pub deprecated: bool, pub dynamic: bool, + pub multi: bool, } impl<'a, V: OptionType<'a>> ConfigOption<'a, V> { @@ -433,6 +595,7 @@ impl<'a, V: OptionType<'a>> ConfigOption<'a, V> { description: self.description.to_string(), deprecated: self.deprecated, dynamic: self.dynamic, + multi: self.multi, } } } @@ -449,6 +612,7 @@ impl<'a> DefaultStringConfigOption<'a> { description: description, deprecated: false, dynamic: false, + multi: false, } } pub fn dynamic(mut self) -> Self { @@ -465,6 +629,45 @@ impl<'a> StringConfigOption<'a> { description: description, deprecated: false, dynamic: false, + multi: false, + } + } + pub fn dynamic(mut self) -> Self { + self.dynamic = true; + self + } +} + +impl<'a> DefaultStringArrayConfigOption<'a> { + pub const fn new_str_arr_with_default( + name: &'a str, + default: &'a str, + description: &'a str, + ) -> Self { + Self { + name, + default, + description, + deprecated: false, + dynamic: false, + multi: true, + } + } + pub fn dynamic(mut self) -> Self { + self.dynamic = true; + self + } +} + +impl<'a> StringArrayConfigOption<'a> { + pub const fn new_str_arr_no_default(name: &'a str, description: &'a str) -> Self { + Self { + name, + default: (), + description, + deprecated: false, + dynamic: false, + multi: true, } } pub fn dynamic(mut self) -> Self { @@ -481,6 +684,7 @@ impl<'a> DefaultIntegerConfigOption<'a> { description: description, deprecated: false, dynamic: false, + multi: false, } } pub fn dynamic(mut self) -> Self { @@ -497,6 +701,45 @@ impl<'a> IntegerConfigOption<'a> { description: description, deprecated: false, dynamic: false, + multi: false, + } + } + pub fn dynamic(mut self) -> Self { + self.dynamic = true; + self + } +} + +impl<'a> DefaultIntegerArrayConfigOption<'a> { + pub const fn new_i64_arr_with_default( + name: &'a str, + default: i64, + description: &'a str, + ) -> Self { + Self { + name, + default, + description, + deprecated: false, + dynamic: false, + multi: true, + } + } + pub fn dynamic(mut self) -> Self { + self.dynamic = true; + self + } +} + +impl<'a> IntegerArrayConfigOption<'a> { + pub const fn new_i64_arr_no_default(name: &'a str, description: &'a str) -> Self { + Self { + name, + default: (), + description, + deprecated: false, + dynamic: false, + multi: true, } } pub fn dynamic(mut self) -> Self { @@ -513,6 +756,7 @@ impl<'a> BooleanConfigOption<'a> { default: (), deprecated: false, dynamic: false, + multi: false, } } pub fn dynamic(mut self) -> Self { @@ -529,6 +773,7 @@ impl<'a> DefaultBooleanConfigOption<'a> { default: default, deprecated: false, dynamic: false, + multi: false, } } pub fn dynamic(mut self) -> Self { @@ -545,6 +790,7 @@ impl<'a> FlagConfigOption<'a> { default: (), deprecated: false, dynamic: false, + multi: false, } } pub fn dynamic(mut self) -> Self { @@ -569,6 +815,7 @@ pub struct UntypedConfigOption { #[serde(skip_serializing_if = "is_false")] deprecated: bool, dynamic: bool, + multi: bool, } impl UntypedConfigOption { @@ -613,6 +860,7 @@ mod test { "default": "default", "type": "string", "dynamic": false, + "multi": false, }), ), ( @@ -623,6 +871,7 @@ mod test { "default": 42, "type": "int", "dynamic": false, + "multi": false, }), ), ( @@ -637,6 +886,7 @@ mod test { "default": true, "type": "bool", "dynamic": true, + "multi": false, }), ), ( @@ -647,6 +897,29 @@ mod test { "type" : "flag", "default" : false, "dynamic": false, + "multi": false, + }), + ), + ( + ConfigOption::new_str_arr_with_default("name", "Default1", "description").build(), + json!({ + "name" : "name", + "description": "description", + "type" : "string", + "default" : "Default1", + "dynamic": false, + "multi": true, + }), + ), + ( + ConfigOption::new_i64_arr_with_default("name", -46, "description").build(), + json!({ + "name" : "name", + "description": "description", + "type" : "int", + "default" : -46, + "dynamic": false, + "multi": true, }), ), ]; diff --git a/tests/test_cln_rs.py b/tests/test_cln_rs.py index cc382b1e1..47ab4499d 100644 --- a/tests/test_cln_rs.py +++ b/tests/test_cln_rs.py @@ -74,16 +74,34 @@ def test_plugin_options_handle_defaults(node_factory): """Start a minimal plugin and ensure it is well-behaved """ bin_path = Path.cwd() / "target" / RUST_PROFILE / "examples" / "cln-plugin-startup" - l1 = node_factory.get_node(options={"plugin": str(bin_path), 'opt-option': 31337, "test-option": 31338}) + l1 = node_factory.get_node( + options={ + "plugin": str(bin_path), + "opt-option": 31337, + "test-option": 31338, + "multi-str-option": ["String1", "String2"], + "multi-str-option-default": ["NotDefault1", "NotDefault2"], + "multi-i64-option": [1, 2, 3, 4], + "multi-i64-option-default": [5, 6], + } + ) opts = l1.rpc.testoptions() assert opts["opt-option"] == 31337 assert opts["test-option"] == 31338 + assert opts["multi-str-option"] == ["String1", "String2"] + assert opts["multi-str-option-default"] == ["NotDefault1", "NotDefault2"] + assert opts["multi-i64-option"] == [1, 2, 3, 4] + assert opts["multi-i64-option-default"] == [5, 6] # Do not set any value, should be None now l1 = node_factory.get_node(options={"plugin": str(bin_path)}) opts = l1.rpc.testoptions() assert opts["opt-option"] is None, "opt-option has no default" assert opts["test-option"] == 42, "test-option has a default of 42" + assert opts["multi-str-option"] is None + assert opts["multi-str-option-default"] == ["Default1"] + assert opts["multi-i64-option"] is None + assert opts["multi-i64-option-default"] == [-42] def test_grpc_connect(node_factory):