xyncro / chiron

JSON for F#
https://xyncro.tech/chiron
MIT License
175 stars 41 forks source link

Write/Read nested objects #39

Open haf opened 9 years ago

haf commented 9 years ago

I often have a tuple in F# 3-5 items long, but want to serialise it as an object; then I need to do lots of shenanigans.

With these I don't:

module Json =
    let writeNested key (toJson : _ -> Json<unit>) value : Json<unit> =
      fun json ->
        let inner = snd (toJson value json)
        Json.write key inner json
module Patterns =
  let inline (|InProperty|_|) key (fromJson : 'a -> Json<'a>) =
        Aether.Lens.getPartial (Json.ObjectPLens >??> Aether.mapPLens key)
     >> Option.bind (fromJson (Unchecked.defaultof<_>)
                     >> fst
                     >> function | Value x -> Some x
                                 | Error _ -> None)

Means I can have this usage:

type Interact =
  | Create of owner:Owner * createdByPrincipal:Id * bbf:Money
  | TopUp of amount:Money
  | Charge of amount:Money

  static member ToJson (i : Interact) : Json<unit> =
    let writeInner (owner, createdBy, bbf) : Json<unit> =
      Json.write "owner" owner
      *> Json.write "createdByPrincipal" createdBy
      *> Json.write "bbf" bbf

    match i with
    | Create (o, cb, bbf) -> Json.writeNested "create" writeInner (o, cb, bbf)
    | TopUp amount -> Json.write "topUp" amount
    | Charge amount -> Json.write "charge" amount

  static member FromJson (_ : Interact) : Json<Interact> =
    let inline readInner _ : Json<_ * _ * _> =
      (fun o cb bbf -> o, cb, bbf)
      <!> Json.read "owner"
      <*> Json.read "createdByPrincipal"
      <*> Json.read "bbf"

    function
    | InProperty "create" readInner (o, cb, bbf) as json ->
      Json.init (Create (o, cb, bbf)) json
    | Property "topUp" amount as json ->
      Json.init (TopUp amount) json
    | Property "charge" amount as json ->
      Json.init (Charge amount) json
    | json ->
      Json.error (sprintf "couldn't convert %A to Interact" json) json

What about adding them to the project?

neoeinstein commented 8 years ago

This use case might be solved by the addition of Json.writeWith and Json.readWith from #44.

haf commented 8 years ago

Here's a PropertyWith, please add?

  let inline (|PropertyWith|) fromJson key =
       Lens.getPartial (Json.Object_ >??> key_ key)
    >> Option.bind (fromJson >> function | Value a, _ -> Some a
                                         | _, _ -> None)
kolektiv commented 8 years ago

No problem, I'll get that in very soon - useful addition. There's likely to be a new Aether and Chiron (better in some serious ways I hope) out very soon, I'm kind of considering this weekend...

haf commented 8 years ago

Awesome.

Beware that I've lost the connection with the API changes you've done to Aether (haven't seen any docs either), so I'm currently only using older versions of it.

kolektiv commented 8 years ago

Yeah new docs do need to be done before a release actually... I think I might branch off the new version bits for Chiron for now and do a point release to get this change in, as it's useful to an actual user.

How are you using Aether at the moment? If a library, then the new changes have backwards compatible aliases until the next major version anyway. The new changes are cool though, reducing the number of operators, better type inference, etc.

On 4 December 2015 at 13:13, Henrik Feldt notifications@github.com wrote:

Awesome.

Beware that I've lost the connection with the API changes you've done to Aether (haven't seen any docs either), so I'm currently only using older versions of it.

— Reply to this email directly or view it on GitHub https://github.com/xyncro/chiron/issues/39#issuecomment-161964130.

haf commented 8 years ago

In Suave, in proprietary software, in Logary v4 mostly.

kolektiv commented 8 years ago

Cool. Well if you're usually pulling it in as a library, then it should be fine. And obviously it'll be a new major version, so if you've pinned things also no problem.

haf commented 8 years ago

I'm thinking of using it like this:

type PointValue =
  /// Value at point in time
  | Gauge of Value * Units
  /// Any sort of derived measure
  | Derived of Value * Units
  /// All simple-valued fields' values can be templated into the template string
  /// when outputting the value in the target.
  | Event of template:string
with
  static member private valueUnitsToJson (value : Value, units : Units) : Json<unit> =
    Json.write "value" value
    *> Json.write "units" units

  static member private valueUnitsFromJson : Json<Value * Units> =
    (fun value units -> value, units)
    <!> Json.read "value"
    <*> Json.read "units"

  static member ToJson (pv : PointValue) : Json<unit> =
    let inJsonObject writer =
      writer (Json.Object Map.empty) |> snd

    match pv with
    | Gauge (value, units) ->
      Json.writeWith (PointValue.valueUnitsToJson >> inJsonObject) "gauge" (value, units)

    | Derived (value, units) ->
      Json.writeWith (PointValue.valueUnitsToJson >> inJsonObject) "derived" (value, units)

    | Event template ->
      Json.write "event" template
kolektiv commented 8 years ago

That seems pretty sensible.

kolektiv commented 8 years ago

This has been merged in, and is out in 8.0.0-rc4 - definitely take a look at getting the latest. There's a backwards compat layer, but the new version is nicer when switched!