diff --git a/contrib/msggen/msggen/__main__.py b/contrib/msggen/msggen/__main__.py index e423ac5683f6..e04b37c99c80 100644 --- a/contrib/msggen/msggen/__main__.py +++ b/contrib/msggen/msggen/__main__.py @@ -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) diff --git a/contrib/msggen/msggen/gen/__init__.py b/contrib/msggen/msggen/gen/__init__.py new file mode 100644 index 000000000000..107f713ffcb7 --- /dev/null +++ b/contrib/msggen/msggen/gen/__init__.py @@ -0,0 +1,3 @@ +from .generator import IGenerator, GeneratorChain # noqa +from .grpc import GrpcGenerator, GrpcConverterGenerator, GrpcUnconverterGenerator, GrpcServerGenerator # noqa +from .rust import RustGenerator # noqa diff --git a/contrib/msggen/msggen/gen/generator.py b/contrib/msggen/msggen/gen/generator.py new file mode 100644 index 000000000000..e4123ce19d81 --- /dev/null +++ b/contrib/msggen/msggen/gen/generator.py @@ -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) diff --git a/contrib/msggen/msggen/grpc.py b/contrib/msggen/msggen/gen/grpc.py similarity index 98% rename from contrib/msggen/msggen/grpc.py rename to contrib/msggen/msggen/gen/grpc.py index 56317f71e2ed..4d523a313b00 100644 --- a/contrib/msggen/msggen/grpc.py +++ b/contrib/msggen/msggen/gen/grpc.py @@ -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 @@ } -class GrpcGenerator: +class GrpcGenerator(IGenerator): """A generator that generates protobuf files. """ @@ -235,7 +236,7 @@ def generate(self, service: Service) -> None: self.generate_message(message) -class GrpcConverterGenerator: +class GrpcConverterGenerator(IGenerator): def __init__(self, dest: TextIO): self.dest = dest self.logger = logging.getLogger("msggen.grpc.GrpcConversionGenerator") diff --git a/contrib/msggen/msggen/rust.py b/contrib/msggen/msggen/gen/rust.py similarity index 97% rename from contrib/msggen/msggen/rust.py rename to contrib/msggen/msggen/gen/rust.py index 82d25cb9fb7b..2fbe3278fdf1 100644 --- a/contrib/msggen/msggen/rust.py +++ b/contrib/msggen/msggen/gen/rust.py @@ -5,8 +5,9 @@ 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 diff --git a/contrib/msggen/msggen/utils/__init__.py b/contrib/msggen/msggen/utils/__init__.py new file mode 100644 index 000000000000..eaa1c1aff90d --- /dev/null +++ b/contrib/msggen/msggen/utils/__init__.py @@ -0,0 +1 @@ +from .utils import load_jsonrpc_method, load_jsonrpc_service, repo_root # noqa diff --git a/contrib/msggen/msggen/utils/utils.py b/contrib/msggen/msggen/utils/utils.py new file mode 100644 index 000000000000..59586cd34a12 --- /dev/null +++ b/contrib/msggen/msggen/utils/utils.py @@ -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