Skip to content

Commit c771169

Browse files
authored
Merge pull request #2 from garyb/records
Add fancy record codecs
2 parents c7f03a2 + a1c6be6 commit c771169

File tree

5 files changed

+110
-10
lines changed

5 files changed

+110
-10
lines changed

src/Data/Codec/Argonaut.purs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ module Data.Codec.Argonaut
1818
, JPropCodec
1919
, object
2020
, prop
21+
, record
22+
, recordProp
2123
, module Exports
2224
) where
2325

2426
import Prelude
2527

2628
import Control.Monad.Reader (ReaderT(..), runReaderT)
27-
import Control.Monad.Writer (writer, mapWriter)
29+
import Control.Monad.Writer (Writer, writer, mapWriter)
2830
import Data.Argonaut.Core as J
2931
import Data.Array as A
3032
import Data.Bifunctor as BF
@@ -34,13 +36,17 @@ import Data.Either (Either(..))
3436
import Data.Generic.Rep (class Generic)
3537
import Data.Generic.Rep.Show (genericShow)
3638
import Data.Int as I
39+
import Data.List ((:))
3740
import Data.List as L
38-
import Data.Maybe (Maybe(..), maybe)
41+
import Data.Maybe (Maybe(..), maybe, fromJust)
3942
import Data.Profunctor.Star (Star(..))
4043
import Data.String as S
4144
import Data.StrMap as SM
45+
import Data.Symbol (class IsSymbol, SProxy, reflectSymbol)
4246
import Data.Traversable (traverse)
4347
import Data.Tuple (Tuple(..))
48+
import Partial.Unsafe (unsafePartial)
49+
import Unsafe.Coerce (unsafeCoerce)
4450

4551
-- | Codec type for `Json` values.
4652
type JsonCodec a = BasicCodec (Either JsonDecodeError) J.Json a
@@ -175,8 +181,55 @@ prop key codec = GCodec dec enc
175181
BF.lmap (AtKey key) case SM.lookup key obj of
176182
Just val → decode codec val
177183
NothingLeft MissingValue
184+
enc Star (Writer (L.List J.JAssoc)) a a
178185
enc = Star \val → writer $ Tuple val (pure (Tuple key (encode codec val)))
179186

