cln-plugin: Add options to the getmanifest call

This commit is contained in:
Christian Decker 2022-02-07 11:00:08 +01:00 committed by Rusty Russell
parent fe21b89b56
commit 249fa8675a
5 changed files with 183 additions and 18 deletions

View file

@ -1,12 +1,19 @@
//! This is a test plugin used to verify that we can compile and run
//! plugins using the Rust API against c-lightning.
use cln_plugin::Builder;
use cln_plugin::{options, Builder};
use tokio;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let (plugin, stdin) = Builder::new((), tokio::io::stdin(), tokio::io::stdout()).build();
let (plugin, stdin) = Builder::new((), tokio::io::stdin(), tokio::io::stdout())
.option(options::ConfigOption::new(
"test-option",
options::Value::Integer(42),
"a test-option with default 42",
))
.build();
tokio::spawn(async {
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
log::info!("Hello world");

View file

@ -18,6 +18,10 @@ mod messages;
#[macro_use]
extern crate serde_json;
pub mod options;
use options::ConfigOption;
/// Builder for a new plugin.
pub struct Builder<S, I, O>
where
@ -35,6 +39,8 @@ where
#[allow(dead_code)]
subscriptions: Subscriptions,
options: Vec<ConfigOption>,
}
impl<S, I, O> Builder<S, I, O>
@ -50,9 +56,15 @@ where
output,
hooks: Hooks::default(),
subscriptions: Subscriptions::default(),
options: vec![],
}
}
pub fn option(mut self, opt: options::ConfigOption) -> Builder<S, I, O> {
self.options.push(opt);
self
}
pub fn build(self) -> (Plugin<S, I, O>, I) {
let output = Arc::new(Mutex::new(FramedWrite::new(
self.output,
@ -67,6 +79,7 @@ where
state: Arc::new(Mutex::new(self.state)),
output,
input_type: PhantomData,
options: self.options,
},
self.input,
)
@ -85,7 +98,20 @@ where
/// The state gets cloned for each request
state: Arc<Mutex<S>>,
input_type: PhantomData<I>,
options: Vec<ConfigOption>,
}
impl<S, I, O> Plugin<S, I, O>
where
S: Clone + Send,
I: AsyncRead + Send + Unpin,
O: Send + AsyncWrite + Unpin,
{
pub fn options(&self) -> Vec<ConfigOption> {
self.options.clone()
}
}
impl<S, I, O> Plugin<S, I, O>
where
S: Clone + Send,
@ -130,8 +156,7 @@ where
let state = self.state.clone();
let res: serde_json::Value = match request {
messages::Request::Getmanifest(c) => {
serde_json::to_value(Plugin::<S, I, O>::handle_get_manifest(c, state).await?)
.unwrap()
serde_json::to_value(self.handle_get_manifest(c, state).await?).unwrap()
}
messages::Request::Init(c) => {
serde_json::to_value(Plugin::<S, I, O>::handle_init(c, state).await?).unwrap()
@ -160,10 +185,14 @@ where
}
async fn handle_get_manifest(
&mut self,
_call: messages::GetManifestCall,
_state: Arc<Mutex<S>>,
) -> Result<messages::GetManifestResponse, Error> {
Ok(messages::GetManifestResponse::default())
Ok(messages::GetManifestResponse {
options: self.options.clone(),
rpcmethods: vec![],
})
}
async fn handle_init(
@ -192,6 +221,6 @@ mod test {
#[test]
fn init() {
let builder = Builder::new((), tokio::io::stdin(), tokio::io::stdout());
let plugin = builder.build();
builder.build();
}
}

View file

@ -1,3 +1,4 @@
use crate::options::ConfigOption;
use serde::de::{self, Deserializer};
use serde::{Deserialize, Serialize};
use serde_json::Value;
@ -149,9 +150,9 @@ where
}
#[derive(Serialize, Default, Debug)]
pub struct GetManifestResponse {
options: Vec<()>,
rpcmethods: Vec<()>,
pub(crate) struct GetManifestResponse {
pub(crate) options: Vec<ConfigOption>,
pub(crate) rpcmethods: Vec<()>,
}
#[derive(Serialize, Default, Debug)]

122
plugins/src/options.rs Normal file
View file

@ -0,0 +1,122 @@
use serde::ser::{SerializeStruct, Serializer};
use serde::{Serialize};
#[derive(Clone, Debug)]
pub enum Value {
String(String),
Integer(i64),
Boolean(bool),
}
/// An stringly typed option that is passed to
#[derive(Clone, Debug)]
pub struct ConfigOption {
name: String,
pub(crate) value: Option<Value>,
default: Value,
description: String,
}
impl ConfigOption {
pub fn name(&self) -> &str {
&self.name
}
pub fn default(&self) -> &Value {
&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<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 {
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)?;
}
}
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(),
}
}
pub fn description(&self) -> String {
self.description.clone()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_option_serialize() {
let tests = vec![
(
ConfigOption::new("name", Value::String("default".to_string()), "description"),
json!({
"name": "name",
"description":"description",
"default": "default",
"type": "string",
}),
),
(
ConfigOption::new("name", Value::Integer(42), "description"),
json!({
"name": "name",
"description":"description",
"default": 42,
"type": "int",
}),
),
(
ConfigOption::new("name", Value::Boolean(true), "description"
json!({
"name": "name",
"description":"description",
"default": true,
"type": "booltes",
}),
)),
];
for (input, expected) in tests.iter() {
let res = serde_json::to_value(input).unwrap();
assert_eq!(&res, expected);
}
}
}

View file

@ -21,14 +21,20 @@ def test_rpc_client(node_factory):
def test_plugin_start(node_factory):
"""Start a minimal plugin and ensure it is well-behaved
"""
bin_path = Path.cwd() / "target" / "debug" / "examples" / "cln-plugin-startup"
l1 = node_factory.get_node(options={"plugin": str(bin_path)})
"""Start a minimal plugin and ensure it is well-behaved
"""
bin_path = Path.cwd() / "target" / "debug" / "examples" / "cln-plugin-startup"
l1 = node_factory.get_node(options={"plugin": str(bin_path)})
# The plugin should be in the list of active plugins
plugins = l1.rpc.plugin('list')['plugins']
assert len([p for p in plugins if 'cln-plugin-startup' in p['name'] and p['active']]) == 1
cfg = l1.rpc.listconfigs()
p = cfg['plugins'][0]
p['path'] = None # The path is host-specific, so blank it.
expected = {
'name': 'cln-plugin-startup',
'options': {
'test-option': 42
},
'path': None
}
assert expected == p
# Logging should also work through the log integration
l1.daemon.wait_for_log(r'Hello world')