Skip to content

Commit e2153e7

Browse files
garybthomashoneyman
authored andcommitted
Add optional property combinator
1 parent 3297cce commit e2153e7

File tree

4 files changed

+130
-31
lines changed

4 files changed

+130
-31
lines changed

src/Data/Codec/Argonaut.purs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module Data.Codec.Argonaut
2222
, prop
2323
, record
2424
, recordProp
25+
, recordPropOptional
2526
, fix
2627
, prismaticCodec
2728
, module Exports
@@ -33,6 +34,7 @@ import Control.Monad.Reader (ReaderT(..), runReaderT)
3334
import Control.Monad.Writer (Writer, writer, mapWriter)
3435
import Data.Argonaut.Core as J
3536
import Data.Array as A
37+
import Data.Bifunctor (lmap)
3638
import Data.Bifunctor as BF
3739
import Data.Codec (BasicCodec, Codec, GCodec(..), basicCodec, bihoistGCodec, decode, encode)
3840
import Data.Codec (decode, encode, (~), (<~<), (>~>)) as Exports
@@ -282,6 +284,44 @@ recordProp p codecA codecR =
282284
unsafeSet key a = unsafeCoerce <<< FO.insert key a <<< unsafeCoerce
283285
unsafeGet String Record r' a
284286
unsafeGet s = unsafePartial fromJust <<< FO.lookup s <<< unsafeCoerce
287+
288+
-- | Used with `record` to define an optional field.
289+
-- |
290+
-- | This will only decode the property as `Nothing` if the field does not exist
291+
-- | in the object - having a values such as `null` assigned will need handling
292+
-- | separately.
293+
-- |
294+
-- | The property will be omitted when encoding and the value is `Nothing`.
295+
recordPropOptional
296+
p a r r'
297+
. IsSymbol p
298+
Row.Cons p (Maybe a) r r'
299+
Proxy p
300+
JsonCodec a
301+
JPropCodec (Record r)
302+
JPropCodec (Record r')
303+
recordPropOptional p codecA codecR =
304+
let key = reflectSymbol p in GCodec (dec' key) (enc' key)
305+
where
306+
dec' String ReaderT (FO.Object J.Json) (Either JsonDecodeError) (Record r')
307+
dec' key = ReaderT \obj → do
308+
r ← decode codecR obj
309+
a ← lmap (AtKey key) case FO.lookup key obj of
310+
Just val → Just <$> decode codecA val
311+
_ → Right Nothing
312+
pure $ unsafeSet key a r
313+
enc' String Star (Writer (L.List (Tuple String J.Json))) (Record r') (Record r')
314+
enc' key = Star \val → do
315+
let w = encode codecR (unsafeForget val)
316+
writer $ Tuple val case unsafeGet key val of
317+
Just a → Tuple key (encode codecA a) : w
318+
Nothing → w
319+
unsafeForget Record r' Record r
320+
unsafeForget = unsafeCoerce
321+
unsafeSet String Maybe a Record r Record r'
322+
unsafeSet key a = unsafeCoerce <<< FO.insert key a <<< unsafeCoerce
323+
unsafeGet String Record r' Maybe a
324+
unsafeGet s = unsafePartial fromJust <<< FO.lookup s <<< unsafeCoerce
285325

286326
jsonPrimCodec
287327
a

src/Data/Codec/Argonaut/Common.purs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module Data.Codec.Argonaut.Common
66
import Prelude hiding (map)
77

88
import Data.Array as A
9-
import Data.Codec.Argonaut (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, decode, encode, fix, index, indexedArray, int, jarray, jobject, json, null, number, object, printJsonDecodeError, prop, record, recordProp, string, (<~<), (~))
9+
import Data.Codec.Argonaut (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, decode, encode, fix, index, indexedArray, int, jarray, jobject, json, null, number, object, printJsonDecodeError, prop, record, recordProp, recordPropOptional, string, (<~<), (~))
1010
import Data.Codec.Argonaut.Sum (taggedSum)
1111
import Data.Either (Either(..))
1212
import Data.Functor as F

src/Data/Codec/Argonaut/Compat.purs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
module Data.Codec.Argonaut.Compat
33
( module Data.Codec.Argonaut.Compat
44
, module Data.Codec.Argonaut
5-
, module Common
5+
, module Common
66
) where
77

88
import Prelude
99

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, fix, index, indexedArray, int, jarray, jobject, json, null, number, object, printJsonDecodeError, prop, record, recordProp, string, (<~<), (~))
13+
import Data.Codec.Argonaut (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, decode, encode, fix, index, indexedArray, int, jarray, jobject, json, null, number, object, printJsonDecodeError, prop, record, recordProp, recordPropOptional, 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: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@ import Data.Argonaut.Core as J
88
import Data.Argonaut.Gen (genJson)
99
import Data.Char.Gen (genAsciiChar)
1010
import Data.Codec.Argonaut.Common ((~))
11-
import Data.Codec.Argonaut.Common as JA
11+
import Data.Codec.Argonaut.Common as CA
12+
import Data.Either (Either(..), either, note)
1213
import Data.Generic.Rep (class Generic)
1314
import Data.Maybe (Maybe(..))
15+
import Data.Int as Int
16+
import Data.Maybe (Maybe(..), maybe)
1417
import Data.Newtype (class Newtype, unwrap, wrap)
1518
import Data.Profunctor (dimap)
1619
import Data.Show.Generic (genericShow)
1720
import Data.String.Gen (genAsciiString)
1821
import Effect (Effect)
1922
import Effect.Console (log)
23+
import Foreign.Object as Object
2024
import Foreign.Object.Gen (genForeignObject)
21-
import Test.QuickCheck (Result, quickCheck)
25+
import Test.QuickCheck (Result(..), quickCheck)
2226
import Test.QuickCheck.Gen (Gen)
2327
import Test.Util (genInt, propCodec, propCodec', propCodec'')
2428
import Type.Proxy (Proxy(..))
@@ -43,8 +47,8 @@ main = do
4347
log "Checking Char codec"
4448
quickCheck propChar
4549

46-
log "Checking JArray codec"
47-
quickCheck propJArray
50+
log "Checking CArray codec"
51+
quickCheck propCArray
4852

4953
log "Checking JObject codec"
5054
quickCheck propJObject
@@ -55,32 +59,41 @@ main = do
5559
log "Checking record codec"
5660
quickCheck (propTestRecord codecRecord)
5761

62+
log "Checking record codec with optional field"
63+
quickCheck propTestRecordOptional
64+
65+
log "Checking record codec with optional field does include the field"
66+
quickCheck propPresentOptionalField
67+
68+
log "Checking record codec with optional field does omit the field entirely"
69+
quickCheck propMissingOptionalField
70+
5871
log "Checking fixed-point codec"
5972
quickCheck propFix
6073

6174
propNull Gen Result
62-
propNull = propCodec (pure unit) JA.null
75+
propNull = propCodec (pure unit) CA.null
6376

6477
propBoolean Gen Result
65-
propBoolean = propCodec Gen.chooseBool JA.boolean
78+
propBoolean = propCodec Gen.chooseBool CA.boolean
6679

6780
propNumber Gen Result
68-
propNumber = propCodec (Gen.chooseFloat (-100000.0) 100000.0) JA.number
81+
propNumber = propCodec (Gen.chooseFloat (-100000.0) 100000.0) CA.number
6982

7083
propInt Gen Result
71-
propInt = propCodec genInt JA.int
84+
propInt = propCodec genInt CA.int
7285

7386
propString Gen Result
74-
propString = propCodec genAsciiString JA.string
87+
propString = propCodec genAsciiString CA.string
7588

7689
propChar Gen Result
77-
propChar = propCodec genAsciiChar JA.char
90+
propChar = propCodec genAsciiChar CA.char
7891

79-
propJArray Gen Result
80-
propJArray = propCodec'' (show <<< map J.stringify) (Gen.unfoldable genJson) JA.jarray
92+
propCArray Gen Result
93+
propCArray = propCodec'' (show <<< map J.stringify) (Gen.unfoldable genJson) CA.jarray
8194

8295
propJObject Gen Result
83-
propJObject = propCodec'' (show <<< map J.stringify) (genForeignObject genAsciiString genJson) JA.jobject
96+
propJObject = propCodec'' (show <<< map J.stringify) (genForeignObject genAsciiString genJson) CA.jobject
8497

8598
type TestRecord = { tag String, x Int, y Boolean }
8699

@@ -91,27 +104,73 @@ genRecord =
91104
<*> genInt
92105
<*> Gen.chooseBool
93106

94-
codecObject JA.JsonCodec TestRecord
107+
codecObject CA.JsonCodec TestRecord
95108
codecObject =
96-
JA.object "Test Object" $
109+
CA.object "Test Object" $
97110
{ tag: _, x: _, y: _ }
98-
<$> _.tag ~ JA.prop "tag" JA.string
99-
<*> _.x ~ JA.prop "x" JA.int
100-
<*> _.y ~ JA.prop "y" JA.boolean
111+
<$> _.tag ~ CA.prop "tag" CA.string
112+
<*> _.x ~ CA.prop "x" CA.int
113+
<*> _.y ~ CA.prop "y" CA.boolean
101114

102-
codecRecord JA.JsonCodec TestRecord
115+
codecRecord CA.JsonCodec TestRecord
103116
codecRecord =
104-
JA.object "Test Record" $ JA.record
105-
# JA.recordProp (Proxy Proxy "tag") JA.string
106-
# JA.recordProp (Proxy Proxy "x") JA.int
107-
# JA.recordProp (Proxy Proxy "y") JA.boolean
108-
109-
propTestRecord JA.JsonCodec TestRecord Gen Result
117+
CA.object "Test Record" $ CA.record
118+
# CA.recordProp (Proxy Proxy "tag") CA.string
119+
# CA.recordProp (Proxy Proxy "x") CA.int
120+
# CA.recordProp (Proxy Proxy "y") CA.boolean
121+
122+
propTestRecord CA.JsonCodec TestRecord Gen Result
110123
propTestRecord = propCodec' checkEq print genRecord
111124
where
112125
checkEq r1 r2 = r1.tag == r2.tag && r1.x == r2.x && r1.y == r2.y
113126
print { tag, x, y } =
114127
"{ tag: " <> show tag <> ", x: " <> show x <> ", y: " <> show y <> " }"
128+
129+
type TestRecordOptional = { tag String, x Maybe Int }
130+
131+
genRecordOptional Gen TestRecordOptional
132+
genRecordOptional =
133+
{ tag: _, x: _ }
134+
<$> genAsciiString
135+
<*> GenC.genMaybe genInt
136+
137+
codecRecordOptional CA.JsonCodec TestRecordOptional
138+
codecRecordOptional =
139+
CA.object "Test record with optional field" $ CA.record
140+
# CA.recordProp (Proxy Proxy "tag") CA.string
141+
# CA.recordPropOptional (Proxy Proxy "x") CA.int
142+
143+
propTestRecordOptional Gen Result
144+
propTestRecordOptional = propCodec' checkEq print genRecordOptional codecRecordOptional
145+
where
146+
checkEq r1 r2 = r1.tag == r2.tag && r1.x == r2.x
147+
print { tag, x } =
148+
case x of
149+
Just x' → "{ tag: " <> show tag <> ", x: " <> show x <> " }"
150+
Nothing"{ tag: " <> show tag <> " }"
151+
152+
propPresentOptionalField Gen Result
153+
propPresentOptionalField = do
154+
tag ← genAsciiString
155+
x ← genInt
156+
let value = { tag, x: Just x }
157+
let json = CA.encode codecRecordOptional value
158+
pure $ either Failed (pure Success) do
159+
obj ← note "Encoded JSON is not an object" $ J.toObject json
160+
prop ← note "Optional property unexpectedly missing in object" $ Object.lookup "x" obj
161+
n ← note "x value is not a plain number" $ J.toNumber prop
162+
if n == Int.toNumber x
163+
then pure unit
164+
else Left "x value is wrong"
165+
166+
propMissingOptionalField Gen Result
167+
propMissingOptionalField = do
168+
tag ← genAsciiString
169+
let value = { tag, x: Nothing }
170+
let json = CA.encode codecRecordOptional value
171+
pure $ either Failed (pure Success) do
172+
obj ← note "Encoded JSON is not an object" $ J.toObject json
173+
maybe (Right Success) (\_ → Left "Optional property unexpectedly appeared in object") $ Object.lookup "x" obj
115174

116175
newtype FixTest = FixTest (Maybe FixTest)
117176

@@ -126,9 +185,9 @@ genFixTest = Gen.sized \n →
126185
then pure $ FixTest Nothing
127186
else FixTest <$> Gen.resize (_ - 1) (GenC.genMaybe genFixTest)
128187

129-
codecFixTest JA.JsonCodec FixTest
130-
codecFixTest = JA.fix \codec →
131-
dimap unwrap wrap (JA.maybe codec)
188+
codecFixTest CA.JsonCodec FixTest
189+
codecFixTest = CA.fix \codec →
190+
dimap unwrap wrap (CA.maybe codec)
132191

133192
propFix Gen Result
134193
propFix = propCodec genFixTest codecFixTest

0 commit comments

Comments
 (0)