cln-grpc: Add result conversion generator to msggen

This takes the Rust bindings and converts them into the generated
protobuf bindings:

> JSON-RPC -> Rust bindings -> grpc bindings -> protobuf
This commit is contained in:
Christian Decker 2022-01-15 13:56:49 +01:00
parent 4c105d2424
commit 8d3871d791
4 changed files with 276 additions and 2 deletions

View file

@ -2,7 +2,9 @@ cln-grpc-wrongdir:
$(MAKE) -C .. cln-grpc-all
CLN_GRPC_EXAMPLES :=
CLN_GRPC_GENALL = cln-grpc/proto/node.proto
CLN_GRPC_GENALL = cln-grpc/proto/node.proto \
cln-grpc/src/convert.rs
DEFAULT_TARGETS += $(CLN_GRPC_EXAMPLES) $(CLN_GRPC_GENALL)
$(CLN_GRPC_GENALL): $(JSON_SCHEMA)

170
cln-grpc/src/convert.rs Normal file
View file

@ -0,0 +1,170 @@
// This file was automatically derived from the JSON-RPC schemas in
// `doc/schemas`. Do not edit this file manually as it would get
// overwritten.
use std::convert::From;
#[allow(unused_imports)]
use cln_rpc::model::{responses,requests};
use crate::pb;
#[allow(unused_variables)]
impl From<&responses::GetinfoAddress> for pb::GetinfoAddress {
fn from(c: &responses::GetinfoAddress) -> Self {
Self {
item_type: c.item_type as i32,
port: c.port.into(),
address: c.address.clone(),
}
}
}
#[allow(unused_variables)]
impl From<&responses::GetinfoBinding> for pb::GetinfoBinding {
fn from(c: &responses::GetinfoBinding) -> Self {
Self {
item_type: c.item_type as i32,
address: c.address.clone(),
port: c.port.map(|v| v.into()),
socket: c.socket.clone(),
}
}
}
#[allow(unused_variables)]
impl From<&responses::GetinfoResponse> for pb::GetinfoResponse {
fn from(c: &responses::GetinfoResponse) -> Self {
Self {
id: hex::decode(&c.id).unwrap(),
alias: c.alias.clone(),
color: hex::decode(&c.color).unwrap(),
num_peers: c.num_peers.clone(),
num_pending_channels: c.num_pending_channels.clone(),
num_active_channels: c.num_active_channels.clone(),
num_inactive_channels: c.num_inactive_channels.clone(),
version: c.version.clone(),
lightning_dir: c.lightning_dir.clone(),
blockheight: c.blockheight.clone(),
network: c.network.clone(),
fees_collected_msat: Some(c.fees_collected_msat.into()),
address: c.address.iter().map(|s| s.into()).collect(),
binding: c.binding.iter().map(|s| s.into()).collect(),
warning_bitcoind_sync: c.warning_bitcoind_sync.clone(),
warning_lightningd_sync: c.warning_lightningd_sync.clone(),
}
}
}
#[allow(unused_variables)]
impl From<&responses::ListfundsOutputs> for pb::ListfundsOutputs {
fn from(c: &responses::ListfundsOutputs) -> Self {
Self {
txid: hex::decode(&c.txid).unwrap(),
output: c.output.clone(),
amount_msat: Some(c.amount_msat.into()),
scriptpubkey: hex::decode(&c.scriptpubkey).unwrap(),
address: c.address.clone(),
redeemscript: c.redeemscript.as_ref().map(|v| hex::decode(&v).unwrap()),
status: c.status as i32,
blockheight: c.blockheight.clone(),
}
}
}
#[allow(unused_variables)]
impl From<&responses::ListfundsChannels> for pb::ListfundsChannels {
fn from(c: &responses::ListfundsChannels) -> Self {
Self {
peer_id: hex::decode(&c.peer_id).unwrap(),
our_amount_msat: Some(c.our_amount_msat.into()),
amount_msat: Some(c.amount_msat.into()),
funding_txid: hex::decode(&c.funding_txid).unwrap(),
funding_output: c.funding_output.clone(),
connected: c.connected.clone(),
state: c.state as i32,
short_channel_id: c.short_channel_id.clone(),
}
}
}
#[allow(unused_variables)]
impl From<&responses::ListfundsResponse> for pb::ListfundsResponse {
fn from(c: &responses::ListfundsResponse) -> Self {
Self {
outputs: c.outputs.iter().map(|s| s.into()).collect(),
channels: c.channels.iter().map(|s| s.into()).collect(),
}
}
}
#[allow(unused_variables)]
impl From<&responses::ListchannelsChannels> for pb::ListchannelsChannels {
fn from(c: &responses::ListchannelsChannels) -> Self {
Self {
source: hex::decode(&c.source).unwrap(),
destination: hex::decode(&c.destination).unwrap(),
public: c.public.clone(),
amount_msat: Some(c.amount_msat.into()),
message_flags: c.message_flags.into(),
channel_flags: c.channel_flags.into(),
active: c.active.clone(),
last_update: c.last_update.clone(),
base_fee_millisatoshi: c.base_fee_millisatoshi.clone(),
fee_per_millionth: c.fee_per_millionth.clone(),
delay: c.delay.clone(),
htlc_minimum_msat: Some(c.htlc_minimum_msat.into()),
htlc_maximum_msat: c.htlc_maximum_msat.map(|f| f.into()),
features: hex::decode(&c.features).unwrap(),
}
}
}
#[allow(unused_variables)]
impl From<&responses::ListchannelsResponse> for pb::ListchannelsResponse {
fn from(c: &responses::ListchannelsResponse) -> Self {
Self {
channels: c.channels.iter().map(|s| s.into()).collect(),
}
}
}
#[allow(unused_variables)]
impl From<&responses::AddgossipResponse> for pb::AddgossipResponse {
fn from(c: &responses::AddgossipResponse) -> Self {
Self {
}
}
}
#[allow(unused_variables)]
impl From<&responses::AutocleaninvoiceResponse> for pb::AutocleaninvoiceResponse {
fn from(c: &responses::AutocleaninvoiceResponse) -> Self {
Self {
enabled: c.enabled.clone(),
expired_by: c.expired_by.clone(),
cycle_seconds: c.cycle_seconds.clone(),
}
}
}
#[allow(unused_variables)]
impl From<&responses::CheckmessageResponse> for pb::CheckmessageResponse {
fn from(c: &responses::CheckmessageResponse) -> Self {
Self {
verified: c.verified.clone(),
pubkey: c.pubkey.as_ref().map(|v| hex::decode(&v).unwrap()),
}
}
}
#[allow(unused_variables)]
impl From<&responses::CloseResponse> for pb::CloseResponse {
fn from(c: &responses::CloseResponse) -> Self {
Self {
item_type: c.item_type as i32,
tx: c.tx.as_ref().map(|v| hex::decode(&v).unwrap()),
txid: c.txid.as_ref().map(|v| hex::decode(&v).unwrap()),
}
}
}

View file

@ -1,5 +1,5 @@
from msggen.model import Method, CompositeField, Service
from msggen.grpc import GrpcGenerator
from msggen.grpc import GrpcGenerator, GrpcConverterGenerator
from msggen.rust import RustGenerator
from pathlib import Path
import subprocess
@ -132,6 +132,10 @@ def gengrpc(service):
fname = repo_root() / "cln-grpc" / "proto" / "node.proto"
dest = open(fname, "w")
GrpcGenerator(dest).generate(service)
fname = repo_root() / "cln-grpc" / "src" / "convert.rs"
dest = open(fname, "w")
GrpcConverterGenerator(dest).generate(service)
def genrustjsonrpc(service):
fname = repo_root() / "cln-rpc" / "src" / "model.rs"
dest = open(fname, "w")

View file

@ -151,3 +151,101 @@ class GrpcGenerator:
for message in [f for f in fields if isinstance(f, CompositeField)]:
self.generate_message(message)
class GrpcConverterGenerator:
def __init__(self, dest: TextIO):
self.dest = dest
self.logger = logging.getLogger("msggen.grpc.GrpcConversionGenerator")
def generate_array(self, prefix, field: ArrayField):
if isinstance(field.itemtype, CompositeField):
self.generate_composite(prefix, field.itemtype)
def generate_composite(self, prefix, field: CompositeField):
"""Generates the conversions from JSON-RPC to GRPC.
"""
# First pass: generate any sub-fields before we generate the
# top-level field itself.
for f in field.fields:
if isinstance(f, ArrayField):
self.generate_array(prefix, f)
# And now we can convert the current field:
self.write(f"""\
#[allow(unused_variables)]
impl From<&{prefix}::{field.typename}> for pb::{field.typename} {{
fn from(c: &{prefix}::{field.typename}) -> Self {{
Self {{
""")
for f in field.fields:
name = f.normalized()
if isinstance(f, ArrayField):
self.write(f"{name}: c.{name}.iter().map(|s| s.into()).collect(),\n", numindent=3)
elif isinstance(f, EnumField):
self.write(f"{name}: c.{name} as i32,\n", numindent=3)
elif isinstance(f, PrimitiveField):
typ = f.typename + ("?" if not f.required else "")
# We may need to reduce or increase the size of some
# types, or have some conversion such as
# hex-decoding. Also includes the `Some()` that grpc
# requires for non-native types.
rhs = {
'u8': f'c.{name}.into()',
'u16': f'c.{name}.into()',
'u16?': f'c.{name}.map(|v| v.into())',
'msat': f'Some(c.{name}.into())',
'msat?': f'c.{name}.map(|f| f.into())',
'pubkey': f'hex::decode(&c.{name}).unwrap()',
'pubkey?': f'c.{name}.as_ref().map(|v| hex::decode(&v).unwrap())',
'hex': f'hex::decode(&c.{name}).unwrap()',
'hex?': f'c.{name}.as_ref().map(|v| hex::decode(&v).unwrap())',
'txid': f'hex::decode(&c.{name}).unwrap()',
'txid?': f'c.{name}.as_ref().map(|v| hex::decode(&v).unwrap())',
}.get(
typ,
f'c.{name}.clone()' # default to just assignment
)
self.write(f"{name}: {rhs},\n", numindent=3)
self.write(f"""\
}}
}}
}}
""")
def generate_requests(self, service):
for meth in service.methods:
req = meth.request
self.generate_composite("requests", req)
def generate_responses(self, service):
for meth in service.methods:
res = meth.response
self.generate_composite("responses", res)
def generate(self, service: Service) -> None:
self.write("""
// This file was automatically derived from the JSON-RPC schemas in
// `doc/schemas`. Do not edit this file manually as it would get
// overwritten.
use std::convert::From;
#[allow(unused_imports)]
use cln_rpc::model::{responses,requests};
use crate::pb;
""")
self.generate_responses(service)
def write(self, text: str, numindent: int = 0) -> None:
raw = dedent(text)
if numindent > 0:
raw = indent(text, " " * numindent)
self.dest.write(raw)