-
Notifications
You must be signed in to change notification settings - Fork 910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cln_plugin
Refactor ConfigOption
#7042
cln_plugin
Refactor ConfigOption
#7042
Conversation
We used to store all `ConfigOptions` in a `Vec`. I decided to use a `HashMap` isntead because it will be easier to implement dynamic options later on.
a1485de
to
80836f0
Compare
Breaking changes here. This improves the semantics of `ConfigOption` and `options::Value` drastically. We've been using `options::Value` for 2 purposes 1. To specify the type and default value of an option 2. Coummunicate how a user configured an option We fall here in the pit-fall of being poor at both purposes. I've edited the code to ensure - `options::Value` -> To read or specify the value of an option - `option::ValueType` -> To specify the type of an option **Configure an option** Let's create an string-typed option create an option named `"required-string-opt"` the developer has to use the following code. ```rust let option = ConfigOption::new("required-string", Value::OptString, "description"); ``` The semantics of `OptString` might falsely suggest that it is optional to specify the config. However, we use `OptString` to say we are not providing a default-value. After this commit we can use instead ```rust let option = ConfigOption::new_str_no_default("required-string", "description"); ``` For reading a configured value the `option::Value` is somewhat cumbersome. The old version of had 6 different types of value ```rust let value = plugin.option("required-string"); match value { String(s) => {}, // User has configured string value or default Integer(i) => {}, // User has configured int value or default Boolean(b) => {}, // User has configured bool value or default OptString => {}, // User has not configured value and no default OptInteger = {}, // User has not configured value and no default OptBOolean => {}, // User has not configured value and no default } ``` This has been changed to ```rust let value = plugin.option("required-string"); match value { Some(String(s)) => {}, Some(Integer(i)) => {}, Some(Boolean(b)) => {}, None => {}, } ```
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<i64> = 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)?; } ```
In the old version requesting the config-value of an option was a little bit tricky. Let's say we want to have a plugin which uses a default port of 1234. ```rust let value = plugin.option("plugin-port"); match value { Some(Value::Integer(_)) => {}, Some(Value::String(_)) => {}, // Can never happen Some(Value::Boolean(_)) => {}, // Can never happen None => {}, // Can never happen } ``` Many users of the `cln_plugin` crate are overly cautious and handle all these error scenario's. Which is completely unneeded. Core Lightning will complain if you put a `String` where an `Integer` is expected and will never send the value to the plug-in. This change makes the API much more ergonomical and actually motivates some of the changes in previous commits. ``` const MY_OPTION : ConfigOption<i64> = ConfigOption::new_i64_with_default( "plugin-port', 1235, "Description"); let value : Result<i64> = plugin.option(MY_OPTION); ``` The result will provide a proper error-message. It is also safe to `unwrap` the result because it will only be triggered if the user neglected to provide the option to the `Builder`.
80836f0
to
f57beb2
Compare
plugins/src/options.rs
Outdated
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, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, would you say this is an example of type-erasure? I've been looking for a good example to see why that is useful :-)
//! ```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<String> = 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(()) | ||
//! } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool, this is incredibly useful, thanks ^^
This PR contains breaking changes.
I refactored
ConfigOption
in thecln_plugin
crate and added some basic documentation.It becomes easier to request the configured value of the
ConfigOption
.I've marked this as draft because I still want to add some integration tests