reson

0.0.2 • Public • Published

ReSon

ReSon is a ReScript library that provides a convenient way to parse JSON data in a type-safe manner. Inspired by the classify approach described in the ReScript documentation and libraries like aeson, ReSon aims to simplify the process of parsing and validating JSON structures.

Features

  • Type-safe parsing of JSON data
  • Error handling for invalid JSON and data types
  • Support for parsing nested objects, arrays, and tuples
  • Optional and nullable field handling
  • Composable parsing combinators for building complex parsers

Installation

  1. Get it from npm
npm install --save reson
  1. Update bscofig.json's "bs-dependencies"
  "bs-dependenncies": ["ReSon"]

Usage

It all wraps around ReSon.t, which is

type t<'a> = result<'a, error>

Where error is the following:

type rec error =
  | NotJSON
  | NotNumber
  | NotString
  | NotBoolean
  | NotArray
  | NotObject
  | MissingField(string)
  | Field(string, error)
  | ArrayItem(int, error)
  | Nor(error, error)
  | NotTuple2
  | Tuple2(int, error)
  | NotTuple3
  | Tuple3(int, error)
  | NotTuple4
  | Tuple4(int, error)
  | NotTuple5
  | Tuple5(int, error)
  | Custom(string)

There are several primitives that are available:

    let number: Js.Json.t => t<float>
    let string: Js.Json.t => t<string>
    let boolean: Js.Json.t => t<bool>
    let object: Js.Json.t => t<Js.Dict.t<Js.Json.t>>

    let initParse: string => t<Js.Json.t>

Example

    // This will print "34"
    let s = `34`
    let x = initParse(s)->Result.flatMap(number)
    switch x {
    | Ok(x) => Js.log(x)
    | Error(e) => Js.log(e->printError)
    }

There's an error type which can be pretty-printed

    // This will print "Not a number"
    let s = `false`
    let x = initParse(s)->Result.flatMap(number)
    switch x {
    | Ok(x) => Js.log(x)
    | Error(e) => Js.log(e->printError)
    }

Combining parsers together

There are also few helper functions, that allow combining, and updating existing parsers.

    // allows any other parser to accept null as possible value
    let nullable: (Js.Json.t => t<'a>, Js.Json.t) => t<option<'a>>
    // parses items of array with given parses
    let array: (Js.Json.t => t<'a>, Js.Json.t) => t<array<'a>>

    // Just a wrapper around Result.map so that parsed result can be updated
    let map: ('a => t<'b>, 'b => 'd, 'a) => t<'d>

    // to parse results of parse, a wrapper around Result.flatMap, see examples below
    let with: (Js.Json.t => t<'b>, 'b => t<'c>, Js.Json.t) => t<'c>
    // to be used with(object), see example
    let field: (Js.Dict.t<Js.Json.t>, string, Js.Json.t => t<'a>) => t<'a>
    // Same as field, but allows field to not exist
    let optional: (Js.Dict.t<Js.Json.t>, string, Js.Json.t => t<'a>) => t<option<'a>>

    // Parses json arrays like [false, 13] into pair.
    // there are tuple2, tuple3, tuple4, and tuple5
    let tuple2: ( Js.Json.t => result<'a, error>, Js.Json.t => result<'b, error>, Js.Json.t,) => t<('a, 'b)>

Example

type myType = {
  key: float,
  otherKey: string,
  optional: option<string>,
}

// recommend to create constructor function
let make = (key, otherKey, optional) => {key, otherKey, optional}

let s = `{"key": 1, "otherKey": "some text here"}`

let p = with(object)(obj => {
  pure(make)
  ->apply(obj->field("key", number))
  ->apply(obj->field("otherKey", string))
  ->apply(obj->optional("optional", string))
})

// will print `{ key: 1, otherKey: 'some text here', optional: undefined }`
switch initParse(s)->Result.flatMap(p) {
| Ok(x) => Js.log(x)
| Error(e) => Js.log(e->printError)
}

let s = `{}`

// will print `Missing field "key"`
switch initParse(s)->Result.flatMap(p) {
| Ok(x) => Js.log(x)
| Error(e) => Js.log(e->printError)
}

let s = `{"key": "not a number"}`
// will print `Error on path [key]: Not a number`
switch initParse(s)->Result.flatMap(p) {
| Ok(x) => Js.log(x)
| Error(e) => Js.log(e->printError)
}

type nested = {key: float}
type withNested = {nested: nested}

let makeNested = key => {key}
let make = nested => {nested}

let s = `{"nested": {
  "key": "not a number"
}}`

let parseNested = with(object)(obj => {
  pure(key => {key})->apply(field(obj, "key", number))
})

let p = with(object)(obj => {
  pure(make)->apply(field(obj, "nested", parseNested))
})

// will print Error on path [nested.key]: Not a number
//                           ^ so nested path really looks cool
switch initParse(s)->Result.flatMap(p) {
| Ok(x) => Js.log(x)
| Error(e) => Js.log(e->printError)
}

Sum types

There are situations when there can be some sort of variation:

  // In Ts we would say that
  type T = number | string

Idiomatic way of expressing it in ReScript would be something like this:

  type t = OfFloat(float) | OfString(string)

Parser for such type can be constructed by using or helper function.

let parseOfFloat = number->map(x => OfFloat(x))
let parseOfString = string->map(x => OfString(x))

let parseSumType = parseOfFloat->or(parseOfString)

Another typical situation is when in TS we would say

  type T = "tag1" | "tag2"

And in ReScript we usually express it like:

  type t = [#tag1 | #tag2]

To parse such type I suggest using this little patternn:

  let p = with(string)(s => switch s {
    | "tag1" => Ok(#tag1)
    | "tag2" => Ok(#tag2)
    | _ => Error(Custom("Unknown tag " ++ s))
  })

The End

For further details I sugges taking a look at test cases or the src, it's just ~200 lines of code. If you think there's a way to improve the way these functions are interacted with on the surface, I'll hear you out in Issues.

Readme

Keywords

Package Sidebar

Install

npm i reson

Weekly Downloads

0

Version

0.0.2

License

MIT

Unpacked Size

24 kB

Total Files

5

Last publish

Collaborators

  • sudo97