msggen: introduce chain of responsibility pattern to make msggen extensible

Changelog-Added: msggen: introduce chain of responsibility pattern to make msggen extensible

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
This commit is contained in:
Vincenzo Palazzo 2022-04-24 14:34:33 +02:00 committed by Christian Decker
parent 80db867a30
commit 4e902fbd88
7 changed files with 195 additions and 141 deletions

View File

@ -1,153 +1,31 @@
from msggen.model import Method, CompositeField, Service
from msggen.grpc import GrpcGenerator, GrpcConverterGenerator, GrpcUnconverterGenerator, GrpcServerGenerator
from msggen.rust import RustGenerator
from pathlib import Path
import subprocess
import json
from msggen.gen.grpc import GrpcGenerator, GrpcConverterGenerator, GrpcUnconverterGenerator, GrpcServerGenerator
from msggen.gen.rust import RustGenerator
from msggen.gen.generator import GeneratorChain
from msggen.utils import repo_root, load_jsonrpc_service
# Sometimes we want to rename a method, due to a name clash
method_name_override = {
"Connect": "ConnectPeer",
}
def repo_root():
path = subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
return Path(path.strip().decode('UTF-8'))
def load_jsonrpc_method(name):
"""Load a method based on the file naming conventions for the JSON-RPC.
"""
base_path = (repo_root() / "doc" / "schemas").resolve()
req_file = base_path / f"{name.lower()}.request.json"
resp_file = base_path / f"{name.lower()}.schema.json"
request = CompositeField.from_js(json.load(open(req_file)), path=name)
response = CompositeField.from_js(json.load(open(resp_file)), path=name)
# Normalize the method request and response typename so they no
# longer conflict.
request.typename += "Request"
response.typename += "Response"
return Method(
name=method_name_override.get(name, name),
request=request,
response=response,
)
def load_jsonrpc_service():
method_names = [
"Getinfo",
"ListPeers",
"ListFunds",
"SendPay",
"ListChannels",
"AddGossip",
"AutoCleanInvoice",
"CheckMessage",
"Close",
"Connect",
"CreateInvoice",
"Datastore",
"CreateOnion",
"DelDatastore",
"DelExpiredInvoice",
"DelInvoice",
"Invoice",
"ListDatastore",
"ListInvoices",
"SendOnion",
"ListSendPays",
"ListTransactions",
"Pay",
"ListNodes",
"WaitAnyInvoice",
"WaitInvoice",
"WaitSendPay",
"NewAddr",
"Withdraw",
"KeySend",
"FundPsbt",
"SendPsbt",
"SignPsbt",
"UtxoPsbt",
"TxDiscard",
"TxPrepare",
"TxSend",
# "decodepay",
# "decode",
# "delpay",
# "disableoffer",
"Disconnect",
"Feerates",
# "fetchinvoice",
# "fundchannel_cancel",
# "fundchannel_complete",
# "fundchannel",
# "fundchannel_start",
# "funderupdate",
# "getlog",
"GetRoute",
# "getsharedsecret",
"ListForwards",
# "listoffers",
"ListPays",
# "multifundchannel",
# "multiwithdraw",
# "offerout",
# "offer",
# "openchannel_abort",
# "openchannel_bump",
# "openchannel_init",
# "openchannel_signed",
# "openchannel_update",
# "parsefeerate",
"Ping",
# "plugin",
# "reserveinputs",
# "sendcustommsg",
# "sendinvoice",
# "sendonionmessage",
# "setchannelfee",
"SignMessage",
# "unreserveinputs",
# "waitblockheight",
# "ListConfigs",
# "check", # No point in mapping this one
# "Stop", # Breaks a core assumption (root is an object) can't map unless we change this
# "notifications", # No point in mapping this
# "help",
]
methods = [load_jsonrpc_method(name) for name in method_names]
service = Service(name="Node", methods=methods)
service.includes = ['primitives.proto'] # Make sure we have the primitives included.
return service
def gengrpc(service, meta):
def gengrpc(generator_chain: GeneratorChain, meta):
"""Load all mapped RPC methods, wrap them in a Service, and split them into messages.
"""
fname = repo_root() / "cln-grpc" / "proto" / "node.proto"
dest = open(fname, "w")
GrpcGenerator(dest, meta).generate(service)
generator_chain.add_generator(GrpcGenerator(dest, meta))
fname = repo_root() / "cln-grpc" / "src" / "convert.rs"
dest = open(fname, "w")
GrpcConverterGenerator(dest).generate(service)
GrpcUnconverterGenerator(dest).generate(service)
generator_chain.add_generator(GrpcConverterGenerator(dest))
generator_chain.add_generator(GrpcUnconverterGenerator(dest))
fname = repo_root() / "cln-grpc" / "src" / "server.rs"
dest = open(fname, "w")
GrpcServerGenerator(dest).generate(service)
generator_chain.add_generator(GrpcServerGenerator(dest))
def genrustjsonrpc(service):
def genrustjsonrpc(generator_chain: GeneratorChain):
fname = repo_root() / "cln-rpc" / "src" / "model.rs"
dest = open(fname, "w")
RustGenerator(dest).generate(service)
generator_chain.add_generator(RustGenerator(dest))
def load_msggen_meta():
@ -163,8 +41,13 @@ def write_msggen_meta(meta):
def run():
service = load_jsonrpc_service()
meta = load_msggen_meta()
gengrpc(service, meta)
genrustjsonrpc(service)
generator_chain = GeneratorChain()
gengrpc(generator_chain, meta)
genrustjsonrpc(generator_chain)
generator_chain.generate(service)
write_msggen_meta(meta)

View File

