Giter VIP home page Giter VIP logo

decoder.flow's Introduction

decoder.flow

travis package downloads styled with prettier

Library was implemented after Elm JSON.Decode, it mostly keeps same API although some parts are modifed to fit better JS and Flow semantics.

Library for turning arbitrary input into typed data. You can use this library to convert arbitrary JSON into nicely structured (and flow typed) data.

The core concept in this library is a decoder. It decodes JSON values into typed values. Library provides some primitive decoders for handling primitive data types and some functions to put those together to form decoders that can handle more complex data.

Library can also be used to put together structure data parsers that take strings of input and return typed data. See parse function for more details.

Import

Rest of the the document & provided code examples assumes that library is installed & imported as follows:

import * as Decoder from "./"

Primitive value decoders

Decoder.Decoder<a>

A value that knows how to decode arbirtary JSON value (and/or parse arbitrary JSON string) into value of type a.

Decoder.String:Decoder.Decoder<string>

Decoder that decodes JSON string to a string value.

Decoder.decode(Decoder.String, true) //>  Result.Error Expecting a String but instead got: `true`
Decoder.decode(Decoder.String, 42) //>  Result.Error Expecting a String but instead got: `42`
Decoder.decode(Decoder.String, "Hello") //> Result.Ok "Hello"
Decoder.decode(Decoder.String, {hello:42}) //>  Result.Error Expecting a String but instead got: {hello:42}`

Decoder.Boolean:Decoder.Decoder<boolean>

Decoder that decodes JSON boolean into a boolean value.

Decoder.decode(Decoder.Boolean, true) //> Result.Ok true
Decoder.decode(Decoder.Boolean, 42) //>  Result.Error Expecting a Boolean but instead got: `42`
Decoder.decode(Decoder.Boolean, 3.14) //>  Result.Error Expecting a Boolean but instead got: `3.14`
Decoder.decode(Decoder.Boolean, "Hello") //>  Result.Error Expecting a Boolean but instead got: `"Hello"`
Decoder.decode(Decoder.Boolean, {hello:42}) //>  Result.Error Expecting a Boolean but instead got: `{hello:42}`

Decoder.Float:Decoder.Decoder<Decoder.float>

Decoder that decodes JSON number into a Decoder.float value, which is an opaque type alias for number type. Note that while (-)Infinity and NaN are valid float type values they aren't valid JSON numbers & this decoder will error decoding them. If you find yourself needing a way to decode as NaN or Infinity think twice and if you're absolutely sure let us know in an issue & given a convincing use case we'll add decoders for them.

Decoder.decode(Decoder.Float, 42) //> Result.Ok 42
Decoder.decode(Decoder.Float, 3.14) // Result.Ok 3.14
Decoder.decode(Decoder.Float, NaN) //>  Result.Error Expecting a Float but instead got: `NaN`
Decoder.decode(Decoder.Float, Infinity) //>  Result.Error Expecting a Float but instead got: `Infinity`
Decoder.decode(Decoder.Float, true) //>  Result.Error Expecting a Float but instead got: `true`
Decoder.decode(Decoder.Float, "hello") // Result.Error Expecting a Float but instead got: `"hello"`
Decoder.decode(Decoder.Float, {hello:42}) //> Result.Error Expecting a Float but instead got: `{hello:42}`

Decoder.Integer:Decoder.Decoder<Decoder.integer>

Decoder that decodes JSON number into a Decoder.integer value, which is an opaque type alias for number type guaranteed to be an integer (passing it to Number.isInteger returns true) and there for it excludes (-)Infinity and NaN:

Decoder.decode(Decoder.Integer, 42) //> Ok 42
Decoder.decode(Decoder.Integer, 3.14) //  Result.Error Expecting an Integer but instead got: `3.14`
Decoder.decode(Decoder.Integer, NaN) //>  Result.Error Expecting an Integer but instead got: `NaN`
Decoder.decode(Decoder.Integer, Infinity) //>  Result.Error Expecting an Integer but instead got: `Infinity`
Decoder.decode(Decoder.Integer, true) //>  Result.Error Expecting an Integer but instead got: `true`
Decoder.decode(Decoder.Integer, "hello") //>  Result.Error Expecting an Integer but instead got: `"hello"`
Decoder.decode(Decoder.Integer, {hello:42}) //>  Result.Error Expecting an Integer but instead got: `{hello:42}`

Data structure decoders

Decoder.optional <a> (Decoder<a>):Decoder.Decoder<?a>)

Creates decoder that decodes optional (null / undefined) JSON values into an optional value.

