nkrkv / jzon

ReScript library to encode and decode JSON data with type safety.
https://nkrkv.github.io/jzon/
Other
76 stars 4 forks source link

Need help on JSON:API `data:` field #10

Closed argent-smith closed 3 years ago

argent-smith commented 3 years ago

Hi. In JSON:API spec there's a document top-level data: field which can be either an object or an array of objects, i.e:

{
  "data":  {"id": "1", "type": "foos", "payload": "zzzz"}
}

or

{
  "data": [
    {"id": "1", "type": "foos", "payload": "zzzz"},
    {"id": "2", "type": "foos", "payload": "xxxx"}
  ]
}

How to make a codec for such a case?

nkrkv commented 3 years ago

This is an interesting case. Perhaps it is worthwhile to add one to the stock Jzon library. Meanwhile, you can do it yourself:

type dataItem = {
  id: string,
  type_: string,
  payload: string,
}

type message = {data: array<dataItem>}

module ResultX = {
  // This should really belong to the standard lib
  let sequence = (results: array<result<'ok, 'err>>): result<array<'ok>, 'err> => {
    results->Array.reduce(Ok([]), (maybeAcc, res) => {
      maybeAcc->Result.flatMap(acc => res->Result.map(x => acc->Array.concat([x])))
    })
  }
}

module Codecs = {
  // Introduce a new codec for such case
  let scalarOrArray = itemCodec =>
    Jzon.custom(
      items =>
        items->Array.length == 1
          ? Jzon.encode(itemCodec, items->Array.getExn(0))
          : Jzon.encode(Jzon.array(itemCodec), items),
      json =>
        switch json->Js.Json.decodeArray {
        | Some(itemJsons) => itemJsons->Array.map(Jzon.decode(itemCodec))->ResultX.sequence
        | None => Jzon.decode(itemCodec, json)->Result.map(item => [item])
        },
    )

  let dataItem = Jzon.object3(
    ({id, type_, payload}) => (id, type_, payload),
    ((id, type_, payload)) => {id: id, type_: type_, payload: payload}->Ok,
    Jzon.field("id", Jzon.string),
    Jzon.field("type", Jzon.string),
    Jzon.field("payload", Jzon.string),
  )

  let message = Jzon.object1(
    ({data}) => data,
    data => {data: data}->Ok,
    Jzon.field("data", scalarOrArray(dataItem)),
  )
}

Test.test("Scalar or array -> single", () => {
  Codecs.message
  ->Jzon.decodeString(`{
    "data": {"id": "1", "type": "foos", "payload": "zzzz"}
  }`)
  ->Assert.okOf({data: [{id: "1", type_: "foos", payload: "zzzz"}]}, ~message="Decodes correctly")

  Assert.roundtrips(
    {data: [{id: "1", type_: "foos", payload: "zzzz"}]},
    Codecs.message,
    ~message="Does roundtrip",
  )
})

Test.test("Scalar or array -> multiple", () => {
  Codecs.message
  ->Jzon.decodeString(`{
    "data": [
      {"id": "1", "type": "foos", "payload": "zzzz"},
      {"id": "2", "type": "foos", "payload": "xxxx"}
    ]
  }`)
  ->Assert.okOf(
    {
      data: [{id: "1", type_: "foos", payload: "zzzz"}, {id: "2", type_: "foos", payload: "xxxx"}],
    },
    ~message="Decodes correctly",
  )

  Assert.roundtrips(
    {
      data: [{id: "1", type_: "foos", payload: "zzzz"}, {id: "2", type_: "foos", payload: "xxxx"}],
    },
    Codecs.message,
    ~message="Does roundtrip",
  )
})

Does it make sense?

argent-smith commented 3 years ago

Makes sense, thnx!