Skip to content

Commit 0807876

Browse files
authored
Merge pull request #51 from garyb/optional-record-sugar
Add `Optional` helper for record sugar
2 parents 023f599 + d124f5a commit 0807876

File tree

3 files changed

+99
-22
lines changed

3 files changed

+99
-22
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ decodeStringArray ∷ J.Json → Either CA.JsonDecodeError (Array String)
3535
decodeStringArray = CA.decode codec
3636
```
3737

38-
To parse a serialized `String` into a `J.Json` structure use the [`Parser.jsonParser`](https://pursuit.purescript.org/packages/purescript-argonaut-core/5.1.0/docs/Data.Argonaut.Parser).
38+
To parse a serialized `String` into a `J.Json` structure use the [`Parser.jsonParser`](https://pursuit.purescript.org/packages/purescript-argonaut-core/docs/Data.Argonaut.Parser).
3939

40-
To /"stringify"/ (serialize) your `Array String` to a serialized JSON `String` we would use the [`stringify`](https://pursuit.purescript.org/packages/purescript-argonaut-core/5.1.0/docs/Data.Argonaut.Core#v:stringify) like so:
40+
To "stringify" (serialize) your `Array String` to a serialized JSON `String` we would use the [`stringify`](https://pursuit.purescript.org/packages/purescript-argonaut-core/docs/Data.Argonaut.Core#v:stringify) like so:
4141

4242
```purescript
4343
import Control.Category ((>>>))
@@ -110,6 +110,33 @@ codec =
110110
})
111111
```
112112

113+
#### Optional properties
114+
115+
Objects with optional properties can be defined using the [`CAR.optional`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut.Record#v:optional):
116+
117+
```purescript
118+
type Person =
119+
{ name ∷ String
120+
, age ∷ Int
121+
, active ∷ Boolean
122+
, email ∷ Maybe String
123+
}
124+
125+
codec ∷ CA.JsonCodec Person
126+
codec =
127+
CA.object "Person"
128+
(CAR.record
129+
{ name: CA.string
130+
, age: CA.int
131+
, active: CA.boolean
132+
, email: CAR.optional CA.string
133+
})
134+
```
135+
136+
If the value being decoded has no `email` field, the resulting `Person` will have `Nothing` for `email` now rather than failing to decode. When encoding, if an optional value is `Nothing`, the field will be omitted from the resulting JSON object.
137+
138+
This combinator only deals with entirely missing properties, so values like `null` will still need to be handled explicitly.
139+
113140
### Sum types and variants
114141

115142
This library comes with codec support for [`purescript-variant`](https://github.com/natefaubion/purescript-variant) out of the box and codecs for sums are often based on the variant codec.

src/Data/Codec/Argonaut/Record.purs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
module Data.Codec.Argonaut.Record where
22

33
import Data.Codec.Argonaut as CA
4+
import Data.Maybe (Maybe)
45
import Data.Symbol (class IsSymbol)
56
import Prim.Row as R
67
import Prim.RowList as RL
78
import Record as Rec
8-
import Type.Equality as TE
9+
import Safe.Coerce (coerce)
910
import Type.Proxy (Proxy(..))
1011
import Unsafe.Coerce (unsafeCoerce)
1112

@@ -38,6 +39,19 @@ record
3839
CA.JPropCodec (Record ro)
3940
record = rowListCodec (Proxy Proxy rl)
4041

42+
-- | Used to wrap codec values provided in `record` to indicate the field is optional.
43+
-- |
44+
-- | This will only decode the property as `Nothing` if the field does not exist
45+
-- | in the object - having a values such as `null` assigned will need handling
46+
-- | separately.
47+
-- |
48+
-- | The property will be omitted when encoding and the value is `Nothing`.
49+
newtype Optional a = Optional (CA.JsonCodec a)
50+
51+
-- | A lowercase alias for `Optional`, provided for stylistic reasons only.
52+
optional a. CA.JsonCodec a Optional a
53+
optional = Optional
54+
4155
-- | The class used to enable the building of `Record` codecs by providing a
4256
-- | record of codecs.
4357
class RowListCodec (rlRL.RowList Type) (riRow Type) (roRow Type) | rl ri ro where
@@ -46,19 +60,34 @@ class RowListCodec (rl ∷ RL.RowList Type) (ri ∷ Row Type) (ro ∷ Row Type)
4660
instance rowListCodecNilRowListCodec RL.Nil () () where
4761
rowListCodec _ _ = CA.record
4862

49-
instance rowListCodecCons
63+
instance rowListCodecConsOptional
64+
( RowListCodec rs ri' ro'
65+
, R.Cons sym (Optional a) ri' ri
66+
, R.Cons sym (Maybe a) ro' ro
67+
, IsSymbol sym
68+
)
69+
RowListCodec (RL.Cons sym (Optional a) rs) ri ro where
70+
rowListCodec _ codecs =
71+
CA.recordPropOptional (Proxy Proxy sym) codec tail
72+
where
73+
codec CA.JsonCodec a
74+
codec = coerce (Rec.get (Proxy Proxy sym) codecs Optional a)
75+
76+
tail CA.JPropCodec (Record ro')
77+
tail = rowListCodec (Proxy Proxy rs) ((unsafeCoerce Record ri Record ri') codecs)
78+
79+
else instance rowListCodecCons
5080
( RowListCodec rs ri' ro'
5181
, R.Cons sym (CA.JsonCodec a) ri' ri
5282
, R.Cons sym a ro' ro
5383
, IsSymbol sym
54-
, TE.TypeEquals co (CA.JsonCodec a)
5584
)
56-
RowListCodec (RL.Cons sym co rs) ri ro where
85+
RowListCodec (RL.Cons sym (CA.JsonCodec a) rs) ri ro where
5786
rowListCodec _ codecs =
5887
CA.recordProp (Proxy Proxy sym) codec tail
5988
where
6089
codec CA.JsonCodec a
61-
codec = TE.from (Rec.get (Proxy Proxy sym) codecs)
90+
codec = Rec.get (Proxy Proxy sym) codecs
6291

6392
tail CA.JPropCodec (Record ro')
6493
tail = rowListCodec (Proxy Proxy rs) ((unsafeCoerce Record ri Record ri') codecs)

test/Test/Record.purs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import Prelude
55
import Control.Monad.Gen as Gen
66
import Control.Monad.Gen.Common as GenC
77
import Data.Argonaut.Core (stringify)
8-
import Data.Codec.Argonaut.Common as JA
9-
import Data.Codec.Argonaut.Record as JAR
10-
import Data.Maybe (Maybe(..))
8+
import Data.Argonaut.Core as Json
9+
import Data.Codec.Argonaut.Common as CA
10+
import Data.Codec.Argonaut.Record as CAR
11+
import Data.Maybe (Maybe(..), fromJust)
1112
import Data.Newtype (class Newtype, unwrap, wrap)
1213
import Data.Profunctor (dimap)
1314
import Data.String.Gen (genAsciiString)
1415
import Effect (Effect)
1516
import Effect.Console (log)
16-
import Test.QuickCheck (quickCheck)
17+
import Foreign.Object as Object
18+
import Partial.Unsafe (unsafePartial)
19+
import Test.QuickCheck (assertEquals, quickCheck, quickCheckGen)
1720
import Test.QuickCheck.Gen (Gen)
1821
import Test.Util (genInt, propCodec)
1922

@@ -26,14 +29,15 @@ type OuterR =
2629
type InnerR =
2730
{ n Int
2831
, m Boolean
32+
, o Maybe Boolean
2933
}
3034

3135
newtype Outer = Outer OuterR
3236

3337
derive instance newtypeOuterNewtype Outer _
3438

3539
instance showOuterShow Outer where
36-
show (Outer r) = "Outer " <> stringify (JA.encode outerCodec r)
40+
show (Outer r) = "Outer " <> stringify (CA.encode outerCodec r)
3741

3842
instance eqOuterEq Outer where
3943
eq (Outer o1) (Outer o2) =
@@ -44,19 +48,20 @@ instance eqOuter ∷ Eq Outer where
4448
Just i1, Just i2 → i1.n == i2.n && i1.m == i2.m
4549
_, _ → false
4650

47-
outerCodec JA.JsonCodec OuterR
51+
outerCodec CA.JsonCodec OuterR
4852
outerCodec =
49-
JA.object "Outer" $ JAR.record
50-
{ a: JA.int
51-
, b: JA.string
52-
, c: JA.maybe innerCodec
53+
CA.object "Outer" $ CAR.record
54+
{ a: CA.int
55+
, b: CA.string
56+
, c: CA.maybe innerCodec
5357
}
5458

55-
innerCodec JA.JsonCodec InnerR
59+
innerCodec CA.JsonCodec InnerR
5660
innerCodec =
57-
JA.object "Inner" $ JAR.record
58-
{ n: JA.int
59-
, m: JA.boolean
61+
CA.object "Inner" $ CAR.record
62+
{ n: CA.int
63+
, m: CA.boolean
64+
, o: CAR.optional CA.boolean
6065
}
6166

6267
genOuter Gen OuterR
@@ -70,9 +75,25 @@ genInner ∷ Gen InnerR
7075
genInner = do
7176
n ← genInt
7277
m ← Gen.chooseBool
73-
pure { n, m }
78+
o ← GenC.genMaybe Gen.chooseBool
79+
pure { n, m, o }
7480

7581
main Effect Unit
7682
main = do
7783
log "Checking record codec"
7884
quickCheck $ propCodec (Outer <$> genOuter) (dimap unwrap wrap outerCodec)
85+
86+
log "Check optional Nothing is missing from json"
87+
quickCheckGen do
88+
v ← genInner
89+
let obj = unsafePartial $ fromJust $ Json.toObject $ CA.encode innerCodec (v { o = Nothing })
90+
pure $ assertEquals [ "m", "n" ] (Object.keys obj)
91+
92+
log "Check optional Just is present in the json"
93+
quickCheckGen do
94+
b ← Gen.chooseBool
95+
v ← genInner
96+
let obj = unsafePartial $ fromJust $ Json.toObject $ CA.encode innerCodec (v { o = Just b })
97+
pure $ assertEquals [ "m", "n", "o" ] (Object.keys obj)
98+
99+
pure unit

0 commit comments

Comments
 (0)