Decoder.decode(Decoder.optional(Decoder.Integer), 13) //> Result.Ok 13
Decoder.decode(Decoder.optional(Decoder.Integer), null) //> Result.Ok null
Decoder.decode(Decoder.optional(Decoder.Integer), undefined) //> Result.Ok null
Decoder.decode(Decoder.optional(Decoder.Integer), false) //> Result.Error Expecting an Integer but instead got: `false`
Decoder.decode(Decoder.optional(Decoder.Integer), "hello") //> Result.Error Expecting an Integer but instead got: `"hello"`

Decoder.array <a> (Decoder<a>):Decoder.Decoder<a[]>

Creates a decoder for JSON arrays, where each element is decoded via provided decoder:

Decoder.decode(Decoder.array(Decoder.Boolean), [true, false]) //> Result.Ok [true, false]
Decoder.decode(Decoder.array(Decoder.Float), [1, 2.2, 3]) //> Result.Ok [1, 2.2, 3]
Decoder.decode(Decoder.array(Decoder.Integer), [1, 2.2, 3]) //>  Result.Error Expecting an Integer at input[1] but instead got: `2.2`

Decoder.dictionary <a> (Decoder<a>):Decoder.Docoder<{[string]:a}>

Creates a decoder for JSON (dictionary) objects, where each value is decoded via provided decoder. If you are trying to decode JSON objects that have values of different types (a.k.a structs) consider using Decoder.record instead.

Decoder.decode(Decoder.dictionary(Decoder.Float), {
  alice: 42,
  bob: 99.8
}) //> Result.Ok {"alice":42,"bob":99.8}


Decoder.decode(Decoder.dictionary(Decoder.Integer), {
  alice: 42,
  bob: 99.8
}) //> Result.Error Expecting an Integer at input["bob"] but instead got: `99.8`

Decoder.record <a:{}> (a):Decoder.Decoder<Decoder.Record<a>>

Creates a decoder for JSON (struct) objects, where each field is decoded with corresponding decoder over corresponding field in JSON (struct):

const point = Decoder.record({
  x: Decoder.Integer,
  y: Decoder.Integer
})

Decoder.decode(point, { x: 3, y: 5 }) //> Result.Ok {x:3, y:5}
Decoder.decode(point, { x: 3, y: 5, z: 7 }) //> Result.Ok {x:3, y:5}
Decoder.decode(point, { x: 3, y: 5.2 }) //> Result.Error Expecting an Integer at input["y"] but instead got: `5.2`

Nested value decoders

Decoder.field <a> (string, Decoder.Decoder<a>):Decoder.Decoder<a>

Creates a decoder JSON object property decoder, where property matching a provided name is decoded via provided decoder:

Decoder.decode(Decoder.field("x", Decoder.Integer), { x: 3 }) //> Result.Ok 3
Decoder.decode(Decoder.field("x", Decoder.Integer), { x: 3, y: 4 }) //> Result.Ok 3
Decoder.decode(Decoder.field("x", Decoder.Integer), { x: true }) //>  Result.Error Expecting an Integer at input["x"] but instead got: `true`
Decoder.decode(Decoder.field("x", Decoder.Integer), { y: 4 }) //>   Result.Error Expecting an object with a field named 'x' but instead got: `{"y":4}`
Decoder.decode(Decoder.field("x", Decoder.Integer), "x=3") //> Result.Error Expecting an object with a field named 'x' but instead got: `"x=3"`
Decoder.decode(Decoder.field("name", Decoder.String), { name: "Tom" }) //> Result.Ok "Tom"

Note that object can have other fields. Lots of them! The only thing this decoder cares about is if x is present and that the value there is an Integer.

Decoder.at <a> (string[], Decoder.Decoder<a>):Decoder.Decoder<a>

Creates a decode for a nested JSON object property:

const profile = { person: { name: "Tom", age: 42 } }
Decoder.decode(Decoder.at(["person", "name"], Decoder.String), profile) //> Result.Ok "Tom"
Decoder.decode(Decoder.at(["person", "age"], Decoder.Integer), profile) //> Result.Ok 42

This is really just a shorthand for saying things like:

Decoder.decode(
  Decoder.field("person", Decoder.field("age", Decoder.Integer)),
  profile
) //> Result.Ok 42

Decoder.index <a> (number, Decoder.Decoder<a>):Decoder.Decoder<a>

Creates a decoder JSON array element decoder, where provided number is an index for the element which is decoded via provided decoder:

const users = ["alice", "bob", "chuck"]
Decoder.decode(Decoder.index(0, Decoder.String), users) //> Result.Ok "alice"
Decoder.decode(Decoder.index(1, Decoder.String), users) //> Result.Ok "bob"
Decoder.decode(Decoder.index(2, Decoder.String), users) //> Result.Ok "chuck"
Decoder.decode(Decoder.index(3, Decoder.String), users) //>  Result.Error Expecting a longer (>=4) array but instead got: `["alice","bob","chuck"]`

Inconsistent structure decoders

