spartanz / schemaz

A purely-functional library for defining type-safe schemas for algebraic data types, providing free generators, SQL queries, JSON codecs, binary codecs, and migration from this schema definition
https://spartanz.github.io/schemaz
Apache License 2.0
164 stars 18 forks source link

Play JSON codecs #37

Closed vil1 closed 5 years ago

vil1 commented 5 years ago

This PR adds interpreters for (en|de)coders to/from the Play! JSON AST in a playJson module, and is therefore part of #32.

More specifically, it introduces interpreters for play.api.libs.json.Reads and play.api.libs.json.Writes.

JSON having no standard support for mere products (aka tuples), I had to make a decision about how we represent such mere products. At the same time, I had to devise a way to handle differently products that are part of a Record (which must be represented as flat JsObjects) and "mere products" (represented as I arbitrarily decided).

Solving that problem made me introduce HCoalgebra, hyloNT and HEnvT.

These three constructs are used to derive Reads and Writes from a schema.

First I use an HCoalgebra to "push-down" information (using HEnvT) so that every :*: node "knows" if it's part of a Record or not, and then I use this information in the HAlgebra to decide how to render this particular :*: node.

The :*: nodes that are under a Record node are rendered as a single JsObject: we know that these nodes contain only ProductTerms which are rendered as JsObject with a single field, these JsObjects are then merged to form the record's representation.

Other :*: node are considered as tuples, which are (debatably) rendered as { "_1": "foo", "_2": { "_1": "bar", "_2": ...}}.

Tests ensure that the Reads and Writes derived from a given schema are symmetrical, ie. reads(writes(data)) == data.

vil1 commented 5 years ago

I don't quite follow the annotation logic you employ during the coalgebra. you start out with a false, a record causes any further nodes to carry along a true, a :*: passes false to left and carries along on the right and any other just carries the flag along.

but shouldn't a record pass a true to all immediate children and in any other case pass a false?

This needs clarification indeed.

What I want is to label with true all the "chain" of :*: that are under a Record, but not their children.

So I schematically (no pun intended) end up with something like:

false -> Record(
  true -> :*:(
    false -> x // <- is guarantied to be a ProductTerm,
    true -> :*:(
       false -> y,
       true -> z  // also guarantied to be a ProductTerm
    )
  )
)

The "last child" (here z) ends up labelled with true but it doesn't matter (it is guarantied to be a ProductTerm and I don't use the label for rendering those).

Does that make more sense?