reson
v0.0.2
Published
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 vali
Downloads
16
Readme
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
- Get it from npm
npm install --save reson- 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 | stringIdiomatic 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.
