packages feed

DOH (empty) → 0.1.0.0

raw patch · 26 files changed

+2308/−0 lines, 26 filesdep +QuickCheckdep +aesondep +aeson-casingsetup-changed

Dependencies added: QuickCheck, aeson, aeson-casing, base, bytestring, containers, filepath, http-client, http-client-tls, http-types, interpolatedstring-perl6, lens, mtl, quickcheck-instances, text, time, unordered-containers, uri

Files

+ DOH.cabal view
@@ -0,0 +1,84 @@+-- Initial DO.cabal generated by cabal init.  For further documentation, +-- see http://haskell.org/cabal/users-guide/++name:                DOH+version:             0.1.0.0+synopsis:            A complete API wrapper for DigitalOcean API V2+description:         A complete API wrapper for DigitalOcean API V2+                     .+                     To get started, see @Network.DigitalOcean@ module below.+license:             MIT+license-file:        LICENSE+author:              Yiğit Özkavcı+maintainer:          yigitozkavci8@gmail.com+-- copyright:           +category:            Network+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10+source-repository head+  type: git+  location: https://github.com/inzva/DigitalOcean.hs+library+  exposed-modules:     Network.DigitalOcean+                       Network.DigitalOcean.Http,+                       Network.DigitalOcean.Types,+                       Network.DigitalOcean.Utils.Actions,+                       Network.DigitalOcean.Utils.Pagination,+                       Network.DigitalOcean.Services,+                       Network.DigitalOcean.Services.Account,+                       Network.DigitalOcean.Services.Action,+                       Network.DigitalOcean.Services.Certificate,+                       Network.DigitalOcean.Services.Domain,+                       Network.DigitalOcean.Services.DomainRecord,+                       Network.DigitalOcean.Services.Droplet,+                       Network.DigitalOcean.Services.Firewall,+                       Network.DigitalOcean.Services.FloatingIp,+                       Network.DigitalOcean.Services.Image,+                       Network.DigitalOcean.Services.LoadBalancer,+                       Network.DigitalOcean.Services.Region,+                       Network.DigitalOcean.Services.Size,+                       Network.DigitalOcean.Services.Snapshot,+                       Network.DigitalOcean.Services.SSHKey,+                       Network.DigitalOcean.Services.Volume+  -- other-modules:       +  -- other-extensions:    +  build-depends:       base >=4.9 && <4.10,+                       mtl,+                       text,+                       http-client,+                       http-types,+                       http-client-tls,+                       bytestring,+                       uri,+                       aeson,+                       aeson-casing,+                       time,+                       lens,+                       containers,+                       filepath,+                       unordered-containers+  hs-source-dirs:      src+  default-language:    Haskell2010++test-suite do-test+  default-language:    Haskell2010+  type:           exitcode-stdio-1.0+  main-is:        Spec.hs+  build-depends:  base >=4.9 && <4.10,+                  mtl,+                  text,+                  http-client,+                  http-types,+                  http-client-tls,+                  bytestring,+                  uri,+                  aeson,+                  time,+                  aeson-casing,+                  lens,+                  QuickCheck,+                  quickcheck-instances,+                  interpolatedstring-perl6,+                  filepath+  hs-source-dirs: src, test
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2017 ++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,2 @@+# DigitalOcean.hs+Haskell bindings for DigitalOcean API v2
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Network/DigitalOcean.hs view
@@ -0,0 +1,413 @@+{-|+Module      : DigitalOcean+Description : This module is where user shall interract with the whole API+Copyright   : (c) Yiğit Özkavcı, 2017+License     : MIT+Maintainer  : yigitozkavci8@gmail.com+Stability   : beta+Portability : POSIX++Every computation within this module should be assessed via `DO` monad.++An example usage:++@+{&#45;\# LANGUAGE OverloadedStrings \#&#45;}++module Main where++import 'Network.DigitalOcean'+import Control.Monad.Except+import Control.Monad.Reader++client :: 'Client'+client = 'Client' \"my api key\"++main :: IO ()+main = do+  result <- runExceptT $ (runReaderT $ \"runDO\" doActions) client+  case result of+    Left err -> print err+    Right _ -> return ()++createViaSshKeys :: 'DO' ()+createViaSshKeys = do+  -- Read a public key from a key pair and create ssh keys on DigitalOcean with it+  pubKey <- liftIO $ readFile \"\/Users\/yigitozkavci\/.ssh\/do_api_rsa.pub\"+  sshKey <- 'createSSHKey' ('SSHKeyPayload' \"my ssh key\" pubKey) +  +  -- Create 2 droplets with our newly uploaded ssh keys+  let dropletPayload = 'IDropletPayload' \"nyc3\" \"512mb\" \"ubuntu-14-04-x64\" (Just [sshkeyFingerprint sshKey]) Nothing Nothing Nothing Nothing Nothing Nothing Nothing+  droplets <- map dropletId <$\> 'createDroplets' ["droplet-1", "droplet-2"] dropletPayload++  -- Take snapshot of our newly created droplets+  forM_ droplets $ \dropletId -> 'performDropletAction' dropletId ('TakeSnapshot' (Just "bulk snapshot"))+@+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TupleSections #-}++module Network.DigitalOcean where++-----------------------------------------------------------------+import           Data.Maybe                (maybe)+import           Data.Monoid               ((<>))+import           Control.Lens+import           Data.List                 (intercalate)+import           Control.Monad+import           Control.Monad.Reader+import           Control.Monad.Except+import           Data.Bool                 (bool)+import qualified Data.Set                  as Set+-----------------------------------------------------------------+import           Network.DigitalOcean.Types+import           Network.DigitalOcean.Http+import           Network.DigitalOcean.Utils.Pagination+import           Network.DigitalOcean.Services+-----------------------------------------------------------------++-- * Account+getAccounts :: DO Account+getAccounts = unResponse <$> get' AccountEndpoint++-- * Actions+getActions :: Maybe PaginationConfig -> DO [Action]+getActions config = getPaginated config ActionsEndpoint Nothing++getAction :: ActionId -> DO Action+getAction id' =+  unResponse <$> get' (ActionEndpoint id')++-- * Regions+getRegions :: DO [Region]+getRegions =+  unResponse <$> get' RegionsEndpoint++-- * Volumes+getVolumes :: DO [Volume]+getVolumes =+  unResponse <$> get' VolumesEndpoint++getVolume :: VolumeId -> DO Volume+getVolume id' =+  unResponse <$> get' (VolumeEndpoint id')++createVolume :: VolumePayload -> DO Volume+createVolume =+  fmap unResponse . post VolumesEndpoint Nothing++getVolumesByName :: String -> String -> DO [Volume]+getVolumesByName region name =+  let queryParams = Just [("name", name), ("region", region)] in+  unResponse <$> get VolumesEndpoint queryParams++deleteVolume :: VolumeId -> DO ()+deleteVolume id' =+  delete' (VolumeEndpoint id')++deleteVolumeByName :: String -> String -> DO ()+deleteVolumeByName region name =+  delete VolumesEndpoint (Just [("name", name), ("region", region)]) EmptyPayload++performSingleVolumeAction :: VolumeId -> VolumeAction -> DO Action+performSingleVolumeAction volumeId action =+  unResponse <$> post (VolumeActionsEndpoint volumeId) Nothing action++performListVolumeAction :: VolumeAction -> DO Action+performListVolumeAction action =+  unResponse <$> post VolumesActionsEndpoint Nothing action++performVolumeAction :: VolumeAction -> DO Action+performVolumeAction action@(Attach volumeId _ _) = performSingleVolumeAction volumeId action+performVolumeAction action@(Detach volumeId _ _) = performSingleVolumeAction volumeId action+performVolumeAction action@(ResizeVolume volumeId _ _) = performSingleVolumeAction volumeId action+performVolumeAction action@AttachByName {}       = performListVolumeAction action+performVolumeAction action@DetachByName {}       = performListVolumeAction action++getVolumeActions :: VolumeId -> DO [Action]+getVolumeActions volumeId =+  unResponse <$> get' (VolumeActionsEndpoint volumeId)++getVolumeAction :: VolumeId -> ActionId -> DO Action+getVolumeAction volumeId actionId =+  unResponse <$> get' (VolumeActionEndpoint volumeId actionId)++-- * Snapshots+data ResourceType = VolumeResource+                  | DropletResource++instance Show ResourceType where+  show VolumeResource = "volume"+  show DropletResource = "droplet"++getSnapshots :: Maybe ResourceType -> DO [Snapshot]+getSnapshots resourceType = do+  let queryParams = ((:[]) . ("resource_type",) . show) <$> resourceType+  unResponse <$> get SnapshotsEndpoint queryParams++getSnapshot :: SnapshotId -> DO Snapshot+getSnapshot id' =+  unResponse <$> get' (SnapshotEndpoint id')++deleteSnapshot :: SnapshotId -> DO ()+deleteSnapshot id' =+  delete' (SnapshotEndpoint id')++getSnapshotsOfVolume :: VolumeId -> DO [Snapshot]+getSnapshotsOfVolume volumeId =+  unResponse <$> get' (VolumeSnapshotsEndpoint volumeId)++createSnapshotOfVolume :: VolumeId -> SnapshotPayload -> DO Snapshot+createSnapshotOfVolume volumeId =+  fmap unResponse . post (VolumeSnapshotsEndpoint volumeId) Nothing++-- * Certificates+createCertificate :: Certificatepayload -> DO Certificate+createCertificate = fmap unResponse . post CertificatesEndpoint Nothing++getCertificate :: CertificateId -> DO Certificate+getCertificate id' =+  unResponse <$> get' (CertificateEndpoint id')++getCertificates :: Maybe PaginationConfig -> DO [Certificate]+getCertificates config = getPaginated config CertificatesEndpoint Nothing++deleteCertificate :: CertificateId -> DO ()+deleteCertificate id' = delete' (CertificateEndpoint id')++-- * Domains+getDomains :: DO [Domain]+getDomains =+  unResponse <$> get' DomainsEndpoint++getDomain :: DomainName -> DO Domain+getDomain name' =+  unResponse <$> get' (DomainEndpoint name')++createDomain :: DomainPayload -> DO Domain+createDomain = fmap unResponse . post DomainsEndpoint Nothing++deleteDomain :: DomainName -> DO ()+deleteDomain name' = delete' (DomainEndpoint name')++-- * Domain Records+getDomainRecords :: DomainName -> DO [DomainRecord]+getDomainRecords domainName' =+  unResponse <$> get' (DomainRecordsEndpoint domainName')++createDomainRecord :: DomainName -> DomainRecordPayload -> DO DomainRecord+createDomainRecord domainName' =+  fmap unResponse . post (DomainRecordsEndpoint domainName') Nothing++getDomainRecord :: DomainName -> DomainRecordId -> DO DomainRecord+getDomainRecord dn' drid' =+  unResponse <$> get' (DomainRecordEndpoint dn' drid')++updateDomainRecord :: DomainName -> DomainRecordId -> DomainRecordPayload -> DO DomainRecord+updateDomainRecord dn' drid' =+  fmap unResponse . put (DomainRecordEndpoint dn' drid') Nothing++deleteDomainRecord :: DomainName -> DomainRecordId -> DO ()+deleteDomainRecord dn' drid' =+  delete' (DomainRecordEndpoint dn' drid')++-- * Images+getImages :: Maybe PaginationConfig -> ImageOptions -> DO [Image]+getImages config ImageOptions {..} =+  getPaginated config ImagesEndpoint (Just queryParams)+  where+    queryParams = +      maybe [] ((:[]) . ("type",) . show) imageType' +++      bool [] [("private", "true")] isPrivate++getImageActions :: ImageId -> DO [Action]+getImageActions id' =+  unResponse <$> get' (ImageActionsEndpoint id')++getImageAction :: ImageId -> ActionId -> DO Action+getImageAction imageId actionId = unResponse <$> get' (ImageActionEndpoint imageId actionId)++getImage :: ImageId -> DO [Image]+getImage id' =+  unResponse <$> get' (ImageEndpoint id')++getImageBySlug :: String -> DO [Image]+getImageBySlug slug = unResponse <$> get' (ImageBySlugEndpoint slug)++updateImage :: ImageId -> ImagePayload -> DO Image+updateImage id' = fmap unResponse . put (ImageEndpoint id') Nothing++deleteImage :: ImageId -> DO ()+deleteImage id' = delete' (ImageEndpoint id')++performImageAction :: ImageId -> ImageAction -> DO Action+performImageAction id' = fmap unResponse . post (ImageActionsEndpoint id') Nothing ++-- * Sizes+getSizes :: DO [Size]+getSizes = unResponse <$> get' SizesEndpoint++-- * Droplets+getDroplets :: Maybe PaginationConfig -> DO [Droplet]+getDroplets config = getPaginated config DropletsEndpoint Nothing++createDroplet :: DropletName -> IDropletPayload -> DO Droplet+createDroplet name payload = unResponse <$> post DropletsEndpoint Nothing (SingleDropletPayload name payload)++createDroplets :: [DropletName] -> IDropletPayload -> DO [Droplet]+createDroplets names payload = unResponse <$> post DropletsEndpoint Nothing (MultipleDropletPayload names payload)++getDroplet :: DropletId -> DO Droplet+getDroplet id' = unResponse <$> get' (DropletEndpoint id')++getDropletsByTag :: String -> DO [Droplet]+getDropletsByTag tag = unResponse <$> get DropletsEndpoint (Just [("tag_name", tag)])++getDropletKernels :: DropletId -> DO [Kernel]+getDropletKernels id' = unResponse <$> get' (DropletKernelsEndpoint id')++getDropletSnapshots :: DropletId -> DO [Snapshot]+getDropletSnapshots id' = unResponse <$> get' (DropletSnapshotsEndpoint id')++getDropletBackups :: DropletId -> DO [Backup]+getDropletBackups id' = unResponse <$> get' (DropletBackupsEndpoint id')++getDropletActions :: DropletId -> DO [Action]+getDropletActions id' = unResponse <$> get' (DropletActionsEndpoint id')++deleteDroplet :: DropletId -> DO ()+deleteDroplet id' = delete' (DropletEndpoint id')++deleteDropletByTag :: String -> DO ()+deleteDropletByTag tag = delete DropletsEndpoint (Just [("tag_name", tag)]) EmptyPayload++getDropletNeighbors :: DropletId -> DO [Droplet]+getDropletNeighbors id' = unResponse <$> get' (DropletNeighborsEndpoint id')++getNeighbors :: DO Neighbors+getNeighbors = unResponse <$> get' DropletsNeighborsEndpoint++performDropletAction :: DropletId -> DropletAction -> DO Action+performDropletAction id' = fmap unResponse . post (DropletActionsEndpoint id') Nothing ++{- Warning! Currently has issue with the response format, see:+ - https://github.com/digitalocean/api-v2/issues/164+ -}+performDropletActionOnTag :: String -> DropletAction -> DO [Action]+performDropletActionOnTag tag action = do+  unless (actionAllowedAsBulk action) $+    throwError $+      "Action " ++ show action ++ " not allowed as bulk. See \+      \https://developers.digitalocean.com/documentation/v2/#acting-on-tagged-droplets"+  unResponse <$> post DropletsActionsEndpoint (Just [("tag_name", tag)]) action++getDropletAction :: DropletId -> ActionId -> DO Action+getDropletAction dropletId actionId = unResponse <$> get' (DropletActionEndpoint dropletId actionId)++-- * Floating IPs+getFloatingIps :: DO [FloatingIp]+getFloatingIps = unResponse <$> get' FloatingIpsEndpoint++createFloatingIp :: FloatingIpPayload -> DO FloatingIp+createFloatingIp = fmap unResponse . post FloatingIpsEndpoint Nothing++getFloatingIp :: IpAddress -> DO FloatingIp+getFloatingIp ip = unResponse <$> get' (FloatingIpEndpoint ip)++deleteFloatingIp :: IpAddress -> DO ()+deleteFloatingIp ip = delete' (FloatingIpEndpoint ip)++performFloatingIpAction :: IpAddress -> FloatingIpAction -> DO Action+performFloatingIpAction ip = fmap unResponse . post (FloatingIpActionsEndpoint ip) Nothing++getFloatingIpActions :: IpAddress -> DO [Action]+getFloatingIpActions ip = unResponse <$> get' (FloatingIpActionsEndpoint ip)++getFloatingIpAction :: IpAddress -> ActionId -> DO Action+getFloatingIpAction ip aId = unResponse <$> get' (FloatingIpActionEndpoint ip aId)++-- * Firewalls+createFirewall :: FirewallPayload -> DO Firewall+createFirewall = fmap unResponse . post FirewallsEndpoint Nothing++getFirewall :: FirewallId -> DO Firewall+getFirewall id' = unResponse <$> get' (FirewallEndpoint id')++getFirewalls :: DO [Firewall]+getFirewalls = unResponse <$> get' FirewallsEndpoint++updateFirewall :: FirewallId -> FirewallPayload -> DO Firewall+updateFirewall id' = fmap unResponse . put (FirewallEndpoint id') Nothing++deleteFirewall :: FirewallId -> DO ()+deleteFirewall id' = delete' (FirewallEndpoint id')++addDropletsToFirewall :: FirewallId -> DropletsPayload -> DO ()+addDropletsToFirewall id' = fmap unResponse . post (FirewallDropletsEndpoint id') Nothing++removeDropletsFromFirewall :: FirewallId -> DropletsPayload -> DO ()+removeDropletsFromFirewall id' = delete (FirewallDropletsEndpoint id') Nothing++addTagsToFirewall :: FirewallId -> TagsPayload -> DO ()+addTagsToFirewall id' = fmap unResponse . post (FirewallTagsEndpoint id') Nothing++removeTagsFromFirewall :: FirewallId -> TagsPayload -> DO ()+removeTagsFromFirewall id' = delete (FirewallTagsEndpoint id') Nothing++addRulesToFirewall :: FirewallId -> FirewallRulesPayload -> DO ()+addRulesToFirewall id' = fmap unResponse . post (FirewallRulesEndpoint id') Nothing++removeRulesFromFirewall :: FirewallId -> FirewallRulesPayload -> DO ()+removeRulesFromFirewall id' = delete (FirewallRulesEndpoint id') Nothing++-- * Load Balancers+createLoadBalancer :: LoadBalancerPayload -> DO LoadBalancer+createLoadBalancer = fmap unResponse . post LoadBalancersEndpoint Nothing++getLoadBalancer :: LoadBalancerId -> DO LoadBalancer+getLoadBalancer id' = unResponse <$> get' (LoadBalancerEndpoint id')++getLoadBalancers :: DO [LoadBalancer]+getLoadBalancers = unResponse <$> get' LoadBalancersEndpoint++updateLoadBalancer :: LoadBalancerPayload -> DO LoadBalancer+updateLoadBalancer = fmap unResponse . put LoadBalancersEndpoint Nothing++deleteLoadBalancer :: LoadBalancerId -> DO ()+deleteLoadBalancer id' = delete' (LoadBalancerEndpoint id')++addDropletsToLoadBalancer :: LoadBalancerId -> DropletsPayload -> DO ()+addDropletsToLoadBalancer id' = post (LoadBalancerDropletsEndpoint id') Nothing++removeDropletsFromLoadBalancer :: LoadBalancerId -> DropletsPayload -> DO ()+removeDropletsFromLoadBalancer id' = delete (LoadBalancerDropletsEndpoint id') Nothing++addForwardingRulesToLoadBalancer :: LoadBalancerId -> [ForwardingRule] -> DO ()+addForwardingRulesToLoadBalancer id' = post (LoadBalancerForwardingRulesEndpoint id') Nothing++removeForwardingRulesFromLoadBalancer :: LoadBalancerId -> [ForwardingRule] -> DO ()+removeForwardingRulesFromLoadBalancer id' = delete (LoadBalancerForwardingRulesEndpoint id') Nothing++-- * SSH Keys+getSSHKeys :: DO [SSHKey]+getSSHKeys = unResponse <$> get' SSHKeysEndpoint++createSSHKey :: SSHKeyPayload -> DO SSHKey+createSSHKey = fmap unResponse . post SSHKeysEndpoint Nothing++getSSHKey :: Either SSHKeyId String -> DO SSHKey+getSSHKey idn = get' $ case idn of+  Left id' -> SSHKeyEndpoint id'+  Right fingerprint -> SSHKeyWithFingerprintEndpoint fingerprint++updateSSHKey :: Either SSHKeyId String -> String -> DO SSHKey+updateSSHKey idn name =+  unResponse <$>+    put (either SSHKeyEndpoint SSHKeyWithFingerprintEndpoint idn) Nothing (SSHKeyNamePayload name)++destroySSHKey :: Either SSHKeyId String -> DO ()+destroySSHKey = delete' . either SSHKeyEndpoint SSHKeyWithFingerprintEndpoint
+ src/Network/DigitalOcean/Http.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RankNTypes #-}++module Network.DigitalOcean.Http where+  +-----------------------------------------------------------------+import qualified Data.Text                 as T+import qualified Data.ByteString           as BS+import qualified Data.ByteString.Char8     as BSC+import qualified Data.ByteString.Lazy      as LBS+import           Network.HTTP.Client       hiding (Proxy)+import           Network.HTTP.Client.TLS+import           System.FilePath           ((</>))+import           Data.Aeson+import           Data.Proxy+import           Data.Monoid+import           Data.Bool                 (bool)+import           Control.Monad+import           Control.Monad.Reader+import           Control.Monad.Except+import           Data.Maybe+import           Network.HTTP.Types.Status (statusCode)+import           Text.URI+-----------------------------------------------------------------+import           Network.DigitalOcean.Types+import           Network.DigitalOcean.Utils.Pagination+-----------------------------------------------------------------++baseURI :: String+baseURI = "https://api.digitalocean.com/v2"++type ReqWithPayload a = forall p. (Payload p) => Endpoint -> Maybe QueryParams -> p -> DO a+type ReqWithoutPayload a = Endpoint -> Maybe QueryParams -> DO a++makeRequest :: forall proxy p a. (FromJSON a, Payload p) => RequestMethod -> String -> Maybe QueryParams -> Maybe p -> DO a+makeRequest method uri queryParams mbPayload = do+  client <- ask+  let uri' = uri <> maybe mempty showQueryParams queryParams+  when (isNothing $ parseURI uri') $ throwError $ "URI cannot be parsed: " <> uri'+  manager <- liftIO newTlsManager+  initialRequest <- liftIO $ parseRequest uri'+  let request = initialRequest { method = BSC.pack $ show method+                               , requestHeaders = [ ("Authorization", "Bearer " `BS.append` apiKey client)+                                                  , ("Content-Type", "application/json")+                                                  ]+                               }+  let request' = maybe request (\payload -> request { requestBody = RequestBodyLBS (encode payload) }) mbPayload+  response <- liftIO $ httpLbs request' manager+  let respStatus = statusCode $ responseStatus response+  when (respStatus < 200 || respStatus > 300) $ throwError $ "Non-success response: " <> show respStatus <> "Body:" <> show (responseBody response)+  let body = bool (responseBody response) "[]" (respStatus == 204)+  case (eitherDecode body :: Either String a) of+    Left err -> throwError $ "Error occured for response body:" <> BSC.unpack (LBS.toStrict $ responseBody response) <> err+    Right resource -> return resource++{- `get` that is working with absolute uris -}+getA :: forall a. FromJSON a => String -> Maybe QueryParams -> DO a+getA uri queryParams = makeRequest Get uri queryParams (Just EmptyPayload)++get :: FromJSON a => ReqWithoutPayload a+get endp = getA (baseURI </> show endp)++get' :: FromJSON a => Endpoint -> DO a+get' endp = get endp Nothing++post :: forall a. (FromJSON a) => ReqWithPayload a+post endp q' payload = makeRequest Post (baseURI </> show endp) q' (Just payload)++delete :: ReqWithPayload ()+delete endp q' payload = makeRequest Delete (baseURI </> show endp) q' (Just payload)++delete' :: Endpoint -> DO ()+delete' endp = delete endp Nothing EmptyPayload++put :: forall a. (FromJSON a) => ReqWithPayload a+put endp q' payload = makeRequest Put (baseURI </> show endp) q' (Just payload)++getPaginated :: forall a. Paginatable a => Maybe PaginationConfig -> ReqWithoutPayload [a]+getPaginated config endp q' = do+  let queryParams = paginationQueryParams config +++                    fromMaybe [] q'+  pagination <- get endp (Just queryParams)+  curr <$> paginateUntil (fromMaybe defaultPaginationConfig config) pagination (\url -> getA (addPaginationParam url) Nothing)+  where+    addPaginationParam = (<> maybe mempty (<> "&page_size=") (show . pageSize <$> config))
+ src/Network/DigitalOcean/Services.hs view
@@ -0,0 +1,33 @@+module Network.DigitalOcean.Services (+    module Network.DigitalOcean.Services.Region+  , module Network.DigitalOcean.Services.Action+  , module Network.DigitalOcean.Services.Account+  , module Network.DigitalOcean.Services.Volume+  , module Network.DigitalOcean.Services.Snapshot+  , module Network.DigitalOcean.Services.Certificate+  , module Network.DigitalOcean.Services.Domain+  , module Network.DigitalOcean.Services.DomainRecord+  , module Network.DigitalOcean.Services.Image+  , module Network.DigitalOcean.Services.Size+  , module Network.DigitalOcean.Services.Droplet+  , module Network.DigitalOcean.Services.FloatingIp+  , module Network.DigitalOcean.Services.Firewall+  , module Network.DigitalOcean.Services.LoadBalancer+  , module Network.DigitalOcean.Services.SSHKey+) where++import Network.DigitalOcean.Services.Region+import Network.DigitalOcean.Services.Action+import Network.DigitalOcean.Services.Account+import Network.DigitalOcean.Services.Volume+import Network.DigitalOcean.Services.Snapshot+import Network.DigitalOcean.Services.Certificate+import Network.DigitalOcean.Services.Domain+import Network.DigitalOcean.Services.DomainRecord+import Network.DigitalOcean.Services.Image+import Network.DigitalOcean.Services.Size+import Network.DigitalOcean.Services.Droplet+import Network.DigitalOcean.Services.FloatingIp+import Network.DigitalOcean.Services.Firewall+import Network.DigitalOcean.Services.LoadBalancer+import Network.DigitalOcean.Services.SSHKey
+ src/Network/DigitalOcean/Services/Account.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.DigitalOcean.Services.Account where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+-----------------------------------------------------------------++data Account = Account+  { accountDropletLimit    :: Int+  , accountFloatingIpLimit :: Int+  , accountEmail           :: String+  , accountUuid            :: String+  , accountEmailVerified   :: Bool+  , accountStatus          :: String+  , accountStatusMessage   :: String+  } deriving (Show, Generic)++instance FromJSON (Response Account) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "account")++instance FromJSON Account where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase
+ src/Network/DigitalOcean/Services/Action.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DeriveGeneric #-}++module Network.DigitalOcean.Services.Action where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        Data.Time.Clock+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+import        Network.DigitalOcean.Utils.Pagination+-----------------------------------------------------------------++data Action = Action+  { actionId            :: ActionId+  , actionStatus        :: String -- TODO: Make a type+  , actionType          :: String+  , actionStartedAt     :: UTCTime+  , actionCompletedAt   :: Maybe UTCTime+  , actionResourceId    :: Int+  , actionResourceType  :: String -- TODO: Make a type+  , actionRegionSlug    :: Maybe String+  } deriving (Show, Generic)++instance FromJSON (Response Action) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "action")++instance FromJSON (Response [Action]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "actions")++instance FromJSON Action where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance FromJSON (PaginationState Action) where+  parseJSON (Object v) = parsePaginationState v "actions"++instance Paginatable Action where
+ src/Network/DigitalOcean/Services/Certificate.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}++module Network.DigitalOcean.Services.Certificate where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        Data.Time.Clock+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+import        Network.DigitalOcean.Utils.Pagination+import        Network.DigitalOcean.Services.Action+-----------------------------------------------------------------++data Certificate = Certificate+  { certificateId               :: CertificateId+  , certificateName             :: String+  , certificateNotAfter         :: String+  , certificateSha1Fingerprint  :: String+  , certificateCreatedAt        :: UTCTime+  } deriving (Show, Generic)++instance FromJSON (Response Certificate) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "certificate")++instance FromJSON (Response [Certificate]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "certificates")++instance FromJSON Certificate where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance FromJSON (PaginationState Certificate) where+  parseJSON (Object v) = parsePaginationState v "certificates"++instance Paginatable Certificate where++data Certificatepayload = Certificatepayload+  { certificatepayloadName :: String+  , certificatepayloadPrivateKey :: String+  , certificatepayloadLeafCertificate :: String+  , certificatepayloadCertificateChain :: Maybe String+  } deriving (Show, Generic)++instance ToJSON Certificatepayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload Certificatepayload
+ src/Network/DigitalOcean/Services/Domain.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}++module Network.DigitalOcean.Services.Domain where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        Data.Time.Clock+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+import        Network.DigitalOcean.Utils.Pagination+import        Network.DigitalOcean.Services.Action+-----------------------------------------------------------------++data Domain = Domain+  { domainName     :: String+  , domainTtl      :: String+  , domainZoneFile :: String+  } deriving (Show, Generic)++instance FromJSON (Response Domain) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "domain")++instance FromJSON (Response [Domain]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "domains")++instance FromJSON Domain where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance FromJSON (PaginationState Domain) where+  parseJSON (Object v) = parsePaginationState v "domains"++instance Paginatable Domain++data DomainPayload = DomainPayload+  { domainpayloadName      :: String+  , domainpayloadIpAddress :: String+  } deriving (Show, Generic)++instance ToJSON DomainPayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload DomainPayload
+ src/Network/DigitalOcean/Services/DomainRecord.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}++module Network.DigitalOcean.Services.DomainRecord where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+import        Network.DigitalOcean.Utils.Pagination+import        Network.DigitalOcean.Services.Action+-----------------------------------------------------------------++data DomainRecord = DomainRecord+  { domainrecordId       :: Int+  , domainrecordType     :: String+  , domainrecordName     :: String+  , domainrecordData     :: String+  , domainrecordPriority :: Maybe Int+  , domainrecordPort     :: Maybe Int+  , domainrecordTtl      :: Int+  , domainrecordWeight   :: Maybe Int+  } deriving (Show, Generic)++instance FromJSON (Response DomainRecord) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "domain_record")++instance FromJSON (Response [DomainRecord]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "domain_records")++instance FromJSON DomainRecord where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance FromJSON (PaginationState DomainRecord) where+  parseJSON (Object v) = parsePaginationState v "domain_records"++instance Paginatable DomainRecord++data DomainRecordPayload = DomainRecordPayload+  { domainrecordpayloadType     :: String+  , domainrecordpayloadName     :: Maybe String+  , domainrecordpayloadData     :: Maybe String+  , domainrecordpayloadPriority :: Maybe Int+  , domainrecordpayloadPort     :: Maybe Int+  , domainrecordpayloadTtl      :: Maybe Int+  , domainrecordpayloadWeight   :: Maybe Int+  } deriving (Show, Generic)++instance ToJSON DomainRecordPayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload DomainRecordPayload+
+ src/Network/DigitalOcean/Services/Droplet.hs view
@@ -0,0 +1,226 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}++module Network.DigitalOcean.Services.Droplet where++-----------------------------------------------------------------+import           Data.Aeson+import           Data.Aeson.Casing+import           GHC.Generics+import           Data.Time.Clock+import           Data.Map+import qualified Data.HashMap.Lazy as HML+-----------------------------------------------------------------+import           Network.DigitalOcean.Types+import           Network.DigitalOcean.Utils.Pagination+import           Network.DigitalOcean.Utils.Actions+import           Network.DigitalOcean.Services.Region+import           Network.DigitalOcean.Services.Image+import           Network.DigitalOcean.Services.Size+-----------------------------------------------------------------++data Kernel = Kernel+  { kernelId :: Int+  , kernelName :: String+  , kernelVersion :: String+  } deriving (Show, Generic)++instance FromJSON Kernel where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance FromJSON (Response [Kernel]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "kernels")++-----------------------------------------------------------------++data DropletStatus =+    New+  | Active+  | Off+  | Archive+  deriving (Show, Generic)++instance FromJSON DropletStatus where+  parseJSON "new"     = return New+  parseJSON "active"  = return Active+  parseJSON "off"     = return Off+  parseJSON "archive" = return Archive++-----------------------------------------------------------------++data Network = Network+  { networkIpAddress :: IpAddress+  , networkNetmask   :: IpAddress+  , networkGateway   :: IpAddress+  , networkType      :: String -- ^ Whether there is another type of network is available is not documented in DO API.+  } deriving (Show, Generic)++instance FromJSON Network where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++data Networks = Networks+  { v4 :: [Network]+  , v6 :: [Network]+  } deriving (Show)++instance FromJSON Networks where+  parseJSON (Object v) = Networks+    <$> v .: "v4"+    <*> v .: "v6"++-----------------------------------------------------------------++newtype Backup = Backup Image+  deriving Show++instance FromJSON Backup where+  parseJSON v = Backup <$> parseJSON v++instance FromJSON (Response [Backup]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "backups")+  +-----------------------------------------------------------------++newtype Neighbors = Neighbors [[Droplet]]+  deriving Show++instance FromJSON Neighbors where+  parseJSON v = Neighbors <$> parseJSON v++instance FromJSON (Response Neighbors) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "neighbors")++-----------------------------------------------------------------++data Droplet = Droplet+  { dropletId          :: DropletId+  , dropletName        :: String+  , dropletMemory      :: Int+  , dropletVcpus       :: Int+  , dropletDisk        :: Int+  , dropletLocked      :: Bool+  , dropletCreatedAt   :: UTCTime+  , dropletStatus      :: DropletStatus+  , dropletBackupIds   :: [Int]+  , dropletSnapshotIds :: [SnapshotId]+  , dropletFeatures    :: [String]+  , dropletRegion      :: Region+  , dropletImage       :: Image+  , dropletSize        :: Size+  , dropletSizeSlug    :: String+  , dropletNetworks    :: Networks+  , dropletKernel      :: Maybe Kernel+  --droplet , nextBackupWindow :: Maybe BackupWindow -- No example for this yet+  , dropletTags        :: [String]+  , dropletVolumeIds   :: Maybe [VolumeId]+  } deriving (Show, Generic)++instance FromJSON (Response Droplet) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "droplet")++instance FromJSON (Response [Droplet]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "droplets")++instance FromJSON Droplet where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance FromJSON (PaginationState Droplet) where+  parseJSON (Object v) = parsePaginationState v "droplets"++instance Paginatable Droplet++data DropletPayload =+    SingleDropletPayload String IDropletPayload+  | MultipleDropletPayload [String] IDropletPayload++{- https://developers.digitalocean.com/documentation/v2/#create-a-new-droplet -}+data IDropletPayload = IDropletPayload+  { dropletpayloadRegion            :: String+  , dropletpayloadSize              :: String+  , dropletpayloadImage             :: String -- ^ number (if using an image ID), or String (if using a public image slug)+  , dropletpayloadSshKeys           :: Maybe [String]+  , dropletpayloadBackups           :: Maybe Bool+  , dropletpayloadIpv6              :: Maybe Bool+  , dropletpayloadPrivateNetworking :: Maybe Bool+  , dropletpayloadUserData          :: Maybe String+  , dropletpayloadMonitoring        :: Maybe Bool+  , dropletpayloadVolumes           :: Maybe [String]+  , dropletpayloadTags              :: Maybe [String]+  } deriving (Show, Generic)++instance ToJSON IDropletPayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++mergeAeson :: Value -> Value -> Value+mergeAeson (Object x) (Object y) = Object $ HML.unions [x, y]++instance ToJSON DropletPayload where+  toJSON (SingleDropletPayload name payload) =+    object [ "name" .= name ] `mergeAeson` toJSON payload+  toJSON (MultipleDropletPayload names payload) =+    object [ "names" .= names ] `mergeAeson` toJSON payload++instance Payload DropletPayload++-----------------------------------------------------------------++data DropletAction =+    EnableBackups+  | DisableBackups+  | Reboot+  | PowerCycle+  | Shutdown+  | PowerOff+  | PowerOn+  | Restore String+  | PasswordReset+  | ResizeDroplet (Maybe Bool) String+  | Rebuild String+  | Rename String+  | ChangeKernel Int+  | EnableIpV6+  | EnablePrivateNetworking+  | TakeSnapshot (Maybe String)+  deriving (Eq, Show)++{- Reference:+ - https://developers.digitalocean.com/documentation/v2/#acting-on-tagged-droplets+ -}+actionAllowedAsBulk :: DropletAction -> Bool+actionAllowedAsBulk PowerCycle              = True+actionAllowedAsBulk PowerOn                 = True+actionAllowedAsBulk PowerOff                = True+actionAllowedAsBulk Shutdown                = True+actionAllowedAsBulk EnablePrivateNetworking = True+actionAllowedAsBulk EnableIpV6              = True+actionAllowedAsBulk EnableBackups           = True+actionAllowedAsBulk DisableBackups          = True+actionAllowedAsBulk TakeSnapshot {}         = True+actionAllowedAsBulk _                       = False++instance ToJSON DropletAction where+  toJSON EnableBackups             = object [ actionType' "enable_backups" ]+  toJSON DisableBackups            = object [ actionType' "disable_backups" ]+  toJSON Reboot                    = object [ actionType' "reboot" ]+  toJSON PowerCycle                = object [ actionType' "power_cycle" ]+  toJSON Shutdown                  = object [ actionType' "shutdown" ]+  toJSON PowerOff                  = object [ actionType' "power_off" ]+  toJSON PowerOn                   = object [ actionType' "power_on" ]+  toJSON (Restore imgId)           = object [ actionType' "restore", "image" .= imgId ]+  toJSON PasswordReset             = object [ actionType' "password_reset" ]+  toJSON (ResizeDroplet disk size) = object [ actionType' "resize", "disk" .= disk, "size" .= size ]+  toJSON (Rebuild imgId)           = object [ actionType' "rebuild", "image" .= imgId ]+  toJSON (Rename name)             = object [ actionType' "rename", "name" .= name ]+  toJSON (ChangeKernel id')        = object [ actionType' "change_kernel", "kernel" .= id' ]+  toJSON EnableIpV6                = object [ actionType' "enable_ipv6" ]+  toJSON EnablePrivateNetworking   = object [ actionType' "enable_private_networking" ]+  toJSON (TakeSnapshot name)       = object [ actionType' "snapshot", "name" .= name ]++instance Payload DropletAction+
+ src/Network/DigitalOcean/Services/Firewall.hs view
@@ -0,0 +1,162 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}++module Network.DigitalOcean.Services.Firewall where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        Data.Time.Clock+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+import        Network.DigitalOcean.Utils.Pagination+-----------------------------------------------------------------++data PendingChange = PendingChange+  { pendingchangeDropletId :: DropletId+  , pendingchangeRemoving :: Bool+  , pendingchangeStatus :: String+  } deriving (Show, Generic)++instance FromJSON PendingChange where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++-----------------------------------------------------------------++data Protocol =+    Tcp+  | Udp+  | Icmp+  deriving Show++instance FromJSON Protocol where+  parseJSON (String s) = return $+    case s of+      "tcp"  -> Tcp+      "udp"  -> Udp+      "icmp" -> Icmp++instance ToJSON Protocol where+  toJSON Tcp = String "tcp"+  toJSON Udp = String "udp"+  toJSON Icmp = String "icmp"++-----------------------------------------------------------------++data Channel = Channel+  { channelAddresses        :: Maybe [String]+  , channelDropletIds       :: Maybe [DropletId]+  , channelLoadBalancerUids :: Maybe [String]+  , channelTags             :: Maybe [String]+  } deriving (Show, Generic)++instance FromJSON Channel where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance ToJSON Channel where+  toJSON = genericToJSON $ aesonPrefix snakeCase++data InboundRule = InboundRule+  { inboundruleProtocol :: Protocol+  , inboundrulePorts :: String+  , inboundruleSources :: Channel+  } deriving (Show, Generic)++instance FromJSON InboundRule where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance ToJSON InboundRule where+  toJSON = genericToJSON $ aesonPrefix snakeCase++data OutboundRule = OutboundRule+  { outboundruleProtocol     :: Protocol+  , outboundrulePorts        :: String+  , outboundruleDestinations :: Channel+  } deriving (Show, Generic)++instance FromJSON OutboundRule where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance ToJSON OutboundRule where+  toJSON = genericToJSON $ aesonPrefix snakeCase++-----------------------------------------------------------------++data Firewall = Firewall+  { firewallId             :: FirewallId+  , firewallStatus         :: String+  , firewallCreatedAt      :: UTCTime+  , firewallPendingChanges :: [PendingChange]+  , firewallName           :: String+  , firewallInboundRules   :: [InboundRule]+  , firewallOutboundRules  :: [OutboundRule]+  , firewallDropletIds     :: [DropletId]+  , firewallTags           :: [String]+  } deriving (Show, Generic)++instance FromJSON (Response Firewall) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "firewall")++instance FromJSON (Response [Firewall]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "firewalls")++instance FromJSON Firewall where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance FromJSON (PaginationState Firewall) where+  parseJSON (Object v) = parsePaginationState v "domains"++instance Paginatable Firewall++-----------------------------------------------------------------++data FirewallPayload = FirewallPayload+  { firewallpayloadName          :: String+  , firewallpayloadInboundRules  :: [InboundRule]+  , firewallpayloadOutboundRules :: [OutboundRule]+  , firewallpayloadDropletIds    :: Maybe [DropletId]+  , firewallpayloadTags          :: Maybe [String]+  } deriving (Show, Generic)++instance ToJSON FirewallPayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload FirewallPayload++-----------------------------------------------------------------++data DropletsPayload = DropletsPayload+  { dropletspayloadDropletIds :: [DropletId]+  } deriving (Show, Generic)++instance ToJSON DropletsPayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload DropletsPayload++-----------------------------------------------------------------++data TagsPayload = TagsPayload+  { tagspayloadTags :: [DropletId]+  } deriving (Show, Generic)++instance ToJSON TagsPayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload TagsPayload++-----------------------------------------------------------------++data FirewallRulesPayload = FirewallRulesPayload+  { inboundRules :: [InboundRule]+  , outboundRules :: [OutboundRule]+  } deriving (Show, Generic)++instance ToJSON FirewallRulesPayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload FirewallRulesPayload
+ src/Network/DigitalOcean/Services/FloatingIp.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}++module Network.DigitalOcean.Services.FloatingIp where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        Data.Time.Clock+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+import        Network.DigitalOcean.Utils.Pagination+import        Network.DigitalOcean.Utils.Actions+import        Network.DigitalOcean.Services.Region+import        Network.DigitalOcean.Services.Droplet+-----------------------------------------------------------------++data FloatingIp = FloatingIp+  { floatingipIp      :: IpAddress+  , floatingipRegion  :: Region+  , floatingipDroplet :: Droplet+  } deriving (Show, Generic)++instance FromJSON (Response FloatingIp) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "floating_ip")++instance FromJSON (Response [FloatingIp]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "floating_ips")++instance FromJSON FloatingIp where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance FromJSON (PaginationState FloatingIp) where+  parseJSON (Object v) = parsePaginationState v "floating_ips"++instance Paginatable FloatingIp++data FloatingIpPayload =+    FloatingIpRegionPayload RegionSlug+  | FloatingIpDropletPayload DropletId+  deriving Show++instance ToJSON FloatingIpPayload where+  toJSON (FloatingIpRegionPayload regionSlug) = object [ "region" .= regionSlug ]+  toJSON (FloatingIpDropletPayload dropletId) = object [ "droplet_id" .= dropletId ]++instance Payload FloatingIpPayload++data FloatingIpAction =+    AssignToDroplet DropletId+  | Unassign+  deriving Show++instance ToJSON FloatingIpAction where+  toJSON (AssignToDroplet dropletId) = object [ actionType' "assign", "droplet_id" .= dropletId ]+  toJSON Unassign = object [ actionType' "unassign" ]++instance Payload FloatingIpAction
+ src/Network/DigitalOcean/Services/Image.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}++module Network.DigitalOcean.Services.Image where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        Data.Time.Clock+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+import        Network.DigitalOcean.Utils.Pagination+import        Network.DigitalOcean.Utils.Actions+-----------------------------------------------------------------++data Image = Image+  { imageId            :: ImageId+  , imageName          :: String+  , imageType          :: String+  , imageDistribution  :: String+  , imageSlug          :: Maybe String+  , imagePublic        :: Bool+  , imageRegions       :: [String]+  , imageMinDiskSize   :: Int+  , imageSizeGigabytes :: Double+  , imageCreatedAt     :: UTCTime+  } deriving (Show, Generic)++instance FromJSON (Response Image) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "image")++instance FromJSON (Response [Image]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "images")++instance FromJSON Image where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance FromJSON (PaginationState Image) where+  parseJSON (Object v) = parsePaginationState v "images"++instance Paginatable Image++data ImageType =+  DistributionImage+  | ApplicationImage++instance Show ImageType where+  show DistributionImage = "distribution"+  show ApplicationImage = "application"++data ImageOptions = ImageOptions+  { imageType' :: Maybe ImageType -- ^ Reference: https://developers.digitalocean.com/documentation/v2/#images+  , isPrivate  :: Bool            -- ^ If True, only user's images will be returned.+  }++data ImagePayload = ImagePayload+  { imagepayloadName :: String+  } deriving (Show, Generic)++instance Payload ImagePayload++instance ToJSON ImagePayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++data ImageAction =+    Transfer+  | ConvertToSnapshot++instance ToJSON ImageAction where+  toJSON Transfer = object [ actionType' "transfer" ]+  toJSON ConvertToSnapshot = object [ actionType' "convert" ]++instance Payload ImageAction
+ src/Network/DigitalOcean/Services/LoadBalancer.hs view
@@ -0,0 +1,134 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}++module Network.DigitalOcean.Services.LoadBalancer where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        Data.Time.Clock+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+import        Network.DigitalOcean.Utils.Pagination+import        Network.DigitalOcean.Services.Region+-----------------------------------------------------------------++data LoadBalancerAlgorithm =+    RoundRobin+  | LeastConnections+  deriving (Show, Generic)++instance FromJSON LoadBalancerAlgorithm where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance ToJSON LoadBalancerAlgorithm where+  toJSON = genericToJSON $ aesonPrefix snakeCase++-----------------------------------------------------------------++data ForwardingRule = ForwardingRule+  { forwardingrulesEntryProtocol  :: String+  , forwardingrulesEntryPort      :: Int+  , forwardingrulesTargetProtocol :: String+  , forwardingrulesTargetPort     :: Int+  , forwardingrulesCertificateId  :: Maybe String+  , forwardingrulesTlsPassthrough :: Maybe Bool+  } deriving (Show, Generic)++instance FromJSON ForwardingRule where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance ToJSON ForwardingRule where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload [ForwardingRule]++-----------------------------------------------------------------++data HealthCheck = HealthCheck+  { healthcheckProtocol               :: String+  , healthcheckPort                   :: Int+  , healthcheckPath                   :: Maybe String+  , healthcheckCheckIntervalSeconds   :: Maybe Int+  , healthcheckResponseTimeoutSeconds :: Maybe Int+  , healthcheckUnhealthyThreshold     :: Maybe Int+  , healthcheckHealthyThreshold       :: Maybe Int+  } deriving (Show, Generic)++instance FromJSON HealthCheck where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance ToJSON HealthCheck where+  toJSON = genericToJSON $ aesonPrefix snakeCase++-----------------------------------------------------------------++data StickySessions = StickySessions+  { stickysessionsType             :: String+  , stickysessionsCookieName       :: String+  , stickysessionsCookieTtlSeconds :: String+  } deriving (Show, Generic)++instance FromJSON StickySessions where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance ToJSON StickySessions where+  toJSON = genericToJSON $ aesonPrefix snakeCase++-----------------------------------------------------------------++data LoadBalancer = LoadBalancer+  { loadbalancerId                  :: String+  , loadbalancerName                :: String+  , loadbalancerIp                  :: IpAddress+  , loadbalancerAlgorithm           :: LoadBalancerAlgorithm+  , loadbalancerStatus              :: String -- Make this a new type+  , loadbalancerCreatedAt           :: UTCTime+  , loadbalancerForwardingRules     :: [ForwardingRule]+  , loadbalancerHealthCheck         :: HealthCheck+  , loadbalancerStickySessions      :: StickySessions+  , loadbalancerRegion              :: Region+  , loadbalancerTag                 :: String+  , loadbalancerDropletIds          :: [DropletId]+  , loadbalancerRedirectHttpToHttps :: Bool+  } deriving (Show, Generic)++instance FromJSON LoadBalancer where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance ToJSON LoadBalancer where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance FromJSON (Response LoadBalancer) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "load_balancer")++instance FromJSON (Response [LoadBalancer]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "load_balancers")++instance FromJSON (PaginationState LoadBalancer) where+  parseJSON (Object v) = parsePaginationState v "load_balancers"++instance Paginatable LoadBalancer++-----------------------------------------------------------------++data LoadBalancerPayload = LoadBalancerPayload+  { loadbalancerpayloadName                :: String+  , loadbalancerpayloadAlgorithm           :: Maybe String+  , loadbalancerpayloadRegion              :: String+  , loadbalancerpayloadForwardingRules     :: [ForwardingRule]+  , loadbalancerpayloadHealthCheck         :: Maybe HealthCheck+  , loadbalancerpayloadStickySessions      :: Maybe StickySessions+  , loadbalancerpayloadRedirectHttpToHttps :: Maybe Bool+  , loadbalancerpayloadDropletIds          :: Maybe [Int]+  , loadbalancerpayloadTag                 :: String -- https://developers.digitalocean.com/documentation/v2/#create-a-new-load-balancer-with-droplet-tag+  } deriving (Show, Generic)++instance ToJSON LoadBalancerPayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload LoadBalancerPayload
+ src/Network/DigitalOcean/Services/Region.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DeriveGeneric #-}++module Network.DigitalOcean.Services.Region where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+-----------------------------------------------------------------++data Region = Region+  { regionSlug      :: String+  , regionName      :: String+  , regionSizes     :: [String] -- TODO: Make a type+  , regionAvailable :: Bool+  , regionFeatures  :: [String] -- TODO: Make a type+  } deriving (Show, Generic)++instance FromJSON (Response [Region]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "regions")++instance FromJSON Region where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance ToJSON Region where+  toJSON = genericToJSON $ aesonPrefix snakeCase
+ src/Network/DigitalOcean/Services/SSHKey.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DeriveGeneric #-}++module Network.DigitalOcean.Services.SSHKey where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+-----------------------------------------------------------------++data SSHKey = SSHKey+  { sshkeyId          :: SSHKeyId+  , sshkeyFingerprint :: String+  , sshkeyPublicKey   :: String+  , sshkeyName        :: String+  } deriving (Show, Generic)++instance FromJSON SSHKey where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++instance ToJSON SSHKey where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance FromJSON (Response SSHKey) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "ssh_key")++instance FromJSON (Response [SSHKey]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "ssh_keys")++-----------------------------------------------------------------++data SSHKeyPayload = SSHKeyPayload+  { sshkeypayloadName      :: String+  , sshkeypayloadPublicKey :: String+  } deriving (Show, Generic)++instance ToJSON SSHKeyPayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload SSHKeyPayload++-----------------------------------------------------------------++data SSHKeyNamePayload = SSHKeyNamePayload+  { sshkeynamepayloadName :: String+  } deriving (Show, Generic)++instance ToJSON SSHKeyNamePayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload SSHKeyNamePayload
+ src/Network/DigitalOcean/Services/Size.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DeriveGeneric #-}++module Network.DigitalOcean.Services.Size where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+-----------------------------------------------------------------++data Size = Size+  { sizeSlug         :: String+  , sizeAvailable    :: Bool+  , sizeTransfer     :: Double+  , sizePriceMonthly :: Double+  , sizePriceHourly  :: Double+  , sizeMemory       :: Int+  , sizeVcpus        :: Int+  , sizeDisk         :: Int+  -- , sizeRegions      :: [String]+  } deriving (Show, Generic)++instance FromJSON (Response [Size]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "sizes")++instance FromJSON Size where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase+
+ src/Network/DigitalOcean/Services/Snapshot.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DeriveGeneric #-}++module Network.DigitalOcean.Services.Snapshot where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        Data.Time.Clock+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Types+-----------------------------------------------------------------++data Snapshot = Snapshot+  { snapshotId             :: String+  , snapshotName           :: String+  , snapshotCreatedAt      :: UTCTime+  , snapshotRegions        :: [String]+  , snapshotResourceId     :: String+  , snapshotResourceType   :: String+  , snapshotMinDiskSize    :: Double+  , snapshotSizeGigabytes  :: Double+  } deriving (Show, Generic)++instance FromJSON (Response [Snapshot]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "snapshots")++instance FromJSON (Response Snapshot) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "snapshot")++instance FromJSON Snapshot where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++newtype SnapshotPayload = SnapshotPayload+  { snapshotpayloadName :: String+  } deriving (Show, Generic)++instance ToJSON SnapshotPayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload SnapshotPayload where
+ src/Network/DigitalOcean/Services/Volume.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE DeriveGeneric #-}++module Network.DigitalOcean.Services.Volume where++-----------------------------------------------------------------+import        Data.Aeson+import        Data.Aeson.Casing+import        Data.Time.Clock+import        GHC.Generics+-----------------------------------------------------------------+import        Network.DigitalOcean.Services.Region+import        Network.DigitalOcean.Types+-----------------------------------------------------------------++data Volume = Volume+  { volumeId :: String+  , volumeRegion :: Region+  , volumeDropletIds :: [Int]+  , volumeName :: String+  , volumeDescription :: String+  , volumeSizeGigabytes :: Double+  , volumeCreatedAt :: UTCTime+  } deriving (Show, Generic)++instance FromJSON (Response [Volume]) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "volumes")++instance FromJSON (Response Volume) where+  parseJSON (Object v) =+    fmap Response $ parseJSON =<< (v .: "volume")++instance FromJSON Volume where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++data VolumePayload = VolumePayload+  { volumepayloadSizeGigabytes :: Int+  , volumepayloadName          :: String+  , volumepayloadDescripton    :: String+  , volumepayloadRegion        :: String+  , volumepayloadSnapshotId    :: String+  } deriving (Show, Generic)++instance ToJSON VolumePayload where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Payload VolumePayload++type VolumeName = String++data VolumeAction =+    Attach VolumeId DropletId (Maybe RegionSlug)+  | Detach VolumeId DropletId (Maybe RegionSlug)+  | ResizeVolume VolumeId Int (Maybe RegionSlug)+  | AttachByName VolumeName DropletId (Maybe RegionSlug)+  | DetachByName VolumeName DropletId (Maybe RegionSlug)++instance Payload VolumeAction where+instance ToJSON VolumeAction where+  toJSON (Attach _ dropletId region) =+    object [ "droplet_id".= dropletId+           , "region"    .= region+           , "type"      .= ("attach" :: String)+           ]+  toJSON (Detach _ dropletId region) =+    object [ "droplet_id" .= dropletId+           , "region"     .= region+           , "type"       .= ("detach" :: String)+           ]+  toJSON (ResizeVolume _ size region) =+    object [ "size" .= size+           , "region"     .= region+           , "type"       .= ("resize" :: String)+           ]+  toJSON (AttachByName volumeName dropletId region) =+    object [ "droplet_id" .= dropletId+           , "region"     .= region+           , "volume_name".= volumeName+           , "type"       .= ("attach" :: String)+           ]+  toJSON (DetachByName volumeName dropletId region) =+    object [ "droplet_id" .= dropletId+           , "region"     .= region+           , "volume_name".= volumeName+           , "type"       .= ("detach" :: String)+           ]
+ src/Network/DigitalOcean/Types.hs view
@@ -0,0 +1,220 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE LambdaCase #-}++module Network.DigitalOcean.Types where++-----------------------------------------------------------------+import           GHC.Generics+import           Data.Aeson+import           Data.Time.Clock+import           Control.Monad.State+import           Control.Monad.Identity+import           Control.Lens+import           Control.Monad+import           Control.Monad.Reader+import           Control.Monad.Except+import qualified Data.Text                 as T+import qualified Data.ByteString           as BS+import           System.FilePath.Posix     ((</>))+-----------------------------------------------------------------+import           Data.Monoid+import           Data.List+import qualified Data.Set                  as Set+-----------------------------------------------------------------++type QueryParams = [(String, String)]++data RequestMethod =+    Get+  | Post+  | Put+  | Delete++instance Show RequestMethod where+  show Get  = "GET"+  show Post = "POST"+  show Put  = "PUT"+  show Delete  = "DELETE"++showQueryParams :: QueryParams -> String+showQueryParams = \case+  [] -> ""+  ls -> "?" <> (intercalate "&" . map (\(k, v) -> k <> "=" <> v) $ ls)++class (FromJSON a, Show a, FromJSON (PaginationState a)) => Paginatable a where++data Paginatable a => PaginationState a = PaginationState+  { curr     :: [a]+  , page     :: Int+  , nextUrl  :: Maybe String+  , total    :: Maybe Int+  , isLast   :: Bool+  } deriving (Show)++data PaginationConfig = PaginationConfig+  { pageSize :: Int+  , resultLimit :: Int+  }++-- https://developers.digitalocean.com/documentation/v2/#links+defaultPaginationConfig :: PaginationConfig+defaultPaginationConfig = PaginationConfig 25 100++newtype DO a = DO { runDO :: ReaderT Client (ExceptT String IO) a }+  deriving (Functor, Applicative, Monad, MonadIO, MonadError String, MonadReader Client)++newtype Client = Client { apiKey :: BS.ByteString }++type DoErr = T.Text++newtype Response a = Response { unResponse :: a } deriving (Generic, Show)++class (ToJSON a) => Payload a ++data EmptyPayload = EmptyPayload+instance Payload EmptyPayload where+instance ToJSON EmptyPayload where+  toJSON EmptyPayload = object []++instance FromJSON (Response ()) where+  parseJSON _ = return $ Response ()++data Endpoint =+    AccountEndpoint+  | ActionsEndpoint+  | ActionEndpoint ActionId+  | RegionsEndpoint+  | VolumesEndpoint+  | SnapshotsEndpoint+  | DomainsEndpoint+  | ImagesEndpoint+  | SizesEndpoint+  | DropletsEndpoint+  | RecordsEndpoint+  | KernelsEndpoint+  | BackupsEndpoint+  | NeighborsEndpoint+  | FloatingIpsEndpoint+  | DropletsNeighborsEndpoint+  | TagsEndpoint+  | RulesEndpoint+  | FirewallsEndpoint+  | LoadBalancersEndpoint+  | ForwardingRulesEndpoint+  | KeysEndpoint+  | SSHKeysEndpoint+  | VolumeEndpoint VolumeId+  | SnapshotEndpoint SnapshotId+  | VolumeSnapshotsEndpoint VolumeId+  | VolumesActionsEndpoint+  | VolumeActionEndpoint VolumeId ActionId+  | VolumeActionsEndpoint VolumeId+  | CertificateEndpoint CertificateId+  | CertificatesEndpoint+  | DomainEndpoint DomainName+  | DomainRecordsEndpoint DomainName+  | DomainRecordEndpoint DomainName DomainRecordId+  | ImageActionsEndpoint ImageId+  | ImageActionEndpoint ImageId ActionId+  | ImageEndpoint ImageId+  | ImageBySlugEndpoint String+  | DropletEndpoint DropletId+  | DropletKernelsEndpoint DropletId+  | DropletSnapshotsEndpoint DropletId+  | DropletBackupsEndpoint DropletId+  | DropletActionsEndpoint DropletId+  | DropletNeighborsEndpoint DropletId+  | DropletsActionsEndpoint+  | DropletActionEndpoint DropletId ActionId+  | FloatingIpEndpoint IpAddress+  | FloatingIpActionsEndpoint IpAddress+  | FloatingIpActionEndpoint IpAddress ActionId+  | FirewallEndpoint FirewallId+  | FirewallDropletsEndpoint FirewallId+  | FirewallTagsEndpoint FirewallId+  | FirewallRulesEndpoint FirewallId+  | LoadBalancerEndpoint LoadBalancerId+  | LoadBalancerDropletsEndpoint LoadBalancerId+  | LoadBalancerForwardingRulesEndpoint LoadBalancerId+  | SSHKeyEndpoint SSHKeyId+  | SSHKeyWithFingerprintEndpoint String++instance Show Endpoint where+  show AccountEndpoint                           = "account"+  show ActionsEndpoint                           = "actions"+  show RegionsEndpoint                           = "regions"+  show VolumesEndpoint                           = "volumes"+  show SnapshotsEndpoint                         = "snapshots"+  show CertificatesEndpoint                      = "certificates"+  show DomainsEndpoint                           = "domains"+  show ImagesEndpoint                            = "images"+  show SizesEndpoint                             = "sizes"+  show DropletsEndpoint                          = "droplets"+  show RecordsEndpoint                           = "records"+  show KernelsEndpoint                           = "kernels"+  show BackupsEndpoint                           = "backups"+  show NeighborsEndpoint                         = "neighbors"+  show DropletsNeighborsEndpoint                 = "reports/droplet_neighbors"+  show FloatingIpsEndpoint                       = "floating_ips"+  show TagsEndpoint                              = "tags"+  show RulesEndpoint                             = "rules"+  show FirewallsEndpoint                         = "firewalls"+  show LoadBalancersEndpoint                     = "load_balancers"+  show ForwardingRulesEndpoint                   = "forwarding_rules"+  show KeysEndpoint                              = "keys"+  show SSHKeysEndpoint                           = show AccountEndpoint        </> show KeysEndpoint+  show (ActionEndpoint id')                      = show ActionsEndpoint        </> show id'+  show (VolumeEndpoint id')                      = show VolumesEndpoint        </> id'+  show (SnapshotEndpoint id')                    = show SnapshotsEndpoint      </> id'+  show (VolumeSnapshotsEndpoint id')             = show VolumesEndpoint        </> id'                    </> show SnapshotsEndpoint+  show VolumesActionsEndpoint                    = show VolumesEndpoint        </> show SnapshotsEndpoint+  show (VolumeActionsEndpoint vId)               = show VolumesEndpoint        </> vId                    </> show ActionsEndpoint+  show (VolumeActionEndpoint vId aId)            = show VolumesEndpoint        </> vId                    </> show ActionsEndpoint   </> show aId+  show (CertificateEndpoint id')                 = show CertificatesEndpoint   </> id'+  show (DomainEndpoint name')                    = show DomainsEndpoint        </> name'+  show (DomainRecordsEndpoint name')             = show (DomainEndpoint name') </> show RecordsEndpoint+  show (DomainRecordEndpoint d' dr')             = show (DomainEndpoint d')    </> show RecordsEndpoint   </> show dr'+  show (ImageActionsEndpoint id')                = show ImagesEndpoint         </> show id'               </> show ActionsEndpoint+  show (ImageActionEndpoint iId aId)             = show ImagesEndpoint         </> show iId               </> show ActionsEndpoint   </> show aId+  show (ImageEndpoint id')                       = show ImagesEndpoint         </> show id'+  show (ImageBySlugEndpoint name')               = show ImagesEndpoint         </> name'+  show (DropletEndpoint id')                     = show DropletsEndpoint       </> show id'+  show (DropletKernelsEndpoint id')              = show DropletsEndpoint       </> show id'               </> show KernelsEndpoint+  show (DropletSnapshotsEndpoint id')            = show DropletsEndpoint       </> show id'               </> show SnapshotsEndpoint+  show (DropletBackupsEndpoint id')              = show DropletsEndpoint       </> show id'               </> show BackupsEndpoint+  show (DropletActionsEndpoint id')              = show DropletsEndpoint       </> show id'               </> show ActionsEndpoint+  show (DropletNeighborsEndpoint id')            = show DropletsEndpoint       </> show id'               </> show NeighborsEndpoint+  show DropletsActionsEndpoint                   = show DropletsEndpoint       </> show ActionsEndpoint+  show (DropletActionEndpoint dId aId)           = show DropletsEndpoint       </> show dId               </> show ActionsEndpoint   </> show aId+  show (FloatingIpEndpoint ipAddr)               = show FloatingIpsEndpoint    </> ipAddr+  show (FloatingIpActionsEndpoint ipAddr)        = show FloatingIpsEndpoint    </> ipAddr                 </> show ActionsEndpoint+  show (FloatingIpActionEndpoint ipAddr aId)     = show FloatingIpsEndpoint    </> ipAddr                 </> show ActionsEndpoint   </> show aId+  show (FirewallEndpoint id')                    = show FirewallsEndpoint      </> id'+  show (FirewallDropletsEndpoint id')            = show FirewallsEndpoint      </> id'                    </> show DropletsEndpoint+  show (FirewallTagsEndpoint id')                = show FirewallsEndpoint      </> id'                    </> show TagsEndpoint+  show (FirewallRulesEndpoint id')               = show FirewallsEndpoint      </> id'                    </> show RulesEndpoint+  show (LoadBalancerEndpoint id')                = show FirewallsEndpoint      </> id'+  show (LoadBalancerDropletsEndpoint id')        = show FirewallsEndpoint      </> id'                    </> show DropletsEndpoint+  show (LoadBalancerForwardingRulesEndpoint id') = show FirewallsEndpoint      </> id'                    </> show ForwardingRulesEndpoint+  show (SSHKeyEndpoint id')                      = show SSHKeysEndpoint        </> show id'+  show (SSHKeyWithFingerprintEndpoint f)                      = show SSHKeysEndpoint        </> f++type VolumeId       = String+type ActionId       = Int+type CertificateId  = String+type SnapshotId     = String+type DomainName     = String+type DomainRecordId = Int+type DropletId      = Int+type RegionSlug     = String+type ImageId        = Int+type DropletName    = String+type IpAddress      = String+type FirewallId     = String+type LoadBalancerId = String+type SSHKeyId       = Int
+ src/Network/DigitalOcean/Utils/Actions.hs view
@@ -0,0 +1,8 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.DigitalOcean.Utils.Actions (actionType') where++import Data.Aeson++actionType' :: KeyValue kv => String -> kv+actionType' = (.=) "type"
+ src/Network/DigitalOcean/Utils/Pagination.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE TupleSections #-}++module Network.DigitalOcean.Utils.Pagination where++-----------------------------------------------------------------+import           Data.Aeson+import           Data.Aeson.Types+import qualified Data.Text                  as T+import qualified Data.Text                  as T+-----------------------------------------------------------------+import           Network.DigitalOcean.Types+import qualified Data.Set                   as Set+-----------------------------------------------------------------++paginate+  :: (Paginatable a, FromJSON (PaginationState a))+  => (String -> DO (PaginationState a))+  -> PaginationState a+  -> DO (PaginationState a)+paginate f s =+  case nextUrl s of+    Just url -> do+      newState <- f url+      return $ newState { curr = curr s ++ curr newState }+    Nothing -> return s { isLast = True }+    +paginationQueryParams :: Maybe PaginationConfig -> QueryParams+paginationQueryParams Nothing = []+paginationQueryParams (Just (PaginationConfig pageSize _)) = [("per_page", show pageSize)]++paginateUntil+  :: (Paginatable a, FromJSON (PaginationState a))+  => PaginationConfig+  -> PaginationState a+  -> (String -> DO (PaginationState a))+  -> DO (PaginationState a)+paginateUntil config@PaginationConfig {..} state@PaginationState {..} f =+  if length curr >= resultLimit || isLast+    then+      return state+    else do+      newState <- paginate f state +      paginateUntil config newState f++-- Taken from: https://stackoverflow.com/questions/28368980/composing-optional-aeson-parsers+(/?) :: FromJSON a => Parser (Maybe Object) -> T.Text -> Parser (Maybe a)+maybeParser /? key = maybeParser >>= maybe (return Nothing) (.:? key)++parsePaginationState :: Paginatable a => Object -> T.Text -> Parser (PaginationState a)+parsePaginationState v key = do+  values <- v .: key+  (next, total) <- parse_meta+  let page = 1+  return $ PaginationState values page next total False+  where+    parse_meta :: Parser (Maybe String, Maybe Int)+    parse_meta =+      (,)+        <$> v .:? "links" /? "pages" /? "next"+        <*> (v .:? "meta" /? "total")
+ test/Spec.hs view
@@ -0,0 +1,228 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ExtendedDefaultRules #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE NoMonomorphismRestriction #-}++module Main where++import Control.Monad.Reader+import Control.Monad.Except+import Network.DigitalOcean+import Network.DigitalOcean.Types+import Network.DigitalOcean.Services+import Test.QuickCheck+import Test.QuickCheck.Monadic+import Test.QuickCheck.Instances+import Test.QuickCheck.Property+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString.Lazy.Char8 as LBSC+import Data.Aeson+import Data.Aeson.Casing+-- import Text.RawString.QQ+import Text.InterpolatedString.Perl6 (qc)+import Data.Maybe+import Data.String+import GHC.Generics+import Data.Proxy++-----------------------------------------------------------------++prop_Response :: forall proxy service. (FromJSON service) => proxy service -> LBS.ByteString -> Property+prop_Response _ resp =+  let result = eitherDecode resp :: Either String service in+  case result of+    Left err -> error err+    Right acc -> label "wow" True++-----------------------------------------------------------------++instance Arbitrary Account where+  arbitrary =+    Account+      <$> choose (1, 100)+      <*> choose (1, 10)+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> oneof (return <$> ["active", "warning", "locked"])+      <*> arbitrary++instance ToJSON Account where+  toJSON = genericToJSON $ aesonPrefix snakeCase++prop_Account :: Account -> Property+prop_Account account = prop_Response (Proxy :: Proxy Account) (encode account)++-----------------------------------------------------------------++instance Arbitrary Action where+  arbitrary =+    Action+      <$> choose (1, 100000000)+      <*> oneof (return <$> ["in-progress", "completed", "errored"])+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> choose (1, 100000000)+      <*> arbitrary+      <*> arbitrary++instance ToJSON Action where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Arbitrary (Response Action) where+  arbitrary = Response <$> arbitrary++instance ToJSON (Response Action) where+  toJSON (Response action) = object [ "action" .= action]++instance Arbitrary (Response [Action]) where+  arbitrary = Response <$> arbitrary++instance ToJSON (Response [Action]) where+  toJSON (Response actions) = object [ "actions" .= actions]++prop_Action :: Action -> Response Action -> Response [Action] -> Property+prop_Action v v' v'' = prop_Response (Proxy :: Proxy Action) (encode v)+                           .&&. prop_Response (Proxy :: Proxy (Response Action)) (encode v')+                           .&&. prop_Response (Proxy :: Proxy (Response [Action])) (encode v'')++-----------------------------------------------------------------++instance Arbitrary Certificate where+  arbitrary =+    Certificate+      <$> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary++instance ToJSON Certificate where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Arbitrary (Response Certificate) where+  arbitrary = Response <$> arbitrary++instance ToJSON (Response Certificate) where+  toJSON (Response certificate) = object [ "certificate" .= certificate]++instance Arbitrary (Response [Certificate]) where+  arbitrary = Response <$> arbitrary++instance ToJSON (Response [Certificate]) where+  toJSON (Response certificates) = object [ "certificates" .= certificates]++prop_Certificate :: Certificate -> Response Certificate -> Response [Certificate] -> Property+prop_Certificate v v' v'' = prop_Response (Proxy :: Proxy Certificate) (encode v)+                       .&&. prop_Response (Proxy :: Proxy (Response Certificate)) (encode v')+                       .&&. prop_Response (Proxy :: Proxy (Response [Certificate])) (encode v'')++-----------------------------------------------------------------++instance Arbitrary Region where+  arbitrary =+    Region+      <$> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary++instance ToJSON Region where+  toJSON = genericToJSON $ aesonPrefix snakeCase++prop_Region :: Region -> Property+prop_Region = prop_Response (Proxy :: Proxy Region) . encode++instance Arbitrary (Response [Region]) where+  arbitrary = Response <$> arbitrary++instance ToJSON (Response [Region]) where+  toJSON (Response regions) = object [ "regions" .= regions]++prop_RegionsResponse :: Response [Region] -> Property+prop_RegionsResponse = prop_Response (Proxy :: Proxy (Response [Region])) . encode++-----------------------------------------------------------------++instance Arbitrary Snapshot where+  arbitrary =+    Snapshot+      <$> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary++instance ToJSON Snapshot where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Arbitrary (Response Snapshot) where+  arbitrary = Response <$> arbitrary++instance ToJSON (Response Snapshot) where+  toJSON (Response snapshot) = object [ "snapshot" .= snapshot]++instance Arbitrary (Response [Snapshot]) where+  arbitrary = Response <$> arbitrary++instance ToJSON (Response [Snapshot]) where+  toJSON (Response snapshots) = object [ "snapshots" .= snapshots]++prop_Snapshot :: Snapshot -> Response Snapshot -> Response [Snapshot] -> Property+prop_Snapshot v v' v'' = prop_Response (Proxy :: Proxy Snapshot) (encode v)+                  .&&. prop_Response (Proxy :: Proxy (Response Snapshot)) (encode v')+                  .&&. prop_Response (Proxy :: Proxy (Response [Snapshot])) (encode v'')++-----------------------------------------------------------------++instance Arbitrary Volume where+  arbitrary =+    Volume+      <$> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary++instance ToJSON Volume where+  toJSON = genericToJSON $ aesonPrefix snakeCase++instance Arbitrary (Response Volume) where+  arbitrary = Response <$> arbitrary++instance ToJSON (Response Volume) where+  toJSON (Response volume) = object [ "volume" .= volume]++instance Arbitrary (Response [Volume]) where+  arbitrary = Response <$> arbitrary++instance ToJSON (Response [Volume]) where+  toJSON (Response volumes) = object [ "volumes" .= volumes]++prop_Volume :: Volume -> Response Volume -> Response [Volume] -> Property+prop_Volume v v' v'' = prop_Response (Proxy :: Proxy Volume) (encode v)+                  .&&. prop_Response (Proxy :: Proxy (Response Volume)) (encode v')+                  .&&. prop_Response (Proxy :: Proxy (Response [Volume])) (encode v'')++-----------------------------------------------------------------++runTests = quickCheck . withMaxSuccess 100 $ prop_Account+                                         .&&. prop_Action+                                         .&&. prop_Certificate+                                         .&&. prop_Region+                                         .&&. prop_RegionsResponse+                                         .&&. prop_Snapshot+                                         .&&. prop_Volume++main :: IO ()+main = return ()