Skip to content

Commit 0f004b6

Browse files
committed
Merge pull request #3 from zudov/generics
Generic deriving
2 parents ecc1bdb + 302aebe commit 0f004b6

File tree

6 files changed

+169
-15
lines changed

6 files changed

+169
-15
lines changed

bower.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
"license": "MIT",
2424
"dependencies": {
2525
"purescript-argonaut-core": "^0.2.0",
26-
"purescript-integers": "^0.2.1"
26+
"purescript-integers": "^0.2.1",
27+
"purescript-generics": "^0.5.1"
2728
},
2829
"devDependencies": {
29-
"purescript-strongcheck": "^0.12.1"
30+
"purescript-strongcheck": "^0.12.1",
31+
"purescript-strongcheck-generics": "^0.1.0"
3032
}
3133
}

docs/Data/Argonaut/Decode.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ instance decodeList :: (DecodeJson a) => DecodeJson (List a)
2525
instance decodeMap :: (Ord a, DecodeJson a, DecodeJson b) => DecodeJson (Map a b)
2626
```
2727

28+
#### `gDecodeJson`
29+
30+
``` purescript
31+
gDecodeJson :: forall a. (Generic a) => Json -> Either String a
32+
```
33+
34+
Decode `Json` representation of a value which has a `Generic` type.
35+
36+
#### `gDecodeJson'`
37+
38+
``` purescript
39+
gDecodeJson' :: GenericSignature -> Json -> Either String GenericSpine
40+
```
41+
42+
Decode `Json` representation of a `GenericSpine`.
43+
2844
#### `decodeMaybe`
2945

3046
``` purescript

docs/Data/Argonaut/Encode.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,20 @@ instance encodeStrMap :: (EncodeJson a) => EncodeJson (StrMap a)
2525
instance encodeMap :: (Ord a, EncodeJson a, EncodeJson b) => EncodeJson (Map a b)
2626
```
2727

28+
#### `gEncodeJson`
29+
30+
``` purescript
31+
gEncodeJson :: forall a. (Generic a) => a -> Json
32+
```
33+
34+
Encode any `Generic` data structure into `Json`.
35+
36+
#### `gEncodeJson'`
37+
38+
``` purescript
39+
gEncodeJson' :: GenericSpine -> Json
40+
```
41+
42+
Encode `GenericSpine` into `Json`.
43+
2844

src/Data/Argonaut/Decode.purs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module Data.Argonaut.Decode
22
( DecodeJson
33
, decodeJson
4+
, gDecodeJson
5+
, gDecodeJson'
46
, decodeMaybe
57
) where
68

@@ -20,23 +22,65 @@ import Data.Argonaut.Core
2022
, toNumber
2123
, toObject
2224
, toString
25+
, toBoolean
2326
)
27+
import Data.Array (zipWithA)
2428
import Data.Either (either, Either(..))
2529
import Data.Int (fromNumber)
2630
import Data.Maybe (maybe, Maybe(..))
27-
import Data.Traversable (traverse)
2831
import Data.Tuple (Tuple(..))
2932
import Data.String
3033
import Data.List (List(..), toList)
3134
import Control.Alt
32-
import Data.Traversable (traverse)
35+
import Control.Bind ((=<<))
36+
import Data.Traversable (traverse, for)
37+
import Data.Foldable (find)
38+
import Data.Generic
3339

3440
import qualified Data.StrMap as M
3541
import qualified Data.Map as Map
3642

3743
class DecodeJson a where
3844
decodeJson :: Json -> Either String a
3945

