packages feed

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 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++![build](https://github.com/kelecorix/accumulate-haskell-client/workflows/build/badge.svg)+![Hackage](https://img.shields.io/hackage/v/api-rpc-accumulate)+[![License](https://img.shields.io/badge/license-MIT-blue.svg)](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