@ -0,0 +1,3 @@
from .generator import IGenerator, GeneratorChain # noqa
from .grpc import GrpcGenerator, GrpcConverterGenerator, GrpcUnconverterGenerator, GrpcServerGenerator # noqa
from .rust import RustGenerator # noqa

View File

@ -0,0 +1,36 @@
"""
Generator interface!
author: https://github.com/vincenzopalazzo
"""
from abc import ABC, abstractmethod
from msggen.model import Service
class IGenerator(ABC):
"""
Change of responsibility handler that need to be
implemented by all the generators.
"""
@abstractmethod
def generate(self, service: Service):
pass
class GeneratorChain:
"""
Chain responsibility patter implementation to generalize
the generation method.
"""
def __init__(self):
self.generators = []
def add_generator(self, generator: IGenerator) -> None:
self.generators.append(generator)
def generate(self, service: Service) -> None:
for _, generator in enumerate(self.generators):
generator.generate(service)

View File

@ -1,5 +1,6 @@
# A grpc model
from .model import ArrayField, Field, CompositeField, EnumField, PrimitiveField, Service
from msggen.model import ArrayField, Field, CompositeField, EnumField, PrimitiveField, Service
from msggen.gen import IGenerator
from typing import TextIO, List, Dict, Any
from textwrap import indent, dedent
import re
@ -51,7 +52,7 @@ method_name_overrides = {
}
class GrpcGenerator:
class GrpcGenerator(IGenerator):
"""A generator that generates protobuf files.
"""
@ -235,7 +236,7 @@ class GrpcGenerator:
self.generate_message(message)
class GrpcConverterGenerator:
class GrpcConverterGenerator(IGenerator):
def __init__(self, dest: TextIO):
self.dest = dest
self.logger = logging.getLogger("msggen.grpc.GrpcConversionGenerator")

View File

@ -5,8 +5,9 @@ import logging
import sys
import re
from .model import (ArrayField, CompositeField, EnumField,
PrimitiveField, Service)
from msggen.model import (ArrayField, CompositeField, EnumField,
PrimitiveField, Service)
from msggen.gen.generator import IGenerator
logger = logging.getLogger(__name__)
@ -196,7 +197,7 @@ def gen_composite(c) -> Tuple[str, str]:
return ("", r)
class RustGenerator:
class RustGenerator(IGenerator):
def __init__(self, dest: TextIO):
self.dest = dest

View File

@ -0,0 +1 @@
from .utils import load_jsonrpc_method, load_jsonrpc_service, repo_root # noqa

View File

@ -0,0 +1,129 @@
import subprocess
import json
from pathlib import Path
from msggen.model import Method, CompositeField, Service
# Sometimes we want to rename a method, due to a name clash
# FIXME: need to be generalized?
method_name_override = {
"Connect": "ConnectPeer",
}
def repo_root():
path = subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
return Path(path.strip().decode('UTF-8'))
def load_jsonrpc_method(name, schema_dir: str = None):
"""Load a method based on the file naming conventions for the JSON-RPC.
"""
if schema_dir is None:
base_path = (repo_root() / "doc" / "schemas").resolve()
else:
base_path = schema_dir
req_file = base_path / f"{name.lower()}.request.json"
resp_file = base_path / f"{name.lower()}.schema.json"
request = CompositeField.from_js(json.load(open(req_file)), path=name)
response = CompositeField.from_js(json.load(open(resp_file)), path=name)
# Normalize the method request and response typename so they no
# longer conflict.
request.typename += "Request"
response.typename += "Response"
return Method(
name=method_name_override.get(name, name),
request=request,
response=response,
)
def load_jsonrpc_service(schema_dir: str = None):
method_names = [
"Getinfo",
"ListPeers",
"ListFunds",
"SendPay",
"ListChannels",
"AddGossip",
"AutoCleanInvoice",
"CheckMessage",
"Close",
"Connect",
"CreateInvoice",
"Datastore",
"CreateOnion",
"DelDatastore",
"DelExpiredInvoice",
"DelInvoice",
"Invoice",
"ListDatastore",
"ListInvoices",
"SendOnion",
"ListSendPays",
"ListTransactions",
"Pay",
"ListNodes",
"WaitAnyInvoice",
"WaitInvoice",
"WaitSendPay",
"NewAddr",
"Withdraw",
"KeySend",
"FundPsbt",
"SendPsbt",
"SignPsbt",
"UtxoPsbt",
"TxDiscard",
"TxPrepare",
"TxSend",
# "decodepay",
# "decode",
# "delpay",
# "disableoffer",
"Disconnect",
"Feerates",
# "fetchinvoice",
# "fundchannel_cancel",
# "fundchannel_complete",
# "fundchannel",
# "fundchannel_start",
# "funderupdate",
# "getlog",
"GetRoute",
# "getsharedsecret",
"ListForwards",
# "listoffers",
"ListPays",
# "multifundchannel",
# "multiwithdraw",
# "offerout",
# "offer",
# "openchannel_abort",
# "openchannel_bump",
# "openchannel_init",
# "openchannel_signed",
# "openchannel_update",
# "parsefeerate",
"Ping",
# "plugin",
# "reserveinputs",
# "sendcustommsg",
# "sendinvoice",
# "sendonionmessage",
# "setchannelfee",
"SignMessage",
# "unreserveinputs",
# "waitblockheight",
# "ListConfigs",
# "check", # No point in mapping this one
# "Stop", # Breaks a core assumption (root is an object) can't map unless we change this
# "notifications", # No point in mapping this
# "help",
]
methods = [load_jsonrpc_method(name, schema_dir=schema_dir) for name in method_names]
service = Service(name="Node", methods=methods)
service.includes = ['primitives.proto'] # Make sure we have the primitives included.
return service