From 36948078504b47e86bd0d0c312f54e3172fadf71 Mon Sep 17 00:00:00 2001 From: "joe.miyamoto" Date: Mon, 18 Apr 2022 13:16:17 +0900 Subject: [PATCH 1/3] Add fsharp generators to msggen msggen now produces F# request/response from schema.json files in a similar way to rust. --- contrib/msggen/msggen/__main__.py | 15 ++ contrib/msggen/msggen/fsharp.py | 348 ++++++++++++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 contrib/msggen/msggen/fsharp.py diff --git a/contrib/msggen/msggen/__main__.py b/contrib/msggen/msggen/__main__.py index e423ac5683f6..30eb9477c104 100644 --- a/contrib/msggen/msggen/__main__.py +++ b/contrib/msggen/msggen/__main__.py @@ -1,6 +1,7 @@ from msggen.model import Method, CompositeField, Service from msggen.grpc import GrpcGenerator, GrpcConverterGenerator, GrpcUnconverterGenerator, GrpcServerGenerator from msggen.rust import RustGenerator +from msggen.fsharp import FSharpGenerator, FSharpClientExtensionGenerator from pathlib import Path import subprocess import json @@ -150,6 +151,18 @@ def genrustjsonrpc(service): RustGenerator(dest).generate(service) +def genfsharpjsonrpc(service): + request_file_name = repo_root() / "contrib" / "ClnSharp" / "Requests.fs" + dest1 = open(request_file_name, "w") + FSharpGenerator(dest1).generate(service) + + +def genfsharpclient(service): + client_file_name = repo_root() / "contrib" / "ClnSharp" / "Client.Methods.fs" + dest1 = open(client_file_name, "w") + FSharpClientExtensionGenerator(dest1).generate(service) + + def load_msggen_meta(): meta = json.load(open('.msggen.json', 'r')) return meta @@ -165,6 +178,8 @@ def run(): meta = load_msggen_meta() gengrpc(service, meta) genrustjsonrpc(service) + genfsharpjsonrpc(service) + genfsharpclient(service) write_msggen_meta(meta) diff --git a/contrib/msggen/msggen/fsharp.py b/contrib/msggen/msggen/fsharp.py new file mode 100644 index 000000000000..cb024196b623 --- /dev/null +++ b/contrib/msggen/msggen/fsharp.py @@ -0,0 +1,348 @@ +import re +import sys +from typing import TextIO, Tuple +from textwrap import dedent, indent +import logging +import importlib.metadata +from .model import (ArrayField, CompositeField, EnumField, + PrimitiveField, Service) + +logger = logging.getLogger(__name__) + +typemap = { + 'boolean': 'bool', + 'hex': 'string', + 'msat': 'LNMoney', + 'msat_or_all': 'AmountOrAll', + 'msat_or_any': 'AmountOrAny', + 'number': 'int64', + 'pubkey': 'PubKey', + 'short_channel_id': 'ShortChannelId', + 'signature': 'string', + 'string': 'string', + 'txid': 'string', + 'float': 'float', + 'feerate': 'Feerate', + 'outpoint': 'OutPoint', + 'outputdesc': 'OutputDescriptor', + 'hash': 'Hash', + 'secret': 'PrivKey', + + 'u8': 'byte', + 'u16': 'uint16', + 'u32': 'uint32', + 'u64': 'uint64', + 'i8': 'byte', + 'i16': 'int16', + 'i32': 'int32', + 'i64': 'int64', +} + +converter_map = { + 'int64': 'MSatJsonConverter', + 'PubKey': 'PubKeyJsonConverter', + 'ShortChannelId': 'ShortChannelIdJsonConverter', + 'PrivKey': 'PrivKeyJsonConverter', + 'Hash': 'HashJsonConverter', + 'AmountOrAny': 'AmountOrAnyJsonConverter', + 'AmountOrAll': 'AmountOrAllJsonConverter', + 'OutPoint': 'OutPointJsonConverter', + 'Feerate': 'FeerateJsonConverter', + 'OutputDescriptor': 'OutputDescriptorJsonConverter', +} + +# Manual overrides for some of the auto-generated types for paths +overrides = { + 'ListPeers.peers[].channels[].state_changes[].old_state': "ChannelState", + 'ListPeers.peers[].channels[].state_changes[].new_state': "ChannelState", + 'ListPeers.peers[].channels[].state_changes[].cause': "ChannelStateChangeCause", + 'ListPeers.peers[].channels[].htlcs[].state': None, + 'ListPeers.peers[].channels[].opener': "ChannelSide", + 'ListPeers.peers[].channels[].closer': "ChannelSide", + 'ListPeers.peers[].channels[].features[]': "string", + 'ListFunds.channels[].state': 'ChannelState', + 'ListTransactions.transactions[].type[]': None, + 'Invoice.exposeprivatechannels': None, +} + +version = importlib.metadata.version("msggen") + +header = f""" +/// This file was automatically generated using following command: +/// ```bash +/// {' '.join(sys.argv)} +/// ``` +/// +/// Do not edit this file, it'll be overwritten. Rather edit the schema that +/// This file was generated from +namespace ClnSharp + +""" + + +def to_PascalCase(s): + return s.replace("_", " ").title().replace(" ", "") + + +def normalize_varname(field): + """Make sure that the variable name of this field is valid + """ + field.path = field.path.replace("-", "_") + field.path = re.sub(r'(?)>]\n' + if p.required: + defi += f' []\n {to_PascalCase(p.name.normalized())}: {typename}\n' + else: + defi += f' []\n []\n {to_PascalCase(p.name.normalized())}: {typename} option\n' + return defi, decl + + +def gen_enum(e): + defi, decl = "", "" + if e.path in overrides and overrides[e.path] is None: + return "", "" + if e.description != "": + decl += f"/// {e.description}\n" + + decl += f"type {e.typename} =\n" + + for i, v in enumerate(e.variants): + if v is None: + continue + name = v.normalized() + decl += f' | [] {name} = {i} \n' + + decl += "\n\n" + + typename = e.typename + if e.path in overrides: + decl = "" + typename = overrides[e.path] + + if e.required: + defi = f' // Path `{e.path}`\n' + defi += f' []\n' + defi += f' [)>]\n' + defi += f' {to_PascalCase(e.name.normalized())}: {typename}\n' + else: + defi = f' []\n' + defi += f' []\n' + defi += f' {to_PascalCase(e.name.normalized())}: {typename} option\n' + return defi, decl + + +def gen_array(a: ArrayField): + name = a.name.normalized().replace("[]", "") + logger.debug(f"Generating array field {a.name} -> {name} ({a.path})") + _, decl = gen_field(a.itemtype) + + if a.path in overrides: + decl = "" + itemtype = overrides[a.path] + elif isinstance(a.itemtype, PrimitiveField): + itemtype = a.itemtype.typename + elif isinstance(a.itemtype, CompositeField): + itemtype = a.itemtype.typename + elif isinstance(a.itemtype, EnumField): + itemtype = a.itemtype.typename + + if itemtype is None: + return "", "" + + itemtype = typemap.get(itemtype, itemtype) + alias = a.name.normalized() + if a.required: + defi = f' []\n {to_PascalCase(name)}: {itemtype}{" []" * a.dims}\n' + else: + defi = f' []\n' + defi += f" []\n" + defi += f" {to_PascalCase(name)}: {itemtype}{' []' * a.dims} option\n" + return defi, decl + + +def gen_field(field): + if isinstance(field, CompositeField): + return gen_composite(field) + elif isinstance(field, EnumField): + return gen_enum(field) + elif isinstance(field, ArrayField): + return gen_array(field) + elif isinstance(field, PrimitiveField): + return gen_primitive(field) + else: + raise ValueError(f"Unmanaged type {field}") + + +def gen_composite(c) -> Tuple[str, str]: + logger.debug(f"Generating composite field {c.name} ({c.path})") + fields = [] + for f in c.fields: + fields.append(gen_field(f)) + r = "".join([f[1] for f in fields]) + if len(fields) == 0: + r += f'type {c.typename} = unit\n' + else: + r += f'[]\n' + r += '[]\n' + r += f"type {c.typename} =" + r += " {\n" + r += "".join([f[0] for f in fields]) + r += "}\n" + r += "\n" + return ("", r) + + +class FSharpGenerator: + def __init__(self, dest: TextIO): + self.dest = dest + + def write(self, text: str, numindent: int = 0) -> None: + raw = dedent(text) + if numindent > 0: + raw = indent(text, " " * numindent) + self.dest.write(raw) + + def generate_requests(self, service: Service): + """""" + + self.write(f'[]\n') + self.write("module Requests =\n") + + for method in service.methods: + _, decl = gen_composite(method.request) + self.write(decl, numindent=1) + + self.write("\n\n") + + def generate_responses(self, service: Service): + self.write(f'[]\n') + self.write("module Responses = \n") + for method in service.methods: + _, decl = gen_composite(method.response) + self.write(decl, numindent=1) + + self.write("\n\n") + + def generate_enums(self, service: Service): + """The reqeust and Response enums serve as parsing primitives. + """ + self.write(f""" + [] + type internal Request = + """) + + for method in service.methods: + self.write(f"| {method.name} of Requests.{method.request.typename}\n", numindent=1) + self.write(f"with\n", numindent=2) + self.write(f"member this.MethodName =\n", numindent=2) + self.write(f"match this with\n", numindent=3) + for method in service.methods: + self.write(f"| {method.name} _ -> \"{method.name.lower()}\"\n", numindent=3) + + self.write(f"member this.Data =\n", numindent=2) + self.write(f"match this with\n", numindent=3) + for method in service.methods: + self.write(f"| {method.name} x -> x |> box\n", numindent=3) + + self.write("\n") + + self.write(f""" + [] + type private Response = + """) + + for method in service.methods: + self.write(f"| {method.name} of Responses.{method.response.typename}\n", numindent=1) + self.write("\n") + + def write_header(self): + self.write(header) + opens = """ +open System.Text.Json +open System.Text.Json.Serialization + +open NBitcoin +open NBitcoin.Scripting +open DotNetLightning.Utils + +""" + self.write(opens) + + def generate(self, service: Service): + self.write_header() + self.generate_requests(service) + self.generate_responses(service) + self.generate_enums(service) + + +class FSharpClientExtensionGenerator: + def __init__(self, dest: TextIO): + self.dest = dest + + def write(self, text: str, numindent: int = 0) -> None: + raw = dedent(text) + if numindent > 0: + raw = indent(text, " " * numindent) + self.dest.write(raw) + + def generate_methods(self, service: Service): + self.write(f""" + +[] +[] +type ClnClientExtensions = +""") + + for method in service.methods: + req_fields = [] + resp_fields = [] + for f in method.request.fields: + req_fields.append(gen_field(f)) + for f in method.response.fields: + resp_fields.append(gen_field(f)) + self.write("[]\n", numindent=1) + if len(req_fields) == 0: + self.write( + f"static member {method.name}Async(this: ClnClient, [] ct: CancellationToken) =\n", + numindent=1) + if len(resp_fields) == 0: + self.write(f"this.SendCommandAsync(\"{method.name.lower()}\", null, ct=ct) :> Task\n", numindent=2) + else: + self.write( + f"this.SendCommandAsync(\"{method.name.lower()}\", null, ct=ct)\n", + numindent=2) + else: + self.write( + f"static member {method.name}Async(this: ClnClient, req: Requests.{method.request.typename}, [] ct: CancellationToken) =\n", + numindent=1) + if len(resp_fields) == 0: + self.write(f"this.SendCommandAsync(Request.{method.name} req, ct=ct) :> Task\n", numindent=2) + else: + self.write( + f"this.SendCommandAsync(Request.{method.name} req, ct=ct)\n", + numindent=2) + + def write_header(self): + self.write(header) + opens = """ +open System.Runtime.InteropServices +open System.Runtime.CompilerServices +open System.Threading +open System.Threading.Tasks + +""" + self.write(opens) + + def generate(self, service: Service): + self.write_header() + self.generate_methods(service) From 1ad422de6e8af8e9e21a19850c6e9bc614c0ca52 Mon Sep 17 00:00:00 2001 From: "joe.miyamoto" Date: Thu, 21 Apr 2022 10:24:38 +0900 Subject: [PATCH 2/3] stop using `version` in fsharp generators. `importlib.metadata` is only usable in python 3.8+ And using backport described in https://github.com/python-poetry/poetry/issues/273#issuecomment-1103812336 will cause following error in CI. > "importlib_metadata.PackageNotFoundError: No package metadata was found for msggen" So lets just keep it simple by not having version information in genrated code. --- contrib/msggen/msggen/fsharp.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/contrib/msggen/msggen/fsharp.py b/contrib/msggen/msggen/fsharp.py index cb024196b623..367a001b0641 100644 --- a/contrib/msggen/msggen/fsharp.py +++ b/contrib/msggen/msggen/fsharp.py @@ -3,7 +3,6 @@ from typing import TextIO, Tuple from textwrap import dedent, indent import logging -import importlib.metadata from .model import (ArrayField, CompositeField, EnumField, PrimitiveField, Service) @@ -12,7 +11,7 @@ typemap = { 'boolean': 'bool', 'hex': 'string', - 'msat': 'LNMoney', + 'msat': 'int64', 'msat_or_all': 'AmountOrAll', 'msat_or_any': 'AmountOrAny', 'number': 'int64', @@ -65,8 +64,6 @@ 'Invoice.exposeprivatechannels': None, } -version = importlib.metadata.version("msggen") - header = f""" /// This file was automatically generated using following command: /// ```bash @@ -114,6 +111,7 @@ def gen_enum(e): if e.description != "": decl += f"/// {e.description}\n" + decl += f'[]\n' decl += f"type {e.typename} =\n" for i, v in enumerate(e.variants): @@ -192,7 +190,7 @@ def gen_composite(c) -> Tuple[str, str]: if len(fields) == 0: r += f'type {c.typename} = unit\n' else: - r += f'[]\n' + r += f'[]\n' r += '[]\n' r += f"type {c.typename} =" r += " {\n" @@ -215,7 +213,7 @@ def write(self, text: str, numindent: int = 0) -> None: def generate_requests(self, service: Service): """""" - self.write(f'[]\n') + self.write(f'[]\n') self.write("module Requests =\n") for method in service.methods: @@ -225,7 +223,7 @@ def generate_requests(self, service: Service): self.write("\n\n") def generate_responses(self, service: Service): - self.write(f'[]\n') + self.write(f'[]\n') self.write("module Responses = \n") for method in service.methods: _, decl = gen_composite(method.response) @@ -237,7 +235,7 @@ def generate_enums(self, service: Service): """The reqeust and Response enums serve as parsing primitives. """ self.write(f""" - [] + [] type internal Request = """) @@ -257,7 +255,7 @@ def generate_enums(self, service: Service): self.write("\n") self.write(f""" - [] + [] type private Response = """) @@ -271,10 +269,6 @@ def write_header(self): open System.Text.Json open System.Text.Json.Serialization -open NBitcoin -open NBitcoin.Scripting -open DotNetLightning.Utils - """ self.write(opens) @@ -298,7 +292,7 @@ def write(self, text: str, numindent: int = 0) -> None: def generate_methods(self, service: Service): self.write(f""" -[] +[] [] type ClnClientExtensions = """) From 6376342438a69a87991f12100b4ca89782f86ed4 Mon Sep 17 00:00:00 2001 From: "joe.miyamoto" Date: Sat, 23 Apr 2022 09:15:28 +0900 Subject: [PATCH 3/3] adjust F# client generation into reusable style According to the conversation in https://github.com/ElementsProject/lightning/pull/5201, generated code for 3rd-party generater should not reside in this repository. Instead, only generators under `msggen` should be included. This commit adjusts F# generator to follow the following rule of thumb. 1. no native F# code, only generators, so every primitives are now reside in `msggen/fsharp.py` with plain text. 2. no object equivalent to `RPCClient`, only DTOs must be generated. Implementing client is now the consumers duty. --- contrib/msggen/msggen/__main__.py | 11 +- contrib/msggen/msggen/fsharp.py | 346 ++++++++++++++++++++++++------ 2 files changed, 286 insertions(+), 71 deletions(-) diff --git a/contrib/msggen/msggen/__main__.py b/contrib/msggen/msggen/__main__.py index 30eb9477c104..4080c4481f1c 100644 --- a/contrib/msggen/msggen/__main__.py +++ b/contrib/msggen/msggen/__main__.py @@ -1,7 +1,7 @@ from msggen.model import Method, CompositeField, Service from msggen.grpc import GrpcGenerator, GrpcConverterGenerator, GrpcUnconverterGenerator, GrpcServerGenerator from msggen.rust import RustGenerator -from msggen.fsharp import FSharpGenerator, FSharpClientExtensionGenerator +from msggen.fsharp import FSharpGenerator from pathlib import Path import subprocess import json @@ -152,17 +152,11 @@ def genrustjsonrpc(service): def genfsharpjsonrpc(service): - request_file_name = repo_root() / "contrib" / "ClnSharp" / "Requests.fs" + request_file_name = repo_root() / "contrib" / "Model.fs" dest1 = open(request_file_name, "w") FSharpGenerator(dest1).generate(service) -def genfsharpclient(service): - client_file_name = repo_root() / "contrib" / "ClnSharp" / "Client.Methods.fs" - dest1 = open(client_file_name, "w") - FSharpClientExtensionGenerator(dest1).generate(service) - - def load_msggen_meta(): meta = json.load(open('.msggen.json', 'r')) return meta @@ -179,7 +173,6 @@ def run(): gengrpc(service, meta) genrustjsonrpc(service) genfsharpjsonrpc(service) - genfsharpclient(service) write_msggen_meta(meta) diff --git a/contrib/msggen/msggen/fsharp.py b/contrib/msggen/msggen/fsharp.py index 367a001b0641..afd361f79449 100644 --- a/contrib/msggen/msggen/fsharp.py +++ b/contrib/msggen/msggen/fsharp.py @@ -236,7 +236,7 @@ def generate_enums(self, service: Service): """ self.write(f""" [] - type internal Request = + type Request = """) for method in service.methods: @@ -256,7 +256,7 @@ def generate_enums(self, service: Service): self.write(f""" [] - type private Response = + type Response = """) for method in service.methods: @@ -269,74 +269,296 @@ def write_header(self): open System.Text.Json open System.Text.Json.Serialization +""" + primitives = """ +open System + +type ChannelState = + | OPENINGD = 0 + | CHANNELD_AWAITING_LOCKIN = 1 + | CHANNELD_NORMAL = 2 + | CHANNELD_SHUTTING_DOWN = 3 + | CLOSINGD_SIGEXCHANGE = 4 + | CLOSINGD_COMPLETE = 5 + | AWAITING_UNILATERAL = 6 + | FUNDING_SPEND_SEEN = 7 + | ONCHAIN = 8 + | DUALOPEND_OPEN_INIT = 9 + | DUALOPEND_AWAITING_LOCKIN = 10 + +type ChannelStateChangeCause = + | [] UNKNOWN = 0 + | [] LOCAL = 1 + | [] USER = 2 + | [] REMOTE = 3 + | [] PROTOCOL = 4 + | [] ONCHAIN = 5 + +type [] msat + +type AmountOrAny = + | Amount of int64 + | Any + +type AmountOrAll = + | Amount of int64 + | All + +type Feerate = + | Slow + | Normal + | Urgent + | PerKb of uint32 + | PerKw of uint32 + + with + override this.ToString() = + match this with + | Slow -> "slow" + | Normal -> "normal" + | Urgent -> "urgent" + | PerKb v -> $"{v} perkb" + | PerKw v -> $"{v} perkw" + +type ChannelSide = + | [] LOCAL = 0 + | [] REMOTE = 1 + +[] +type ShortChannelId = ShortChannelId of string + with + member this.Value = let (ShortChannelId v) = this in v + override this.ToString() = this.Value + +[] +type PrivKey = PrivKey of byte[] + with + member this.Value = let (PrivKey v) = this in v + override this.ToString() = this.Value |> Convert.ToHexString + +[] +type PubKey = PubKey of byte[] + with + member this.Value = let (PubKey v) = this in v + override this.ToString() = this.Value |> Convert.ToHexString + +[] +type OutputDescriptor = OutputDescriptor of string + with + member this.Value = let (OutputDescriptor v) = this in v + override this.ToString() = this.Value + +[] +type Hash = Hash of byte[] + with + member this.Value = let (Hash v) = this in v + override this.ToString() = this.Value |> Convert.ToHexString + +[] +type OutPoint = { + VOut: uint32 + PrevHash: byte[] +} + with + override this.ToString() = $"{this.PrevHash |> Convert.ToHexString}:{this.VOut}" """ self.write(opens) + self.write(primitives) - def generate(self, service: Service): - self.write_header() - self.generate_requests(service) - self.generate_responses(service) - self.generate_enums(service) - - -class FSharpClientExtensionGenerator: - def __init__(self, dest: TextIO): - self.dest = dest - - def write(self, text: str, numindent: int = 0) -> None: - raw = dedent(text) - if numindent > 0: - raw = indent(text, " " * numindent) - self.dest.write(raw) - - def generate_methods(self, service: Service): - self.write(f""" + json_converters = """ +open System.Runtime.CompilerServices -[] -[] -type ClnClientExtensions = -""") +[] +module private PrimitiveExtensions = + let parseClnAmount(s: string): int64 = + if s |> String.IsNullOrWhiteSpace then + raise <| FormatException($"Invalid string for money. null") + else if s.EndsWith("msat") then + s.Substring(0, s.Length - 4) |> int64 |> unbox + else if s.EndsWith("sat") then + s.Substring(0, s.Length - 3) |> int64 |> unbox + else if s.EndsWith("btc") then + s.Substring(0, s.Length - 3) |> int64 |> unbox + else + raise <| FormatException $"Invalid string for money {s}" + + +type MSatJsonConverter() = + inherit JsonConverter>() + override this.Write(writer, value, options) = + writer.WriteStringValue(value.ToString() + "msat") + override this.Read(reader, _typeToConvert, _options) = + reader.GetString() + |> parseClnAmount + +type PubKeyJsonConverter() = + inherit JsonConverter() + + override this.Write(writer, value, _options) = + value.ToString() + |> writer.WriteStringValue + override this.Read(reader, _typeToConvert, _options) = + let b = reader.GetString() |> Convert.FromHexString + if b.Length <> 33 then + raise <| JsonException($"Invalid length for pubkey: {b.Length}, it must be 33") + else + b |> PubKey + +type ShortChannelIdJsonConverter() = + inherit JsonConverter() + override this.Write(writer, value, _options) = + value.ToString() + |> writer.WriteStringValue + override this.Read(reader, _typeToConvert, _options) = + reader.GetString() |> ShortChannelId + +type PrivKeyJsonConverter() = + inherit JsonConverter() + + override this.Write(writer, value, _options) = + value.ToString() + |> writer.WriteStringValue + + override this.Read(reader, _typeToConvert, _options) = + let b = reader.GetString() |> Convert.FromHexString + PrivKey b + +type HashJsonConverter() = + inherit JsonConverter() + override this.Write(writer, value, _options) = + value.ToString() + |> writer.WriteStringValue + + override this.Read(reader, _typeToConvert, _options) = + reader.GetString() + |> Convert.FromHexString + |> Hash + +type AmountOrAnyJsonConverter() = + inherit JsonConverter() + + override this.Write(writer, value, options) = + match value with + | AmountOrAny.Any -> + writer.WriteStringValue "any" + | AmountOrAny.Amount a -> + writer.WriteStringValue(a.ToString() + "msat") + override this.Read(reader, _typeToConvert, _options) = + match reader.GetString() with + | "any" -> + AmountOrAny.Any + | x -> + parseClnAmount x + |> AmountOrAny.Amount + +type AmountOrAllJsonConverter() = + inherit JsonConverter() + + override this.Write(writer, value, options) = + match value with + | AmountOrAll.All -> + writer.WriteStringValue "all" + | AmountOrAll.Amount a -> + writer.WriteStringValue(a.ToString() + "msat") + override this.Read(reader, _typeToConvert, _options) = + match reader.GetString() with + | "all" -> + AmountOrAll.All + | x -> + parseClnAmount x + |> AmountOrAll.Amount + +type OutPointJsonConverter() = + inherit JsonConverter() + + override this.Write(writer, value, options) = + writer.WriteStringValue(value.ToString()) + + override this.Read(reader, _typeToConvert, _options) = + let splits = reader.GetString().Split ":" + if splits.Length <> 2 then + raise <| JsonException("not a valid txid:output tuple") + else + { + OutPoint.PrevHash = splits.[0] |> Convert.FromHexString + VOut = splits.[1] |> uint32 + } + +type FeerateJsonConverter() = + inherit JsonConverter() + + override this.Write(writer, value, options) = + value.ToString() |> writer.WriteStringValue + + override this.Read(reader, _typeToConvert, _options) = + let s = reader.GetString() + let number = + s + |> Seq.choose(fun c -> match UInt32.TryParse($"{c}") with | true, v -> Some v | _ -> None) + |> Seq.fold(fun acc d -> acc * 10u + d) 0u + + let s = s.ToLowerInvariant() + if s.EndsWith "perkw" then + Feerate.PerKw(number) + else if s.EndsWith "perkb" then + Feerate.PerKb number + else if s = "slow" then + Feerate.Slow + else if s = "normal" then + Feerate.Normal + else if s = "urgent" then + Feerate.Urgent + else + raise <| JsonException $"Unable to parse feerate from string {s}" + +type OutputDescriptorJsonConverter() = + inherit JsonConverter() + + override this.Write(writer, value, options) = + value.ToString() |> writer.WriteStringValue + + override this.Read(reader, _typeToConvert, _options) = + reader.GetString() + |> OutputDescriptor + +[] +type RouteHop = { + [] + [)>] + Id: PubKey + + [] + [)>] + Scid: ShortChannelId + + [] + [)>] + Feebase: int64 + + [] + Feeprop: uint32 + + [] + Expirydelta: uint16 +} - for method in service.methods: - req_fields = [] - resp_fields = [] - for f in method.request.fields: - req_fields.append(gen_field(f)) - for f in method.response.fields: - resp_fields.append(gen_field(f)) - self.write("[]\n", numindent=1) - if len(req_fields) == 0: - self.write( - f"static member {method.name}Async(this: ClnClient, [] ct: CancellationToken) =\n", - numindent=1) - if len(resp_fields) == 0: - self.write(f"this.SendCommandAsync(\"{method.name.lower()}\", null, ct=ct) :> Task\n", numindent=2) - else: - self.write( - f"this.SendCommandAsync(\"{method.name.lower()}\", null, ct=ct)\n", - numindent=2) - else: - self.write( - f"static member {method.name}Async(this: ClnClient, req: Requests.{method.request.typename}, [] ct: CancellationToken) =\n", - numindent=1) - if len(resp_fields) == 0: - self.write(f"this.SendCommandAsync(Request.{method.name} req, ct=ct) :> Task\n", numindent=2) - else: - self.write( - f"this.SendCommandAsync(Request.{method.name} req, ct=ct)\n", - numindent=2) +[] +type Routehint = { + [] + Hops: RouteHop[] +} - def write_header(self): - self.write(header) - opens = """ -open System.Runtime.InteropServices -open System.Runtime.CompilerServices -open System.Threading -open System.Threading.Tasks +[] +type RoutehintList = { + [] + Hints: Routehint [] +} """ - self.write(opens) + self.write(json_converters) def generate(self, service: Service): self.write_header() - self.generate_methods(service) + self.generate_requests(service) + self.generate_responses(service) + self.generate_enums(service)