thoth-org / Thoth.Json

Library for working with JSON in a type safe manner, this libs is targeting Fable
https://thoth-org.github.io/Thoth.Json/
MIT License
150 stars 36 forks source link

Consider add `expect` decoder #164

Closed lontivero closed 1 year ago

lontivero commented 1 year ago

In some situations it could be convenient to have a early circuit shortener to use in combination with oneOf . For example, the Nostr protocol serializes messages as arrays, here an example:

["EVENT", <subscription_id>, <event JSON as defined above>]
["OK", <event_id>, <success>, <message>]
["NOTICE", <message>]
["EOSE", <message>]

In this case it is the first element ~is~ the one that indicates the type of message and how it should be deserialized so, the oneOf decoder can be used but we want to stop immediately if the first element doesn't match. My proposal is to have a decoder like the following:

   let expect expectedValue: Decoder<string> =
        Decode.string
        |> Decode.andThen (fun value ->
           if value = expectedValue then
              Decode.succeed value
           else
              Decode.fail $"'{expectedValue}' was expected but '{value} was found instead'")

I've used it here: https://github.com/lontivero/Nostra/blob/5c76df4ca8aa18022e4f84202693146aa53438fc/Nostra.Core/Client.fs#L182-L205

Please if this is not the best way to do it I would appreciate your advice. Thanks for the library, it is really really cool.

MangelMaxime commented 1 year ago

The decoder that you propose is too specialised to be inside of Thoth.Json. At least, in the current form.

The decoder is called expect but only works with string type where expect implied that you expect a particular value which could be string, int, object, etc.

You can avoid the use of oneOf in your decoder by using:

Decode.index 0 Decode.string
        |> Decode.andThen (function
            | "EVENT" ->
                // ...
            | "OK" -> 
                // ...
            | unknown -> 
                sprintf "'%s' is not a supported type for ReplayMessage" unknown
                |> Decode.fail
        )

Full example code:

Fable REPL - Online Demo


type SubscriptionId = SubscriptionId of string

module SubscriptionId =

    let decoder : Decoder<SubscriptionId> =
        Decode.string
        |> Decode.map SubscriptionId

type EventId = EventId of string

module EventId =
    let decoder : Decoder<EventId> =
        Decode.string
        |> Decode.map EventId

type RelayMessage =
    | RMEvent of SubscriptionId * int
    | RMACK of EventId * bool * string

module ReplayMessage =

    let decoder : Decoder<RelayMessage> =
        Decode.index 0 Decode.string
        |> Decode.andThen (function
            | "EVENT" ->
                // You can move this decoder in its own function to make the code more readable
                Decode.map2
                    (fun subId value ->
                        RMEvent (subId, value)
                    )
                    (Decode.index 1 SubscriptionId.decoder)
                    (Decode.index 2 Decode.int)

            | "OK" -> 
                // You can move this decoder in its own function to make the code more readable
                Decode.map3
                    (fun eventId isSuccess message ->
                        RMACK (eventId, isSuccess, message)
                    )
                    (Decode.index 1 EventId.decoder)
                    (Decode.index 2 Decode.bool)
                    (Decode.index 3 Decode.string)

            | unknown -> 
                sprintf "'%s' is not a supported type for ReplayMessage" unknown
                |> Decode.fail
        )
lontivero commented 1 year ago

I see. It is completely unnecessary.

Thank you very much for the code, I will do as you say.