Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Commit d764f04

Browse files
committed
[CBR-26] Re-introduce generic approach for serializing wallet errors
This is done through the combination of 'SOP.Generic', 'Generic' and 'HasDiagnostic' classe. Basically, what we miss from the old implementation are the names of all the partial reccord fields that were used to wrap the diagnostic part of the JSEnd response. These names are now provided by the 'HasDiagnostic' class that need to be implemented for each JSEnd error. From there, we provide two generic functions to convert a type 'a' to a JSON value and respectively, to parse any value to a type a. The main trick for the parsing is to leverage the 'Generic' instance and to construct a JSON value as Aeson would have serialized it in a first place. The serialization becomes quite straighforward once we can serialize the diagnostic which is done via an internal Generic class, working with a few type representations (not all, we don't need them). This approach is much more readable and maintainable than previously and will save us some errors due to typos or wrong formatting.
1 parent d1f6527 commit d764f04

File tree

6 files changed

+190
-232
lines changed

6 files changed

+190
-232
lines changed

wallet-new/src/Cardano/Wallet/API/Response.hs

+21-31
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,11 @@ import Prelude
2323
import Universum (Buildable, Exception, Text, decodeUtf8, toText,
2424
(<>))
2525

26-
import Control.Lens hiding ((.=))
27-
import Data.Aeson (FromJSON (..), ToJSON (..), eitherDecode, encode,
28-
object, pairs, withObject, (.:), (.=))
26+
import Control.Lens
27+
import Data.Aeson (FromJSON (..), ToJSON (..), eitherDecode, encode)
2928
import Data.Aeson.Encode.Pretty (encodePretty)
3029
import qualified Data.Aeson.Options as Serokell
3130
import Data.Aeson.TH
32-
import Data.Aeson.Types (Value (..))
3331
import qualified Data.Char as Char
3432
import Data.Swagger as S hiding (Example, example)
3533
import Data.Typeable
@@ -50,9 +48,12 @@ import Cardano.Wallet.API.Request.Pagination (Page (..),
5048
PerPage (..))
5149
import Cardano.Wallet.API.Request.Sort (SortOperations (..))
5250
import Cardano.Wallet.API.Response.Filter.IxSet as FilterBackend
53-
import Cardano.Wallet.API.Response.JSend (ResponseStatus (..))
51+
import Cardano.Wallet.API.Response.JSend (HasDiagnostic (..),
52+
ResponseStatus (..))
5453
import Cardano.Wallet.API.Response.Sort.IxSet as SortBackend
5554
import Cardano.Wallet.API.V1.Errors (ToServantError (..))
55+
import Cardano.Wallet.API.V1.Generic (jsendErrorGenericParseJSON,
56+
jsendErrorGenericToJSON)
5657
import Cardano.Wallet.API.V1.Swagger.Example (Example, example)
5758

5859
-- | Extra information associated with an HTTP response.
@@ -237,43 +238,32 @@ instance ToJSON a => MimeRender ValidJSON a where
237238

238239
newtype JSONValidationError
239240
= JSONValidationFailed Text
240-
deriving (Generic, Show, Eq)
241+
deriving (Eq, Show, Generic)
241242

242243
deriveGeneric ''JSONValidationError
243244

244245
instance ToJSON JSONValidationError where
245-
toEncoding (JSONValidationFailed weValidationError) = pairs $ mconcat
246-
[ "status" .= ErrorStatus
247-
, "diagnostic" .= object
248-
[ "validationError" .= weValidationError
249-
]
250-
, "message" .= String "JSONValidationFailed"
251-
]
246+
toJSON =
247+
jsendErrorGenericToJSON
252248

253249
instance FromJSON JSONValidationError where
254-
parseJSON = withObject "JSONValidationError" $ \o -> do
255-
message <- o .: "message"
256-
case message :: Text of
257-
"JSONValidationFailed" -> do
258-
diag <- o .: "diagnostic"
259-
err <- diag .: "validationError"
260-
pure $ JSONValidationFailed err
261-
_ ->
262-
fail "Incorrect JSON encoding for JSONValidationError"
250+
parseJSON =
251+
jsendErrorGenericParseJSON
263252

264253
instance Exception JSONValidationError
265254

266255
instance Arbitrary JSONValidationError where
267-
arbitrary = oneof
268-
[ pure $ JSONValidationFailed "JSON validation failed."
269-
]
256+
arbitrary =
257+
pure (JSONValidationFailed "JSON validation failed.")
270258

271259
instance Buildable JSONValidationError where
272-
build = \case
273-
JSONValidationFailed _ ->
274-
bprint "Couldn't decode a JSON input."
260+
build _ =
261+
bprint "Couldn't decode a JSON input."
262+
263+
instance HasDiagnostic JSONValidationError where
264+
getDiagnosticKey _ =
265+
"validationError"
275266

276267
instance ToServantError JSONValidationError where
277-
declareServantError = \case
278-
JSONValidationFailed _ ->
279-
err400
268+
declareServantError _ =
269+
err400

wallet-new/src/Cardano/Wallet/API/Response/JSend.hs

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ data ResponseStatus =
1717

1818
deriveJSON defaultOptions { Data.Aeson.TH.constructorTagModifier = map Char.toLower . reverse . drop 6 . reverse } ''ResponseStatus
1919

20+
class HasDiagnostic a where
21+
getDiagnosticKey :: a -> Text
22+
23+
noDiagnosticKey :: Text
24+
noDiagnosticKey =
25+
error "Contructor has declared no diagnostic key but apparently requires one! Have a look at HasDiagnostic instances!"
26+
2027
instance Arbitrary ResponseStatus where
2128
arbitrary = elements [minBound .. maxBound]
2229

Original file line numberDiff line numberDiff line change
@@ -1,43 +1,120 @@
11
{-# LANGUAGE PolyKinds #-}
22

33
module Cardano.Wallet.API.V1.Generic
4-
( gconsNames
5-
, gconsName
6-
) where
4+
( jsendErrorGenericToJSON
5+
, jsendErrorGenericParseJSON
6+
, gconsNames
7+
, gconsName
8+
) where
79

810
import Universum hiding (All, Generic)
911

12+
import Data.Aeson (GFromJSON, Object, ToJSON, Value (..), Zero,
13+
genericParseJSON, object, tagSingleConstructors,
14+
withObject, (.:), (.=))
15+
import Data.Aeson.Types (Parser)
1016
import Data.List ((!!))
11-
import Generics.SOP
17+
import GHC.Generics
18+
19+
import Cardano.Wallet.API.Response.JSend (HasDiagnostic (..),
20+
ResponseStatus (..))
21+
22+
import qualified Data.Aeson as Aeson
23+
import qualified Data.HashMap.Lazy as HM
24+
import qualified Generics.SOP as SOP
1225

1326
--
1427
-- Misc
1528
--
1629

30+
-- | Get the ADT constructor's name of the given value
31+
gconsName
32+
:: forall a. (SOP.Generic a, SOP.HasDatatypeInfo a)
33+
=> a -> Text
34+
gconsName a =
35+
gconsNames (Proxy @a) !! SOP.hindex (SOP.from a)
36+
1737
-- | Get all constructors names available of an ADT
1838
gconsNames
19-
:: forall a. (HasDatatypeInfo a, SListI (Code a))
39+
:: forall a. (SOP.HasDatatypeInfo a, SOP.SListI (SOP.Code a))
2040
=> Proxy a -> [Text]
2141
gconsNames =
22-
map toText . hcollapse . hliftA (K . constructorName) . gconsInfos
42+
map toText . SOP.hcollapse . SOP.hliftA (SOP.K . SOP.constructorName) . gconsInfos
2343

2444

25-
-- | Get the ADT constructor's name of the given value
26-
gconsName
27-
:: forall a. (Generic a, HasDatatypeInfo a)
28-
=> a -> Text
29-
gconsName a =
30-
gconsNames (Proxy @a) !! hindex (from a)
45+
--
46+
-- JSendError Encoding helper
47+
--
48+
jsendErrorGenericToJSON ::
49+
( GDiagnosticToJSON (Rep a)
50+
, HasDiagnostic a
51+
, Generic a
52+
, SOP.Generic a
53+
, SOP.HasDatatypeInfo a
54+
) => a -> Value
55+
jsendErrorGenericToJSON a = object
56+
[ "message" .= gconsName a
57+
, "status" .= ErrorStatus
58+
, "diagnostic" .= gDiagnosticToJSON (getDiagnosticKey a) (from a)
59+
]
60+
61+
jsendErrorGenericParseJSON ::
62+
( Generic a
63+
, GFromJSON Zero (Rep a)
64+
) => Value
65+
-> Parser a
66+
jsendErrorGenericParseJSON = withObject "JSEndError" $ \o -> do
67+
message <- o .: "message"
68+
diagnostic <- o .: "diagnostic" >>= parseDiagnostic
69+
genericParseJSON opts $ object
70+
[ "tag" .= String message
71+
, "contents" .= diagnostic
72+
]
73+
where
74+
opts :: Aeson.Options
75+
opts = Aeson.defaultOptions { tagSingleConstructors = True }
76+
77+
parseDiagnostic :: Object -> Parser Value
78+
parseDiagnostic hm =
79+
case HM.toList hm of
80+
[] -> pure (object mempty)
81+
[(_, value)] -> pure value
82+
_ -> fail "Invalid ToJSON encoding for JSEndError"
3183

3284

3385
--
3486
-- INTERNALS
3587
--
3688

3789
gconsInfos
38-
:: forall a. (HasDatatypeInfo a)
39-
=> Proxy a -> NP ConstructorInfo (Code a)
40-
gconsInfos pa = case datatypeInfo pa of
41-
Newtype _ _ conInfo -> conInfo :* Nil
42-
ADT _ _ consInfo -> consInfo
90+
:: forall a. (SOP.HasDatatypeInfo a)
91+
=> Proxy a
92+
-> SOP.NP SOP.ConstructorInfo (SOP.Code a)
93+
gconsInfos pa = case SOP.datatypeInfo pa of
94+
SOP.Newtype _ _ conInfo -> conInfo SOP.:* SOP.Nil
95+
SOP.ADT _ _ consInfo -> consInfo
96+
97+
98+
-- | This class helps us define generically errors JSON instances without
99+
-- relying on partial field in records.
100+
-- This is used to encode the diagnostic object of an error, as a singleton
101+
-- with field 'Text' whenever there's one.
102+
--
103+
-- NOTE: We haven't defined instances for everything because we do not want to
104+
-- suppport all kind of error structures, but only sums with a unary or
105+
-- nullary constructors.
106+
class GDiagnosticToJSON (f :: * -> *) where
107+
gDiagnosticToJSON :: Text -> f a -> Value
108+
109+
instance (GDiagnosticToJSON f) => GDiagnosticToJSON (M1 i c f) where
110+
gDiagnosticToJSON k (M1 f) = gDiagnosticToJSON k f
111+
112+
instance (GDiagnosticToJSON f, GDiagnosticToJSON g) => GDiagnosticToJSON (f :+: g) where
113+
gDiagnosticToJSON k (L1 f) = gDiagnosticToJSON k f
114+
gDiagnosticToJSON k (R1 g) = gDiagnosticToJSON k g
115+
116+
instance (ToJSON c) => GDiagnosticToJSON (K1 i c) where
117+
gDiagnosticToJSON k (K1 c) = object [ k .= c ]
43118

119+
instance GDiagnosticToJSON U1 where
120+
gDiagnosticToJSON _ _ = object mempty

wallet-new/src/Cardano/Wallet/API/V1/Migration/Types.hs

+16-25
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ import Universum hiding (elems)
1414

1515
import qualified Control.Lens as Lens
1616
import qualified Control.Monad.Catch as Catch
17-
import Data.Aeson (FromJSON (..), ToJSON (..), object, pairs,
18-
withObject, (.:), (.=))
19-
import Data.Aeson.Types (Value (..))
17+
import Data.Aeson (FromJSON (..), ToJSON (..))
2018
import Data.Map (elems)
2119
import Data.Time.Clock.POSIX (POSIXTime)
2220
import Data.Time.Units (fromMicroseconds, toMicroseconds)
@@ -27,10 +25,11 @@ import Generics.SOP.TH (deriveGeneric)
2725
import GHC.Generics (Generic)
2826
import Servant (err422)
2927
import Test.QuickCheck (Arbitrary (..))
30-
import Test.QuickCheck.Gen (oneof)
3128

32-
import Cardano.Wallet.API.Response.JSend (ResponseStatus (..))
29+
import Cardano.Wallet.API.Response.JSend (HasDiagnostic (..))
3330
import Cardano.Wallet.API.V1.Errors (ToServantError (..))
31+
import Cardano.Wallet.API.V1.Generic (jsendErrorGenericParseJSON,
32+
jsendErrorGenericToJSON)
3433
import Cardano.Wallet.API.V1.Types (V1 (..))
3534
import qualified Cardano.Wallet.API.V1.Types as V1
3635
import qualified Pos.Chain.Txp as V0
@@ -350,35 +349,27 @@ newtype MigrationError
350349
deriveGeneric ''MigrationError
351350

352351
instance ToJSON MigrationError where
353-
toEncoding (MigrationFailed weDescription) = pairs $ mconcat
354-
[ "status" .= ErrorStatus
355-
, "diagnostic" .= object
356-
[ "description" .= weDescription
357-
]
358-
, "message" .= String "MigrationFailed"
359-
]
352+
toJSON =
353+
jsendErrorGenericToJSON
360354

361355
instance FromJSON MigrationError where
362-
parseJSON = withObject "MigrationError" $ \o -> do
363-
message <- o .: "message"
364-
case message :: Text of
365-
"MigrationFailed" -> do
366-
diag <- o .: "diagnostic"
367-
desc <- diag .: "description"
368-
pure $ MigrationFailed desc
369-
_ ->
370-
fail "Incorrect JSON encoding for MigrationError"
356+
parseJSON =
357+
jsendErrorGenericParseJSON
371358

372359
instance Exception MigrationError
373360

374361
instance Arbitrary MigrationError where
375-
arbitrary = oneof
376-
[ pure $ MigrationFailed "Migration failed."
377-
]
362+
arbitrary =
363+
pure (MigrationFailed "Migration failed.")
378364

379365
instance Buildable MigrationError where
380366
build _ =
381367
bprint "Error while migrating a legacy type into the current version."
382368

369+
instance HasDiagnostic MigrationError where
370+
getDiagnosticKey _ =
371+
"description"
372+
383373
instance ToServantError MigrationError where
384-
declareServantError _ = err422
374+
declareServantError _ =
375+
err422

wallet-new/src/Cardano/Wallet/API/V1/Swagger.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import Control.Lens ((?~))
2828
import Data.Aeson (encode)
2929
import Data.Aeson.Encode.Pretty
3030
import Data.Map (Map)
31-
import Data.Swagger hiding (Example, Header, example)
31+
import Data.Swagger hiding (Example, Header)
3232
import Data.Typeable
3333
import Formatting (build, sformat)
3434
import NeatInterpolation

0 commit comments

Comments
 (0)