├── .gitattributes ├── .gitignore ├── src ├── Decode_AsResult_OfParseError.re ├── Decode.re ├── Decode_AsOption.re ├── Decode_NonEmptyList.re ├── Decode_AsResult_OfStringNel.re ├── Decode_ParseError.rei ├── Decode_ParseError.re ├── Decode_Base.re ├── Decode_AsOption.rei ├── Decode_AsResult_OfStringNel.rei └── Decode_AsResult_OfParseError.rei ├── .npmignore ├── website ├── sidebars.json ├── package.json ├── static │ └── css │ │ └── custom.css ├── pages │ └── en │ │ ├── index.js │ │ └── docs.js ├── README.md ├── core │ └── Footer.js ├── siteConfig.js └── i18n │ └── en.json ├── bsconfig.json ├── docs ├── installation.md ├── what-and-why.md ├── simple-example.md ├── return-types.md ├── working-with-errors.md ├── decoding-simple-values.md ├── decoding-optional-values.md ├── decoding-objects.md └── decoding-variants.md ├── LICENSE ├── .circleci └── config.yml ├── package.json ├── CONTRIBUTING.md ├── README.md ├── test ├── Decode_AsResult_OfCustom_test.re ├── Decode_AsResult_OfStringNel_test.re ├── utils │ └── Decode_TestSampleData.re ├── Decode_AsResult_OfParseError_test.re └── Decode_AsOption_test.re └── CHANGELOG.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.re linguist-language=Reason 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | */log 3 | .vscode 4 | 5 | # generated 6 | _build/ 7 | _opam/ 8 | test/output/ 9 | /lib/bs 10 | *.bs.js 11 | /dist 12 | .merlin 13 | .bsb.lock 14 | /website/build 15 | /coverage 16 | 17 | # os files 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /src/Decode_AsResult_OfParseError.re: -------------------------------------------------------------------------------- 1 | module NonEmptyList = Relude.NonEmpty.List; 2 | module ParseError = Decode_ParseError; 3 | 4 | [@ocaml.warning "-3"] 5 | module Result = 6 | ParseError.ResultOf({ 7 | type t = ParseError.base; 8 | let handle = t => t; 9 | }); 10 | 11 | include Decode_Base.Make(Result.TransformError, Result); 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # keep the tests and docs out of npm 2 | test 3 | /docs 4 | /website 5 | CONTRIBUTING.md 6 | CHANGELOG.md 7 | 8 | # ...plus all of the stuff from gitignore 9 | node_modules 10 | */log 11 | .vscode 12 | 13 | # generated 14 | _build/ 15 | _opam/ 16 | test/output/ 17 | /lib/bs 18 | *.bs.js 19 | /dist 20 | .merlin 21 | .bsb.lock 22 | /coverage 23 | 24 | # os files 25 | .DS_Store 26 | -------------------------------------------------------------------------------- /website/sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "Getting Started": [ 4 | "what-and-why", 5 | "installation", 6 | "simple-example", 7 | "return-types", 8 | "working-with-errors" 9 | ], 10 | "Decoding Guide": [ 11 | "decoding-simple-values", 12 | "decoding-optional-values", 13 | "decoding-objects", 14 | "decoding-variants" 15 | ], 16 | "API": ["result-of-parseerror"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "examples": "docusaurus-examples", 4 | "start": "docusaurus-start", 5 | "build": "docusaurus-build", 6 | "publish-gh-pages": "docusaurus-publish", 7 | "write-translations": "docusaurus-write-translations", 8 | "version": "docusaurus-version", 9 | "rename-version": "docusaurus-rename-version" 10 | }, 11 | "devDependencies": { 12 | "docusaurus": "^1.14.7", 13 | "reason-highlightjs": "^0.2.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Decode.re: -------------------------------------------------------------------------------- 1 | module Base = Decode_Base; 2 | 3 | [@deprecated "Extending the result type is deprecated"] 4 | module Make = Base.Make; 5 | 6 | module ParseError = Decode_ParseError; 7 | 8 | [@deprecated "Use Decode.ParseError instead"] 9 | module AsOption = Decode_AsOption; 10 | 11 | module AsResult = { 12 | module OfParseError = Decode_AsResult_OfParseError; 13 | 14 | [@deprecated "Use Decode.AsResult.OfParseError instead"] 15 | module OfStringNel = Decode_AsResult_OfStringNel; 16 | }; 17 | -------------------------------------------------------------------------------- /website/static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* your custom css */ 2 | 3 | .nav-footer .sitemap { 4 | padding: 0 10px; 5 | } 6 | 7 | .nav-footer .sitemap a { 8 | margin: 0; 9 | padding: 3px 0; 10 | } 11 | 12 | @media only screen and (min-device-width: 360px) and (max-device-width: 736px) { 13 | } 14 | 15 | @media only screen and (min-width: 1024px) { 16 | } 17 | 18 | @media only screen and (max-width: 1023px) { 19 | } 20 | 21 | @media only screen and (min-width: 1400px) { 22 | } 23 | 24 | @media only screen and (min-width: 1500px) { 25 | } 26 | -------------------------------------------------------------------------------- /src/Decode_AsOption.re: -------------------------------------------------------------------------------- 1 | [@ocaml.warning "-3"] 2 | module OptionTransform: 3 | Decode_ParseError.TransformError with type t('a) = option('a) = { 4 | type t('a) = option('a); 5 | let valErr = (_, _) => None; 6 | let arrErr = (_, opt) => opt; 7 | let missingFieldErr = _ => None; 8 | let objErr = (_, opt) => opt; 9 | let lazyAlt = (opt, fn) => 10 | switch (opt) { 11 | | Some(v) => Some(v) 12 | | None => fn() 13 | }; 14 | }; 15 | 16 | module DecodeAsOption = 17 | Decode_Base.Make(OptionTransform, Relude.Option.Monad); 18 | 19 | include DecodeAsOption; 20 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bs-decode", 3 | "bsc-flags": [], 4 | "bs-dependencies": ["bs-bastet", "relude"], 5 | "bs-dev-dependencies": ["@glennsl/bs-jest"], 6 | "sources": [ 7 | { 8 | "dir": "src", 9 | "subdirs": true 10 | }, 11 | { 12 | "dir": "./test", 13 | "type": "dev", 14 | "subdirs": true 15 | } 16 | ], 17 | "package-specs": { 18 | "module": "commonjs", 19 | "in-source": true 20 | }, 21 | "refmt": 3, 22 | "suffix": ".bs.js", 23 | "warnings": { 24 | "number": "+A-40-42", 25 | "error": "+A" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /website/pages/en/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require("react"); 9 | const Redirect = require("../../core/Redirect.js"); 10 | 11 | const siteConfig = require(process.cwd() + "/siteConfig.js"); 12 | 13 | class Index extends React.Component { 14 | render() { 15 | return ( 16 | 20 | ); 21 | } 22 | } 23 | 24 | module.exports = Index; 25 | -------------------------------------------------------------------------------- /website/pages/en/docs.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Copyright (c) 2017-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const React = require("react"); 10 | const Redirect = require("../../core/Redirect.js"); 11 | 12 | const siteConfig = require(process.cwd() + "/siteConfig.js"); 13 | 14 | class Docs extends React.Component { 15 | render() { 16 | return ( 17 | 21 | ); 22 | } 23 | } 24 | 25 | module.exports = Docs; 26 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: installation 3 | title: Installation 4 | --- 5 | 6 | **Install via npm:** 7 | 8 | ```sh 9 | npm install --save bs-decode 10 | ``` 11 | 12 | Note that `bs-decode` includes two `peerDependencies`. These are peers instead of regular dependencies to allow you to specify the version in one place so you're less likely to end up with conflicting versions nested in your `node_modules`. If you haven't already, you can install the peer dependencies like this: 13 | 14 | ```sh 15 | npm install --save relude bs-bastet 16 | ``` 17 | 18 | **Update your bsconfig.json** 19 | 20 | ```json 21 | "bs-dependencies": [ 22 | "bs-bastet", 23 | "bs-decode", 24 | "relude" 25 | ], 26 | ``` 27 | -------------------------------------------------------------------------------- /src/Decode_NonEmptyList.re: -------------------------------------------------------------------------------- 1 | /** 2 | * We re-export some of Relude.NonEmptyList to make it easier for downstream 3 | * projects to work with the output of `bs-decode` without explicitly adding 4 | * a dependency on Relude. 5 | * 6 | * Specifically, we add a module type, so that specific kinds of decoders can 7 | * include this definition of `NonEmptyList` in their rei files. 8 | */ 9 | module type Nel = { 10 | type t('a) = Relude.NonEmpty.List.t('a); 11 | let make: ('a, list('a)) => t('a); 12 | let pure: 'a => t('a); 13 | let cons: ('a, t('a)) => t('a); 14 | let map: ('a => 'b, t('a)) => t('b); 15 | let foldLeft: (('b, 'a) => 'b, 'b, t('a)) => 'b; 16 | let head: t('a) => 'a; 17 | let tail: t('a) => list('a); 18 | let toSequence: t('a) => list('a); 19 | }; 20 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | The documentation website for `bs-decode` was created with [Docusaurus](https://docusaurus.io/). 4 | 5 | ## Contribute Docs 6 | 7 | If some aspect of `bs-decode`'s documentation is incomplete or incorrect, please [open an issue](https://github.com/mlms13/bs-decode/issues) or edit the markdown files in the `/docs` folder in the root of the `bs-decode` project. 8 | 9 | ## Run the Website 10 | 11 | If you wish to make any significant edits to the docs website, you may want to generate the static site and serve it locally for testing. To do so, clone this repo, `cd` into the `/website` directory (where this README is located), and run: 12 | 13 | ```sh 14 | # Run with Yarn 15 | yarn start 16 | 17 | # ...or run with npm 18 | npm run start 19 | ``` 20 | -------------------------------------------------------------------------------- /website/core/Footer.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class Footer extends React.Component { 4 | docUrl(doc, language) { 5 | const baseUrl = this.props.config.baseUrl; 6 | return `${baseUrl}docs/${language ? `${language}/` : ''}${doc}`; 7 | } 8 | 9 | pageUrl(doc, language) { 10 | const baseUrl = this.props.config.baseUrl; 11 | return baseUrl + (language ? `${language}/` : '') + doc; 12 | } 13 | 14 | render() { 15 | return ( 16 | 25 | ); 26 | } 27 | } 28 | 29 | module.exports = Footer; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Michael Martin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10.15.1 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: npm run cleanbuild 38 | - run: npm run test 39 | - run: npm run coverage 40 | - store_artifacts: 41 | path: coverage 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bs-decode", 3 | "version": "1.2.0", 4 | "description": "Type-safe JSON decoding for ReasonML and OCaml", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "build": "bsb -make-world", 11 | "clean": "bsb -clean-world", 12 | "cleanbuild": "npm run clean && npm run build", 13 | "watch": "bsb -make-world -w", 14 | "test": "jest --coverage", 15 | "coverage": "cat ./coverage/lcov.info | coveralls" 16 | }, 17 | "homepage": "https://mlms13.github.io/bs-decode/docs", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/mlms13/bs-decode.git" 21 | }, 22 | "bugs": "https://github.com/mlms13/bs-decode/issues", 23 | "keywords": [ 24 | "reasonml", 25 | "bucklescript", 26 | "json", 27 | "decode", 28 | "validation", 29 | "result", 30 | "applicative" 31 | ], 32 | "author": "Michael Martin", 33 | "license": "MIT", 34 | "peerDependencies": { 35 | "bs-bastet": "^2.0.0", 36 | "relude": "^0.66.1" 37 | }, 38 | "devDependencies": { 39 | "@glennsl/bs-jest": "^0.7.0", 40 | "bs-bastet": "^2.0.0", 41 | "bs-platform": "^7.2.2", 42 | "coveralls": "^3.0.9", 43 | "relude": "^0.66.1" 44 | }, 45 | "jest": { 46 | "testPathIgnorePatterns": [ 47 | "./test/output", 48 | "_build", 49 | "_opam" 50 | ], 51 | "coveragePathIgnorePatterns": [ 52 | "./test" 53 | ], 54 | "testMatch": [ 55 | "**/test/*.js" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /website/siteConfig.js: -------------------------------------------------------------------------------- 1 | const reasonHighlightJs = require("reason-highlightjs"); 2 | 3 | const siteConfig = { 4 | title: 'bs-decode', 5 | tagline: 'Type-safe JSON decoding for ReasonML and OCaml', 6 | url: 'https://mlms13.github.io', 7 | baseUrl: '/bs-decode/', 8 | 9 | projectName: 'bs-decode', 10 | organizationName: 'mlms13', 11 | 12 | // For no header links in the top nav bar -> headerLinks: [], 13 | headerLinks: [ 14 | { doc: 'what-and-why', label: 'Docs' }, 15 | { href: 'https://github.com/mlms13/bs-decode', label: 'GitHub' }, 16 | ], 17 | 18 | users: [ 19 | ], 20 | 21 | /* path to images for header/footer */ 22 | // headerIcon: 'img/docusaurus.svg', 23 | footerIcon: 'img/docusaurus.svg', 24 | favicon: 'img/favicon.png', 25 | 26 | /* Colors for website */ 27 | colors: { 28 | primaryColor: '#2299bb', 29 | secondaryColor: '#1188aa', 30 | }, 31 | 32 | copyright: `Copyright © ${new Date().getFullYear()} Michael Martin`, 33 | 34 | highlight: { 35 | 36 | theme: "atom-one-light", 37 | hljs: function (hljs) { 38 | hljs.registerLanguage("reason", reasonHighlightJs); 39 | } 40 | }, 41 | 42 | scripts: [], 43 | 44 | // On page navigation for the current documentation page. 45 | onPageNav: 'separate', 46 | cleanUrl: true, 47 | 48 | // Open Graph and Twitter card images. 49 | ogImage: null, //'img/docusaurus.png', 50 | twitterImage: null, //'img/docusaurus.png', 51 | 52 | repoUrl: 'https://github.com/mlms13/bs-decode', 53 | }; 54 | 55 | module.exports = siteConfig; 56 | -------------------------------------------------------------------------------- /website/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "This file is auto-generated by write-translations.js", 3 | "localized-strings": { 4 | "next": "Next", 5 | "previous": "Previous", 6 | "tagline": "Type-safe JSON decoding for ReasonML and OCaml", 7 | "docs": { 8 | "decoding-objects": { 9 | "title": "Decoding Object Fields" 10 | }, 11 | "decoding-optional-values": { 12 | "title": "Optional Values and Recovery" 13 | }, 14 | "decoding-simple-values": { 15 | "title": "Simple Values" 16 | }, 17 | "decoding-variants": { 18 | "title": "Decoding Variants" 19 | }, 20 | "installation": { 21 | "title": "Installation" 22 | }, 23 | "return-types": { 24 | "title": "Return Types" 25 | }, 26 | "simple-example": { 27 | "title": "Simple Example" 28 | }, 29 | "what-and-why": { 30 | "title": "What & Why" 31 | }, 32 | "working-with-errors": { 33 | "title": "Working With Errors" 34 | } 35 | }, 36 | "links": { 37 | "Docs": "Docs", 38 | "GitHub": "GitHub" 39 | }, 40 | "categories": { 41 | "Getting Started": "Getting Started", 42 | "Decoding Guide": "Decoding Guide", 43 | "API": "API" 44 | } 45 | }, 46 | "pages-strings": { 47 | "Help Translate|recruit community translators for your project": "Help Translate", 48 | "Edit this Doc|recruitment message asking to edit the doc source": "Edit", 49 | "Translate this Doc|recruitment message asking to translate the docs": "Translate" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/what-and-why.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: what-and-why 3 | title: What & Why 4 | --- 5 | 6 | `bs-decode` gives developers in the Bucklescript world a way to decode JSON values into structured ReasonML and OCaml types. While popular alternatives already exist in the ecosystem, `bs-decode` differentiates itself by tracking **all** decoding failures (rather than failing on the first) and by avoiding runtime exceptions as a means of surfacing errors. 7 | 8 | `bs-decode` was heavily influenced by Elm's [Json.Decode](https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode) and [Decode Pipeline](https://package.elm-lang.org/packages/NoRedInk/elm-decode-pipeline/3.0.1/Json-Decode-Pipeline) libraries. 9 | 10 | ## Exception-free 11 | 12 | Exceptions are hard to work with. The compiler doesn't ensure that you've checked exhaustively for them, and they can't be stored as values or passed to functions once raised. 13 | 14 | Instead, `bs-decode` chooses to represent errors as data, in the form of `option` or Belt's Result type. By representing decode failures as data, the compiler can help ensure that you've handled failures before accessing the data, leading to more resilient programs and better user experiences. 15 | 16 | ## Complete Errors 17 | 18 | When decoding large objects or arrays of JSON data, you might encounter multiple failures. By collecting each failure instead of stopping once the first failure is reached, `bs-decode` gives you much more complete information for debugging. See [Working With Errors](working-with-errors.md) for examples of this. 19 | 20 | Additionally, having access to a complete, structured representation of errors could allow for creative recovery or representation of those failures. 21 | -------------------------------------------------------------------------------- /src/Decode_AsResult_OfStringNel.re: -------------------------------------------------------------------------------- 1 | open Relude.Globals; 2 | 3 | // we are intentionally aliasing this module here and including the alias in the 4 | // rei to make it easy for users to interact with NonEmptyLists without needing 5 | // to use Relude directly 6 | module NonEmptyList = NonEmpty.List; 7 | 8 | module ResultUtil = { 9 | module ParseError = Decode_ParseError; 10 | type t('a) = result('a, NonEmptyList.t(string)); 11 | 12 | let map = Result.map; 13 | 14 | let apply = (a, b) => 15 | switch (a, b) { 16 | | (Ok(f), Ok(v)) => Ok(f(v)) 17 | | (Ok(_), Error(v)) => Error(v) 18 | | (Error(v), Ok(_)) => Error(v) 19 | | (Error(xa), Error(xb)) => Error(NonEmptyList.concat(xa, xb)) 20 | }; 21 | 22 | let pure = Result.pure; 23 | let flat_map = Result.bind; 24 | 25 | [@ocaml.warning "-3"] 26 | module Transform: 27 | ParseError.TransformError with 28 | type t('a) = result('a, NonEmptyList.t(string)) = { 29 | type t('a) = result('a, NonEmptyList.t(string)); 30 | 31 | let pureErr = NonEmptyList.pure >> Result.error; 32 | let mapErr = fn => Result.bimap(id, NonEmptyList.map(fn)); 33 | let valErr = (v, json) => pureErr(ParseError.failureToString(v, json)); 34 | 35 | let arrErr = (pos, v) => 36 | mapErr( 37 | x => 38 | "While decoding array, at position " 39 | ++ String.fromInt(pos) 40 | ++ ": " 41 | ++ x, 42 | v, 43 | ); 44 | 45 | let missingFieldErr = field => 46 | pureErr("Object field \"" ++ field ++ "\" was missing"); 47 | 48 | let objErr = field => 49 | mapErr(x => 50 | "While decoding object, for field \"" ++ field ++ "\": " ++ x 51 | ); 52 | 53 | let lazyAlt = (res, fn) => Result.fold(_ => fn(), Result.ok, res); 54 | }; 55 | }; 56 | 57 | include Decode_Base.Make(ResultUtil.Transform, ResultUtil); 58 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Getting Started 2 | 3 | 1. Fork [this repo on Github](https://github.com/mlms13/bs-decode) and clone it locally 4 | 2. `npm install` to grab dependencies 5 | 3. `npm run cleanbuild && npm run test` to build and run tests 6 | 4. Make your changes (and add appropriate tests) 7 | 5. [Open a pull request](https://help.github.com/en/articles/about-pull-requests) with your changes 8 | 9 | ### Code style and conventions 10 | 11 | We use [Relude](https://github.com/reazen/relude/) as our standard library, so try to avoid using `Belt` or `Pervasives` when working with lists, options, etc. 12 | 13 | To make the code easier to understand for other new contributors, we've tried to minimize our use of infix functions. Using `>>` for forward composition is fine, though. 14 | 15 | Also, please make sure that your code has been formatted with [`refmt`](https://reasonml.github.io/docs/en/editor-plugins). 16 | 17 | ### Project structure 18 | 19 | - `Decode_Base.re` is where all of the decoders are defined 20 | - `Decode_As*.re` modules construct `Decode_Base` with everything it needs to produce decoders for a specific output type (e.g. `option`, `result`) 21 | - `Decode_ParseError.re` defines the structured errors and helper functions to work with `result` values of that error type 22 | 23 | ### Tests 24 | 25 | `bs-decode` currently has 100% test coverage, and we hope to keep it that way. :) Running `npm run test` (or `jest --coverage`) will run your tests and give you a coverage report. You can see the detailed report in `./coverage/lcov-report/index.html`, which will help you track down any uncovered functions or branches. 26 | 27 | ### Documentation 28 | 29 | The documentation website is currently generated with [Docusaurus](https://docusaurus.io/). For more information on contributing to, running, and publishing the website, see [the README in the website folder](https://github.com/mlms13/bs-decode/blob/master/website/README.md). 30 | 31 | Separately from the website, we maintain `*.rei` interface files that provide type hints and doc comments to editors. When adding new functionality, make sure you update the appropriate interface files with type signatures and comments. 32 | -------------------------------------------------------------------------------- /docs/simple-example.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: simple-example 3 | title: Simple Example 4 | --- 5 | 6 | Let's explore the basics of decoding with a simple User record type: 7 | 8 | ```reasonml 9 | type user = { 10 | name: string, 11 | age: int, 12 | isAdmin: bool, 13 | lastLogin: option(Js.Date.t) 14 | }; 15 | ``` 16 | 17 | Next we'll build a decode function for our `user` type. `bs-decode` offers a few ways to decode JSON objects. For this example, we'll pick the "Pipeline" approach. 18 | 19 | ```reasonml 20 | // start with a simple constructor function for our type 21 | let make = (name, age, isAdmin, lastLogin) => 22 | { name, age, isAdmin, lastLogin }; 23 | 24 | // now we build up a decoder 25 | let decode = json => 26 | Decode.AsResult.OfParseError.Pipeline.( 27 | succeed(make) 28 | |> field("name", string) 29 | |> field("age", intFromNumber) 30 | |> field("isAdmin", boolean) 31 | |> optionalField("lastLogin", date) 32 | |> run(json) 33 | ); 34 | ``` 35 | 36 | Finally, imagine we have the following JSON value: 37 | 38 | ```reasonml 39 | let obj: Js.Json.t = [%bs.raw {| 40 | { 41 | "name": "Michael", 42 | "age": 32, 43 | "isAdmin": true 44 | } 45 | |}]; 46 | ``` 47 | 48 | We can decode this JSON object: 49 | 50 | ```reasonml 51 | decode(obj); // Ok({ name: "Michael", ...}) 52 | ``` 53 | 54 | Hopefully this is enough to get you started. You can learn more about [decoding primitive values](decoding-simple-values.md) and other techniques for [decoding objects](decoding-objects.md). If you want to [decode custom variants](decoding-variants.md) directly from the JSON, that's a bit more involved but definitely possible. 55 | 56 | Many of the examples will locally open (or alias) `Decode.AsResult.OfParseError`. The full module path of that decoder is a mouthful, but [other options are available](return-types.md) if you'd rather decode into an Option or a Result with string errors. 57 | 58 | Finally, we only demonstrated here what happens when you attempt to decode good JSON values. When you use the decoder we built above to decode bad JSON, you'll get back a `Belt.Result.Error` containing a structured error type. You can read more about [working with errors](working-with-errors.md). 59 | -------------------------------------------------------------------------------- /src/Decode_ParseError.rei: -------------------------------------------------------------------------------- 1 | type base = [ 2 | | `ExpectedNull 3 | | `ExpectedBoolean 4 | | `ExpectedString 5 | | `ExpectedNumber 6 | | `ExpectedInt 7 | | `ExpectedArray 8 | | `ExpectedTuple(int) 9 | | `ExpectedObject 10 | | `ExpectedValidDate 11 | | `ExpectedValidOption 12 | ]; 13 | 14 | type t('a) = 15 | | Val('a, Js.Json.t) 16 | | TriedMultiple(Relude.NonEmpty.List.t(t('a))) 17 | | Arr(Relude.NonEmpty.List.t((int, t('a)))) 18 | | Obj(Relude.NonEmpty.List.t((string, objError('a)))) 19 | and objError('a) = 20 | | MissingField 21 | | InvalidField(t('a)); 22 | 23 | type failure = t(base); 24 | 25 | [@deprecated "Extending the result type is deprecated"] 26 | module type TransformError = { 27 | type t('a); 28 | let valErr: (base, Js.Json.t) => t('a); 29 | let arrErr: (int, t('a)) => t('a); 30 | let missingFieldErr: string => t('a); 31 | let objErr: (string, t('a)) => t('a); 32 | let lazyAlt: (t('a), unit => t('a)) => t('a); 33 | }; 34 | 35 | let arrPure: (int, t('a)) => t('a); 36 | let objPure: (string, objError('a)) => t('a); 37 | let combine: (t('a), t('a)) => t('a); 38 | 39 | let failureToString: (base, Js.Json.t) => string; 40 | 41 | let toDebugString: 42 | (~level: int=?, ~pre: string=?, ('a, Js.Json.t) => string, t('a)) => string; 43 | 44 | let failureToDebugString: t(base) => string; 45 | 46 | [@deprecated "Extending the result type is deprecated"] 47 | module type ValError = { 48 | type t; 49 | let handle: base => t; 50 | }; 51 | 52 | [@deprecated "Extending the result type is deprecated"] 53 | [@ocaml.warning "-3"] 54 | module ResultOf: 55 | (Err: ValError) => 56 | { 57 | type error = t(Err.t); 58 | type nonrec t('a) = result('a, error); 59 | let map: ('a => 'b, t('a)) => t('b); 60 | let apply: (t('a => 'b), t('a)) => t('b); 61 | let pure: 'a => t('a); 62 | let flat_map: (t('a), 'a => t('b)) => t('b); 63 | 64 | module TransformError: { 65 | type nonrec t('a) = result('a, error); 66 | let valErr: (base, Js.Json.t) => t('a); 67 | let arrErr: (int, t('a)) => t('a); 68 | let missingFieldErr: string => t('a); 69 | let objErr: (string, t('a)) => t('a); 70 | let lazyAlt: (t('a), unit => t('a)) => t('a); 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /docs/return-types.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: return-types 3 | title: Return Types 4 | --- 5 | 6 | `bs-decode` provides decode functions that will return values wrapped in either an `option` or a `result`. Many of these error handling ideas were inspired by [Composable Error Handling in OCaml](http://keleshev.com/composable-error-handling-in-ocaml), which is a great read that goes into more detail than our summary here. 7 | 8 | - `Decode.AsResult.OfParseError`: all functions return `Ok(value)` for success, or `Error(Decode.ParseError.t)` for failures 9 | - `Decode.AsResult.OfStringNel`: all functions return `Ok(value)` for success, or `Error(NonEmptyList.t(string))` for failures 10 | - `Decode.AsOption`: all functions return `Some(value)` for success or `None` for failures 11 | 12 | ## Result of ParseError 13 | 14 | While it's possible to choose other error representations when using `bs-decode`, this is the preferred choice for decoding, as it provides the richest error information. In a future release, this will be the only supported value for holding the decode output. 15 | 16 | For more information on how to work with the structured error data returned by these decoders, see [Working With Errors](working-with-errors.md). 17 | 18 | ## Option (Deprecated) 19 | 20 | If you prefer to work with `option` for the sake of simplicity, it's recommended that you still use `Decode.AsResult.OfParseError` and convert the `result` to `option` after running the decoder. 21 | 22 | ## Result of NonEmptyList String (Deprecated) 23 | 24 | Instead of providing structured, typed error information when a decoder fails, `Decode.AsResult.OfStringNel` returns a non-empty list of `string` error messages. The benefit is that string error messages are easy to read and easy to extend with your own validation error messages. However, the error messages provided aren't as rich in information as the errors when using `Decode.AsResult.OfParseError`, and if your goal is simply to read the output, it's better to use those decoders and convert the error messages to a string using `Decode.ParseError.failureToDebugString`. 25 | 26 | We're hoping to reduce the need for custom error messages (the biggest benefit of using this set of decoders), and maintaining multiple types of `result` decoders adds quite a bit of complexity to the `bs-decode` library, so this set of decoders will likely be removed in a future release. 27 | -------------------------------------------------------------------------------- /docs/working-with-errors.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: working-with-errors 3 | title: Working With Errors 4 | --- 5 | 6 | ## The Error Type 7 | 8 | When using `Decode.AsResult.OfParseError`, each decoder can fail with a Result Error that holds a variant value representing the structured decode error. 9 | 10 | The underlying/primitive decode errors look like: 11 | 12 | ```reasonml 13 | type failure = [ 14 | | `ExpectedBoolean 15 | | `ExpectedString 16 | | `ExpectedNumber 17 | | `ExpectedInt 18 | | `ExpectedArray 19 | | `ExpectedTuple(int) // expected size of 20 | | `ExpectedObject 21 | | `ExpectedValidDate 22 | | `ExpectedValidOption 23 | ] 24 | ``` 25 | 26 | The full error structure (where the base `Val` errors hold the failure type above) looks like: 27 | 28 | ```reasonml 29 | type t = 30 | | Val(failure, Js.Json.t) 31 | | TriedMultiple(NonEmptyList.t(t)) 32 | | Arr(NonEmptyList.t((int, t))) 33 | | Obj(NonEmptyList.t((string, objError))) 34 | and objError = 35 | | MissingField 36 | | InvalidField(t); 37 | ``` 38 | 39 | Ultimately, this is saying that a `ParseError` will be either a `Val` error, a `TriedMultiple` error, an `Arr` error, or an `Obj` error. 40 | 41 | - `Val` errors contain one of the primitive errors defined above and the JSON that it failed to decode 42 | - `TriedMultiple` happens when multiple decoders were attempted (e.g. using `alt`) and it contains a non-empty list of other errors 43 | - `Arr` errors hold a non-empty list of position/error pairs 44 | - `Obj` errors hold a non-empty list of fieldname/object-error pairs 45 | - Object errors happen either because the field is a `MissingField`, or 46 | - the field exists but its value couldn't be decoded (`InvalidField` which recursively contains any of the above errors) 47 | 48 | This error structure can be destructured with recursive pattern matching. 49 | 50 | ## Logging 51 | 52 | One of the most common reasons to pattern match on the error structure is to log what went wrong while decoding. This is a common enough use case that helpers are provided to do just that: 53 | 54 | ```reasonml 55 | switch (decode(json)) { 56 | | Ok(v) => ... // do something with your successful value 57 | | Error(err) => 58 | Js.log(Decode.ParseError.failureToDebugString(err)) 59 | }; 60 | ``` 61 | 62 | The actual output of `failureToDebugString` will depend on the decoder and the JSON value you pass to it, but the string output of that function could look something like: 63 | 64 | ```sh 65 | Failed to decode array: 66 | At position 3: Failed to decode object: 67 | Field "foo" is required but was not present 68 | Field "bar" had an invalid value: Failed to decode array: 69 | At position 0: Expected int but found [] 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/decoding-simple-values.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: decoding-simple-values 3 | title: Simple Values 4 | --- 5 | 6 | The following decoders are provided out-of-the-box for decoding values from JSON. To keep things terse, we'll assume that `Decode.ParseError` is open in the local scope and we've aliased the decode module as `module D = Decode.AsResult.OfParseError`. 7 | 8 | Note that the output of our decode functions is wrapped in a `result` because we're using `Decode.AsResult....`. You could instead use `Decode.AsOption`, which would decode successes as `Some(value)` and errors as `None`. 9 | 10 | **String** 11 | 12 | ```reasonml 13 | let json = Js.Json.string("foo"); 14 | D.string(json); // Ok("foo") 15 | 16 | let json = Js.Json.number(3.14); 17 | D.string(json); // Error(Val(`ExpectedString, json)) 18 | ``` 19 | 20 | **Int and Float** 21 | 22 | JSON numbers can be decoded as either floats or ints. These decoders are suffixed with `...FromNumber` to allow you to open locally without getting compiler complaints about shadowing `float` from OCaml's Pervasives. 23 | 24 | ```reasonml 25 | let json = Js.Json.number(3.); 26 | D.floatFromNumber(json); // Ok(3.0) 27 | D.intFromNumber(json); // Ok(3) 28 | 29 | let json = Js.Json.number(3.14); 30 | D.floatFromNumber(json); // Ok(3.14) 31 | D.intFromNumber(json); // Error(`ExpectedInt, json) 32 | ``` 33 | 34 | Note that `intFromNumber` will reject numbers with fractional parts (rather than rounding up or down). If you don't want this behavior, you can use `floatFromNumber`, then `map` the Result through `int_of_float`, which will drop the fractional part. 35 | 36 | **Boolean** 37 | 38 | ```reasonml 39 | let json = Js.Json.boolean(true); 40 | D.boolean(json); // Ok(true) 41 | ``` 42 | 43 | **Lists and Arrays** 44 | 45 | JSON arrays can be decoded into either arrays or lists. Decoding a JSON array requires you to also pass a decoder for the inner type. 46 | 47 | ```reasonml 48 | let json = Js.Json.array([| Js.Json.string("a") |]); 49 | 50 | D.array(D.string, json); // Ok([| "a" |]) 51 | D.list(D.string, json); // Ok([ "a" ]) 52 | 53 | // Error(Arr(NonEmptyList.pure((0, Val(`ExpectedInt, Js.Json.string("")))))) 54 | D.list(D.intFromNumber, json); 55 | ``` 56 | 57 | **Date** 58 | 59 | JSON doesn't natively support dates, but `bs-decode` provides decoders that will try to build a date from a JSON float or from a JSON string that can be understood by `new Date` in JavaScript. 60 | 61 | ```reasonml 62 | let json = Js.Json.string("2018-11-17T05:40:35.869Z"); 63 | D.date(json); // Ok(Js.Date.fromString("2018-11-17T05:40:35.869Z")) 64 | 65 | let json = Js.Json.number(1542433304450.0); 66 | D.date(json); // Ok(Js.Date.fromFloat(1542433304450.0)); 67 | ``` 68 | 69 | While the `Js.Date.from*` functions in the comments above could return invalid Date objects, `bs-decode` will reject invalid dates as Result errors. 70 | 71 | **Dict** 72 | 73 | JSON objects can also be used to store key/value pairs, where the keys are strings. This can be represented as a `Js.Dict.t('a)` in ReasonML. 74 | 75 | ```reasonml 76 | // Assume `json` is a value that looks like: 77 | // { "foo": 3, "bar": 2 } 78 | 79 | // Ok(Js.Dict.fromList([("foo", 3), ("bar", 2)])) 80 | D.dict(D.intFromNumber, json); 81 | ``` 82 | -------------------------------------------------------------------------------- /docs/decoding-optional-values.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: decoding-optional-values 3 | title: Optional Values and Recovery 4 | --- 5 | 6 | Sometimes, you may need to decode nullable JSON values, or JSON values that can be one of several different types. `bs-decode` provides several functions to help you in these cases. 7 | 8 | **Optional Values** 9 | 10 | Simple decoders can be wrapped in `D.optional` to allow them to tolerate `null` values and decode into `option('a)` rather than `'a`. 11 | 12 | ```reasonml 13 | let jsonNull = Js.Json.null; 14 | let jsonStr = Js.Json.string("foo"); 15 | 16 | Decode.(optional(string, jsonNull)); // Ok(None) 17 | Decode.(optional(string, jsonStr)); // Ok(Some("foo")) 18 | Decode.(optional(boolean, jsonStr)); // Error(Val(`ExpectedBoolean, jsonStr)) 19 | ``` 20 | 21 | Note that unlike Elm's `Json.Decode`, optional values aren't atuomatically recovered as `None`. An optional `string` decoder will fail when given a JSON value that isn't a string. 22 | 23 | **Optional Fields** 24 | 25 | This gets into [decoding objects](decoding-objects.md), which is covered elsewhere. But it's important to be aware of the specialized `optionalField` function, in addition to the normal `optional` function. `optionalField` will tolerate both missing fields in a JSON object as well as present fields with `null` values, however like the normal `optional` function, this won't automatically recover from unexpected JSON. 26 | 27 | ```reasonml 28 | let json: Js.Json.t = [%bs.raw {| 29 | { 30 | "name": "Michael", 31 | "age": null 32 | } 33 | |}]; 34 | 35 | Decode.(optionalField("name", string, json)); // Ok(Some("Michael")) 36 | Decode.(optionalField("age", intFromNumber, json)); // Ok(None) 37 | Decode.(optionalField("isAdmin", boolean, json)); // Ok(None) 38 | Decode.(optionalField("name", boolean, json)); // Error(...) 39 | 40 | // compare with `optional` which is probably not what you want: 41 | 42 | // Error(Val(`ExpectedInt, ...)) 43 | Decode.(optional(field("age", intFromNumber), json)); 44 | 45 | // Error(Obj(NonEmptyList.pure(("isAdmin", MissingField)))) 46 | Decode.(field("isAdmin", optional(boolean), json)); 47 | ``` 48 | 49 | **Try Multiple Decoders** 50 | 51 | If a JSON value could be one of several types, you can try multiple decoders in order using `alt` or `oneOf`. Decoding will end successfully on the first success, or with a `TriedMultiple` error once all provided decoders have been attempted. 52 | 53 | Note that each attempt is evaluated lazily, so subsequent decoders will only be run if no success has been found yet. 54 | 55 | ```reasonml 56 | // each decoder in `oneOf` has to return the same type 57 | type t = 58 | | B(bool) 59 | | S(string) 60 | | I(int) 61 | | F(float); 62 | 63 | let json = Js.Json.string("foo"); 64 | 65 | // functions from `json => Result.t(t, ...)` 66 | let decodeB = Decode.(boolean |> map(v => B(v))); 67 | let decodeS = Decode.(string |> p(v => S(v))); 68 | let decodeI = Decode.(intFromNumber |> map(v => I(v))); 69 | let decodeF = Decode.(floatFromNumber |> map(v => F(v))); 70 | 71 | // here comes the part you actually care about 72 | 73 | Decode.oneOf(decodeB, [decodeS, decodeI], json); // Ok(S("foo")) 74 | Decode.oneOf(decodeB, [decodeI], json); // Error(Val(`ExpectedInt, ...)) 75 | ``` 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bs-decode 2 | 3 | [![build status](https://img.shields.io/circleci/build/github/mlms13/bs-decode.svg?style=flat-square)](https://circleci.com/gh/mlms13/bs-decode) 4 | [![test coverage](https://img.shields.io/coveralls/github/mlms13/bs-decode.svg?style=flat-square)](https://coveralls.io/github/mlms13/bs-decode) 5 | [![npm version](https://img.shields.io/npm/v/bs-decode.svg?style=flat-square)](https://www.npmjs.com/package/bs-decode) 6 | [![license](https://img.shields.io/github/license/mlms13/bs-decode.svg?style=flat-square)](https://github.com/mlms13/bs-decode/blob/master/LICENSE) 7 | 8 | > **Note** 9 | > 10 | > bs-decode has been stable and used in production for several years, so a v1 release makes sense. This is the final release that will be compatible with BuckleScript as we turn our attention to the newer OCaml features available in Melange. 11 | 12 | [Read the Documentation](https://mlms13.github.io/bs-decode/docs/) 13 | 14 | Decode JSON values into structured ReasonML and OCaml types. Inspired by Elm's [Json.Decode](https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode) and the [Decode Pipeline](https://package.elm-lang.org/packages/NoRedInk/elm-decode-pipeline/3.0.1/Json-Decode-Pipeline), `bs-decode` is an alternative to [bs-json](https://github.com/glennsl/bs-json) that focuses on structured, type-safe error handling, rather than exceptions. Additionally, `bs-decode` collects up _everything_ that went wrong while parsing the JSON, rather than failing on the first error. 15 | 16 | ## Installation 17 | 18 | **Install via npm:** 19 | 20 | `npm install --save bs-decode relude bs-bastet` 21 | 22 | **Update your bsconfig.json** 23 | 24 | ``` 25 | "bs-dependencies": [ 26 | "bs-bastet", 27 | "bs-decode", 28 | "relude" 29 | ], 30 | ``` 31 | 32 | 33 | ## Usage 34 | 35 | The following is available to give you an idea of how the library works, but [the complete documentation](https://mlms13.github.io/bs-decode/docs/simple-example) will probably be more useful if you want to write your own decoders. 36 | 37 | ```reason 38 | // imagine you have a `user` type and `make` function to construct one 39 | type user = { 40 | name: string, 41 | age: int, 42 | isAdmin: bool, 43 | lastLogin: option(Js.Date.t) 44 | }; 45 | 46 | let make = (name, age, isAdmin, lastLogin) => 47 | { name, age, isAdmin, lastLogin }; 48 | 49 | /** 50 | * Given a JSON value that looks like: 51 | * { "name": "Alice", "age": 44, "roles": ["admin"] } 52 | * 53 | * you can write a function to convert this JSON into a value of type `user` 54 | */ 55 | module Decode = Decode.AsResult.OfParseError; // module alias for brevity 56 | 57 | let decode = json => 58 | Decode.Pipeline.( 59 | succeed(make) 60 | |> field("name", string) 61 | |> field("age", intFromNumber) 62 | |> field("roles", list(string) |> map(List.contains("admin"))) 63 | |> optionalField("lastLogin", date) 64 | |> run(json) 65 | ); 66 | 67 | let myUser = decode(json); /* Ok({ name: "Alice", ...}) */ 68 | ``` 69 | 70 | ## Contributing 71 | 72 | All contributions are welcome! This obviously includes code changes and documentation improvements ([see CONTRIBUTING](https://github.com/mlms13/bs-decode/blob/master/CONTRIBUTING.md)), but we also appreciate any feedback you want to provide (in the form of [Github issues](https://github.com/mlms13/bs-decode/issues)) about concepts that are confusing or poorly explained in [the docs](https://mlms13.github.io/bs-decode/docs/what-and-why). 73 | 74 | ## License 75 | 76 | Released under the [MIT license](https://github.com/mlms13/bs-decode/blob/master/LICENSE). 77 | -------------------------------------------------------------------------------- /test/Decode_AsResult_OfCustom_test.re: -------------------------------------------------------------------------------- 1 | /** 2 | * This module demonstrates how to extend the base failure type with additional 3 | * constructors. 4 | */ 5 | open Jest; 6 | open Expect; 7 | open Relude.Globals; 8 | 9 | module ParseError = Decode_ParseError; 10 | module Sample = Decode_TestSampleData; 11 | 12 | type customError = [ ParseError.base | `InvalidColor | `InvalidShape]; 13 | 14 | [@ocaml.warning "-3"] 15 | module ResultCustom = 16 | Decode.ParseError.ResultOf({ 17 | type t = customError; 18 | let handle = x => (x :> t); 19 | }); 20 | 21 | [@ocaml.warning "-3"] 22 | module Decode = Decode.Make(ResultCustom.TransformError, ResultCustom); 23 | 24 | let toDebugString = (err, json) => 25 | switch (err) { 26 | | `InvalidColor => "Expected color but found " ++ Js.Json.stringify(json) 27 | | `InvalidShape => "Expected shape but found " ++ Js.Json.stringify(json) 28 | | #ParseError.base as e => ParseError.failureToString(e, json) 29 | }; 30 | 31 | // helper module to represent the color type and provie a simple decoder 32 | module Color = { 33 | type t = 34 | | Red 35 | | Green 36 | | Blue; 37 | 38 | let fromString = 39 | fun 40 | | "red" => Result.ok(Red) 41 | | "green" => Result.ok(Green) 42 | | "blue" => Result.ok(Blue) 43 | | str => 44 | Result.error(ParseError.Val(`InvalidColor, Js.Json.string(str))); 45 | 46 | let decode = 47 | Decode.( 48 | string |> map(String.toLowerCase) |> flatMap(fromString >> const) 49 | ); 50 | }; 51 | 52 | describe("Color (simple string validation)", () => { 53 | test("Color.decode success", () => 54 | expect(Color.decode(Sample.jsonStringBlue)) 55 | |> toEqual(Result.ok(Color.Blue)) 56 | ); 57 | 58 | test("Color.decode failure (invalid color string)", () => 59 | expect(Color.decode(Sample.jsonStringYellow)) 60 | |> toEqual( 61 | Result.error( 62 | ParseError.Val(`InvalidColor, Sample.jsonStringYellow), 63 | ), 64 | ) 65 | ); 66 | 67 | test("Color.decode failure (not a string)", () => 68 | expect(Color.decode(Sample.jsonNull)) 69 | |> toEqual( 70 | Result.error(ParseError.Val(`ExpectedString, Sample.jsonNull)), 71 | ) 72 | ); 73 | }); 74 | 75 | module Shape = { 76 | type t = 77 | | Rectangle(float, float) 78 | | Square(float) 79 | | Circle(float); 80 | 81 | let makeRectangle = (width, height) => Rectangle(width, height); 82 | let makeSquare = side => Square(side); 83 | let makeCircle = radius => Circle(radius); 84 | 85 | let fromKind = kind => 86 | switch (kind) { 87 | | "Rectangle" => 88 | Decode.( 89 | map2( 90 | makeRectangle, 91 | field("width", floatFromNumber), 92 | field("height", floatFromNumber), 93 | ) 94 | ) 95 | | "Square" => Decode.(field("side", floatFromNumber) |> map(makeSquare)) 96 | | "Circle" => 97 | Decode.(field("radius", floatFromNumber) |> map(makeCircle)) 98 | | _ => (json => Result.error(ParseError.Val(`InvalidShape, json))) 99 | }; 100 | 101 | let decode = Decode.(field("kind", string) |> flatMap(fromKind)); 102 | }; 103 | 104 | describe("Shape (complex object decoding)", () => { 105 | test("Shape.decode (success, rectangle)", () => 106 | expect(Shape.decode(Sample.jsonShapeRectangle)) 107 | |> toEqual(Result.ok(Shape.Rectangle(3.5, 7.0))) 108 | ); 109 | 110 | test("Shape.decode (success, square)", () => 111 | expect(Shape.decode(Sample.jsonShapeSquare)) 112 | |> toEqual(Result.ok(Shape.Square(4.0))) 113 | ); 114 | 115 | test("Shape.decode (success, circle)", () => 116 | expect(Shape.decode(Sample.jsonShapeCircle)) 117 | |> toEqual(Result.ok(Shape.Circle(2.0))) 118 | ); 119 | 120 | test("Shape.decode (failure, invalid kind)", () => 121 | expect(Shape.decode(Sample.jsonShapeInvalid)) 122 | |> toEqual( 123 | Result.error( 124 | ParseError.Val(`InvalidShape, Sample.jsonShapeInvalid), 125 | ), 126 | ) 127 | ); 128 | }); 129 | -------------------------------------------------------------------------------- /test/Decode_AsResult_OfStringNel_test.re: -------------------------------------------------------------------------------- 1 | open Jest; 2 | open Expect; 3 | open Relude.Globals; 4 | 5 | [@ocaml.warning "-3"] 6 | module Decode = Decode.AsResult.OfStringNel; 7 | module Sample = Decode_TestSampleData; 8 | module Nel = Decode.NonEmptyList; 9 | 10 | let makeErr = (msg, json) => 11 | Result.error(Nel.pure(msg ++ " " ++ Js.Json.stringify(json))); 12 | 13 | describe("Simple decode errors", () => { 14 | test("boolean", () => 15 | expect(Decode.boolean(Sample.jsonNull)) 16 | |> toEqual(makeErr("Expected boolean but found", Sample.jsonNull)) 17 | ); 18 | 19 | test("string", () => 20 | expect(Decode.string(Sample.jsonNull)) 21 | |> toEqual(makeErr("Expected string but found", Sample.jsonNull)) 22 | ); 23 | 24 | test("floatFromNumber", () => 25 | expect(Decode.floatFromNumber(Sample.jsonNull)) 26 | |> toEqual(makeErr("Expected number but found", Sample.jsonNull)) 27 | ); 28 | 29 | test("intFromNumber (non-number)", () => 30 | expect(Decode.intFromNumber(Sample.jsonNull)) 31 | |> toEqual(makeErr("Expected number but found", Sample.jsonNull)) 32 | ); 33 | 34 | test("intFromNumber (float)", () => 35 | expect(Decode.intFromNumber(Sample.jsonFloat)) 36 | |> toEqual(makeErr("Expected int but found", Sample.jsonFloat)) 37 | ); 38 | 39 | test("date", () => 40 | expect(Decode.date(Sample.jsonString)) 41 | |> toEqual(makeErr("Expected a valid date but found", Sample.jsonString)) 42 | ); 43 | 44 | test("variant", () => 45 | expect(Decode.variantFromString(Sample.colorFromJs, Sample.jsonString)) 46 | |> toEqual( 47 | makeErr("Expected a valid option but found", Sample.jsonString), 48 | ) 49 | ); 50 | 51 | test("array", () => 52 | expect(Decode.array(Decode.string, Sample.jsonNull)) 53 | |> toEqual(makeErr("Expected array but found", Sample.jsonNull)) 54 | ); 55 | 56 | test("tuple", () => 57 | expect(Decode.(tuple(string, boolean, Sample.jsonArrayEmpty))) 58 | |> toEqual( 59 | makeErr("Expected tuple of size 2 but found", Sample.jsonArrayEmpty), 60 | ) 61 | ); 62 | 63 | test("object", () => 64 | expect(Decode.field("x", Decode.string, Sample.jsonArrayEmpty)) 65 | |> toEqual(makeErr("Expected object but found", Sample.jsonArrayEmpty)) 66 | ); 67 | }); 68 | 69 | describe("Inner decoders", () => { 70 | test("array inner value (success)", () => 71 | expect(Decode.(array(string, Sample.jsonArrayString))) 72 | |> toEqual(Result.ok(Sample.valArrayString)) 73 | ); 74 | 75 | test("array inner value (failure)", () => 76 | expect(Decode.(array(boolean, Sample.jsonArrayString))) 77 | |> toEqual( 78 | Result.error( 79 | NonEmpty.List.make( 80 | "While decoding array, at position 0: Expected boolean but found \"A\"", 81 | [ 82 | "While decoding array, at position 1: Expected boolean but found \"B\"", 83 | "While decoding array, at position 2: Expected boolean but found \"C\"", 84 | ], 85 | ), 86 | ), 87 | ) 88 | ); 89 | 90 | test("field (missing)", () => 91 | expect(Decode.field("x", Decode.string, Sample.jsonDictEmpty)) 92 | |> toEqual( 93 | Result.error(NonEmpty.List.pure("Object field \"x\" was missing")), 94 | ) 95 | ); 96 | 97 | test("field (multiple, failure on second)", () => 98 | expect( 99 | Decode.( 100 | map4( 101 | Sample.makeJob, 102 | field("title", string), 103 | field("manager", string), 104 | field("startDate", date), 105 | pure(None), 106 | Sample.jsonJobCeo, 107 | ) 108 | ), 109 | ) 110 | |> toEqual( 111 | makeErr( 112 | "While decoding object, for field \"manager\": Expected string but found", 113 | Js.Json.null, 114 | ), 115 | ) 116 | ); 117 | 118 | test("oneOf", () => { 119 | let decodeUnion = 120 | Decode.( 121 | oneOf( 122 | map(Sample.unionS, string), 123 | [ 124 | map(Sample.unionN, optional(floatFromNumber)), 125 | map(Sample.unionB, boolean), 126 | ], 127 | ) 128 | ); 129 | 130 | expect(decodeUnion(Sample.jsonTrue)) 131 | |> toEqual(Result.ok(Sample.(B(valBool)))); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /docs/decoding-objects.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: decoding-objects 3 | title: Decoding Object Fields 4 | --- 5 | 6 | ## Decoding Fields 7 | 8 | Decoding values from a JSON object requires specifying the string key of the field to be decoded, as well as an inner decode function to parse the value. 9 | 10 | ```reasonml 11 | // imagine `json` is `{ "foo": "xyz", "bar": 4 }` 12 | 13 | // Ok("xyz") 14 | Decode.field("foo", Decode.string, json); 15 | 16 | // Ok(4) 17 | Decode.field("bar", Decode.intFromNumber, json); 18 | 19 | // Error(Obj(NonEmptyList.pure(("missing", MissingField)))) 20 | Decode.field("missing", Decode.intFromNumber, json); 21 | 22 | // Ok(None) 23 | Decode.optionalField("missing", Decode.intFromNumber, json); 24 | ``` 25 | 26 | ### Decoding Nested Fields 27 | 28 | If a value you need is nested deeply in a JSON structure and you don't want to decode all of the intermediate bits of JSON, you can use `at` to dig through multiple keys: 29 | 30 | ```reasonml 31 | // json looks like { "a": { "b": { "c": false }}} 32 | Decode.at(["a", "b", "c"], Decode.boolean); // Ok(false) 33 | 34 | // Error(Obj(("d", MissingField))) 35 | Decode.at(["a", "b", "c", "d"], Decode.boolean); 36 | ``` 37 | 38 | ## Building Records 39 | 40 | Back to that `user` type we defined in our [simple example](simple-example.md): 41 | 42 | ```reasonml 43 | type user = { 44 | name: string, 45 | age: int, 46 | isAdmin: bool, 47 | lastLogin: option(Js.Date.t) 48 | }; 49 | 50 | let make = (name, age, isAdmin, lastLogin) => 51 | { name, age, isAdmin, lastLogin }; 52 | ``` 53 | 54 | ...the real goal of `bs-decode` is to give you the tools you need to build up complex record types (like this one) from JSON. You can certainly use `field` and `optionalField` to piece these things together, but the following approaches will make your life easier. 55 | 56 | ### Let-Ops (Coming Soon) 57 | 58 | In the very near future, `bs-decode` will be switching from BuckleScript to Melange. This will give us access to newer OCaml features, such as [binding operators](https://v2.ocaml.org/manual/bindingops.html). This dramatically simplifies the syntax for decoding and constructing complex objects: 59 | 60 | ```reasonml 61 | let decode = { 62 | open Decode; 63 | let+ name = field("name", string) 64 | and+ age = field("age", intFromNumber) 65 | and+ isAdmin = field("isAdmin", boolean) 66 | and+ lastLogin = optionalField("lastLogin", date); 67 | User.make(name, age, isAdmin, lastLogin); 68 | }; 69 | ``` 70 | 71 | Once available, this will replace the "Pipeline" decoding (see below). Unlike the other strategies outlined below, the order of the field decoders doesn't matter. It's much easier to see how each field is used in the constructor, and it works with labeled functions and literal record construction. 72 | 73 | ### Haskell Validation Style 74 | 75 | It's also possible to use `map` and `apply` functions (often in their infix form `<$>` and `<*>`) to build up a larger decoder from smaller ones. This style my look more familiar if you've used validation libraries in Haskell. 76 | 77 | ```reasonml 78 | let ((<$>), (<*>)) = Decode.(map, apply); 79 | 80 | let decode = 81 | Decode.( 82 | User.make 83 | <$> field("name", string) 84 | <*> field("age", intFromNumber) 85 | <*> field("isAdmin", boolean) 86 | <*> optionalField("lastLogin", date) 87 | ); 88 | ``` 89 | 90 | ### Combining Decoders with `mapN` 91 | 92 | The provided `map2`...`map5` functions can be used to take the results of up to 5 decoders and combine them using a function that receives the up-to-5 values if all decoders succeed. This is simple to use, but obviously limiting if the record you want to construct has more than 5 fields. 93 | 94 | ```reasonml 95 | let decode = 96 | Decode.( 97 | map4( 98 | User.make, 99 | field("name", string), 100 | field("age", intFromNumber), 101 | field("isAdmin", boolean), 102 | optionalField("lastLogin", date), 103 | ) 104 | ); 105 | ``` 106 | 107 | ### Pipeline-Style (Deprecated) 108 | 109 | **Note:** This style of decoding has been deprecated and will be removed in v2.0. 110 | 111 | Given the `user` type above and its `make` function, you can build up a record by decoding each field in a style inspired by the [Elm Decode Pipeline](https://package.elm-lang.org/packages/NoRedInk/elm-decode-pipeline/3.0.1/) library for Elm. 112 | 113 | The order of decoded fields is significant, as the pipeline leverages the partial application of the `make` function. Each `field` or `optionalField` line in the example below fills in the next available slot in the `make` function. 114 | 115 | ```reasonml 116 | let decode = json => 117 | Decode.Pipeline.( 118 | succeed(make) 119 | |> field("name", string) 120 | |> field("age", intFromNumber) 121 | |> field("isAdmin", boolean) 122 | |> optionalField("lastLogin", date) 123 | |> run(json) 124 | ); 125 | ``` 126 | 127 | Unlike other decode functions we've looked at, the Pipeline style is not eager. Instead, nothing will be decoded until the whole pipeline is executed by using the `run` function with the appropriate JSON. 128 | -------------------------------------------------------------------------------- /test/utils/Decode_TestSampleData.re: -------------------------------------------------------------------------------- 1 | // Simple JSON values 2 | let jsonNull: Js.Json.t = [%raw {| null |}]; 3 | let jsonTrue: Js.Json.t = [%raw {| true |}]; 4 | let jsonFalse: Js.Json.t = [%raw {| false |}]; 5 | let jsonString: Js.Json.t = [%raw {| "string" |}]; 6 | let jsonStringTrue: Js.Json.t = [%raw {| "true" |}]; 7 | let jsonString4: Js.Json.t = [%raw {| "4" |}]; 8 | let jsonFloat: Js.Json.t = [%raw {| 3.14 |}]; 9 | let jsonInt: Js.Json.t = [%raw {| 1 |}]; 10 | let jsonLargeFloat: Js.Json.t = [%raw {|1542433304450|}]; 11 | let jsonIntZero: Js.Json.t = [%raw {| 0 |}]; 12 | let jsonDateNumber: Js.Json.t = [%raw {| 1542433304450.0 |}]; 13 | let jsonDateString: Js.Json.t = [%raw {| "2018-11-17T05:40:35.869Z" |}]; 14 | 15 | // Simple typed values 16 | let valBool = true; 17 | let valString = "string"; 18 | let valString4 = "4"; 19 | let valFloat = 3.14; 20 | let valInt = 1; 21 | let valIntZero = 0; 22 | let valDateNumber = Js.Date.fromFloat(1542433304450.0); 23 | let valDateString = Js.Date.fromString("2018-11-17T05:40:35.869Z"); 24 | 25 | // Nested JSON values 26 | let jsonArrayEmpty: Js.Json.t = [%raw {| [] |}]; 27 | let jsonArrayString: Js.Json.t = [%raw {| ["A", "B", "C"] |}]; 28 | let jsonArrayNested: Js.Json.t = [%raw {| [["a", "b"], [], ["c"]] |}]; 29 | let jsonTuple: Js.Json.t = [%raw {| ["A", true ] |}]; 30 | let jsonTuple3: Js.Json.t = [%raw {| ["A", true, 3 ] |}]; 31 | let jsonTuple4: Js.Json.t = [%raw {| ["A", true, false, "B" ] |}]; 32 | let jsonTuple5: Js.Json.t = [%raw {| ["A", "B", "C", "D", "E" ] |}]; 33 | let jsonTuple6: Js.Json.t = [%raw {| ["A", "B", "C", "D", "E", "F"] |}]; 34 | 35 | // Nested typed values 36 | let valArrayString = [|"A", "B", "C"|]; 37 | let valArrayNested = [|[|"a", "b"|], [||], [|"c"|]|]; 38 | let valListEmpty = []; 39 | let valListString = ["A", "B", "C"]; 40 | let valTuple = ("A", true); 41 | let valTuple3 = ("A", true, 3); 42 | let valTuple4 = ("A", true, false, "B"); 43 | let valTuple5 = ("A", "B", "C", "D", "E"); 44 | 45 | // JSON variant values 46 | let jsonStringBlue: Js.Json.t = [%raw {| "blue" |}]; 47 | let jsonStringYellow: Js.Json.t = [%raw {| "yellow" |}]; 48 | let jsonIntFive: Js.Json.t = [%raw {| 5 |}]; 49 | 50 | let jsonShapeSquare: Js.Json.t = [%raw {| { "kind": "Square", "side": 4 } |}]; 51 | let jsonShapeCircle: Js.Json.t = [%raw 52 | {| { "kind": "Circle", "radius": 2 } |} 53 | ]; 54 | let jsonShapeRectangle: Js.Json.t = [%raw 55 | {| 56 | { 57 | "kind": "Rectangle", 58 | "width": 3.5, 59 | "height": 7 60 | } 61 | |} 62 | ]; 63 | 64 | let jsonShapeInvalid: Js.Json.t = [%raw 65 | {| 66 | { 67 | "kind": "Line", 68 | "points": [ 69 | { "x": 4, "y": 0 }, 70 | { "x": 4, "y": 4 } 71 | ] 72 | } 73 | |} 74 | ]; 75 | 76 | // typed variants and converters 77 | [@bs.deriving jsConverter] 78 | type color = [ | `blue | `red | `green]; 79 | 80 | [@bs.deriving jsConverter] 81 | type numbers = 82 | | Zero 83 | | One 84 | | Two; 85 | 86 | // JSON object values 87 | let jsonDictEmpty: Js.Json.t = [%raw {| {} |}]; 88 | let jsonDictFloat: Js.Json.t = [%raw 89 | {| 90 | { 91 | "key1": 3.14, 92 | "key2": 2.22, 93 | "key3": 100.0 94 | } 95 | |} 96 | ]; 97 | 98 | let jsonJobCeo: Js.Json.t = [%raw 99 | {| 100 | { 101 | "title": "CEO", 102 | "companyName": "My Company", 103 | "startDate": "2016-04-01T00:00:00.0Z", 104 | "manager": null 105 | } 106 | |} 107 | ]; 108 | 109 | let jsonPersonBill: Js.Json.t = [%raw 110 | {| 111 | { 112 | "name": "Bill", 113 | "age": 27, 114 | "job": { 115 | "title": "Designer", 116 | "companyName": "My Company", 117 | "startDate": "2018-11-17T05:40:35.869Z", 118 | "manager": { 119 | "name": "Jane", 120 | "age": 38, 121 | "job": { 122 | "title": "CEO", 123 | "companyName": "My Company", 124 | "startDate": "2016-04-01T00:00:00.0Z", 125 | } 126 | } 127 | } 128 | } 129 | |} 130 | ]; 131 | 132 | // Typed dicts and records 133 | let valDictAsArray = [|("key1", 3.14), ("key2", 2.22), ("key3", 100.0)|]; 134 | let valDictEmpty: Js.Dict.t(string) = Js.Dict.empty(); 135 | let valDictFloat = Js.Dict.fromArray(valDictAsArray); 136 | let valMapFloat = Belt.Map.String.fromArray(valDictAsArray); 137 | 138 | type job = { 139 | title: string, 140 | companyName: string, 141 | startDate: Js.Date.t, 142 | manager: option(employee), 143 | } 144 | and employee = { 145 | name: string, 146 | age: int, 147 | job, 148 | }; 149 | 150 | let makeJob = (title, companyName, startDate, manager) => { 151 | title, 152 | companyName, 153 | startDate, 154 | manager, 155 | }; 156 | 157 | let makeEmployee = (name, age, job) => {name, age, job}; 158 | 159 | let jobCeo = 160 | makeJob( 161 | "CEO", 162 | "My Company", 163 | Js.Date.fromString("2016-04-01T00:00:00.0Z"), 164 | None, 165 | ); 166 | 167 | let employeeJane = makeEmployee("Jane", 38, jobCeo); 168 | 169 | let jobDesigner = 170 | makeJob("Designer", "My Company", valDateString, Some(employeeJane)); 171 | 172 | let employeeBill = makeEmployee("Bill", 27, jobDesigner); 173 | 174 | // Typed union for oneOf 175 | type union = 176 | | S(string) 177 | | N(option(float)) 178 | | B(bool); 179 | 180 | let unionS = v => S(v); 181 | let unionN = v => N(v); 182 | let unionB = v => B(v); 183 | -------------------------------------------------------------------------------- /src/Decode_ParseError.re: -------------------------------------------------------------------------------- 1 | open Relude.Globals; 2 | 3 | type base = [ 4 | | `ExpectedNull 5 | | `ExpectedBoolean 6 | | `ExpectedString 7 | | `ExpectedNumber 8 | | `ExpectedInt 9 | | `ExpectedArray 10 | | `ExpectedTuple(int) 11 | | `ExpectedObject 12 | | `ExpectedValidDate 13 | | `ExpectedValidOption 14 | ]; 15 | 16 | type t('a) = 17 | | Val('a, Js.Json.t) 18 | | TriedMultiple(Nel.t(t('a))) 19 | | Arr(Nel.t((int, t('a)))) 20 | | Obj(Nel.t((string, objError('a)))) 21 | and objError('a) = 22 | | MissingField 23 | | InvalidField(t('a)); 24 | 25 | type failure = t(base); 26 | 27 | let arrPure = (pos, err) => Arr(Nel.pure((pos, err))); 28 | let objPure = (field, err) => Obj(Nel.pure((field, err))); 29 | 30 | module type TransformError = { 31 | type t('a); 32 | let valErr: (base, Js.Json.t) => t('a); 33 | let arrErr: (int, t('a)) => t('a); 34 | let missingFieldErr: string => t('a); 35 | let objErr: (string, t('a)) => t('a); 36 | let lazyAlt: (t('a), unit => t('a)) => t('a); 37 | }; 38 | 39 | /* 40 | * This is almost like Semigroup's `append`, but associativity only holds when 41 | * both errors are `Arr(...)` or `Obj(...)`. In practice this is fine, because 42 | * those are the only times when you actually want to combine errors. 43 | */ 44 | let combine = (a, b) => 45 | switch (a, b) { 46 | | (Arr(xs), Arr(ys)) => Arr(Nel.concat(xs, ys)) 47 | | (Obj(xs), Obj(ys)) => Obj(Nel.concat(xs, ys)) 48 | | (TriedMultiple(xs), x) => TriedMultiple(Nel.(concat(xs, pure(x)))) 49 | | (Val(_), _) 50 | | (Arr(_), _) 51 | | (Obj(_), _) => a 52 | }; 53 | 54 | let makeTriedMultiple = 55 | fun 56 | | TriedMultiple(_) as x => x 57 | | Val(_) as err => TriedMultiple(Nel.pure(err)) 58 | | Arr(_) as err => TriedMultiple(Nel.pure(err)) 59 | | Obj(_) as err => TriedMultiple(Nel.pure(err)); 60 | 61 | let failureToPartialString = 62 | fun 63 | | `ExpectedNull => "Expected null" 64 | | `ExpectedBoolean => "Expected boolean" 65 | | `ExpectedString => "Expected string" 66 | | `ExpectedNumber => "Expected number" 67 | | `ExpectedInt => "Expected int" 68 | | `ExpectedArray => "Expected array" 69 | | `ExpectedTuple(size) => "Expected tuple of size " ++ Int.toString(size) 70 | | `ExpectedObject => "Expected object" 71 | | `ExpectedValidDate => "Expected a valid date" 72 | | `ExpectedValidOption => "Expected a valid option"; 73 | 74 | let failureToString = (v, json) => 75 | failureToPartialString(v) ++ " but found " ++ Js.Json.stringify(json); 76 | 77 | /** 78 | * Traverse the tree of errors and produce properly-indented error strings: 79 | * 80 | * Failed to decode array: 81 | * At position 0: Expected string but found 3.5 82 | * 83 | * Failed to decode array: 84 | * At position 3: Failed to decode object: 85 | * Field "foo" is required but was not present 86 | * Field "bar" had an invalid value: Failed to decode array: 87 | * At position 0: Expected int but found [] 88 | */ 89 | let rec toDebugString = (~level=0, ~pre="", innerToString, v) => { 90 | let spaces = indent => String.repeat(indent * 4, " "); 91 | 92 | let msg = 93 | switch (v) { 94 | | Val(x, json) => innerToString(x, json) 95 | | TriedMultiple(xs) => 96 | let childMessages = 97 | xs 98 | |> Nel.map(toDebugString(~level=level + 1, innerToString)) 99 | |> Nel.toSequence 100 | |> List.String.joinWith("\n"); 101 | 102 | "Attempted multiple decoders, which all failed:\n" ++ childMessages; 103 | 104 | | Arr(xs) => 105 | let childMessages = 106 | xs 107 | |> Nel.map(((i, err)) => 108 | toDebugString( 109 | ~level=level + 1, 110 | ~pre="At position " ++ string_of_int(i) ++ ": ", 111 | innerToString, 112 | err, 113 | ) 114 | ) 115 | |> Nel.toSequence 116 | |> List.String.joinWith("\n"); 117 | 118 | "Failed to decode array:\n" ++ childMessages; 119 | 120 | | Obj(nel) => 121 | let childMessages = 122 | nel 123 | |> Nel.map(((field, err)) => { 124 | let fieldStr = "\"" ++ field ++ "\""; 125 | switch (err) { 126 | | MissingField => 127 | spaces(level + 1) 128 | ++ "Field " 129 | ++ fieldStr 130 | ++ " is required, but was not present" 131 | | InvalidField(err) => 132 | toDebugString( 133 | ~level=level + 1, 134 | ~pre="Field " ++ fieldStr ++ " had an invalid value: ", 135 | innerToString, 136 | err, 137 | ) 138 | }; 139 | }) 140 | |> Nel.toSequence 141 | |> List.String.joinWith("\n"); 142 | 143 | "Failed to decode object:\n" ++ childMessages; 144 | }; 145 | 146 | spaces(level) ++ pre ++ msg; 147 | }; 148 | 149 | let failureToDebugString = err => toDebugString(failureToString, err); 150 | 151 | module type ValError = { 152 | type t; 153 | let handle: base => t; 154 | }; 155 | 156 | module ResultOf = (Err: ValError) => { 157 | type error = t(Err.t); 158 | type nonrec t('a) = result('a, error); 159 | let map = Result.map; 160 | let apply = (f, v) => 161 | switch (f, v) { 162 | | (Ok(fn), Ok(a)) => Result.ok(fn(a)) 163 | | (Ok(_), Error(_) as err) => err 164 | | (Error(_) as err, Ok(_)) => err 165 | | (Error(fnx), Error(ax)) => Result.error(combine(fnx, ax)) 166 | }; 167 | 168 | let pure = Result.pure; 169 | let flat_map = Result.bind; 170 | 171 | module TransformError: TransformError with type t('a) = result('a, error) = { 172 | type t('a) = result('a, error); 173 | 174 | let valErr = (v, json) => Result.error(Val(Err.handle(v), json)); 175 | let arrErr = pos => Result.mapError(arrPure(pos)); 176 | let missingFieldErr = field => 177 | Result.error(objPure(field, MissingField)); 178 | let objErr = field => 179 | Result.mapError(x => objPure(field, InvalidField(x))); 180 | let lazyAlt = (res, fn) => 181 | switch (res) { 182 | | Ok(_) as ok => ok 183 | | Error(x) => 184 | let err = makeTriedMultiple(x); 185 | Result.mapError(combine(err), fn()); 186 | }; 187 | }; 188 | }; 189 | -------------------------------------------------------------------------------- /docs/decoding-variants.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: decoding-variants 3 | title: Decoding Variants 4 | --- 5 | 6 | ## Challenges With Variants 7 | 8 | Many types in Reason have a 1-to-1 mapping to JSON value types (think strings, ints, floats, arrays, etc). This is not true of Reason's variant types. While it's certainly possible to store variant values in JSON, the exact representation will vary depending on the preferences of the person who encoded the value. 9 | 10 | At a high level, variant types in Reason are a way to represent "or" relationships in the type system. When decoding JSON, `alt` (and the related `oneOf`) allows you to express that a JSON value may have one representation "or" a different representation. This forms the foundation of decoding variants. We'll look at more specific examples below. 11 | 12 | ### Simple Variants (Enums) 13 | 14 | In their simplest form, Reason variant types are simply an enumeration of possible values, where each value carries no extra data. For example: 15 | 16 | ```reasonml 17 | type color = Blue | Red | Green; 18 | ``` 19 | 20 | While there are a number of ways you could represent values of this type in JSON, let's assume that the values are encoded as the string values `"blue"` or `"red"` or `"green"`. In this case, you can write a decoder for each variant constructor and then combine them together using `alt` to form a single decoder: 21 | 22 | ```reasonml 23 | let decodeBlue = Decode.(literalString("blue") |> map(_ => Blue)); 24 | let decodeRed = Decode.(literalString("red") |> map(_ => Red)); 25 | let decodeGreen = Decode.(literalString("green") |> map(_ => Green)); 26 | 27 | // decode by chaining together `alt` 28 | let decode = Decode.(decodeBlue |> alt(decodeRed) |> alt(decodeGreen)); 29 | 30 | // the same decoder, using `oneOf` 31 | let decode = Decode.oneOf(decodeBlue, [decodeRed, decodeGreen]); 32 | ``` 33 | 34 | The need to decode string literals from JSON into simple Reason variants is common enough that we provide a `stringUnion` decoder that allows you to simply express the relationship between strings and variant values as pairs: 35 | 36 | ```reasonml 37 | let decode = Decode.stringUnion( 38 | ("blue", Blue), 39 | [("red", Red), ("green", Green)] 40 | ); 41 | ``` 42 | 43 | Whether you choose to use `alt`, `oneOf`, or `stringUnion`, you could expect the following outcomes when running your decode function: 44 | 45 | ```reasonml 46 | // Ok(Blue) 47 | decode(Js.Json.string("blue")); 48 | 49 | // Error(Val(`ExpectedValidOption, ...)) 50 | decode(Js.Json.string("yellow")); 51 | ``` 52 | 53 | ### Complex Variants 54 | 55 | Not all variants are as simple as our `color` variant above. Often, each branch of a Reason variant could carry its own additional data. Consider the following: 56 | 57 | ```reasonml 58 | type shape = 59 | | Rectangle(float, float) 60 | | Square(float) 61 | | Circle(float); 62 | ``` 63 | 64 | There are countless ways this data could be represented in JSON, but let's assume the following valid representations: 65 | 66 | ```json 67 | [ 68 | { "type": "rectangle", "width": 2.5, "height": 4 }, 69 | { "type": "square", "side": 3 }, 70 | { "type": "circle", "radius": 1.8 } 71 | ] 72 | ``` 73 | 74 | There's a little more to it now, but the same general strategy from before will still work. We can write a decoder for each possible variant branch, then we combine each decoder together using `oneOf`: 75 | 76 | ```reasonml 77 | let decodeRectangle = Decode.( 78 | map3( 79 | (_, width, height) => Rectangle(width, height), 80 | field("type", literalString("rectangle")), 81 | field("width", floatFromNumber), 82 | field("height", floatFromNumber), 83 | ) 84 | ); 85 | 86 | let decodeSquare = Decode.( 87 | map2( 88 | (_, side) => Square(side), 89 | field("type", literalString("square")), 90 | field("side", floatFromNumber), 91 | ) 92 | ); 93 | 94 | let decodeCircle = Decode.( 95 | map2( 96 | (_, radius) => Circle(radius), 97 | field("type", literalString("circle")), 98 | field("radius", literalString("radius")), 99 | ) 100 | ); 101 | 102 | let decode = Decode.oneOf(decodeRectangle, [decodeSquare, decodeCircle]); 103 | ``` 104 | 105 | ## Extending Base Errors (Deprecated) 106 | 107 | **Note:** for complex variants, the above strategies involving `alt` already give you very detailed error information about each decoder that was attempted and failed. The downside of that approach is that `stringLiteral` decoders currently don't report back the specific string that the decoder was looking for. In the future, those errors will be improved, and the method of extending the error type itself (described below) will be removed. 108 | 109 | In some cases, `ExpectedValidOption` might not be enough debugging information. You may want custom handling (or simply, more specific error messages) when something goes wrong during decoding. 110 | 111 | You can get this by extending the underlying `Decode.ParseError.base` type with extra constructors. You use this extension to build your own custom `Decode.ParseError`, which in turn can be used to build a custom decode module on top of `Decode.Base`. 112 | 113 | This may sound overwhelming, but the whole thing can be accomplished in about 6 lines of code: 114 | 115 | ```reasonml 116 | module R = 117 | Decode.ParseError.ResultOf({ 118 | type t = [ Decode.ParseError.base | `InvalidColor | `InvalidShape]; 119 | let handle = x => (x :> t); 120 | }); 121 | 122 | module D = Decode.Base.Make(R.TransformError, R); 123 | ``` 124 | 125 | Now we have a `D` that is slightly different from the `Decode.AsResult.OfParseError` that we've been using up until now. This `D` can produce `Val` parse errors that can include `InvalidColor` or `InvalidShape` (in addition to the usual error cases). 126 | 127 | We can change the `parseColor` function we defined above to return a Result where the Error is of type `InvalidColor` and it includes the specific piece of JSON we were trying to parse, for detailed debugging: 128 | 129 | ```reasonml 130 | let parseColor = 131 | fun 132 | | "Blue" => Ok(Blue) 133 | | "Red" => Ok(Red) 134 | | "Green" => Ok(Green) 135 | | str => Error(Decode.ParseError.Val(`InvalidColor, Js.Json.string(str))); 136 | 137 | let decodeColor = json => D.string(json) |> Result.flatMap(parseColor); 138 | ``` 139 | 140 | Now we have a `decodeColor` function that produces the same output type as any other `decode` function we may write, but it includes error information specific to our `color` parsing. 141 | -------------------------------------------------------------------------------- /src/Decode_Base.re: -------------------------------------------------------------------------------- 1 | open Relude.Globals; 2 | open BsBastet.Interface; 3 | 4 | module ParseError = Decode_ParseError; 5 | 6 | [@ocaml.warning "-3"] 7 | module Make = 8 | (T: ParseError.TransformError, M: MONAD with type t('a) = T.t('a)) => { 9 | type t('a) = Js.Json.t => M.t('a); 10 | 11 | let map = (f, decode) => decode >> M.map(f); 12 | let apply = (f, decode, json) => M.apply(f(json), decode(json)); 13 | let pure = (v, _) => M.pure(v); 14 | let flat_map = (decode, f, json) => M.flat_map(decode(json), f(_, json)); 15 | let alt = (a, b, json) => T.lazyAlt(a(json), () => b(json)); 16 | 17 | include Relude.Extensions.Apply.ApplyExtensions({ 18 | type nonrec t('a) = t('a); 19 | let map = map; 20 | let apply = apply; 21 | }); 22 | 23 | let flatMap = (f, decode) => flat_map(decode, f); 24 | 25 | let oneOf = (decode, rest) => List.foldLeft(alt, decode, rest); 26 | 27 | let value = (decode, failure, json) => 28 | decode(json) |> Option.foldLazy(() => T.valErr(failure, json), M.pure); 29 | 30 | let okJson = M.pure; 31 | 32 | let null = value(Js.Json.decodeNull, `ExpectedNull) |> map(_ => ()); 33 | 34 | let boolean = value(Js.Json.decodeBoolean, `ExpectedBoolean); 35 | 36 | let string = value(Js.Json.decodeString, `ExpectedString); 37 | 38 | let floatFromNumber = value(Js.Json.decodeNumber, `ExpectedNumber); 39 | 40 | let intFromNumber = { 41 | let isInt = num => float(int_of_float(num)) == num; 42 | flatMap( 43 | v => isInt(v) ? pure(int_of_float(v)) : T.valErr(`ExpectedInt), 44 | floatFromNumber, 45 | ); 46 | }; 47 | 48 | let date = { 49 | let fromFloat = map(Js.Date.fromFloat, floatFromNumber); 50 | let fromString = map(Js.Date.fromString, string); 51 | let isValid = date => 52 | date 53 | |> Js.Date.toJSONUnsafe 54 | |> Js.Nullable.return 55 | |> Js.Nullable.isNullable 56 | ? T.valErr(`ExpectedValidDate) : pure(date); 57 | 58 | alt(fromFloat, fromString) |> flatMap(isValid); 59 | }; 60 | 61 | let literal = (eq, decode, a) => 62 | decode 63 | |> flatMap(v => eq(v, a) ? pure(a) : T.valErr(`ExpectedValidOption)); 64 | 65 | let literalString = literal((==), string); 66 | 67 | let literalInt = literal((==), intFromNumber); 68 | 69 | let literalFloat = literal((==), floatFromNumber); 70 | 71 | let literalBool = literal((==), boolean); 72 | 73 | let literalTrue = literalBool(true); 74 | 75 | let literalFalse = literalBool(false); 76 | 77 | let union = (decode, first, rest) => { 78 | let mkDecode = ((s, v)) => decode(s) |> map(_ => v); 79 | first |> mkDecode |> oneOf(_, rest |> List.map(mkDecode)); 80 | }; 81 | 82 | let stringUnion = first => union(literalString, first); 83 | 84 | let intUnion = first => union(literalInt, first); 85 | 86 | let variantFromJson = (jsonToJs, jsToVariant) => 87 | jsonToJs 88 | |> map(jsToVariant) 89 | |> flatMap(Option.foldLazy(() => T.valErr(`ExpectedValidOption), pure)); 90 | 91 | let variantFromString = (stringToVariant, json) => 92 | variantFromJson(string, stringToVariant, json); 93 | 94 | let variantFromInt = (intToVariant, json) => 95 | variantFromJson(intFromNumber, intToVariant, json); 96 | 97 | let optional = (decode, json) => 98 | switch (Js.Json.decodeNull(json)) { 99 | | Some(_) => pure(None, json) 100 | | None => map(v => Some(v), decode, json) 101 | }; 102 | 103 | let array = decode => { 104 | let map2 = (f, a, b) => M.map(f, a) |> M.apply(_, b); 105 | let decodeEach = (arr, _json) => 106 | Array.foldLeft( 107 | ((pos, acc), curr) => { 108 | let decoded = T.arrErr(pos, decode(curr)); 109 | let result = map2(flip(Array.append), acc, decoded); 110 | (pos + 1, result); 111 | }, 112 | (0, M.pure([||])), 113 | arr, 114 | ) 115 | |> snd; 116 | 117 | value(Js.Json.decodeArray, `ExpectedArray) |> flatMap(decodeEach); 118 | }; 119 | 120 | let arrayJson = array(okJson); 121 | 122 | let list = decode => array(decode) |> map(Array.toList); 123 | 124 | let listJson = list(okJson); 125 | 126 | let arrayAt = (position, decode) => 127 | array(okJson) 128 | |> flatMap((arr, json) => 129 | arr 130 | |> Array.at(position) 131 | |> Option.foldLazy( 132 | () => T.valErr(`ExpectedTuple(position + 1), json), 133 | decode, 134 | ) 135 | ); 136 | 137 | let tupleN = (size, extract, decode) => 138 | array(M.pure) 139 | |> map(extract) 140 | |> flatMap(Option.fold(T.valErr(`ExpectedTuple(size)), decode)); 141 | 142 | let tuple = (da, db) => 143 | tupleN(2, Tuple.fromArray, ((a, b)) => 144 | map2(Tuple.make, _ => T.arrErr(0, da(a)), _ => T.arrErr(1, db(b))) 145 | ); 146 | 147 | let tuple2 = tuple; 148 | 149 | let tuple3 = (da, db, dc) => 150 | tupleN(3, Tuple.fromArray3, ((a, b, c)) => 151 | map3(Tuple.make3, _ => da(a), _ => db(b), _ => dc(c)) 152 | ); 153 | 154 | let tuple4 = (da, db, dc, dd) => 155 | tupleN(4, Tuple.fromArray4, ((a, b, c, d)) => 156 | map4(Tuple.make4, _ => da(a), _ => db(b), _ => dc(c), _ => dd(d)) 157 | ); 158 | 159 | let tuple5 = (da, db, dc, dd, de) => 160 | tupleN(5, Tuple.fromArray5, ((a, b, c, d, e)) => 161 | map5( 162 | Tuple.make5, 163 | _ => da(a), 164 | _ => db(b), 165 | _ => dc(c), 166 | _ => dd(d), 167 | _ => de(e), 168 | ) 169 | ); 170 | 171 | let tupleAtLeast2 = (da, db) => 172 | tupleN(2, Tuple.fromArrayAtLeast2, ((a, b)) => 173 | map2(Tuple.make2, _ => da(a), _ => db(b)) 174 | ); 175 | 176 | let tupleAtLeast3 = (da, db, dc) => 177 | tupleN(3, Tuple.fromArrayAtLeast3, ((a, b, c)) => 178 | map3(Tuple.make3, _ => da(a), _ => db(b), _ => dc(c)) 179 | ); 180 | 181 | let tupleAtLeast4 = (da, db, dc, dd) => 182 | tupleN(4, Tuple.fromArrayAtLeast4, ((a, b, c, d)) => 183 | map4(Tuple.make4, _ => da(a), _ => db(b), _ => dc(c), _ => dd(d)) 184 | ); 185 | 186 | let tupleAtLeast5 = (da, db, dc, dd, de) => 187 | tupleN(5, Tuple.fromArrayAtLeast5, ((a, b, c, d, e)) => 188 | map5( 189 | Tuple.make5, 190 | _ => da(a), 191 | _ => db(b), 192 | _ => dc(c), 193 | _ => dd(d), 194 | _ => de(e), 195 | ) 196 | ); 197 | 198 | let dict = decode => { 199 | let rec decodeEntries = 200 | fun 201 | | [] => pure([]) 202 | | [(key, value), ...xs] => 203 | map2( 204 | (decodedValue, rest) => [(key, decodedValue), ...rest], 205 | _ => T.objErr(key, decode(value)), 206 | decodeEntries(xs), 207 | ); 208 | 209 | value(Js.Json.decodeObject, `ExpectedObject) 210 | |> map(Js.Dict.entries >> Array.toList) 211 | |> flatMap(decodeEntries) 212 | |> map(Js.Dict.fromList); 213 | }; 214 | 215 | let dictJson = dict(okJson); 216 | 217 | let stringMap = decode => 218 | dict(decode) |> map(Js.Dict.entries) |> map(Belt.Map.String.fromArray); 219 | 220 | let rec at = (fields, decode) => 221 | switch (fields) { 222 | | [] => decode 223 | | [x, ...xs] => 224 | value(Js.Json.decodeObject, `ExpectedObject) 225 | |> map(Js.Dict.get(_, x)) 226 | |> flatMap(Option.fold(_ => T.missingFieldErr(x), pure)) 227 | |> flatMap(at(xs, decode) >> T.objErr(x) >> const) 228 | }; 229 | 230 | let field = (name, decode) => at([name], decode); 231 | 232 | let optionalField = (name, decode) => 233 | value(Js.Json.decodeObject, `ExpectedObject) 234 | |> map(Js.Dict.get(_, name)) 235 | |> flatMap( 236 | fun 237 | | None => pure(None) 238 | | Some(v) => (_ => optional(decode, v) |> T.objErr(name)), 239 | ); 240 | 241 | let fallback = (decode, recovery) => alt(decode, pure(recovery)); 242 | 243 | let tupleFromFields = ((fieldA, decodeA), (fieldB, decodeB)) => 244 | map2(Tuple.make, field(fieldA, decodeA), field(fieldB, decodeB)); 245 | 246 | let hush = (decode, json) => decode(json) |> Result.toOption; 247 | 248 | module Pipeline = { 249 | let succeed = pure; 250 | 251 | let pipe = (a, b) => apply(b, a); 252 | 253 | let optionalField = (name, decode) => pipe(optionalField(name, decode)); 254 | 255 | let fallbackField = (name, decode, recovery) => 256 | pipe(fallback(field(name, decode), recovery)); 257 | 258 | let field = (name, decode) => pipe(field(name, decode)); 259 | 260 | let at = (fields, decode) => pipe(at(fields, decode)); 261 | 262 | let hardcoded = v => pipe(pure(v)); 263 | 264 | /** 265 | * `run` takes a decoder and some json, and it passes that json to the 266 | * decoder. The result is that your decoder is run with the provided json 267 | */ 268 | let run = (|>); 269 | 270 | /** 271 | * Alias many functions from outside the Pipeline for easy local opens 272 | */ 273 | let map = map; 274 | let apply = apply; 275 | let map2 = map2; 276 | let map3 = map3; 277 | let map4 = map4; 278 | let map5 = map5; 279 | let pure = pure; 280 | let flatMap = flatMap; 281 | let boolean = boolean; 282 | let string = string; 283 | let floatFromNumber = floatFromNumber; 284 | let intFromNumber = intFromNumber; 285 | let intFromNumber = intFromNumber; 286 | let date = date; 287 | let variantFromJson = variantFromJson; 288 | let variantFromString = variantFromString; 289 | let variantFromInt = variantFromInt; 290 | let optional = optional; 291 | let array = array; 292 | let list = list; 293 | 294 | let tuple = tuple; 295 | let tuple2 = tuple2; 296 | let tuple3 = tuple3; 297 | let tuple4 = tuple4; 298 | let tuple5 = tuple5; 299 | let tupleAtLeast2 = tupleAtLeast2; 300 | let tupleAtLeast3 = tupleAtLeast3; 301 | let tupleAtLeast4 = tupleAtLeast4; 302 | let tupleAtLeast5 = tupleAtLeast5; 303 | 304 | let tupleFromFields = tupleFromFields; 305 | let dict = dict; 306 | let stringMap = stringMap; 307 | let oneOf = oneOf; 308 | let fallback = fallback; 309 | }; 310 | }; 311 | -------------------------------------------------------------------------------- /src/Decode_AsOption.rei: -------------------------------------------------------------------------------- 1 | let map: ('a => 'b, Js.Json.t => option('a), Js.Json.t) => option('b); 2 | let apply: 3 | (Js.Json.t => option('a => 'b), Js.Json.t => option('a), Js.Json.t) => 4 | option('b); 5 | 6 | let map2: 7 | ( 8 | ('a, 'b) => 'c, 9 | Js.Json.t => option('a), 10 | Js.Json.t => option('b), 11 | Js.Json.t 12 | ) => 13 | option('c); 14 | 15 | let map3: 16 | ( 17 | ('a, 'b, 'c) => 'd, 18 | Js.Json.t => option('a), 19 | Js.Json.t => option('b), 20 | Js.Json.t => option('c), 21 | Js.Json.t 22 | ) => 23 | option('d); 24 | 25 | let map4: 26 | ( 27 | ('a, 'b, 'c, 'd) => 'e, 28 | Js.Json.t => option('a), 29 | Js.Json.t => option('b), 30 | Js.Json.t => option('c), 31 | Js.Json.t => option('d), 32 | Js.Json.t 33 | ) => 34 | option('e); 35 | 36 | let map5: 37 | ( 38 | ('a, 'b, 'c, 'd, 'e) => 'f, 39 | Js.Json.t => option('a), 40 | Js.Json.t => option('b), 41 | Js.Json.t => option('c), 42 | Js.Json.t => option('d), 43 | Js.Json.t => option('e), 44 | Js.Json.t 45 | ) => 46 | option('f); 47 | 48 | let pure: ('a, Js.Json.t) => option('a); 49 | 50 | let flatMap: 51 | (('a, Js.Json.t) => option('b), Js.Json.t => option('a), Js.Json.t) => 52 | option('b); 53 | 54 | let alt: 55 | (Js.Json.t => option('a), Js.Json.t => option('a), Js.Json.t) => 56 | option('a); 57 | 58 | let okJson: Js.Json.t => option(Js.Json.t); 59 | let boolean: Js.Json.t => option(bool); 60 | let string: Js.Json.t => option(Js.String.t); 61 | let floatFromNumber: Js.Json.t => option(float); 62 | let intFromNumber: Js.Json.t => option(int); 63 | let date: Js.Json.t => option(Js.Date.t); 64 | let literal: 65 | (('a, 'a) => bool, Js.Json.t => option('a), 'a, Js.Json.t) => option('a); 66 | let literalString: (string, Js.Json.t) => option(string); 67 | let literalInt: (int, Js.Json.t) => option(int); 68 | let literalFloat: (float, Js.Json.t) => option(float); 69 | let stringUnion: 70 | ((string, 'a), list((string, 'a)), Js.Json.t) => option('a); 71 | let variantFromJson: 72 | (Js.Json.t => option('a), 'a => option('b), Js.Json.t) => option('b); 73 | let variantFromString: (string => option('a), Js.Json.t) => option('a); 74 | let variantFromInt: (int => option('a), Js.Json.t) => option('a); 75 | 76 | let optional: (Js.Json.t => option('a), Js.Json.t) => option(option('a)); 77 | 78 | let array: (Js.Json.t => option('a), Js.Json.t) => option(array('a)); 79 | let list: (Js.Json.t => option('a), Js.Json.t) => option(list('a)); 80 | 81 | let tuple: 82 | (Js.Json.t => option('a), Js.Json.t => option('b), Js.Json.t) => 83 | option(('a, 'b)); 84 | 85 | let tuple2: 86 | (Js.Json.t => option('a), Js.Json.t => option('b), Js.Json.t) => 87 | option(('a, 'b)); 88 | 89 | let tuple3: 90 | ( 91 | Js.Json.t => option('a), 92 | Js.Json.t => option('b), 93 | Js.Json.t => option('c), 94 | Js.Json.t 95 | ) => 96 | option(('a, 'b, 'c)); 97 | 98 | let tuple4: 99 | ( 100 | Js.Json.t => option('a), 101 | Js.Json.t => option('b), 102 | Js.Json.t => option('c), 103 | Js.Json.t => option('d), 104 | Js.Json.t 105 | ) => 106 | option(('a, 'b, 'c, 'd)); 107 | 108 | let tuple5: 109 | ( 110 | Js.Json.t => option('a), 111 | Js.Json.t => option('b), 112 | Js.Json.t => option('c), 113 | Js.Json.t => option('d), 114 | Js.Json.t => option('e), 115 | Js.Json.t 116 | ) => 117 | option(('a, 'b, 'c, 'd, 'e)); 118 | 119 | let tupleAtLeast2: 120 | (Js.Json.t => option('a), Js.Json.t => option('b), Js.Json.t) => 121 | option(('a, 'b)); 122 | 123 | let tupleAtLeast3: 124 | ( 125 | Js.Json.t => option('a), 126 | Js.Json.t => option('b), 127 | Js.Json.t => option('c), 128 | Js.Json.t 129 | ) => 130 | option(('a, 'b, 'c)); 131 | 132 | let tupleAtLeast4: 133 | ( 134 | Js.Json.t => option('a), 135 | Js.Json.t => option('b), 136 | Js.Json.t => option('c), 137 | Js.Json.t => option('d), 138 | Js.Json.t 139 | ) => 140 | option(('a, 'b, 'c, 'd)); 141 | 142 | let tupleAtLeast5: 143 | ( 144 | Js.Json.t => option('a), 145 | Js.Json.t => option('b), 146 | Js.Json.t => option('c), 147 | Js.Json.t => option('d), 148 | Js.Json.t => option('e), 149 | Js.Json.t 150 | ) => 151 | option(('a, 'b, 'c, 'd, 'e)); 152 | 153 | let tupleFromFields: 154 | ( 155 | (string, Js.Json.t => option('a)), 156 | (string, Js.Json.t => option('b)), 157 | Js.Json.t 158 | ) => 159 | option(('a, 'b)); 160 | 161 | let dict: (Js.Json.t => option('a), Js.Json.t) => option(Js.Dict.t('a)); 162 | let stringMap: 163 | (Js.Json.t => option('a), Js.Json.t) => option(Belt.Map.String.t('a)); 164 | 165 | let at: (list(string), Js.Json.t => option('a), Js.Json.t) => option('a); 166 | 167 | let field: (string, Js.Json.t => option('a), Js.Json.t) => option('a); 168 | 169 | let optionalField: 170 | (string, Js.Json.t => option('a), Js.Json.t) => option(option('a)); 171 | 172 | let fallback: (Js.Json.t => option('a), 'a, Js.Json.t) => option('a); 173 | 174 | let oneOf: 175 | (Js.Json.t => option('a), list(Js.Json.t => option('a)), Js.Json.t) => 176 | option('a); 177 | 178 | [@deprecated "Will be removed in favor up the upcoming addition of letops"] 179 | module Pipeline: { 180 | let succeed: ('a, Js.Json.t) => option('a); 181 | 182 | let pipe: 183 | (Js.Json.t => option('a), Js.Json.t => option('a => 'b), Js.Json.t) => 184 | option('b); 185 | 186 | let field: 187 | ( 188 | string, 189 | Js.Json.t => option('a), 190 | Js.Json.t => option('a => 'b), 191 | Js.Json.t 192 | ) => 193 | option('b); 194 | 195 | let at: 196 | ( 197 | list(string), 198 | Js.Json.t => option('a), 199 | Js.Json.t => option('a => 'b), 200 | Js.Json.t 201 | ) => 202 | option('b); 203 | 204 | let optionalField: 205 | ( 206 | string, 207 | Js.Json.t => option('a), 208 | Js.Json.t => option(option('a) => 'b), 209 | Js.Json.t 210 | ) => 211 | option('b); 212 | 213 | let fallbackField: 214 | ( 215 | string, 216 | Js.Json.t => option('a), 217 | 'a, 218 | Js.Json.t => option('a => 'b), 219 | Js.Json.t 220 | ) => 221 | option('b); 222 | 223 | let fallback: (Js.Json.t => option('a), 'a, Js.Json.t) => option('a); 224 | 225 | let hardcoded: 226 | ('a, Js.Json.t => option('a => 'c), Js.Json.t) => option('c); 227 | 228 | let run: (Js.Json.t, Js.Json.t => option('a)) => option('a); 229 | 230 | let map: ('a => 'b, Js.Json.t => option('a), Js.Json.t) => option('b); 231 | let apply: 232 | (Js.Json.t => option('a => 'b), Js.Json.t => option('a), Js.Json.t) => 233 | option('b); 234 | 235 | let map2: 236 | ( 237 | ('a, 'b) => 'c, 238 | Js.Json.t => option('a), 239 | Js.Json.t => option('b), 240 | Js.Json.t 241 | ) => 242 | option('c); 243 | 244 | let map3: 245 | ( 246 | ('a, 'b, 'c) => 'd, 247 | Js.Json.t => option('a), 248 | Js.Json.t => option('b), 249 | Js.Json.t => option('c), 250 | Js.Json.t 251 | ) => 252 | option('d); 253 | 254 | let map4: 255 | ( 256 | ('a, 'b, 'c, 'd) => 'e, 257 | Js.Json.t => option('a), 258 | Js.Json.t => option('b), 259 | Js.Json.t => option('c), 260 | Js.Json.t => option('d), 261 | Js.Json.t 262 | ) => 263 | option('e); 264 | 265 | let map5: 266 | ( 267 | ('a, 'b, 'c, 'd, 'e) => 'f, 268 | Js.Json.t => option('a), 269 | Js.Json.t => option('b), 270 | Js.Json.t => option('c), 271 | Js.Json.t => option('d), 272 | Js.Json.t => option('e), 273 | Js.Json.t 274 | ) => 275 | option('f); 276 | 277 | let pure: ('a, Js.Json.t) => option('a); 278 | 279 | let flatMap: 280 | (('a, Js.Json.t) => option('b), Js.Json.t => option('a), Js.Json.t) => 281 | option('b); 282 | 283 | let boolean: Js.Json.t => option(bool); 284 | let string: Js.Json.t => option(Js.String.t); 285 | let floatFromNumber: Js.Json.t => option(float); 286 | let intFromNumber: Js.Json.t => option(int); 287 | let date: Js.Json.t => option(Js.Date.t); 288 | let variantFromJson: 289 | (Js.Json.t => option('a), 'a => option('b), Js.Json.t) => option('b); 290 | let variantFromString: (string => option('a), Js.Json.t) => option('a); 291 | let variantFromInt: (int => option('a), Js.Json.t) => option('a); 292 | 293 | let optional: (Js.Json.t => option('a), Js.Json.t) => option(option('a)); 294 | 295 | let array: (Js.Json.t => option('a), Js.Json.t) => option(array('a)); 296 | let list: (Js.Json.t => option('a), Js.Json.t) => option(list('a)); 297 | 298 | let tuple: 299 | (Js.Json.t => option('a), Js.Json.t => option('b), Js.Json.t) => 300 | option(('a, 'b)); 301 | 302 | let tuple2: 303 | (Js.Json.t => option('a), Js.Json.t => option('b), Js.Json.t) => 304 | option(('a, 'b)); 305 | 306 | let tuple3: 307 | ( 308 | Js.Json.t => option('a), 309 | Js.Json.t => option('b), 310 | Js.Json.t => option('c), 311 | Js.Json.t 312 | ) => 313 | option(('a, 'b, 'c)); 314 | 315 | let tuple4: 316 | ( 317 | Js.Json.t => option('a), 318 | Js.Json.t => option('b), 319 | Js.Json.t => option('c), 320 | Js.Json.t => option('d), 321 | Js.Json.t 322 | ) => 323 | option(('a, 'b, 'c, 'd)); 324 | 325 | let tuple5: 326 | ( 327 | Js.Json.t => option('a), 328 | Js.Json.t => option('b), 329 | Js.Json.t => option('c), 330 | Js.Json.t => option('d), 331 | Js.Json.t => option('e), 332 | Js.Json.t 333 | ) => 334 | option(('a, 'b, 'c, 'd, 'e)); 335 | 336 | let tupleAtLeast2: 337 | (Js.Json.t => option('a), Js.Json.t => option('b), Js.Json.t) => 338 | option(('a, 'b)); 339 | 340 | let tupleAtLeast3: 341 | ( 342 | Js.Json.t => option('a), 343 | Js.Json.t => option('b), 344 | Js.Json.t => option('c), 345 | Js.Json.t 346 | ) => 347 | option(('a, 'b, 'c)); 348 | 349 | let tupleAtLeast4: 350 | ( 351 | Js.Json.t => option('a), 352 | Js.Json.t => option('b), 353 | Js.Json.t => option('c), 354 | Js.Json.t => option('d), 355 | Js.Json.t 356 | ) => 357 | option(('a, 'b, 'c, 'd)); 358 | 359 | let tupleAtLeast5: 360 | ( 361 | Js.Json.t => option('a), 362 | Js.Json.t => option('b), 363 | Js.Json.t => option('c), 364 | Js.Json.t => option('d), 365 | Js.Json.t => option('e), 366 | Js.Json.t 367 | ) => 368 | option(('a, 'b, 'c, 'd, 'e)); 369 | 370 | let tupleFromFields: 371 | ( 372 | (string, Js.Json.t => option('a)), 373 | (string, Js.Json.t => option('b)), 374 | Js.Json.t 375 | ) => 376 | option(('a, 'b)); 377 | 378 | let dict: (Js.Json.t => option('a), Js.Json.t) => option(Js.Dict.t('a)); 379 | let stringMap: 380 | (Js.Json.t => option('a), Js.Json.t) => option(Belt.Map.String.t('a)); 381 | 382 | let oneOf: 383 | (Js.Json.t => option('a), list(Js.Json.t => option('a)), Js.Json.t) => 384 | option('a); 385 | }; 386 | -------------------------------------------------------------------------------- /test/Decode_AsResult_OfParseError_test.re: -------------------------------------------------------------------------------- 1 | open Jest; 2 | open Expect; 3 | open Relude.Globals; 4 | 5 | module Decode = Decode.AsResult.OfParseError; 6 | module Sample = Decode_TestSampleData; 7 | 8 | let valErr = (err, json) => Result.error(Decode.ParseError.Val(err, json)); 9 | let arrErr = (first, rest) => 10 | Result.error(Decode.ParseError.Arr(NonEmpty.List.make(first, rest))); 11 | 12 | let objErr = (first, rest) => 13 | Result.error(Decode.ParseError.Obj(NonEmpty.List.make(first, rest))); 14 | 15 | let objErrSingle = (field, err) => objErr((field, err), []); 16 | 17 | describe("Decode utils", () => { 18 | test("hush (success)", () => { 19 | let decodeBooleanOpt = Decode.(boolean |> hush); 20 | expect(decodeBooleanOpt(Sample.jsonTrue)) |> toEqual(Some(true)); 21 | }); 22 | 23 | test("hush (failure)", () => { 24 | let decodeStringOpt = Decode.(string |> hush); 25 | expect(decodeStringOpt(Sample.jsonNull)) |> toEqual(None); 26 | }); 27 | }); 28 | 29 | describe("Simple decoders", () => { 30 | let decodeIntColor = 31 | Decode.intUnion((0, `blue), [(1, `red), (2, `green)]); 32 | 33 | test("null (success)", () => 34 | expect(Decode.null(Sample.jsonNull)) |> toEqual(Result.ok()) 35 | ); 36 | 37 | test("null (failure)", () => 38 | expect(Decode.null(Sample.jsonFalse)) 39 | |> toEqual(valErr(`ExpectedNull, Sample.jsonFalse)) 40 | ); 41 | 42 | test("boolean", () => 43 | expect(Decode.boolean(Sample.jsonNull)) 44 | |> toEqual(valErr(`ExpectedBoolean, Sample.jsonNull)) 45 | ); 46 | 47 | test("string", () => 48 | expect(Decode.string(Sample.jsonNull)) 49 | |> toEqual(valErr(`ExpectedString, Sample.jsonNull)) 50 | ); 51 | 52 | test("floatFromNumber", () => 53 | expect(Decode.floatFromNumber(Sample.jsonString)) 54 | |> toEqual(valErr(`ExpectedNumber, Sample.jsonString)) 55 | ); 56 | 57 | test("intFromNumber (non-number)", () => 58 | expect(Decode.intFromNumber(Sample.jsonNull)) 59 | |> toEqual(valErr(`ExpectedNumber, Sample.jsonNull)) 60 | ); 61 | 62 | test("intFromNumber (string containing int)", () => 63 | expect(Decode.intFromNumber(Sample.jsonString4)) 64 | |> toEqual(valErr(`ExpectedNumber, Sample.jsonString4)) 65 | ); 66 | 67 | test("intFromNumber (out-of-range int)", () => 68 | expect(Decode.intFromNumber(Sample.jsonLargeFloat)) 69 | |> toEqual(valErr(`ExpectedInt, Sample.jsonLargeFloat)) 70 | ); 71 | 72 | test("intFromNumber (float)", () => 73 | expect(Decode.intFromNumber(Sample.jsonFloat)) 74 | |> toEqual(valErr(`ExpectedInt, Sample.jsonFloat)) 75 | ); 76 | 77 | test("date", () => 78 | expect(Decode.date(Sample.jsonString)) 79 | |> toEqual(valErr(`ExpectedValidDate, Sample.jsonString)) 80 | ); 81 | 82 | test("date (failure)", () => 83 | expect(Decode.date(Sample.jsonNull)) 84 | |> toEqual( 85 | Result.error( 86 | Decode.ParseError.( 87 | TriedMultiple( 88 | NonEmpty.List.make( 89 | Val(`ExpectedNumber, Sample.jsonNull), 90 | [Val(`ExpectedString, Sample.jsonNull)], 91 | ), 92 | ) 93 | ), 94 | ), 95 | ) 96 | ); 97 | 98 | test("literalTrue (success)", () => 99 | expect(Decode.literalTrue(Sample.jsonTrue)) |> toEqual(Result.ok(true)) 100 | ); 101 | 102 | test("literalTrue (failure)", () => 103 | expect(Decode.literalTrue(Sample.jsonFalse)) 104 | |> toEqual(valErr(`ExpectedValidOption, Sample.jsonFalse)) 105 | ); 106 | 107 | test("literalFalse (success)", () => 108 | expect(Decode.literalFalse(Sample.jsonFalse)) 109 | |> toEqual(Result.ok(false)) 110 | ); 111 | 112 | test("literalFalse (failure)", () => 113 | expect(Decode.literalFalse(Sample.jsonTrue)) 114 | |> toEqual(valErr(`ExpectedValidOption, Sample.jsonTrue)) 115 | ); 116 | 117 | test("intUnion (success)", () => 118 | expect(decodeIntColor(Sample.jsonIntZero)) |> toEqual(Result.ok(`blue)) 119 | ); 120 | 121 | test("intUnion (failure)", () => 122 | expect(decodeIntColor(Sample.jsonIntFive)) 123 | |> toEqual( 124 | Result.error( 125 | Decode.ParseError.( 126 | TriedMultiple( 127 | NonEmpty.List.make( 128 | Val(`ExpectedValidOption, Sample.jsonIntFive), 129 | [ 130 | Val(`ExpectedValidOption, Sample.jsonIntFive), 131 | Val(`ExpectedValidOption, Sample.jsonIntFive), 132 | ], 133 | ), 134 | ) 135 | ), 136 | ), 137 | ) 138 | ); 139 | 140 | test("variant", () => 141 | [@ocaml.warning "-3"] 142 | expect(Decode.variantFromString(Sample.colorFromJs, Sample.jsonString)) 143 | |> toEqual(valErr(`ExpectedValidOption, Sample.jsonString)) 144 | ); 145 | 146 | test("array", () => 147 | expect(Decode.array(Decode.string, Sample.jsonNull)) 148 | |> toEqual(valErr(`ExpectedArray, Sample.jsonNull)) 149 | ); 150 | 151 | test("arrayJson (success)", () => 152 | expect(Decode.arrayJson(Sample.jsonArrayString)) 153 | |> toEqual(Result.ok([|"A", "B", "C"|] |> Array.map(Js.Json.string))) 154 | ); 155 | 156 | test("arrayJson (failure, null)", () => 157 | expect(Decode.arrayJson(Sample.jsonNull)) 158 | |> toEqual(valErr(`ExpectedArray, Sample.jsonNull)) 159 | ); 160 | 161 | test("listJson (success)", () => 162 | expect(Decode.listJson(Sample.jsonArrayString)) 163 | |> toEqual(Result.ok(["A", "B", "C"] |> List.map(Js.Json.string))) 164 | ); 165 | 166 | test("listJson (failure, number)", () => 167 | expect(Decode.arrayJson(Sample.jsonFloat)) 168 | |> toEqual(valErr(`ExpectedArray, Sample.jsonFloat)) 169 | ); 170 | 171 | test("object", () => 172 | expect(Decode.field("x", Decode.string, Sample.jsonString)) 173 | |> toEqual(valErr(`ExpectedObject, Sample.jsonString)) 174 | ); 175 | 176 | test("dictJson (success)", () => 177 | expect(Decode.dictJson(Sample.jsonDictEmpty)) 178 | |> toEqual(Result.ok(Js.Dict.empty())) 179 | ); 180 | 181 | test("dictJson (failure, array)", () => 182 | expect(Decode.dictJson(Sample.jsonArrayEmpty)) 183 | |> toEqual(valErr(`ExpectedObject, Sample.jsonArrayEmpty)) 184 | ); 185 | }); 186 | 187 | describe("Inner decoders", () => { 188 | test("array", () => 189 | expect(Decode.array(Decode.string, Sample.jsonArrayString)) 190 | |> toEqual(Result.ok(Sample.valArrayString)) 191 | ); 192 | 193 | test("array (failure on inner decode)", () => 194 | expect(Decode.array(Decode.boolean, Sample.jsonArrayString)) 195 | |> toEqual( 196 | arrErr( 197 | (0, Val(`ExpectedBoolean, Js.Json.string("A"))), 198 | [ 199 | (1, Val(`ExpectedBoolean, Js.Json.string("B"))), 200 | (2, Val(`ExpectedBoolean, Js.Json.string("C"))), 201 | ], 202 | ), 203 | ) 204 | ); 205 | 206 | test("arrayAt (success)", () => 207 | expect(Decode.arrayAt(1, Decode.string, Sample.jsonArrayString)) 208 | |> toEqual(Result.ok("B")) 209 | ); 210 | 211 | test("arrayAt (failure, non-array)", () => 212 | expect(Decode.arrayAt(3, Decode.string, Sample.jsonNull)) 213 | |> toEqual(valErr(`ExpectedArray, Sample.jsonNull)) 214 | ); 215 | 216 | test("arrayAt (failure, empty array)", () => 217 | expect(Decode.arrayAt(1, Decode.string, Sample.jsonArrayEmpty)) 218 | |> toEqual(valErr(`ExpectedTuple(2), Sample.jsonArrayEmpty)) 219 | ); 220 | 221 | test("tuple (fails on null)", () => 222 | [@ocaml.warning "-3"] 223 | expect(Decode.(tuple2(string, boolean, Sample.jsonNull))) 224 | |> toEqual(valErr(`ExpectedArray, Sample.jsonNull)) 225 | ); 226 | 227 | test("tuple (fails on wrong size)", () => 228 | [@ocaml.warning "-3"] 229 | expect(Decode.(tuple2(string, boolean, Sample.jsonArrayEmpty))) 230 | |> toEqual(valErr(`ExpectedTuple(2), Sample.jsonArrayEmpty)) 231 | ); 232 | 233 | test("tuple (fails on inner decode)", () => 234 | [@ocaml.warning "-3"] 235 | expect(Decode.(tuple2(boolean, string, Sample.jsonTuple))) 236 | |> toEqual( 237 | arrErr( 238 | (0, Val(`ExpectedBoolean, Js.Json.string("A"))), 239 | [(1, Val(`ExpectedString, Js.Json.boolean(true)))], 240 | ), 241 | ) 242 | ); 243 | 244 | test("field (missing)", () => 245 | expect(Decode.field("x", Decode.string, Sample.jsonJobCeo)) 246 | |> toEqual(objErrSingle("x", MissingField)) 247 | ); 248 | 249 | test("field (failure on inner decode)", () => 250 | expect(Decode.field("manager", Decode.string, Sample.jsonJobCeo)) 251 | |> toEqual( 252 | objErrSingle( 253 | "manager", 254 | InvalidField(Val(`ExpectedString, Sample.jsonNull)), 255 | ), 256 | ) 257 | ); 258 | 259 | test("optionalField (failure on outer structure)", () => 260 | expect(Decode.optionalField("field", Decode.string, Sample.jsonString)) 261 | |> toEqual(valErr(`ExpectedObject, Sample.jsonString)) 262 | ); 263 | 264 | test("optionalField (failure on inner decode)", () => 265 | expect(Decode.(optionalField("title", boolean, Sample.jsonJobCeo))) 266 | |> toEqual( 267 | objErrSingle( 268 | "title", 269 | InvalidField(Val(`ExpectedBoolean, Js.Json.string("CEO"))), 270 | ), 271 | ) 272 | ); 273 | 274 | let decodeUnion = 275 | Decode.( 276 | oneOf( 277 | map(Sample.unionS, string), 278 | [ 279 | map(Sample.unionN, optional(floatFromNumber)), 280 | map(Sample.unionB, boolean), 281 | ], 282 | ) 283 | ); 284 | 285 | let failureString = {|Attempted multiple decoders, which all failed: 286 | Expected string but found {} 287 | Expected number but found {} 288 | Expected boolean but found {}|}; 289 | 290 | test("oneOf (success on first)", () => 291 | expect(decodeUnion(Sample.jsonString)) 292 | |> toEqual(Result.ok(Sample.S(Sample.valString))) 293 | ); 294 | 295 | test("oneOf (success on last)", () => 296 | expect(decodeUnion(Sample.jsonTrue)) 297 | |> toEqual(Result.ok(Sample.B(Sample.valBool))) 298 | ); 299 | 300 | test("oneOf (failure)", () => 301 | expect(decodeUnion(Sample.jsonDictEmpty)) 302 | |> toEqual( 303 | Result.error( 304 | Decode.ParseError.( 305 | TriedMultiple( 306 | NonEmpty.List.make( 307 | Val(`ExpectedString, Sample.jsonDictEmpty), 308 | [ 309 | Val(`ExpectedNumber, Sample.jsonDictEmpty), 310 | Val(`ExpectedBoolean, Sample.jsonDictEmpty), 311 | ], 312 | ), 313 | ) 314 | ), 315 | ), 316 | ) 317 | ); 318 | 319 | test("oneOf (failure to string)", () => 320 | expect( 321 | decodeUnion(Sample.jsonDictEmpty) 322 | |> Result.mapError(Decode.ParseError.failureToDebugString), 323 | ) 324 | |> toEqual(Result.error(failureString)) 325 | ); 326 | }); 327 | 328 | describe("Large, nested decoder", () => { 329 | [@ocaml.warning "-3"] 330 | let decodeJob = 331 | Decode.( 332 | map4( 333 | Sample.makeJob, 334 | field("title", string), 335 | field("x", string), 336 | field("title", date), 337 | pure(None), 338 | ) 339 | ); 340 | 341 | let decoded = decodeJob(Sample.jsonJobCeo); 342 | 343 | let error = 344 | Decode.ParseError.( 345 | Obj( 346 | NonEmpty.List.make( 347 | ("x", MissingField), 348 | [ 349 | ( 350 | "title", 351 | InvalidField(Val(`ExpectedValidDate, Js.Json.string("CEO"))), 352 | ), 353 | ], 354 | ), 355 | ) 356 | ); 357 | 358 | let objErrString = {|Failed to decode object: 359 | Field "x" is required, but was not present 360 | Field "title" had an invalid value: Expected a valid date but found "CEO"|}; 361 | 362 | let arrErrString = {|Failed to decode array: 363 | At position 0: Expected string but found null|}; 364 | 365 | test("map4, field", () => 366 | expect(decoded) |> toEqual(Result.error(error)) 367 | ); 368 | 369 | test("toDebugString (obj)", () => 370 | expect(Decode.ParseError.failureToDebugString(error)) 371 | |> toEqual(objErrString) 372 | ); 373 | 374 | test("toDebugString (arr)", () => 375 | expect( 376 | Decode.ParseError.( 377 | arrPure(0, Val(`ExpectedString, Sample.jsonNull)) 378 | |> failureToDebugString 379 | ), 380 | ) 381 | |> toEqual(arrErrString) 382 | ); 383 | }); 384 | 385 | // ParseErrors only know how to combine Arr+Arr and Obj+Obj. In most situations 386 | // this is all that matters. In all other cases, the first error is chosen. 387 | describe("Parse error combinations", () => { 388 | let combine = (a, b) => (a, b); 389 | 390 | let arrError = 391 | Decode.ParseError.( 392 | Arr( 393 | NonEmpty.List.make( 394 | (0, Val(`ExpectedBoolean, Js.Json.string("A"))), 395 | [ 396 | (1, Val(`ExpectedBoolean, Js.Json.string("B"))), 397 | (2, Val(`ExpectedBoolean, Js.Json.string("C"))), 398 | ], 399 | ), 400 | ) 401 | ); 402 | 403 | let objError = Decode.ParseError.(objPure("x", MissingField)); 404 | 405 | test("combine Val/Val", () => 406 | [@ocaml.warning "-3"] 407 | expect(Decode.(map2(combine, string, boolean, Sample.jsonNull))) 408 | |> toEqual(valErr(`ExpectedString, Sample.jsonNull)) 409 | ); 410 | 411 | test("combine Arr/Val", () => 412 | [@ocaml.warning "-3"] 413 | expect( 414 | Decode.(map2(combine, list(boolean), boolean, Sample.jsonArrayString)), 415 | ) 416 | |> toEqual(Result.error(arrError)) 417 | ); 418 | 419 | test("combine Obj/Val", () => 420 | [@ocaml.warning "-3"] 421 | expect( 422 | Decode.( 423 | map2(combine, field("x", boolean), boolean, Sample.jsonDictEmpty) 424 | ), 425 | ) 426 | |> toEqual(Result.error(objError)) 427 | ); 428 | }); 429 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Upcoming 4 | 5 | More deprecations have been added to prepare for v2.0, as well as some new functions to ease the pain of those deprecations. 6 | 7 | ### :warning: Deprecated features 8 | 9 | - `map2`...`map5` have been deprecated and will be replaced by the upcoming `and+` 10 | - `tuple2`...`tuple5` and `tupleAtLeast2`...`tupleAtLeast5` have been deprecated for the same reason, and they can be recreated using `arrayAt` (see below) 11 | - `ParseError.ResultOf` (and related modules) is deprecated for the same reason `Decode.Make` is deprecated 12 | 13 | ### :sparkles: New features 14 | 15 | - `arrayAt` allows decoding only specific positions of arrays, which is useful for building your own tuples 16 | - `null` is a new decoder that only succeeds if it encounters a JSON `null` value 17 | - `dictJson`, `arrayJson`, `listJson` were added to allow decoding outer structures while preserving inner JSON 18 | 19 | ## 1.1.0 (Mar 25, 2023) 20 | 21 | This release adds helpers to ease the transition away from the deprecated features in the 1.0 release. Barring any bugs that require fixing, this is the last planned release in the 1.x series. 22 | 23 | ### :warning: Deprecated features 24 | 25 | - Some deprecations mentioned in the previous release notes weren't actually deprecated in the code. Those will now trigger compiler warnings. 26 | 27 | ### :sparkles: New features 28 | 29 | - `hush` is a new function that takes a `Decode.AsResult.OfParseError` decoder and converts it into a `Js.Json.t => option('a)` decoder (effectively "hushing" the error). This should make the transition away from `Decode.AsOption` easier. 30 | - `literalBool`, `literalTrue`, and `literalFalse` join the other `literal*` decoders that first decode, then further validate the output 31 | - `intUnion` works like `stringUnion` and should make the transition away from `variantFromInt` easier 32 | 33 | ## 1.0.0 (Mar 9, 2023) 34 | 35 | Version 1.0 is here! Very little has changed since 0.11.2; mostly this release indicates the stability (or lack of maintenance, depending on how you look at it) over the last several years. The 1.x releases will be the last to support BuckleScript as we turn our attention to Melange. 36 | 37 | There are no breaking changes in this release, but there are a handful of deprecations. We should end up with decent alternatives to all of the features that will eventually be removed, but if any of this concerns you, please let me know! 38 | 39 | ### :warning: Deprecated features 40 | 41 | - `Decode.AsResult.OfStringNel` will be removed in an upcoming release. The errors are less useful than `OfParseError`, and there's a cost to maintaining multiple decoder types 42 | - `Decode.AsOption` will also be removed. Again, there's a maintenance burden in keeping it, and if you really want `option`, it's easy enough to convert the `result` return into an `option`. 43 | - `Decode.Make` (for making your own custom output types) will eventually be removed as we focus on `ParseError` 44 | - `variantFromJson` and `variantFromString` will be removed in favor of new, simpler, more consistent tools for decoding variants (see the new `literal` functions below) 45 | - The `Decode.Pipeline` module is now deprecated. See [the docs](https://mlms13.github.io/bs-decode/docs/decoding-objects) for alternatives and a teaser about upcoming changes that will make this experience way better 46 | - `stringMap` will be removed as we try to limit how much we depend on Belt-specific features. You can use the `dict` decoder instead and convert the `Js.Dict` to a `Belt.String.Map`. 47 | 48 | ### :sparkles: New features 49 | 50 | - Functions have been added to decode literal values e.g. `literalString`, `literalInt`, and `literalFloat`. These functions first decode to the expected type, then ensure the value exactly matches the provided value. This is useful for decoding unions of string literals (e.g. `"dev" | "prod"`) 51 | - Decoding sting unions is such a common need that we've provided a `stringUnion` function to make it even simpler 52 | 53 | ### :memo: Documentation 54 | 55 | - The [documentation site](https://mlms13.github.io/bs-decode/docs/what-and-why) got a handful of updates to explain the deprecations 56 | - The guide on [decoding variants](https://mlms13.github.io/bs-decode/docs/decoding-variants) was almost completely rewritten and might be particularly helpful 57 | 58 | ### :robot: Dependency updates 59 | 60 | - `bs-bastet` was bumped to `2.0` and `relude` was bumped to `0.66.1`. As peer dependencies, this change is the most likely to impact your existing projects, but hopefully the update isn't too bad 61 | 62 | ## 0.11.2 (Jun 28, 2020) 63 | 64 | ### :bug: Bug fixes 65 | 66 | - Compiled output files will no longer have a `/./` in the middle of the path, which causes issues with some bundlers/dev setups (thanks @hamza0867 for finding and fixing this!) 67 | 68 | ## 0.11.1 (Mar 31, 2020) 69 | 70 | ### :bug: Bug fixes 71 | 72 | - Revert the `public` flag in the `bsconfig.json`. There are apparently some strange quirks that cause aliased modules not to compile in downstream projects, so I got rid of it. You still shouldn't need to access `Decode_*` modules directly (instead use `Decode.AsWhatever`) but those modules won't be hidden from you. 73 | 74 | ## 0.11.0 (Mar 31, 2020) 75 | 76 | ### :rotating_light: Breaking changes 77 | 78 | - The `bs-abstract` peer-dependency is now `bs-bastet`, and the required Relude version is 0.59+. See the [Relude release notes](https://github.com/reazen/relude/releases/tag/v0.59.0) and the [Bastet migration guide](https://github.com/Risto-Stevcev/bastet/blob/1.2.5/MIGRATION.md) for details. 79 | - Alias everything in `Decode` and prevent direct access to the `Decode_*` modules via Bucklescript's `public` flag 80 | 81 | ### :sparkles: New features 82 | 83 | - `okJson` is a decoder that always passes and preserves the input JSON 84 | 85 | ## 0.10.0 (Mar 3, 2020) 86 | 87 | ### :rotating_light: Breaking changes 88 | 89 | - All error-related types now live in `Decode.ParseError`. Specifically, this means that `DecodeBase.failure` is now `Decode.ParseError.base`, which is important if you're extending the base errors to create your own custom errors. 90 | 91 | ### :memo: Documentation 92 | 93 | - README explains peer dependencies better 94 | - Nested array decoding is demonstrated in the tests 95 | - Haskell-style object decoding operates on the decoders, not the result 96 | 97 | ### :heavy_check_mark: Code quality 98 | 99 | - Bump dependencies and allow compilation with Bucklescript 7.1 100 | - Internally, use structural typing for typeclasses rather than named modules 101 | 102 | ## 0.9.0 (Oct 7, 2019) 103 | 104 | ### :sparkles: New features 105 | 106 | - Decode into a Belt Map (with string keys) 107 | - Allow easy access to tuple helpers from Pipeline 108 | - `pipe` function itself is now public 109 | 110 | ### :heavy_check_mark: Code quality 111 | 112 | - More tests around string and int decoding 113 | - Bump dependencies 114 | 115 | ## 0.8.1 (Jul 12, 2019) 116 | 117 | ### :bug: Bug fixes 118 | 119 | - `optionField` values that are present but fail to decode will now report the field in the `ParseError` 120 | 121 | ### :heavy_check_mark: Code quality 122 | 123 | - CI will now show failures correctly if tests don't pass 124 | 125 | ## 0.8.0 (Jul 3, 2019) 126 | 127 | ### :rotating_light: Breaking 128 | 129 | - Long-deprecated `int` and `float` functions have been removed. You should be using `intFromNumber` and `floatFromNumber` instead to avoid shadowing issues 130 | - `tuple` (which previously contructed a `tuple` from a JSON object) is now `tupleFromFields` 131 | - Base `failure` type now includes an `ExpectedTuple(int)` constructor, where `int` is the expected size. This is only a breaking change if you're manually matching on values of the base `failure` type. 132 | 133 | ### :sparkles: New features 134 | 135 | - JSON arrays can now be decoded directly into tuples using `tuple2`...`tuple5` (which will fail if the JSON array is larger than expected) or `tupleAtLeast2`...`tupleAtLeast5` (which will tolerate longer arrays) 136 | 137 | ## 0.7.0 (Jun 25, 2019) 138 | 139 | ### :rotating_light: Breaking 140 | 141 | - `relude` and `bs-abstract` are now peerDependencies. This means you'll need to add these dependencies to your own `package.json`, and you're much less likely to end up with duplicate versions of these packages. 142 | 143 | ## 0.6.2 (Jun 7, 2019) 144 | 145 | ### :bug: Bug fixes 146 | 147 | - `Decode.array` (and its friend `list`) had a regression where they could fail in some browsers when given large amounts of data, but stack safety has been restored 148 | 149 | ## 0.6.1 (May 28, 2019) 150 | 151 | ### :heavy_check_mark: Code quality 152 | 153 | - Fix links in `package.json` 154 | 155 | ## 0.6.0 (May 27, 2019) 156 | 157 | ### :rotating_light: Breaking 158 | 159 | - `Decode.fallback` doesn't assume you want to work with `field`s, but you can use `Decode.(fallback(field("x", string), "default"))` if you want the old behavior 160 | - `Decode.Pipeline.fallback` has the same new behavior, but it provides `fallbackField` to achieve the old behavior 161 | - `Decode.ParseError.map` has been removed (it wasn't used internally or documented) 162 | - The `Decode.ParseError` variant type now includes a `TriedMultiple` constructor. This is only a breaking change if you are matching directly on values of this type. 163 | 164 | ### :bug: Bug fixes 165 | 166 | - `Decode.AsResult.OfStringNel` actually collects multiple errors now 167 | 168 | ### :sparkles: New features 169 | 170 | - `Decode.alt` allows combining decode functions and picking the first success 171 | - `Decode.AsResult.OfStringNel` now includes all of the same `map`, `flatMap`, etc functions for decoders 172 | - `Decode.ParseError` is aliased as `Decode.AsResult.OfParseError.ParseError` so parse errors can be accessed from an aliased decode module 173 | 174 | ### :heavy_check_mark: Code quality 175 | 176 | - Reorganize tests so that `Decode_AsOption` tests decoders pass-vs-fail, `Decode_AsResult_*` tests failure reporting 177 | - Test coverage has increased to 100% 178 | - Internally, many functions were re-written to use `map`, `flatMap`, `alt`, etc on the decoders themselves, rather than running the decoders and transforming the output 179 | - `Js.Dict.key` was changed to `string` in interface files for better editor suggestions 180 | 181 | ## 0.5.1 (May 22, 2019) 182 | 183 | ### :heavy_check_mark: Code quality 184 | 185 | - Bump Relude dependency to the latest version 186 | - Add continuous integration and coverage reporting 187 | 188 | ## 0.5.0 (May 13, 2019) 189 | 190 | ### :rotating_light: Breaking 191 | 192 | - `Decode.oneOf` now takes a single decoder, followed by a list of decoders instead of requiring you to construct a `NonEmptyList` (#28) 193 | - `Decode.ok` has been removed; if you want to construct a decoder that always succeeds (ignoring the JSON input), use `Decode.pure` instead 194 | - Decoders that produce `NonEmptyList`s of errors now use `Relude.NonEmpty.List` instead of the implementation from `bs-nonempty` due to more active maintenance and a broader collection of helper functions 195 | - `Decode.ResultUtils` is no longer part of the public interface. Decode functions themselves are more easily composable (see "New stuff" below), so there's less need to transform the output after decoding. Plus [better libraries](https://github.com/reazen/relude) exist if you want to work with `option` and `result` types. (#33) 196 | 197 | ### :sparkles: New features 198 | 199 | - Decode functions now have `map`, `apply`, `map2`...`map5`, and `flatMap` functions, meaning you can transform decoders before actually running them. (#23) 200 | - `Decode.NonEmptyList` is exposed for all decode modules that return a `NonEmptyList` of errors. This means you can do basic operations with the errors without bringing in an additional library (#24) 201 | - `Decode.Pipeline` aliases most of the base decoders (e.g. `string`, `intFromNumber`, etc), so locally-opening `Decode.Pipeline` should get you everything you need (#27) 202 | 203 | ### :heavy_check_mark: Code quality 204 | 205 | - Internal use of infix functions has been greatly reduced to improve code clarity 206 | - Tests have fewer global opens and fewer aliased functions 207 | 208 | ## 0.4.0 (Jan 24, 2019) 209 | 210 | ### :rotating_light: Breaking 211 | 212 | - `float` and `int` are now `floatFromNumber` and `intFromNumber` to avoid compiler warnings related to shadowing `Pervasives.float` 213 | 214 | ### :sparkles: New features 215 | 216 | - `Decode.date` decodes numbers or ISO-8601 strings into `Js.Date.t` 217 | - `Decode.oneOf` attempts multiple decoders 218 | - `Decode.dict` decodes an object into a `Js.Dict.t` 219 | - `Decode.variantFromJson` (and `variantFromString`, `variantFromInt`) decode JSON values directly into Reason variants 220 | - `Decode.Pipeline.at` now brings `at` functionality into the pipeline, to allow digging into nested JSON objects 221 | 222 | ### :memo: Documentation 223 | 224 | - The `docs/` folder has more content 225 | - The website is unpublished but mostly finished 226 | 227 | ## 0.3.3 (Oct 12, 2018) 228 | 229 | ### :bug: Bug fixes 230 | 231 | - `Decode.int` now works correctly with `0` 232 | 233 | ## 0.3.1 (Oct 12, 2018) 234 | 235 | ### :sparkles: New features 236 | 237 | - `Decode.AsResult.OfStringNel` now provides a consistent `ResultUtil` module that matches `...OfParseError` 238 | 239 | ### :heavy_check_mark: Code quality 240 | 241 | - `*.rei` files now exist to help with editor suggestions 242 | 243 | 244 | ## 0.3.0 (Oct 10, 2018) 245 | 246 | ### :rotating_light: Breaking 247 | 248 | - Rename `decode*` functions (e.g. `decodeString` is now `string`) 249 | - Flip arguments to `Pipeline.run` so it can be piped with `|>` (thanks @gavacho) 250 | 251 | ### :sparkles: New features 252 | 253 | - `Decode.boolean` was missing but now exists (thinks @johnhaley81) 254 | 255 | ## 0.2.0 (Sep 28, 2018) 256 | 257 | ### :rotating_light: Breaking 258 | 259 | - Basic failure type is now a polymorphic variant so it's easier to extend with custom validations 260 | - Many submodules were renamed and reorganized 261 | 262 | ### :sparkles: New features 263 | 264 | - `Decode` top-level modules provides a convenient way to access everything 265 | 266 | ### :memo: Documentation 267 | 268 | - Some basic examples now exist 269 | 270 | ## 0.1.0 (Sep 24, 2018) 271 | 272 | Initial release includes basic decode functions and the choice of decoding into an `option` or a `Belt.Result.t` with recursively structured errors. 273 | -------------------------------------------------------------------------------- /src/Decode_AsResult_OfStringNel.rei: -------------------------------------------------------------------------------- 1 | module NonEmptyList: Decode_NonEmptyList.Nel; 2 | 3 | let map: 4 | ('a => 'b, Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 5 | result('b, NonEmptyList.t(string)); 6 | 7 | let apply: 8 | ( 9 | Js.Json.t => result('a => 'b, NonEmptyList.t(string)), 10 | Js.Json.t => result('a, NonEmptyList.t(string)), 11 | Js.Json.t 12 | ) => 13 | result('b, NonEmptyList.t(string)); 14 | 15 | let map2: 16 | ( 17 | ('a, 'b) => 'c, 18 | Js.Json.t => result('a, NonEmptyList.t(string)), 19 | Js.Json.t => result('b, NonEmptyList.t(string)), 20 | Js.Json.t 21 | ) => 22 | result('c, NonEmptyList.t(string)); 23 | 24 | let map3: 25 | ( 26 | ('a, 'b, 'c) => 'd, 27 | Js.Json.t => result('a, NonEmptyList.t(string)), 28 | Js.Json.t => result('b, NonEmptyList.t(string)), 29 | Js.Json.t => result('c, NonEmptyList.t(string)), 30 | Js.Json.t 31 | ) => 32 | result('d, NonEmptyList.t(string)); 33 | 34 | let map4: 35 | ( 36 | ('a, 'b, 'c, 'd) => 'e, 37 | Js.Json.t => result('a, NonEmptyList.t(string)), 38 | Js.Json.t => result('b, NonEmptyList.t(string)), 39 | Js.Json.t => result('c, NonEmptyList.t(string)), 40 | Js.Json.t => result('d, NonEmptyList.t(string)), 41 | Js.Json.t 42 | ) => 43 | result('e, NonEmptyList.t(string)); 44 | 45 | let map5: 46 | ( 47 | ('a, 'b, 'c, 'd, 'e) => 'f, 48 | Js.Json.t => result('a, NonEmptyList.t(string)), 49 | Js.Json.t => result('b, NonEmptyList.t(string)), 50 | Js.Json.t => result('c, NonEmptyList.t(string)), 51 | Js.Json.t => result('d, NonEmptyList.t(string)), 52 | Js.Json.t => result('e, NonEmptyList.t(string)), 53 | Js.Json.t 54 | ) => 55 | result('f, NonEmptyList.t(string)); 56 | 57 | let pure: ('a, Js.Json.t) => result('a, NonEmptyList.t(string)); 58 | 59 | let flatMap: 60 | ( 61 | ('a, Js.Json.t) => result('b, NonEmptyList.t(string)), 62 | Js.Json.t => result('a, NonEmptyList.t(string)), 63 | Js.Json.t 64 | ) => 65 | result('b, NonEmptyList.t(string)); 66 | 67 | let alt: 68 | ( 69 | Js.Json.t => result('a, NonEmptyList.t(string)), 70 | Js.Json.t => result('a, NonEmptyList.t(string)), 71 | Js.Json.t 72 | ) => 73 | result('a, NonEmptyList.t(string)); 74 | 75 | let okJson: Js.Json.t => result(Js.Json.t, NonEmptyList.t(string)); 76 | let boolean: Js.Json.t => result(bool, NonEmptyList.t(string)); 77 | let string: Js.Json.t => result(string, NonEmptyList.t(string)); 78 | let floatFromNumber: Js.Json.t => result(float, NonEmptyList.t(string)); 79 | let intFromNumber: Js.Json.t => result(int, NonEmptyList.t(string)); 80 | let date: Js.Json.t => result(Js.Date.t, NonEmptyList.t(string)); 81 | let literal: 82 | ( 83 | ('a, 'a) => bool, 84 | Js.Json.t => result('a, NonEmptyList.t(string)), 85 | 'a, 86 | Js.Json.t 87 | ) => 88 | result('a, NonEmptyList.t(string)); 89 | 90 | let literalString: 91 | (string, Js.Json.t) => result(string, NonEmptyList.t(string)); 92 | 93 | let literalInt: (int, Js.Json.t) => result(int, NonEmptyList.t(string)); 94 | 95 | let literalFloat: 96 | (float, Js.Json.t) => result(float, NonEmptyList.t(string)); 97 | 98 | let stringUnion: 99 | ((string, 'a), list((string, 'a)), Js.Json.t) => 100 | result('a, NonEmptyList.t(string)); 101 | 102 | let variantFromJson: 103 | ( 104 | Js.Json.t => result('a, NonEmptyList.t(string)), 105 | 'a => option('b), 106 | Js.Json.t 107 | ) => 108 | result('b, NonEmptyList.t(string)); 109 | let variantFromString: 110 | (string => option('a), Js.Json.t) => result('a, NonEmptyList.t(string)); 111 | let variantFromInt: 112 | (int => option('a), Js.Json.t) => result('a, NonEmptyList.t(string)); 113 | 114 | let optional: 115 | (Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 116 | result(option('a), NonEmptyList.t(string)); 117 | 118 | let array: 119 | (Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 120 | result(array('a), NonEmptyList.t(string)); 121 | 122 | let list: 123 | (Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 124 | result(list('a), NonEmptyList.t(string)); 125 | 126 | let tuple: 127 | ( 128 | Js.Json.t => result('a, NonEmptyList.t(string)), 129 | Js.Json.t => result('b, NonEmptyList.t(string)), 130 | Js.Json.t 131 | ) => 132 | result(('a, 'b), NonEmptyList.t(string)); 133 | 134 | let tuple2: 135 | ( 136 | Js.Json.t => result('a, NonEmptyList.t(string)), 137 | Js.Json.t => result('b, NonEmptyList.t(string)), 138 | Js.Json.t 139 | ) => 140 | result(('a, 'b), NonEmptyList.t(string)); 141 | 142 | let tuple3: 143 | ( 144 | Js.Json.t => result('a, NonEmptyList.t(string)), 145 | Js.Json.t => result('b, NonEmptyList.t(string)), 146 | Js.Json.t => result('c, NonEmptyList.t(string)), 147 | Js.Json.t 148 | ) => 149 | result(('a, 'b, 'c), NonEmptyList.t(string)); 150 | 151 | let tuple4: 152 | ( 153 | Js.Json.t => result('a, NonEmptyList.t(string)), 154 | Js.Json.t => result('b, NonEmptyList.t(string)), 155 | Js.Json.t => result('c, NonEmptyList.t(string)), 156 | Js.Json.t => result('d, NonEmptyList.t(string)), 157 | Js.Json.t 158 | ) => 159 | result(('a, 'b, 'c, 'd), NonEmptyList.t(string)); 160 | 161 | let tuple5: 162 | ( 163 | Js.Json.t => result('a, NonEmptyList.t(string)), 164 | Js.Json.t => result('b, NonEmptyList.t(string)), 165 | Js.Json.t => result('c, NonEmptyList.t(string)), 166 | Js.Json.t => result('d, NonEmptyList.t(string)), 167 | Js.Json.t => result('e, NonEmptyList.t(string)), 168 | Js.Json.t 169 | ) => 170 | result(('a, 'b, 'c, 'd, 'e), NonEmptyList.t(string)); 171 | 172 | let tupleAtLeast2: 173 | ( 174 | Js.Json.t => result('a, NonEmptyList.t(string)), 175 | Js.Json.t => result('b, NonEmptyList.t(string)), 176 | Js.Json.t 177 | ) => 178 | result(('a, 'b), NonEmptyList.t(string)); 179 | 180 | let tupleAtLeast3: 181 | ( 182 | Js.Json.t => result('a, NonEmptyList.t(string)), 183 | Js.Json.t => result('b, NonEmptyList.t(string)), 184 | Js.Json.t => result('c, NonEmptyList.t(string)), 185 | Js.Json.t 186 | ) => 187 | result(('a, 'b, 'c), NonEmptyList.t(string)); 188 | 189 | let tupleAtLeast4: 190 | ( 191 | Js.Json.t => result('a, NonEmptyList.t(string)), 192 | Js.Json.t => result('b, NonEmptyList.t(string)), 193 | Js.Json.t => result('c, NonEmptyList.t(string)), 194 | Js.Json.t => result('d, NonEmptyList.t(string)), 195 | Js.Json.t 196 | ) => 197 | result(('a, 'b, 'c, 'd), NonEmptyList.t(string)); 198 | 199 | let tupleAtLeast5: 200 | ( 201 | Js.Json.t => result('a, NonEmptyList.t(string)), 202 | Js.Json.t => result('b, NonEmptyList.t(string)), 203 | Js.Json.t => result('c, NonEmptyList.t(string)), 204 | Js.Json.t => result('d, NonEmptyList.t(string)), 205 | Js.Json.t => result('e, NonEmptyList.t(string)), 206 | Js.Json.t 207 | ) => 208 | result(('a, 'b, 'c, 'd, 'e), NonEmptyList.t(string)); 209 | 210 | let tupleFromFields: 211 | ( 212 | (string, Js.Json.t => result('a, NonEmptyList.t(string))), 213 | (string, Js.Json.t => result('b, NonEmptyList.t(string))), 214 | Js.Json.t 215 | ) => 216 | result(('a, 'b), NonEmptyList.t(string)); 217 | 218 | let dict: 219 | (Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 220 | result(Js.Dict.t('a), NonEmptyList.t(string)); 221 | 222 | let stringMap: 223 | (Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 224 | result(Belt.Map.String.t('a), NonEmptyList.t(string)); 225 | 226 | let at: 227 | ( 228 | list(string), 229 | Js.Json.t => result('a, NonEmptyList.t(string)), 230 | Js.Json.t 231 | ) => 232 | result('a, NonEmptyList.t(string)); 233 | 234 | let field: 235 | (string, Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 236 | result('a, NonEmptyList.t(string)); 237 | 238 | let optionalField: 239 | (string, Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 240 | result(option('a), NonEmptyList.t(string)); 241 | 242 | let fallback: 243 | (Js.Json.t => result('a, NonEmptyList.t(string)), 'a, Js.Json.t) => 244 | result('a, NonEmptyList.t(string)); 245 | 246 | let oneOf: 247 | ( 248 | Js.Json.t => result('a, NonEmptyList.t(string)), 249 | list(Js.Json.t => result('a, NonEmptyList.t(string))), 250 | Js.Json.t 251 | ) => 252 | result('a, NonEmptyList.t(string)); 253 | 254 | [@deprecated "Will be removed in favor up the upcoming addition of letops"] 255 | module Pipeline: { 256 | let succeed: ('a, Js.Json.t) => result('a, NonEmptyList.t(string)); 257 | 258 | let pipe: 259 | ( 260 | Js.Json.t => result('a, NonEmptyList.t(string)), 261 | Js.Json.t => result('a => 'b, NonEmptyList.t(string)), 262 | Js.Json.t 263 | ) => 264 | result('b, NonEmptyList.t(string)); 265 | 266 | let field: 267 | ( 268 | string, 269 | Js.Json.t => result('a, NonEmptyList.t(string)), 270 | Js.Json.t => result('a => 'b, NonEmptyList.t(string)), 271 | Js.Json.t 272 | ) => 273 | result('b, NonEmptyList.t(string)); 274 | 275 | let at: 276 | ( 277 | list(string), 278 | Js.Json.t => result('a, NonEmptyList.t(string)), 279 | Js.Json.t => result('a => 'b, NonEmptyList.t(string)), 280 | Js.Json.t 281 | ) => 282 | result('b, NonEmptyList.t(string)); 283 | 284 | let optionalField: 285 | ( 286 | string, 287 | Js.Json.t => result('a, NonEmptyList.t(string)), 288 | Js.Json.t => result(option('a) => 'b, NonEmptyList.t(string)), 289 | Js.Json.t 290 | ) => 291 | result('b, NonEmptyList.t(string)); 292 | 293 | let fallbackField: 294 | ( 295 | string, 296 | Js.Json.t => result('a, NonEmptyList.t(string)), 297 | 'a, 298 | Js.Json.t => result('a => 'b, NonEmptyList.t(string)), 299 | Js.Json.t 300 | ) => 301 | result('b, NonEmptyList.t(string)); 302 | 303 | let fallback: 304 | (Js.Json.t => result('a, NonEmptyList.t(string)), 'a, Js.Json.t) => 305 | result('a, NonEmptyList.t(string)); 306 | 307 | let hardcoded: 308 | ('a, Js.Json.t => result('a => 'c, NonEmptyList.t(string)), Js.Json.t) => 309 | result('c, NonEmptyList.t(string)); 310 | 311 | let run: 312 | (Js.Json.t, Js.Json.t => result('a, NonEmptyList.t(string))) => 313 | result('a, NonEmptyList.t(string)); 314 | 315 | let map: 316 | ('a => 'b, Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 317 | result('b, NonEmptyList.t(string)); 318 | 319 | let apply: 320 | ( 321 | Js.Json.t => result('a => 'b, NonEmptyList.t(string)), 322 | Js.Json.t => result('a, NonEmptyList.t(string)), 323 | Js.Json.t 324 | ) => 325 | result('b, NonEmptyList.t(string)); 326 | 327 | let map2: 328 | ( 329 | ('a, 'b) => 'c, 330 | Js.Json.t => result('a, NonEmptyList.t(string)), 331 | Js.Json.t => result('b, NonEmptyList.t(string)), 332 | Js.Json.t 333 | ) => 334 | result('c, NonEmptyList.t(string)); 335 | 336 | let map3: 337 | ( 338 | ('a, 'b, 'c) => 'd, 339 | Js.Json.t => result('a, NonEmptyList.t(string)), 340 | Js.Json.t => result('b, NonEmptyList.t(string)), 341 | Js.Json.t => result('c, NonEmptyList.t(string)), 342 | Js.Json.t 343 | ) => 344 | result('d, NonEmptyList.t(string)); 345 | 346 | let map4: 347 | ( 348 | ('a, 'b, 'c, 'd) => 'e, 349 | Js.Json.t => result('a, NonEmptyList.t(string)), 350 | Js.Json.t => result('b, NonEmptyList.t(string)), 351 | Js.Json.t => result('c, NonEmptyList.t(string)), 352 | Js.Json.t => result('d, NonEmptyList.t(string)), 353 | Js.Json.t 354 | ) => 355 | result('e, NonEmptyList.t(string)); 356 | 357 | let map5: 358 | ( 359 | ('a, 'b, 'c, 'd, 'e) => 'f, 360 | Js.Json.t => result('a, NonEmptyList.t(string)), 361 | Js.Json.t => result('b, NonEmptyList.t(string)), 362 | Js.Json.t => result('c, NonEmptyList.t(string)), 363 | Js.Json.t => result('d, NonEmptyList.t(string)), 364 | Js.Json.t => result('e, NonEmptyList.t(string)), 365 | Js.Json.t 366 | ) => 367 | result('f, NonEmptyList.t(string)); 368 | 369 | let pure: ('a, Js.Json.t) => result('a, NonEmptyList.t(string)); 370 | 371 | let flatMap: 372 | ( 373 | ('a, Js.Json.t) => result('b, NonEmptyList.t(string)), 374 | Js.Json.t => result('a, NonEmptyList.t(string)), 375 | Js.Json.t 376 | ) => 377 | result('b, NonEmptyList.t(string)); 378 | 379 | let boolean: Js.Json.t => result(bool, NonEmptyList.t(string)); 380 | let string: Js.Json.t => result(Js.String.t, NonEmptyList.t(string)); 381 | let floatFromNumber: Js.Json.t => result(float, NonEmptyList.t(string)); 382 | let intFromNumber: Js.Json.t => result(int, NonEmptyList.t(string)); 383 | let date: Js.Json.t => result(Js.Date.t, NonEmptyList.t(string)); 384 | let variantFromJson: 385 | ( 386 | Js.Json.t => result('a, NonEmptyList.t(string)), 387 | 'a => option('b), 388 | Js.Json.t 389 | ) => 390 | result('b, NonEmptyList.t(string)); 391 | let variantFromString: 392 | (string => option('a), Js.Json.t) => result('a, NonEmptyList.t(string)); 393 | let variantFromInt: 394 | (int => option('a), Js.Json.t) => result('a, NonEmptyList.t(string)); 395 | 396 | let optional: 397 | (Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 398 | result(option('a), NonEmptyList.t(string)); 399 | 400 | let array: 401 | (Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 402 | result(array('a), NonEmptyList.t(string)); 403 | let list: 404 | (Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 405 | result(list('a), NonEmptyList.t(string)); 406 | 407 | let tuple: 408 | ( 409 | Js.Json.t => result('a, NonEmptyList.t(string)), 410 | Js.Json.t => result('b, NonEmptyList.t(string)), 411 | Js.Json.t 412 | ) => 413 | result(('a, 'b), NonEmptyList.t(string)); 414 | 415 | let tuple2: 416 | ( 417 | Js.Json.t => result('a, NonEmptyList.t(string)), 418 | Js.Json.t => result('b, NonEmptyList.t(string)), 419 | Js.Json.t 420 | ) => 421 | result(('a, 'b), NonEmptyList.t(string)); 422 | 423 | let tuple3: 424 | ( 425 | Js.Json.t => result('a, NonEmptyList.t(string)), 426 | Js.Json.t => result('b, NonEmptyList.t(string)), 427 | Js.Json.t => result('c, NonEmptyList.t(string)), 428 | Js.Json.t 429 | ) => 430 | result(('a, 'b, 'c), NonEmptyList.t(string)); 431 | 432 | let tuple4: 433 | ( 434 | Js.Json.t => result('a, NonEmptyList.t(string)), 435 | Js.Json.t => result('b, NonEmptyList.t(string)), 436 | Js.Json.t => result('c, NonEmptyList.t(string)), 437 | Js.Json.t => result('d, NonEmptyList.t(string)), 438 | Js.Json.t 439 | ) => 440 | result(('a, 'b, 'c, 'd), NonEmptyList.t(string)); 441 | 442 | let tuple5: 443 | ( 444 | Js.Json.t => result('a, NonEmptyList.t(string)), 445 | Js.Json.t => result('b, NonEmptyList.t(string)), 446 | Js.Json.t => result('c, NonEmptyList.t(string)), 447 | Js.Json.t => result('d, NonEmptyList.t(string)), 448 | Js.Json.t => result('e, NonEmptyList.t(string)), 449 | Js.Json.t 450 | ) => 451 | result(('a, 'b, 'c, 'd, 'e), NonEmptyList.t(string)); 452 | 453 | let tupleAtLeast2: 454 | ( 455 | Js.Json.t => result('a, NonEmptyList.t(string)), 456 | Js.Json.t => result('b, NonEmptyList.t(string)), 457 | Js.Json.t 458 | ) => 459 | result(('a, 'b), NonEmptyList.t(string)); 460 | 461 | let tupleAtLeast3: 462 | ( 463 | Js.Json.t => result('a, NonEmptyList.t(string)), 464 | Js.Json.t => result('b, NonEmptyList.t(string)), 465 | Js.Json.t => result('c, NonEmptyList.t(string)), 466 | Js.Json.t 467 | ) => 468 | result(('a, 'b, 'c), NonEmptyList.t(string)); 469 | 470 | let tupleAtLeast4: 471 | ( 472 | Js.Json.t => result('a, NonEmptyList.t(string)), 473 | Js.Json.t => result('b, NonEmptyList.t(string)), 474 | Js.Json.t => result('c, NonEmptyList.t(string)), 475 | Js.Json.t => result('d, NonEmptyList.t(string)), 476 | Js.Json.t 477 | ) => 478 | result(('a, 'b, 'c, 'd), NonEmptyList.t(string)); 479 | 480 | let tupleAtLeast5: 481 | ( 482 | Js.Json.t => result('a, NonEmptyList.t(string)), 483 | Js.Json.t => result('b, NonEmptyList.t(string)), 484 | Js.Json.t => result('c, NonEmptyList.t(string)), 485 | Js.Json.t => result('d, NonEmptyList.t(string)), 486 | Js.Json.t => result('e, NonEmptyList.t(string)), 487 | Js.Json.t 488 | ) => 489 | result(('a, 'b, 'c, 'd, 'e), NonEmptyList.t(string)); 490 | 491 | let tupleFromFields: 492 | ( 493 | (string, Js.Json.t => result('a, NonEmptyList.t(string))), 494 | (string, Js.Json.t => result('b, NonEmptyList.t(string))), 495 | Js.Json.t 496 | ) => 497 | result(('a, 'b), NonEmptyList.t(string)); 498 | 499 | let dict: 500 | (Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 501 | result(Js.Dict.t('a), NonEmptyList.t(string)); 502 | 503 | let stringMap: 504 | (Js.Json.t => result('a, NonEmptyList.t(string)), Js.Json.t) => 505 | result(Belt.Map.String.t('a), NonEmptyList.t(string)); 506 | 507 | let oneOf: 508 | ( 509 | Js.Json.t => result('a, NonEmptyList.t(string)), 510 | list(Js.Json.t => result('a, NonEmptyList.t(string))), 511 | Js.Json.t 512 | ) => 513 | result('a, NonEmptyList.t(string)); 514 | }; 515 | -------------------------------------------------------------------------------- /test/Decode_AsOption_test.re: -------------------------------------------------------------------------------- 1 | open Jest; 2 | open Expect; 3 | open Relude.Globals; 4 | 5 | [@ocaml.warning "-3"] 6 | module Decode = Decode.AsOption; 7 | module Sample = Decode_TestSampleData; 8 | 9 | describe("Simple decoders", () => { 10 | test("bool (success)", () => 11 | expect(Decode.boolean(Sample.jsonTrue)) 12 | |> toEqual(Some(Sample.valBool)) 13 | ); 14 | 15 | test("bool (fails on null)", () => 16 | expect(Decode.boolean(Sample.jsonNull)) |> toEqual(None) 17 | ); 18 | 19 | test("bool (fails on int)", () => 20 | expect(Decode.boolean(Sample.jsonInt)) |> toEqual(None) 21 | ); 22 | 23 | test("bool (fails on string)", () => 24 | expect(Decode.boolean(Sample.jsonStringTrue)) |> toEqual(None) 25 | ); 26 | 27 | test("string (success)", () => 28 | expect(Decode.string(Sample.jsonString)) 29 | |> toEqual(Some(Sample.valString)) 30 | ); 31 | 32 | test("string (is still string when it contains a number)", () => 33 | expect(Decode.string(Sample.jsonString4)) 34 | |> toEqual(Some(Sample.valString4)) 35 | ); 36 | 37 | test("string (fails on float)", () => 38 | expect(Decode.string(Sample.jsonFloat)) |> toEqual(None) 39 | ); 40 | 41 | test("float (success)", () => 42 | expect(Decode.floatFromNumber(Sample.jsonFloat)) 43 | |> toEqual(Some(Sample.valFloat)) 44 | ); 45 | 46 | test("float (fails on string)", () => 47 | expect(Decode.floatFromNumber(Sample.jsonString)) |> toEqual(None) 48 | ); 49 | 50 | test("int (success)", () => 51 | expect(Decode.intFromNumber(Sample.jsonInt)) 52 | |> toEqual(Some(Sample.valInt)) 53 | ); 54 | 55 | test("int (succeeds on zero)", () => 56 | expect(Decode.intFromNumber(Sample.jsonIntZero)) 57 | |> toEqual(Some(Sample.valIntZero)) 58 | ); 59 | 60 | test("int (fails on string)", () => 61 | expect(Decode.intFromNumber(Sample.jsonString)) |> toEqual(None) 62 | ); 63 | 64 | test("int (fails on float)", () => 65 | expect(Decode.intFromNumber(Sample.jsonFloat)) |> toEqual(None) 66 | ); 67 | 68 | test("date (succeeds on valid string)", () => 69 | expect(Decode.date(Sample.jsonDateString)) 70 | |> toEqual(Some(Sample.valDateString)) 71 | ); 72 | 73 | test("date (succeeds on valid number)", () => 74 | expect(Decode.date(Sample.jsonDateNumber)) 75 | |> toEqual(Some(Sample.valDateNumber)) 76 | ); 77 | 78 | test("date (fails on invalid string)", () => 79 | expect(Decode.date(Sample.jsonString)) |> toEqual(None) 80 | ); 81 | 82 | test("date (fails on invalid number)", () => 83 | expect(Decode.date(Js.Json.number(Js.Float._NaN))) |> toEqual(None) 84 | ); 85 | 86 | test("date (fails on null)", () => 87 | expect(Decode.date(Sample.jsonNull)) |> toEqual(None) 88 | ); 89 | }); 90 | 91 | describe("Literal decoders", () => { 92 | test("literalString (success)", () => 93 | expect(Decode.literalString("blue", Sample.jsonStringBlue)) 94 | |> toEqual(Some("blue")) 95 | ); 96 | 97 | test("literalString (failure, wrong type)", () => 98 | expect(Decode.literalString("blue", Sample.jsonIntFive)) 99 | |> toEqual(None) 100 | ); 101 | 102 | test("literalString (failure, wrong value)", () => 103 | expect(Decode.literalString("blue", Sample.jsonStringYellow)) 104 | |> toEqual(None) 105 | ); 106 | 107 | test("literalInt (success)", () => 108 | expect(Decode.literalInt(5, Sample.jsonIntFive)) |> toEqual(Some(5)) 109 | ); 110 | 111 | test("literalInt (failure, wrong type)", () => 112 | expect(Decode.literalInt(5, Sample.jsonStringBlue)) |> toEqual(None) 113 | ); 114 | 115 | test("literalInt (failure, wrong value)", () => 116 | expect(Decode.literalInt(5, Sample.jsonIntZero)) |> toEqual(None) 117 | ); 118 | 119 | test("literalFloat (success)", () => 120 | expect(Decode.literalFloat(5.0, Sample.jsonIntFive)) 121 | |> toEqual(Some(5.0)) 122 | ); 123 | 124 | test("literalFloat (failure, wrong type)", () => 125 | expect(Decode.literalFloat(5.0, Sample.jsonStringBlue)) |> toEqual(None) 126 | ); 127 | 128 | test("literalFloat (failure, wrong value)", () => 129 | expect(Decode.literalFloat(5.0, Sample.jsonIntZero)) |> toEqual(None) 130 | ); 131 | }); 132 | 133 | describe("Variant decoders", () => { 134 | let decodePolyColor = 135 | Decode.stringUnion( 136 | ("blue", `blue), 137 | [("red", `red), ("green", `green)], 138 | ); 139 | 140 | test("stringUnion (success)", () => 141 | expect(decodePolyColor(Sample.jsonStringBlue)) |> toEqual(Some(`blue)) 142 | ); 143 | 144 | test("stringUnion (failure)", () => 145 | expect(decodePolyColor(Sample.jsonString4)) |> toEqual(None) 146 | ); 147 | 148 | test("variantFromString (success)", () => 149 | expect( 150 | Decode.variantFromString(Sample.colorFromJs, Sample.jsonStringBlue), 151 | ) 152 | |> toEqual(Some(`blue)) 153 | ); 154 | 155 | test("variantFromString (failure)", () => 156 | expect( 157 | Decode.variantFromString(Sample.colorFromJs, Sample.jsonStringYellow), 158 | ) 159 | |> toEqual(None) 160 | ); 161 | 162 | test("variantFromInt (success)", () => 163 | expect(Decode.variantFromInt(Sample.numbersFromJs, Sample.jsonIntZero)) 164 | |> toEqual(Some(Sample.Zero)) 165 | ); 166 | 167 | test("variantFromInt (failure)", () => 168 | expect(Decode.variantFromInt(Sample.numbersFromJs, Sample.jsonIntFive)) 169 | |> toEqual(None) 170 | ); 171 | }); 172 | 173 | describe("Nested decoders", () => { 174 | test("optional float (succeeds on null)", () => 175 | expect(Decode.(optional(floatFromNumber, Sample.jsonNull))) 176 | |> toEqual(Some(None)) 177 | ); 178 | 179 | test("optional float (succeeds on float)", () => 180 | expect(Decode.(optional(floatFromNumber, Sample.jsonFloat))) 181 | |> toEqual(Some(Some(Sample.valFloat))) 182 | ); 183 | 184 | test("optional float (fails on bool)", () => 185 | expect(Decode.(optional(floatFromNumber, Sample.jsonTrue))) 186 | |> toEqual(None) 187 | ); 188 | 189 | test("array (succeeds)", () => 190 | expect(Decode.(array(string, Sample.jsonArrayString))) 191 | |> toEqual(Some(Sample.valArrayString)) 192 | ); 193 | 194 | test("array (succeeds on empty)", () => 195 | expect(Decode.(array(string, Sample.jsonArrayEmpty))) 196 | |> toEqual(Some([||])) 197 | ); 198 | 199 | test("array (succeeds on nested)", () => 200 | expect(Decode.(array(array(string), Sample.jsonArrayNested))) 201 | |> toEqual(Some(Sample.valArrayNested)) 202 | ); 203 | 204 | test("array (fails on non-array)", () => 205 | expect(Decode.(array(string, Sample.jsonString))) |> toEqual(None) 206 | ); 207 | 208 | test("array (fails on invalid inner data)", () => 209 | expect(Decode.(array(boolean, Sample.jsonArrayString))) |> toEqual(None) 210 | ); 211 | 212 | test("list (succeeds)", () => 213 | expect(Decode.(list(string, Sample.jsonArrayString))) 214 | |> toEqual(Some(Sample.valListString)) 215 | ); 216 | 217 | test("list (succeeds on empty)", () => 218 | expect(Decode.(list(string, Sample.jsonArrayEmpty))) 219 | |> toEqual(Some(Sample.valListEmpty)) 220 | ); 221 | 222 | test("tuple2 (succeeds)", () => 223 | expect(Decode.(tuple(string, boolean, Sample.jsonTuple))) 224 | |> toEqual(Some(Sample.valTuple)) 225 | ); 226 | 227 | test("tuple2 (fails on non-array)", () => 228 | expect(Decode.(tuple(string, boolean, Sample.jsonTrue))) 229 | |> toEqual(None) 230 | ); 231 | 232 | test("tuple2 (fails on wrong size)", () => 233 | expect(Decode.(tuple(string, boolean, Sample.jsonArrayEmpty))) 234 | |> toEqual(None) 235 | ); 236 | 237 | test("tuple2 (fails on bad inner value)", () => 238 | expect(Decode.(tuple(string, string, Sample.jsonTuple))) 239 | |> toEqual(None) 240 | ); 241 | 242 | test("tuple3 (succeeds)", () => 243 | expect( 244 | Decode.(tuple3(string, boolean, intFromNumber, Sample.jsonTuple3)), 245 | ) 246 | |> toEqual(Some(Sample.valTuple3)) 247 | ); 248 | 249 | test("tuple4 (succeeds)", () => 250 | expect( 251 | Decode.(tuple4(string, boolean, boolean, string, Sample.jsonTuple4)), 252 | ) 253 | |> toEqual(Some(Sample.valTuple4)) 254 | ); 255 | 256 | test("tuple5 (succeeds)", () => 257 | expect( 258 | Decode.( 259 | tuple5(string, string, string, string, string, Sample.jsonTuple5) 260 | ), 261 | ) 262 | |> toEqual(Some(Sample.valTuple5)) 263 | ); 264 | 265 | test("tupleAtLeast2", () => 266 | expect(Decode.(tupleAtLeast2(string, string, Sample.jsonTuple6))) 267 | |> toEqual(Some(("A", "B"))) 268 | ); 269 | 270 | test("tupleAtLeast3", () => 271 | expect(Decode.(tupleAtLeast3(string, string, string, Sample.jsonTuple6))) 272 | |> toEqual(Some(("A", "B", "C"))) 273 | ); 274 | 275 | test("tupleAtLeast4", () => 276 | expect( 277 | Decode.( 278 | tupleAtLeast4(string, string, string, string, Sample.jsonTuple6) 279 | ), 280 | ) 281 | |> toEqual(Some(("A", "B", "C", "D"))) 282 | ); 283 | 284 | test("tupleAtLeast5", () => 285 | expect( 286 | Decode.( 287 | tupleAtLeast5( 288 | string, 289 | string, 290 | string, 291 | string, 292 | string, 293 | Sample.jsonTuple6, 294 | ) 295 | ), 296 | ) 297 | |> toEqual(Some(("A", "B", "C", "D", "E"))) 298 | ); 299 | 300 | test("dict (succeeds)", () => 301 | expect(Decode.(dict(floatFromNumber, Sample.jsonDictFloat))) 302 | |> toEqual(Some(Sample.valDictFloat)) 303 | ); 304 | 305 | test("dict (succeeds on empty)", () => 306 | expect(Decode.(dict(string, Sample.jsonDictEmpty))) 307 | |> toEqual(Some(Sample.valDictEmpty)) 308 | ); 309 | 310 | test("dict (fails on record)", () => 311 | expect(Decode.(dict(string, Sample.jsonPersonBill))) |> toEqual(None) 312 | ); 313 | 314 | test("stringMap (succeeds)", () => 315 | expect( 316 | Decode.map( 317 | Belt.Map.String.eq(Sample.valMapFloat, _, Float.eq), 318 | Decode.(stringMap(floatFromNumber)), 319 | Sample.jsonDictFloat, 320 | ), 321 | ) 322 | |> toEqual(Some(true)) 323 | ); 324 | 325 | test("stringMap (fails)", () => 326 | expect( 327 | Decode.map( 328 | Belt.Map.String.eq(Sample.valMapFloat, _, Float.eq), 329 | Decode.(stringMap(floatFromNumber)), 330 | Sample.jsonNull, 331 | ), 332 | ) 333 | |> toEqual(None) 334 | ); 335 | 336 | test("at (succeeds on nested field)", () => 337 | expect( 338 | Decode.( 339 | at(["job", "manager", "job", "title"], string, Sample.jsonPersonBill) 340 | ), 341 | ) 342 | |> toEqual(Some("CEO")) 343 | ); 344 | 345 | test("at (succeeds on inner decode with no fields)", () => 346 | expect(Decode.(at([], string, Sample.jsonString))) 347 | |> toEqual(Some(Sample.valString)) 348 | ); 349 | 350 | test("at (fails on missing field)", () => 351 | expect(Decode.(at(["manager", "name"], string, Sample.jsonJobCeo))) 352 | |> toEqual(None) 353 | ); 354 | 355 | test("at (fails on invalid inner data)", () => 356 | expect(Decode.(at(["age"], string, Sample.jsonPersonBill))) 357 | |> toEqual(None) 358 | ); 359 | 360 | test("field (succeeds)", () => 361 | expect(Decode.(field("title", string, Sample.jsonJobCeo))) 362 | |> toEqual(Some("CEO")) 363 | ); 364 | 365 | test("field (fails on missing)", () => 366 | expect(Decode.(field("x", string, Sample.jsonDictEmpty))) 367 | |> toEqual(None) 368 | ); 369 | 370 | test("optionalField (succeeds)", () => 371 | expect(Decode.(optionalField("name", string, Sample.jsonPersonBill))) 372 | |> toEqual(Some(Some("Bill"))) 373 | ); 374 | 375 | test("optionalField (succeeds on empty)", () => 376 | expect(Decode.(optionalField("x", boolean, Sample.jsonDictEmpty))) 377 | |> toEqual(Some(None)) 378 | ); 379 | 380 | test("optionalField (fails on non-object)", () => 381 | expect(Decode.(optionalField("field", string, Sample.jsonString))) 382 | |> toEqual(None) 383 | ); 384 | 385 | test("tupleFromFields (success)", () => 386 | expect( 387 | Decode.( 388 | tupleFromFields( 389 | ("name", string), 390 | ("age", intFromNumber), 391 | Sample.jsonPersonBill, 392 | ) 393 | ), 394 | ) 395 | |> toEqual(Some(("Bill", 27))) 396 | ); 397 | 398 | test("tupleFromFields (failure)", () => 399 | expect( 400 | Decode.( 401 | tupleFromFields(("x", string), ("y", string), Sample.jsonDictEmpty) 402 | ), 403 | ) 404 | |> toEqual(None) 405 | ); 406 | }); 407 | 408 | describe("Decode with alternatives/fallbacks", () => { 409 | let decodeExplode = _json => failwith("Explosion"); 410 | let decodeUnion = 411 | Decode.( 412 | oneOf( 413 | map(Sample.unionS, string), 414 | [ 415 | map(Sample.unionN, optional(floatFromNumber)), 416 | map(Sample.unionB, boolean), 417 | ], 418 | ) 419 | ); 420 | 421 | test("alt (doesn't evaluate second if first succeeds)", () => 422 | expect(Decode.(alt(string, decodeExplode, Sample.jsonString))) 423 | |> toEqual(Some(Sample.valString)) 424 | ); 425 | 426 | test("oneOf (succeeds on first)", () => 427 | expect(decodeUnion(Sample.jsonString)) 428 | |> toEqual(Some(Sample.(S(valString)))) 429 | ); 430 | 431 | test("oneOf (succeeds on last)", () => 432 | expect(decodeUnion(Sample.jsonTrue)) 433 | |> toEqual(Some(Sample.(B(valBool)))) 434 | ); 435 | 436 | test("oneOf (failure)", () => 437 | expect(decodeUnion(Sample.jsonPersonBill)) |> toEqual(None) 438 | ); 439 | 440 | test("fallback (used on missing field)", () => 441 | expect( 442 | Decode.(fallback(field("x", boolean), false, Sample.jsonDictEmpty)), 443 | ) 444 | |> toEqual(Some(false)) 445 | ); 446 | 447 | test("fallback (used on invalid inner data)", () => 448 | expect( 449 | Decode.(fallback(field("title", intFromNumber), 1, Sample.jsonJobCeo)), 450 | ) 451 | |> toEqual(Some(1)) 452 | ); 453 | }); 454 | 455 | describe("Decode map, apply, pure, flatMap, etc", () => { 456 | test("map (success)", () => 457 | expect( 458 | Decode.( 459 | map(List.String.contains("A"), list(string), Sample.jsonArrayString) 460 | ), 461 | ) 462 | |> toEqual(Some(true)) 463 | ); 464 | 465 | test("map (failure)", () => 466 | expect(Decode.(map(v => v == true, boolean, Sample.jsonNull))) 467 | |> toEqual(None) 468 | ); 469 | 470 | test("apply", () => 471 | expect( 472 | Decode.apply( 473 | _ => Some(v => List.length(v) == 3), 474 | Decode.(list(string)), 475 | Sample.jsonArrayString, 476 | ), 477 | ) 478 | |> toEqual(Some(true)) 479 | ); 480 | 481 | test("pure", () => { 482 | let decode = Decode.pure(3); 483 | expect(decode(Sample.jsonNull)) |> toEqual(Some(3)); 484 | }); 485 | 486 | test("flatMap (success)", () => 487 | expect( 488 | Decode.( 489 | Sample.jsonArrayString |> flatMap(List.head >> const, list(string)) 490 | ), 491 | ) 492 | |> toEqual(Some("A")) 493 | ); 494 | 495 | test("flatMap (failure on inner failure)", () => 496 | expect( 497 | Decode.( 498 | Sample.jsonArrayEmpty |> flatMap(List.head >> const, list(string)) 499 | ), 500 | ) 501 | |> toEqual(None) 502 | ); 503 | 504 | test("flatMap (failure on outer failure)", () => 505 | expect( 506 | Decode.(Sample.jsonNull |> flatMap(List.head >> const, list(string))), 507 | ) 508 | |> toEqual(None) 509 | ); 510 | }); 511 | 512 | describe("Decode records", () => { 513 | let noManager = (title, company, start) => 514 | Sample.makeJob(title, company, start, None); 515 | 516 | let decodeJobMap3 = 517 | Decode.( 518 | map3( 519 | noManager, 520 | field("title", string), 521 | field("companyName", string), 522 | field("startDate", date), 523 | ) 524 | ); 525 | 526 | let ((<$>), (<*>)) = Decode.(map, apply); 527 | let decodeJobInfix = 528 | Sample.makeJob 529 | <$> Decode.(field("title", string)) 530 | <*> Decode.(field("companyName", string)) 531 | <*> Decode.(field("startDate", date)) 532 | <*> Decode.pure(None); 533 | 534 | [@ocaml.warning "-3"] 535 | let rec decodeJobPipeline = json => 536 | Decode.Pipeline.( 537 | succeed(Sample.makeJob) 538 | |> field("title", string) 539 | |> field("companyName", string) 540 | |> field("startDate", date) 541 | |> optionalField("manager", decodeEmployeePipeline) 542 | |> run(json) 543 | ) 544 | [@ocaml.warning "-3"] 545 | and decodeEmployeePipeline = json => 546 | Decode.Pipeline.( 547 | succeed(Sample.makeEmployee) 548 | |> field("name", string) 549 | |> field("age", intFromNumber) 550 | |> field("job", decodeJobPipeline) 551 | |> run(json) 552 | ); 553 | 554 | [@ocaml.warning "-3"] 555 | let decodeJobPipelineRecovery = 556 | Decode.Pipeline.( 557 | succeed(Sample.makeJob) 558 | |> hardcoded("Title") 559 | |> fallbackField("x", string, "Company") 560 | |> at(["job", "manager", "job", "startDate"], date) 561 | |> hardcoded(None) 562 | ); 563 | 564 | test("map3", () => 565 | expect(decodeJobMap3(Sample.jsonJobCeo)) 566 | |> toEqual(Some(Sample.jobCeo)) 567 | ); 568 | 569 | test("lazy infix", () => 570 | expect(decodeJobInfix(Sample.jsonJobCeo)) 571 | |> toEqual(Some(Sample.jobCeo)) 572 | ); 573 | 574 | test("pipeline", () => 575 | expect(decodeEmployeePipeline(Sample.jsonPersonBill)) 576 | |> toEqual(Some(Sample.employeeBill)) 577 | ); 578 | 579 | test("pipeline hardcoded/fallback", () => 580 | expect(decodeJobPipelineRecovery(Sample.jsonPersonBill)) 581 | |> toEqual( 582 | Some( 583 | Sample.makeJob("Title", "Company", Sample.jobCeo.startDate, None), 584 | ), 585 | ) 586 | ); 587 | }); 588 | 589 | // here we import a gigantic json file (as raw json, to avoid slowing down the 590 | // compiler) 591 | [@bs.module] external bigjson: Js.Json.t = "./utils/BigJson.json"; 592 | 593 | describe("Big JSON array", () => 594 | test("is stack-safe", () => 595 | expect(Decode.array(Option.pure, bigjson) |> Option.isSome) 596 | |> toEqual(true) 597 | ) 598 | ); 599 | -------------------------------------------------------------------------------- /src/Decode_AsResult_OfParseError.rei: -------------------------------------------------------------------------------- 1 | [@deprecated 2 | "NonEmptyList helpers shouldn't be needed, but you can find them in Relude" 3 | ] 4 | module NonEmptyList: Decode_NonEmptyList.Nel; 5 | module ParseError = Decode_ParseError; 6 | 7 | let map: 8 | ('a => 'b, Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 9 | result('b, ParseError.failure); 10 | 11 | let apply: 12 | ( 13 | Js.Json.t => result('a => 'b, ParseError.failure), 14 | Js.Json.t => result('a, ParseError.failure), 15 | Js.Json.t 16 | ) => 17 | result('b, ParseError.failure); 18 | 19 | [@deprecated "Will be removed in 2.0 with the addition of and+"] 20 | let map2: 21 | ( 22 | ('a, 'b) => 'c, 23 | Js.Json.t => result('a, ParseError.failure), 24 | Js.Json.t => result('b, ParseError.failure), 25 | Js.Json.t 26 | ) => 27 | result('c, ParseError.failure); 28 | 29 | [@deprecated "Will be removed in 2.0 with the addition of and+"] 30 | let map3: 31 | ( 32 | ('a, 'b, 'c) => 'd, 33 | Js.Json.t => result('a, ParseError.failure), 34 | Js.Json.t => result('b, ParseError.failure), 35 | Js.Json.t => result('c, ParseError.failure), 36 | Js.Json.t 37 | ) => 38 | result('d, ParseError.failure); 39 | 40 | [@deprecated "Will be removed in 2.0 with the addition of and+"] 41 | let map4: 42 | ( 43 | ('a, 'b, 'c, 'd) => 'e, 44 | Js.Json.t => result('a, ParseError.failure), 45 | Js.Json.t => result('b, ParseError.failure), 46 | Js.Json.t => result('c, ParseError.failure), 47 | Js.Json.t => result('d, ParseError.failure), 48 | Js.Json.t 49 | ) => 50 | result('e, ParseError.failure); 51 | 52 | [@deprecated "Will be removed in 2.0 with the addition of and+"] 53 | let map5: 54 | ( 55 | ('a, 'b, 'c, 'd, 'e) => 'f, 56 | Js.Json.t => result('a, ParseError.failure), 57 | Js.Json.t => result('b, ParseError.failure), 58 | Js.Json.t => result('c, ParseError.failure), 59 | Js.Json.t => result('d, ParseError.failure), 60 | Js.Json.t => result('e, ParseError.failure), 61 | Js.Json.t 62 | ) => 63 | result('f, ParseError.failure); 64 | 65 | let pure: ('a, Js.Json.t) => result('a, ParseError.failure); 66 | 67 | let flatMap: 68 | ( 69 | ('a, Js.Json.t) => result('b, ParseError.failure), 70 | Js.Json.t => result('a, ParseError.failure), 71 | Js.Json.t 72 | ) => 73 | result('b, ParseError.failure); 74 | 75 | let alt: 76 | ( 77 | Js.Json.t => result('a, ParseError.failure), 78 | Js.Json.t => result('a, ParseError.failure), 79 | Js.Json.t 80 | ) => 81 | result('a, ParseError.failure); 82 | 83 | let okJson: Js.Json.t => result(Js.Json.t, ParseError.failure); 84 | let null: Js.Json.t => result(unit, ParseError.failure); 85 | let boolean: Js.Json.t => result(bool, ParseError.failure); 86 | let string: Js.Json.t => result(string, ParseError.failure); 87 | let floatFromNumber: Js.Json.t => result(float, ParseError.failure); 88 | let intFromNumber: Js.Json.t => result(int, ParseError.failure); 89 | let date: Js.Json.t => result(Js.Date.t, ParseError.failure); 90 | let literal: 91 | ( 92 | ('a, 'a) => bool, 93 | Js.Json.t => result('a, ParseError.failure), 94 | 'a, 95 | Js.Json.t 96 | ) => 97 | result('a, ParseError.failure); 98 | let literalString: (string, Js.Json.t) => result(string, ParseError.failure); 99 | let literalInt: (int, Js.Json.t) => result(int, ParseError.failure); 100 | let literalFloat: (float, Js.Json.t) => result(float, ParseError.failure); 101 | let literalBool: (bool, Js.Json.t) => result(bool, ParseError.failure); 102 | let literalTrue: Js.Json.t => result(bool, ParseError.failure); 103 | let literalFalse: Js.Json.t => result(bool, ParseError.failure); 104 | 105 | let union: 106 | ( 107 | ('a, Js.Json.t) => result('a, ParseError.failure), 108 | ('a, 'b), 109 | list(('a, 'b)), 110 | Js.Json.t 111 | ) => 112 | result('b, ParseError.failure); 113 | 114 | let stringUnion: 115 | ((string, 'a), list((string, 'a)), Js.Json.t) => 116 | result('a, ParseError.failure); 117 | 118 | let intUnion: 119 | ((int, 'a), list((int, 'a)), Js.Json.t) => result('a, ParseError.failure); 120 | 121 | [@deprecated "Use literal instead"] 122 | let variantFromJson: 123 | ( 124 | Js.Json.t => result('a, ParseError.failure), 125 | 'a => option('b), 126 | Js.Json.t 127 | ) => 128 | result('b, ParseError.failure); 129 | 130 | [@deprecated "Use stringUnion instead"] 131 | let variantFromString: 132 | (string => option('a), Js.Json.t) => result('a, ParseError.failure); 133 | 134 | [@deprecated "Use literalInt and alt/oneOf instead"] 135 | let variantFromInt: 136 | (int => option('a), Js.Json.t) => result('a, ParseError.failure); 137 | 138 | let optional: 139 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 140 | result(option('a), ParseError.failure); 141 | 142 | let array: 143 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 144 | result(array('a), ParseError.failure); 145 | 146 | let arrayJson: Js.Json.t => result(array(Js.Json.t), ParseError.failure); 147 | 148 | let list: 149 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 150 | result(list('a), ParseError.failure); 151 | 152 | let listJson: Js.Json.t => result(list(Js.Json.t), ParseError.failure); 153 | 154 | let arrayAt: 155 | (int, Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 156 | result('a, ParseError.failure); 157 | 158 | [@deprecated "Use arrayAt instead"] 159 | let tuple: 160 | ( 161 | Js.Json.t => result('a, ParseError.failure), 162 | Js.Json.t => result('b, ParseError.failure), 163 | Js.Json.t 164 | ) => 165 | result(('a, 'b), ParseError.failure); 166 | 167 | [@deprecated "Use arrayAt instead"] 168 | let tuple2: 169 | ( 170 | Js.Json.t => result('a, ParseError.failure), 171 | Js.Json.t => result('b, ParseError.failure), 172 | Js.Json.t 173 | ) => 174 | result(('a, 'b), ParseError.failure); 175 | 176 | [@deprecated "Use arrayAt instead"] 177 | let tuple3: 178 | ( 179 | Js.Json.t => result('a, ParseError.failure), 180 | Js.Json.t => result('b, ParseError.failure), 181 | Js.Json.t => result('c, ParseError.failure), 182 | Js.Json.t 183 | ) => 184 | result(('a, 'b, 'c), ParseError.failure); 185 | 186 | [@deprecated "Use arrayAt instead"] 187 | let tuple4: 188 | ( 189 | Js.Json.t => result('a, ParseError.failure), 190 | Js.Json.t => result('b, ParseError.failure), 191 | Js.Json.t => result('c, ParseError.failure), 192 | Js.Json.t => result('d, ParseError.failure), 193 | Js.Json.t 194 | ) => 195 | result(('a, 'b, 'c, 'd), ParseError.failure); 196 | 197 | [@deprecated "Use arrayAt instead"] 198 | let tuple5: 199 | ( 200 | Js.Json.t => result('a, ParseError.failure), 201 | Js.Json.t => result('b, ParseError.failure), 202 | Js.Json.t => result('c, ParseError.failure), 203 | Js.Json.t => result('d, ParseError.failure), 204 | Js.Json.t => result('e, ParseError.failure), 205 | Js.Json.t 206 | ) => 207 | result(('a, 'b, 'c, 'd, 'e), ParseError.failure); 208 | 209 | [@deprecated "Use arrayAt instead"] 210 | let tupleAtLeast2: 211 | ( 212 | Js.Json.t => result('a, ParseError.failure), 213 | Js.Json.t => result('b, ParseError.failure), 214 | Js.Json.t 215 | ) => 216 | result(('a, 'b), ParseError.failure); 217 | 218 | [@deprecated "Use arrayAt instead"] 219 | let tupleAtLeast3: 220 | ( 221 | Js.Json.t => result('a, ParseError.failure), 222 | Js.Json.t => result('b, ParseError.failure), 223 | Js.Json.t => result('c, ParseError.failure), 224 | Js.Json.t 225 | ) => 226 | result(('a, 'b, 'c), ParseError.failure); 227 | 228 | [@deprecated "Use arrayAt instead"] 229 | let tupleAtLeast4: 230 | ( 231 | Js.Json.t => result('a, ParseError.failure), 232 | Js.Json.t => result('b, ParseError.failure), 233 | Js.Json.t => result('c, ParseError.failure), 234 | Js.Json.t => result('d, ParseError.failure), 235 | Js.Json.t 236 | ) => 237 | result(('a, 'b, 'c, 'd), ParseError.failure); 238 | 239 | [@deprecated "Use arrayAt instead"] 240 | let tupleAtLeast5: 241 | ( 242 | Js.Json.t => result('a, ParseError.failure), 243 | Js.Json.t => result('b, ParseError.failure), 244 | Js.Json.t => result('c, ParseError.failure), 245 | Js.Json.t => result('d, ParseError.failure), 246 | Js.Json.t => result('e, ParseError.failure), 247 | Js.Json.t 248 | ) => 249 | result(('a, 'b, 'c, 'd, 'e), ParseError.failure); 250 | 251 | [@deprecated "Use field instead and construct your own tuple"] 252 | let tupleFromFields: 253 | ( 254 | (string, Js.Json.t => result('a, ParseError.failure)), 255 | (string, Js.Json.t => result('b, ParseError.failure)), 256 | Js.Json.t 257 | ) => 258 | result(('a, 'b), ParseError.failure); 259 | 260 | let dict: 261 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 262 | result(Js.Dict.t('a), ParseError.failure); 263 | 264 | let dictJson: Js.Json.t => result(Js.Dict.t(Js.Json.t), ParseError.failure); 265 | 266 | [@deprecated "Use dict instead, and convert the Dict to a Map"] 267 | let stringMap: 268 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 269 | result(Belt.Map.String.t('a), ParseError.failure); 270 | 271 | let at: 272 | (list(string), Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 273 | result('a, ParseError.failure); 274 | 275 | let field: 276 | (string, Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 277 | result('a, ParseError.failure); 278 | 279 | let optionalField: 280 | (string, Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 281 | result(option('a), ParseError.failure); 282 | 283 | let fallback: 284 | (Js.Json.t => result('a, ParseError.failure), 'a, Js.Json.t) => 285 | result('a, ParseError.failure); 286 | 287 | let oneOf: 288 | ( 289 | Js.Json.t => result('a, ParseError.failure), 290 | list(Js.Json.t => result('a, ParseError.failure)), 291 | Js.Json.t 292 | ) => 293 | result('a, ParseError.failure); 294 | 295 | let hush: 296 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => option('a); 297 | 298 | [@deprecated "Will be removed in favor up the upcoming addition of letops"] 299 | module Pipeline: { 300 | let succeed: ('a, Js.Json.t) => result('a, ParseError.failure); 301 | 302 | let pipe: 303 | ( 304 | Js.Json.t => result('a, ParseError.failure), 305 | Js.Json.t => result('a => 'b, ParseError.failure), 306 | Js.Json.t 307 | ) => 308 | result('b, ParseError.failure); 309 | 310 | let field: 311 | ( 312 | string, 313 | Js.Json.t => result('a, ParseError.failure), 314 | Js.Json.t => result('a => 'b, ParseError.failure), 315 | Js.Json.t 316 | ) => 317 | result('b, ParseError.failure); 318 | 319 | let at: 320 | ( 321 | list(string), 322 | Js.Json.t => result('a, ParseError.failure), 323 | Js.Json.t => result('a => 'b, ParseError.failure), 324 | Js.Json.t 325 | ) => 326 | result('b, ParseError.failure); 327 | 328 | let optionalField: 329 | ( 330 | string, 331 | Js.Json.t => result('a, ParseError.failure), 332 | Js.Json.t => result(option('a) => 'b, ParseError.failure), 333 | Js.Json.t 334 | ) => 335 | result('b, ParseError.failure); 336 | 337 | let fallbackField: 338 | ( 339 | string, 340 | Js.Json.t => result('a, ParseError.failure), 341 | 'a, 342 | Js.Json.t => result('a => 'b, ParseError.failure), 343 | Js.Json.t 344 | ) => 345 | result('b, ParseError.failure); 346 | 347 | let fallback: 348 | (Js.Json.t => result('a, ParseError.failure), 'a, Js.Json.t) => 349 | result('a, ParseError.failure); 350 | 351 | let hardcoded: 352 | ('a, Js.Json.t => result('a => 'c, ParseError.failure), Js.Json.t) => 353 | result('c, ParseError.failure); 354 | 355 | let run: 356 | (Js.Json.t, Js.Json.t => result('a, ParseError.failure)) => 357 | result('a, ParseError.failure); 358 | 359 | let map: 360 | ('a => 'b, Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 361 | result('b, ParseError.failure); 362 | 363 | let apply: 364 | ( 365 | Js.Json.t => result('a => 'b, ParseError.failure), 366 | Js.Json.t => result('a, ParseError.failure), 367 | Js.Json.t 368 | ) => 369 | result('b, ParseError.failure); 370 | 371 | let map2: 372 | ( 373 | ('a, 'b) => 'c, 374 | Js.Json.t => result('a, ParseError.failure), 375 | Js.Json.t => result('b, ParseError.failure), 376 | Js.Json.t 377 | ) => 378 | result('c, ParseError.failure); 379 | 380 | let map3: 381 | ( 382 | ('a, 'b, 'c) => 'd, 383 | Js.Json.t => result('a, ParseError.failure), 384 | Js.Json.t => result('b, ParseError.failure), 385 | Js.Json.t => result('c, ParseError.failure), 386 | Js.Json.t 387 | ) => 388 | result('d, ParseError.failure); 389 | 390 | let map4: 391 | ( 392 | ('a, 'b, 'c, 'd) => 'e, 393 | Js.Json.t => result('a, ParseError.failure), 394 | Js.Json.t => result('b, ParseError.failure), 395 | Js.Json.t => result('c, ParseError.failure), 396 | Js.Json.t => result('d, ParseError.failure), 397 | Js.Json.t 398 | ) => 399 | result('e, ParseError.failure); 400 | 401 | let map5: 402 | ( 403 | ('a, 'b, 'c, 'd, 'e) => 'f, 404 | Js.Json.t => result('a, ParseError.failure), 405 | Js.Json.t => result('b, ParseError.failure), 406 | Js.Json.t => result('c, ParseError.failure), 407 | Js.Json.t => result('d, ParseError.failure), 408 | Js.Json.t => result('e, ParseError.failure), 409 | Js.Json.t 410 | ) => 411 | result('f, ParseError.failure); 412 | 413 | let pure: ('a, Js.Json.t) => result('a, ParseError.failure); 414 | 415 | let flatMap: 416 | ( 417 | ('a, Js.Json.t) => result('b, ParseError.failure), 418 | Js.Json.t => result('a, ParseError.failure), 419 | Js.Json.t 420 | ) => 421 | result('b, ParseError.failure); 422 | 423 | let boolean: Js.Json.t => result(bool, ParseError.failure); 424 | let string: Js.Json.t => result(Js.String.t, ParseError.failure); 425 | let floatFromNumber: Js.Json.t => result(float, ParseError.failure); 426 | let intFromNumber: Js.Json.t => result(int, ParseError.failure); 427 | let date: Js.Json.t => result(Js.Date.t, ParseError.failure); 428 | let variantFromJson: 429 | ( 430 | Js.Json.t => result('a, ParseError.failure), 431 | 'a => option('b), 432 | Js.Json.t 433 | ) => 434 | result('b, ParseError.failure); 435 | let variantFromString: 436 | (string => option('a), Js.Json.t) => result('a, ParseError.failure); 437 | let variantFromInt: 438 | (int => option('a), Js.Json.t) => result('a, ParseError.failure); 439 | 440 | let optional: 441 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 442 | result(option('a), ParseError.failure); 443 | 444 | let array: 445 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 446 | result(array('a), ParseError.failure); 447 | 448 | let list: 449 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 450 | result(list('a), ParseError.failure); 451 | 452 | let tuple: 453 | ( 454 | Js.Json.t => result('a, ParseError.failure), 455 | Js.Json.t => result('b, ParseError.failure), 456 | Js.Json.t 457 | ) => 458 | result(('a, 'b), ParseError.failure); 459 | 460 | let tuple2: 461 | ( 462 | Js.Json.t => result('a, ParseError.failure), 463 | Js.Json.t => result('b, ParseError.failure), 464 | Js.Json.t 465 | ) => 466 | result(('a, 'b), ParseError.failure); 467 | 468 | let tuple3: 469 | ( 470 | Js.Json.t => result('a, ParseError.failure), 471 | Js.Json.t => result('b, ParseError.failure), 472 | Js.Json.t => result('c, ParseError.failure), 473 | Js.Json.t 474 | ) => 475 | result(('a, 'b, 'c), ParseError.failure); 476 | 477 | let tuple4: 478 | ( 479 | Js.Json.t => result('a, ParseError.failure), 480 | Js.Json.t => result('b, ParseError.failure), 481 | Js.Json.t => result('c, ParseError.failure), 482 | Js.Json.t => result('d, ParseError.failure), 483 | Js.Json.t 484 | ) => 485 | result(('a, 'b, 'c, 'd), ParseError.failure); 486 | 487 | let tuple5: 488 | ( 489 | Js.Json.t => result('a, ParseError.failure), 490 | Js.Json.t => result('b, ParseError.failure), 491 | Js.Json.t => result('c, ParseError.failure), 492 | Js.Json.t => result('d, ParseError.failure), 493 | Js.Json.t => result('e, ParseError.failure), 494 | Js.Json.t 495 | ) => 496 | result(('a, 'b, 'c, 'd, 'e), ParseError.failure); 497 | 498 | let tupleAtLeast2: 499 | ( 500 | Js.Json.t => result('a, ParseError.failure), 501 | Js.Json.t => result('b, ParseError.failure), 502 | Js.Json.t 503 | ) => 504 | result(('a, 'b), ParseError.failure); 505 | 506 | let tupleAtLeast3: 507 | ( 508 | Js.Json.t => result('a, ParseError.failure), 509 | Js.Json.t => result('b, ParseError.failure), 510 | Js.Json.t => result('c, ParseError.failure), 511 | Js.Json.t 512 | ) => 513 | result(('a, 'b, 'c), ParseError.failure); 514 | 515 | let tupleAtLeast4: 516 | ( 517 | Js.Json.t => result('a, ParseError.failure), 518 | Js.Json.t => result('b, ParseError.failure), 519 | Js.Json.t => result('c, ParseError.failure), 520 | Js.Json.t => result('d, ParseError.failure), 521 | Js.Json.t 522 | ) => 523 | result(('a, 'b, 'c, 'd), ParseError.failure); 524 | 525 | let tupleAtLeast5: 526 | ( 527 | Js.Json.t => result('a, ParseError.failure), 528 | Js.Json.t => result('b, ParseError.failure), 529 | Js.Json.t => result('c, ParseError.failure), 530 | Js.Json.t => result('d, ParseError.failure), 531 | Js.Json.t => result('e, ParseError.failure), 532 | Js.Json.t 533 | ) => 534 | result(('a, 'b, 'c, 'd, 'e), ParseError.failure); 535 | 536 | let tupleFromFields: 537 | ( 538 | (string, Js.Json.t => result('a, ParseError.failure)), 539 | (string, Js.Json.t => result('b, ParseError.failure)), 540 | Js.Json.t 541 | ) => 542 | result(('a, 'b), ParseError.failure); 543 | 544 | let dict: 545 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 546 | result(Js.Dict.t('a), ParseError.failure); 547 | 548 | let stringMap: 549 | (Js.Json.t => result('a, ParseError.failure), Js.Json.t) => 550 | result(Belt.Map.String.t('a), ParseError.failure); 551 | 552 | let oneOf: 553 | ( 554 | Js.Json.t => result('a, ParseError.failure), 555 | list(Js.Json.t => result('a, ParseError.failure)), 556 | Js.Json.t 557 | ) => 558 | result('a, ParseError.failure); 559 | }; 560 | --------------------------------------------------------------------------------