cln_plugin: Request value as rust primitive

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`.
This commit is contained in:
Erik De Smedt 2024-02-02 13:10:51 +01:00 committed by Christian Decker
parent 543e67495c
commit 74d13bb334
4 changed files with 295 additions and 143 deletions

View file

@ -47,7 +47,7 @@ async fn main() -> Result<(), anyhow::Error> {
async fn testoptions(p: Plugin<()>, _v: serde_json::Value) -> Result<serde_json::Value, Error> {
Ok(json!({
"opt-option": format!("{:?}", p.option("opt-option").unwrap())
"opt-option": format!("{:?}", p.option_str("opt-option").unwrap())
}))
}

View file

@ -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<u8>,
}
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)?;

View file

@ -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<String, Hook<S>>,
options: HashMap<String, UntypedConfigOption>,
option_values: HashMap<String, Option<options::Value>>,
rpcmethods: HashMap<String, RpcMethod<S>>,
subscriptions: HashMap<String, Subscription<S>>,
notifications: Vec<NotificationTopic>,
@ -66,6 +67,7 @@ where
input: FramedRead<I, JsonRpcCodec>,
output: Arc<Mutex<FramedWrite<O, JsonCodec>>>,
options: HashMap<String, UntypedConfigOption>,
option_values: HashMap<String, Option<options::Value>>,
configuration: Configuration,
rpcmethods: HashMap<String, AsyncCallback<S>>,
hooks: HashMap<String, AsyncCallback<S>>,
@ -100,6 +102,7 @@ where
state: S,
/// "options" field of "init" message sent by cln
options: HashMap<String, UntypedConfigOption>,
option_values: HashMap<String, Option<options::Value>>,
/// "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<options::Value> = 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<S> Plugin<S>
where
S: Clone + Send,
{
pub fn option(&self, name: &str) -> Option<options::Value> {
self.options.get(name).and_then(|x| x.value.clone())
pub fn option_str(&self, name: &str) -> Result<Option<options::Value>> {
self.option_values
.get(name)
.ok_or(anyhow!("No option named {}", name))
.map(|c| c.clone())
}
pub fn option<OV: OptionType>(
&self,
config_option: &options::ConfigOption<OV>,
) -> Result<OV::OutputValue> {
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<options::Value> {
self.options.get(name).and_then(|c| c.value.clone())
pub fn option_str(&self, name: &str) -> Result<Option<options::Value>> {
self.option_values
.get(name)
.ok_or(anyhow!("No option named '{}'", name))
.map(|c| c.clone())
}
pub fn option<OV: OptionType>(
&self,
config_option: &options::ConfigOption<OV>,
) -> Result<OV::OutputValue> {
let value = self.option_str(config_option.name())?;
Ok(OV::from_value(&value))
}
/// return the cln configuration send to the

View file

@ -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<Value>;
type OutputValue;
type DefaultValue;
fn convert_default(value: &Self::DefaultValue) -> Option<Value>;
fn from_value(value: &Option<Value>) -> Self::OutputValue;
fn get_value_type() -> ValueType;
}
impl OptionType for &str {
fn convert_default(value: Option<&Self>) -> Option<Value> {
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<Value> {
Some(Value::String(value.to_string()))
}
fn from_value(value: &Option<Value>) -> 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> {
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<Value> {
Some(Value::Integer(*value))
}
}
impl OptionType for i64 {
fn convert_default(value: Option<&Self>) -> Option<Value> {
value.map(|i| Value::Integer(*i))
fn from_value(value: &Option<Value>) -> 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> {
value.map(|b| Value::Boolean(*b))
fn get_value_type() -> ValueType {
ValueType::Integer
}
}
impl OptionType for Option<String> {
fn convert_default(_value: Option<&Self>) -> Option<Value> {
None
impl OptionType for config_type::DefaultBoolean {
type OutputValue = bool;
type DefaultValue = bool;
fn convert_default(value: &bool) -> Option<Value> {
Some(Value::Boolean(*value))
}
fn from_value(value: &Option<Value>) -> 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<Value> {
None
impl OptionType for config_type::Flag {
type OutputValue = bool;
type DefaultValue = ();
fn convert_default(_value: &()) -> Option<Value> {
Some(Value::Boolean(false))
}
fn from_value(value: &Option<Value>) -> 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<i64> {
fn convert_default(_value: Option<&Self>) -> Option<Value> {
impl OptionType for config_type::String {
type OutputValue = Option<String>;
type DefaultValue = ();
fn convert_default(_value: &()) -> Option<Value> {
None
}
fn from_value(value: &Option<Value>) -> Option<String> {
match value {
Some(Value::String(s)) => Some(s.to_string()),
None => None,
_ => panic!(
"Type mismatch. Expected Option<string> but found {:?}",
value
),
}
}
fn get_value_type() -> ValueType {
ValueType::String
}
}
impl OptionType for Option<bool> {
fn convert_default(_value: Option<&Self>) -> Option<Value> {
impl OptionType for config_type::Integer {
type OutputValue = Option<i64>;
type DefaultValue = ();
fn convert_default(_value: &()) -> Option<Value> {
None
}
fn from_value(value: &Option<Value>) -> Self::OutputValue {
match value {
Some(Value::Integer(i)) => Some(*i),
None => None,
_ => panic!(
"Type mismatch. Expected Option<Integer> but found {:?}",
value
),
}
}
fn get_value_type() -> ValueType {
ValueType::Integer
}
}
impl OptionType for config_type::Boolean {
type OutputValue = Option<bool>;
type DefaultValue = ();
fn convert_default(_value: &()) -> Option<Value> {
None
}
fn from_value(value: &Option<Value>) -> Self::OutputValue {
match value {
Some(Value::Boolean(b)) => Some(*b),
None => None,
_ => panic!(
"Type mismatch. Expected Option<Boolean> 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<S>(&self, serializer: S) -> std::prelude::v1::Result<S::Ok, S::Error>
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<V>,
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<V: OptionType> 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: <V as OptionType>::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<i64>> {
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<bool>> {
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<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
default: Option<Value>,
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<V> 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<bool> = ConfigOption::new_flag("flag-option", "A flag option");
const _: ConfigOption<bool> =
// 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<Option<bool>> =
const _: BooleanConfigOption =
ConfigOption::new_bool_no_default("bool-option", "A boolean option");
const _: ConfigOption<Option<i64>> =
const _: IntegerConfigOption =
ConfigOption::new_i64_no_default("integer-option", "A flag option");
const _: ConfigOption<i64> =
const _: DefaultIntegerConfigOption =
ConfigOption::new_i64_with_default("integer-option", 12, "A flag option");
const _: ConfigOption<Option<&str>> =
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");
}