packages feed

mackerel-client (empty) → 0.0.0

raw patch · 36 files changed

+1935/−0 lines, 36 filesdep +aesondep +aeson-qqdep +basesetup-changed

Dependencies added: aeson, aeson-qq, base, bytestring, data-default, directory, filepath, hspec, htoml, http-client, http-client-tls, http-types, mackerel-client, parsec, raw-strings-qq, split, text, unordered-containers

Files

+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2016 itchyny <https://github.com/itchyny>++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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ mackerel-client.cabal view
@@ -0,0 +1,82 @@+name:                   mackerel-client+version:                0.0.0+author:                 itchyny+maintainer:             itchyny <https://github.com/itchyny>+license:                MIT+license-file:           LICENSE+category:               Web+build-type:             Simple+cabal-version:          >=1.8+synopsis:               An API client library for Mackerel (https://mackerel.io)+description:            This library provides bindings to Mackerel APIs.+                        .+                        The official site of Mackerel: https://mackerel.io/.+                        The reference of Mackerel API: https://mackerel.io/api-docs/.++library+  hs-source-dirs:       src+  ghc-options:          -Wall+  exposed-modules:      Web.Mackerel+                      , Web.Mackerel.Types+                      , Web.Mackerel.Types.Organization+                      , Web.Mackerel.Types.User+                      , Web.Mackerel.Types.Service+                      , Web.Mackerel.Types.Role+                      , Web.Mackerel.Types.Host+                      , Web.Mackerel.Types.Monitor+                      , Web.Mackerel.Types.Alert+                      , Web.Mackerel.Types.Dashboard+                      , Web.Mackerel.Api+                      , Web.Mackerel.Api.Organization+                      , Web.Mackerel.Api.User+                      , Web.Mackerel.Api.Service+                      , Web.Mackerel.Api.Role+                      , Web.Mackerel.Api.Host+                      , Web.Mackerel.Api.Monitor+                      , Web.Mackerel.Api.Alert+                      , Web.Mackerel.Api.Dashboard+                      , Web.Mackerel.Client+                      , Web.Mackerel.Config+  other-modules:        Web.Mackerel.Internal.TH+                      , Web.Mackerel.Internal.Api+  build-depends:        base >= 4.7 && < 5.0+                      , filepath+                      , directory+                      , http-types+                      , http-client+                      , http-client-tls+                      , bytestring+                      , parsec+                      , aeson+                      , htoml+                      , text+                      , unordered-containers+                      , data-default+                      , split++test-suite spec+  hs-source-dirs:       test+  main-is:              Spec.hs+  type:                 exitcode-stdio-1.0+  ghc-options:          -Wall+  other-modules:        Types.OrganizationSpec+                      , Types.UserSpec+                      , Types.ServiceSpec+                      , Types.RoleSpec+                      , Types.HostSpec+                      , Types.MonitorSpec+                      , Types.AlertSpec+                      , Types.DashboardSpec+                      , ConfigSpec+  build-depends:        base >= 4.7 && < 5.0+                      , mackerel-client+                      , aeson+                      , aeson-qq+                      , raw-strings-qq+                      , unordered-containers+                      , data-default+                      , hspec++source-repository head+  type:     git+  location: git@github.com:itchyny/mackerel-client-hs.git
+ src/Web/Mackerel.hs view
@@ -0,0 +1,6 @@+module Web.Mackerel (module Mackerel) where++import Web.Mackerel.Api as Mackerel+import Web.Mackerel.Client as Mackerel+import Web.Mackerel.Config as Mackerel+import Web.Mackerel.Types as Mackerel
+ src/Web/Mackerel/Api.hs view
@@ -0,0 +1,11 @@+module Web.Mackerel.Api (module Api, ApiError, errorStatusCode, errorMessage) where++import Web.Mackerel.Api.Alert as Api+import Web.Mackerel.Api.Dashboard as Api+import Web.Mackerel.Api.Host as Api+import Web.Mackerel.Api.Monitor as Api+import Web.Mackerel.Api.Organization as Api+import Web.Mackerel.Api.Role as Api+import Web.Mackerel.Api.Service as Api+import Web.Mackerel.Api.User as Api+import Web.Mackerel.Internal.Api (ApiError, errorStatusCode, errorMessage)
+ src/Web/Mackerel/Api/Alert.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Alert API.+module Web.Mackerel.Api.Alert (listAlerts, closeAlert) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import qualified Data.HashMap.Lazy as HM+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Alert++data ListAlertsResponse = ListAlertsResponse { responseAlerts :: [Alert] }+$(deriveJSON options ''ListAlertsResponse)++listAlerts :: Client -> IO (Either ApiError [Alert])+listAlerts client+  = request client GET "/api/v0/alerts" [] emptyBody (createHandler responseAlerts)++closeAlert :: Client -> AlertId -> String -> IO (Either ApiError Alert)+closeAlert client (AlertId alertId') reason = do+  let body = Just $ HM.fromList [("reason", reason) :: (String, String)]+  request client POST ("/api/v0/alerts/" <> BS.pack alertId' <> "/close") [] body (createHandler id)
+ src/Web/Mackerel/Api/Dashboard.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Dashboard API.+module Web.Mackerel.Api.Dashboard+  ( listDashboards+  , getDashboard+  , createDashboard+  , updateDashboard+  , deleteDashboard+  ) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Dashboard++data ListDashboardsResponse = ListDashboardsResponse { responseDashboards :: [Dashboard] }+$(deriveJSON options ''ListDashboardsResponse)++listDashboards :: Client -> IO (Either ApiError [Dashboard])+listDashboards client+  = request client GET "/api/v0/dashboards" [] emptyBody (createHandler responseDashboards)++getDashboard :: Client -> DashboardId -> IO (Either ApiError Dashboard)+getDashboard client (DashboardId dashboardId')+  = request client GET ("/api/v0/dashboards/" <> BS.pack dashboardId') [] emptyBody (createHandler id)++createDashboard :: Client -> DashboardCreate -> IO (Either ApiError Dashboard)+createDashboard client dashboard+  = request client POST "/api/v0/dashboards" [] (Just dashboard) (createHandler id)++updateDashboard :: Client -> Dashboard -> IO (Either ApiError Dashboard)+updateDashboard client dashboard = do+  let (DashboardId dashboardId') = dashboardId dashboard+  request client PUT ("/api/v0/dashboards/" <> BS.pack dashboardId') [] (Just dashboard) (createHandler id)++deleteDashboard :: Client -> DashboardId -> IO (Either ApiError Dashboard)+deleteDashboard client (DashboardId dashboardId')+  = request client DELETE ("/api/v0/dashboards/" <> BS.pack dashboardId') [] emptyBody (createHandler id)
+ src/Web/Mackerel/Api/Host.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Host API.+module Web.Mackerel.Api.Host+  ( createHost+  , getHost+  , updateHost+  , updateHostStatus+  , updateHostRoleFullnames+  , retireHost+  , listHosts+  , ListHostsParams(..)+  , listHostMetricNames+  ) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Default (Default(..))+import qualified Data.HashMap.Lazy as HM+import Data.Maybe (maybeToList)+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Host++data CreateHostResponse = CreateHostResponse { responseId :: HostId }+$(deriveJSON options ''CreateHostResponse)++createHost :: Client -> HostCreate -> IO (Either ApiError HostId)+createHost client host+  = request client POST "/api/v0/hosts" [] (Just host) (createHandler responseId)++data GetHostResponse = GetHostResponse { responseHost :: Host }+$(deriveJSON options ''GetHostResponse)++getHost :: Client -> HostId -> IO (Either ApiError Host)+getHost client (HostId hostId')+  = request client GET ("/api/v0/hosts/" <> BS.pack hostId') [] emptyBody (createHandler responseHost)++updateHost :: Client -> HostId -> HostCreate -> IO (Either ApiError HostId)+updateHost client (HostId hostId') host+  = request client PUT ("/api/v0/hosts/" <> BS.pack hostId') [] (Just host) (createHandler responseId)++data SuccessResponse = SuccessResponse { responseSuccess :: Bool }+$(deriveJSON options ''SuccessResponse)++updateHostStatus :: Client -> HostId -> HostStatus -> IO (Either ApiError Bool)+updateHostStatus client (HostId hostId') status = do+  let body = Just $ HM.fromList [("status", status) :: (String, HostStatus)]+  request client POST ("/api/v0/hosts/" <> BS.pack hostId' <> "/status") [] body (createHandler responseSuccess)++updateHostRoleFullnames :: Client -> HostId -> [String] -> IO (Either ApiError Bool)+updateHostRoleFullnames client (HostId hostId') roleFullnames = do+  let body = Just $ HM.fromList [("roleFullnames", roleFullnames ) :: (String, [String])]+  request client PUT ("/api/v0/hosts/" <> BS.pack hostId' <> "/role-fullnames") [] body (createHandler responseSuccess)++retireHost :: Client -> HostId -> IO (Either ApiError Bool)+retireHost client (HostId hostId')+  = request client POST ("/api/v0/hosts/" <> BS.pack hostId' <> "/retire") [] emptyBody (createHandler responseSuccess)++data ListHostsParams+  = ListHostsParams {+    listHostsParamsService :: Maybe String,+    listHostsParamsRoles :: [String],+    listHostsParamsName :: [String],+    listHostsParamsStatus :: [HostStatus]+  }++instance Default ListHostsParams where+  def = ListHostsParams Nothing [] [] [HostStatusWorking, HostStatusStandby, HostStatusMaintenance, HostStatusPoweroff]++data ListHostsResponse = ListHostsResponse { responseHosts :: [Host] }+$(deriveJSON options ''ListHostsResponse)++listHosts :: Client -> ListHostsParams -> IO (Either ApiError [Host])+listHosts client params = do+  let query = [ ("service", Just $ BS.pack s) | s <- maybeToList (listHostsParamsService params) ] +++              [ ("role", Just $ BS.pack r) | r <- listHostsParamsRoles params ] +++              [ ("name", Just $ BS.pack r) | r <- listHostsParamsName params ] +++              [ ("status", Just $ BS.pack $ show s) | s <- listHostsParamsStatus params ]+  request client GET "/api/v0/hosts" query emptyBody (createHandler responseHosts)++data ListMetricNamesResponse = ListMetricNamesResponse { responseNames :: [String] }+$(deriveJSON options ''ListMetricNamesResponse)++listHostMetricNames :: Client -> HostId -> IO (Either ApiError [String])+listHostMetricNames client (HostId hostId')+  = request client GET ("/api/v0/hosts/" <> BS.pack hostId' <> "/metric-names") [] emptyBody (createHandler responseNames)
+ src/Web/Mackerel/Api/Monitor.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Monitor API.+module Web.Mackerel.Api.Monitor+  ( listMonitors+  , createMonitor+  , updateMonitor+  , deleteMonitor+  ) where++import Data.Aeson (Value)+import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Monitor++data ListMonitorsResponse = ListMonitorsResponse { responseMonitors :: [Monitor] }+$(deriveJSON options ''ListMonitorsResponse)++listMonitors :: Client -> IO (Either ApiError [Monitor])+listMonitors client+  = request client GET "/api/v0/monitors" [] emptyBody (createHandler responseMonitors)++createMonitor :: Client -> Monitor -> IO (Either ApiError Monitor)+createMonitor client monitor+  = request client POST "/api/v0/monitors" [] (Just monitor) (createHandler id)++updateMonitor :: Client -> Monitor -> IO (Either ApiError Monitor)+updateMonitor client monitor = do+  let Just (MonitorId monitorId') = monitorId monitor+  request client PUT ("/api/v0/monitors/" <> BS.pack monitorId') [] (Just monitor) (createHandler id)++deleteMonitor :: Client -> MonitorId -> IO (Either ApiError Value)+deleteMonitor client (MonitorId monitorId')+  = request client DELETE ("/api/v0/monitors/" <> BS.pack monitorId') [] emptyBody (createHandler id)
+ src/Web/Mackerel/Api/Organization.hs view
@@ -0,0 +1,12 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Organization API.+module Web.Mackerel.Api.Organization (getOrganization) where++import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Types.Organization++getOrganization :: Client -> IO (Either ApiError Organization)+getOrganization client = request client GET "/api/v0/org" [] emptyBody (createHandler id)
+ src/Web/Mackerel/Api/Role.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Role API.+module Web.Mackerel.Api.Role (listRoles) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Role++data ListRolesResponse = ListRolesResponse { responseRoles :: [Role] }+$(deriveJSON options ''ListRolesResponse)++listRoles :: Client -> String -> IO (Either ApiError [Role])+listRoles client serviceName'+  = request client GET ("/api/v0/services/" <> BS.pack serviceName' <> "/roles") [] emptyBody (createHandler responseRoles)
+ src/Web/Mackerel/Api/Service.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Service API.+module Web.Mackerel.Api.Service+  ( listServices+  , listServiceMetricNames+  ) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Service++data ListServicesResponse = ListServicesResponse { responseServices :: [Service] }+$(deriveJSON options ''ListServicesResponse)++listServices :: Client -> IO (Either ApiError [Service])+listServices client+  = request client GET "/api/v0/services" [] emptyBody (createHandler responseServices)++data ListMetricNamesResponse = ListMetricNamesResponse { responseNames :: [String] }+$(deriveJSON options ''ListMetricNamesResponse)++listServiceMetricNames :: Client -> String -> IO (Either ApiError [String])+listServiceMetricNames client serviceName'+  = request client GET ("/api/v0/services/" <> BS.pack serviceName' <> "/metric-names") [] emptyBody (createHandler responseNames)
+ src/Web/Mackerel/Api/User.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | User API.+module Web.Mackerel.Api.User (listUsers, deleteUser) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.User++data ListUsersResponse = ListUsersResponse { responseUsers :: [User] }+$(deriveJSON options ''ListUsersResponse)++listUsers :: Client -> IO (Either ApiError [User])+listUsers client+  = request client GET "/api/v0/users" [] emptyBody (createHandler responseUsers)++deleteUser :: Client -> UserId -> IO (Either ApiError User)+deleteUser client (UserId userId')+  = request client DELETE ("/api/v0/users/" <> BS.pack userId') [] emptyBody (createHandler id)
+ src/Web/Mackerel/Client.hs view
@@ -0,0 +1,17 @@+module Web.Mackerel.Client (Client, apiKey, apiBase, userAgent) where++import Data.Default (Default(..))++data Client+  = Client {+    apiKey :: String,+    apiBase :: String,+    userAgent :: String+  } deriving (Eq, Show)++instance Default Client where+  def = Client {+    apiKey = "",+    apiBase = "https://mackerel.io",+    userAgent = "mackerel-client-hs"+  }
+ src/Web/Mackerel/Config.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE OverloadedStrings #-}+module Web.Mackerel.Config+  ( Config(..)+  , HostStatusConfig(..)+  , loadConfig+  , parseConfig+  , loadClient+  , mackerelRoot+  , confFile+  , pidFile+  ) where++import Data.Aeson+import Data.Aeson.Types (Value(..), Result(..), typeMismatch)+import Data.Default (Default(..))+import Data.List (isPrefixOf, isInfixOf)+import Data.Maybe (fromMaybe)+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import System.Directory (getHomeDirectory)+import System.FilePath ((</>))+import System.Info (os)+import Text.Toml (parseTomlDoc)++import Web.Mackerel.Client+import Web.Mackerel.Types.Host++data Config+  = Config {+    configApiBase :: Maybe String,+    configApiKey :: Maybe String,+    configRoot :: Maybe String,+    configPidfile :: Maybe String,+    configRoles :: Maybe [String],+    configVerbose :: Maybe Bool,+    configDiagnostic :: Maybe Bool,+    configDisplayname :: Maybe String,+    configHostStatus :: Maybe HostStatusConfig,+    configHttpProxy :: Maybe String+  } deriving (Eq, Show)++instance FromJSON Config where+  parseJSON (Object o)+    = Config <$> o .:? "apibase" <*> o .:? "apikey" <*> o .:? "root"+             <*> o .:? "pidfile" <*> o .:? "roles" <*> o .:? "verbose"+             <*> o .:? "diagnostic" <*> o .:? "display_name" <*> o .:? "host_status"+             <*> o .:? "http_proxy"+  parseJSON o = typeMismatch "Config" o++instance Default Config where+  def = Config def def def def def def def def def def++data HostStatusConfig+  = HostStatusConfig {+    hostStatusOnStart :: Maybe HostStatus,+    hostStatusOnStop :: Maybe HostStatus+  } deriving (Eq, Show)++instance FromJSON HostStatusConfig where+  parseJSON (Object o)+    = HostStatusConfig <$> o .:? "on_start" <*> o .:? "on_stop"+  parseJSON o = typeMismatch "HostStatusConfig" o++instance Default HostStatusConfig where+  def = HostStatusConfig def def++agentName :: String+agentName = "mackerel-agent"++loadConfig :: IO (Either String Config)+loadConfig = parseConfig <$> (Text.readFile =<< confFile)++parseConfig :: Text.Text -> Either String Config+parseConfig cnt+  = case parseTomlDoc "" cnt of+         Left _ -> Left "toml parse error"+         Right res -> case fromJSON $ toJSON res of+                           Error err -> Left err+                           Success r -> Right r++loadClient :: IO (Either String Client)+loadClient = fmap (\c -> def { apiKey = fromMaybe (apiKey def) $ configApiKey c,+                               apiBase = fromMaybe (apiBase def) $ configApiBase c }) <$> loadConfig++mackerelRoot :: IO FilePath+mackerelRoot | isBSD || isDarwin = getHomeDirectory >>= \home -> return $ home </> "Library" </> agentName+             | isWindows = error "Windows is not supported yet."+             | otherwise = return "/var/lib/mackerel-agent"++confFile :: IO FilePath+confFile | isLinux = return $ "/etc" </> agentName </> agentName ++ ".conf"+         | otherwise = mackerelRoot >>= \root -> return $ root </> agentName ++ ".conf"++pidFile :: IO FilePath+pidFile | isLinux = return "/var/run/mackerel-agent.pid"+        | otherwise = mackerelRoot >>= \root -> return $ root </> agentName ++ ".pid"++isBSD :: Bool+isBSD = "bsd" `isInfixOf` os++isDarwin :: Bool+isDarwin = os == "darwin"++isWindows :: Bool+isWindows = "mingw" `isPrefixOf` os++isLinux :: Bool+isLinux = not isBSD && not isDarwin && not isWindows
+ src/Web/Mackerel/Internal/Api.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Internal helpers for APIs.+module Web.Mackerel.Internal.Api (request, emptyBody, createHandler, ApiError, errorStatusCode, errorMessage) where++import Control.Monad ((>=>))+import Data.Aeson (encode, decode, (.:), FromJSON, ToJSON)+import Data.Aeson.Types (parseMaybe)+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy as LBS+import Data.Maybe (maybe, fromMaybe)+import Data.Semigroup ((<>))+import Network.HTTP.Client+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Types (StdMethod, renderStdMethod, Query, statusCode)++import Web.Mackerel.Client++type ResponseHandler a = Response LBS.ByteString -> Either ApiError a++-- | Error information of the response of API.+data ApiError = ApiError { errorStatusCode :: Int, errorMessage :: String } deriving (Eq, Show)++-- | Request to Mackerel.+request :: ToJSON a => Client -> StdMethod -> BS.ByteString -> Query -> Maybe a -> ResponseHandler b -> IO (Either ApiError b)+request client method' path' query body handler = do+  initialRequest <- setQueryString query <$> parseRequest (apiBase client)+  handler <$> (httpLbs initialRequest {+    method = renderStdMethod method', path = path', requestBody = RequestBodyLBS $ maybe "" encode body,+    requestHeaders = requestHeaders initialRequest ++ [+      ("X-Api-Key", BS.pack $ apiKey client),+      ("User-Agent", BS.pack $ userAgent client),+      ("Content-Type", "application/json")+    ]+  } =<< newManager (if secure initialRequest then tlsManagerSettings else defaultManagerSettings))++-- | Empty body (to avoid type ambiguity).+emptyBody :: Maybe ()+emptyBody = Nothing++-- | Create an api response handler.+createHandler :: FromJSON a => (a -> b) -> ResponseHandler b+createHandler extractor response+  | statusCode (responseStatus response) == 200 = maybe (Left decodeError) (Right . extractor) (decode (responseBody response))+  | otherwise = Left $ maybe decodeError getApiError $ decode (responseBody response)+  where getApiError json = ApiError {+          errorStatusCode = statusCode $ responseStatus response,+          errorMessage = fromMaybe "" $ parseMaybe (((.: "error") >=> (.: "message")) <> (.: "error")) json+        }+        decodeError = ApiError {+          errorStatusCode = statusCode $ responseStatus response,+          errorMessage = show $ responseBody response+        }
+ src/Web/Mackerel/Internal/TH.hs view
@@ -0,0 +1,23 @@+-- | Helpers for deriveJSON.+module Web.Mackerel.Internal.TH (options, kebabCase, snakeCase) where++import Data.Aeson.TH (Options(..), defaultOptions)+import Data.Char (isLower, isUpper, toLower)+import Data.List (intercalate)+import Data.List.Split (keepDelimsL, split, whenElt)++options :: Options+options = defaultOptions {+  fieldLabelModifier = (\(c:cs) -> toLower c : cs) . dropWhile isLower,+  constructorTagModifier = map toLower,+  omitNothingFields = True+}++kebabCase :: String -> String+kebabCase = modifyCase "-"++snakeCase :: String -> String+snakeCase = modifyCase "_"++modifyCase :: String -> String -> String+modifyCase cs = intercalate cs . map (map toLower) . dropWhile null . split (keepDelimsL $ whenElt isUpper)
+ src/Web/Mackerel/Types.hs view
@@ -0,0 +1,10 @@+module Web.Mackerel.Types (module Types) where++import Web.Mackerel.Types.Alert as Types+import Web.Mackerel.Types.Dashboard as Types+import Web.Mackerel.Types.Host as Types+import Web.Mackerel.Types.Monitor as Types+import Web.Mackerel.Types.Organization as Types+import Web.Mackerel.Types.Role as Types+import Web.Mackerel.Types.Service as Types+import Web.Mackerel.Types.User as Types
+ src/Web/Mackerel/Types/Alert.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Alert where++import Data.Aeson+import qualified Data.Aeson as Aeson+import Data.Aeson.TH (deriveJSON, constructorTagModifier)+import Data.Aeson.Types (typeMismatch)+import Data.Char (toUpper)+import qualified Data.Text as Text++import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Host (HostId)+import Web.Mackerel.Types.Monitor (MonitorId, MonitorType)++data AlertId = AlertId String+            deriving (Eq, Show)++instance FromJSON AlertId where+  parseJSON (Aeson.String alertId) = return $ AlertId $ Text.unpack alertId+  parseJSON o = typeMismatch "AlertId" o++instance ToJSON AlertId where+  toJSON (AlertId alertId) = toJSON alertId++data AlertStatus = AlertStatusOk+                 | AlertStatusCritical+                 | AlertStatusWarning+                 | AlertStatusUnknown+                 deriving (Eq, Show)++$(deriveJSON options { constructorTagModifier = map toUpper . drop 11 } ''AlertStatus)++data Alert+  = Alert {+    alertId :: AlertId,+    alertStatus :: AlertStatus,+    alertMonitorId :: Maybe MonitorId,+    alertType :: MonitorType,+    alertHostId :: Maybe HostId,+    alertValue :: Maybe Double,+    alertMessage :: Maybe String,+    alertReason :: Maybe String,+    alertOpenedAt :: Integer,+    alertClosedAt :: Maybe Integer+  } deriving (Eq, Show)++$(deriveJSON options ''Alert)
+ src/Web/Mackerel/Types/Dashboard.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Dashboard where++import Data.Aeson+import qualified Data.Aeson as Aeson+import Data.Aeson.TH (deriveJSON, fieldLabelModifier)+import Data.Aeson.Types (typeMismatch)+import Data.Char (toLower)+import qualified Data.Text as Text++import Web.Mackerel.Internal.TH++data DashboardId = DashboardId String+            deriving (Eq, Show)++instance FromJSON DashboardId where+  parseJSON (Aeson.String dashboardId') = return $ DashboardId $ Text.unpack dashboardId'+  parseJSON o = typeMismatch "DashboardId" o++instance ToJSON DashboardId where+  toJSON (DashboardId dashboardId') = toJSON dashboardId'++data DashboardCreate+  = DashboardCreate {+    dashboardCreateTitle :: String,+    dashboardCreateBodyMarkdown :: String,+    dashboardCreateUrlPath :: String+  } deriving (Eq, Show)++$(deriveJSON options { fieldLabelModifier = (\(c:cs) -> toLower c : cs) . drop 15 } ''DashboardCreate)++data Dashboard+  = Dashboard {+    dashboardId :: DashboardId,+    dashboardTitle :: String,+    dashboardBodyMarkdown :: String,+    dashboardUrlPath :: String,+    dashboardCreatedAt :: Integer,+    dashboardUpdatedAt :: Integer+  } deriving (Eq, Show)++$(deriveJSON options ''Dashboard)
+ src/Web/Mackerel/Types/Host.hs view
@@ -0,0 +1,148 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+module Web.Mackerel.Types.Host where++import Control.Applicative ((<|>))+import Data.Aeson+import qualified Data.Aeson as Aeson+import Data.Aeson.TH (deriveJSON, constructorTagModifier, fieldLabelModifier)+import Data.Aeson.Types (Parser, typeMismatch)+import Data.Char (toLower)+import Data.Default (Default(..))+import qualified Data.HashMap.Lazy as HM+import Data.Maybe (isJust)+import qualified Data.Text as Text++import Web.Mackerel.Internal.TH++data HostId = HostId String+            deriving (Eq, Show)++instance FromJSON HostId where+  parseJSON (Aeson.String hostId) = return $ HostId $ Text.unpack hostId+  parseJSON o = typeMismatch "HostId" o++instance ToJSON HostId where+  toJSON (HostId hostId) = toJSON hostId++data HostMetaCloud+  = HostMetaCloud {+    metaCloudProvider :: String,+    metaCloudMetadata :: HM.HashMap String Value+  } deriving (Eq, Show)++$(deriveJSON options { fieldLabelModifier = map toLower . drop 9 } ''HostMetaCloud)++data HostMetaCpu+  = HostMetaCpu {+    metaCpuCacheSize :: Maybe String,+    metaCpuCoreId :: Maybe String,+    metaCpuCores :: Maybe String,+    metaCpuFamily :: Maybe String,+    metaCpuMhz :: Maybe String,+    metaCpuModel :: Maybe String,+    metaCpuModelName :: Maybe String,+    metaCpuPhysicalId :: Maybe String,+    metaCpuStepping :: Maybe String,+    metaCpuVendorId :: Maybe String+  } deriving (Eq, Show)++instance FromJSON HostMetaCpu where+  parseJSON (Object o)+    = HostMetaCpu+      <$> o .:? "cache_size" <*> o .:? "core_id" <*> o .:? "cores" <*> o .:? "family"+      <*> (o .:? "mhz" <|> fmap show <$> (o .:? "mhz" :: Parser (Maybe Integer)))+      <*> (o .:? "model" <|> fmap show <$> (o .:? "model" :: Parser (Maybe Integer)))+      <*> o .:? "model_name" <*> o .:? "physical_id" <*> o .:? "stepping" <*> o .:? "vendor_id"+  parseJSON o = typeMismatch "HostMetaCpu" o++instance ToJSON HostMetaCpu where+  toJSON (HostMetaCpu cacheSize coreId cores family mhz model modelName physicalId stepping vendorId)+    = object [ key .= value |+        (key, value) <- [+          ("cache_size", cacheSize), ("core_id", coreId), ("cores", cores), ("family", family),+          ("mhz", mhz), ("model", model), ("model_name", modelName),+          ("physical_id", physicalId), ("stepping", stepping), ("vendor_id", vendorId)+        ],+        isJust value+    ]++data HostMeta+  = HostMeta {+    metaAgentName :: Maybe String,+    metaAgentRevision :: Maybe String,+    metaAgentVersion :: Maybe String,+    metaBlockDevice :: Maybe (HM.HashMap String (HM.HashMap String String)),+    metaCpu :: Maybe [HostMetaCpu],+    metaFilesystem :: Maybe (HM.HashMap String (HM.HashMap String Value)),+    metaKernel :: Maybe (HM.HashMap String String),+    metaMemory :: Maybe (HM.HashMap String String),+    metaCloud :: Maybe HostMetaCloud+  } deriving (Eq, Show)++instance Default HostMeta where+  def = HostMeta def def def def def def def def def++$(deriveJSON options {+  fieldLabelModifier+    = \xs -> let ys = drop 4 xs in if take 5 ys == "Agent" then kebabCase ys else snakeCase ys+} ''HostMeta)++data HostStatus = HostStatusWorking+                | HostStatusStandby+                | HostStatusMaintenance+                | HostStatusPoweroff+                deriving Eq++instance Show HostStatus where+  show HostStatusWorking = "working"+  show HostStatusStandby = "standby"+  show HostStatusMaintenance = "maintenance"+  show HostStatusPoweroff = "poweroff"++instance Read HostStatus where+  readsPrec _ xs = [ (hs, drop (length str) xs) | (hs, str) <- pairs', take (length str) xs == str ]+    where pairs' = [(HostStatusWorking, "working"), (HostStatusStandby, "standby"), (HostStatusMaintenance, "maintenance"), (HostStatusPoweroff, "poweroff")]++$(deriveJSON options { constructorTagModifier = map toLower . drop 10 } ''HostStatus)++data HostInterface+  = HostInterface {+    hostInterfaceName :: String,+    hostInterfaceMacAddress :: Maybe String,+    hostInterfaceIpv4Addresses :: Maybe [String],+    hostInterfaceIpv6Addresses :: Maybe [String]+  } deriving (Eq, Show)++$(deriveJSON options { fieldLabelModifier = (\(c:cs) -> toLower c : cs) . drop 13 } ''HostInterface)++data Host+  = Host {+    hostId :: HostId,+    hostName :: String,+    hostDisplayName :: Maybe String,+    hostStatus :: HostStatus,+    hostMemo :: String,+    hostRoles :: HM.HashMap String [String],+    hostIsRetired :: Bool,+    hostCreatedAt :: Integer,+    hostMeta :: HostMeta,+    hostInterfaces :: [HostInterface]+  } deriving (Eq, Show)++$(deriveJSON options ''Host)++data HostCreate+  = HostCreate {+    hostCreateName :: String,+    hostCreateDisplayName :: Maybe String,+    hostCreateCustomIdentifier :: Maybe String,+    hostCreateMeta :: HostMeta,+    hostCreateInterfaces :: Maybe [HostInterface],+    hostCreateRoleFullnames :: Maybe [String],+    hostCreateChecks :: Maybe [String]+  } deriving (Eq, Show)++instance Default HostCreate where+  def = HostCreate def def def def def def def++$(deriveJSON options { fieldLabelModifier = (\(c:cs) -> toLower c : cs) . drop 10 } ''HostCreate)
+ src/Web/Mackerel/Types/Monitor.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Monitor where++import Data.Aeson+import qualified Data.Aeson as Aeson+import Data.Aeson.TH (deriveJSON, SumEncoding(..), sumEncoding, constructorTagModifier, fieldLabelModifier)+import Data.Aeson.Types (typeMismatch)+import Data.Char (toLower)+import Data.List (isSuffixOf)+import qualified Data.Text as Text++import Web.Mackerel.Internal.TH++data MonitorId = MonitorId String+               deriving (Eq, Show)++instance FromJSON MonitorId where+  parseJSON (Aeson.String monitorId') = return $ MonitorId $ Text.unpack monitorId'+  parseJSON o = typeMismatch "MonitorId" o++instance ToJSON MonitorId where+  toJSON (MonitorId monitorId') = toJSON monitorId'++data MonitorOperator = MonitorGreaterThan | MonitorLessThan+                     deriving Eq++instance Show MonitorOperator where+  show MonitorGreaterThan = ">"+  show MonitorLessThan = "<"++instance Read MonitorOperator where+  readsPrec _ xs = [ (op, drop 1 xs) | (op, opstr) <- [(MonitorGreaterThan, ">"), (MonitorLessThan, "<")], take 1 xs == opstr ]++instance FromJSON MonitorOperator where+  parseJSON (Aeson.String str)+    | Text.unpack str == ">" = return MonitorGreaterThan+    | Text.unpack str == "<" = return MonitorLessThan+  parseJSON o = typeMismatch "MonitorOperator" o++instance ToJSON MonitorOperator where+  toJSON op = toJSON (show op)++data MonitorExternalHeader+  = MonitorExternalHeader {+    monitorHeaderName :: String,+    monitorHeaderValue :: String+  } deriving (Eq, Show)++$(deriveJSON options { fieldLabelModifier = map toLower . drop 13 } ''MonitorExternalHeader)++data MonitorType = MonitorTypeConnectivity+                 | MonitorTypeHost+                 | MonitorTypeService+                 | MonitorTypeExternal+                 | MonitorTypeCheck+                 | MonitorTypeExpression+                 deriving (Eq, Show)++$(deriveJSON options { constructorTagModifier = map toLower . drop 11 } ''MonitorType)++data Monitor+  = MonitorHost {+    monitorId :: Maybe MonitorId,+    monitorName :: String,+    monitorDuration :: Integer,+    monitorMetric :: String,+    monitorOperator :: MonitorOperator,+    monitorWarning :: Double,+    monitorCritical :: Double,+    monitorIsMute :: Maybe Bool,+    monitorNotificationInterval :: Maybe Integer,+    monitorScopes :: Maybe [String],+    monitorExcludeScopes :: Maybe [String]+  }+  | MonitorConnectivity {+    monitorId :: Maybe MonitorId,+    monitorName :: String,+    monitorIsMute :: Maybe Bool,+    monitorNotificationInterval :: Maybe Integer,+    monitorScopes :: Maybe [String],+    monitorExcludeScopes :: Maybe [String]+  }+  | MonitorService {+    monitorId :: Maybe MonitorId,+    monitorName :: String,+    monitorService :: String,+    monitorDuration :: Integer,+    monitorMetric :: String,+    monitorOperator :: MonitorOperator,+    monitorWarning :: Double,+    monitorCritical :: Double,+    monitorIsMute :: Maybe Bool,+    monitorNotificationInterval :: Maybe Integer+  }+  | MonitorExternal {+    monitorId :: Maybe MonitorId,+    monitorName :: String,+    monitorUrl :: String,+    monitorServiceOption :: Maybe String,+    monitorResponseTimeDuration :: Maybe Double,+    monitorResponseTimeWarning :: Maybe Double,+    monitorResponseTimeCritical :: Maybe Double,+    monitorContainsString :: Maybe String,+    monitorMaxCheckAttempts :: Maybe Integer,+    monitorCertificationExpirationWarning :: Maybe Integer,+    monitorCertificationExpirationCritical :: Maybe Integer,+    monitorSkipCertificateVerification :: Maybe Bool,+    monitorHeaders :: Maybe [MonitorExternalHeader],+    monitorIsMute :: Maybe Bool,+    monitorNotificationInterval :: Maybe Integer+  }+  | MonitorExpression {+    monitorId :: Maybe MonitorId,+    monitorName :: String,+    monitorExpression :: String,+    monitorOperator :: MonitorOperator,+    monitorWarning :: Double,+    monitorCritical :: Double,+    monitorIsMute :: Maybe Bool,+    monitorNotificationInterval :: Maybe Integer+  } deriving (Eq, Show)++$(deriveJSON options {+  sumEncoding = TaggedObject "type" "type",+  constructorTagModifier = map toLower . drop 7,+  fieldLabelModifier = \xs ->+    let ys = fieldLabelModifier options xs+        in if "Option" `isSuffixOf` ys then take (length ys - 6) ys else ys+} ''Monitor)
+ src/Web/Mackerel/Types/Organization.hs view
@@ -0,0 +1,11 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Organization where++import Data.Aeson.TH (deriveJSON)++import Web.Mackerel.Internal.TH++data Organization = Organization { organizationName :: String }+                  deriving (Eq, Show)++$(deriveJSON options ''Organization)
+ src/Web/Mackerel/Types/Role.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Role where++import Data.Aeson.TH (deriveJSON)++import Web.Mackerel.Internal.TH++data Role+  = Role {+    roleName :: String,+    roleMemo :: String+  } deriving (Eq, Show)++$(deriveJSON options ''Role)
+ src/Web/Mackerel/Types/Service.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Service where++import Data.Aeson.TH (deriveJSON)++import Web.Mackerel.Internal.TH++data Service+  = Service {+    serviceName :: String,+    serviceMemo :: String,+    serviceRoles :: [String]+  } deriving (Eq, Show)++$(deriveJSON options ''Service)
+ src/Web/Mackerel/Types/User.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.User where++import Data.Aeson+import qualified Data.Aeson as Aeson+import Data.Aeson.TH (deriveJSON)+import Data.Aeson.Types (typeMismatch)+import qualified Data.Text as Text++import Web.Mackerel.Internal.TH++data UserId = UserId String+            deriving (Eq, Show)++instance FromJSON UserId where+  parseJSON (Aeson.String alertId) = return $ UserId $ Text.unpack alertId+  parseJSON o = typeMismatch "UserId" o++instance ToJSON UserId where+  toJSON (UserId alertId) = toJSON alertId++data User+  = User {+    userId :: UserId,+    userScreenName :: String,+    userEmail :: String+  } deriving (Eq, Show)++$(deriveJSON options ''User)
+ test/ConfigSpec.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module ConfigSpec where++import Control.Monad (forM_)+import Data.Default+import Test.Hspec+import Text.RawString.QQ++import Web.Mackerel.Config+import Web.Mackerel.Types.Host++spec :: Spec+spec = do++  let config1 = def {+        configApiKey = Just "<Mackerel-API-KEY>"+      }++  let toml1 = [r|+        apikey = "<Mackerel-API-KEY>"+      |]++  let config2 = Config {+        configApiBase = Just "https://mackerel.io",+        configApiKey = Just "<Mackerel-API-KEY>",+        configRoot = Just "/var/lib/mackerel-agent",+        configPidfile = Just "/var/run/mackerel-agent.pid",+        configRoles = Just ["service1:role1", "service2:role3"],+        configVerbose = Just True,+        configDiagnostic = Just False,+        configDisplayname = Just "Example Host",+        configHostStatus = Just HostStatusConfig {+          hostStatusOnStart = Just HostStatusWorking,+          hostStatusOnStop = Just HostStatusPoweroff+        },+        configHttpProxy = Just "http://localhost:8080"+      }++  let toml2 = [r|+        apibase = "https://mackerel.io"+        apikey = "<Mackerel-API-KEY>"+        root = "/var/lib/mackerel-agent"+        pidfile = "/var/run/mackerel-agent.pid"++        verbose = true+        diagnostic = false++        display_name = "Example Host"+        http_proxy = "http://localhost:8080"++        roles = [ "service1:role1", "service2:role3" ]++        include = "/etc/mackerel-agent/conf.d/*.conf"++        [host_status]+        on_start = "working"+        on_stop  = "poweroff"+      |]++  describe "parseConfig" $+    it "should parse a configuration toml" $+      forM_ [(toml1, config1), (toml2, config2)] $ \(toml, config) ->+        parseConfig toml `shouldBe` Right config
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/Types/AlertSpec.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.AlertSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.Alert+import Web.Mackerel.Types.Host+import Web.Mackerel.Types.Monitor++spec :: Spec+spec = do++  let alert1 = Alert {+        alertId = AlertId "abcde0",+        alertStatus = AlertStatusCritical,+        alertMonitorId = Just $ MonitorId "abcde2",+        alertType = MonitorTypeConnectivity,+        alertHostId = Just $ HostId "abcde1",+        alertValue = Nothing,+        alertMessage = Nothing,+        alertReason = Nothing,+        alertOpenedAt = 1483196400,+        alertClosedAt = Nothing+      }++  let json1 = [aesonQQ|+    {+      "id": "abcde0",+      "status": "CRITICAL",+      "monitorId": "abcde2",+      "type": "connectivity",+      "hostId": "abcde1",+      "openedAt": 1483196400+    }+  |]++  let alert2 = Alert {+        alertId = AlertId "abcde0",+        alertStatus = AlertStatusWarning,+        alertMonitorId = Just $ MonitorId "abcde2",+        alertType = MonitorTypeHost,+        alertHostId = Just $ HostId "abcde1",+        alertValue = Just 25.0,+        alertMessage = Nothing,+        alertReason = Nothing,+        alertOpenedAt = 1483196400,+        alertClosedAt = Nothing+      }++  let json2 = [aesonQQ|+    {+      "id": "abcde0",+      "status": "WARNING",+      "monitorId": "abcde2",+      "type": "host",+      "hostId": "abcde1",+      "value": 25.0,+      "openedAt": 1483196400+    }+  |]++  let alert3 = Alert {+        alertId = AlertId "abcde0",+        alertStatus = AlertStatusCritical,+        alertMonitorId = Just $ MonitorId "abcde2",+        alertType = MonitorTypeExternal,+        alertHostId = Nothing,+        alertValue = Just 121,+        alertMessage = Just "SSL certification will expire within 696 days (2018-11-28T21:00:00+0900)",+        alertReason = Just "Close this alert",+        alertOpenedAt = 1483196400,+        alertClosedAt = Just 1483282800+      }++  let json3 = [aesonQQ|+    {+      "id": "abcde0",+      "status": "CRITICAL",+      "monitorId": "abcde2",+      "type": "external",+      "value": 121,+      "message": "SSL certification will expire within 696 days (2018-11-28T21:00:00+0900)",+      "reason": "Close this alert",+      "openedAt": 1483196400,+      "closedAt": 1483282800+    }+  |]++  describe "Alert FromJSON" $ do+    it "should parse a json" $+      forM_ [(alert1, json1), (alert2, json2), (alert3, json3)] $ \(alert, json) ->+        decode (encode json) `shouldBe` Just alert++    it "should reject an invalid json" $ do+      decode "{}" `shouldBe` (Nothing :: Maybe Alert)+      let (Object hm) = json1+      forM_ ["id", "status", "type", "openedAt"] $ \key ->+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Alert)++  describe "Alert ToJSON" $+    it "should encode into a json" $+      forM_ [(alert1, json1), (alert2, json2), (alert3, json3)] $ \(alert, json) ->+        decode (encode alert) `shouldBe` Just json
+ test/Types/DashboardSpec.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.DashboardSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.Dashboard++spec :: Spec+spec = do++  let dashboard = Dashboard {+        dashboardId = DashboardId "abcde",+        dashboardTitle = "This is a dashboard",+        dashboardBodyMarkdown = "# Example\n[example](https://example.com)",+        dashboardUrlPath = "example",+        dashboardCreatedAt = 1483196400,+        dashboardUpdatedAt = 1483282800+      }++  let json = [aesonQQ|+    {+      "id": "abcde",+      "title": "This is a dashboard",+      "bodyMarkdown": "# Example\n[example](https://example.com)",+      "urlPath": "example",+      "createdAt": 1483196400,+      "updatedAt": 1483282800+    }+  |]++  let dashboardCreate = DashboardCreate {+        dashboardCreateTitle = "This is a dashboard",+        dashboardCreateBodyMarkdown = "# Example\n[example](https://example.com)",+        dashboardCreateUrlPath = "example"+      }++  let jsonCreate = [aesonQQ|+    {+      "title": "This is a dashboard",+      "bodyMarkdown": "# Example\n[example](https://example.com)",+      "urlPath": "example"+    }+  |]++  describe "Dashboard FromJSON" $ do+    it "should parse a json" $+      decode (encode json) `shouldBe` Just dashboard++    it "should reject an invalid json" $ do+      decode "{}" `shouldBe` (Nothing :: Maybe Dashboard)+      let (Object hm) = json+      forM_ ["id", "title", "bodyMarkdown", "urlPath", "createdAt", "updatedAt"] $ \key ->+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Dashboard)++  describe "Dashboard ToJSON" $+    it "should encode into a json" $+      decode (encode dashboard) `shouldBe` Just json++  describe "DashboardCreate FromJSON" $+    it "should parse a json" $+      decode (encode jsonCreate) `shouldBe` Just dashboardCreate++  describe "DashboardCreate ToJSON" $+    it "should encode into a json" $+      decode (encode dashboardCreate) `shouldBe` Just jsonCreate
+ test/Types/HostSpec.hs view
@@ -0,0 +1,314 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.HostSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import Data.Default+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.Host++spec :: Spec+spec = do++  let host1 = Host {+        hostId = HostId "abcede",+        hostName = "foohost.example",+        hostDisplayName = Nothing,+        hostStatus = HostStatusWorking,+        hostMemo = "",+        hostRoles = HM.fromList [],+        hostIsRetired = False,+        hostCreatedAt = 1483196400,+        hostMeta = def,+        hostInterfaces = []+      }++  let json1 = [aesonQQ|+    {+      "id": "abcede",+      "name": "foohost.example",+      "status": "working",+      "memo": "",+      "roles": {},+      "isRetired": false,+      "createdAt": 1483196400,+      "meta": {},+      "interfaces": []+    }+  |]++  let host2 = Host {+        hostId = HostId "abcede",+        hostName = "foohost.example",+        hostDisplayName = Just "Host Foo",+        hostStatus = HostStatusWorking,+        hostMemo = "host memo",+        hostRoles = HM.fromList [("foo", ["bar", "baz"]), ("qux", ["quux"])],+        hostIsRetired = False,+        hostCreatedAt = 1483196400,+        hostMeta = def {+          metaAgentName = Just "mackerel-agent/0.37.0 (Revision 3c11c2c)",+          metaAgentRevision = Just "3c11c2c",+          metaAgentVersion = Just "0.37.0",+          metaBlockDevice = Just $ HM.fromList [+            ("xvda", HM.fromList [+              ("removable", "0"),+              ("size", "251658240")+            ])+          ],+          metaCpu = Just [+            HostMetaCpu {+              metaCpuCacheSize = Just "25600 KB",+              metaCpuCoreId = Just "0",+              metaCpuCores = Just "1",+              metaCpuFamily = Just "6",+              metaCpuMhz = Just "2500.064",+              metaCpuModel = Just "62",+              metaCpuModelName = Just "Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz",+              metaCpuPhysicalId = Just "0",+              metaCpuStepping = Just "4",+              metaCpuVendorId = Just "GenuineIntel"+            }+          ],+          metaFilesystem = Just $ HM.fromList [+            ("/dev/xvda", HM.fromList [+              ("kb_available", Number $ read "2526784"),+              ("kb_size", Number $ read "8125880"),+              ("kb_used", Number $ read "5163284"),+              ("mount", "/"),+              ("percent_used", "68%")+            ])+          ],+          metaKernel = Just $ HM.fromList [+            ("machine", "x86_64"),+            ("name", "Linux"),+            ("os", "GNU/Linux"),+            ("platform_name", "Debian"),+            ("platform_version", "8.3"),+            ("release", "3.16.0-4-amd64"),+            ("version", "#1 SMP Debian 3.16.7-ckt20-1+deb8u3 (2016-01-17)")+          ],+          metaMemory = Just $ HM.fromList [+            ("active", "77456kB"),+            ("buffers", "11720kB"),+            ("cached", "83456kB"),+            ("free", "3685992kB"),+            ("inactive", "59368kB"),+            ("total", "3865572kB")+          ],+          metaCloud = Just HostMetaCloud {+            metaCloudProvider = "ec2",+            metaCloudMetadata = HM.fromList [+              ("ami-id", "ami-000"),+              ("hostname", "ip-0-0-0-0"),+              ("instance-id", "i-00000"),+              ("instance-type", "m3.medium")+            ]+          }+        },+        hostInterfaces = [+          HostInterface {+            hostInterfaceName = "en0",+            hostInterfaceMacAddress = Just "ff:ff:ff:ff:ff:ff",+            hostInterfaceIpv4Addresses = Just ["192.168.100.2"],+            hostInterfaceIpv6Addresses = Just ["2001:268:c00f:8a08::"]+          }+        ]+      }++  let json2 = [aesonQQ|+    {+      "id": "abcede",+      "name": "foohost.example",+      "displayName": "Host Foo",+      "status": "working",+      "memo": "host memo",+      "roles": {+        "foo": ["bar", "baz"],+        "qux": ["quux"]+      },+      "isRetired": false,+      "createdAt": 1483196400,+      "meta": {+        "agent-name": "mackerel-agent/0.37.0 (Revision 3c11c2c)",+        "agent-revision": "3c11c2c",+        "agent-version": "0.37.0",+        "block_device": {+          "xvda": {+            "removable": "0",+            "size": "251658240"+          }+        },+        "cpu": [+          {+            "cache_size": "25600 KB",+            "core_id": "0",+            "cores": "1",+            "family": "6",+            "mhz": "2500.064",+            "model": "62",+            "model_name": "Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz",+            "physical_id": "0",+            "stepping": "4",+            "vendor_id": "GenuineIntel"+          }+        ],+        "filesystem": {+          "/dev/xvda": {+            "kb_available": 2526784,+            "kb_size": 8125880,+            "kb_used": 5163284,+            "mount": "/",+            "percent_used": "68%"+          }+        },+        "kernel": {+          "machine": "x86_64",+          "name": "Linux",+          "os": "GNU/Linux",+          "platform_name": "Debian",+          "platform_version": "8.3",+          "release": "3.16.0-4-amd64",+          "version": "#1 SMP Debian 3.16.7-ckt20-1+deb8u3 (2016-01-17)"+        },+        "memory": {+          "active": "77456kB",+          "buffers": "11720kB",+          "cached": "83456kB",+          "free": "3685992kB",+          "inactive": "59368kB",+          "total": "3865572kB"+        },+        "cloud": {+          "provider": "ec2",+          "metadata": {+            "ami-id": "ami-000",+            "hostname": "ip-0-0-0-0",+            "instance-id": "i-00000",+            "instance-type": "m3.medium"+          }+        }+      },+      "interfaces": [+        {+          "name": "en0",+          "macAddress": "ff:ff:ff:ff:ff:ff",+          "ipv4Addresses": ["192.168.100.2"],+          "ipv6Addresses": ["2001:268:c00f:8a08::"]+        }+      ]+    }+  |]++  describe "Host FromJSON" $ do+    it "should parse a json" $+      forM_ [(json1, host1), (json2, host2)] $ \(json, host) ->+        decode (encode json) `shouldBe` Just host++    it "should reject an invalid json" $ do+      decode "{}" `shouldBe` (Nothing :: Maybe Host)+      let (Object hm) = json1+      forM_ ["id", "name", "status", "memo", "roles", "isRetired", "createdAt", "meta", "interfaces"] $ \key ->+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Host)+      forM_ ["status", "roles", "isRetired", "createdAt", "meta", "interfaces"] $ \key ->+        decode (encode (Object (HM.insert key "invalid" hm))) `shouldBe` (Nothing :: Maybe Host)++  describe "Host ToJSON" $+    it "should encode into a json" $+      forM_ [(json1, host1), (json2, host2)] $ \(json, host) ->+        decode (encode host) `shouldBe` Just json++  describe "HostStatus Show" $+    it "should show properly" $ do+      show HostStatusWorking `shouldBe` "working"+      show HostStatusMaintenance `shouldBe` "maintenance"+      show [HostStatusWorking, HostStatusStandby, HostStatusMaintenance, HostStatusPoweroff] `shouldBe` "[working,standby,maintenance,poweroff]"++  describe "HostStatus Read" $+    it "should read properly" $ do+      read "working" `shouldBe` HostStatusWorking+      read "maintenance" `shouldBe` HostStatusMaintenance+      read "[working,standby,maintenance,poweroff]" `shouldBe` [HostStatusWorking, HostStatusStandby, HostStatusMaintenance, HostStatusPoweroff]++  let hostCreate1 = HostCreate {+        hostCreateName = "foohost.example",+        hostCreateDisplayName = Nothing,+        hostCreateCustomIdentifier = Nothing,+        hostCreateMeta = def,+        hostCreateInterfaces = Nothing,+        hostCreateRoleFullnames = Nothing,+        hostCreateChecks = Nothing+      }++  let jsonCreate1 = [aesonQQ|+    {+      "name": "foohost.example",+      "meta": {}+    }+  |]++  let hostCreate2 = HostCreate {+        hostCreateName = "foohost.example",+        hostCreateDisplayName = Just "Foo host",+        hostCreateCustomIdentifier = Just "foo-host-identifier",+        hostCreateMeta = def {+          metaAgentName = Just "mackerel-agent/0.37.0 (Revision 3c11c2c)",+          metaAgentRevision = Just "3c11c2c",+          metaAgentVersion = Just "0.37.0"+        },+        hostCreateInterfaces = Just [+          HostInterface {+            hostInterfaceName = "en0",+            hostInterfaceMacAddress = Just "ff:ff:ff:ff:ff:ff",+            hostInterfaceIpv4Addresses = Just ["192.168.100.2"],+            hostInterfaceIpv6Addresses = Just ["2001:268:c00f:8a08::"]+          }+        ],+        hostCreateRoleFullnames = Just ["foo:bar", "foo:baz"],+        hostCreateChecks = Just [+          "plugin.checks.access_log",+          "plugin.checks.check_cron"+        ]+      }++  let jsonCreate2 = [aesonQQ|+    {+      "name": "foohost.example",+      "displayName":  "Foo host",+      "customIdentifier": "foo-host-identifier",+      "meta": {+        "agent-name": "mackerel-agent/0.37.0 (Revision 3c11c2c)",+        "agent-revision": "3c11c2c",+        "agent-version": "0.37.0"+      },+      "interfaces": [+        {+          "name": "en0",+          "macAddress": "ff:ff:ff:ff:ff:ff",+          "ipv4Addresses": ["192.168.100.2"],+          "ipv6Addresses": ["2001:268:c00f:8a08::"]+        }+      ],+      "roleFullnames": [+        "foo:bar",+        "foo:baz"+      ],+      "checks": [+        "plugin.checks.access_log",+        "plugin.checks.check_cron"+      ]+    }+  |]++  describe "HostCreate FromJSON" $+    it "should parse a json" $+      forM_ [(jsonCreate1, hostCreate1), (jsonCreate2, hostCreate2)] $ \(json, host) ->+        decode (encode json) `shouldBe` Just host++  describe "HostCreate ToJSON" $+    it "should encode into a json" $+      forM_ [(jsonCreate1, hostCreate1), (jsonCreate2, hostCreate2)] $ \(json, host) ->+        decode (encode host) `shouldBe` Just json
+ test/Types/MonitorSpec.hs view
@@ -0,0 +1,173 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.MonitorSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode)+import Data.Aeson.QQ+import Test.Hspec++import Web.Mackerel.Types.Monitor++spec :: Spec+spec = do++  let hostMonitor = MonitorHost {+        monitorId = Just $ MonitorId "abcde1",+        monitorName = "Monitor custom.foo.bar",+        monitorDuration = 5,+        monitorMetric = "custom.foo.bar",+        monitorOperator = MonitorGreaterThan,+        monitorWarning = 10.0,+        monitorCritical = 20.0,+        monitorIsMute = Just False,+        monitorNotificationInterval = Just 30,+        monitorScopes = Just ["service0"],+        monitorExcludeScopes = Just ["service0:role3"]+      }++  let hostMonitorJson = [aesonQQ|+    {+      "type": "host",+      "id": "abcde1",+      "name": "Monitor custom.foo.bar",+      "duration": 5,+      "metric": "custom.foo.bar",+      "operator": ">",+      "warning": 10.0,+      "critical": 20.0,+      "isMute": false,+      "notificationInterval": 30,+      "scopes": ["service0"],+      "excludeScopes": ["service0:role3"]+    }+  |]++  let connectivity = MonitorConnectivity {+        monitorId = Just $ MonitorId "abcde2",+        monitorName = "connectivity",+        monitorIsMute = Just False,+        monitorNotificationInterval = Nothing,+        monitorScopes = Nothing,+        monitorExcludeScopes = Nothing+      }++  let connectivityJson = [aesonQQ|+    {+      "type": "connectivity",+      "id": "abcde2",+      "name": "connectivity",+      "isMute": false+    }+  |]++  let serviceMonitor = MonitorService {+        monitorId = Just $ MonitorId "abcde3",+        monitorName = "Service count",+        monitorService = "service1",+        monitorDuration = 5,+        monitorMetric = "custom.service.count",+        monitorOperator = MonitorGreaterThan,+        monitorWarning = 100,+        monitorCritical = 200,+        monitorIsMute = Just False,+        monitorNotificationInterval = Just 30+      }++  let serviceMonitorJson = [aesonQQ|+    {+      "type": "service",+      "id": "abcde3",+      "name": "Service count",+      "service": "service1",+      "duration": 5,+      "metric": "custom.service.count",+      "operator": ">",+      "warning": 100,+      "critical": 200,+      "isMute": false,+      "notificationInterval": 30+    }+  |]++  let externalMonitor = MonitorExternal {+        monitorId = Just $ MonitorId "abcde4",+        monitorName = "Example external monitor",+        monitorUrl = "https://example.com",+        monitorServiceOption = Just "service1",+        monitorResponseTimeDuration = Just 5,+        monitorResponseTimeWarning = Just 3000,+        monitorResponseTimeCritical = Just 5000,+        monitorContainsString = Just "Example Domain",+        monitorMaxCheckAttempts = Just 5,+        monitorCertificationExpirationWarning = Just 1200,+        monitorCertificationExpirationCritical = Just 60,+        monitorSkipCertificateVerification = Just True,+        monitorHeaders = Just [MonitorExternalHeader "Cache-Control" "no-cache"],+        monitorIsMute = Just True,+        monitorNotificationInterval = Just 60+      }++  let externalMonitorJson = [aesonQQ|+    {+      "type": "external",+      "id": "abcde4",+      "name": "Example external monitor",+      "url": "https://example.com",+      "service": "service1",+      "responseTimeDuration": 5,+      "responseTimeWarning": 3000,+      "responseTimeCritical": 5000,+      "containsString": "Example Domain",+      "maxCheckAttempts": 5,+      "certificationExpirationWarning": 1200,+      "certificationExpirationCritical": 60,+      "skipCertificateVerification": true,+      "headers": [{ "name": "Cache-Control", "value": "no-cache" }],+      "isMute": true,+      "notificationInterval": 60+    }+  |]++  let expressionMonitor = MonitorExpression {+        monitorId = Just $ MonitorId "abcde5",+        monitorName = "Example expression monitor",+        monitorExpression = "min(role(\"foo:bar\", \"custom.foo.bar\"))",+        monitorOperator = MonitorLessThan,+        monitorWarning = 10.0,+        monitorCritical = 20.0,+        monitorIsMute = Just False,+        monitorNotificationInterval = Nothing+      }++  let expressionMonitorJson = [aesonQQ|+    {+      "type": "expression",+      "id": "abcde5",+      "name": "Example expression monitor",+      "expression": "min(role(\"foo:bar\", \"custom.foo.bar\"))",+      "operator": "<",+      "warning": 10.0,+      "critical": 20.0,+      "isMute": false+    }+  |]++  let monitors = [+          (hostMonitor, hostMonitorJson),+          (connectivity, connectivityJson),+          (serviceMonitor, serviceMonitorJson),+          (externalMonitor, externalMonitorJson),+          (expressionMonitor, expressionMonitorJson)+        ]++  describe "Monitor FromJSON" $+    it "should parse jsons" $ do+      forM_ monitors $ \(monitor, json) ->+        decode (encode json) `shouldBe` Just monitor+      decode (encode (map snd monitors)) `shouldBe` Just (map fst monitors)++  describe "Monitor ToJSON" $+    it "should encode into jsons" $ do+      forM_ monitors $ \(monitor, json) ->+        decode (encode monitor) `shouldBe` Just json+      decode (encode (map fst monitors)) `shouldBe` Just (map snd monitors)
+ test/Types/OrganizationSpec.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.OrganizationSpec where++import Data.Aeson (decode, encode)+import Data.Aeson.QQ+import Test.Hspec++import Web.Mackerel.Types.Organization++spec :: Spec+spec = do++  let organization = Organization {+        organizationName = "FooOrganization"+      }++  let json = [aesonQQ|+    {+      "name": "FooOrganization"+    }+  |]++  describe "Organization FromJSON" $ do+    it "should parse a json" $+      decode (encode json) `shouldBe` Just organization++    it "should reject an invalid json" $+      decode "{}" `shouldBe` (Nothing :: Maybe Organization)++  describe "Organization ToJSON" $+    it "should encode into a json" $+      decode (encode organization) `shouldBe` Just json
+ test/Types/RoleSpec.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.RoleSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.Role++spec :: Spec+spec = do++  let role' = Role {+        roleName = "FooRole",+        roleMemo = "Role memo"+      }++  let json = [aesonQQ|+    {+      "name": "FooRole",+      "memo": "Role memo"+    }+  |]++  describe "Role FromJSON" $ do+    it "should parse a json" $+      decode (encode json) `shouldBe` Just role'++    it "should reject an invalid json" $ do+      decode "{}" `shouldBe` (Nothing :: Maybe Role)+      let (Object hm) = json+      forM_ ["name", "memo"] $ \key ->+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Role)++  describe "Role ToJSON" $+    it "should encode into a json" $+      decode (encode role') `shouldBe` Just json
+ test/Types/ServiceSpec.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.ServiceSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.Service++spec :: Spec+spec = do++  let service = Service {+        serviceName = "FooService",+        serviceMemo = "service memo",+        serviceRoles = [ "role0", "role1", "role2" ]+      }++  let json = [aesonQQ|+    {+      "name": "FooService",+      "memo": "service memo",+      "roles": [+        "role0",+        "role1",+        "role2"+      ]+    }+  |]++  describe "Service FromJSON" $ do+    it "should parse a json" $+      decode (encode json) `shouldBe` Just service++    it "should reject an invalid json" $ do+      decode "{}" `shouldBe` (Nothing :: Maybe Service)+      let (Object hm) = json+      forM_ ["name", "memo", "roles"] $ \key ->+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Service)++  describe "Service ToJSON" $+    it "should encode into a json" $+      decode (encode service) `shouldBe` Just json
+ test/Types/UserSpec.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.UserSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.User++spec :: Spec+spec = do++  let user = User {+        userId = UserId "abcde",+        userScreenName = "Example Mackerel",+        userEmail = "mackerel@example.com"+      }++  let json = [aesonQQ|+    {+      "id": "abcde",+      "screenName": "Example Mackerel",+      "email": "mackerel@example.com"+    }+  |]++  describe "User FromJSON" $ do+    it "should parse a json" $+      decode (encode json) `shouldBe` Just user++    it "should reject an invalid json" $ do+      decode "{}" `shouldBe` (Nothing :: Maybe User)+      let (Object hm) = json+      forM_ ["id", "screenName", "email"] $ \key ->+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe User)++  describe "User ToJSON" $+    it "should encode into a json" $+      decode (encode user) `shouldBe` Just json