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

Commit

Permalink
[CO-319] Make V1/Error part of V1/Types
Browse files Browse the repository at this point in the history
To realize this, we had to extract JSONValidationFailed and MigrationFailed constructor from WalletError.
They're now defined as constructor in different data-types (resp. JSONValidationError and MigrationError).
  • Loading branch information
KtorZ committed Jun 25, 2018
1 parent c797b3d commit 6161cb7
Show file tree
Hide file tree
Showing 15 changed files with 379 additions and 364 deletions.
2 changes: 1 addition & 1 deletion crypto/Pos/Crypto/Orphans.hs
Expand Up @@ -5,7 +5,7 @@ module Pos.Crypto.Orphans
) where

import Prelude (show)
import Universum hiding (show)
import Universum

import qualified Cardano.Crypto.Wallet as CC
import qualified Crypto.SCRAPE as Scrape
Expand Down
2 changes: 1 addition & 1 deletion wallet-new/cardano-sl-wallet-new.cabal
Expand Up @@ -39,8 +39,8 @@ library
Cardano.Wallet.API.V1
Cardano.Wallet.API.V1.Accounts
Cardano.Wallet.API.V1.Addresses
Cardano.Wallet.API.V1.Errors
Cardano.Wallet.API.V1.Generic
Cardano.Wallet.API.V1.Errors
Cardano.Wallet.API.V1.Handlers
Cardano.Wallet.API.V1.Headers
Cardano.Wallet.API.V1.Info
Expand Down
4 changes: 2 additions & 2 deletions wallet-new/integration/TransactionSpecs.hs
Expand Up @@ -5,12 +5,12 @@ module TransactionSpecs (transactionSpecs) where

import Universum

import Cardano.Wallet.API.V1.Errors hiding (describe)
import Cardano.Wallet.Client.Http
import Control.Lens
import qualified Pos.Core as Core
import Test.Hspec

import qualified Pos.Core as Core

import Util


Expand Down
2 changes: 1 addition & 1 deletion wallet-new/integration/WalletSpecs.hs
Expand Up @@ -5,8 +5,8 @@ module WalletSpecs (walletSpecs) where

import Universum

import Cardano.Wallet.API.V1.Types (WalletError (WalletAlreadyExists))
import Cardano.Wallet.Client.Http
import Cardano.Wallet.API.V1.Errors (WalletError (WalletAlreadyExists))
import Control.Lens
import Test.Hspec

