Skip to content

Commit 368fc25

Browse files
authored
Merge pull request #65 from m-bock/sum-flat
`sumFlat` - Encodes sum types with single record fields in one object
2 parents 9dab273 + f336ea3 commit 368fc25

File tree

3 files changed

+225
-16
lines changed

3 files changed

+225
-16
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"pulp": "^16.0.0",
1010
"purescript": "^0.15.0",
1111
"purescript-psa": "^0.8.2",
12-
"purs-tidy": "^0.9.2",
12+
"purs-tidy": "^0.11.0",
1313
"rimraf": "^3.0.0"
1414
}
1515
}

src/Data/Codec/Argonaut/Sum.purs

Lines changed: 146 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
module Data.Codec.Argonaut.Sum
22
( Encoding(..)
3+
, FlatEncoding
34
, class GCases
45
, class GFields
6+
, class GFlatCases
57
, defaultEncoding
8+
, defaultFlatEncoding
69
, enumSum
710
, gCasesDecode
811
, gCasesEncode
912
, gFieldsDecode
1013
, gFieldsEncode
14+
, gFlatCasesDecode
15+
, gFlatCasesEncode
1116
, sum
17+
, sumFlat
18+
, sumFlatWith
1219
, sumWith
1320
, taggedSum
14-
) where
21+
)
22+
where
1523

1624
import Prelude
1725

