Skip to content

Commit d3066c2

Browse files
authored
Merge pull request #2 from rightfold/master
Add generics-rep encoding and decoding
2 parents 93bb2c9 + 49ce140 commit d3066c2

File tree

6 files changed

+209
-4
lines changed

6 files changed

+209
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![Build status](https://travis-ci.org/purescript-contrib/purescript-argonaut-generic.svg?branch=master)](https://travis-ci.org/purescript-contrib/purescript-argonaut-generic)
55
[![Maintainer: slamdata](https://img.shields.io/badge/maintainer-slamdata-lightgrey.svg)](http://github.com/slamdata)
66

7-
This package provides `gEncodeJson` and `gDecodeJson` functions for any data types that have a `Generic` instance. Currently only [`purescript-generics`](https://github.com/purescript/purescript-generics) is supported (not [`purescript-generics-rep`](https://github.com/purescript/purescript-generics-rep)).
7+
This package provides `gEncodeJson` and `gDecodeJson` functions for any data types that have a `Generic` instance, and `genericEncodeJson` and `genericDecodeJson` functions for any data types that have a `Rep.Generic` instance.
88

99
## Installation
1010

bower.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
"dependencies": {
1919
"purescript-generics": "^4.0.0",
2020
"purescript-argonaut-core": "^3.1.0",
21-
"purescript-argonaut-codecs": "^3.0.0"
21+
"purescript-argonaut-codecs": "^3.0.0",
22+
"purescript-generics-rep": "^5.1.0"
2223
},
23-
"devDependencies": {}
24+
"devDependencies": {
25+
"purescript-assert": "^3.0.0",
26+
"purescript-console": "^3.0.0"
27+
}
2428
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
module Data.Argonaut.Decode.Generic.Rep (
2+
class DecodeRep,
3+
class DecodeRepArgs,
4+
class DecodeRepFields,
5+
decodeRep,
6+
decodeRepArgs,
7+
decodeRepFields,
8+
genericDecodeJson
9+
) where
10+
11+
import Prelude
12+
13+
import Control.Alt ((<|>))
14+
import Data.Argonaut.Core (Json, toArray, toObject, toString)
15+
import Data.Argonaut.Decode.Class (class DecodeJson, decodeJson)
16+
import Data.Array (uncons)
17+
import Data.Bifunctor (lmap)
18+
import Data.Either (Either(..))
19+
import Data.Generic.Rep as Rep
20+
import Data.Maybe (Maybe, maybe)
21+
import Data.StrMap as SM
22+
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
23+
24+
class DecodeRep r where
25+
decodeRep :: Json -> Either String r
26+
27+
instance decodeRepNoConstructors :: DecodeRep Rep.NoConstructors where
28+
decodeRep _ = Left "Cannot decode empty data type"
29+
30+
instance decodeRepSum :: (DecodeRep a, DecodeRep b) => DecodeRep (Rep.Sum a b) where
31+
decodeRep j = Rep.Inl <$> decodeRep j <|> Rep.Inr <$> decodeRep j
32+
33+
instance decodeRepConstructor :: (IsSymbol name, DecodeRepArgs a) => DecodeRep (Rep.Constructor name a) where
34+
decodeRep j = do
35+
let name = reflectSymbol (SProxy :: SProxy name)
36+
let decodingErr msg = "When decoding a " <> name <> ": " <> msg
37+
jObj <- mFail (decodingErr "expected an object") (toObject j)
38+
jTag <- mFail (decodingErr "'tag' property is missing") (SM.lookup "tag" jObj)
39+
tag <- mFail (decodingErr "'tag' property is not a string") (toString jTag)
40+
when (tag /= name) $
41+
Left $ decodingErr "'tag' property has an incorrect value"
42+
jValues <- mFail (decodingErr "'values' property is missing") (SM.lookup "values" jObj)
43+
values <- mFail (decodingErr "'values' property is not an array") (toArray jValues)
44+
{init, rest} <- lmap decodingErr $ decodeRepArgs values
45+
when (rest /= []) $
46+
Left $ decodingErr "'values' property had too many values"
47+
pure $ Rep.Constructor init
48+
49+
class DecodeRepArgs r where
50+
decodeRepArgs :: Array Json -> Either String {init :: r, rest :: Array Json}
51+
52+
instance decodeRepArgsNoArguments :: DecodeRepArgs Rep.NoArguments where
53+
decodeRepArgs js = Right {init: Rep.NoArguments, rest: js}
54+
55+
instance decodeRepArgsProduct :: (DecodeRepArgs a, DecodeRepArgs b) => DecodeRepArgs (Rep.Product a b) where
56+
decodeRepArgs js = do
57+
{init: a, rest: js'} <- decodeRepArgs js
58+
{init: b, rest: js''} <- decodeRepArgs js'
59+
pure {init: Rep.Product a b, rest: js''}
60+
61+
instance decodeRepArgsArgument :: (DecodeJson a) => DecodeRepArgs (Rep.Argument a) where
62+
decodeRepArgs js = do
63+
{head, tail} <- mFail "too few values were present" (uncons js)
64+
{init: _, rest: tail} <<< Rep.Argument <$> decodeJson head
65+
66+
instance decodeRepArgsRec :: (DecodeRepFields fields) => DecodeRepArgs (Rep.Rec fields) where
67+
decodeRepArgs js = do
68+
{head, tail} <- mFail "too few values were present" (uncons js)
69+
jObj <- mFail "record is not encoded as an object" (toObject head)
70+
{init: _, rest: tail} <<< Rep.Rec <$> decodeRepFields jObj
71+
72+
class DecodeRepFields r where
73+
decodeRepFields :: SM.StrMap Json -> Either String r
74+
75+
instance decodeRepFieldsProduct :: (DecodeRepFields a, DecodeRepFields b) => DecodeRepFields (Rep.Product a b) where
76+
decodeRepFields js = Rep.Product <$> decodeRepFields js <*> decodeRepFields js
77+
78+
instance decodeRepFieldsField :: (IsSymbol field, DecodeJson a) => DecodeRepFields (Rep.Field field a) where
79+
decodeRepFields js = do
80+
let name = reflectSymbol (SProxy :: SProxy field)
81+
value <- mFail ("the field '" <> name <> "' is not present") (SM.lookup name js)
82+
Rep.Field <$> decodeJson value
83+
84+
-- | Decode `Json` representation of a value which has a `Generic` type.
85+
genericDecodeJson :: forall a r. Rep.Generic a r => DecodeRep r => Json -> Either String a
86+
genericDecodeJson = map Rep.to <<< decodeRep
87+
88+
mFail :: forall a. String -> Maybe a -> Either String a
89+
mFail msg = maybe (Left msg) Right

src/Data/Argonaut/Encode/Generic.purs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module Data.Argonaut.Encode.Generic (
2-
gEncodeJson,
2+
gEncodeJson,
33
gEncodeJson'
44
) where
55

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
module Data.Argonaut.Encode.Generic.Rep (
2+
class EncodeRep,
3+
class EncodeRepArgs,
4+
class EncodeRepFields,
5+
encodeRep,
6+
encodeRepArgs,
7+
encodeRepFields,
8+
genericEncodeJson
9+
) where
10+
11+
import Prelude
12+
13+
import Data.Argonaut.Encode.Class (class EncodeJson, encodeJson)
14+
import Data.Argonaut.Core (Json, fromArray, fromObject, fromString)
15+
import Data.Generic.Rep as Rep
16+
import Data.StrMap as SM
17+
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
18+
19+
class EncodeRep r where
20+
encodeRep :: r -> Json
21+
22+
instance encodeRepNoConstructors :: EncodeRep Rep.NoConstructors where
23+
encodeRep r = encodeRep r
24+
25+
instance encodeRepSum :: (EncodeRep a, EncodeRep b) => EncodeRep (Rep.Sum a b) where
26+
encodeRep (Rep.Inl a) = encodeRep a
27+
encodeRep (Rep.Inr b) = encodeRep b
28+
29+
instance encodeRepConstructor :: (IsSymbol name, EncodeRepArgs a) => EncodeRep (Rep.Constructor name a) where
30+
encodeRep (Rep.Constructor a) =
31+
fromObject
32+
$ SM.insert "tag" (fromString (reflectSymbol (SProxy :: SProxy name)))
33+
$ SM.insert "values" (fromArray (encodeRepArgs a))
34+
$ SM.empty
35+
36+
class EncodeRepArgs r where
37+
encodeRepArgs :: r -> Array Json
38+
39+
instance encodeRepArgsNoArguments :: EncodeRepArgs Rep.NoArguments where
40+
encodeRepArgs Rep.NoArguments = []
41+
42+
instance encodeRepArgsProduct :: (EncodeRepArgs a, EncodeRepArgs b) => EncodeRepArgs (Rep.Product a b) where
43+
encodeRepArgs (Rep.Product a b) = encodeRepArgs a <> encodeRepArgs b
44+
45+
instance encodeRepArgsArgument :: (EncodeJson a) => EncodeRepArgs (Rep.Argument a) where
46+
encodeRepArgs (Rep.Argument a) = [encodeJson a]
47+
48+
instance encodeRepArgsRec :: (EncodeRepFields fields) => EncodeRepArgs (Rep.Rec fields) where
49+
encodeRepArgs (Rep.Rec fields) = [fromObject $ encodeRepFields fields]
50+
51+
class EncodeRepFields r where
52+
encodeRepFields :: r -> SM.StrMap Json
53+
54+
instance encodeRepFieldsProduct :: (EncodeRepFields a, EncodeRepFields b) => EncodeRepFields (Rep.Product a b) where
55+
encodeRepFields (Rep.Product a b) =
56+
SM.union (encodeRepFields a) (encodeRepFields b)
57+
58+
instance encodeRepFieldsField :: (IsSymbol field, EncodeJson a) => EncodeRepFields (Rep.Field field a) where
59+
encodeRepFields (Rep.Field a) =
60+
SM.singleton (reflectSymbol (SProxy :: SProxy field))
61+
(encodeJson a)
62+
63+
-- | Encode any `Generic` data structure into `Json`.
64+
genericEncodeJson :: forall a r. Rep.Generic a r => EncodeRep r => a -> Json
65+
genericEncodeJson = encodeRep <<< Rep.from

test/Main.purs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
module Test.Main
2+
( main
3+
) where
4+
5+
import Prelude
6+
7+
import Control.Monad.Eff (Eff)
8+
import Control.Monad.Eff.Console (CONSOLE, log)
9+
import Data.Argonaut.Core (stringify)
10+
import Data.Argonaut.Decode.Class (class DecodeJson, decodeJson)
11+
import Data.Argonaut.Decode.Generic.Rep (genericDecodeJson)
12+
import Data.Argonaut.Encode.Class (class EncodeJson, encodeJson)
13+
import Data.Argonaut.Encode.Generic.Rep (genericEncodeJson)
14+
import Data.Either (Either(..))
15+
import Data.Generic.Rep (class Generic)
16+
import Data.Generic.Rep.Show (genericShow)
17+
import Test.Assert (ASSERT, assert)
18+
19+
data Example
20+
= Either (Either String Example)
21+
| Record {foo :: Int, bar :: String}
22+
| Product Int Int Example
23+
24+
derive instance eqExample :: Eq Example
25+
derive instance genericExample :: Generic Example _
26+
instance showExample :: Show Example where
27+
show a = genericShow a
28+
instance encodeJsonExample :: EncodeJson Example where
29+
encodeJson a = genericEncodeJson a
30+
instance decodeJson :: DecodeJson Example where
31+
decodeJson a = genericDecodeJson a
32+
33+
main :: forall eff. Eff (assert :: ASSERT, console :: CONSOLE | eff) Unit
34+
main = do
35+
example $ Either $ Left "foo"
36+
example $ Either $ Right $ Either $ Left "foo"
37+
example $ Record {foo: 42, bar: "bar"}
38+
example $ Product 1 2 $ Either $ Left "foo"
39+
where
40+
example original = do
41+
let json = encodeJson original
42+
let parsed = decodeJson json
43+
log $ "Original: " <> show original
44+
log $ "To JSON: " <> stringify json
45+
log $ "From JSON: " <> show parsed
46+
assert $ parsed == Right original
47+
log $ "--------------------------------------------------------------------------------"

0 commit comments

Comments
 (0)