Expand Down
72 changes: 50 additions & 22 deletions wallet-new/src/Cardano/Wallet/API/Response.hs
@@ -1,10 +1,12 @@
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
module Cardano.Wallet.API.Response (
Metadata (..)
, ResponseStatus(..)
, WalletResponse(..)
, JSONValidationError(..)
-- * Generating responses for collections
, respondWith
-- * Generating responses for single resources
Expand All @@ -13,33 +15,37 @@ module Cardano.Wallet.API.Response (
) where

import Prelude
import Universum (Buildable, decodeUtf8, toText, (<>))
import Universum (Buildable, Text, decodeUtf8, toText, (<>))

import Cardano.Wallet.API.Indices (Indexable', IxSet')
import Cardano.Wallet.API.Request (RequestParams (..))
import Cardano.Wallet.API.Request.Filter (FilterOperations (..))
import Cardano.Wallet.API.Request.Pagination (Page (..), PaginationMetadata (..),
PaginationParams (..), PerPage (..))
import Cardano.Wallet.API.Request.Sort (SortOperations (..))
import Cardano.Wallet.API.Response.Filter.IxSet as FilterBackend
import Cardano.Wallet.API.Response.JSend (ResponseStatus (..))
import Cardano.Wallet.API.Response.Sort.IxSet as SortBackend
import Cardano.Wallet.API.V1.Errors (ToServantError (..))
import Cardano.Wallet.API.V1.Generic (gparseJsend, gtoJsend)
import Control.Lens
import Data.Aeson
import Data.Aeson.Encode.Pretty (encodePretty)
import Data.Aeson.TH
import qualified Data.Char as Char
import Data.Swagger as S
import qualified Data.Text.Buildable
import Data.Typeable
import Formatting (bprint, build, (%))
import Generics.SOP.TH (deriveGeneric)
import GHC.Generics (Generic)
import qualified Serokell.Aeson.Options as Serokell
import Servant (err400)
import Servant.API.ContentTypes (Accept (..), JSON, MimeRender (..), MimeUnrender (..),
OctetStream)
import Test.QuickCheck

import Cardano.Wallet.API.Indices (Indexable', IxSet')
import Cardano.Wallet.API.Request (RequestParams (..))
import Cardano.Wallet.API.Request.Filter (FilterOperations (..))
import Cardano.Wallet.API.Request.Pagination (Page (..), PaginationMetadata (..),
PaginationParams (..), PerPage (..))
import Cardano.Wallet.API.Request.Sort (SortOperations (..))
import Cardano.Wallet.API.Response.Filter.IxSet as FilterBackend
import Cardano.Wallet.API.Response.Sort.IxSet as SortBackend
import Cardano.Wallet.API.V1.Errors (WalletError (JSONValidationFailed))
import qualified Data.Char as Char
import qualified Data.Text.Buildable
import qualified Serokell.Aeson.Options as Serokell


-- | Extra information associated with an HTTP response.
data Metadata = Metadata
Expand Down Expand Up @@ -136,7 +142,7 @@ respondWith :: (Monad m, Indexable' a)
-> m (WalletResponse [a])
respondWith RequestParams{..} fops sorts generator = do
(theData, paginationMetadata) <- paginate rpPaginationParams . sortData sorts . applyFilters fops <$> generator
return $ WalletResponse {
return WalletResponse {
wrData = theData
, wrStatus = SuccessStatus
, wrMeta = Metadata paginationMetadata
Expand Down Expand Up @@ -174,18 +180,40 @@ data ValidJSON deriving Typeable

instance FromJSON a => MimeUnrender ValidJSON a where
mimeUnrender _ bs = case eitherDecode bs of
Left err -> Left $ decodeUtf8 $ encodePretty (jsonValidationFailed err)
Left err -> Left $ decodeUtf8 $ encodePretty (JSONValidationFailed $ toText err)
Right v -> return v
where
-- NOTE Cheating a bit with type params here, ideally, we would like
-- types we can render ToJSON, though we only use the JSONValidationFailed
-- which doesn't rely on the type-parameters.
jsonValidationFailed :: String -> WalletError () () ()
jsonValidationFailed =
JSONValidationFailed . toText

instance Accept ValidJSON where
contentType _ = contentType (Proxy @ JSON)

instance ToJSON a => MimeRender ValidJSON a where
mimeRender _ = mimeRender (Proxy @ JSON)


--
-- Error from parsing / validating JSON inputs
--

newtype JSONValidationError
= JSONValidationFailed Text
deriving (Show, Eq)

deriveGeneric ''JSONValidationError

instance ToJSON JSONValidationError where
toJSON = gtoJsend ErrorStatus

instance FromJSON JSONValidationError where
parseJSON = gparseJsend

instance Exception JSONValidationError

instance Buildable JSONValidationError where
build = \case
JSONValidationFailed _ ->
bprint "Couldn't decode a JSON input."

instance ToServantError JSONValidationError where
declareServantError = \case
JSONValidationFailed _ ->
err400
211 changes: 18 additions & 193 deletions wallet-new/src/Cardano/Wallet/API/V1/Errors.hs
@@ -1,202 +1,27 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

module Cardano.Wallet.API.V1.Errors where

import Universum

import Data.Aeson
import Generics.SOP.TH (deriveGeneric)
import Servant
import Test.QuickCheck (Arbitrary (arbitrary))
import Test.QuickCheck.Gen (oneof)

import Cardano.Wallet.API.Response.JSend (ResponseStatus (ErrorStatus))
import Cardano.Wallet.API.V1.Generic (gparseJsend, gtoJsend)
import Cardano.Wallet.API.V1.Headers (applicationJson)
import Data.Aeson (ToJSON, encode)
import Formatting (build, sformat)
import Servant (ServantErr (..))

import qualified Network.HTTP.Types as HTTP


--
-- Error handling
--

-- | Type representing any error which might be thrown by wallet.
--
-- Errors are represented in JSON in the JSend format (<https://labs.omniti.com/labs/jsend>):
-- ```
-- {
-- "status": "error"
-- "message" : <constr_name>,
-- "diagnostic" : <data>
-- }
-- ```
-- where `<constr_name>` is a string containing name of error's constructor (e. g. `NotEnoughMoney`),
-- and `<data>` is an object containing additional error data.
-- Additional data contains constructor fields, field names are record field names without
-- a `we` prefix, e. g. for `OutputIsRedeem` error "diagnostic" field will be the following:
-- ```
-- {
-- "address" : <address>
-- }
-- ```
--
-- Additional data in constructor should be represented as record fields.
-- Otherwise TemplateHaskell will raise an error.
--
-- If constructor does not have additional data (like in case of `WalletNotFound` error),
-- then "diagnostic" field will be empty object.
--
-- TODO: change fields' types to actual Cardano core types, like `Coin` and `Address`
data WalletError address syncProgress syncPercentage =
NotEnoughMoney { weNeedMore :: !Int }
| OutputIsRedeem { weAddress :: !address }
| MigrationFailed { weDescription :: !Text }
| JSONValidationFailed { weValidationError :: !Text }
| UnknownError { weMsg :: !Text }
| InvalidAddressFormat { weMsg :: !Text }
| WalletNotFound
-- FIXME(akegalj): https://iohk.myjetbrains.com/youtrack/issue/CSL-2496
| WalletAlreadyExists
| AddressNotFound
| TxFailedToStabilize
| TxRedemptionDepleted
| TxSafeSignerNotFound { weAddress :: address }
| MissingRequiredParams { requiredParams :: NonEmpty (Text, Text) }
| WalletIsNotReadyToProcessPayments { weStillRestoring :: syncProgress }
-- ^ The @Wallet@ where a @Payment@ is being originated is not fully
-- synced (its 'WalletSyncState' indicates it's either syncing or
-- restoring) and thus cannot accept new @Payment@ requests.
| NodeIsStillSyncing { wenssStillSyncing :: syncPercentage }
-- ^ The backend couldn't process the incoming request as the underlying
-- node is still syncing with the blockchain.
deriving (Show, Eq)


--
-- Instances for `WalletError`

-- deriveWalletErrorJSON ''WalletError
deriveGeneric ''WalletError

instance (ToJSON a, ToJSON b, ToJSON c) => ToJSON (WalletError a b c) where
toJSON = gtoJsend ErrorStatus

instance (FromJSON a, FromJSON b, FromJSON c) => FromJSON (WalletError a b c) where
parseJSON = gparseJsend

instance (Typeable a, Show a, Typeable b, Show b, Typeable c, Show c) =>
Exception (WalletError a b c)

instance (Arbitrary a, Arbitrary b, Arbitrary c) => Arbitrary (WalletError a b c) where
arbitrary = oneof
[ NotEnoughMoney <$> arbitrary
, OutputIsRedeem <$> arbitrary
, pure (MigrationFailed "Migration failed.")
, pure (JSONValidationFailed "Expected String, found Null.")
, pure (UnknownError "Unknown error.")
, pure (InvalidAddressFormat "Invalid Base58 representation.")
, pure WalletNotFound
, pure WalletAlreadyExists
, pure AddressNotFound
, pure TxFailedToStabilize
, pure TxRedemptionDepleted
, TxSafeSignerNotFound <$> arbitrary
, pure (MissingRequiredParams (("wallet_id", "walletId") :| []))
, WalletIsNotReadyToProcessPayments <$> arbitrary
, NodeIsStillSyncing <$> arbitrary
]


--
-- Helpers
--

-- | Give a short description of an error
describe :: forall a b c. WalletError a b c -> String
describe = \case
NotEnoughMoney _ ->
"Not enough available coins to proceed."
OutputIsRedeem _ ->
"One of the TX outputs is a redemption address."
MigrationFailed _ ->
"Error while migrating a legacy type into the current version."
JSONValidationFailed _ ->
"Couldn't decode a JSON input."
UnknownError _ ->
"Unexpected internal error."
InvalidAddressFormat _ ->
"Provided address format is not valid."
WalletNotFound ->
"Reference to an unexisting wallet was given."
WalletAlreadyExists ->
"Can't create or restore a wallet. The wallet already exists."
AddressNotFound ->
"Reference to an unexisting address was given."
MissingRequiredParams _ ->
"Missing required parameters in the request payload."
WalletIsNotReadyToProcessPayments _ ->
"This wallet is restoring, and it cannot send new transactions until restoration completes."
NodeIsStillSyncing _ ->
"The node is still syncing with the blockchain, and cannot process the request yet."
TxRedemptionDepleted ->
"The redemption address was already used."
TxSafeSignerNotFound _ ->
"The safe signer at the specified address was not found."
TxFailedToStabilize ->
"We were unable to find a set of inputs to satisfy this transaction."


-- | Convert wallet errors to Servant errors
toServantError
:: forall a b c. (ToJSON a, ToJSON b, ToJSON c)
=> WalletError a b c
-> ServantErr
toServantError err =
mkServantErr $ case err of
NotEnoughMoney{} ->
err403
OutputIsRedeem{} ->
err403
MigrationFailed{} ->
err422
JSONValidationFailed{} ->
err400
UnknownError{} ->
err500
WalletNotFound{} ->
err404
WalletAlreadyExists{} ->
err403
InvalidAddressFormat{} ->
err401
AddressNotFound{} ->
err404
MissingRequiredParams{} ->
err400
WalletIsNotReadyToProcessPayments{} ->
err403
NodeIsStillSyncing{} ->
err412 -- Precondition failed
TxFailedToStabilize{} ->
err500
TxRedemptionDepleted{} ->
err400
TxSafeSignerNotFound{} ->
err400
where
mkServantErr serr@ServantErr{..} = serr
{ errBody = encode err
, errHeaders = applicationJson : errHeaders
}

-- |
toHttpStatus
:: forall a b c. (ToJSON a, ToJSON b, ToJSON c)
=> WalletError a b c
-> HTTP.Status
toHttpStatus err = HTTP.Status (errHTTPCode $ toServantError err)
(encodeUtf8 $ describe err)
class (ToJSON e) => ToServantError e where
declareServantError :: e -> ServantErr
toServantError :: e -> ServantErr
toServantError err =
mkServantErr (declareServantError err)
where
mkServantErr serr@ServantErr{..} = serr
{ errBody = encode err
, errHeaders = applicationJson : errHeaders
}

class (ToServantError e, Buildable e) => ToHttpErrorStatus e where
toHttpErrorStatus :: e -> HTTP.Status
toHttpErrorStatus err =
HTTP.Status (errHTTPCode $ toServantError err) (encodeUtf8 $ sformat build err)

0 comments on commit 6161cb7

Please sign in to comment.