├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .tidyrc.json ├── LICENSE ├── README.md ├── bower.json ├── package.json ├── src ├── JSON.js ├── JSON.purs └── JSON │ ├── Array.js │ ├── Array.purs │ ├── Gen.purs │ ├── Internal.js │ ├── Internal.purs │ ├── Object.js │ ├── Object.purs │ └── Path.purs └── test └── Main.purs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module" 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "strict": [2, "global"], 9 | "block-scoped-var": 2, 10 | "consistent-return": 2, 11 | "eqeqeq": [2, "smart"], 12 | "guard-for-in": 2, 13 | "no-caller": 2, 14 | "no-extend-native": 2, 15 | "no-loop-func": 2, 16 | "no-new": 2, 17 | "no-param-reassign": 2, 18 | "no-return-assign": 2, 19 | "no-unused-expressions": 2, 20 | "no-use-before-define": 2, 21 | "radix": [2, "always"], 22 | "indent": [2, 2], 23 | "quotes": [2, "double"], 24 | "semi": [2, "always"] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - uses: purescript-contrib/setup-purescript@main 14 | 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: "12" 18 | 19 | - name: Install dependencies 20 | run: | 21 | npm install -g bower 22 | npm install 23 | bower install --production 24 | 25 | - name: Build source 26 | run: npm run-script build 27 | 28 | - name: Run tests 29 | run: | 30 | bower install 31 | npm run-script test --if-present 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.* 2 | !/.gitignore 3 | !/.github 4 | !/.eslintrc.json 5 | !/.editorconfig 6 | !/.tidyrc.json 7 | /bower_components/ 8 | /node_modules/ 9 | /output/ 10 | package-lock.json 11 | yarn.lock 12 | -------------------------------------------------------------------------------- /.tidyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "importSort": "ide", 3 | "importWrap": "source", 4 | "indent": 2, 5 | "operatorsFile": null, 6 | "ribbon": 1, 7 | "typeArrowPlacement": "first", 8 | "unicode": "never", 9 | "width": null 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 PureScript 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-json 2 | 3 | [![Build status](https://github.com/purescript/purescript-json/workflows/CI/badge.svg?branch=master)](https://github.com/purescript/purescript-json/actions?query=workflow%3ACI+branch%3Amaster) 4 | 5 | Standard types and basic operations for working with JSON. 6 | 7 | For efficiency and performance reasons this library provides an interface for working with JSON without using PureScript ADTs, and instead operates on the underlying representation. 8 | 9 | ## Differences from Argonaut 10 | 11 | This library is similar to the traditionally used `argonaut-core` library, but has been implemented with an eye to making it backend agnostic. As such, it does not use `Foreign.Object` as the representation for JSON objects, does not use `Array JSON`, and instead provides its own `JObject` and `JArray` types. 12 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-json", 3 | "license": "MIT", 4 | "keywords": [ 5 | "purescript", 6 | "argonaut", 7 | "json" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/garyb/purescript-json.git" 12 | }, 13 | "ignore": [ 14 | "**/.*", 15 | "bower_components", 16 | "node_modules", 17 | "output", 18 | "test", 19 | "bower.json", 20 | "package.json" 21 | ], 22 | "dependencies": { 23 | "purescript-prelude": "^6.0.1", 24 | "purescript-functions": "^6.0.0", 25 | "purescript-integers": "^6.0.0", 26 | "purescript-maybe": "^6.0.0", 27 | "purescript-either": "^6.1.0", 28 | "purescript-tuples": "^7.0.0", 29 | "purescript-foldable-traversable": "^6.0.0", 30 | "purescript-gen": "^4.0.0", 31 | "purescript-strings": "^6.0.1", 32 | "purescript-unfoldable": "^6.0.0" 33 | }, 34 | "devDependencies": { 35 | "purescript-assert": "^6.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf output && rimraf .pulp-cache", 5 | "build": "eslint src && purs-tidy check --config-require src/**/*.purs && pulp build -- --censor-lib --strict", 6 | "test": "pulp test" 7 | }, 8 | "devDependencies": { 9 | "eslint": "^8.40.0", 10 | "pulp": "^16.0.2", 11 | "purescript-psa": "^0.8.2", 12 | "purs-tidy": "^0.9.3", 13 | "rimraf": "^5.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/JSON.js: -------------------------------------------------------------------------------- 1 | const coerce = (x) => x; 2 | 3 | export const _null = null; 4 | 5 | export const fromBoolean = coerce; 6 | 7 | export const fromInt = coerce; 8 | 9 | export const fromString = coerce; 10 | 11 | export const fromJArray = coerce; 12 | 13 | export const fromJObject = coerce; 14 | 15 | export const print = (j) => JSON.stringify(j); 16 | 17 | export const printIndented = (j) => JSON.stringify(j, null, 2); 18 | -------------------------------------------------------------------------------- /src/JSON.purs: -------------------------------------------------------------------------------- 1 | module JSON 2 | ( parse 3 | , null 4 | , fromBoolean 5 | , fromNumber 6 | , fromNumberWithDefault 7 | , fromInt 8 | , fromString 9 | , fromArray 10 | , fromJArray 11 | , fromJObject 12 | , case_ 13 | , toNull 14 | , toBoolean 15 | , toNumber 16 | , toInt 17 | , toString 18 | , toArray 19 | , toJArray 20 | , toJObject 21 | , print 22 | , printIndented 23 | , module Exports 24 | ) where 25 | 26 | import Prelude 27 | 28 | import Data.Either (Either(..)) 29 | import Data.Function.Uncurried (runFn2, runFn3, runFn7) 30 | import Data.Int as Int 31 | import Data.Maybe (Maybe(..)) 32 | import JSON.Internal (JArray, JObject, JSON) 33 | import JSON.Internal (JArray, JObject, JSON, isNull) as Exports 34 | import JSON.Internal as Internal 35 | 36 | -- | Attempts to parse a string as a JSON value. If parsing fails, an error message detailing the 37 | -- | cause may be returned in the `Left` of the result. 38 | parse :: String -> Either String JSON 39 | parse j = runFn3 Internal._parse Left Right j 40 | 41 | -- | The JSON `null` value. 42 | null :: JSON 43 | null = _null 44 | 45 | -- | The JSON `null` value. 46 | foreign import _null :: JSON 47 | 48 | -- | Converts a `Boolean` into `JSON`. 49 | foreign import fromBoolean :: Boolean -> JSON 50 | 51 | -- | Converts a `Number` into `JSON`. 52 | -- | 53 | -- | The PureScript `Number` type admits infinities and a `NaN` value which are not allowed in JSON, 54 | -- | so when encountered, this function will treat those values as 0. 55 | fromNumber :: Number -> JSON 56 | fromNumber n = runFn2 Internal._fromNumberWithDefault 0 n 57 | 58 | -- | Creates a `Number` into `JSON`, using a fallback `Int` value for cases where the 59 | -- | PureScript number value is not valid for JSON (`NaN`, `infinity`). 60 | fromNumberWithDefault :: Int -> Number -> JSON 61 | fromNumberWithDefault fallback n = runFn2 Internal._fromNumberWithDefault fallback n 62 | 63 | -- | Converts an `Int` into `JSON`. 64 | -- | 65 | -- | Note: JSON doesn't have a concept of integers. This is provided 66 | -- | as a convenience to avoid having to convert `Int` to `Number` before creating a `JSON` value. 67 | foreign import fromInt :: Int -> JSON 68 | 69 | -- | Converts a `String` into `JSON`. 70 | -- | 71 | -- | **Note**: this does not parse a string as a JSON value, it takes a PureScript string and 72 | -- | produces the corresponding `JSON` value for that string, similar to the other functions like 73 | -- | `fromBoolean` and `fromNumber`. 74 | -- | 75 | -- | To take a string that contains printed JSON and turn it into a `JSON` value, see 76 | -- | [`parse`](#v:parse). 77 | foreign import fromString :: String -> JSON 78 | 79 | -- | Converts a `JArray` into `JSON`. 80 | foreign import fromJArray :: JArray -> JSON 81 | 82 | -- | Converts an array of `JSON` values into `JSON`. 83 | fromArray :: Array JSON -> JSON 84 | fromArray js = fromJArray (Internal.fromArray js) 85 | 86 | -- | Converts a `JObject` into `JSON`. 87 | foreign import fromJObject :: JObject -> JSON 88 | 89 | -- | Performs case analysis on a JSON value. 90 | -- | 91 | -- | As the `JSON` type is not a PureScript sum type, pattern matching cannot be used to 92 | -- | discriminate between the potential varieties of value. This function provides an equivalent 93 | -- | mechanism by accepting functions that deal with each variety, similar to an exaustive `case` 94 | -- | statement. 95 | -- | 96 | -- | The `Unit` case is for `null` values. 97 | case_ 98 | :: forall a 99 | . (Unit -> a) 100 | -> (Boolean -> a) 101 | -> (Number -> a) 102 | -> (String -> a) 103 | -> (JArray -> a) 104 | -> (JObject -> a) 105 | -> JSON 106 | -> a 107 | case_ a b c d e f json = runFn7 Internal._case a b c d e f json 108 | 109 | fail :: forall a b. a -> Maybe b 110 | fail _ = Nothing 111 | 112 | -- | Converts a `JSON` value to `Null` if the `JSON` is `null`. 113 | toNull :: JSON -> Maybe Unit 114 | toNull json = runFn7 Internal._case Just fail fail fail fail fail json 115 | 116 | -- | Converts a `JSON` value to `Boolean` if the `JSON` is a boolean. 117 | toBoolean :: JSON -> Maybe Boolean 118 | toBoolean json = runFn7 Internal._case fail Just fail fail fail fail json 119 | 120 | -- | Converts a `JSON` value to `Number` if the `JSON` is a number. 121 | toNumber :: JSON -> Maybe Number 122 | toNumber json = runFn7 Internal._case fail fail Just fail fail fail json 123 | 124 | -- | Converts a `JSON` `Number` into an `Int`. 125 | -- | 126 | -- | This is provided for convenience only. 127 | toInt :: JSON -> Maybe Int 128 | toInt = toNumber >=> Int.fromNumber 129 | 130 | -- | Converts a `JSON` value to `String` if the `JSON` is a string. 131 | toString :: JSON -> Maybe String 132 | toString json = runFn7 Internal._case fail fail fail Just fail fail json 133 | 134 | -- | Converts a `JSON` value to `JArray` if the `JSON` is an array. 135 | toJArray :: JSON -> Maybe JArray 136 | toJArray json = runFn7 Internal._case fail fail fail fail Just fail json 137 | 138 | -- | Converts a `JSON` value to `Array JSON` if the `JSON` is an array. 139 | toArray :: JSON -> Maybe (Array JSON) 140 | toArray json = Internal.toArray <$> toJArray json 141 | 142 | -- | Converts a `JSON` value to `Object` if the `JSON` is an object. 143 | toJObject :: JSON -> Maybe JObject 144 | toJObject json = runFn7 Internal._case fail fail fail fail fail Just json 145 | 146 | -- | Prints a JSON value as a compact (single line) string. 147 | foreign import print :: JSON -> String 148 | 149 | -- | Prints a JSON value as a "pretty" string with newlines and indentation. 150 | foreign import printIndented :: JSON -> String 151 | -------------------------------------------------------------------------------- /src/JSON/Array.js: -------------------------------------------------------------------------------- 1 | export const singleton = (x) => [x]; 2 | -------------------------------------------------------------------------------- /src/JSON/Array.purs: -------------------------------------------------------------------------------- 1 | module JSON.Array 2 | ( fromFoldable 3 | , singleton 4 | , index 5 | , toUnfoldable 6 | , module Exports 7 | ) where 8 | 9 | import Data.Array as Array 10 | import Data.Foldable (class Foldable) 11 | import Data.Function.Uncurried (runFn4) 12 | import Data.Maybe (Maybe(..)) 13 | import Data.Unfoldable (class Unfoldable) 14 | import JSON.Internal (JArray, JSON, _index, fromArray, toArray) 15 | import JSON.Internal (JArray, empty, fromArray, length, toArray) as Exports 16 | 17 | -- | Creates a `JArray` from a `Foldable` source of `JSON`. 18 | fromFoldable :: forall f. Foldable f => f JSON -> JArray 19 | fromFoldable js = fromArray (Array.fromFoldable js) 20 | 21 | -- | Creates a `JArray` with a single entry. 22 | foreign import singleton :: JSON -> JArray 23 | 24 | -- | Attempts to read a value from the specified index of a `JArray`. 25 | index :: Int -> JArray -> Maybe JSON 26 | index ix arr = runFn4 _index Nothing Just ix arr 27 | 28 | -- | Unfolds a `JArray` into `JSON` items 29 | toUnfoldable :: forall f. Unfoldable f => JArray -> f JSON 30 | toUnfoldable js = Array.toUnfoldable (toArray js) 31 | -------------------------------------------------------------------------------- /src/JSON/Gen.purs: -------------------------------------------------------------------------------- 1 | module JSON.Gen where 2 | 3 | import Prelude 4 | 5 | import Control.Lazy (class Lazy, defer) 6 | import Control.Monad.Gen (class MonadGen) 7 | import Control.Monad.Gen as Gen 8 | import Control.Monad.Rec.Class (class MonadRec) 9 | import Data.NonEmpty ((:|)) 10 | import Data.String.Gen (genUnicodeString) 11 | import Data.Tuple (Tuple(..)) 12 | import JSON as J 13 | import JSON.Array as JArray 14 | import JSON.Object as JObject 15 | 16 | -- | A generator for random `JSON` values of any variety. 17 | genJSON :: forall m. MonadGen m => MonadRec m => Lazy (m J.JSON) => m J.JSON 18 | genJSON = Gen.resize (min 5) $ Gen.sized genJSON' 19 | where 20 | genJSON' :: Int -> m J.JSON 21 | genJSON' size 22 | | size > 1 = Gen.resize (_ - 1) (Gen.choose genArray genObject) 23 | | otherwise = genLeaf 24 | 25 | -- | A generator for JSON arrays containing items based on the passed generator. 26 | genJArrayOf :: forall m. MonadGen m => MonadRec m => m J.JSON -> m J.JArray 27 | genJArrayOf inner = JArray.fromArray <$> Gen.unfoldable inner 28 | 29 | -- | A generator for JSON arrays containing random items. 30 | genJArray :: forall m. MonadGen m => MonadRec m => Lazy (m J.JSON) => m J.JArray 31 | genJArray = genJArrayOf (defer \_ -> genJSON) 32 | 33 | -- | A generator for JSON vaues that are arrays containing items based on the passed generator. 34 | genArrayOf :: forall m. MonadGen m => MonadRec m => m J.JSON -> m J.JSON 35 | genArrayOf inner = J.fromJArray <$> genJArrayOf inner 36 | 37 | -- | A generator for JSON vaues that are arrays containing random items. 38 | genArray :: forall m. MonadGen m => MonadRec m => Lazy (m J.JSON) => m J.JSON 39 | genArray = J.fromJArray <$> genJArray 40 | 41 | -- | A generator for JSON objects containing entries based on the passed generator. 42 | genJObjectOf :: forall m. MonadGen m => MonadRec m => m (Tuple String J.JSON) -> m J.JObject 43 | genJObjectOf inner = JObject.fromEntries <$> (Gen.unfoldable inner) 44 | 45 | -- | A generator for JSON objects containing random entries. 46 | genJObject :: forall m. MonadGen m => MonadRec m => Lazy (m J.JSON) => m J.JObject 47 | genJObject = genJObjectOf (Tuple <$> genUnicodeString <*> defer \_ -> genJSON) 48 | 49 | -- | A generator for JSON values that are objects containing entries based on the passed generator. 50 | genObjectOf :: forall m. MonadGen m => MonadRec m => m (Tuple String J.JSON) -> m J.JSON 51 | genObjectOf inner = J.fromJObject <$> genJObjectOf inner 52 | 53 | -- | A generator for JSON values that are objects containing random entries. 54 | genObject :: forall m. MonadGen m => MonadRec m => Lazy (m J.JSON) => m J.JSON 55 | genObject = J.fromJObject <$> genJObject 56 | 57 | -- | A generator for JSON leaf (null, boolean, number, string) values. 58 | genLeaf :: forall m. MonadGen m => MonadRec m => m J.JSON 59 | genLeaf = Gen.oneOf $ pure J.null :| [ genBoolean, genNumber, genString ] 60 | 61 | -- | A generator for JSON booleans. 62 | genBoolean :: forall m. MonadGen m => m J.JSON 63 | genBoolean = J.fromBoolean <$> Gen.chooseBool 64 | 65 | -- | A generator for JSON numbers. 66 | genNumber :: forall m. MonadGen m => m J.JSON 67 | genNumber = J.fromNumber <$> Gen.chooseFloat (-1000000.0) 1000000.0 68 | 69 | -- | A generator for JSON integers. 70 | genInt :: forall m. MonadGen m => m J.JSON 71 | genInt = J.fromInt <$> Gen.chooseInt (-1000000) 1000000 72 | 73 | -- | A generator for JSON strings. 74 | genString :: forall m. MonadGen m => MonadRec m => m J.JSON 75 | genString = J.fromString <$> genUnicodeString 76 | -------------------------------------------------------------------------------- /src/JSON/Internal.js: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString; 2 | const hasOwnProperty = Object.prototype.hasOwnProperty; 3 | 4 | export const _parse = (left, right, s) => { 5 | try { 6 | return right(JSON.parse(s)); 7 | } 8 | catch (e) { 9 | return left(e.message); 10 | } 11 | }; 12 | 13 | export const _fromNumberWithDefault = (fallback, n) => isNaN(n) || !isFinite(n) ? fallback : n; 14 | 15 | export const _case = (isNull, isBool, isNum, isStr, isArr, isObj, j) => { 16 | if (j == null) return isNull(null); 17 | const ty = typeof j; 18 | if (ty === "boolean") return isBool(j); 19 | if (ty === "number") return isNum(j); 20 | if (ty === "string") return isStr(j); 21 | if (toString.call(j) === "[object Array]") return isArr(j); 22 | return isObj(j); 23 | }; 24 | 25 | export const toArray = (js) => js; 26 | export const fromArray = (js) => js; 27 | 28 | export const _fromEntries = (fst, snd, entries) => { 29 | const result = {}; 30 | for (var i = 0; i < entries.length; i++) { 31 | result[fst(entries[i])] = snd(entries[i]); 32 | } 33 | return result; 34 | }; 35 | 36 | export const _insert = (k, v, obj) => 37 | Object.assign({ [k]: v }, obj); 38 | 39 | export const _delete = (k, obj) => { 40 | if (!Object.hasOwn(obj, k)) return obj; 41 | const result = Object.assign({}, obj); 42 | delete result[k]; 43 | return result; 44 | }; 45 | 46 | export const _entries = (tuple, obj) => 47 | Object.entries(obj).map(([k, v]) => tuple(k)(v)); 48 | 49 | export const _lookup = (nothing, just, key, obj) => 50 | hasOwnProperty.call(obj, key) ? just(obj[key]) : nothing; 51 | 52 | export const empty = []; 53 | 54 | export const length = (arr) => arr.length; 55 | 56 | export const _index = (nothing, just, ix, arr) => 57 | ix >= 0 && ix < arr.length ? just(arr[ix]) : nothing; 58 | 59 | export const _append = (xs, ys) => xs.concat(ys); 60 | 61 | export const isNull = (json) => json == null; 62 | -------------------------------------------------------------------------------- /src/JSON/Internal.purs: -------------------------------------------------------------------------------- 1 | module JSON.Internal where 2 | 3 | import Prelude 4 | 5 | import Data.Function.Uncurried (Fn2, Fn3, Fn4, Fn7, runFn2, runFn7) 6 | import Data.Tuple (Tuple(..)) 7 | 8 | -- | A type that represents all varieties of JSON value. 9 | -- | 10 | -- | This is not a PureScript sum type, instead the underlying JSON representation is used for 11 | -- | efficiency and performance reasons. 12 | foreign import data JSON :: Type 13 | 14 | instance Eq JSON where 15 | eq a b = 16 | runFn7 _case 17 | (\x -> runFn7 _case (eq x) _false _false _false _false _false a) 18 | (\x -> runFn7 _case _false (eq x) _false _false _false _false a) 19 | (\x -> runFn7 _case _false _false (eq x) _false _false _false a) 20 | (\x -> runFn7 _case _false _false _false (eq x) _false _false a) 21 | (\x -> runFn7 _case _false _false _false _false (eq x) _false a) 22 | (\x -> runFn7 _case _false _false _false _false _false (eq x) a) 23 | b 24 | 25 | _false :: forall a. a -> Boolean 26 | _false _ = false 27 | 28 | instance Ord JSON where 29 | compare a b = 30 | runFn7 _case 31 | (\x -> runFn7 _case (compare x) _gt _gt _gt _gt _gt b) 32 | (\x -> runFn7 _case _lt (compare x) _gt _gt _gt _gt b) 33 | (\x -> runFn7 _case _lt _lt (compare x) _gt _gt _gt b) 34 | (\x -> runFn7 _case _lt _lt _lt (compare x) _gt _gt b) 35 | (\x -> runFn7 _case _lt _lt _lt _lt (compare x) _gt b) 36 | (\x -> runFn7 _case _lt _lt _lt _lt _lt (compare x) b) 37 | a 38 | 39 | _lt :: forall a. a -> Ordering 40 | _lt _ = LT 41 | 42 | _gt :: forall a. a -> Ordering 43 | _gt _ = GT 44 | 45 | -- | A type that represents JSON arrays. Similar to the JSON type, this is not a PureScript type, 46 | -- | but represents the underlying representation for JSON arrays. 47 | foreign import data JArray :: Type 48 | 49 | -- | Converts a `JArray` into an `Array` of `JSON` values 50 | foreign import toArray :: JArray -> Array JSON 51 | 52 | -- | Converts an `Array` of `JSON` values into a `JArray`. 53 | foreign import fromArray :: Array JSON -> JArray 54 | 55 | -- | An empty `JArray`. 56 | foreign import empty :: JArray 57 | 58 | instance Eq JArray where 59 | eq xs ys 60 | | length xs == length ys = eq (toArray xs) (toArray ys) 61 | | otherwise = false 62 | 63 | instance Ord JArray where 64 | compare x y = compare (toArray x) (toArray y) 65 | 66 | instance Semigroup JArray where 67 | append xs ys = runFn2 _append xs ys 68 | 69 | instance Monoid JArray where 70 | mempty = empty 71 | 72 | -- | A type that represents JSON objects. Similar to the JSON type, this is not a PureScript type, 73 | -- | but represents the underlying representation for JSON objects. 74 | foreign import data JObject :: Type 75 | 76 | instance Eq JObject where 77 | eq x y = eq (runFn2 _entries Tuple x) (runFn2 _entries Tuple y) 78 | 79 | instance Ord JObject where 80 | compare x y = compare (runFn2 _entries Tuple x) (runFn2 _entries Tuple y) 81 | 82 | foreign import _parse 83 | :: forall f 84 | . Fn3 85 | (forall a b. a -> f a b) 86 | (forall a b. b -> f a b) 87 | String 88 | (f String JSON) 89 | 90 | foreign import _fromNumberWithDefault :: Fn2 Int Number JSON 91 | 92 | foreign import _case 93 | :: forall a 94 | . Fn7 95 | (Unit -> a) 96 | (Boolean -> a) 97 | (Number -> a) 98 | (String -> a) 99 | (JArray -> a) 100 | (JObject -> a) 101 | JSON 102 | a 103 | 104 | foreign import _insert :: Fn3 String JSON JObject JObject 105 | 106 | foreign import _delete :: Fn2 String JObject JObject 107 | 108 | foreign import _fromEntries 109 | :: forall f 110 | . Fn3 111 | (forall x y. f x y -> x) 112 | (forall x y. f x y -> y) 113 | (Prim.Array (f String JSON)) 114 | JObject 115 | 116 | foreign import _entries :: forall c. Fn2 (String -> JSON -> c) JObject (Prim.Array c) 117 | 118 | foreign import _lookup 119 | :: forall f 120 | . Fn4 121 | (forall a. f a) 122 | (forall a. a -> f a) 123 | String 124 | JObject 125 | (f JSON) 126 | 127 | foreign import _index 128 | :: forall f 129 | . Fn4 130 | (forall a. f a) 131 | (forall a. a -> f a) 132 | Int 133 | JArray 134 | (f JSON) 135 | 136 | foreign import length :: JArray -> Int 137 | 138 | foreign import _append 139 | :: Fn2 140 | JArray 141 | JArray 142 | JArray 143 | 144 | foreign import isNull :: JSON -> Boolean 145 | -------------------------------------------------------------------------------- /src/JSON/Object.js: -------------------------------------------------------------------------------- 1 | export const empty = {}; 2 | -------------------------------------------------------------------------------- /src/JSON/Object.purs: -------------------------------------------------------------------------------- 1 | module JSON.Object 2 | ( fromEntries 3 | , fromFoldable 4 | , fromFoldableWithIndex 5 | , empty 6 | , singleton 7 | , insert 8 | , delete 9 | , entries 10 | , keys 11 | , values 12 | , lookup 13 | , toUnfoldable 14 | , module Exports 15 | ) where 16 | 17 | import Data.Array as Array 18 | import Data.Foldable (class Foldable) 19 | import Data.FoldableWithIndex (class FoldableWithIndex, foldrWithIndex) 20 | import Data.Function.Uncurried (runFn2, runFn3, runFn4) 21 | import Data.Maybe (Maybe(..)) 22 | import Data.Tuple (Tuple(..), fst, snd) 23 | import Data.Unfoldable (class Unfoldable) 24 | import JSON.Internal (JObject) as Exports 25 | import JSON.Internal (JObject, JSON, _delete, _entries, _fromEntries, _insert, _lookup) 26 | 27 | -- | Creates an `JObject` from an array of key/value pairs. 28 | fromEntries :: Array (Tuple String JSON) -> JObject 29 | fromEntries kvs = runFn3 _fromEntries fst snd kvs 30 | 31 | -- | Creates an `JObject` from a foldable source of key/value pairs. 32 | fromFoldable :: forall f. Foldable f => f (Tuple String JSON) -> JObject 33 | fromFoldable kvs = fromEntries (Array.fromFoldable kvs) 34 | 35 | -- | Creates an `JObject` from an indexed foldable source. 36 | fromFoldableWithIndex :: forall f. FoldableWithIndex String f => f JSON -> JObject 37 | fromFoldableWithIndex kvs = fromEntries (foldrWithIndex (\k v -> Array.cons (Tuple k v)) [] kvs) 38 | 39 | -- | An empty `JObject`. 40 | foreign import empty :: JObject 41 | 42 | -- | Creates an `JObject` with a single entry. 43 | singleton :: String -> JSON -> JObject 44 | singleton k v = runFn3 _insert k v empty 45 | 46 | -- | Inserts an entry into an `JObject`. If the key already exists the value will be overwritten. 47 | insert :: String -> JSON -> JObject -> JObject 48 | insert k v obj = runFn3 _insert k v obj 49 | 50 | -- | Deletes an entry from an `JObject`. This will have no effect if the key does not exist in the 51 | -- | object. 52 | delete :: String -> JObject -> JObject 53 | delete k obj = runFn2 _delete k obj 54 | 55 | -- | Extracts the key/value pairs of an `JObject`. 56 | entries :: JObject -> Array (Tuple String JSON) 57 | entries obj = runFn2 _entries Tuple obj 58 | 59 | -- | Extracts the keys of an `JObject`. 60 | keys :: JObject -> Array String 61 | keys obj = runFn2 _entries (\k _ -> k) obj 62 | 63 | -- | Extracts the values of an `JObject`. 64 | values :: JObject -> Array JSON 65 | values obj = runFn2 _entries (\_ v -> v) obj 66 | 67 | -- | Attempts to fetch the value for a key from an `JObject`. If the key is not present `Nothing` is 68 | -- | returned. 69 | lookup :: String -> JObject -> Maybe JSON 70 | lookup k obj = runFn4 _lookup Nothing Just k obj 71 | 72 | -- | Unfolds an object into key/value pairs. 73 | toUnfoldable :: forall f. Unfoldable f => JObject -> f (Tuple String JSON) 74 | toUnfoldable obj = Array.toUnfoldable (entries obj) 75 | -------------------------------------------------------------------------------- /src/JSON/Path.purs: -------------------------------------------------------------------------------- 1 | module JSON.Path where 2 | 3 | import Prelude 4 | 5 | import Data.Generic.Rep (class Generic) 6 | import Data.Maybe (Maybe(..)) 7 | import JSON (JSON) 8 | import JSON as JSON 9 | import JSON.Array as JArray 10 | import JSON.Object as JObject 11 | 12 | -- | A path to a location in a JSON document. 13 | data Path 14 | = Tip 15 | | AtKey String Path 16 | | AtIndex Int Path 17 | 18 | derive instance Eq Path 19 | derive instance Ord Path 20 | derive instance Generic Path _ 21 | 22 | instance Show Path where 23 | show = case _ of 24 | Tip -> "Tip" 25 | AtKey key rest -> "(AtKey " <> show key <> " " <> show rest <> ")" 26 | AtIndex ix rest -> "(AtIndex " <> show ix <> " " <> show rest <> ")" 27 | 28 | -- | Attempts to get the value at the path in a JSON document. 29 | get :: Path -> JSON -> Maybe JSON 30 | get path json = 31 | case path of 32 | Tip -> Just json 33 | AtKey key rest -> get rest =<< JObject.lookup key =<< JSON.toJObject json 34 | AtIndex ix rest -> get rest =<< JArray.index ix =<< JSON.toJArray json 35 | 36 | -- | Prints the path as a basic JSONPath expression. 37 | print :: Path -> String 38 | print path = "$" <> go path 39 | where 40 | go :: Path -> String 41 | go p = case p of 42 | Tip -> "" 43 | AtKey k rest -> "." <> k <> go rest -- TODO: ["quoted"] paths also 44 | AtIndex ix rest -> "[" <> show ix <> "]" <> go rest 45 | 46 | -- | Extends the tip of the first path with the second path. 47 | -- | 48 | -- | For example, `$.data[0]` extended with `$.info.title` would result in `$.data[0].info.title`. 49 | extend :: Path -> Path -> Path 50 | extend p1 p2 = case p1 of 51 | Tip -> p2 52 | AtKey key rest -> AtKey key (extend rest p2) 53 | AtIndex ix rest -> AtIndex ix (extend rest p2) 54 | 55 | -- | Finds the common prefix of two paths. If they have nothing in common the result will be the 56 | -- | root. 57 | findCommonPrefix :: Path -> Path -> Path 58 | findCommonPrefix = case _, _ of 59 | AtKey k1 rest1, AtKey k2 rest2 | k1 == k2 -> AtKey k1 (findCommonPrefix rest1 rest2) 60 | AtIndex i1 rest1, AtIndex i2 rest2 | i1 == i2 -> AtIndex i1 (findCommonPrefix rest1 rest2) 61 | _, _ -> Tip 62 | 63 | -- | Attempts to strip the first path from the start of the second path. `Nothing` is returned if 64 | -- | the second path does not start with the prefix. 65 | -- | 66 | -- | For example, stripping a prefix of `$.data[0]` from `$.data[0].info.title` would result in 67 | -- | `$.info.title`. 68 | stripPrefix :: Path -> Path -> Maybe Path 69 | stripPrefix = case _, _ of 70 | AtKey k1 rest1, AtKey k2 rest2 | k1 == k2 -> stripPrefix rest1 rest2 71 | AtIndex i1 rest1, AtIndex i2 rest2 | i1 == i2 -> stripPrefix rest1 rest2 72 | Tip, tail -> Just tail 73 | _, _ -> Nothing 74 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.Tuple (Tuple(..)) 7 | import Effect (Effect) 8 | import Effect.Console (log) 9 | import JSON as J 10 | import JSON.Array as JA 11 | import JSON.Object as JO 12 | import JSON.Path as Path 13 | import Test.Assert (assertTrue) 14 | 15 | main :: Effect Unit 16 | main = do 17 | 18 | log "Check numeric comparisons" 19 | assertTrue $ J.fromInt 1 == J.fromInt 1 20 | assertTrue $ J.fromInt 1 < J.fromInt 2 21 | assertTrue $ J.fromInt 42 > J.fromInt 0 22 | 23 | log "Check string comparisons" 24 | assertTrue $ J.fromString "json" == J.fromString "json" 25 | assertTrue $ J.fromString "a" < J.fromString "b" 26 | assertTrue $ J.fromString "q" > J.fromString "p" 27 | 28 | log "Check array comparisons" 29 | assertTrue $ J.fromJArray (JA.fromArray []) == J.fromJArray (JA.fromArray []) 30 | assertTrue $ J.fromJArray (JA.fromArray [ J.fromInt 1 ]) == J.fromJArray (JA.fromArray [ J.fromInt 1 ]) 31 | assertTrue $ J.fromJArray (JA.fromArray [ J.fromInt 1 ]) < J.fromJArray (JA.fromArray [ J.fromInt 2 ]) 32 | 33 | log "Check object comparisons" 34 | assertTrue $ JO.empty == JO.empty 35 | assertTrue $ J.fromJObject (JO.fromEntries [ Tuple "a" (J.fromInt 1) ]) == J.fromJObject (JO.fromEntries [ Tuple "a" (J.fromInt 1) ]) 36 | assertTrue $ J.fromJObject (JO.fromEntries [ Tuple "a" (J.fromInt 1) ]) < J.fromJObject (JO.fromEntries [ Tuple "a" (J.fromInt 2) ]) 37 | 38 | log "Check isNull" 39 | assertTrue $ J.isNull J.null 40 | assertTrue $ not $ J.isNull (J.fromInt 1) 41 | 42 | log "Check array index" 43 | assertTrue $ JA.index (-1) (JA.fromArray (J.fromInt <$> [ 0, 2, 4 ])) == Nothing 44 | assertTrue $ JA.index 0 (JA.fromArray (J.fromInt <$> [ 0, 2, 4 ])) == Just (J.fromInt 0) 45 | assertTrue $ JA.index 1 (JA.fromArray (J.fromInt <$> [ 0, 2, 4 ])) == Just (J.fromInt 2) 46 | assertTrue $ JA.index 2 (JA.fromArray (J.fromInt <$> [ 0, 2, 4 ])) == Just (J.fromInt 4) 47 | assertTrue $ JA.index 3 (JA.fromArray (J.fromInt <$> [ 0, 2, 4 ])) == Nothing 48 | 49 | log "Check array concat" 50 | assertTrue $ JA.fromArray (J.fromInt <$> [ 1, 2 ]) <> JA.fromArray (J.fromInt <$> [ 2, 3 ]) == JA.fromArray (J.fromInt <$> [ 1, 2, 2, 3 ]) 51 | 52 | log "Check path printing" 53 | assertTrue $ Path.print (Path.AtKey "data" (Path.AtIndex 0 (Path.AtKey "field" Path.Tip))) == "$.data[0].field" 54 | 55 | log "Check path get" 56 | assertTrue $ Path.get Path.Tip (J.fromString "hello") == Just (J.fromString "hello") 57 | assertTrue $ Path.get Path.Tip (J.fromJArray (JA.fromArray [ J.fromInt 42 ])) == Just (J.fromJArray (JA.fromArray [ J.fromInt 42 ])) 58 | assertTrue $ Path.get (Path.AtIndex 0 Path.Tip) (J.fromJArray (JA.fromArray [ J.fromInt 42, J.fromString "X", J.fromBoolean true ])) == Just (J.fromInt 42) 59 | assertTrue $ Path.get (Path.AtIndex 1 Path.Tip) (J.fromJArray (JA.fromArray [ J.fromInt 42, J.fromString "X", J.fromBoolean true ])) == Just (J.fromString "X") 60 | assertTrue $ Path.get (Path.AtIndex 5 Path.Tip) (J.fromJArray (JA.fromArray [ J.fromInt 42, J.fromString "X", J.fromBoolean true ])) == Nothing 61 | assertTrue $ Path.get (Path.AtKey "a" Path.Tip) (J.fromJObject (JO.fromEntries [ Tuple "a" (J.fromInt 1), Tuple "x" (J.fromBoolean false) ])) == Just (J.fromInt 1) 62 | assertTrue $ Path.get (Path.AtKey "x" Path.Tip) (J.fromJObject (JO.fromEntries [ Tuple "a" (J.fromInt 1), Tuple "x" (J.fromBoolean false) ])) == Just (J.fromBoolean false) 63 | assertTrue $ Path.get (Path.AtKey "z" Path.Tip) (J.fromJObject (JO.fromEntries [ Tuple "a" (J.fromInt 1), Tuple "x" (J.fromBoolean false) ])) == Nothing 64 | assertTrue $ Path.get (Path.AtIndex 1 (Path.AtKey "x" Path.Tip)) (J.fromJArray (JA.fromArray [ J.fromString "skip", (J.fromJObject (JO.fromEntries [ Tuple "a" (J.fromInt 1), Tuple "x" (J.fromBoolean false) ])) ])) == Just (J.fromBoolean false) 65 | 66 | log "Check path extend" 67 | assertTrue do 68 | let p1 = Path.AtKey "data" $ Path.AtIndex 0 $ Path.Tip 69 | let p2 = Path.AtKey "info" $ Path.AtKey "title" $ Path.Tip 70 | let expected = Path.AtKey "data" $ Path.AtIndex 0 $ Path.AtKey "info" $ Path.AtKey "title" $ Path.Tip 71 | Path.extend p1 p2 == expected 72 | 73 | log "Check path findCommonPrefix" 74 | assertTrue do 75 | let p1 = Path.AtKey "y" $ Path.AtKey "x" $ Path.AtIndex 1 $ Path.Tip 76 | let p2 = Path.AtKey "y" $ Path.AtKey "x" $ Path.AtIndex 0 $ Path.Tip 77 | let expected = Path.AtKey "y" $ Path.AtKey "x" $ Path.Tip 78 | Path.findCommonPrefix p1 p2 == expected 79 | assertTrue do 80 | let p1 = Path.AtKey "other" $ Path.Tip 81 | let p2 = Path.AtKey "y" $ Path.AtKey "x" $ Path.AtIndex 0 $ Path.Tip 82 | let expected = Path.Tip 83 | Path.findCommonPrefix p1 p2 == expected 84 | 85 | log "Check path stripPrefix" 86 | assertTrue do 87 | let p1 = Path.AtKey "y" Path.Tip 88 | let p2 = Path.AtKey "y" $ Path.AtKey "x" $ Path.AtIndex 0 $ Path.Tip 89 | let expected = Path.AtKey "x" $ Path.AtIndex 0 Path.Tip 90 | Path.stripPrefix p1 p2 == Just expected 91 | assertTrue do 92 | let p1 = Path.AtKey "y" $ Path.AtKey "x" $ Path.Tip 93 | let p2 = Path.AtKey "y" $ Path.AtKey "x" $ Path.AtIndex 0 Path.Tip 94 | let expected = Path.AtIndex 0 Path.Tip 95 | Path.stripPrefix p1 p2 == Just expected 96 | assertTrue do 97 | let p1 = Path.AtKey "other" Path.Tip 98 | let p2 = Path.AtKey "y" $ Path.AtKey "x" $ Path.AtIndex 0 Path.Tip 99 | Path.stripPrefix p1 p2 == Nothing 100 | --------------------------------------------------------------------------------