From 543e67495c48599bad8cfa63dc5ad7a673cb4e9f Mon Sep 17 00:00:00 2001 From: Erik De Smedt Date: Fri, 2 Feb 2024 09:08:50 +0100 Subject: [PATCH] Allow `ConfigOption` to be specified as const This is the first part of two commits that attempts to simplify the API that is used to retrieve configuration values. Previously, we encouraged the following pattern to build a plugin. ```rust let configured_plugin = Builder::new( tokio::io::stdin(), tokio::io::stdout()) .option(ConfigOption::new_i64_with_default("some-option", 0, "Description")) .configure(); let value = configured_plugion.option("some-option"); match value { Some(Integer(i)) => {}, // Config provided None => {}, // No config set _ => {}, // This should never happened } ``` This commit helps to move to the following pattern ```rust const SOME_OPTION : ConfigOption = ConfigOption::new_i64_with_default( "some-option", "description"); async fn main() -> Result<()> { let plugin = Builder::new(tokio::io::stdin(), tokio::io::stdoout()) .option(SOME_OPTION) .configure() .await?; let value : i64 = plugin.option(SOME_OPTION)?; } ``` --- plugins/examples/cln-plugin-startup.rs | 18 +-- plugins/grpc-plugin/src/main.rs | 2 +- plugins/src/lib.rs | 7 +- plugins/src/options.rs | 189 +++++++++++++++---------- 4 files changed, 132 insertions(+), 84 deletions(-) diff --git a/plugins/examples/cln-plugin-startup.rs b/plugins/examples/cln-plugin-startup.rs index 48144ceee..c876ba2e4 100644 --- a/plugins/examples/cln-plugin-startup.rs +++ b/plugins/examples/cln-plugin-startup.rs @@ -12,15 +12,15 @@ async fn main() -> Result<(), anyhow::Error> { let state = (); if let Some(plugin) = Builder::new(tokio::io::stdin(), tokio::io::stdout()) - .option( - options::ConfigOption::new_i64_with_default( - "test-option", - 42, - "a test-option with default 42", - ) - .build(), - ) - .option(options::ConfigOption::new_opt_i64("opt-option", "An optional option").build()) + .option(options::ConfigOption::new_i64_with_default( + "test-option", + 42, + "a test-option with default 42", + )) + .option(options::ConfigOption::new_i64_no_default( + "opt-option", + "An optional option", + )) .rpcmethod("testmethod", "This is a test", testmethod) .rpcmethod( "testoptions", diff --git a/plugins/grpc-plugin/src/main.rs b/plugins/grpc-plugin/src/main.rs index 6d16dea92..3f6de54db 100644 --- a/plugins/grpc-plugin/src/main.rs +++ b/plugins/grpc-plugin/src/main.rs @@ -25,7 +25,7 @@ async fn main() -> Result<()> { "grpc-port", -1, "Which port should the grpc plugin listen for incoming connections?", - ).build()) + )) .configure() .await? { diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs index c57860e72..ce2ed1e3c 100644 --- a/plugins/src/lib.rs +++ b/plugins/src/lib.rs @@ -130,8 +130,11 @@ where } } - pub fn option(mut self, opt: options::UntypedConfigOption) -> Builder { - self.options.insert(opt.name().to_string(), opt); + pub fn option( + mut self, + opt: options::ConfigOption, + ) -> Builder { + self.options.insert(opt.name().to_string(), opt.build()); self } diff --git a/plugins/src/options.rs b/plugins/src/options.rs index 35f22a71b..465aa18f6 100644 --- a/plugins/src/options.rs +++ b/plugins/src/options.rs @@ -3,14 +3,53 @@ use serde::ser::{SerializeStruct, Serializer}; use serde::Serialize; // Marker trait for possible values of options -pub trait OptionType {} +pub trait OptionType { + fn convert_default(value: Option<&Self>) -> Option; +} -impl OptionType for String {} -impl OptionType for i64 {} -impl OptionType for bool {} -impl OptionType for Option {} -impl OptionType for Option {} -impl OptionType for Option {} +impl OptionType for &str { + fn convert_default(value: Option<&Self>) -> Option { + value.map(|s| Value::String(s.to_string())) + } +} + +impl OptionType for String { + fn convert_default(value: Option<&Self>) -> Option { + value.map(|s| Value::String(s.clone())) + } +} +impl OptionType for i64 { + fn convert_default(value: Option<&Self>) -> Option { + value.map(|i| Value::Integer(*i)) + } +} +impl OptionType for bool { + fn convert_default(value: Option<&Self>) -> Option { + value.map(|b| Value::Boolean(*b)) + } +} + +impl OptionType for Option { + fn convert_default(_value: Option<&Self>) -> Option { + None + } +} + +impl OptionType for Option<&str> { + fn convert_default(_value: Option<&Self>) -> Option { + None + } +} +impl OptionType for Option { + fn convert_default(_value: Option<&Self>) -> Option { + None + } +} +impl OptionType for Option { + fn convert_default(_value: Option<&Self>) -> Option { + None + } +} #[derive(Clone, Debug, Serialize)] pub enum ValueType { @@ -87,118 +126,106 @@ impl Value { } #[derive(Clone, Debug)] -pub struct ConfigOption { - name: String, +pub struct ConfigOption<'a, V: OptionType> { + name: &'a str, default: Option, value_type: ValueType, - description: String, + description: &'a str, } -impl ConfigOption { +impl ConfigOption<'_, V> { pub fn build(&self) -> UntypedConfigOption { UntypedConfigOption { - name: self.name.clone(), + name: self.name.to_string(), value_type: self.value_type.clone(), - default: self.default.as_ref().map(|s| Value::String(s.clone())), - description: self.description.clone(), + default: OptionType::convert_default(self.default.as_ref()), value: None, + description: self.description.to_string(), } } +} - pub fn new_str_with_default, S2: AsRef, S3: AsRef>( - name: S1, - default: S2, - description: S3, +impl ConfigOption<'_, &'static str> { + pub const fn new_str_with_default( + name: &'static str, + default: &'static str, + description: &'static str, ) -> Self { Self { - name: name.as_ref().to_string(), - default: Some(default.as_ref().to_string()), + name: name, + default: Some(default), value_type: ValueType::String, - description: description.as_ref().to_string(), + description: description, } } } -impl ConfigOption { - pub fn build(&self) -> UntypedConfigOption { - UntypedConfigOption { - name: self.name.clone(), - value_type: self.value_type.clone(), - default: self.default.map(|i| Value::Integer(i)), - description: self.description.clone(), - value: None, +impl ConfigOption<'_, Option<&str>> { + pub const fn new_str_no_default(name: &'static str, description: &'static str) -> Self { + Self { + name, + default: None, + value_type: ValueType::String, + description, } } +} - pub fn new_i64_with_default, C: AsRef>( - name: A, +impl ConfigOption<'_, i64> { + pub const fn new_i64_with_default( + name: &'static str, default: i64, - description: C, + description: &'static str, ) -> Self { Self { - name: name.as_ref().to_string(), + name: name, default: Some(default), value_type: ValueType::Integer, - description: description.as_ref().to_string(), + description: description, } } } -impl ConfigOption> { - pub fn new_opt_i64, S2: AsRef>(name: S1, description: S2) -> Self { +impl ConfigOption<'_, Option> { + pub const fn new_i64_no_default(name: &'static str, description: &'static str) -> Self { Self { - name: name.as_ref().to_string(), + name: name, default: None, value_type: ValueType::Integer, - description: description.as_ref().to_string(), - } - } - - pub fn build(&self) -> UntypedConfigOption { - UntypedConfigOption { - name: self.name.clone(), - value_type: self.value_type.clone(), - default: None, - description: self.description.clone(), - value: None, + description: description, } } } -impl ConfigOption { - pub fn build(&self) -> UntypedConfigOption { - let default = match self.value_type { - ValueType::Flag => Some(Value::Boolean(false)), - ValueType::Boolean => self.default.map(|b| Value::Boolean(b)), - _ => panic!("Failed to build type"), - }; - - UntypedConfigOption { - name: self.name.clone(), - value_type: self.value_type.clone(), - default, - description: self.description.clone(), - value: None, +impl ConfigOption<'_, Option> { + pub const fn new_bool_no_default(name: &'static str, description: &'static str) -> Self { + Self { + name, + description, + default: None, + value_type: ValueType::Boolean, } } +} - pub fn new_bool_with_default, S2: AsRef>( - name: S1, +impl ConfigOption<'_, bool> { + pub const fn new_bool_with_default( + name: &'static str, default: bool, - description: S2, + description: &'static str, ) -> Self { Self { - name: name.as_ref().to_string(), - description: description.as_ref().to_string(), + name, + description, default: Some(default), value_type: ValueType::Boolean, } } - pub fn new_flag, S2: AsRef>(name: S1, description: S2) -> Self { + pub const fn new_flag(name: &'static str, description: &'static str) -> Self { Self { - name: name.as_ref().to_string(), - description: description.as_ref().to_string(), + name, + description, default: Some(false), value_type: ValueType::Flag, } @@ -256,7 +283,7 @@ impl Serialize for UntypedConfigOption { } } -impl ConfigOption +impl ConfigOption<'_, V> where V: OptionType, { @@ -319,10 +346,28 @@ mod test { } } + #[test] + fn const_config_option() { + const _: ConfigOption = ConfigOption::new_flag("flag-option", "A flag option"); + const _: ConfigOption = + ConfigOption::new_bool_with_default("bool-option", false, "A boolean option"); + const _: ConfigOption> = + ConfigOption::new_bool_no_default("bool-option", "A boolean option"); + + const _: ConfigOption> = + ConfigOption::new_i64_no_default("integer-option", "A flag option"); + const _: ConfigOption = + ConfigOption::new_i64_with_default("integer-option", 12, "A flag option"); + + const _: ConfigOption> = + ConfigOption::new_str_no_default("integer-option", "A flag option"); + const _: ConfigOption<&str> = + 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")); } }