Skip to content

Commit c093363

Browse files
stevladimirandreasabel
authored andcommitted
[ #206 ] Deriving FromField/ToField instances
Works only for the following representations: * Single nullary constructor By default encodes constructor name * Sum with nullary or unary constructors Encoding is similar to 'UntaggedValue' encoding from 'aeson'
1 parent 87c33fd commit c093363

File tree

3 files changed

+156
-1
lines changed

3 files changed

+156
-1
lines changed

src/Data/Csv.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,15 @@ module Data.Csv
100100
, FromField(..)
101101
, ToField(..)
102102

103-
-- ** 'Generic' record conversion
103+
-- ** 'Generic' type conversion
104104
-- $genericconversion
105105
, genericParseRecord
106106
, genericToRecord
107107
, genericParseNamedRecord
108108
, genericToNamedRecord
109109
, genericHeaderOrder
110+
, genericParseField
111+
, genericToField
110112

111113
-- *** 'Generic' type conversion options
112114
, Options

src/Data/Csv/Conversion.hs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ module Data.Csv.Conversion
4444
, genericParseNamedRecord
4545
, genericToNamedRecord
4646
, genericHeaderOrder
47+
, genericParseField
48+
, genericToField
4749

4850
-- *** Generic type conversion options
4951
, Options
@@ -760,6 +762,12 @@ parseBoth (k, v) = (,) <$> parseField k <*> parseField v
760762
class FromField a where
761763
parseField :: Field -> Parser a
762764

765+
default parseField :: (Generic a, GFromField (Rep a)) => Field -> Parser a
766+
parseField = genericParseField defaultOptions
767+
768+
genericParseField :: (Generic a, GFromField (Rep a)) => Options -> Field -> Parser a
769+
genericParseField opts = fmap to . gParseField opts
770+
763771
-- | A type that can be converted to a single CSV field.
764772
--
765773
-- Example type and instance:
@@ -775,6 +783,12 @@ class FromField a where
775783
class ToField a where
776784
toField :: a -> Field
777785

786+
default toField :: (Generic a, GToField (Rep a)) => a -> Field
787+
toField = genericToField defaultOptions
788+
789+
genericToField :: (Generic a, GToField (Rep a)) => Options -> a -> Field
790+
genericToField opts = gToField opts . from
791+
778792
-- | 'Nothing' if the 'Field' is 'B.empty', 'Just' otherwise.
779793
instance FromField a => FromField (Maybe a) where
780794
parseField s
@@ -1370,6 +1384,77 @@ instance (ToField a, Selector s) => GToRecord (M1 S s (K1 i a)) (B.ByteString, B
13701384
where
13711385
name = T.encodeUtf8 (T.pack (fieldLabelModifier opts (selName m)))
13721386

1387+
class GFromField (f :: k -> *) where
1388+
gParseField :: Options -> Field -> Parser (f p)
1389+
1390+
-- Type with single nullary constructor
1391+
instance (Constructor c) => GFromField (D1 meta (C1 c U1)) where
1392+
gParseField opts = fmap M1 . gParseField' opts
1393+
1394+
-- Sum type with nullary or unary constructors
1395+
instance (Datatype t, GFromField' c1, GFromField' c2) => GFromField (D1 t (c1 :+: c2)) where
1396+
gParseField opts field = fmap M1 $
1397+
(L1 <$> gParseField' opts field)
1398+
<|> (R1 <$> gParseField' opts field)
1399+
<|> fail errMsg
1400+
where
1401+
errMsg =
1402+
"Can't parse " <> datatypeName (Proxy :: Proxy t d f) <> " from " <> show field
1403+
1404+
class GToField (f :: k -> *) where
1405+
gToField :: Options -> f p -> Field
1406+
1407+
-- Type with single nullary constructor
1408+
instance (Constructor c) => GToField (D1 meta (C1 c U1)) where
1409+
gToField opts = gToField' opts . unM1
1410+
1411+
-- Sum type with nullary or unary constructors
1412+
instance (GToField' c1, GToField' c2) => GToField (D1 t (c1 :+: c2)) where
1413+
gToField opts (M1 (L1 val)) = gToField' opts val
1414+
gToField opts (M1 (R1 val)) = gToField' opts val
1415+
1416+
-- Helper classes for FromField/ToField
1417+
1418+
class GFromField' (f :: k -> *) where
1419+
gParseField' :: Options -> Field -> Parser (f p)
1420+
1421+
-- Nullary constructor
1422+
instance (Constructor c) => GFromField' (C1 c U1) where
1423+
gParseField' opts field = do
1424+
if field == expected then pure val else fail $ "Expected " <> show expected
1425+
where
1426+
expected = encodeConstructor opts val
1427+
val :: C1 c U1 p
1428+
val = M1 U1
1429+
1430+
-- Unary constructor
1431+
instance (FromField a) => GFromField' (C1 c (S1 meta (K1 i a))) where
1432+
gParseField' _ = fmap (M1 . M1 . K1) . parseField
1433+
1434+
-- Sum
1435+
instance (GFromField' c1, GFromField' c2) => GFromField' (c1 :+: c2) where
1436+
gParseField' opts field =
1437+
fmap L1 (gParseField' opts field) <|> fmap R1 (gParseField' opts field)
1438+
1439+
class GToField' (f :: k -> *) where
1440+
gToField' :: Options -> f p -> Field
1441+
1442+
-- Nullary constructor
1443+
instance (Constructor c) => GToField' (C1 c U1) where
1444+
gToField' = encodeConstructor
1445+
1446+
-- Unary constructor
1447+
instance (ToField a) => GToField' (C1 c (S1 meta (K1 i a))) where
1448+
gToField' _ = toField . unK1 . unM1 . unM1
1449+
1450+
-- Sum
1451+
instance (GToField' c1, GToField' c2) => GToField' (c1 :+: c2) where
1452+
gToField' opts (L1 val) = gToField' opts val
1453+
gToField' opts (R1 val) = gToField' opts val
1454+
1455+
encodeConstructor :: (Constructor c) => Options -> C1 c f p -> B.ByteString
1456+
encodeConstructor opts = T.encodeUtf8 . T.pack . fieldLabelModifier opts . conName
1457+
13731458
-- We statically fail on sum types and product types without selectors
13741459
-- (field names).
13751460

tests/UnitTests.hs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import Control.Applicative (Const)
1414
import qualified Data.ByteString as B
1515
import qualified Data.ByteString.Lazy as BL
1616
import qualified Data.ByteString.Lazy.Char8 as BL8
17+
import Data.Char (toLower)
1718
import qualified Data.HashMap.Strict as HM
1819
import Data.Int
20+
import qualified Data.List as L
1921
import Data.Scientific (Scientific)
2022
import qualified Data.Text as T
2123
import qualified Data.Text.Lazy as LT
@@ -448,6 +450,71 @@ instance DefaultOrdered SampleType where
448450
instance Arbitrary SampleType where
449451
arbitrary = SampleType <$> arbitrary <*> arbitrary <*> arbitrary
450452

453+
------------------------------------------------------------------------
454+
-- Generic ToField/FromField tests
455+
456+
data Foo = Foo
457+
deriving (Eq, Generic, Show)
458+
459+
instance FromField Foo
460+
instance ToField Foo
461+
462+
-- -- Should not compile
463+
--
464+
-- -- Newtype
465+
-- newtype Foo1 = Foo1 Int deriving (Eq, Generic, Show)
466+
-- instance FromField Foo1
467+
-- instance ToField Foo1
468+
-- newtype FooRec1 = FooRec1 { unFooRec1 :: Int } deriving (Eq, Generic, Show)
469+
-- instance FromField FooRec1
470+
-- instance ToField FooRec1
471+
-- newtype FooRecF1 a = FooRecF1 { unFooRecF1 :: a } deriving (Eq, Generic, Show)
472+
-- instance (FromField a) => FromField (FooRecF1 a)
473+
-- instance (ToField a) => ToField (FooRecF1 a)
474+
-- -- Product
475+
-- data Foo2 = Foo2 Char Int deriving (Eq, Generic, Show)
476+
-- instance FromField Foo2
477+
-- instance ToField Foo2
478+
479+
data Bar = BarN1 | BarU Int | BarN2
480+
deriving (Eq, Generic, Show)
481+
482+
instance FromField Bar
483+
instance ToField Bar
484+
instance Arbitrary Bar where
485+
arbitrary = frequency [(1, pure BarN1), (3, BarU <$> arbitrary), (1, pure BarN2)]
486+
487+
data BazEnum = BazOne | BazTwo | BazThree
488+
deriving (Bounded, Enum, Eq, Generic, Show)
489+
490+
instance FromField BazEnum where
491+
parseField = genericParseField bazOptions
492+
instance ToField BazEnum where
493+
toField = genericToField bazOptions
494+
instance Arbitrary BazEnum where
495+
arbitrary = elements [minBound..maxBound]
496+
497+
bazOptions :: Options
498+
bazOptions = defaultOptions { fieldLabelModifier = go }
499+
where go = maybe (error "No prefix Baz") (map toLower) . L.stripPrefix "Baz"
500+
501+
genericFieldTests :: [TF.Test]
502+
genericFieldTests =
503+
[ testGroup "nullary constructor"
504+
[ testCase "encoding" $ toField Foo @?= "Foo"
505+
, testCase "decoding" $ runParser (parseField "Foo") @?= Right Foo ]
506+
, testCase "decoding failure" $ runParser (parseField "foo")
507+
@?= (Left "Expected \"Foo\"" :: Either String Foo)
508+
, testProperty "sum type roundtrip" (roundtripProp :: Bar -> Bool)
509+
, testGroup "constructor modifier"
510+
[ testCase "encoding" $ toField BazOne @?= "one"
511+
, testCase "decoding" $ runParser (parseField "two") @?= Right BazTwo
512+
, testProperty "roundtrip" (roundtripProp :: BazEnum -> Bool) ]
513+
]
514+
where
515+
roundtripProp :: (Eq a, FromField a, ToField a) => a -> Bool
516+
roundtripProp x = runParser (parseField $ toField x) == Right x
517+
451518
------------------------------------------------------------------------
452519
-- Test harness
453520

@@ -458,6 +525,7 @@ allTests = [ testGroup "positional" positionalTests
458525
, testGroup "custom-options" customOptionsTests
459526
, testGroup "instances" instanceTests
460527
, testGroup "generic-conversions" genericConversionTests
528+
, testGroup "generic-field-conversions" genericFieldTests
461529
]
462530

463531
main :: IO ()

0 commit comments

Comments
 (0)