Decoder.maybe <a> (Decoder.Decoder<a>):Decoder.Decoder<?a>

Creates decoder helpful for dealing with optional fields:

const tom = { name: "tom", age: 42 }
Decoder.decode(Decoder.maybe(Decoder.field("age", Decoder.Integer)), tom) //> Result.Ok 42
Decoder.decode(Decoder.maybe(Decoder.field("name", Decoder.Integer)), tom) //> Result.Ok null
Decoder.decode(Decoder.maybe(Decoder.field("height", Decoder.Float)), tom) //> Result.Ok null

Decoder.decode(Decoder.field("age", Decoder.maybe(Decoder.Integer)), tom) //> Result.Ok 42
Decoder.decode(Decoder.field("name", Decoder.maybe(Decoder.Integer)), tom) //> Result.Ok null
Decoder.decode(Decoder.field("height", Decoder.maybe(Decoder.Integer)), tom) //> Result.Error Expecting an object with a field named 'height' but instead got: `{"name":"tom","age":42}`

Notice the last example! Error says that object with a field named height is expected but passed object does not has one so it errors.

Point is, maybe will make exactly what it contains conditional. For optional fields, this means you probably want it outside a use of field or at.

Decoder.annul <a> (a):Decoder.Decoder<a>

Creates a decoder that decodes null as provided value. Decoding anything but null will error.

Decoder.decode(Decoder.annul(false), null) //> Result.Ok false
Decoder.decode(Decoder.annul(42), null) //> Result.Ok 42
Decoder.decode(Decoder.annul(42), 42) //> Result.Error Expecting a null but instead got: `42`
Decoder.decode(Decoder.annul(42), false) //> Result.Error Expecting a null but instead got: `false`

Decoder.either <a> (Decoder.Decoder<a>[]):Decoder<a>

Creates a decoder that tries provided decoders until one succeeds or all of them error. It is useful if the JSON may come in a couple of different formats. For example, say you want to read an array of strings, but some of the elements can be nulls.

const badName = Decoder.either(Decoder.String, Decoder.annul(""))
Decoder.decode(Decoder.array(badName), ["alice", "bob", null, "chuck"]) //> ["alice", "bob", "", "chuck"]

Why would someone generate JSON like this? Questions like this are not good for your health. The point is that you can use either to handle situations like this!

You could also use either to help version your data. Try the latest format, then a few older ones that you still support.

Decoder.ok <a> (a):Decoder.Decoder<a>

Creates a decoder that decodes to provided value regardless of what is it decoding.

Decoder.decode(Decoder.ok(42), true) //> Result.Ok 42
Decoder.decode(Decoder.ok(42), [1, 2, 3]) //> Result.Ok 42
Decoder.decode(Decoder.ok(42), "hello") //> Result.Ok 42

It is mostly useful in combination with either:

const name = Decoder.either(Decoder.field('username', Decoder.String),
                            Decoder.field('email', Decoder.String),
                            Decoder.ok('stranger'))
Decoder.decode(name, {username:"Jack"}) //> Result.Ok "Jack"
Decoder.decode(name, {email:"[email protected]"}) //> Result.Ok "[email protected]"
Decoder.decode(name, {}) //> Result.Ok "stranger"

Decoder.error <a> (string):Decoder.Decoder<a>

Creates a decoder that errors with provided message regardless of what is it decoding.

Decoder.decode(Decoder.error("Boom!"), true) //> Result.Error Boom!
Decoder.decode(Decoder.error("Boom!"), [1, 2, 3]) //> Result.Error Boom!
Decoder.decode(Decoder.error("Boom!"), "hello") //> Result.Error Boom!

It is useful in combination with either to provide more contectual error messages:

const phone = Decoder.either(
  Decoder.field('cell', Decoder.String),
  Decoder.field('home', Decoder.String),
  Decoder.error('No phone number'))

Decoder.decode(phone, {cell:"415-5588-0000", home:"415-8855-0000"}) //> Result.Ok "415-5588-0000"
Decoder.decode(phone, {home:"415-8855-0000"}) //> Result.Ok "415-8855-0000"
Decoder.decode(phone, {}) //> Result.Error No phone number

Run Decoders

Decoder.decode <a> (Decoder.Decoder<a>, json:mixed):Decoder.Result<a>

Runs given Decoder<a> on a given JSON value. Returns Result that either contains Decoder.Error if value can't be decoded with a given decoder or a Result.Ok<a>.

Decoder.parse <a> (Decoder.Decoder<a>, input:string):Decoder.Result<a>

Parses given input string into a JSON value and then runs given Decoder<a> on it. Returns Result with Result.Error<Decoder.ParseError> if the string is not well-formed JSON or Result.Error<Decoder.Error> if the value can't be decoded with a given Decoder<a>. If operation is successfull returns Result.Ok<a>.

