diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..870e5de --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.* +!/.gitignore +!/.travis.yml +bower_components/ +node_modules/ +output/ +dist/ +npm-debug.log \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0d10c95 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: node_js +node_js: + - 0.10 +env: + - TAG=v0.7.0 +install: + - wget -O $HOME/purescript.tar.gz https://github.com/purescript/purescript/releases/download/$TAG/linux64.tar.gz + - sudo tar zxvf $HOME/purescript.tar.gz -C /usr/local/bin purescript/psc{,i,-docs,-bundle} --strip-components=1 + - sudo chmod a+x /usr/local/bin/psc{,i,-docs,-bundle} + - npm install bower gulp -g + - npm install && bower install +script: + - gulp test \ No newline at end of file diff --git a/README.md b/README.md index 9f37eb5..cdd0583 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ +[![Build Status](https://travis-ci.org/purescript-contrib/purescript-argonaut-codecs.svg?branch=master)](https://travis-ci.org/purescript-contrib/purescript-argonaut-codecs) + # purescript-argonaut-codecs + +`EncodeJson` and `DecodeJson` classes and instances for __purescript-argonaut__. Also useful combinators for encoding ando decoding stuff. + +## Installation + +```shell +bower install purescript-argonaut-codecs +``` + +## Documentaion + +- [Data.Argonaut.Encode](docs/Data/Argonaut/Encode.md) +- [Data.Argonaut.Decode](docs/Data/Argonaut/Decode.md) +- [Data.Argonaut.Combinators](docs/Data/Argonaut/Combinators.md) diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..0b26df8 --- /dev/null +++ b/bower.json @@ -0,0 +1,38 @@ +{ + "name": "purescript-argonaut-codecs", + "homepage": "https://github.com/purescript-contrib/purescript-argonaut-codecs", + "authors": [ + "Maxim Zimaliev ", + "Hardy Jones <>", + "John A. De Goes " + ], + "description": "Codecs for purescript argonaut", + "keywords": [ + "purescript", + "json", + "argonaut", + "encode", + "decode", + "codec", + "combinators" + ], + "license": "MIT", + "dependencies": { + "purescript-argonaut-core": "~0.1.0", + "purescript-maybe": "~0.3.2", + "purescript-either": "~0.2.0", + "purescript-arrays": "~0.4.0", + "purescript-strings": "~0.5.3", + "purescript-lists": "~0.7.0", + "purescript-maps": "~0.4.0", + "purescript-foldable-traversable": "~0.4.0", + "purescript-unfoldable": "~0.4.0", + "purescript-tuples": "~0.4.0", + "purescript-control": "~0.3.0" + }, + "devDependencies": { + "purescript-eff": "~0.1.0", + "purescript-console": "~0.1.0", + "purescript-strongcheck": "~0.10.0" + } +} diff --git a/docs/Data/Argonaut/Combinators.md b/docs/Data/Argonaut/Combinators.md new file mode 100644 index 0000000..f6c07b3 --- /dev/null +++ b/docs/Data/Argonaut/Combinators.md @@ -0,0 +1,35 @@ +## Module Data.Argonaut.Combinators + +#### `(:=)` + +``` purescript +(:=) :: forall a. (EncodeJson a) => String -> a -> JAssoc +``` + +_non-associative / precedence 7_ + +#### `(~>)` + +``` purescript +(~>) :: forall a. (EncodeJson a) => JAssoc -> a -> Json +``` + +_right-associative / precedence 6_ + +#### `(?>>=)` + +``` purescript +(?>>=) :: forall a b. Maybe a -> String -> Either String a +``` + +_left-associative / precedence 1_ + +#### `(.?)` + +``` purescript +(.?) :: forall a. (DecodeJson a) => JObject -> String -> Either String a +``` + +_non-associative / precedence 7_ + + diff --git a/docs/Data/Argonaut/Decode.md b/docs/Data/Argonaut/Decode.md new file mode 100644 index 0000000..de47fd8 --- /dev/null +++ b/docs/Data/Argonaut/Decode.md @@ -0,0 +1,34 @@ +## Module Data.Argonaut.Decode + +#### `DecodeJson` + +``` purescript +class DecodeJson a where + decodeJson :: Json -> Either String a +``` + +##### Instances +``` purescript +instance decodeJsonMaybe :: (DecodeJson a) => DecodeJson (Maybe a) +instance decodeJsonTuple :: (DecodeJson a, DecodeJson b) => DecodeJson (Tuple a b) +instance decodeJsonEither :: (DecodeJson a, DecodeJson b) => DecodeJson (Either a b) +instance decodeJsonNull :: DecodeJson Unit +instance decodeJsonBoolean :: DecodeJson Boolean +instance decodeJsonNumber :: DecodeJson Number +instance decodeJsonInt :: DecodeJson Int +instance decodeJsonString :: DecodeJson String +instance decodeJsonJson :: DecodeJson Json +instance decodeJsonChar :: DecodeJson Char +instance decodeStrMap :: (DecodeJson a) => DecodeJson (StrMap a) +instance decodeArray :: (DecodeJson a) => DecodeJson (Array a) +instance decodeList :: (DecodeJson a) => DecodeJson (List a) +instance decodeMap :: (Ord a, DecodeJson a, DecodeJson b) => DecodeJson (Map a b) +``` + +#### `decodeMaybe` + +``` purescript +decodeMaybe :: forall a. (DecodeJson a) => Json -> Maybe a +``` + + diff --git a/docs/Data/Argonaut/Encode.md b/docs/Data/Argonaut/Encode.md new file mode 100644 index 0000000..21a6ff2 --- /dev/null +++ b/docs/Data/Argonaut/Encode.md @@ -0,0 +1,28 @@ +## Module Data.Argonaut.Encode + +#### `EncodeJson` + +``` purescript +class EncodeJson a where + encodeJson :: a -> Json +``` + +##### Instances +``` purescript +instance encodeJsonMaybe :: (EncodeJson a) => EncodeJson (Maybe a) +instance encodeJsonTuple :: (EncodeJson a, EncodeJson b) => EncodeJson (Tuple a b) +instance encodeJsonEither :: (EncodeJson a, EncodeJson b) => EncodeJson (Either a b) +instance encodeJsonUnit :: EncodeJson Unit +instance encodeJsonJBoolean :: EncodeJson Boolean +instance encodeJsonJNumber :: EncodeJson Number +instance encodeJsonInt :: EncodeJson Int +instance encodeJsonJString :: EncodeJson String +instance encodeJsonJson :: EncodeJson Json +instance encodeJsonChar :: EncodeJson Char +instance encodeJsonArray :: (EncodeJson a) => EncodeJson (Array a) +instance encodeJsonList :: (EncodeJson a) => EncodeJson (List a) +instance encodeStrMap :: (EncodeJson a) => EncodeJson (StrMap a) +instance encodeMap :: (Ord a, EncodeJson a, EncodeJson b) => EncodeJson (Map a b) +``` + + diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..78ac15d --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,70 @@ +'use strict' + + +var gulp = require('gulp') + , purescript = require('gulp-purescript') + , run = require('gulp-run') + , runSequence = require('run-sequence') + ; + +function sequence() { + var args = [].slice.apply(arguments); + return function() { + runSequence.apply(null, args); + }; +} + +var sources = [ + 'src/**/*.purs', + 'bower_components/purescript-*/src/**/*.purs' +]; + +var foreigns = [ + 'src/**/*.js', + 'bower_components/purescript-*/src/**/*.js' +]; + +var testSources = [ + 'test/**/*.purs' +]; + +var testForeigns = [ + 'test/**/*.js' +]; + +gulp.task('docs', function() { + return purescript.pscDocs({ + src: sources, + docgen: { + "Data.Argonaut.Encode": "docs/Data/Argonaut/Encode.md", + "Data.Argonaut.Decode": "docs/Data/Argonaut/Decode.md", + "Data.Argonaut.Combinators": "docs/Data/Argonaut/Combinators.md" + } + }); +}); + + +gulp.task('make', function() { + return purescript.psc({ + src: sources, + ffi: foreigns + }); +}); + +gulp.task('test-make', function() { + return purescript.psc({ + src: sources.concat(testSources), + ffi: foreigns.concat(testForeigns) + }); +}); + +gulp.task('test', ['test-make'], function() { + return purescript.pscBundle({ + src: "output/**/*.js", + main: "Test.Main", + output: "dist/test.js" + }).pipe(run('node dist/test.js')); +}); + + +gulp.task("default", sequence("make", "docs")); diff --git a/package.json b/package.json new file mode 100644 index 0000000..7498c5b --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "purescript-argonaut-codecs", + "description": "Codecs for purescript-argonaut library", + "license": "MIT", + "dependencies": { + "gulp": "^3.9.0", + "gulp-purescript": "^0.5.0", + "gulp-run": "^1.6.8", + "run-sequence": "^1.1.1" + } +} diff --git a/src/Data/Argonaut/Combinators.purs b/src/Data/Argonaut/Combinators.purs new file mode 100644 index 0000000..2a0ba3d --- /dev/null +++ b/src/Data/Argonaut/Combinators.purs @@ -0,0 +1,43 @@ +module Data.Argonaut.Combinators + ( (:=) + , (~>) + , (?>>=) + , (.?) + ) where + +import Prelude + +import Data.Argonaut.Core + ( foldJsonObject + , fromObject + , jsonSingletonObject + , Json() + , JAssoc() + , JObject() + ) +import Data.Argonaut.Encode (encodeJson, EncodeJson) +import Data.Argonaut.Decode (DecodeJson, decodeJson) +import Data.Either (Either(..)) +import Data.Maybe (Maybe(..), maybe) +import Data.Tuple (Tuple(..)) + +import qualified Data.StrMap as M + +infix 7 := +infix 7 .? +infixr 6 ~> +infixl 1 ?>>= + +(:=) :: forall a. (EncodeJson a) => String -> a -> JAssoc +(:=) k v = Tuple k $ encodeJson v + +(~>) :: forall a. (EncodeJson a) => JAssoc -> a -> Json +(~>) (Tuple k v) a = foldJsonObject (jsonSingletonObject k v) (M.insert k v >>> fromObject) (encodeJson a) + +(?>>=) :: forall a b. Maybe a -> String -> Either String a +(?>>=) (Just x) _ = Right x +(?>>=) _ str = Left $ "Couldn't decode " ++ str + +-- obj .? "foo" +(.?) :: forall a. (DecodeJson a) => JObject -> String -> Either String a +(.?) o s = maybe (Left $ "Expected field " ++ show s) decodeJson (M.lookup s o) diff --git a/src/Data/Argonaut/Decode.purs b/src/Data/Argonaut/Decode.purs new file mode 100644 index 0000000..62e0de4 --- /dev/null +++ b/src/Data/Argonaut/Decode.purs @@ -0,0 +1,93 @@ +module Data.Argonaut.Decode + ( DecodeJson + , decodeJson + , decodeMaybe + ) where + +import Prelude + +import Data.Argonaut.Core + ( Json() + , JNumber() + , JString() + , foldJsonNull + , foldJsonBoolean + , foldJsonNumber + , foldJsonString + , foldJsonArray + , foldJsonObject + , toArray + , toNumber + , toObject + , toString + ) +import Data.Either (either, Either(..)) +import Data.Int (fromNumber) +import Data.Maybe (maybe, Maybe(..)) +import Data.Traversable (traverse) +import Data.Tuple (Tuple(..)) +import Data.String +import Data.List (List(..), toList) +import Control.Alt +import Data.Traversable (traverse) + +import qualified Data.StrMap as M +import qualified Data.Map as Map + +class DecodeJson a where + decodeJson :: Json -> Either String a + +instance decodeJsonMaybe :: (DecodeJson a) => DecodeJson (Maybe a) where + decodeJson j = (Just <$> decodeJson j) <|> pure Nothing + +instance decodeJsonTuple :: (DecodeJson a, DecodeJson b) => DecodeJson (Tuple a b) where + decodeJson j = decodeJson j >>= f where + f (Cons a (Cons b Nil)) = Tuple <$> decodeJson a <*> decodeJson b + +instance decodeJsonEither :: (DecodeJson a, DecodeJson b) => DecodeJson (Either a b) where + decodeJson j = (Left <$> decodeJson j) <|> (Right <$> decodeJson j) + +instance decodeJsonNull :: DecodeJson Unit where + decodeJson = foldJsonNull (Left "Not null.") (const $ Right unit) + +instance decodeJsonBoolean :: DecodeJson Boolean where + decodeJson = foldJsonBoolean (Left "Not a Boolean.") Right + +instance decodeJsonNumber :: DecodeJson Number where + decodeJson = foldJsonNumber (Left "Not a Number.") Right + +instance decodeJsonInt :: DecodeJson Int where + decodeJson num = foldJsonNumber (Left "Not a Number.") go num + where go num = maybe (Left "Not an Int") Right $ fromNumber num + +instance decodeJsonString :: DecodeJson String where + decodeJson = foldJsonString (Left "Not a String.") Right + +instance decodeJsonJson :: DecodeJson Json where + decodeJson = Right + +instance decodeJsonChar :: DecodeJson Char where + decodeJson j = (charAt 0 <$> decodeJson j) >>= go where + go Nothing = Left $ "Expected character but found: " ++ show j + go (Just c) = Right c + +instance decodeStrMap :: (DecodeJson a) => DecodeJson (M.StrMap a) where + decodeJson json = maybe (Left "Couldn't decode.") Right $ do + obj <- toObject json + traverse decodeMaybe obj + +instance decodeArray :: (DecodeJson a) => DecodeJson (Array a) where + decodeJson json = maybe (Left "Couldn't decode.") Right $ do + obj <- toArray json + traverse decodeMaybe obj + +instance decodeList :: (DecodeJson a) => DecodeJson (List a) where + decodeJson json = maybe (Left "Couldn't decode.") Right $ do + lst <- toList <$> toArray json + traverse decodeMaybe lst + +instance decodeMap :: (Ord a, DecodeJson a, DecodeJson b) => DecodeJson (Map.Map a b) where + decodeJson j = Map.fromList <$> decodeJson j + +decodeMaybe :: forall a. (DecodeJson a) => Json -> Maybe a +decodeMaybe json = either (const Nothing) pure $ decodeJson json diff --git a/src/Data/Argonaut/Encode.purs b/src/Data/Argonaut/Encode.purs new file mode 100644 index 0000000..f11366f --- /dev/null +++ b/src/Data/Argonaut/Encode.purs @@ -0,0 +1,83 @@ +module Data.Argonaut.Encode + ( EncodeJson + , encodeJson + ) where + +import Prelude + +import Data.Argonaut.Core + ( Json(..) + , foldJsonObject + , jsonNull + , fromNull + , fromBoolean + , fromNumber + , fromString + , fromArray + , fromObject + , jsonEmptyArray + , jsonEmptyObject + , jsonSingletonObject + ) +import Data.String (fromChar) +import Data.Maybe +import Data.Either +import Data.List (List(..), fromList) +import Data.Int (toNumber) +import Data.Unfoldable () +import Data.Foldable (foldr) +import Data.Tuple (Tuple(..)) + +import qualified Data.StrMap as SM +import qualified Data.Map as M + +class EncodeJson a where + encodeJson :: a -> Json + +instance encodeJsonMaybe :: (EncodeJson a) => EncodeJson (Maybe a) where + encodeJson Nothing = jsonNull + encodeJson (Just a) = encodeJson a + +instance encodeJsonTuple :: (EncodeJson a, EncodeJson b) => EncodeJson (Tuple a b) where + encodeJson (Tuple a b) = encodeJson [encodeJson a, encodeJson b] + +instance encodeJsonEither :: (EncodeJson a, EncodeJson b) => EncodeJson (Either a b) where + encodeJson (Left a) = encodeJson a + encodeJson (Right b) = encodeJson b + +instance encodeJsonUnit :: EncodeJson Unit where + encodeJson = const jsonNull + +instance encodeJsonJBoolean :: EncodeJson Boolean where + encodeJson = fromBoolean + +instance encodeJsonJNumber :: EncodeJson Number where + encodeJson = fromNumber + +instance encodeJsonInt :: EncodeJson Int where + encodeJson = fromNumber <<< toNumber + +instance encodeJsonJString :: EncodeJson String where + encodeJson = fromString + +instance encodeJsonJson :: EncodeJson Json where + encodeJson = id + +instance encodeJsonChar :: EncodeJson Char where + encodeJson = encodeJson <<< fromChar + +instance encodeJsonArray :: (EncodeJson a) => EncodeJson (Array a) where + encodeJson json = fromArray (encodeJson <$> json) + +instance encodeJsonList :: (EncodeJson a) => EncodeJson (List a) where + encodeJson json = + let arr :: Array a + arr = fromList json + in fromArray (encodeJson <$> arr) + +instance encodeStrMap :: (EncodeJson a) => EncodeJson (SM.StrMap a) where + encodeJson m = fromObject (encodeJson <$> m) + +instance encodeMap :: (Ord a, EncodeJson a, EncodeJson b) => EncodeJson (M.Map a b) where + encodeJson = encodeJson <<< M.toList + diff --git a/test/Test/Main.purs b/test/Test/Main.purs new file mode 100644 index 0000000..d2b4139 --- /dev/null +++ b/test/Test/Main.purs @@ -0,0 +1,133 @@ +module Test.Main where + +import Prelude + +import Data.Argonaut.Core +import Data.Argonaut.Decode (decodeJson, DecodeJson) +import Data.Argonaut.Encode (encodeJson, EncodeJson) +import Data.Argonaut.Combinators ((:=), (~>), (?>>=), (.?)) +import Data.Either +import Data.Tuple +import Data.Maybe +import Data.Array +import Data.Foldable (foldl) +import Data.List (toList, List(..)) +import Control.Monad.Eff.Console +import qualified Data.StrMap as M + +import Test.StrongCheck +import Test.StrongCheck.Gen + + +genJNull :: Gen Json +genJNull = pure jsonNull + +genJBool :: Gen Json +genJBool = fromBoolean <$> arbitrary + +genJNumber :: Gen Json +genJNumber = fromNumber <$> arbitrary + +genJString :: Gen Json +genJString = fromString <$> arbitrary + +genJArray :: Size -> Gen Json +genJArray sz = fromArray <$> vectorOf sz (genJson $ sz - 1) + +genJObject :: Size -> Gen Json +genJObject sz = do + v <- vectorOf sz (genJson $ sz - 1) + k <- vectorOf (length v) (arbitrary :: Gen AlphaNumString) + return $ let f (AlphaNumString s) = s ++ "x" + k' = f <$> k + in fromObject <<< M.fromList <<< toList <<< nubBy (\a b -> (fst a) == (fst b)) $ zipWith Tuple k' v + +genJson :: Size -> Gen Json +genJson 0 = oneOf genJNull [genJBool, genJNumber, genJString] +genJson n = frequency (Tuple 1.0 genJNull) rest where + rest = toList [Tuple 2.0 genJBool, + Tuple 2.0 genJNumber, + Tuple 3.0 genJString, + Tuple 1.0 (genJArray n), + Tuple 1.0 (genJObject n)] + +-- orphan, but it's just for tests +instance arbitraryJson :: Arbitrary Json where + arbitrary = sized genJson + + +prop_encode_then_decode :: Json -> Boolean +prop_encode_then_decode json = + Right json == (decodeJson $ encodeJson $ json) + +prop_decode_then_encode :: Json -> Boolean +prop_decode_then_encode json = + let decoded = (decodeJson json) :: Either String Json in + Right json == (decoded >>= (encodeJson >>> pure)) + + +encodeDecodeCheck = do + log "Showing small sample of JSON" + showSample (genJson 10) + + log "Testing that any JSON can be encoded and then decoded" + quickCheck' 20 prop_encode_then_decode + + log "Testing that any JSON can be decoded and then encoded" + quickCheck' 20 prop_decode_then_encode + +prop_assoc_builder_str :: Tuple String String -> Boolean +prop_assoc_builder_str (Tuple key str) = + case (key := str) of + Tuple k json -> + (key == k) && (decodeJson json == Right str) + +newtype Obj = Obj Json +unObj :: Obj -> Json +unObj (Obj j) = j + +instance arbitraryObj :: Arbitrary Obj where + arbitrary = Obj <$> (genJObject 5) + + +prop_assoc_append :: (Tuple JAssoc Obj) -> Boolean +prop_assoc_append (Tuple assoc@(Tuple key val) (Obj obj)) = + let appended = assoc ~> obj + in case toObject appended >>= M.lookup key of + Just val -> true + _ -> false + + +prop_get_jobject_field :: Obj -> Boolean +prop_get_jobject_field (Obj obj) = + maybe false go $ toObject obj + where + go :: JObject -> Boolean + go obj = + let keys = M.keys obj + in foldl (\ok key -> ok && (isJust $ M.lookup key obj)) true keys + +assert_maybe_msg :: Boolean +assert_maybe_msg = + (isLeft (Nothing ?>>= "Nothing is Left")) + && + ((Just 2 ?>>= "Nothing is left") == Right 2) + + + + +combinatorsCheck = do + log "Check assoc builder `:=`" + quickCheck' 20 prop_assoc_builder_str + log "Check JAssoc append `~>`" + quickCheck' 20 prop_assoc_append + log "Check get field `obj .? 'foo'`" + quickCheck' 20 prop_get_jobject_field + log "Assert maybe to either convertion" + assert assert_maybe_msg + + + +main = do + encodeDecodeCheck + combinatorsCheck