diff --git a/plugins/examples/cln-plugin-startup.rs b/plugins/examples/cln-plugin-startup.rs index c876ba2e4..4b82c5361 100644 --- a/plugins/examples/cln-plugin-startup.rs +++ b/plugins/examples/cln-plugin-startup.rs @@ -47,7 +47,7 @@ async fn main() -> Result<(), anyhow::Error> { async fn testoptions(p: Plugin<()>, _v: serde_json::Value) -> Result { Ok(json!({ - "opt-option": format!("{:?}", p.option("opt-option").unwrap()) + "opt-option": format!("{:?}", p.option_str("opt-option").unwrap()) })) } diff --git a/plugins/grpc-plugin/src/main.rs b/plugins/grpc-plugin/src/main.rs index 3f6de54db..303f0fc43 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"); @@ -21,11 +25,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_i64_with_default( - "grpc-port", - -1, - "Which port should the grpc plugin listen for incoming connections?", - )) + .option(OPTION_GRPC_PORT) .configure() .await? { @@ -33,17 +33,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 ce2ed1e3c..1dd19a4ac 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, FeatureBits, NotificationTopic}; -use options::UntypedConfigOption; +use options::{OptionType, UntypedConfigOption}; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; @@ -44,6 +44,7 @@ where hooks: HashMap>, options: HashMap, + option_values: HashMap>, rpcmethods: HashMap>, subscriptions: HashMap>, notifications: Vec, @@ -66,6 +67,7 @@ where input: FramedRead, output: Arc>>, options: HashMap, + option_values: HashMap>, configuration: Configuration, rpcmethods: HashMap>, hooks: HashMap>, @@ -100,6 +102,7 @@ where state: S, /// "options" field of "init" message sent by cln 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. @@ -121,6 +124,9 @@ where hooks: HashMap::new(), subscriptions: HashMap::new(), 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(), @@ -334,6 +340,7 @@ where notifications: self.notifications, subscriptions, options: self.options, + option_values: self.option_values, configuration, hooks: HashMap::new(), })) @@ -390,26 +397,21 @@ where // Match up the ConfigOptions and fill in their values if we // have a matching entry. - for (_name, opt) in self.options.iter_mut() { - let val = call.options.get(opt.name()); + for (name, option) in self.options.iter() { + let json_value = call.options.get(name); + let default_value = option.default(); - opt.value = match (&opt.value, &opt.default(), &val) { - (_, Some(OValue::String(_)), Some(JValue::String(s))) => { - Some(OValue::String(s.clone())) - } - (_, None, Some(JValue::String(s))) => Some(OValue::String(s.clone())), - (_, None, None) => None, + 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), + }; - (_, Some(OValue::Integer(_)), Some(JValue::Number(s))) => { - Some(OValue::Integer(s.as_i64().unwrap())) - } - (_, None, Some(JValue::Number(s))) => Some(OValue::Integer(s.as_i64().unwrap())), - (_, Some(OValue::Boolean(_)), Some(JValue::Bool(s))) => Some(OValue::Boolean(*s)), - (_, None, Some(JValue::Bool(s))) => Some(OValue::Boolean(*s)), - (o, _, _) => panic!("Type mismatch for option {:?}", o), - } + self.option_values.insert(name.to_string(), option_value); } - Ok(call.configuration) } } @@ -505,8 +507,19 @@ impl Plugin where S: Clone + Send, { - pub fn option(&self, name: &str) -> Option { - self.options.get(name).and_then(|x| x.value.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)) } } @@ -529,6 +542,7 @@ where let plugin = Plugin { state, options: self.options, + option_values: self.option_values, configuration: self.configuration, wait_handle, sender, @@ -590,8 +604,19 @@ where Ok(()) } - pub fn option(&self, name: &str) -> Option { - self.options.get(name).and_then(|c| c.value.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 diff --git a/plugins/src/options.rs b/plugins/src/options.rs index 465aa18f6..8f45439f5 100644 --- a/plugins/src/options.rs +++ b/plugins/src/options.rs @@ -1,54 +1,185 @@ -use anyhow::Result; -use serde::ser::{SerializeStruct, Serializer}; +use serde::ser::Serializer; use serde::Serialize; -// Marker trait for possible values of options +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; +} + +pub type IntegerConfigOption<'a> = ConfigOption<'a, config_type::Integer>; +pub type StringConfigOption<'a> = ConfigOption<'a, config_type::String>; +pub type BooleanConfigOption<'a> = ConfigOption<'a, config_type::Boolean>; + +pub type DefaultIntegerConfigOption<'a> = ConfigOption<'a, config_type::DefaultInteger>; +pub type DefaultStringConfigOption<'a> = ConfigOption<'a, config_type::DefaultString>; +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 { - fn convert_default(value: Option<&Self>) -> Option; + 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 &str { - fn convert_default(value: Option<&Self>) -> Option { - value.map(|s| Value::String(s.to_string())) +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 String { - fn convert_default(value: Option<&Self>) -> Option { - value.map(|s| Value::String(s.clone())) +impl OptionType for config_type::DefaultInteger { + type OutputValue = i64; + type DefaultValue = i64; + + fn convert_default(value: &Self::DefaultValue) -> Option { + Some(Value::Integer(*value)) } -} -impl OptionType for i64 { - fn convert_default(value: Option<&Self>) -> Option { - value.map(|i| Value::Integer(*i)) + + fn from_value(value: &Option) -> i64 { + match value { + Some(Value::Integer(i)) => *i, + _ => panic!("Type mismatch. Expected Integer but found {:?}", value), + } } -} -impl OptionType for bool { - fn convert_default(value: Option<&Self>) -> Option { - value.map(|b| Value::Boolean(*b)) + + fn get_value_type() -> ValueType { + ValueType::Integer } } -impl OptionType for Option { - fn convert_default(_value: Option<&Self>) -> Option { - None +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 Option<&str> { - fn convert_default(_value: Option<&Self>) -> Option { - None +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 Option { - fn convert_default(_value: Option<&Self>) -> Option { + +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 Option { - fn convert_default(_value: Option<&Self>) -> Option { + +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)] @@ -70,6 +201,19 @@ pub enum Value { Boolean(bool), } +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 { /// Returns true if the `Value` is a String. Returns false otherwise. /// @@ -127,25 +271,27 @@ impl Value { #[derive(Clone, Debug)] pub struct ConfigOption<'a, V: OptionType> { - name: &'a str, - default: Option, - value_type: ValueType, - description: &'a str, + /// 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: self.value_type.clone(), - default: OptionType::convert_default(self.default.as_ref()), - value: None, + value_type: V::get_value_type(), + default: ::convert_default(&self.default), description: self.description.to_string(), + deprecated: self.deprecated, } } } -impl ConfigOption<'_, &'static str> { +impl DefaultStringConfigOption<'_> { pub const fn new_str_with_default( name: &'static str, default: &'static str, @@ -153,25 +299,25 @@ impl ConfigOption<'_, &'static str> { ) -> Self { Self { name: name, - default: Some(default), - value_type: ValueType::String, + default: default, description: description, + deprecated: false, } } } -impl ConfigOption<'_, Option<&str>> { +impl StringConfigOption<'_> { pub const fn new_str_no_default(name: &'static str, description: &'static str) -> Self { Self { name, - default: None, - value_type: ValueType::String, - description, + default: (), + description : description, + deprecated: false, } } } -impl ConfigOption<'_, i64> { +impl DefaultIntegerConfigOption<'_> { pub const fn new_i64_with_default( name: &'static str, default: i64, @@ -179,36 +325,36 @@ impl ConfigOption<'_, i64> { ) -> Self { Self { name: name, - default: Some(default), - value_type: ValueType::Integer, + default: default, description: description, + deprecated: false, } } } -impl ConfigOption<'_, Option> { +impl IntegerConfigOption<'_> { pub const fn new_i64_no_default(name: &'static str, description: &'static str) -> Self { Self { name: name, - default: None, - value_type: ValueType::Integer, + default: (), description: description, + deprecated: false, } } } -impl ConfigOption<'_, Option> { +impl BooleanConfigOption<'_> { pub const fn new_bool_no_default(name: &'static str, description: &'static str) -> Self { Self { name, description, - default: None, - value_type: ValueType::Boolean, + default: (), + deprecated: false, } } } -impl ConfigOption<'_, bool> { +impl DefaultBooleanConfigOption<'_> { pub const fn new_bool_with_default( name: &'static str, default: bool, @@ -217,29 +363,38 @@ impl ConfigOption<'_, bool> { Self { name, description, - default: Some(default), - value_type: ValueType::Boolean, - } - } - - pub const fn new_flag(name: &'static str, description: &'static str) -> Self { - Self { - name, - description, - default: Some(false), - value_type: ValueType::Flag, + 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)] +#[derive(Clone, Debug, Serialize)] pub struct UntypedConfigOption { name: String, + #[serde(rename = "type")] pub(crate) value_type: ValueType, - pub(crate) value: Option, + #[serde(skip_serializing_if = "Option::is_none")] default: Option, description: String, + #[serde(skip_serializing_if = "is_false")] + deprecated: bool, } impl UntypedConfigOption { @@ -251,38 +406,6 @@ impl UntypedConfigOption { } } -// 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 UntypedConfigOption { - 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 { - Some(Value::String(ss)) => { - s.serialize_field("default", ss)?; - } - Some(Value::Integer(i)) => { - s.serialize_field("default", i)?; - } - Some(Value::Boolean(b)) => { - match self.value_type { - ValueType::Boolean => s.serialize_field("default", b)?, - ValueType::Flag => {} - _ => {} // This should never happen - } - } - _ => {} - } - s.serialize_field("type", &self.value_type)?; - s.serialize_field("description", &self.description)?; - s.end() - } -} - impl ConfigOption<'_, V> where V: OptionType, @@ -298,6 +421,7 @@ where #[cfg(test)] mod test { + use super::*; #[test] @@ -335,7 +459,8 @@ mod test { json!({ "name" : "name", "description": "description", - "type" : "flag" + "type" : "flag", + "default" : false }), ), ]; @@ -348,20 +473,24 @@ mod test { #[test] fn const_config_option() { - const _: ConfigOption = ConfigOption::new_flag("flag-option", "A flag option"); - const _: ConfigOption = + // 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 _: ConfigOption> = + const _: BooleanConfigOption = ConfigOption::new_bool_no_default("bool-option", "A boolean option"); - const _: ConfigOption> = + const _: IntegerConfigOption = ConfigOption::new_i64_no_default("integer-option", "A flag option"); - const _: ConfigOption = + const _: DefaultIntegerConfigOption = ConfigOption::new_i64_with_default("integer-option", 12, "A flag option"); - const _: ConfigOption> = + const _: StringConfigOption = ConfigOption::new_str_no_default("integer-option", "A flag option"); - const _: ConfigOption<&str> = + const _: DefaultStringConfigOption = ConfigOption::new_str_with_default("integer-option", "erik", "A flag option"); }