|
| 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}})" |
0 commit comments