@@ -23,8 +31,9 @@ import Data.Array as Array
2331
import Data.Bifunctor (lmap)
2432
import Data.Codec (codec', encode)
2533
import Data.Codec as Codec
26-
import Data.Codec.Argonaut (JsonCodec, JsonDecodeError(..), jobject)
34+
import Data.Codec.Argonaut (JPropCodec, JsonCodec, JsonDecodeError(..), jobject)
2735
import Data.Codec.Argonaut as CA
36+
import Data.Codec.Argonaut.Record as CAR
2837
import Data.Either (Either(..), note)
2938
import Data.Generic.Rep (class Generic, Argument(..), Constructor(..), NoArguments(..), Product(..), Sum(..), from, to)
3039
import Data.Maybe (Maybe(..), maybe)
@@ -206,8 +215,8 @@ else instance gCasesConstructorManyArgs ∷
206215
instance gCasesSum
207216
( GCases r1 (Constructor name lhs)
208217
, GCases r2 rhs
209-
, Row.Cons name codecs1 () r1
210-
, Row.Cons name codecs1 r2 r
218+
, Row.Cons name codec () r1
219+
, Row.Cons name codec r2 r
211220
, Row.Union r1 r2 r
212221
, Row.Lacks name r2
213222
, IsSymbol name
@@ -216,8 +225,8 @@ instance gCasesSum ∷
216225
gCasesEncode Encoding Record r Sum (Constructor name lhs) rhs Json
217226
gCasesEncode encoding r =
218227
let
219-
codecs1 = Record.get (Proxy @name) r codecs1
220-
r1 = Record.insert (Proxy @name) codecs1 {} Record r1
228+
codec = Record.get (Proxy @name) r codec
229+
r1 = Record.insert (Proxy @name) codec {} Record r1
221230
r2 = unsafeDelete (Proxy @name) r Record r2
222231
in
223232
case _ of
@@ -227,8 +236,8 @@ instance gCasesSum ∷
227236
gCasesDecode Encoding Record r Json Either JsonDecodeError (Sum (Constructor name lhs) rhs)
228237
gCasesDecode encoding r tagged = do
229238
let
230-
codecs1 = Record.get (Proxy @name) r codecs1
231-
r1 = Record.insert (Proxy @name) codecs1 {} Record r1
239+
codec = Record.get (Proxy @name) r codec
240+
r1 = Record.insert (Proxy @name) codec {} Record r1
232241
r2 = Record.delete (Proxy @name) r Record r2
233242
let
234243
lhs = gCasesDecode encoding r1 tagged _ (Constructor name lhs)
@@ -393,7 +402,135 @@ encodeSumCase encoding tag jsons =
393402
encode jobject $ Obj.fromFoldable $ catMaybes
394403
[ tagEntry, valEntry ]
395404

405+
type FlatEncoding (tagSymbol) =
406+
{ tag Proxy tag
407+
}
408+
409+
defaultFlatEncoding FlatEncoding "tag"
410+
defaultFlatEncoding = { tag: Proxy }
411+
412+
sumFlat r rep a. GFlatCases "tag" r rep Generic a rep String Record r JsonCodec a
413+
sumFlat = sumFlatWith defaultFlatEncoding
414+
415+
sumFlatWith @tag r rep a. GFlatCases tag r rep Generic a rep FlatEncoding tag -> String Record r JsonCodec a
416+
sumFlatWith _ name r =
417+
dimap from to $ codec' dec enc
418+
where
419+
dec = gFlatCasesDecode @tag r >>> (lmap $ Named name)
420+
enc = gFlatCasesEncode @tag r
421+
422+
class GFlatCasesSymbol Row Type Type Constraint
423+
class
424+
GFlatCases tag r rep
425+
where
426+
gFlatCasesEncode Record r rep Json
427+
gFlatCasesDecode Record r Json Either JsonDecodeError rep
428+
429+
instance gFlatCasesConstructorNoArg
430+
( Row.Cons name Unit () rc
431+
, Row.Cons tag String () rf
432+
, IsSymbol name
433+
, IsSymbol tag
434+
)
435+
GFlatCases tag rc (Constructor name NoArguments) where
436+
gFlatCasesEncode Record rc Constructor name NoArguments Json
437+
gFlatCasesEncode _ (Constructor NoArguments) =
438+
let
439+
name = reflectSymbol (Proxy @name) String
440+
propCodec = CAR.record {} JPropCodec {}
441+
propCodecWithTag = CA.recordProp (Proxy @tag) CA.string propCodec JPropCodec (Record rf)
442+
codecWithTag = CA.object ("case " <> name) propCodecWithTag JsonCodec (Record rf)
443+
rcWithTag = Record.insert (Proxy @tag) name {} Record rf
444+
in
445+
CA.encode codecWithTag rcWithTag
446+
447+
gFlatCasesDecode Record rc Json Either JsonDecodeError (Constructor name NoArguments)
448+
gFlatCasesDecode _ json = do
449+
let
450+
name = reflectSymbol (Proxy @name) String
451+
452+
propCodec = CAR.record {} JPropCodec {}
453+
propCodecWithTag = CA.recordProp (Proxy @tag) CA.string propCodec JPropCodec (Record rf)
454+
codecWithTag = CA.object ("case " <> name) propCodecWithTag JsonCodec (Record rf)
455+
r ← CA.decode codecWithTag json _ (Record rf)
456+
let actualTag = Record.get (Proxy @tag) r String
457+
458+
when (actualTag /= name)
459+
$ Left
460+
$ TypeMismatch ("Expecting tag `" <> name <> "`, got `" <> actualTag <> "`")
461+
462+
pure (Constructor NoArguments)
463+
464+
instance gFlatCasesConstructorSingleArg
465+
( Row.Cons name (JPropCodec (Record rf)) () rc
466+
, Row.Lacks tag rf
467+
, Row.Cons tag String rf rf'
468+
, IsSymbol name
469+
, IsSymbol tag
470+
)
471+
GFlatCases tag rc (Constructor name (Argument (Record rf))) where
472+
gFlatCasesEncode Record rc Constructor name (Argument (Record rf)) Json
473+
gFlatCasesEncode rc (Constructor (Argument rf)) =
474+
let
475+
name = reflectSymbol (Proxy @name) String
476+
propCodec = Record.get (Proxy @name) rc JPropCodec (Record rf)
477+
propCodecWithTag = CA.recordProp (Proxy @tag) CA.string propCodec JPropCodec (Record rf')
478+
codecWithTag = CA.object ("case " <> name) propCodecWithTag JsonCodec (Record rf')
479+
rcWithTag = Record.insert (Proxy @tag) name rf Record rf'
480+
in
481+
CA.encode codecWithTag rcWithTag
482+
483+
gFlatCasesDecode Record rc Json Either JsonDecodeError (Constructor name (Argument (Record rf)))
484+
gFlatCasesDecode rc json = do
485+
let
486+
name = reflectSymbol (Proxy @name) String
487+
propCodec = Record.get (Proxy @name) rc JPropCodec (Record rf)
488+
propCodecWithTag = CA.recordProp (Proxy @tag) CA.string propCodec JPropCodec (Record rf')
489+
codecWithTag = CA.object ("case " <> name) propCodecWithTag JsonCodec (Record rf')
490+
r ← CA.decode codecWithTag json _ (Record rf')
491+
492+
let actualTag = Record.get (Proxy @tag) r String
493+
when (actualTag /= name)
494+
$ Left
495+
$ TypeMismatch ("Expecting tag `" <> name <> "`, got `" <> actualTag <> "`")
496+
497+
let r' = Record.delete (Proxy @tag) r Record rf
498+
pure (Constructor (Argument r'))
499+
500+
instance gFlatCasesSum
501+
( GFlatCases tag r1 (Constructor name lhs)
502+
, GFlatCases tag r2 rhs
503+
, Row.Cons name codec () r1
504+
, Row.Cons name codec r2 r
505+
, Row.Union r1 r2 r
506+
, Row.Lacks name r2
507+
, IsSymbol name
508+
)
509+
GFlatCases tag r (Sum (Constructor name lhs) rhs) where
510+
gFlatCasesEncode Record r Sum (Constructor name lhs) rhs Json
511+
gFlatCasesEncode r =
512+
let
513+
codec = Record.get (Proxy @name) r codec
514+
r1 = Record.insert (Proxy @name) codec {} Record r1
515+
r2 = unsafeDelete (Proxy @name) r Record r2
516+
in
517+
case _ of
518+
Inl lhs → gFlatCasesEncode @tag r1 lhs
519+
Inr rhs → gFlatCasesEncode @tag r2 rhs
520+
521+
gFlatCasesDecode Record r Json Either JsonDecodeError (Sum (Constructor name lhs) rhs)
522+
gFlatCasesDecode r tagged = do
523+
let
524+
codec = Record.get (Proxy @name) r codec
525+
r1 = Record.insert (Proxy @name) codec {} Record r1
526+
r2 = Record.delete (Proxy @name) r Record r2
527+
let
528+
lhs = gFlatCasesDecode @tag r1 tagged _ (Constructor name lhs)
529+
rhs = gFlatCasesDecode @tag r2 tagged _ rhs
530+
(Inl <$> lhs) <|> (Inr <$> rhs)
531+
396532
-- | Same as `Record.delete` but deleting only happens at the type level
397533
-- | and the value is left untouched.
398534
unsafeDelete r1 r2 l a. IsSymbol l Row.Lacks l r1 Row.Cons l a r1 r2 Proxy l Record r2 Record r1
399-
unsafeDelete _ r = unsafeCoerce r
535+
unsafeDelete _ r = unsafeCoerce r
536+

test/Test/Sum.purs

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,22 @@ import Data.Bifunctor (lmap)
99
import Data.Codec (decode, encode)
1010
import Data.Codec.Argonaut (JsonCodec)
1111
import Data.Codec.Argonaut as C
12-
import Data.Codec.Argonaut.Sum (Encoding(..), defaultEncoding, sumWith)
12+
import Data.Codec.Argonaut.Record as CR
13+
import Data.Codec.Argonaut.Sum (Encoding(..), defaultEncoding, sumFlat, sumFlatWith, sumWith)
1314
import Data.Generic.Rep (class Generic)
1415
import Data.Show.Generic (genericShow)
1516
import Data.String as Str
1617
import Data.Tuple.Nested ((/\))
1718
import Effect (Effect)
1819
import Effect.Console (log)
1920
import Effect.Exception (error, throw)
20-
import Test.QuickCheck (quickCheck)
21+
import Test.QuickCheck (class Arbitrary, arbitrary, quickCheck)
2122
import Test.QuickCheck.Arbitrary (genericArbitrary)
22-
import Test.QuickCheck.Gen (Gen)
2323
import Test.Util (propCodec)
24+
import Type.Prelude (Proxy(..))
25+
import Type.Proxy (Proxy)
26+
27+
--------------------------------------------------------------------------------
2428

2529
data Sample
2630
= Foo
@@ -30,8 +34,8 @@ data Sample
3034
derive instance Generic Sample _
3135
derive instance Eq Sample
3236

33-
genMySum Gen Sample
34-
genMySum = genericArbitrary
37+
instance Arbitrary Sample where
38+
arbitrary = genericArbitrary
3539

3640
instance Show Sample where
3741
show = genericShow
@@ -43,6 +47,42 @@ codecSample encoding = sumWith encoding "Sample"
4347
, "Baz": C.boolean /\ C.string /\ C.int
4448
}
4549

50+
--------------------------------------------------------------------------------
51+
52+
data SampleFlat
53+
= FlatFoo
54+
| FlatBar { errors Int }
55+
| FlatBaz
56+
{ active Boolean
57+
, name String
58+
, pos { x Int, y Int }
59+
}
60+
61+
derive instance Generic SampleFlat _
62+
derive instance Eq SampleFlat
63+
64+
instance Arbitrary SampleFlat where
65+
arbitrary = genericArbitrary
66+
67+
instance Show SampleFlat where
68+
show = genericShow
69+
70+
codecSampleFlat JsonCodec SampleFlat
71+
codecSampleFlat = sumFlatWith { tag: Proxy @"tag" } "Sample"
72+
{ "FlatFoo": unit
73+
, "FlatBar": CR.record { errors: C.int }
74+
, "FlatBaz": CR.record
75+
{ active: C.boolean
76+
, name: C.string
77+
, pos: CR.object "Pos"
78+
{ x: C.int
79+
, y: C.int
80+
}
81+
}
82+
}
83+
84+
--------------------------------------------------------------------------------
85+
4686
check a. Show a Eq a JsonCodec a a String Effect Unit
4787
check codec val expectEncoded = do
4888
let encodedStr = stringifyWithIndent 2 $ encode codec val
@@ -313,5 +353,37 @@ main = do
313353
, "}"
314354
]
315355

316-
quickCheck (propCodec genMySum (codecSample defaultEncoding))
356+
quickCheck (propCodec arbitrary (codecSample defaultEncoding))
357+
358+
log "Check sum flat"
359+
do
360+
check codecSampleFlat FlatFoo
361+
$ Str.joinWith "\n"
362+
[ "{"
363+
, " \"tag\": \"FlatFoo\""
364+
, "}"
365+
]
366+
367+
check codecSampleFlat (FlatBar { errors: 42 })
368+
$ Str.joinWith "\n"
369+
[ "{"
370+
, " \"tag\": \"FlatBar\","
371+
, " \"errors\": 42"
372+
, "}"
373+
]
374+
375+
check codecSampleFlat (FlatBaz { active: true, name: "hello", pos: { x: 42, y: 42 } })
376+
$ Str.joinWith "\n"
377+
[ "{"
378+
, " \"tag\": \"FlatBaz\","
379+
, " \"active\": true,"
380+
, " \"name\": \"hello\","
381+
, " \"pos\": {"
382+
, " \"x\": 42,"
383+
, " \"y\": 42"
384+
, " }"
385+
, "}"
386+
]
387+
388+
quickCheck (propCodec arbitrary codecSampleFlat)
317389

0 commit comments

Comments
 (0)