api-rpc-accumulate (empty) → 0.1.0.0
raw patch · 12 files changed
+724/−0 lines, 12 filesdep +aesondep +aeson-casingdep +api-rpc-accumulatesetup-changed
Dependencies added: aeson, aeson-casing, api-rpc-accumulate, base, bytestring, http-client, http-client-tls, http-conduit, json-alt, network, remote-json, remote-json-client, remote-monad, stm, text, time, transformers, unordered-containers
Files
- LICENSE +30/−0
- README.md +38/−0
- Setup.hs +2/−0
- api-rpc-accumulate.cabal +91/−0
- app/Main.hs +35/−0
- src/Accumulate/RPC/Api.hs +163/−0
- src/Accumulate/RPC/JsonRpc.hs +117/−0
- src/Accumulate/RPC/Types.hs +38/−0
- src/Accumulate/RPC/Types/ApiDataMetricsResponse.hs +54/−0
- src/Accumulate/RPC/Types/ApiDataResponse.hs +82/−0
- src/Accumulate/RPC/Types/ApiDataVersionResponse.hs +60/−0
- tests/Spec.hs +14/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Sergey Bushnyak (c) 2019++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Sergey Bushnyak nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,38 @@+# Haskell client for Accumulate++++[](https://github.com/kelecorix/api-rpc-accumulate/blob/master/LICENSE)++A JSON-RPC Haskell client for the Factom protocol. Each response has special ADT(algebraic data type) that automatically converted from JSON response. Using [Remote Monad](https://ku-fpg.github.io/files/Gill-15-RemoteMonad.pdf) pattern multiple request can be batched and executed simulatenously, following more robust approach and reducing usage of expensive RPC calls.++Choosing a batching strategy. There are two strategies:+- `Weak` - a single command or a single procedure, or+- `Strong` - a sequence of commands, optionally terminated by a procedure++# Installation++You can install package from [Hackage](https://hackage.haskell.org/package/api-rpc-accumulate) and build with Cabal, but we recommend to use [Stack](https://haskellstack.org) tool. Add to you dependencies in stack.yaml and cabal file dependency `- api-rpc-accumulate`.++To run and test from repository++1. Build with stack+```bash+$ stack build+```+2. Load REPL with stack for evaluation+```+$ stack repl+```++3. execute required methods++## Usage++## Contributions++The Library developed by Kelecorix, Inc. If you're an active user or find it useful we strongly encourage you to support our efforts and ensure long maintenance by contributing a small donation to one of the following cryptocurrency addresses:++- BTC: 39oVXpsgsyW8ZgzsnX3sV7HLdtXWfT96qN+- ETH: 0x9cDBA6bb44772259B3A3fb89cf233A147a720f34+- FCT: FA38cwer93mmPw1HxjScLmK1yF9iJTu5P87T2vdkbuLovm2YXyss
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ api-rpc-accumulate.cabal view
@@ -0,0 +1,91 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 1805692927b5d8cf72892a1cf66efcd49daa3d660596d04a38013452643c9fb2++name: api-rpc-accumulate+version: 0.1.0.0+synopsis: JSON-RPC API client for Accumulate blockchain+description: JSON-RPC API client for Accumulate blockchain. Contains basic models and api calls.+category: Web+homepage: https://github.com/kelecorix/accumulate-haskell-client#readme+bug-reports: https://github.com/kelecorix/accumulate-haskell-client/issues+author: Sergey Bushnyak+maintainer: sergey.bushnyak@sigrlami.eu+copyright: Copyright: (c) 2021-2022 Sergey Bushnyak, Kelecorix, Inc+license: MIT+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md++source-repository head+ type: git+ location: https://github.com/kelecorix/accumulate-haskell-client++library+ exposed-modules:+ Accumulate.RPC.Api+ Accumulate.RPC.Types+ other-modules:+ Accumulate.RPC.JsonRpc+ Accumulate.RPC.Types.ApiDataMetricsResponse+ Accumulate.RPC.Types.ApiDataResponse+ Accumulate.RPC.Types.ApiDataVersionResponse+ Paths_api_rpc_accumulate+ hs-source-dirs:+ src+ build-depends:+ aeson+ , aeson-casing+ , base >=4.7 && <5+ , bytestring+ , http-client+ , http-client-tls+ , http-conduit+ , json-alt+ , network+ , remote-json+ , remote-json-client+ , remote-monad+ , text+ , time+ , transformers+ , unordered-containers+ default-language: Haskell2010++executable acme-app+ main-is: Main.hs+ other-modules:+ Paths_api_rpc_accumulate+ hs-source-dirs:+ app+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ aeson+ , api-rpc-accumulate+ , base+ , bytestring+ , http-client+ , http-conduit+ , stm+ , text+ , time+ default-language: Haskell2010++test-suite acme-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_api_rpc_accumulate+ hs-source-dirs:+ tests+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ api-rpc-accumulate+ , base+ , bytestring+ default-language: Haskell2010
+ app/Main.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Control.Concurrent (forkIO, threadDelay)+import Control.Concurrent.STM.TVar+import Control.Monad (forever)+import qualified Data.Text as T+import Data.Time.Clock+import System.Environment++import qualified Accumulate.RPC.Api as AcmeApi+import qualified Accumulate.RPC.Types as AcmeApi++--------------------------------------------------------------------------------+main :: IO ()+main = do+ let delay = 60000 -- 1 request per minute available+ a <- getArgs+ timeVar <- newTVarIO (delay * 1000)+ case a of+ ["-t"] -> do+ -- read token from file+ return ()+ otherwise -> do+ putStrLn $ "Accumulate | Establishing connection"+ putStrLn $ "Accumulate | Getting last entries\n"++ -- let s = weakSession (traceSendAPI "" $ clientSendAPI endpoint)+ -- h <-+ -- send s $ do+ -- h <- reqGetData "acc://9c549cbba290efeb8029184caac3d36c5bfcacb361a29282/ACME"+ -- return h+ -- print h+ return ()
+ src/Accumulate/RPC/Api.hs view
@@ -0,0 +1,163 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}++module Accumulate.RPC.Api+ ( runTCPClient+ , reqGetData+ ) where++import Control.Concurrent+import Control.Exception (bracket)+import Control.Monad.IO.Class+import Control.Remote.Monad.JSON+import Control.Remote.Monad.JSON.Client+import Control.Remote.Monad.JSON.Router+import Control.Remote.Monad.JSON.Trace+import Data.Aeson+import Data.Aeson.Types+import Data.Text as T+import Network.Socket (HostName, ServiceName,+ SocketType (Stream),+ addrAddress, addrFamily,+ addrProtocol,+ addrSocketType, close,+ connect, defaultHints,+ getAddrInfo, socket)++import Accumulate.RPC.JsonRpc (JsonRpcT, runJsonRpcT)+import Accumulate.RPC.Types.ApiDataResponse+import Accumulate.RPC.Types.ApiDataMetricsResponse+import Accumulate.RPC.Types.ApiDataVersionResponse++--------------------------------------------------------------------------------++endpoint = "http://95.141.37.250:35554/v1"++runTCPClient :: HostName -> ServiceName -> JsonRpcT IO a -> IO a+runTCPClient host port f = do+ addr <- resolve host port+ bracket (open addr) close talk+ where+ resolve host' port' = do+ let hints = defaultHints {addrSocketType = Stream}+ addr:_ <- getAddrInfo (Just hints) (Just host') (Just port')+ return addr+ open addr = do+ sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)+ connect sock $ addrAddress addr+ return sock+ talk sock = runJsonRpcT sock f+++-- Options to convert internal types into Aeson values+-- $ Named [("jsonrpc", String "2.0"), ("id", toJSON (0::Int))]++--------------------------------------------------------------------------------+-- Data retrieval methods++-- | getData returns Accumulate Object by URL+--+reqGetData :: Text -> RPC APIDataResponse+reqGetData url = method "get" $ Named [("url", String url)] -- Named [("wait", Bool False), ("url", String url)]++-- | GetDirectory returns ADI directory entries+--+reqGetDirectory :: Text -> RPC ()+reqGetDirectory url = method "get-directory" $ List [String url]++--------------------------------------------------------------------------------+-- ADIs++-- |+--+reqGetADI :: Text -> RPC ()+reqGetADI url = method "adi" $ List [String url]++-- |+--+reqCreateADI :: Text -> RPC ()+reqCreateADI url = method "adi-create" $ List [String url]+++--------------------------------------------------------------------------------+-- Key Management++-- |+--+reqGetKeyBook :: Text -> RPC ()+reqGetKeyBook url = method "sig-spec-group" $ List [String url]+++--------------------------------------------------------------------------------+-- Tokens++-- | Get infromation about token+--+reqGetToken :: Text -> RPC ()+reqGetToken url = method "token" $ List [String url]+++-- | Get information about token transaction+--+reqGetTokenTx :: Text -> RPC ()+reqGetTokenTx txid = method "token-tx" $ List [String txid]++-- | Create new token transaction+--+reqCreateTokenTx :: Text -> RPC ()+reqCreateTokenTx url = method "token-tx-create" $ List [String url]++--------------------------------------------------------------------------------+-- Metrics++-- |+--+reqGetMetrics :: Text -> Text -> RPC APIDataMetricsResponse+reqGetMetrics metricName duration =+ method "metrics" $ Named [ ("metric" , String metricName)+ , ("duration", String duration)+ ]++reqGetVersion :: RPC APIDataVersionResponse+reqGetVersion = method "version" $ List []+++--------------------------------------------------------------------------------+-- Faucet++-- |+--+reqFaucet :: Text -> RPC ()+reqFaucet url = method "faucet" $ Named [("url", String url)]+++--------------------------------------------------------------------------------+-- Credits++-- |+--+reqAddCredits :: Text -> RPC ()+reqAddCredits url = method "add-credits" $ List [String url]+++main = do+ let s = weakSession (traceSendAPI "" $ clientSendAPI endpoint)+ (m,v) <-+ send s $ do+ --h <- reqGetData "acc://c9359900016daa23da0f4c07e66be42c398fe2b10017cecb/ACME"+ --t <- reqGetTokenTx ""+ m <- reqGetMetrics "tps" "1h"+ v <- reqGetVersion+ return (m, v)+ putStrLn "-----------"+ print $ show $ m+ putStrLn "-----------"+ print $ show $ v
+ src/Accumulate/RPC/JsonRpc.hs view
@@ -0,0 +1,117 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Accumulate.RPC.JsonRpc+ ( JsonRpcT(..)+ , Method(..)+ , Request(..)+ , Response(..)+ , Error+ , version+ , runJsonRpcT+ , request+ , mkDefaultRequest+ ) where++import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)+import Control.Monad.Trans.State.Lazy (StateT (..), evalStateT, get,+ modify')+import Data.Aeson (FromJSON (..), ToJSON (..),+ eitherDecode', encode,+ withObject, (.:), (.:?))+import Data.Aeson.Casing (snakeCase)+import Data.Aeson.TH (defaultOptions, deriveJSON,+ fieldLabelModifier,+ omitNothingFields)+import Data.ByteString.Lazy (fromStrict, toStrict)+import Data.Either (Either)+import Data.Text (Text)+import Network.Socket (Socket)+import Network.Socket.ByteString (recv, sendAll)++--------------------------------------------------------------------------------++newtype Version =+ Version Text+ deriving (Eq, Show, ToJSON, FromJSON)++version :: Version+version = Version "2.0"++newtype Method =+ Method Text+ deriving (Eq, Show, ToJSON, FromJSON)++data Request a =+ Request+ { reqJsonrpc :: Version+ , reqMethod :: Method+ , reqParams :: a+ , reqId :: Int+ }+ deriving (Show, Eq)++deriveJSON+ defaultOptions+ {omitNothingFields = True, fieldLabelModifier = snakeCase . drop 3}+ ''Request++mkDefaultRequest :: ToJSON a => Method -> a -> Request a+mkDefaultRequest method req = Request version method req 0++data Error =+ Error+ { code :: Int+ , message :: Text+ , errData :: Maybe Text+ }+ deriving (Eq, Show)++deriveJSON defaultOptions ''Error++-- | TODO: handle jsonrpc errors+data Response a =+ Response+ { resJsonrpc :: Version+ , resResult :: Either Error a+ , resId :: Int+ }+ deriving (Show, Eq)++instance FromJSON a => FromJSON (Response a) where+ parseJSON =+ withObject "response" $ \o -> do+ v <- o .: "jsonrpc"+ jid <- o .: "id"+ result <- o .:? "result"+ e <- o .:? "error"+ let r = maybe (maybe (fail "invalid response") Left e) Right result+ return $ Response v r jid++unwrapJson :: (Show a, FromJSON a) => Response a -> Either Error a+unwrapJson (Response _ r _) = r++newtype JsonRpcT m a =+ JsonRpcT+ { unJsonRpcT :: StateT Int (ReaderT Socket m) a+ }+ deriving (Functor, Applicative, Monad, MonadIO)++runJsonRpcT :: MonadIO m => Socket -> JsonRpcT m a -> m a+runJsonRpcT s jm = flip runReaderT s . flip evalStateT 0 $ unJsonRpcT jm++request ::+ (MonadIO m, Show b, ToJSON a, FromJSON b)+ => Method+ -> a+ -> JsonRpcT m (Either Error b)+request method params = do+ s <- JsonRpcT $ lift ask+ req <- toStrict . encode . Request version method params <$> JsonRpcT get+ liftIO $ sendAll s req+ JsonRpcT $ modify' (+ 1)+ response <- fromStrict <$> liftIO (recv s 262144)+ either fail (return . unwrapJson) $ eitherDecode' response
+ src/Accumulate/RPC/Types.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeSynonymInstances #-}++module Accumulate.RPC.Types where++import Data.Aeson+import Data.Aeson.TH+import Data.Maybe+import qualified Data.Text as T+import Data.Time+import GHC.Generics+++--------------------------------------------------------------------------------++data ApiRequest =+ ApiRequest+ { jsonRpc :: T.Text+ , id :: Int+ , method :: T.Text+ , params :: Maybe Object+ }+ deriving (Eq, Show, Generic, ToJSON)++data ApiResponse a =+ ApiResponse+ { jsonRpc :: T.Text+ , id :: Int+ , result :: a+ }+ deriving (Eq, Show, Generic, FromJSON)
+ src/Accumulate/RPC/Types/ApiDataMetricsResponse.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE StrictData #-}+{-# LANGUAGE OverloadedStrings #-}++module Accumulate.RPC.Types.ApiDataMetricsResponse+ ( APIDataMetricsResponse (..)+ , Data (..)+ , decodeTopLevel+ ) where++import Data.Aeson+import Data.Aeson.Types (emptyObject)+import Data.ByteString.Lazy (ByteString)+import Data.HashMap.Strict (HashMap)+import Data.Text (Text)++data APIDataMetricsResponse = APIDataMetricsResponse+ { apiDataMetricsResponseDataAPIDataMetricsResponse :: Data+ , sponsorAPIDataMetricsResponse :: Text+ , keyPageAPIDataMetricsResponse :: Maybe Text+ , apiDataMetricsResponseTypeAPIDataMetricsResponse :: Text+ } deriving (Show)++data Data = Data+ { valueData :: Text+ } deriving (Show)++decodeTopLevel :: ByteString -> Maybe APIDataMetricsResponse+decodeTopLevel = decode++instance ToJSON APIDataMetricsResponse where+ toJSON (APIDataMetricsResponse apiDataMetricsResponseDataAPIDataMetricsResponse sponsorAPIDataMetricsResponse keyPageAPIDataMetricsResponse apiDataMetricsResponseTypeAPIDataMetricsResponse) =+ object+ [ "data" .= apiDataMetricsResponseDataAPIDataMetricsResponse+ , "sponsor" .= sponsorAPIDataMetricsResponse+ , "keyPage" .= keyPageAPIDataMetricsResponse+ , "type" .= apiDataMetricsResponseTypeAPIDataMetricsResponse+ ]++instance FromJSON APIDataMetricsResponse where+ parseJSON (Object v) = APIDataMetricsResponse+ <$> v .: "data"+ <*> v .: "sponsor"+ <*> v .: "keyPage"+ <*> v .: "type"++instance ToJSON Data where+ toJSON (Data valueData) =+ object+ [ "value" .= valueData+ ]++instance FromJSON Data where+ parseJSON (Object v) = Data+ <$> v .: "value"
+ src/Accumulate/RPC/Types/ApiDataResponse.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StrictData #-}++module Accumulate.RPC.Types.ApiDataResponse+ ( APIDataResponse (..),+ Data (..),+ decodeApiData,+ )+where++import Data.Aeson+import Data.Aeson.Types (emptyObject)+import Data.ByteString.Lazy (ByteString)+import Data.HashMap.Strict (HashMap)+import Data.Text (Text)++--------------------------------------------------------------------------------++data APIDataResponse = APIDataResponse+ { apiDataResponseDataAPIDataResponse :: Maybe Data,+ sponsorAPIDataResponse :: Maybe Text,+ keyPageAPIDataResponse :: Maybe (Maybe Text),+ mdRootAPIDataResponse :: Maybe Text,+ apiDataResponseTypeAPIDataResponse :: Maybe Text+ }+ deriving (Show)++data Data = Data+ { txCountData :: Maybe Int,+ urlData :: Maybe Text,+ balanceData :: Maybe Text,+ keyBookURLData :: Maybe Text,+ tokenURLData :: Maybe Text,+ creditBalanceData :: Maybe Text,+ nonceData :: Maybe Int+ }+ deriving (Show)++decodeApiData :: ByteString -> Maybe APIDataResponse+decodeApiData = decode++instance ToJSON APIDataResponse where+ toJSON (APIDataResponse apiDataResponseDataAPIDataResponse sponsorAPIDataResponse keyPageAPIDataResponse mdRootAPIDataResponse apiDataResponseTypeAPIDataResponse) =+ object+ [ "data" .= apiDataResponseDataAPIDataResponse,+ "sponsor" .= sponsorAPIDataResponse,+ "keyPage" .= keyPageAPIDataResponse,+ "mdRoot" .= mdRootAPIDataResponse,+ "type" .= apiDataResponseTypeAPIDataResponse+ ]++instance FromJSON APIDataResponse where+ parseJSON (Object v) =+ APIDataResponse+ <$> v .:? "data"+ <*> v .:? "sponsor"+ <*> v .:? "keyPage"+ <*> v .:? "mdRoot"+ <*> v .:? "type"++instance ToJSON Data where+ toJSON (Data txCountData urlData balanceData keyBookURLData tokenURLData creditBalanceData nonceData) =+ object+ [ "txCount" .= txCountData,+ "url" .= urlData,+ "balance" .= balanceData,+ "keyBookUrl" .= keyBookURLData,+ "tokenUrl" .= tokenURLData,+ "creditBalance" .= creditBalanceData,+ "nonce" .= nonceData+ ]++instance FromJSON Data where+ parseJSON (Object v) =+ Data+ <$> v .:? "txCount"+ <*> v .:? "url"+ <*> v .:? "balance"+ <*> v .:? "keyBookUrl"+ <*> v .:? "tokenUrl"+ <*> v .:? "creditBalance"+ <*> v .:? "nonce"
+ src/Accumulate/RPC/Types/ApiDataVersionResponse.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE StrictData #-}+{-# LANGUAGE OverloadedStrings #-}++module Accumulate.RPC.Types.ApiDataVersionResponse+ ( APIDataVersionResponse (..)+ , Data (..)+ , decodeTopLevel+ ) where++import Data.Aeson+import Data.Aeson.Types (emptyObject)+import Data.ByteString.Lazy (ByteString)+import Data.HashMap.Strict (HashMap)+import Data.Text (Text)++data APIDataVersionResponse = APIDataVersionResponse+ { apiDataVersionResponseDataAPIDataVersionResponse :: Data+ , sponsorAPIDataVersionResponse :: Text+ , keyPageAPIDataVersionResponse :: Maybe Text+ , apiDataVersionResponseTypeAPIDataVersionResponse :: Text+ } deriving (Show)++data Data = Data+ { versionIsKnownData :: Bool+ , versionData :: Text+ , commitData :: Text+ } deriving (Show)++decodeTopLevel :: ByteString -> Maybe APIDataVersionResponse+decodeTopLevel = decode++instance ToJSON APIDataVersionResponse where+ toJSON (APIDataVersionResponse apiDataVersionResponseDataAPIDataVersionResponse sponsorAPIDataVersionResponse keyPageAPIDataVersionResponse apiDataVersionResponseTypeAPIDataVersionResponse) =+ object+ [ "data" .= apiDataVersionResponseDataAPIDataVersionResponse+ , "sponsor" .= sponsorAPIDataVersionResponse+ , "keyPage" .= keyPageAPIDataVersionResponse+ , "type" .= apiDataVersionResponseTypeAPIDataVersionResponse+ ]++instance FromJSON APIDataVersionResponse where+ parseJSON (Object v) = APIDataVersionResponse+ <$> v .: "data"+ <*> v .: "sponsor"+ <*> v .: "keyPage"+ <*> v .: "type"++instance ToJSON Data where+ toJSON (Data versionIsKnownData versionData commitData) =+ object+ [ "versionIsKnown" .= versionIsKnownData+ , "version" .= versionData+ , "commit" .= commitData+ ]++instance FromJSON Data where+ parseJSON (Object v) = Data+ <$> v .: "versionIsKnown"+ <*> v .: "version"+ <*> v .: "commit"
+ tests/Spec.hs view
@@ -0,0 +1,14 @@+import Control.Exception (evaluate)+import Test.Hspec+import Test.QuickCheck++main :: IO ()+main =+ hspec $ do+ describe "Prelude.head" $ do+ it "returns the first element of a list" $ do+ head [23 ..] `shouldBe` (23 :: Int)+ it "returns the first element of an *arbitrary* list" $+ property $ \x xs -> head (x : xs) == (x :: Int)+ it "throws an exception if used with an empty list" $ do+ evaluate (head []) `shouldThrow` anyException