diff --git a/plugins/examples/cln-plugin-startup.rs b/plugins/examples/cln-plugin-startup.rs index 13f93589194e..81a001773e62 100644 --- a/plugins/examples/cln-plugin-startup.rs +++ b/plugins/examples/cln-plugin-startup.rs @@ -2,26 +2,28 @@ //! plugins using the Rust API against Core Lightning. #[macro_use] extern crate serde_json; -use cln_plugin::{messages, options, Builder, Error, Plugin}; +use cln_plugin::options::{DefaultIntegerConfigOption, IntegerConfigOption}; +use cln_plugin::{messages, Builder, Error, Plugin}; use tokio; const TEST_NOTIF_TAG: &str = "test_custom_notification"; +const TEST_OPTION: DefaultIntegerConfigOption = DefaultIntegerConfigOption::new_i64_with_default( + "test-option", + 42, + "a test-option with default 42", +); + +const TEST_OPTION_NO_DEFAULT: IntegerConfigOption = + IntegerConfigOption::new_i64_no_default("opt-option", "An option without a default"); + #[tokio::main] async fn main() -> Result<(), anyhow::Error> { let state = (); if let Some(plugin) = Builder::new(tokio::io::stdin(), tokio::io::stdout()) - .option(options::ConfigOption::new( - "test-option", - options::Value::Integer(42), - "a test-option with default 42", - )) - .option(options::ConfigOption::new( - "opt-option", - options::Value::OptInteger, - "An optional option", - )) + .option(TEST_OPTION) + .option(TEST_OPTION_NO_DEFAULT) .rpcmethod("testmethod", "This is a test", testmethod) .rpcmethod( "testoptions", @@ -47,8 +49,13 @@ async fn main() -> Result<(), anyhow::Error> { } 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)?; + Ok(json!({ - "opt-option": format!("{:?}", p.option("opt-option").unwrap()) + "test-option": test_option, + "opt-option" : test_option_no_default })) } diff --git a/plugins/grpc-plugin/src/main.rs b/plugins/grpc-plugin/src/main.rs index 782752c22166..27eca08a711f 100644 --- a/plugins/grpc-plugin/src/main.rs +++ b/plugins/grpc-plugin/src/main.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use cln_grpc::pb::node_server::NodeServer; use cln_plugin::{options, Builder}; use log::{debug, warn}; @@ -14,6 +14,10 @@ struct PluginState { ca_cert: Vec, } +const OPTION_GRPC_PORT : options::IntegerConfigOption = options::ConfigOption::new_i64_no_default( + "grpc-port", + "Which port should the grpc plugin listen for incoming connections?"); + #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { debug!("Starting grpc plugin"); @@ -22,11 +26,7 @@ async fn main() -> Result<()> { let directory = std::env::current_dir()?; let plugin = match Builder::new(tokio::io::stdin(), tokio::io::stdout()) - .option(options::ConfigOption::new( - "grpc-port", - options::Value::Integer(-1), - "Which port should the grpc plugin listen for incoming connections?", - )) + .option(OPTION_GRPC_PORT) .configure() .await? { @@ -34,17 +34,15 @@ async fn main() -> Result<()> { None => return Ok(()), }; - let bind_port = match plugin.option("grpc-port") { - Some(options::Value::Integer(-1)) => { - log::info!("`grpc-port` option is not configured, exiting."); + let bind_port = match plugin.option(&OPTION_GRPC_PORT).unwrap() { + Some(port) => port, + None => { + log::info!("'grpc-port' options i not configured. exiting."); plugin - .disable("`grpc-port` option is not configured.") + .disable("Missing 'grpc-port' option") .await?; - return Ok(()); + return Ok(()) } - Some(options::Value::Integer(i)) => i, - None => return Err(anyhow!("Missing 'grpc-port' option")), - Some(o) => return Err(anyhow!("grpc-port is not a valid integer: {:?}", o)), }; let (identity, ca_cert) = tls::init(&directory)?; diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs index 67c6567f8d04..1dd19a4acc00 100644 --- a/plugins/src/lib.rs +++ b/plugins/src/lib.rs @@ -1,12 +1,12 @@ use crate::codec::{JsonCodec, JsonRpcCodec}; pub use anyhow::anyhow; -use anyhow::Context; +use anyhow::{Context, Result}; use futures::sink::SinkExt; use tokio::io::{AsyncReadExt, AsyncWriteExt}; extern crate log; use log::trace; -use messages::{Configuration, NotificationTopic, FeatureBits}; -use options::ConfigOption; +use messages::{Configuration, FeatureBits, NotificationTopic}; +use options::{OptionType, UntypedConfigOption}; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; @@ -43,11 +43,12 @@ where output: Option, hooks: HashMap>, - options: Vec, + options: HashMap, + option_values: HashMap>, rpcmethods: HashMap>, subscriptions: HashMap>, notifications: Vec, - custommessages : Vec, + custommessages: Vec, featurebits: FeatureBits, dynamic: bool, // Do we want the plugin framework to automatically register a logging handler? @@ -65,7 +66,8 @@ where init_id: serde_json::Value, input: FramedRead, output: Arc>>, - options: Vec, + options: HashMap, + option_values: HashMap>, configuration: Configuration, rpcmethods: HashMap>, hooks: HashMap>, @@ -99,7 +101,8 @@ where /// The state gets cloned for each request state: S, /// "options" field of "init" message sent by cln - options: Vec, + options: HashMap, + option_values: HashMap>, /// "configuration" field of "init" message sent by cln configuration: Configuration, /// A signal that allows us to wait on the plugin's shutdown. @@ -120,18 +123,24 @@ where output: Some(output), hooks: HashMap::new(), subscriptions: HashMap::new(), - options: vec![], + options: HashMap::new(), + // Should not be configured by user. + // This values are set when parsing the init-call + option_values: HashMap::new(), rpcmethods: HashMap::new(), notifications: vec![], featurebits: FeatureBits::default(), dynamic: false, - custommessages : vec![], + custommessages: vec![], logging: true, } } - pub fn option(mut self, opt: options::ConfigOption) -> Builder { - self.options.push(opt); + pub fn option( + mut self, + opt: options::ConfigOption, + ) -> Builder { + self.options.insert(opt.name().to_string(), opt.build()); self } @@ -245,7 +254,7 @@ where /// Tells lightningd explicitly to allow custommmessages of the provided /// type - pub fn custommessages(mut self, custommessages : Vec) -> Self { + pub fn custommessages(mut self, custommessages: Vec) -> Self { self.custommessages = custommessages; self } @@ -331,6 +340,7 @@ where notifications: self.notifications, subscriptions, options: self.options, + option_values: self.option_values, configuration, hooks: HashMap::new(), })) @@ -369,7 +379,7 @@ where .collect(); messages::GetManifestResponse { - options: self.options.clone(), + options: self.options.values().cloned().collect(), subscriptions: self.subscriptions.keys().map(|s| s.clone()).collect(), hooks: self.hooks.keys().map(|s| s.clone()).collect(), rpcmethods, @@ -377,7 +387,7 @@ where featurebits: self.featurebits.clone(), dynamic: self.dynamic, nonnumericids: true, - custommessages : self.custommessages.clone() + custommessages: self.custommessages.clone(), } } @@ -387,29 +397,21 @@ where // Match up the ConfigOptions and fill in their values if we // have a matching entry. - for opt in self.options.iter_mut() { - let val = call.options.get(opt.name()); - opt.value = match (&opt, &opt.default(), &val) { - (_, OValue::String(_), Some(JValue::String(s))) => Some(OValue::String(s.clone())), - (_, OValue::OptString, Some(JValue::String(s))) => Some(OValue::String(s.clone())), - (_, OValue::OptString, None) => None, - - (_, OValue::Integer(_), Some(JValue::Number(s))) => { - Some(OValue::Integer(s.as_i64().unwrap())) - } - (_, OValue::OptInteger, Some(JValue::Number(s))) => { - Some(OValue::Integer(s.as_i64().unwrap())) - } - (_, OValue::OptInteger, None) => None, - - (_, OValue::Boolean(_), Some(JValue::Bool(s))) => Some(OValue::Boolean(*s)), - (_, OValue::OptBoolean, Some(JValue::Bool(s))) => Some(OValue::Boolean(*s)), - (_, OValue::OptBoolean, None) => None, - - (o, _, _) => panic!("Type mismatch for option {:?}", o), - } + for (name, option) in self.options.iter() { + let json_value = call.options.get(name); + let default_value = option.default(); + + let option_value: Option = match (json_value, default_value) { + (None, None) => None, + (None, Some(default)) => Some(default.clone()), + (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)), + _ => panic!("Type mismatch for option {}", name), + }; + + self.option_values.insert(name.to_string(), option_value); } - Ok(call.configuration) } } @@ -505,12 +507,19 @@ impl Plugin where S: Clone + Send, { - pub fn option(&self, name: &str) -> Option { - self.options - .iter() - .filter(|o| o.name() == name) - .next() - .map(|co| co.value.clone().unwrap_or(co.default().clone())) + pub fn option_str(&self, name: &str) -> Result> { + self.option_values + .get(name) + .ok_or(anyhow!("No option named {}", name)) + .map(|c| c.clone()) + } + + pub fn option( + &self, + config_option: &options::ConfigOption, + ) -> Result { + let value = self.option_str(config_option.name())?; + Ok(OV::from_value(&value)) } } @@ -533,6 +542,7 @@ where let plugin = Plugin { state, options: self.options, + option_values: self.option_values, configuration: self.configuration, wait_handle, sender, @@ -594,12 +604,19 @@ where Ok(()) } - pub fn option(&self, name: &str) -> Option { - self.options - .iter() - .filter(|o| o.name() == name) - .next() - .map(|co| co.value.clone().unwrap_or(co.default().clone())) + pub fn option_str(&self, name: &str) -> Result> { + self.option_values + .get(name) + .ok_or(anyhow!("No option named '{}'", name)) + .map(|c| c.clone()) + } + + pub fn option( + &self, + config_option: &options::ConfigOption, + ) -> Result { + let value = self.option_str(config_option.name())?; + Ok(OV::from_value(&value)) } /// return the cln configuration send to the @@ -739,8 +756,8 @@ impl Plugin where S: Clone + Send, { - pub fn options(&self) -> Vec { - self.options.clone() + pub fn options(&self) -> Vec { + self.options.values().cloned().collect() } pub fn configuration(&self) -> Configuration { self.configuration.clone() diff --git a/plugins/src/messages.rs b/plugins/src/messages.rs index 8a1bbae956e8..75402bb21959 100644 --- a/plugins/src/messages.rs +++ b/plugins/src/messages.rs @@ -1,4 +1,4 @@ -use crate::options::ConfigOption; +use crate::options::UntypedConfigOption; use serde::de::{self, Deserializer}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -171,7 +171,7 @@ impl NotificationTopic { #[derive(Serialize, Default, Debug)] pub(crate) struct GetManifestResponse { - pub(crate) options: Vec, + pub(crate) options: Vec, pub(crate) rpcmethods: Vec, pub(crate) subscriptions: Vec, pub(crate) notifications: Vec, @@ -180,7 +180,7 @@ pub(crate) struct GetManifestResponse { pub(crate) featurebits: FeatureBits, pub(crate) nonnumericids: bool, #[serde(skip_serializing_if = "Vec::is_empty")] - pub(crate) custommessages : Vec + pub(crate) custommessages: Vec, } #[derive(Serialize, Default, Debug, Clone)] diff --git a/plugins/src/options.rs b/plugins/src/options.rs index 026fea00fa33..843ee6e95021 100644 --- a/plugins/src/options.rs +++ b/plugins/src/options.rs @@ -1,14 +1,353 @@ -use serde::ser::{SerializeStruct, Serializer}; +//! This module contains all logic related to `ConfigOption`'s that can be +//! set in Core Lightning. The [Core Lightning documentation](https://docs.corelightning.org/reference/lightningd-config) +//! describes how the user can specify configuration. This can be done using +//! a command-line argument or by specifying the value in the `config`-file. +//! +//! ## A simple example +//! +//! A config option can either be specified using helper-methods or explicitly. +//! +//! ```no_run +//! use anyhow::Result; +//! +//! use cln_plugin::ConfiguredPlugin; +//! use cln_plugin::Builder; +//! use cln_plugin::options::{StringConfigOption, DefaultStringConfigOption}; +//! +//! const STRING_OPTION : StringConfigOption = +//! StringConfigOption::new_str_no_default( +//! "string-option", +//! "A config option of type string with no default" +//! ); +//! +//! const DEFAULT_STRING_OPTION : DefaultStringConfigOption = +//! DefaultStringConfigOption::new_str_with_default( +//! "string-option", +//! "bitcoin", +//! "A config option which uses 'bitcoin when as a default" +//! ); +//! +//! #[tokio::main] +//! async fn main() -> Result<()>{ +//! let configured_plugin = Builder::new(tokio::io::stdin(), tokio::io::stdout()) +//! .option(STRING_OPTION) +//! .option(DEFAULT_STRING_OPTION) +//! .configure() +//! .await?; +//! +//! let configured_plugin :ConfiguredPlugin<(),_,_> = match configured_plugin { +//! Some(plugin) => plugin, +//! None => return Ok(()) // Core Lightning was started with --help +//! }; +//! +//! // Note the types here. +//! // In `string_option` the developer did not specify a default and `None` +//! // will be returned if the user doesn't specify a configuration. +//! // +//! // In `default_string_option` the developer set a default-value. +//! // If the user doesn't specify a configuration the `String` `"bitcoin"` +//! // will be returned. +//! let string_option : Option = configured_plugin +//! .option(&STRING_OPTION) +//! .expect("Failed to configure option"); +//! let default_string_option : String = configured_plugin +//! .option(&DEFAULT_STRING_OPTION) +//! .expect("Failed to configure option"); +//! +//! // You can start the plugin here +//! // ... +//! +//! Ok(()) +//! } +//! +//! ``` +//! +//! ## Explicit initialization +//! +//! A `ConfigOption` can be initialized explicitly or using one of the helper methods. +//! The two code-samples below are equivalent. The explicit version is more verbose +//! but allows specifying additional information. +//! +//! ``` +//! use cln_plugin::options::{StringConfigOption}; +//! +//! const STRING_OPTION : StringConfigOption = StringConfigOption { +//! name : "string-option", +//! default : (), // We provide no default here +//! description : "A config option of type string that takes no default", +//! deprecated : false, // Option is not deprecated +//! }; +//! ``` +//! +//! ``` +//! use cln_plugin::options::{StringConfigOption}; +//! // This code is equivalent +//! const STRING_OPTION_EQ : StringConfigOption = StringConfigOption::new_str_no_default( +//! "string-option-eq", +//! "A config option of type string that takes no default" +//! ); +//! ``` +//! +//! ## Required options +//! +//! In some cases you want to require the user to specify a value. +//! This can be achieved using [`crate::ConfiguredPlugin::disable`]. +//! +//! ```no_run +//! use anyhow::Result; +//! +//! use cln_plugin::ConfiguredPlugin; +//! use cln_plugin::Builder; +//! use cln_plugin::options::{IntegerConfigOption}; +//! +//! const WEBPORTAL_PORT : IntegerConfigOption = IntegerConfigOption::new_i64_no_default( +//! "webportal-port", +//! "The port on which the web-portal will be exposed" +//! ); +//! +//! #[tokio::main] +//! async fn main() -> Result<()> { +//! let configured_plugin = Builder::new(tokio::io::stdin(), tokio::io::stdout()) +//! .option(WEBPORTAL_PORT) +//! .configure() +//! .await?; +//! +//! let configured_plugin :ConfiguredPlugin<(),_,_> = match configured_plugin { +//! Some(plugin) => plugin, +//! None => return Ok(()) // Core Lightning was started with --help +//! }; +//! +//! let webportal_port : i64 = match(configured_plugin.option(&WEBPORTAL_PORT)?) { +//! Some(port) => port, +//! None => { +//! return configured_plugin.disable("No value specified for webportal-port").await +//! } +//! }; +//! +//! // Start the plugin here +//! //.. +//! +//! Ok(()) +//! } +//! ``` +use serde::ser::Serializer; use serde::Serialize; +pub mod config_type { + pub struct Integer; + pub struct DefaultInteger; + pub struct String; + pub struct DefaultString; + pub struct Boolean; + pub struct DefaultBoolean; + pub struct Flag; +} + +/// 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 String. No default is used. +pub type StringConfigOption<'a> = ConfigOption<'a, config_type::String>; +/// 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 repsentedas an String. A default is used +pub type DefaultStringConfigOption<'a> = ConfigOption<'a, config_type::DefaultString>; +/// 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 +pub type FlagConfigOption<'a> = ConfigOption<'a, config_type::Flag>; + +pub trait OptionType { + type OutputValue; + type DefaultValue; + + fn convert_default(value: &Self::DefaultValue) -> Option; + + fn from_value(value: &Option) -> Self::OutputValue; + + fn get_value_type() -> ValueType; +} + +impl OptionType for config_type::DefaultString { + type OutputValue = String; + type DefaultValue = &'static 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::String(s)) => s.to_string(), + _ => panic!("Type mismatch. Expected string but found {:?}", value), + } + } + + fn get_value_type() -> ValueType { + ValueType::String + } +} + +impl OptionType for config_type::DefaultInteger { + type OutputValue = i64; + type DefaultValue = i64; + + fn convert_default(value: &Self::DefaultValue) -> Option { + Some(Value::Integer(*value)) + } + + fn from_value(value: &Option) -> i64 { + match value { + Some(Value::Integer(i)) => *i, + _ => panic!("Type mismatch. Expected Integer but found {:?}", value), + } + } + + fn get_value_type() -> ValueType { + ValueType::Integer + } +} + +impl OptionType for config_type::DefaultBoolean { + type OutputValue = bool; + type DefaultValue = bool; + + fn convert_default(value: &bool) -> Option { + Some(Value::Boolean(*value)) + } + fn from_value(value: &Option) -> bool { + match value { + Some(Value::Boolean(b)) => *b, + _ => panic!("Type mismatch. Expected Boolean but found {:?}", value), + } + } + + fn get_value_type() -> ValueType { + ValueType::Boolean + } +} + +impl OptionType for config_type::Flag { + type OutputValue = bool; + type DefaultValue = (); + + fn convert_default(_value: &()) -> Option { + Some(Value::Boolean(false)) + } + + fn from_value(value: &Option) -> bool { + match value { + Some(Value::Boolean(b)) => *b, + _ => panic!("Type mismatch. Expected Boolean but found {:?}", value), + } + } + + fn get_value_type() -> ValueType { + ValueType::Flag + } +} + +impl OptionType for config_type::String { + type OutputValue = Option; + type DefaultValue = (); + + fn convert_default(_value: &()) -> Option { + None + } + + fn from_value(value: &Option) -> Option { + match value { + Some(Value::String(s)) => Some(s.to_string()), + None => None, + _ => panic!( + "Type mismatch. Expected Option but found {:?}", + value + ), + } + } + + fn get_value_type() -> ValueType { + ValueType::String + } +} + +impl OptionType for config_type::Integer { + type OutputValue = Option; + type DefaultValue = (); + + fn convert_default(_value: &()) -> Option { + None + } + + fn from_value(value: &Option) -> Self::OutputValue { + match value { + Some(Value::Integer(i)) => Some(*i), + None => None, + _ => panic!( + "Type mismatch. Expected Option but found {:?}", + value + ), + } + } + + fn get_value_type() -> ValueType { + ValueType::Integer + } +} +impl OptionType for config_type::Boolean { + type OutputValue = Option; + type DefaultValue = (); + + fn convert_default(_value: &()) -> Option { + None + } + fn from_value(value: &Option) -> Self::OutputValue { + match value { + Some(Value::Boolean(b)) => Some(*b), + None => None, + _ => panic!( + "Type mismatch. Expected Option but found {:?}", + value + ), + } + } + + fn get_value_type() -> ValueType { + ValueType::Boolean + } +} + +#[derive(Clone, Debug, Serialize)] +pub enum ValueType { + #[serde(rename = "string")] + String, + #[serde(rename = "int")] + Integer, + #[serde(rename = "bool")] + Boolean, + #[serde(rename = "flag")] + Flag, +} + #[derive(Clone, Debug)] pub enum Value { String(String), Integer(i64), Boolean(bool), - OptString, - OptInteger, - OptBoolean, +} + +impl Serialize for Value { + fn serialize(&self, serializer: S) -> std::prelude::v1::Result + where + S: Serializer, + { + match self { + Value::String(s) => serializer.serialize_str(s), + Value::Integer(i) => serializer.serialize_i64(*i), + Value::Boolean(b) => serializer.serialize_bool(*b), + } + } } impl Value { @@ -24,8 +363,9 @@ impl Value { /// otherwise. pub fn as_str(&self) -> Option<&str> { match self { - Value::String(s) => Some(s), - _ => None, + Value::String(s) => Some(&s), + Value::Integer(_) => None, + Value::Boolean(_) => None, } } @@ -63,107 +403,168 @@ impl Value { _ => None, } } +} - /// Return `true` if the option is not None and `false` otherwise. - pub fn is_some(&self) -> bool { - match self { - Value::String(_) => false, - Value::Integer(_) => false, - Value::Boolean(_) => false, - Value::OptString => true, - Value::OptInteger => true, - Value::OptBoolean => true, +#[derive(Clone, Debug)] +pub struct ConfigOption<'a, V: OptionType> { + /// The name of the `ConfigOption`. + pub name: &'a str, + /// The default value of the `ConfigOption` + pub default: V::DefaultValue, + pub description: &'a str, + pub deprecated: bool, +} + +impl ConfigOption<'_, V> { + pub fn build(&self) -> UntypedConfigOption { + UntypedConfigOption { + name: self.name.to_string(), + value_type: V::get_value_type(), + default: ::convert_default(&self.default), + description: self.description.to_string(), + deprecated: self.deprecated, + } + } +} + +impl DefaultStringConfigOption<'_> { + pub const fn new_str_with_default( + name: &'static str, + default: &'static str, + description: &'static str, + ) -> Self { + Self { + name: name, + default: default, + description: description, + deprecated: false, + } + } +} + +impl StringConfigOption<'_> { + pub const fn new_str_no_default(name: &'static str, description: &'static str) -> Self { + Self { + name, + default: (), + description : description, + deprecated: false, + } + } +} + +impl DefaultIntegerConfigOption<'_> { + pub const fn new_i64_with_default( + name: &'static str, + default: i64, + description: &'static str, + ) -> Self { + Self { + name: name, + default: default, + description: description, + deprecated: false, + } + } +} + +impl IntegerConfigOption<'_> { + pub const fn new_i64_no_default(name: &'static str, description: &'static str) -> Self { + Self { + name: name, + default: (), + description: description, + deprecated: false, + } + } +} + +impl BooleanConfigOption<'_> { + pub const fn new_bool_no_default(name: &'static str, description: &'static str) -> Self { + Self { + name, + description, + default: (), + deprecated: false, + } + } +} + +impl DefaultBooleanConfigOption<'_> { + pub const fn new_bool_with_default( + name: &'static str, + default: bool, + description: &'static str, + ) -> Self { + Self { + name, + description, + default: default, + deprecated: false, } } } +impl FlagConfigOption<'_> { + pub const fn new_flag(name: &'static str, description: &'static str) -> Self { + Self { + name, + description, + default: (), + deprecated: false, + } + } +} + +fn is_false(b: &bool) -> bool { + *b == false +} + /// An stringly typed option that is passed to -#[derive(Clone, Debug)] -pub struct ConfigOption { +#[derive(Clone, Debug, Serialize)] +pub struct UntypedConfigOption { name: String, - pub(crate) value: Option, - default: Value, + #[serde(rename = "type")] + pub(crate) value_type: ValueType, + #[serde(skip_serializing_if = "Option::is_none")] + default: Option, description: String, + #[serde(skip_serializing_if = "is_false")] + deprecated: bool, } -impl ConfigOption { +impl UntypedConfigOption { pub fn name(&self) -> &str { &self.name } - pub fn default(&self) -> &Value { + pub fn default(&self) -> &Option { &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)?; - } - Value::OptString => { - s.serialize_field("type", "string")?; - } - Value::OptInteger => { - s.serialize_field("type", "int")?; - } - Value::OptBoolean => { - s.serialize_field("type", "bool")?; - } - } - - 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(), - } +impl ConfigOption<'_, V> +where + V: OptionType, +{ + pub fn name(&self) -> &str { + &self.name } - pub fn description(&self) -> String { - self.description.clone() + pub fn description(&self) -> &str { + &self.description } } #[cfg(test)] mod test { + use super::*; #[test] fn test_option_serialize() { let tests = vec![ ( - ConfigOption::new("name", Value::String("default".to_string()), "description"), + ConfigOption::new_str_with_default("name", "default", "description").build(), json!({ "name": "name", "description":"description", @@ -172,7 +573,7 @@ mod test { }), ), ( - ConfigOption::new("name", Value::Integer(42), "description"), + ConfigOption::new_i64_with_default("name", 42, "description").build(), json!({ "name": "name", "description":"description", @@ -181,7 +582,7 @@ mod test { }), ), ( - ConfigOption::new("name", Value::Boolean(true), "description"), + ConfigOption::new_bool_with_default("name", true, "description").build(), json!({ "name": "name", "description":"description", @@ -189,6 +590,15 @@ mod test { "type": "bool", }), ), + ( + ConfigOption::new_flag("name", "description").build(), + json!({ + "name" : "name", + "description": "description", + "type" : "flag", + "default" : false + }), + ), ]; for (input, expected) in tests.iter() { @@ -196,4 +606,32 @@ mod test { assert_eq!(&res, expected); } } + + #[test] + fn const_config_option() { + // The main goal of this test is to test compilation + + // Initiate every type as a const + const _: FlagConfigOption = ConfigOption::new_flag("flag-option", "A flag option"); + const _: DefaultBooleanConfigOption = + ConfigOption::new_bool_with_default("bool-option", false, "A boolean option"); + const _: BooleanConfigOption = + ConfigOption::new_bool_no_default("bool-option", "A boolean option"); + + const _: IntegerConfigOption = + ConfigOption::new_i64_no_default("integer-option", "A flag option"); + const _: DefaultIntegerConfigOption = + ConfigOption::new_i64_with_default("integer-option", 12, "A flag option"); + + const _: StringConfigOption = + ConfigOption::new_str_no_default("integer-option", "A flag option"); + const _: DefaultStringConfigOption = + ConfigOption::new_str_with_default("integer-option", "erik", "A flag option"); + } + + #[test] + fn test_type_serialize() { + assert_eq!(json!(ValueType::Integer), json!("int")); + assert_eq!(json!(ValueType::Flag), json!("flag")); + } } diff --git a/tests/test_cln_rs.py b/tests/test_cln_rs.py index eda1d9f61f35..de97ec717223 100644 --- a/tests/test_cln_rs.py +++ b/tests/test_cln_rs.py @@ -67,18 +67,20 @@ def test_plugin_start(node_factory): l1.daemon.wait_for_log(r'Got a connect notification') -def test_plugin_optional_opts(node_factory): +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}) + l1 = node_factory.get_node(options={"plugin": str(bin_path), 'opt-option': 31337, "test-option": 31338}) opts = l1.rpc.testoptions() - print(opts) + assert opts["opt-option"] == 31337 + assert opts["test-option"] == 31338 # Do not set any value, should be None now l1 = node_factory.get_node(options={"plugin": str(bin_path)}) opts = l1.rpc.testoptions() - print(opts) + assert opts["opt-option"] is None, "opt-option has no default" + assert opts["test-option"] == 42, "test-option has a default of 42" def test_grpc_connect(node_factory):