mattjbray / ocaml-decoders

Elm-inspired decoders for Ocaml
https://mattjbray.github.io/ocaml-decoders/
Other
84 stars 8 forks source link

Dynamic decoding? #36

Open idkjs opened 3 years ago

idkjs commented 3 years ago

Is it possible to use this lib to dynamically decode json? That is, json you dont know the shape of prior to getting it?

Thank you.

mattjbray commented 3 years ago

Yes, you can use one_of to try a bunch of different shapes in turn, or if the value of some field in your JSON tells you what shape to expect next, you can decode the field and then dispatch to decoders for each different shape. See the section in https://github.com/mattjbray/ocaml-decoders#complicated-json-structure about shapes for an example.

If you can tell me a bit more about your specific use-case I can try to help more.

idkjs commented 3 years ago

@mattjbray thanks for showing interest.

Im trying to re-impletent the kentcdodds/match-sorter lib in reasonml as a learning exercise.

The lib has to be able to take in various types of input and process them and you will never know exactly what it is beyond a list or an object array.

Here is an example of the types of input:

Details ```js const objList = [ { name: 'Janice', color: 'Green' }, { name: 'Fred', color: 'Orange' }, { name: 'George', color: 'Blue' }, { name: 'Jen', color: 'Red' } ]; const fruit = [ 'orange', 'apple', 'grape', 'banana' ]; const things = [ 'google', 'airbnb', 'apple', 'apply', 'app' ]; const otherThings = [ 'fiji apple', 'google', 'app', 'crabapple', 'apple', 'apply' ]; const aliases = [ { aliases: [ { name: { first: 'baz' } }, { name: { first: 'foo' } }, { name: null } ] }, { aliases: [ { name: { first: 'foo' } }, { name: { first: 'bat' } }, null ] }, { aliases: [ { name: { first: 'foo' } }, { name: { first: 'foo' } } ] }, { aliases: null }, {}, null ]; const icecream = [ { favorite: { iceCream: [ { tastes: [ 'vanilla', 'mint' ] }, { tastes: [ 'vanilla', 'chocolate' ] } ] } }, { favorite: { iceCream: [ { tastes: [ 'vanilla', 'candy cane' ] }, { tastes: [ 'vanilla', 'brownie' ] } ] } }, { favorite: { iceCream: [ { tastes: [ 'vanilla', 'birthday cake' ] }, { tastes: [ 'vanilla', 'rocky road' ] }, { tastes: [ 'strawberry' ] } ] } } ]; module.exports = { objList, fruit, things, otherThings, aliases, icecream }; ```

I was looking to see if there are other options to @glennsl/bs-json. I have jsonm working, but as the internet notes, its not the easiest thing to use.

Any ideas you might share will be greatly appreciated, sir. Thanks again.

mattjbray commented 3 years ago

Sounds like a fun exercise!

So it looks like the value the user passes in the keys parameter tells us what shape of JSON to expect.

Here's one way you could do it:

module Matchers (D : Decoders.Decode.S)
  (* parameterising the decoders backend allows us to re-use 
     the same code in Bucklescript and in native Ocaml. *)
  = struct
  (** Represents an item in the input JSON list *)
  type candidate =
    { orig_json : D.value
          (** The original JSON item. We'll return this if any of the values match. *)
    ; strings : string list
          (** Strings extracted from the JSON item we'll match against. *)
    }

  (** Given a [key] like ["a.b.c"], decode the string at c.

      This decoder uses [D.maybe], so instead of failing when the JSON does not
      have the right shape, it will succeed with [None].

      To allow array indexing in the key (e.g. ["a.b.0.c"]), you can write a
      customized version of [D.at] that checks whether an item in the path is an
      integer, and then uses [D.index] instead of [D.field].

      See https://github.com/mattjbray/ocaml-decoders/blob/e00bad1d2e4c2b1394aee9ae5a7a6fe3ab04ecec/src/decode.ml#L606
   *)
  let decode_string_at_key (key : string) : string option D.decoder =
    let path = String.split_on_char '.' key in
    D.maybe (D.at path D.string)

  (** Decode all the strings in an object following [keys]. *)
  let rec decode_strings ~keys () : string list D.decoder =
    let open D.Infix in
    match keys with
    | key :: keys ->
         decode_string_at_key key >>= fun str ->
         decode_strings ~keys () >>= fun strs ->
        let strs = match str with Some str -> str :: strs | None -> strs in
        D.succeed strs
    | [] ->
        D.succeed []

  let decode_candidate ?keys () : candidate D.decoder =
    let open D.Infix in
    D.value >>= fun orig_json ->
    let strings =
      match keys with
      | None ->
          (* No keys passed, assume value is a simple string. *)
          D.maybe D.string |> D.map (function | Some s -> [ s ] | None -> [])
      | Some keys ->
          decode_strings ~keys ()
    in
    strings >>= fun strings ->
    D.succeed { orig_json; strings }

  let decode_candidates ?keys () : candidate list D.decoder =
    D.list (decode_candidate ?keys ())
end

(** Assuming you're using bucklescript - use Decoders_yojson.Basic.Decode or something if you're using native *)
module D = Decoders_bs.Decode
module Matchers_bs = Matchers (D)

let candidates ?keys (json : Js.Json.t) : (Matchers_bs.candidate list, D.error) result =
  D.decode_value (Matchers_bs.decode_candidates ?keys ()) json
idkjs commented 3 years ago

Wow. Thanks. Let me try this. Will revert with news.

idkjs commented 3 years ago

Im trying run through your examples to get familiar.

To do so dune utop . then add the code.

fork (update-ocaml)  dune utop .
──────────────────────────────
p version 2.7.0 (using OCaml v
──────────────────────────────

Type #utop_help for help about using utop.

─( 18:19:49 )─< command 0 >─{ 
utop # module D = Decoders_yojson.Basic.Decode;;
module D =
  Decoders_yojson.Basic.Decode
─( 18:20:04 )─< command 1 >────────────────────────────────────────────────{ counter: 0 }─
utop # 

I can add it to the Readme.md if you would like or just leave this here.

idkjs commented 3 years ago

So, I can't seem to run the examples from the Readme.md for the life of me. I will have to try again tomorrow. I learned a lot about it today and maybe I am tired. Still feels kinda clownish. https://github.com/idkjs/modern-ocaml-decoders

Sorry about the dune ignorance here. I dont really want to leave ocaml/reason land yet and well rescript. So I have had to start diving into dune. I have not got a hold of it yet. For example, I finally got most of the examples compiling here but I cant seem to run them in rtop. This lib is unbound for some reason. Will stay on it.