thoth-org / Thoth.Json

Library for working with JSON in a type safe manner, this libs is targeting Fable

Home Page:https://thoth-org.github.io/Thoth.Json/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add Encode.mapping and Decode.mapping?

njlr opened this issue · comments

commented

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

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

Thoth.Json/src/Decode.fs

Lines 690 to 691 in a1fe5bb

let dict (decoder : Decoder<'value>) : Decoder<Map<string, 'value>> =
map Map.ofList (keyValuePairs decoder)

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
commented

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 👍

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.

Thoth.Json/src/Decode.fs

Lines 496 to 508 in a1fe5bb

let keyValuePairs (decoder : Decoder<'value>) : Decoder<(string * 'value) list> =
fun path value ->
match keys path value with
| Ok objectKeys ->
(Ok [], objectKeys) ||> List.fold (fun acc prop ->
match acc with
| Error _ -> acc
| Ok acc ->
match Helpers.getField prop value |> decoder path with
| Error er -> Error er
| Ok value -> (prop, value)::acc |> Ok)
|> Result.map List.rev
| Error e -> Error e

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

Thoth.Json/src/Decode.fs

Lines 690 to 691 in a1fe5bb

let dict (decoder : Decoder<'value>) : Decoder<Map<string, 'value>> =
map Map.ofList (keyValuePairs decoder)

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

There is also the Auto API to cover:

Thoth.Json/src/Decode.fs

Lines 1153 to 1159 in a1fe5bb

elif fullname = typedefof< Map<string, obj> >.FullName then
let keyDecoder = t.GenericTypeArguments.[0] |> autoDecoder extra caseStrategy false
let valueDecoder = t.GenericTypeArguments.[1] |> autoDecoder extra caseStrategy false
oneOf [
autoObject2 keyDecoder valueDecoder
list (tuple2 keyDecoder valueDecoder)
] |> map (fun ar -> toMap (unbox ar) |> box)

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:

  • Decode.Map, which a capital letter to not conflict with map --> too subtle ?
  • Decode.asMap -> Doesn't follow the same convention as the other decoder
  • Decode.toMap -> Doesn't follow the same convention as the other decoder
  • Decode.mapping looks a bit strange to me
  • Decode.map' not perfect but "ok" ?

Personally, my preference goes to Decode.map'

commented

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:

  • Decode.map Function to create a Decoder<Map<'k, 'v>>
  • Decoder.map Function to map the result of a successful decoding (currently Decode.map)

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.

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 ^^)

commented

PR for the most minimal version of this feature: #136

  • Decode.dict is unchanged
  • Decode.map' is added
  • Decode.Auto supports maps with non-string keys
  • A few tests

Will be included in the next release