46+
-- | Decode `Json` representation of a value which has a `Generic` type.
47+
gDecodeJson :: forall a. (Generic a) => Json -> Either String a
48+
gDecodeJson json = maybe (Left "fromSpine failed") Right <<< fromSpine
49+
=<< gDecodeJson' (toSignature (Proxy :: Proxy a)) json
50+
51+
-- | Decode `Json` representation of a `GenericSpine`.
52+
gDecodeJson' :: GenericSignature -> Json -> Either String GenericSpine
53+
gDecodeJson' signature json = case signature of
54+
SigNumber
55+
-> SNumber <$> mFail "Expected a number" (toNumber json)
56+
SigInt
57+
-> SInt <$> mFail "Expected an integer number" (fromNumber =<< toNumber json)
58+
SigString
59+
-> SString <$> mFail "Expected a string" (toString json)
60+
SigBoolean
61+
-> SBoolean <$> mFail "Expected a boolean" (toBoolean json)
62+
SigArray thunk
63+
-> do jArr <- mFail "Expected an array" $ toArray json
64+
SArray <$> traverse (map const <<< gDecodeJson' (thunk unit)) jArr
65+
SigRecord props
66+
-> do jObj <- mFail "Expected an object" $ toObject json
67+
SRecord <$> for props \({recLabel: lbl, recValue: val})
68+
-> do pf <- mFail ("'" <> lbl <> "' property missing") (M.lookup lbl jObj)
69+
sp <- gDecodeJson' (val unit) pf
70+
pure { recLabel: lbl, recValue: const sp }
71+
SigProd alts
72+
-> do jObj <- mFail "Expected an object" $ toObject json
73+
tag <- mFail "'tag' string property is missing" (toString =<< M.lookup "tag" jObj)
74+
case find ((tag ==) <<< _.sigConstructor) alts of
75+
Nothing -> Left ("'" <> tag <> "' isn't a valid constructor")
76+
Just { sigValues: sigValues } -> do
77+
vals <- mFail "'values' array is missing" (toArray =<< M.lookup "values" jObj)
78+
sps <- zipWithA (\k -> gDecodeJson' (k unit)) sigValues vals
79+
pure (SProd tag (const <$> sps))
80+
where
81+
mFail :: forall a. String -> Maybe a -> Either String a
82+
mFail msg = maybe (Left msg) Right
83+
4084
instance decodeJsonMaybe :: (DecodeJson a) => DecodeJson (Maybe a) where
4185
decodeJson j = (Just <$> decodeJson j) <|> pure Nothing
4286

src/Data/Argonaut/Encode.purs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
module Data.Argonaut.Encode
22
( EncodeJson
33
, encodeJson
4+
, gEncodeJson
5+
, gEncodeJson'
46
) where
57

68
import Prelude
79

810
import Data.Argonaut.Core
9-
( Json(..)
11+
( Json()
1012
, foldJsonObject
1113
, jsonNull
1214
, fromNull
@@ -22,18 +24,38 @@ import Data.Argonaut.Core
2224
import Data.String (fromChar)
2325
import Data.Maybe
2426
import Data.Either
25-
import Data.List (List(..), fromList)
27+
import Data.List (List(..), fromList, toList)
2628
import Data.Int (toNumber)
2729
import Data.Unfoldable ()
28-
import Data.Foldable (foldr)
30+
import Data.Foldable (foldMap, foldr)
2931
import Data.Tuple (Tuple(..))
32+
import Data.Generic
3033

3134
import qualified Data.StrMap as SM
3235
import qualified Data.Map as M
3336

3437
class EncodeJson a where
3538
encodeJson :: a -> Json
3639

40+
-- | Encode any `Generic` data structure into `Json`.
41+
gEncodeJson :: forall a. (Generic a) => a -> Json
42+
gEncodeJson = gEncodeJson' <<< toSpine
43+
44+
-- | Encode `GenericSpine` into `Json`.
45+
gEncodeJson' :: GenericSpine -> Json
46+
gEncodeJson' spine = case spine of
47+
SInt x -> fromNumber $ toNumber x
48+
SString x -> fromString x
49+
SNumber x -> fromNumber x
50+
SBoolean x -> fromBoolean x
51+
SArray thunks -> fromArray (gEncodeJson' <<< (unit #) <$> thunks)
52+
SProd constr args -> fromObject
53+
$ SM.insert "tag" (encodeJson constr)
54+
$ SM.singleton "values" (encodeJson (gEncodeJson' <<< (unit #) <$> args))
55+
SRecord fields -> fromObject $ foldr addField SM.empty fields
56+
where addField field = SM.insert field.recLabel
57+
(gEncodeJson' $ field.recValue unit)
58+
3759
instance encodeJsonMaybe :: (EncodeJson a) => EncodeJson (Maybe a) where
3860
encodeJson Nothing = jsonNull
3961
encodeJson (Just a) = encodeJson a

test/Test/Main.purs

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,23 @@ module Test.Main where
22

33
import Prelude
44

5-
import Data.Argonaut.Core
6-
import Data.Argonaut.Decode (decodeJson, DecodeJson)
7-
import Data.Argonaut.Encode (encodeJson, EncodeJson)
5+
import Data.Argonaut.Core
6+
import Data.Argonaut.Decode (decodeJson, DecodeJson, gDecodeJson, gDecodeJson')
7+
import Data.Argonaut.Encode (encodeJson, EncodeJson, gEncodeJson, gEncodeJson')
88
import Data.Argonaut.Combinators ((:=), (~>), (?>>=), (.?))
99
import Data.Either
1010
import Data.Tuple
1111
import Data.Maybe
1212
import Data.Array
13+
import Data.Generic
1314
import Data.Foldable (foldl)
1415
import Data.List (toList, List(..))
1516
import Control.Monad.Eff.Console
1617
import qualified Data.StrMap as M
1718

1819
import Test.StrongCheck
1920
import Test.StrongCheck.Gen
20-
21+
import Test.StrongCheck.Generic
2122

2223
genJNull :: Gen Json
2324
genJNull = pure jsonNull
@@ -67,7 +68,7 @@ prop_decode_then_encode (TestJson json) =
6768
Right json == (decoded >>= (encodeJson >>> pure))
6869

6970

70-
encodeDecodeCheck = do
71+
encodeDecodeCheck = do
7172
log "Showing small sample of JSON"
7273
showSample (genJson 10)
7374

@@ -81,7 +82,7 @@ prop_assoc_builder_str :: Tuple String String -> Boolean
8182
prop_assoc_builder_str (Tuple key str) =
8283
case (key := str) of
8384
Tuple k json ->
84-
(key == k) && (decodeJson json == Right str)
85+
(key == k) && (decodeJson json == Right str)
8586

8687
newtype Obj = Obj Json
8788
unObj :: Obj -> Json
@@ -110,7 +111,7 @@ prop_get_jobject_field (Obj obj) =
110111
in foldl (\ok key -> ok && (isJust $ M.lookup key obj)) true keys
111112

112113
assert_maybe_msg :: Boolean
113-
assert_maybe_msg =
114+
assert_maybe_msg =
114115
(isLeft (Nothing ?>>= "Nothing is Left"))
115116
&&
116117
((Just 2 ?>>= "Nothing is left") == Right 2)
@@ -127,9 +128,62 @@ combinatorsCheck = do
127128
quickCheck' 20 prop_get_jobject_field
128129
log "Assert maybe to either convertion"
129130
assert assert_maybe_msg
130-
131+
132+
newtype MyRecord = MyRecord { foo :: String, bar :: Int}
133+
derive instance genericMyRecord :: Generic MyRecord
134+
135+
data User = Anonymous
136+
| Guest String
137+
| Registered { name :: String
138+
, age :: Int
139+
, balance :: Number
140+
, banned :: Boolean
141+
, tweets :: Array String
142+
, followers :: Array User
143+
}
144+
derive instance genericUser :: Generic User
145+
146+
prop_iso_generic :: GenericValue -> Boolean
147+
prop_iso_generic genericValue =
148+
Right val.spine == gDecodeJson' val.signature (gEncodeJson' val.spine)
149+
where val = runGenericValue genericValue
150+
151+
prop_decoded_spine_valid :: GenericValue -> Boolean
152+
prop_decoded_spine_valid genericValue =
153+
Right true == (isValidSpine val.signature <$> gDecodeJson' val.signature (gEncodeJson' val.spine))
154+
where val = runGenericValue genericValue
155+
156+
genericsCheck = do
157+
log "Check that decodeJson' and encodeJson' form an isomorphism"
158+
quickCheck prop_iso_generic
159+
log "Check that decodeJson' returns a valid spine"
160+
quickCheck prop_decoded_spine_valid
161+
log "Print samples of values encoded with gEncodeJson"
162+
print $ gEncodeJson 5
163+
print $ gEncodeJson [1, 2, 3, 5]
164+
print $ gEncodeJson (Just "foo")
165+
print $ gEncodeJson (Right "foo" :: Either String String)
166+
print $ gEncodeJson $ MyRecord { foo: "foo", bar: 2}
167+
print $ gEncodeJson "foo"
168+
print $ gEncodeJson Anonymous
169+
print $ gEncodeJson $ Guest "guest's handle"
170+
print $ gEncodeJson $ Registered { name: "user1"
171+
, age: 5
172+
, balance: 26.6
173+
, banned: false
174+
, tweets: ["Hello", "What's up"]
175+
, followers: [ Anonymous
176+
, Guest "someGuest"
177+
, Registered { name: "user2"
178+
, age: 6
179+
, balance: 32.1
180+
, banned: false
181+
, tweets: ["Hi"]
182+
, followers: []
183+
}]}
131184

132185

133186
main = do
134187
encodeDecodeCheck
135188
combinatorsCheck
189+
genericsCheck

0 commit comments

Comments
 (0)