Decoder.parse(Decoder.Boolean, "true") //> Result.Ok true
Decoder.parse(Decoder.Boolean, "42") //> Result.Error Expecting a Boolean but instead got: `42`
Decoder.parse(Decoder.Boolean, "{") //>  Result.Error Parse error: Unexpected end of JSON input
Decoder.parse(Decoder.field("a", Decoder.Integer), '{ "a": 42 }') //> Result.Ok 42

Non-JSON decoders

Library can also be used to extract typed data from arbitrary JS objects and all of the decoders covered will work. There some additional decoders that are specific to data extraction from non-JSON values.

Decoder.accessor <a> (string, Decoder.Decoder<a>):Decoder.Decoder<a>

Creates a decoder that decodes return value of the method that has name as passed string and on an object being decoded:

Decoder.decode(Decoder.accessor("cwd", Decoder.String), process) //> Result.Ok "/Users/gozala/Projects/decoder.flow"
Decoder.decode(Decoder.accessor("pwd", Decoder.String), process) //> Result.Error Expecting an object with a method named 'pwd' but instead got: `{/*...*/}`

Decoder.form <a:{}> (a):Decoder.Decoder<Decoder.Record<a>>

Creates a decoder for objects, where each field is decoded with a corresponding decoder over the passed object. Note that unlike Decoder.record each field is decoded from the object itself rather than same named field, there for fields of the result can be formed arbitrarily:

Decoder.decode(
  Decoder.form({
    title: Decoder.field("title", Decoder.String),
    cwd: Decoder.accessor("cwd", Decoder.String),
    architecture: Decoder.at(
      ["config", "variables", "host_arch"],
      Decoder.String
    ),
    heapUsed: Decoder.accessor(
      "memoryUsage",
      Decoder.field("heapUsed", Decoder.Integer)
    )
  }),
  process
) //> Result.Ok  Result.Ok {"title":"/usr/local/bin/node","cwd":"/Users/gozala/Projects/decoder.flow","architecture":"x64","heapUsed":52124520}

Install

npm install decoder.flow

Prior Art

decoder.flow's People

Contributors

gozala avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar

decoder.flow's Issues

Add a conditional branching combinator

Elm provides andThen : (a -> Decoder b) -> Decoder a -> Decoder b function to create decoders that depend on previous results. For example if you are creating versioned data, you might do something like this:

info : Decoder Info
info =
  field "version" int
    |> andThen infoHelp

infoHelp : Int -> Decoder Info
infoHelp version =
  case version of
    4 ->
      infoDecoder4

    3 ->
      infoDecoder3

    _ ->
      fail <|
        "Trying to decode info, but version "
        ++ toString version ++ " is not supported."

-- infoDecoder4 : Decoder Info
-- infoDecoder3 : Decoder Info

decoder.flow does not provide andThen as it would make serialization and transfer of decoders across the threads impossible. Although similar to how Decoder.record and Decoder.form provide a way to address same use cases as map, map2, ...map8 in Elm we could provide some solution to addressing andThen use cases like the one above. For instance match like combinator could be implemented:

const version = Decoder.field("version", Decoder.Integer)
Decoder.either(
  Decoder.when(version, Decoder.ok(4), infoDecoder4)
  Decoder.when(version, Decoder.ok(3), infoDecoder3)
  Decoder.error("Trying to decode info, but provided version isn't supported"))

Note that in comparison to andThen this is far more limited and even this example unlike original Elm code is unable to include encountered version in the error message, but it still might enable certain use cases that aren't possible today.

Primary limitation of this would be that unlike andThen result can't be carried over to the next decoder, but maybe that could be worked around like in the example below:

const versionedInfo = Decoder.form({
  version: Decoder.field("version", Decoder.Integer)
  info: Decoder.value
})

const infoHelp = Decoder.either(
  Decoder.record({ version: Decoder.ok(4), ok: infoDecoder4 }),
  Decoder.record({ version: Decoder.ok(3), ok: infoDecoder3 }),
  Decoder.record({ version: Decoder.Integer, error: Decoder.ok('Invalid version') }))

const info = Decoder.chain(versionedInfo, infoHelp)

P.S.: We currently have no Decoder.value nor Decoder.chain but they could be easily added.

Sequencing decoders

Hi @Gozala,

I've been wanting to use elm-like decoder in flow, and thank you so much for this wonderful library!

In the current version of this library, is there a way to sequentially combine decoders? The decoded Result type has a map which happens after the execution of a decode or parse. But I'm looking for something that does it during definition. The type signature mimics elm's definition:

Decoder.andThen( 
  fn: (T) => Decoder.Decoder<U>, 
  de: Decoder.Decoder<T> 
): Decoder.Decoder<U>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.