Skip to content

Commit 0b5542a

Browse files
authored
Merge pull request #3 from garyb/misc
Misc additions
2 parents c771169 + 8579b36 commit 0b5542a

File tree

9 files changed

+245
-6
lines changed

9 files changed

+245
-6
lines changed

bower.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"dependencies": {
1919
"purescript-argonaut-core": "^3.1.0",
2020
"purescript-codec": "^2.0.0",
21-
"purescript-generics-rep": "^5.1.0"
21+
"purescript-generics-rep": "^5.1.0",
22+
"purescript-variant": "^1.1.0"
2223
},
2324
"devDependencies": {
2425
"purescript-argonaut-codecs": "^3.1.0",

src/Data/Codec/Argonaut.purs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type JsonCodec a = BasicCodec (Either JsonDecodeError) J.Json a
5454
-- | Error type for failures while decoding.
5555
data JsonDecodeError
5656
= TypeMismatch String
57-
| UnexpectedValue String
57+
| UnexpectedValue J.Json
5858
| AtIndex Int JsonDecodeError
5959
| AtKey String JsonDecodeError
6060
| Named String JsonDecodeError
@@ -74,7 +74,7 @@ printJsonDecodeError err =
7474
where
7575
go = case _ of
7676
TypeMismatch ty → " Expected value of type '" <> ty <> "'."
77-
UnexpectedValue val → " Unexpected value '" <> val <> "'."
77+
UnexpectedValue val → " Unexpected value " <> J.stringify val <> "."
7878
AtIndex ix inner → " At array index " <> show ix <> ":\n" <> go inner
7979
AtKey key inner → " At object key " <> key <> ":\n" <> go inner
8080
Named name inner → " Under '" <> name <> "':\n" <> go inner

src/Data/Codec/Argonaut/Common.purs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ maybe codec = basicCodec dec enc
3030
case tag of
3131
"Just"Just <$> decode (prop "value" codec) obj
3232
"Nothing" → pure Nothing
33-
_ → Left (AtKey "tag" (UnexpectedValue tag))
33+
_ → Left (AtKey "tag" (UnexpectedValue (J.fromString tag)))
3434
enc x = encode jobject $ SM.pureST do
3535
obj ← SMST.new
3636
case x of
@@ -56,7 +56,7 @@ either codecA codecB = taggedSum dec enc
5656
dec tag json = case tag of
5757
Tag "Left"BF.bimap (AtKey "value") Left (decode codecA json)
5858
Tag "Right"BF.bimap (AtKey "value") Right (decode codecB json)
59-
Tag t → Left (AtKey "tag" (UnexpectedValue t))
59+
Tag t → Left (AtKey "tag" (UnexpectedValue (J.fromString t)))
6060
enc = case _ of
6161
Left a → Tuple (Tag "Left") (encode codecA a)
6262
Right b → Tuple (Tag "Right") (encode codecB b)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module Data.Codec.Argonaut.Migration where
2+
3+
import Prelude
4+
5+
import Data.Argonaut.Core as J
6+
import Data.Codec (basicCodec)
7+
import Data.Codec.Argonaut (JsonCodec)
8+
import Data.Maybe (Maybe(..), maybe)
9+
import Data.Tuple (uncurry)
10+
import Data.StrMap as SM
11+
12+
-- | When dealing with a JSON object that may be missing a field, this codec
13+
-- | can be used to alter the JSON before parsing to ensure a default value is
14+
-- | present instead.
15+
addDefaultField String J.Json JsonCodec J.Json
16+
addDefaultField field defaultValue = basicCodec (pure <<< dec) id
17+
where
18+
dec J.Json J.Json
19+
dec j = J.foldJsonObject j (J.fromObject <<< setDefault) j
20+
setDefault J.JObject J.JObject
21+
setDefault = SM.alter (maybe (Just defaultValue) Just) field
22+
23+
-- | When dealing with a JSON object that has had a field name changed, this
24+
-- | codec can be used to alter the JSON before parsing to ensure the new field
25+
-- | name is used instead
26+
renameField String String JsonCodec J.Json
27+
renameField oldName newName = basicCodec (pure <<< dec) id
28+
where
29+
dec J.Json J.Json
30+
dec j = J.foldJsonObject j (J.fromObject <<< rename) j
31+
rename J.JObject J.JObject
32+
rename obj = maybe obj (uncurry (SM.insert newName)) (SM.pop oldName obj)

src/Data/Codec/Argonaut/Variant.purs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
module Data.Codec.Argonaut.Variant where
2+
3+
import Prelude
4+
5+
import Control.Monad.Reader (ReaderT(..), runReaderT)
6+
import Control.Monad.Writer (Writer, writer)
7+
import Data.Argonaut.Core as J
8+
import Data.Codec (GCodec(..))
9+
import Data.Codec.Argonaut (JsonCodec, JsonDecodeError(..), decode, encode, jobject, json, prop, string)
10+
import Data.Either (Either(..))
11+
import Data.Newtype (un)
12+
import Data.Profunctor.Star (Star(..))
13+
import Data.StrMap as SM
14+
import Data.StrMap.ST as SMST
15+
import Data.Symbol (class IsSymbol, reflectSymbol)
16+
import Data.Tuple (Tuple(..))
17+
import Data.Variant (SProxy, Variant, case_, inj, on)
18+
import Unsafe.Coerce (unsafeCoerce)
19+
20+
variant JsonCodec (Variant ())
21+
variant = GCodec (ReaderT (Left <<< UnexpectedValue)) (Star case_)
22+
23+
variantCase
24+
l a r r'
25+
. IsSymbol l
26+
RowCons l a r r'
27+
SProxy l
28+
Either a (JsonCodec a)
29+
JsonCodec (Variant r)
30+
JsonCodec (Variant r')
31+
variantCase proxy eacodec (GCodec dec enc) = GCodec dec' enc'
32+
where
33+
34+
dec' ReaderT J.Json (Either JsonDecodeError) (Variant r')
35+
dec' = ReaderT \j → do
36+
obj ← decode jobject j
37+
tag ← decode (prop "tag" string) obj
38+
if tag == reflectSymbol proxy
39+
then case eacodec of
40+
Left a → pure (inj proxy a)
41+
Right codec → do
42+
value ← decode (prop "value" json) obj
43+
inj proxy <$> decode codec value
44+
else coerceR <$> runReaderT dec j
45+
46+
enc' Star (Writer J.Json) (Variant r') (Variant r')
47+
enc' = Star \v →
48+
on proxy
49+
(\v' → writer $ Tuple v $ encode jobject $
50+
SM.pureST do
51+
obj ← SMST.new
52+
_ ← SMST.poke obj "tag" (encode string (reflectSymbol proxy))
53+
case eacodec of
54+
Left _ → pure obj
55+
Right codec → SMST.poke obj "value" (encode codec v'))
56+
(\v' → un Star enc v' $> v) v
57+
58+
coerceR Variant r Variant r'
59+
coerceR = unsafeCoerce

test/Test/Main.purs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ module Test.Main where
33
import Prelude
44

55
import Control.Monad.Eff.Console (log)
6-
import Test.Prim as Prim
76
import Test.Common as Common
87
import Test.Compat as Compat
8+
import Test.Migration as Migration
9+
import Test.Prim as Prim
910
import Test.QuickCheck (QC)
11+
import Test.Variant as Variant
1012

1113
main :: QC () Unit
1214
main = do
@@ -21,3 +23,11 @@ main = do
2123
log "Checking Compat codecs"
2224
log "------------------------------------------------------------"
2325
Compat.main
26+
log ""
27+
log "Checking Variant codecs"
28+
log "------------------------------------------------------------"
29+
Variant.main
30+
log ""
31+
log "Checking Migration codecs"
32+
log "------------------------------------------------------------"
33+
Migration.main

test/Test/Migration.purs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
module Test.Migration where
2+
3+
import Prelude
4+
5+
import Control.Monad.Eff.Console (log)
6+
import Data.Argonaut.Core as J
7+
import Data.Codec ((<~<))
8+
import Data.Codec.Argonaut.Common as JA
9+
import Data.Codec.Argonaut.Migration as JAM
10+
import Data.Either (Either(..))
11+
import Data.Maybe (Maybe(..))
12+
import Data.String.Gen (genAsciiString)
13+
import Data.StrMap as SM
14+
import Test.QuickCheck (Result(..), QC, quickCheck, (===))
15+
import Test.QuickCheck.Gen (Gen)
16+
import Test.Util (genJObject)
17+
18+
main :: QC () Unit
19+
main = do
20+
log "Checking addDefaultField adds a field if it is missing"
21+
quickCheck propDefaultFieldAdded
22+
23+
log "Checking addDefaultField preserves an existing value if it's already present"
24+
quickCheck propDefaultFieldPreservesOriginal
25+
26+
log "Checking renameField renames a field"
27+
quickCheck propDefaultFieldPreservesOriginal
28+
29+
propDefaultFieldAdded Gen Result
30+
propDefaultFieldAdded = do
31+
let expectedValue = J.fromString "it's here"
32+
missingKey ← genAsciiString
33+
input ← SM.delete missingKey <$> genJObject
34+
pure $ testMigrationCodec { key: missingKey, expectedValue, input }
35+
$ JA.jobject <~< JAM.addDefaultField missingKey expectedValue
36+
37+
propDefaultFieldPreservesOriginal Gen Result
38+
propDefaultFieldPreservesOriginal = do
39+
let expectedValue = J.fromString "it's here"
40+
let unexpectedValue = J.fromString "it shouldn't be here"
41+
missingKey ← genAsciiString
42+
input ← SM.insert missingKey expectedValue <$> genJObject
43+
pure $ testMigrationCodec { key: missingKey, expectedValue, input }
44+
$ JA.jobject <~< JAM.addDefaultField missingKey unexpectedValue
45+
46+
propRenameField Gen Result
47+
propRenameField = do
48+
let expectedValue = J.fromString "it's here"
49+
oldKey ← genAsciiString
50+
newKey ← genAsciiString
51+
input ← SM.insert oldKey expectedValue <$> genJObject
52+
pure $ testMigrationCodec { key: newKey, expectedValue, input }
53+
$ JA.jobject <~< JAM.renameField oldKey newKey
54+
55+
testMigrationCodec
56+
{ key String
57+
, expectedValue J.Json
58+
, input J.JObject
59+
}
60+
JA.JsonCodec J.JObject
61+
Result
62+
testMigrationCodec { key, expectedValue, input } codec =
63+
case JA.decode codec (J.fromObject input) of
64+
Left err → Failed (JA.printJsonDecodeError err)
65+
Right obj →
66+
case SM.lookup key obj of
67+
Just value → value === expectedValue
68+
NothingFailed (JA.printJsonDecodeError (JA.AtKey key JA.MissingValue))

test/Test/Util.purs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ module Test.Util where
33
import Prelude
44

55
import Control.Monad.Gen as Gen
6+
import Data.Argonaut.Core as J
7+
import Data.Argonaut.Gen as GenJ
68
import Data.Codec.Argonaut.Common as JA
79
import Data.Either (Either(..))
10+
import Data.String.Gen (genAsciiString)
11+
import Data.StrMap.Gen as SMG
812
import Test.QuickCheck (Result(..), (<?>))
913
import Test.QuickCheck.Gen (Gen)
1014

@@ -23,3 +27,6 @@ propCodec = propCodec' eq show
2327

2428
genInt Gen Int
2529
genInt = Gen.chooseInt (-100000) 100000
30+
31+
genJObject Gen J.JObject
32+
genJObject = SMG.genStrMap genAsciiString GenJ.genJson

test/Test/Variant.purs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
module Test.Variant where
2+
3+
import Prelude
4+
5+
import Control.Monad.Eff.Console (log)
6+
import Control.Monad.Gen.Common as GenC
7+
import Data.Codec.Argonaut.Common as JA
8+
import Data.Codec.Argonaut.Variant as JAV
9+
import Data.Either (Either(..))
10+
import Data.Maybe (Maybe(..))
11+
import Data.Profunctor (dimap)
12+
import Data.String.Gen (genAsciiString)
13+
import Data.Symbol (SProxy(..))
14+
import Data.Variant as V
15+
import Test.QuickCheck (QC, quickCheck)
16+
import Test.Util (genInt, propCodec)
17+
18+
main :: QC () Unit
19+
main = do
20+
log "Checking Maybe-variant codec"
21+
quickCheck $
22+
propCodec
23+
(GenC.genMaybe genAsciiString)
24+
(codecMaybe JA.string)
25+
26+
log "Checking Either-variant codec"
27+
quickCheck $
28+
propCodec
29+
(GenC.genEither genAsciiString genInt)
30+
(codecEither JA.string JA.int)
31+
32+
codecMaybe a. JA.JsonCodec a JA.JsonCodec (Maybe a)
33+
codecMaybe codecA =
34+
dimap toVariant fromVariant
35+
(JAV.variant
36+
# JAV.variantCase _Just (Right codecA)
37+
# JAV.variantCase _Nothing (Left unit))
38+
where
39+
toVariant = case _ of
40+
Just a → V.inj _Just a
41+
NothingV.inj _Nothing unit
42+
fromVariant = V.case_
43+
# V.on _Just Just
44+
# V.on _Nothing (const Nothing)
45+
_Just = SProxy SProxy "just"
46+
_Nothing = SProxy SProxy "nothing"
47+
48+
codecEither a b. JA.JsonCodec a JA.JsonCodec b JA.JsonCodec (Either a b)
49+
codecEither codecA codecB =
50+
dimap toVariant fromVariant
51+
(JAV.variant
52+
# JAV.variantCase _Left (Right codecA)
53+
# JAV.variantCase _Right (Right codecB))
54+
where
55+
toVariant = case _ of
56+
Left a → V.inj _Left a
57+
Right b → V.inj _Right b
58+
fromVariant = V.case_
59+
# V.on _Left Left
60+
# V.on _Right Right
61+
_Left = SProxy SProxy "left"
62+
_Right = SProxy SProxy "right"

0 commit comments

Comments
 (0)