-
Notifications
You must be signed in to change notification settings - Fork 2
/
Generics.hs
289 lines (248 loc) · 9.41 KB
/
Generics.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
-- | "Generics.SOP" derivation for record types (i.e. products).
module Futurice.Generics (
-- * QuickCheck
QC.Arbitrary(..),
sopArbitrary,
sopShrink,
-- * Cassava
Csv.ToNamedRecord(..),
sopToNamedRecord,
Csv.DefaultOrdered(..),
sopHeaderOrder,
Csv.FromRecord(..),
sopParseRecord,
-- * Aeson
Aeson.ToJSON(..),
Aeson.FromJSON(..),
sopToJSON,
sopParseJSON,
-- * Swagger
Swagger.ToSchema(..),
sopDeclareNamedSchema,
) where
import Futurice.Prelude hiding (Generic, from)
import Prelude ()
import Data.ByteString (ByteString)
import Data.Char (toLower)
import Generics.SOP
import Generics.SOP.Lens
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
import qualified Data.Csv as Csv
import qualified Data.Swagger as Swagger
import qualified Data.Swagger.Declare as Swagger
import qualified Data.Vector as V
import qualified GHC.Exts as Exts
import qualified Test.QuickCheck as QC
-------------------------------------------------------------------------------
-- QuickCheck
-------------------------------------------------------------------------------
-- | Works for non-recursive structures
sopArbitrary
:: (Generic a, All2 QC.Arbitrary (Code a))
=> QC.Gen a
sopArbitrary = to <$> QC.oneof (map hsequence $ apInjs_POP popArbitrary)
where
popArbitrary :: All2 QC.Arbitrary xs => POP QC.Gen xs
popArbitrary = hcpure (Proxy :: Proxy QC.Arbitrary) QC.arbitrary
-- | Works for non-recursive structures
sopShrink
:: forall a. (Generic a, All2 QC.Arbitrary (Code a))
=> a -> [a]
sopShrink = map (to . SOP) . sopShrink' . unSOP . from
where
sopShrink'
:: forall yss. All2 QC.Arbitrary yss
=> NS (NP I) yss -> [NS (NP I) yss]
sopShrink' (Z x) = map Z (sopShrink'' x)
sopShrink' (S x) = map S (sopShrink' x)
sopShrink'' :: forall ys. All QC.Arbitrary ys => NP I ys -> [NP I ys]
sopShrink''
= hsequence
. hcmap (Proxy :: Proxy QC.Arbitrary) (QC.shrink . unI)
-------------------------------------------------------------------------------
-- Cassava
-------------------------------------------------------------------------------
sopToNamedRecord
:: forall a xs.
(Generic a, HasDatatypeInfo a, All Csv.ToField xs, Code a ~ '[xs])
=> a
-> Csv.NamedRecord
sopToNamedRecord
= Csv.namedRecord
. sopToNamedRecord' fieldInfos
. (^. unsop . unSingletonS)
. from
where
fieldInfos = datatypeInfo (Proxy :: Proxy a) ^.
constructorInfo . unSingletonP . fieldInfo
sopToNamedRecord'
:: All Csv.ToField xs => NP FieldInfo xs -> NP I xs
-> [(ByteString, ByteString)]
sopToNamedRecord' fs' xs' = go fs' xs'
where
prefix :: String
prefix = longestFieldInfoPrefix fs'
go :: All Csv.ToField xs => NP FieldInfo xs -> NP I xs -> [(ByteString, ByteString)]
go Nil Nil = []
go (FieldInfo f :* fs) (I x :* xs) =
Csv.namedField (fromString $ processFieldName prefix f) x : go fs xs
#if __GLASGOW_HASKELL__ < 800
go _ _ = error "sopToNamedRecord' go: impossible happened"
#endif
sopHeaderOrder
:: forall a xs.
(Generic a, HasDatatypeInfo a, Code a ~ '[xs])
=> a -- ^ Unused, only for compatibility with @cassava@'s 'headerOrder'
-> Csv.Header
sopHeaderOrder _ = V.fromList (sopHeaderOrder' fieldInfos)
where
fieldInfos = datatypeInfo (Proxy :: Proxy a) ^.
constructorInfo . unSingletonP . fieldInfo
sopHeaderOrder' :: SListI xs => NP FieldInfo xs -> [ByteString]
sopHeaderOrder' fs = hcollapse (hmap f fs)
where
prefix :: String
prefix = longestFieldInfoPrefix fs
f :: FieldInfo a -> K ByteString a
f (FieldInfo n) = K . fromString . processFieldName prefix $ n
sopParseRecord
:: forall a xs.
(Generic a, All Csv.FromField xs, Code a ~ '[xs])
=> Csv.Record
-> Csv.Parser a
sopParseRecord r
| length r == lenXs = to . SOP . Z <$> sopParseRecord' r
| otherwise = fail $ "Cannot match field of length " ++ show (length r) ++ " with record of " ++ show lenXs ++ " fields"
where
lenXs = lengthSList (Proxy :: Proxy xs)
sopParseRecord' :: forall xs. (All Csv.FromField xs) => Csv.Record -> Csv.Parser (NP I xs)
sopParseRecord' r = go (sList :: SList xs) 0
where
go :: All Csv.FromField ys => SList ys -> Int -> Csv.Parser (NP I ys)
go SNil _ = pure Nil
go SCons i = (\h t -> I h :* t) <$> r Csv..! i <*> go sList (i + 1)
-------------------------------------------------------------------------------
-- Aeson
-------------------------------------------------------------------------------
sopToJSON
:: forall a xs.
(Generic a, HasDatatypeInfo a, All Aeson.ToJSON xs, Code a ~ '[xs])
=> a
-> Aeson.Value
sopToJSON
= Aeson.object
. sopToJSON' fieldInfos
. (^. unsop . unSingletonS)
. from
where
fieldInfos = datatypeInfo (Proxy :: Proxy a) ^.
constructorInfo . unSingletonP . fieldInfo
sopToJSON'
:: All Aeson.ToJSON xs => NP FieldInfo xs -> NP I xs
-> [Aeson.Pair]
sopToJSON' fs' xs' = go fs' xs'
where
prefix :: String
prefix = longestFieldInfoPrefix fs'
go :: All Aeson.ToJSON xs => NP FieldInfo xs -> NP I xs -> [Aeson.Pair]
go Nil Nil = []
go (FieldInfo f :* fs) (I x :* xs) =
(fromString $ processFieldName prefix f) Aeson..= x : go fs xs
#if __GLASGOW_HASKELL__ < 800
go _ _ = error "sopToNamedRecord' go: impossible happened"
#endif
sopParseJSON
:: forall a xs.
(Generic a, HasDatatypeInfo a, All Aeson.FromJSON xs, Code a ~ '[xs])
=> Aeson.Value
-> Aeson.Parser a
sopParseJSON = Aeson.withObject tName $ \obj ->
to . SOP . Z <$> sopParseJSON' obj fieldInfos
where
dInfo = datatypeInfo (Proxy :: Proxy a)
tName = dInfo ^. datatypeName
fieldInfos = dInfo ^. constructorInfo . unSingletonP . fieldInfo
sopParseJSON'
:: All Aeson.FromJSON xs
=> Aeson.Object -> NP FieldInfo xs -> Aeson.Parser (NP I xs)
sopParseJSON' obj fs' = go fs'
where
prefix :: String
prefix = longestFieldInfoPrefix fs'
go :: All Aeson.FromJSON ys => NP FieldInfo ys -> Aeson.Parser (NP I ys)
go Nil = pure Nil
go (FieldInfo f :* fs) = (\h t -> I h :* t)
<$> obj Aeson..: (fromString $ processFieldName prefix f)
<*> go fs
-------------------------------------------------------------------------------
-- swagger
-------------------------------------------------------------------------------
type SwaggerM = Swagger.Declare (Swagger.Definitions Swagger.Schema)
type SwaggerPP = (Text, Swagger.Referenced Swagger.Schema)
sopDeclareNamedSchema
:: forall a xs proxy.
(HasDatatypeInfo a, All Swagger.ToSchema xs, Code a ~ '[xs])
=> proxy a
-> SwaggerM Swagger.NamedSchema
sopDeclareNamedSchema _ = do
props <- hsequenceK (hcmap (Proxy :: Proxy Swagger.ToSchema) prop fieldInfos) :: SwaggerM (NP (K SwaggerPP) xs)
pure $ Swagger.NamedSchema (Just $ name ^. packed) $ schema (Exts.fromList . hcollapse $ props)
where
name = datatypeInfo proxy ^. datatypeName
schema props = mempty
& Swagger.type_ .~ Swagger.SwaggerObject
& Swagger.properties .~ props
& Swagger.required .~ hcollapse (hmap req fieldInfos)
prefix :: String
prefix = longestFieldInfoPrefix fieldInfos
req :: forall y. FieldInfo y -> K Text y
req (FieldInfo n) = K $ processFieldName prefix n ^. packed
prop :: forall y. Swagger.ToSchema y => FieldInfo y -> K (SwaggerM SwaggerPP) y
prop (FieldInfo n) = K $ (,) n' <$> s
where
n' = processFieldName prefix n ^. packed
s = Swagger.declareSchemaRef (Proxy :: Proxy y)
fieldInfos :: NP FieldInfo xs
fieldInfos = datatypeInfo proxy ^. constructorInfo . unSingletonP . fieldInfo
proxy :: Proxy a
proxy = Proxy
-------------------------------------------------------------------------------
-- Utilities
-------------------------------------------------------------------------------
longestCommonPrefix :: Eq a => [a] -> [a] -> [a]
longestCommonPrefix [] _ = []
longestCommonPrefix _ [] = []
longestCommonPrefix (x:xs) (y:ys)
| x == y = x : longestCommonPrefix xs ys
| otherwise = []
longestFieldInfoPrefix :: NP FieldInfo xs -> String
longestFieldInfoPrefix Nil = ""
longestFieldInfoPrefix (FieldInfo _ :* Nil) = ""
longestFieldInfoPrefix (FieldInfo a :* FieldInfo b :* Nil) =
longestCommonPrefix a b
longestFieldInfoPrefix (FieldInfo a :* xs) =
longestCommonPrefix a (longestFieldInfoPrefix xs)
lowerFirst :: String -> String
lowerFirst [] = []
lowerFirst (c:cs) = toLower c : cs
processFieldName :: String -> String -> String
processFieldName pfx = lowerFirst . drop (length pfx)
-------------------------------------------------------------------------------
-- generics-sop-lens
-------------------------------------------------------------------------------
fieldInfo :: Lens' (ConstructorInfo xs) (NP FieldInfo xs)
fieldInfo = lens g s
where
g :: ConstructorInfo xs -> NP FieldInfo xs
g (Record _ fs) = fs
g _ = error "fieldInfo get: only record supported"
s :: ConstructorInfo xs -> NP FieldInfo xs -> ConstructorInfo xs
s (Record n _) fs = Record n fs
s _ _ = error "fieldInfo set: only record supported"