187+
-- | The starting value for a object-record codec. Used with `recordProp` it
188+
-- | provides a convenient method for defining codecs for record types that
189+
-- | encode into JSON objects of the same shape.
190+
-- |
191+
-- | For example:
192+
-- | ```
193+
-- | myRecordCodec =
194+
-- | object "MyRecord" $ record
195+
-- | # recordProp (SProxy :: SProxy "tag") tagCodec
196+
-- | # recordProp (SProxy :: SProxy "value") valueCodec
197+
-- | ```
198+
record JPropCodec {}
199+
record = GCodec (pure {}) (Star \val → writer (Tuple val L.Nil))
200+
201+
-- | Used with `record` to define codecs for record types that encode into JSON
202+
-- | objects of the same shape. See the comment on `record` for an example.
203+
recordProp
204+
p a r r'
205+
. IsSymbol p
206+
RowCons p a r r'
207+
SProxy p
208+
JsonCodec a
209+
JPropCodec (Record r)
210+
JPropCodec (Record r')
211+
recordProp p codecA codecR =
212+
let key = reflectSymbol p in GCodec (dec' key) (enc' key)
213+
where
214+
dec' String ReaderT J.JObject (Either JsonDecodeError) (Record r')
215+
dec' key = ReaderT \obj → do
216+
r ← decode codecR obj
217+
a ← BF.lmap (AtKey key) case SM.lookup key obj of
218+
Just val → decode codecA val
219+
NothingLeft MissingValue
220+
pure $ unsafeSet key a r
221+
enc' String Star (Writer (L.List J.JAssoc)) (Record r') (Record r')
222+
enc' key = Star \val →
223+
writer $ Tuple val
224+
$ Tuple key (encode codecA (unsafeGet key val))
225+
: encode codecR (unsafeForget val)
226+
unsafeForget Record r' Record r
227+
unsafeForget = unsafeCoerce
228+
unsafeSet String a Record r Record r'
229+
unsafeSet key a = unsafeCoerce <<< SM.insert key a <<< unsafeCoerce
230+
unsafeGet String Record r' a
231+
unsafeGet s = unsafePartial fromJust <<< SM.lookup s <<< unsafeCoerce
232+
180233
jsonPrimCodec
181234
a
182235
. String

src/Data/Codec/Argonaut/Common.purs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Data.Argonaut.Core as J
99
import Data.Array as A
1010
import Data.Bifunctor as BF
1111
import Data.Codec (basicCodec)
12-
import Data.Codec.Argonaut (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, decode, encode, index, indexedArray, int, jarray, jobject, json, null, number, object, printJsonDecodeError, prop, string, (<~<), (~))
12+
import Data.Codec.Argonaut (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, decode, encode, index, indexedArray, int, jarray, jobject, json, null, number, object, printJsonDecodeError, prop, record, recordProp, string, (<~<), (~))
1313
import Data.Codec.Argonaut.Sum (Tag(..), taggedSum)
1414
import Data.Either (Either(..))
1515
import Data.List as L

src/Data/Codec/Argonaut/Compat.purs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Prelude
1010
import Data.Argonaut.Core as J
1111
import Data.Bifunctor as BF
1212
import Data.Codec (basicCodec, mapCodec)
13-
import Data.Codec.Argonaut (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, decode, encode, index, indexedArray, int, jarray, jobject, json, null, number, object, printJsonDecodeError, prop, string, (<~<), (~))
13+
import Data.Codec.Argonaut (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, decode, encode, index, indexedArray, int, jarray, jobject, json, null, number, object, printJsonDecodeError, prop, record, recordProp, string, (<~<), (~))
1414
import Data.Codec.Argonaut.Common (either, list, map, tuple) as Common
1515
import Data.Either (Either)
1616
import Data.Functor as F

test/Test/Prim.purs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import Control.Monad.Gen as Gen
77
import Data.Argonaut.Core as J
88
import Data.Argonaut.Gen (genJson)
99
import Data.Char.Gen (genAsciiChar)
10+
import Data.Codec.Argonaut.Common ((~))
1011
import Data.Codec.Argonaut.Common as JA
1112
import Data.String.Gen (genAsciiString)
1213
import Data.StrMap.Gen (genStrMap)
14+
import Data.Symbol (SProxy(..))
1315
import Test.QuickCheck (QC, Result, quickCheck)
1416
import Test.QuickCheck.Gen (Gen)
15-
import Test.Util (propCodec)
17+
import Test.Util (propCodec, propCodec', genInt)
1618

1719
main :: QC () Unit
1820
main = do
@@ -40,6 +42,12 @@ main = do
4042
log "Checking JObject codec"
4143
quickCheck propJObject
4244

45+
log "Checking object codec"
46+
quickCheck (propTestRecord codecObject)
47+
48+
log "Checking record codec"
49+
quickCheck (propTestRecord codecRecord)
50+
4351
propNull Gen Result
4452
propNull = propCodec (pure J.jNull) JA.null
4553

@@ -50,7 +58,7 @@ propNumber ∷ Gen Result
5058
propNumber = propCodec (Gen.chooseFloat (-100000.0) 100000.0) JA.number
5159

5260
propInt Gen Result
53-
propInt = propCodec (Gen.chooseInt (-100000) 100000) JA.int
61+
propInt = propCodec genInt JA.int
5462

5563
propString Gen Result
5664
propString = propCodec genAsciiString JA.string
@@ -63,3 +71,34 @@ propJArray = propCodec (Gen.unfoldable genJson) JA.jarray
6371

6472
propJObject Gen Result
6573
propJObject = propCodec (genStrMap genAsciiString genJson) JA.jobject
74+
75+
type TestRecord = { tag String, x Int, y Boolean }
76+
77+
genRecord Gen TestRecord
78+
genRecord =
79+
{ tag: _, x: _, y: _ }
80+
<$> genAsciiString
81+
<*> genInt
82+
<*> Gen.chooseBool
83+
84+
codecObject JA.JsonCodec TestRecord
85+
codecObject =
86+
JA.object "Test Object" $
87+
{ tag: _, x: _, y: _ }
88+
<$> _.tag ~ JA.prop "tag" JA.string
89+
<*> _.x ~ JA.prop "x" JA.int
90+
<*> _.y ~ JA.prop "y" JA.boolean
91+
92+
codecRecord JA.JsonCodec TestRecord
93+
codecRecord =
94+
JA.object "Test Record" $ JA.record
95+
# JA.recordProp (SProxy SProxy "tag") JA.string
96+
# JA.recordProp (SProxy SProxy "x") JA.int
97+
# JA.recordProp (SProxy SProxy "y") JA.boolean
98+
99+
propTestRecord JA.JsonCodec TestRecord Gen Result
100+
propTestRecord = propCodec' checkEq print genRecord
101+
where
102+
checkEq r1 r2 = r1.tag == r2.tag && r1.x == r2.x && r1.y == r2.y
103+
print { tag, x, y } =
104+
"{ tag: " <> show tag <> ", x: " <> show x <> ", y: " <> show y <> " }"

test/Test/Util.purs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@ import Prelude
55
import Control.Monad.Gen as Gen
66
import Data.Codec.Argonaut.Common as JA
77
import Data.Either (Either(..))
8-
import Test.QuickCheck (Result, (===))
8+
import Test.QuickCheck (Result(..), (<?>))
99
import Test.QuickCheck.Gen (Gen)
1010

11-
propCodec a. Eq a Show a Gen a JA.JsonCodec a Gen Result
12-
propCodec gen codec = do
11+
propCodec' a. (a a Boolean) (a String) Gen a JA.JsonCodec a Gen Result
12+
propCodec' eq' show' gen codec = do
1313
x ← gen
14-
pure $ Right x === JA.decode codec (JA.encode codec x)
14+
pure case JA.decode codec (JA.encode codec x) of
15+
Left err →
16+
Failed
17+
$ "Decoding " <> show' x <> " failed with error: " <> JA.printJsonDecodeError err
18+
Right y →
19+
x `eq'` y <?> "Decoded result:\n" <> show' x <> "\n\nDid not match input:\n" <> show' y
20+
21+
propCodec a. Eq a Show a Gen a JA.JsonCodec a Gen Result
22+
propCodec = propCodec' eq show
1523

1624
genInt Gen Int
1725
genInt = Gen.chooseInt (-100000) 100000

0 commit comments

Comments
 (0)