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
152 stars 35 forks source link

Add Encode.mapping and Decode.mapping? #132

Closed njlr closed 1 year ago

njlr commented 2 years ago

I was wondering if the library should have built-in support for maps, similar to lists and sequences?

(Decode.map is already taken of course)

Perhaps...

#r "nuget: Thoth.Json.Net"

open Thoth.Json.Net

module Encode =

  let mapping keyEncoder valueEncoder : Encoder<Map<'k, 'v>> =
    (fun m ->
      m
      |> Map.toSeq
      |> Seq.map (Encode.tuple2 keyEncoder valueEncoder)
      |> Encode.seq)

module Decode =

  let mapping keyDecoder valueDecoder : Decoder<Map<'k, 'v>> =
    Decode.list (Decode.tuple2 keyDecoder valueDecoder)
    |> Decode.andThen
      (fun kvps ->
        let duplicateKeys =
          kvps
          |> Seq.map fst
          |> Seq.groupBy id
          |> Seq.filter (fun (_, vs) -> Seq.length vs > 1)
          |> Seq.map fst

        if Seq.isEmpty duplicateKeys then
          Decode.succeed (Map.ofSeq kvps)
        else
          let duplicateKeysList =
            duplicateKeys
            |> Seq.map string
            |> String.concat ", "

          Decode.fail $"Found duplicate key(s): {duplicateKeysList}")

// Usage...

let m =
  Map.ofSeq
    [
      123, "abc"
      456, "def"
    ]

let json =
  m
  |> Encode.mapping Encode.int Encode.string
  |> Encode.toString 2

printfn "%A" json

let decoded =
  json
  |> Decode.unsafeFromString (Decode.mapping Decode.int Decode.string)

printfn "%A" decoded
MangelMaxime commented 2 years ago

There the dict decoder/encoder which support Map<string, 'T>.

https://github.com/thoth-org/Thoth.Json/blob/a1fe5bbcc53f799fb56bc84f93289f39c64cac5f/src/Decode.fs#L690-L691

I am unsure why I didn't make the key generics at the time too. I see two potential reason:

  1. dict was implemented like that in the Elm library and I didn't think more about it
  2. Could it be that we can't support any type of keys in the JSON representation? For example, can we work with a key represented as a Record? I am taking the record as an example because I think this is probably the most extreme case possible.

Depending on the answer, there are 2 solutions:

  1. Make a breaking change and make dict accept generics for both the keys and the values
  2. Provide a new decoder (new name) as you proposed
njlr commented 2 years ago

Ah, I think I skipped over dict assuming it was for IDictionary. I think the ability to have non-string keys is quite important; this is a key advantage of Fable over JavaScript for me.

Happy to send a PR with some direction :+1:

MangelMaxime commented 2 years ago

Ah, I think I skipped over dict assuming it was for IDictionary.

Yes, you are not the first one. That's the problem when you have a type Map and the function map to support. 😅

Happy to send a PR with some direction 👍

The first thing is to decide how we are going to represent the type as JSON. I think you proposed to use an array representation with the first value being the key and the second the value. I think this is indeed the only way we have to do it.

Then for the actual code changes:

Rewrite keyValuesPairs to accept a keyDecoder and valueDecoder.

https://github.com/thoth-org/Thoth.Json/blob/a1fe5bbcc53f799fb56bc84f93289f39c64cac5f/src/Decode.fs#L496-L508

This is because it allows to have a really simple implementation for the dict decoder.

https://github.com/thoth-org/Thoth.Json/blob/a1fe5bbcc53f799fb56bc84f93289f39c64cac5f/src/Decode.fs#L690-L691

Which would also need to be changed to accept a keyDecoder and valueDecoder.

There is also the Auto API to cover:

https://github.com/thoth-org/Thoth.Json/blob/a1fe5bbcc53f799fb56bc84f93289f39c64cac5f/src/Decode.fs#L1153-L1159

I think the only changes required is to convert typedefof< Map<string, obj> >.FullName to typedefof< Map<obj, obj> >.FullName because the keyDecoder is already auto generating its type.

The same things needs to be done for the encoders and the tests needs to be adapt :). Then we need to port the change to Thoth.Json.Net but in general this is just a matter of copy/pasting the code.

Because, we are going to make a breaking change to the API by adding an arguments to dict, I would like to take this opportunity to change dict to returns the F# dictionary type (is it IDictionary? Is it supported by Fable?).

And then find a good name for the Map type.

Some proposition I have are:

Personally, my preference goes to Decode.map'

njlr commented 2 years ago

Decode.keyValueListMap is verbose, but descriptive. It also gives a clue as to how the map will be encoded as a JSON array.

If breaking changes are on the table, another option might be:

I like Decoder.map because Decoder is a module of functions over decoders, which mirrors List.map, Seq.map, etc.

Although this could be extremely confusing for existing users! It is also a further departure from Elm.

MangelMaxime commented 2 years ago

Decode.keyValueListMap is verbose, but descriptive. It also gives a clue as to how the map will be encoded as a JSON array.

Indeed, it is verbose but if I was to go this way I would probably use Decode.keyValueListAsMap or Decode.keyValueListToMap.

I like Decoder.map because Decoder is a module of functions over decoders, which mirrors List.map, Seq.map, etc.

For now, I think this is too much of a breaking change I think.

I still have the hope to rewrite Thoth.Json from scratch in the future and perhaps it will be a better time to have such a big breaking changes.

It is also a further departure from Elm.

This is not something holding me personally. I mean we already have some specific syntax thanks to classes and reflection support (even if I still think reflection support is an error ^^)

njlr commented 2 years ago

PR for the most minimal version of this feature: https://github.com/thoth-org/Thoth.Json/pull/136

MangelMaxime commented 1 year ago

Will be included in the next release