Skip to content

Commit 023f599

Browse files
authored
Merge pull request #49 from thomashoneyman/optional-props
Add optional property combinator
2 parents 2c32bb4 + 34b3be8 commit 023f599

File tree

6 files changed

+132
-31
lines changed

6 files changed

+132
-31
lines changed

src/Data/Codec/Argonaut.purs

Lines changed: 43 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
@@ -290,6 +291,48 @@ recordProp p codecA codecR =
290291
unsafeGet String Record r' a
291292
unsafeGet s = unsafePartial fromJust <<< FO.lookup s <<< unsafeCoerce
292293

294+
-- | Used with `record` to define an optional field.
295+
-- |
296+
-- | This will only decode the property as `Nothing` if the field does not exist
297+
-- | in the object - having a values such as `null` assigned will need handling
298+
-- | separately.
299+
-- |
300+
-- | The property will be omitted when encoding and the value is `Nothing`.
301+
recordPropOptional
302+
p a r r'
303+
. IsSymbol p
304+
Row.Cons p (Maybe a) r r'
305+
Proxy p
306+
JsonCodec a
307+
JPropCodec (Record r)
308+
JPropCodec (Record r')
309+
recordPropOptional p codecA codecR =
310+
let key = reflectSymbol p in GCodec (dec' key) (enc' key)
311+
where
312+
dec' String ReaderT (FO.Object J.Json) (Either JsonDecodeError) (Record r')
313+
dec' key = ReaderT \obj → do
314+
r ← decode codecR obj
315+
a ← BF.lmap (AtKey key) case FO.lookup key obj of
316+
Just val → Just <$> decode codecA val
317+
_ → Right Nothing
318+
pure $ unsafeSet key a r
319+
320+
enc' String Star (Writer (L.List (Tuple String J.Json))) (Record r') (Record r')
321+
enc' key = Star \val → do
322+
let w = encode codecR (unsafeForget val)
323+
writer $ Tuple val case unsafeGet key val of
324+
Just a → Tuple key (encode codecA a) : w
325+
Nothing → w
326+
327+
unsafeForget Record r' Record r
328+
unsafeForget = unsafeCoerce
329+
330+
unsafeSet String Maybe a Record r Record r'
331+
unsafeSet key a = unsafeCoerce <<< FO.insert key a <<< unsafeCoerce
332+
333+
unsafeGet String Record r' Maybe a
334+
unsafeGet s = unsafePartial fromJust <<< FO.lookup s <<< unsafeCoerce
335+
293336
jsonPrimCodec
294337
a
295338
. String

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: 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, 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

src/Data/Codec/Argonaut/Generic.purs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ instance nullarySumCodecCtor ∷ IsSymbol name ⇒ NullarySumCodec (Constructor
4444
J.fromString $ reflectSymbol (Proxy Proxy name)
4545
nullarySumDecode name j = do
4646
tag ← note (CA.Named name (CA.TypeMismatch "String")) (J.toString j)
47-
if tag /= reflectSymbol (Proxy Proxy name) then
47+
if tag /= reflectSymbol (Proxy Proxy name) then
4848
Left (CA.Named name (CA.UnexpectedValue j))
4949
else
5050
Right (Constructor NoArguments)

src/Data/Codec/Argonaut/Variant.purs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ variantCase proxy eacodec (GCodec dec enc) = GCodec dec' enc'
103103
dec' = ReaderT \j → do
104104
obj ← decode jobject j
105105
tag ← decode (prop "tag" string) obj
106-
if tag == reflectSymbol proxy then
106+
if tag == reflectSymbol proxy then
107107
case eacodec of
108108
Left a → pure (inj proxy a)
109109
Right codec → do

test/Test/Prim.purs

Lines changed: 85 additions & 27 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)
14+
import Data.Int as Int
1315
import Data.Maybe (Maybe(..))
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,28 +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
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
108121

109-
propTestRecord JA.JsonCodec TestRecord Gen Result
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 <> " }"
115128

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 _ → "{ 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 then pure unit
163+
else Left "x value is wrong"
164+
165+
propMissingOptionalField Gen Result
166+
propMissingOptionalField = do
167+
tag ← genAsciiString
168+
let value = { tag, x: Nothing }
169+
let json = CA.encode codecRecordOptional value
170+
pure $ either Failed (pure Success) do
171+
obj ← note "Encoded JSON is not an object" $ J.toObject json
172+
maybe (Right Success) (\_ → Left "Optional property unexpectedly appeared in object") $ Object.lookup "x" obj
173+
116174
newtype FixTest = FixTest (Maybe FixTest)
117175

118176
derive instance newtypeFixTestNewtype FixTest _
@@ -128,9 +186,9 @@ genFixTest = Gen.sized \n →
128186
if n <= 1 then pure $ FixTest Nothing
129187
else FixTest <$> Gen.resize (_ - 1) (GenC.genMaybe genFixTest)
130188

131-
codecFixTest JA.JsonCodec FixTest
132-
codecFixTest = JA.fix \codec →
133-
dimap unwrap wrap (JA.maybe codec)
189+
codecFixTest CA.JsonCodec FixTest
190+
codecFixTest = CA.fix \codec →
191+
dimap unwrap wrap (CA.maybe codec)
134192

135193
propFix Gen Result
136194
propFix = propCodec genFixTest codecFixTest

0 commit comments

Comments
 (0)