Skip to content

Commit

Permalink
cln: add JsonConverter for handling enum
Browse files Browse the repository at this point in the history
Unlike Newtonsoft.Json, System.Text.Json does not
respect EnumMemberAttribute. (see: dotnet/runtime#31081)
This caused an issue when sending request to c-lightning,
when you use System.Text.Json for serialization lib, and there
is a enum value in the request field, the field will be serialized into
an int, instead of a string. Thus causing RPC error.

This commit fixes it by using `JsonStringEnumConverterEx`
(which is taken from an issue comment of issue above).
  • Loading branch information
joemphilips committed Aug 15, 2022
1 parent 7c5fdbb commit fa9a3b3
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 10 deletions.
6 changes: 3 additions & 3 deletions src/DotNetLightning.ClnRpc/DotNetLightning.ClnRpc.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<Compile Include="NewtonsoftJsonConverters.fs" />
<Compile Include="ManuallyDefinedTypes.fs" />
<Compile Include="Requests.fs" />
<Compile Include="SystemTextJsonConverterExtensions.fs" />
<Compile Include="Client.fs" />
<Compile Include="Client.Methods.fs" />
<Compile Include="Plugin/DTOs.fs" />
Expand All @@ -30,14 +31,13 @@
<ProjectReference Include="..\AEZ\AEZ.csproj" PrivateAssets="all" />
<ProjectReference Include="..\Macaroons\Macaroons.csproj" ExcludeAssets="all" />

<ProjectReference Include="..\DotNetLightning.Core\DotNetLightning.Core.fsproj" PrivateAssets="all"/>
<ProjectReference Include="..\DotNetLightning.Core\DotNetLightning.Core.fsproj" PrivateAssets="all" />
</ItemGroup>

<!-- this is a workaround only needed for nuget push (so, not Mono, but just "dotnet nuget"), for more info see
https://github.com/joemphilips/DotNetLightning/issues/14 and https://github.com/joemphilips/DotNetLightning/commit/c98307465f647257df1438beadb4cabc7db757f2
and https://github.com/NuGet/Home/issues/3891 and https://github.com/NuGet/Home/issues/3891#issuecomment-377319939 -->
<Target Name="CopyProjectReferencesToPackage" DependsOnTargets="ResolveReferences"
Condition="'$(MSBuildRuntimeType)'!='Mono'">
<Target Name="CopyProjectReferencesToPackage" DependsOnTargets="ResolveReferences" Condition="'$(MSBuildRuntimeType)'!='Mono'">
<ItemGroup>
<BuildOutputInPackage Include="@(ReferenceCopyLocalPaths-&gt;WithMetadataValue('ReferenceSourceTarget', 'ProjectReference'))" />
</ItemGroup>
Expand Down
22 changes: 22 additions & 0 deletions src/DotNetLightning.ClnRpc/Requests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3105,3 +3105,25 @@ type private Response =
| Ping of Responses.PingResponse
| SignMessage of Responses.SignmessageResponse


open DotNetLightning.ClnRpc.SystemTextJsonConverters
module internal AddJsonConverters =
let addEnumConverters(opts: JsonSerializerOptions) =
opts.Converters.Add(JsonStringEnumConverterEx<Responses.SendpayStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Responses.CloseType>())
opts.Converters.Add(JsonStringEnumConverterEx<Responses.ConnectDirection>())
opts.Converters.Add(JsonStringEnumConverterEx<Responses.CreateinvoiceStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Requests.DatastoreMode>())
opts.Converters.Add(JsonStringEnumConverterEx<Requests.DelinvoiceStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Responses.DelinvoiceStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Responses.SendonionStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Requests.ListsendpaysStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Responses.PayStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Responses.WaitanyinvoiceStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Responses.WaitinvoiceStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Responses.WaitsendpayStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Requests.NewaddrAddresstype>())
opts.Converters.Add(JsonStringEnumConverterEx<Responses.KeysendStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Requests.FeeratesStyle>())
opts.Converters.Add(JsonStringEnumConverterEx<Requests.ListforwardsStatus>())
opts.Converters.Add(JsonStringEnumConverterEx<Requests.ListpaysStatus>())
17 changes: 17 additions & 0 deletions src/DotNetLightning.ClnRpc/SystemTextJsonConverterExtensions.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace DotNetLightning.ClnRpc.SystemTextJsonConverters

open System.Text.Json

open System.Runtime.CompilerServices
open NBitcoin

[<Extension; AbstractClass; Sealed>]
type ClnSharpClientHelpers =
[<Extension>]
static member internal AddDNLJsonConverters
(
this: JsonSerializerOptions,
n: Network
) =
this._AddDNLJsonConverters(n)
DotNetLightning.ClnRpc.AddJsonConverters.addEnumConverters(this)
52 changes: 48 additions & 4 deletions src/DotNetLightning.ClnRpc/SystemTextJsonConverters.fs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ type OutPointJsonConverter() =
raise <| JsonException("not a valid txid:output tuple")
else
let o = OutPoint()
o.Hash <- splits.[0] |> uint256.Parse
o.N <- splits.[1] |> uint32
o.Hash <- splits[0] |> uint256.Parse
o.N <- splits[1] |> uint32
o

type FeerateJsonConverter() =
Expand Down Expand Up @@ -154,10 +154,54 @@ type OutputDescriptorJsonConverter(network: Network) =
reader.GetString() |> fun s -> OutputDescriptor.Parse(s, network)


open System.Collections.Generic
open System.Runtime.Serialization
open System.Linq

/// Taken from https://github.com/dotnet/runtime/issues/31081#issuecomment-848697673
type JsonStringEnumConverterEx<'TEnum when 'TEnum: enum<int32> and 'TEnum: equality and 'TEnum: (new:
unit -> 'TEnum) and 'TEnum: struct and 'TEnum :> Enum>() =
inherit JsonConverter<'TEnum>()

let _enumToString = Dictionary<'TEnum, string>()
let _stringToEnum = Dictionary<string, 'TEnum>()

do
let ty = typeof<'TEnum>

for v in Enum.GetValues<'TEnum>() do
let enumMember = ty.GetMember(v.ToString())[0]

let maybeAttr =
enumMember
.GetCustomAttributes(typeof<EnumMemberAttribute>, false)
.Cast<EnumMemberAttribute>()
.FirstOrDefault()
|> Option.ofObj

_stringToEnum.Add(v.ToString(), v)

match maybeAttr with
| Some attr ->
_enumToString.Add(v, attr.Value)
_stringToEnum.Add(attr.Value, v)
| None -> _enumToString.Add(v, v.ToString())

override this.Read(reader, _typeToConvert, _options) =
let stringV = reader.GetString()

match _stringToEnum.TryGetValue stringV with
| true, v -> v
| _ -> Unchecked.defaultof<'TEnum>

override this.Write(writer, value, _options) =
writer.WriteStringValue(_enumToString[value])


[<Extension; AbstractClass; Sealed>]
type ClnSharpClientHelpers =
type internal ClnSharpClientHelpersCore =
[<Extension>]
static member AddDNLJsonConverters
static member internal _AddDNLJsonConverters
(
this: JsonSerializerOptions,
n: Network
Expand Down
26 changes: 26 additions & 0 deletions tests/DotNetLightning.ClnRpc.Tests/SerializerTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ open DotNetLightning.ClnRpc
open DotNetLightning.ClnRpc.Plugin
open DotNetLightning.ClnRpc.NewtonsoftJsonConverters
open DotNetLightning.ClnRpc.Requests
open DotNetLightning.ClnRpc.Responses
open DotNetLightning.ClnRpc.SystemTextJsonConverters
open DotNetLightning.Serialization
open NBitcoin
Expand Down Expand Up @@ -213,3 +214,28 @@ type SerializerTests() =
Assert.Null(jObj.Root.["exclude"])
Assert.Null(jObj.Root.["cltv"])
()

[<Fact>]
member this.SerializeListPays() =
let req =
{
ListpaysRequest.Bolt11 =
"lnbcrt500u1p305fnmpp5vzsjps8uptzedfmrw8jsuw37m4mdlyjjua0qfzceph3a0nz7rtfqdql2djkuepqw3hjqsj5gvsxzerywfjhxuccqzptxqrrsssp5fak5cm2c3r5wtezcflfg6cs3psrp4kczvp4wly66h85y4m4hsrds9qyyssqqxemaw5w9r6hteaxmmhvqe4nkv654nyk88gahjt5mxfjjzkj945xe6frwuavv8u0fzwcst0mvrxj8nxlj3qad9dxgzv8rg9dup3r5kcqnwpqjk"
|> Some
PaymentHash = None
Status = ListpaysStatus.PENDING |> Some
}

let opts = JsonSerializerOptions()

let data1 =
opts.AddDNLJsonConverters(Network.RegTest)
JsonSerializer.SerializeToDocument(req, opts)

Assert.Equal(
"pending",
data1
.RootElement
.GetProperty("status")
.GetString()
)
22 changes: 19 additions & 3 deletions tools/fsharp_msggen/fsharp_msggen/fsharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,12 +276,30 @@ def write_header(self):
"""
self.write(opens)

def _gen_stj_converter(self, prefix: str, field: EnumField):
self.write(f"opts.Converters.Add(JsonStringEnumConverterEx<{prefix}.{field.typename}>())\n", numindent=2)

def generate_stj_method(self, service: Service):
template = """
open DotNetLightning.ClnRpc.SystemTextJsonConverters
module internal AddJsonConverters =
let addEnumConverters(opts: JsonSerializerOptions) =
"""
self.write(template)
for method in service.methods:
for field in method.request.fields:
if isinstance(field, EnumField):
self._gen_stj_converter("Requests", field)
for field in method.response.fields:
if isinstance(field, EnumField):
self._gen_stj_converter("Responses", field)

def generate(self, service: Service):
self.write_header()
self.generate_requests(service)
self.generate_responses(service)
self.generate_enums(service)

self.generate_stj_method(service)

class FSharpClientExtensionGenerator:
def __init__(self, dest: TextIO):
Expand Down Expand Up @@ -340,8 +358,6 @@ def write_header(self):
"""
self.write(opens)

def generate(self, service: Service):
self.write_header()
self.generate_methods(service)

0 comments on commit fa9a3b3

Please sign in to comment.