mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-02-22 06:41:44 +01:00
cln-plugin: Add options to the getmanifest
call
This commit is contained in:
parent
fe21b89b56
commit
249fa8675a
5 changed files with 183 additions and 18 deletions
|
@ -1,12 +1,19 @@
|
||||||
//! This is a test plugin used to verify that we can compile and run
|
//! This is a test plugin used to verify that we can compile and run
|
||||||
//! plugins using the Rust API against c-lightning.
|
//! plugins using the Rust API against c-lightning.
|
||||||
|
|
||||||
use cln_plugin::Builder;
|
use cln_plugin::{options, Builder};
|
||||||
use tokio;
|
use tokio;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), anyhow::Error> {
|
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::spawn(async {
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
|
||||||
log::info!("Hello world");
|
log::info!("Hello world");
|
||||||
|
|
|
@ -18,6 +18,10 @@ mod messages;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
|
pub mod options;
|
||||||
|
|
||||||
|
use options::ConfigOption;
|
||||||
|
|
||||||
/// Builder for a new plugin.
|
/// Builder for a new plugin.
|
||||||
pub struct Builder<S, I, O>
|
pub struct Builder<S, I, O>
|
||||||
where
|
where
|
||||||
|
@ -35,6 +39,8 @@ where
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
subscriptions: Subscriptions,
|
subscriptions: Subscriptions,
|
||||||
|
|
||||||
|
options: Vec<ConfigOption>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, I, O> Builder<S, I, O>
|
impl<S, I, O> Builder<S, I, O>
|
||||||
|
@ -50,9 +56,15 @@ where
|
||||||
output,
|
output,
|
||||||
hooks: Hooks::default(),
|
hooks: Hooks::default(),
|
||||||
subscriptions: Subscriptions::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) {
|
pub fn build(self) -> (Plugin<S, I, O>, I) {
|
||||||
let output = Arc::new(Mutex::new(FramedWrite::new(
|
let output = Arc::new(Mutex::new(FramedWrite::new(
|
||||||
self.output,
|
self.output,
|
||||||
|
@ -67,6 +79,7 @@ where
|
||||||
state: Arc::new(Mutex::new(self.state)),
|
state: Arc::new(Mutex::new(self.state)),
|
||||||
output,
|
output,
|
||||||
input_type: PhantomData,
|
input_type: PhantomData,
|
||||||
|
options: self.options,
|
||||||
},
|
},
|
||||||
self.input,
|
self.input,
|
||||||
)
|
)
|
||||||
|
@ -85,7 +98,20 @@ where
|
||||||
/// The state gets cloned for each request
|
/// The state gets cloned for each request
|
||||||
state: Arc<Mutex<S>>,
|
state: Arc<Mutex<S>>,
|
||||||
input_type: PhantomData<I>,
|
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>
|
impl<S, I, O> Plugin<S, I, O>
|
||||||
where
|
where
|
||||||
S: Clone + Send,
|
S: Clone + Send,
|
||||||
|
@ -130,8 +156,7 @@ where
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
let res: serde_json::Value = match request {
|
let res: serde_json::Value = match request {
|
||||||
messages::Request::Getmanifest(c) => {
|
messages::Request::Getmanifest(c) => {
|
||||||
serde_json::to_value(Plugin::<S, I, O>::handle_get_manifest(c, state).await?)
|
serde_json::to_value(self.handle_get_manifest(c, state).await?).unwrap()
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
messages::Request::Init(c) => {
|
messages::Request::Init(c) => {
|
||||||
serde_json::to_value(Plugin::<S, I, O>::handle_init(c, state).await?).unwrap()
|
serde_json::to_value(Plugin::<S, I, O>::handle_init(c, state).await?).unwrap()
|
||||||
|
@ -160,10 +185,14 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_get_manifest(
|
async fn handle_get_manifest(
|
||||||
|
&mut self,
|
||||||
_call: messages::GetManifestCall,
|
_call: messages::GetManifestCall,
|
||||||
_state: Arc<Mutex<S>>,
|
_state: Arc<Mutex<S>>,
|
||||||
) -> Result<messages::GetManifestResponse, Error> {
|
) -> Result<messages::GetManifestResponse, Error> {
|
||||||
Ok(messages::GetManifestResponse::default())
|
Ok(messages::GetManifestResponse {
|
||||||
|
options: self.options.clone(),
|
||||||
|
rpcmethods: vec![],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_init(
|
async fn handle_init(
|
||||||
|
@ -192,6 +221,6 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn init() {
|
fn init() {
|
||||||
let builder = Builder::new((), tokio::io::stdin(), tokio::io::stdout());
|
let builder = Builder::new((), tokio::io::stdin(), tokio::io::stdout());
|
||||||
let plugin = builder.build();
|
builder.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::options::ConfigOption;
|
||||||
use serde::de::{self, Deserializer};
|
use serde::de::{self, Deserializer};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
@ -149,9 +150,9 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Default, Debug)]
|
#[derive(Serialize, Default, Debug)]
|
||||||
pub struct GetManifestResponse {
|
pub(crate) struct GetManifestResponse {
|
||||||
options: Vec<()>,
|
pub(crate) options: Vec<ConfigOption>,
|
||||||
rpcmethods: Vec<()>,
|
pub(crate) rpcmethods: Vec<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Default, Debug)]
|
#[derive(Serialize, Default, Debug)]
|
||||||
|
|
122
plugins/src/options.rs
Normal file
122
plugins/src/options.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,14 +21,20 @@ def test_rpc_client(node_factory):
|
||||||
|
|
||||||
|
|
||||||
def test_plugin_start(node_factory):
|
def test_plugin_start(node_factory):
|
||||||
"""Start a minimal plugin and ensure it is well-behaved
|
"""Start a minimal plugin and ensure it is well-behaved
|
||||||
"""
|
"""
|
||||||
bin_path = Path.cwd() / "target" / "debug" / "examples" / "cln-plugin-startup"
|
bin_path = Path.cwd() / "target" / "debug" / "examples" / "cln-plugin-startup"
|
||||||
l1 = node_factory.get_node(options={"plugin": str(bin_path)})
|
l1 = node_factory.get_node(options={"plugin": str(bin_path)})
|
||||||
|
|
||||||
# The plugin should be in the list of active plugins
|
cfg = l1.rpc.listconfigs()
|
||||||
plugins = l1.rpc.plugin('list')['plugins']
|
p = cfg['plugins'][0]
|
||||||
assert len([p for p in plugins if 'cln-plugin-startup' in p['name'] and p['active']]) == 1
|
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')
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue