Skip to content

Commit 5307586

Browse files
cdeckerrustyrussell
authored andcommittedJul 21, 2022
msggen: Add a new generator for grpc -> python converter
To test the grpc interface we'll want to emulate the JSON-RPC interface as best we can, hence when talking to the grpc interface we want to convert back into a parsed JSON format as LightningRpc would have returned it. This is just the simplest way of closing the loop here: ``` pyln-testing --grpc-> cln-grpc --grpc2json ^ | | v | JSON-RPC | | TEST v ^ CLN | | | v pyln-testing <-grpc2py-- cln-grpc <- json2grpc ```
1 parent bac322c commit 5307586

File tree

3 files changed

+1004
-0
lines changed

3 files changed

+1004
-0
lines changed
 

‎contrib/msggen/msggen/__main__.py

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from msggen.gen.grpc import GrpcGenerator, GrpcConverterGenerator, GrpcUnconverterGenerator, GrpcServerGenerator
3+
from msggen.gen.grpc2py import Grpc2PyGenerator
34
from msggen.gen.rust import RustGenerator
45
from msggen.gen.generator import GeneratorChain
56
from msggen.utils import repo_root, load_jsonrpc_service
@@ -22,6 +23,12 @@ def add_handler_gen_grpc(generator_chain: GeneratorChain, meta):
2223
generator_chain.add_generator(GrpcServerGenerator(dest))
2324

2425

26+
def add_handler_get_grpc2py(generator_chain: GeneratorChain):
27+
fname = repo_root() / "contrib" / "pyln-testing" / "pyln" / "testing" / "grpc2py.py"
28+
dest = open(fname, "w")
29+
generator_chain.add_generator(Grpc2PyGenerator(dest))
30+
31+
2532
def add_handler_gen_rust_jsonrpc(generator_chain: GeneratorChain):
2633
fname = repo_root() / "cln-rpc" / "src" / "model.rs"
2734
dest = open(fname, "w")
@@ -45,6 +52,7 @@ def run():
4552

4653
add_handler_gen_grpc(generator_chain, meta)
4754
add_handler_gen_rust_jsonrpc(generator_chain)
55+
add_handler_get_grpc2py(generator_chain)
4856

4957
generator_chain.generate(service)
5058

‎contrib/msggen/msggen/gen/grpc2py.py

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Converts the GRPC messages back to parsed JSON dicts in python.
2+
3+
This can be used to expose a local JSON-RPC socket but then talk to
4+
the node over the GRPC interface.
5+
6+
"""
7+
from msggen.model import ArrayField, CompositeField, EnumField, PrimitiveField, Service
8+
from msggen.gen import IGenerator
9+
import logging
10+
from textwrap import dedent
11+
from typing import TextIO
12+
import re
13+
14+
15+
def decamelcase(c):
16+
return re.sub(r'(?<!^)(?=[A-Z])', '_', c).lower()
17+
18+
19+
override = {
20+
21+
}
22+
23+
24+
class Grpc2PyGenerator(IGenerator):
25+
def __init__(self, dest: TextIO):
26+
self.dest = dest
27+
self.logger = logging.getLogger(__name__)
28+
29+
# Expressions used to convert the right-hand side into the
30+
# format we expect in the dict.
31+
self.converters = {
32+
'hex': "hexlify(m.{name})",
33+
'pubkey': "hexlify(m.{name})",
34+
'secret': "hexlify(m.{name})",
35+
'signature': "hexlify(m.{name})",
36+
'txid': "hexlify(m.{name})",
37+
'hash': "hexlify(m.{name})",
38+
'string': "m.{name}",
39+
'u8': "m.{name}",
40+
'u16': "m.{name}",
41+
'u32': "m.{name}",
42+
'u64': "m.{name}",
43+
'boolean': "m.{name}",
44+
'short_channel_id': "m.{name}",
45+
'msat': "amount2msat(m.{name})",
46+
'number': "m.{name}",
47+
}
48+
49+
def generate_responses(self, service):
50+
for meth in service.methods:
51+
res = meth.response
52+
self.generate_composite(None, res)
53+
54+
def write(self, text: str, cleanup: bool = True) -> None:
55+
if cleanup:
56+
self.dest.write(dedent(text))
57+
else:
58+
self.dest.write(text)
59+
60+
def generate(self, service: Service) -> None:
61+
self.write("""\
62+
# This file was automatically derived from the JSON-RPC schemas in
63+
# `doc/schemas`. Do not edit this file manually as it would get
64+
# overwritten.
65+
66+
import json
67+
68+
69+
def hexlify(b):
70+
return b if b is None else b.hex()
71+
72+
def amount2msat(a):
73+
return a.msat
74+
75+
def amount_or_all2msat(a):
76+
breakpoint()
77+
78+
79+
def remove_default(d):
80+
# grpc is really not good at empty values, they get replaced with the type's default value...
81+
return {k: v for k, v in d.items() if v is not None and v != ""}
82+
""")
83+
84+
self.generate_responses(service)
85+
86+
def generate_enum(self, prefix, field: EnumField):
87+
name = field.name.normalized()
88+
prefix = f"{prefix}_{str(name).lower()}"
89+
if field.path.endswith("[]"):
90+
self.converters[field.path] = "str(i)"
91+
else:
92+
self.converters[field.path] = "str(m.{{name}})"
93+
94+
def generate_composite(self, prefix, field: CompositeField):
95+
name = field.name.normalized()
96+
if prefix:
97+
prefix = f"{prefix}_{str(name).lower()}"
98+
else:
99+
prefix = f"{str(name).lower()}"
100+
101+
for f in field.fields:
102+
if isinstance(f, CompositeField):
103+
self.generate_composite(prefix, f)
104+
105+
elif isinstance(f, ArrayField) and isinstance(f.itemtype, CompositeField):
106+
self.generate_composite(prefix, f.itemtype)
107+
108+
elif isinstance(f, ArrayField) and isinstance(f.itemtype, EnumField):
109+
self.generate_enum(prefix, f.itemtype)
110+
111+
converter_name = f"{prefix}2py"
112+
self.write(f"""
113+
114+
def {converter_name}(m):
115+
return remove_default({{
116+
""")
117+
118+
for f in field.fields:
119+
name = f.normalized()
120+
if isinstance(f, PrimitiveField):
121+
typ = f.typename
122+
123+
rhs = self.converters[typ].format(name=f.name)
124+
125+
self.write(f' "{name}": {rhs}, # PrimitiveField in generate_composite\n', cleanup=False)
126+
127+
elif isinstance(f, ArrayField) and isinstance(f.itemtype, PrimitiveField):
128+
rhs = self.converters[f.itemtype.typename].format(name=name)
129+
self.write(f' "{name}": [{rhs} for i in {rhs}], # ArrayField[primitive] in generate_composite\n', cleanup=False)
130+
131+
elif isinstance(f, ArrayField):
132+
rhs = self.converters[f.path]
133+
134+
self.write(f' "{name}": [{rhs} for i in m.{name}], # ArrayField[composite] in generate_composite\n', cleanup=False)
135+
136+
elif isinstance(f, CompositeField):
137+
rhs = self.converters[f.path].format(name=f.name)
138+
# self.write(f' "{name}": {rhs}, # CompositeField in generate_composite\n', cleanup=False)
139+
140+
elif isinstance(f, EnumField):
141+
name = f.name
142+
self.write(f' "{name}": str(m.{f.name.normalized()}), # EnumField in generate_composite\n', cleanup=False)
143+
144+
self.write(f" }})\n", cleanup=False)
145+
146+
# Add ourselves to the converters so if we were generated as a
147+
# dependency for a composite they can find us again. We have
148+
# two variants: an array one where the items are going to be
149+
# called "i" so we don't clobber and one-of where the field is
150+
# "m.{name}" which will be filled by the caller.
151+
if field.path.endswith("[]"):
152+
self.converters[field.path] = f"{converter_name}(i)"
153+
else:
154+
self.converters[field.path] = f"{converter_name}(m.{{name}})"

‎contrib/pyln-testing/pyln/testing/grpc2py.py

+842
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.