git-annex/Messages/JSON.hs

191 lines
5.1 KiB
Haskell
Raw Normal View History

{- git-annex command-line JSON output and input
-
- Copyright 2011-2018 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
{-# LANGUAGE OverloadedStrings, GADTs #-}
2016-07-26 18:10:29 +00:00
module Messages.JSON (
JSONBuilder,
JSONChunk(..),
emit,
none,
start,
end,
finalize,
addErrorMessage,
note,
info,
2011-12-23 02:03:18 +00:00
add,
complete,
progress,
DualDisp(..),
ObjectMap(..),
JSONActionItem(..),
) where
2016-07-26 18:10:29 +00:00
import Control.Applicative
import qualified Data.Map as M
import qualified Data.Vector as V
import qualified Data.ByteString.Lazy as L
import qualified Data.HashMap.Strict as HM
import System.IO
import System.IO.Unsafe (unsafePerformIO)
import Control.Concurrent
import Data.Maybe
2016-08-08 14:49:49 +00:00
import Data.Monoid
import Prelude
import Types.Messages
2017-02-24 17:42:30 +00:00
import Key
import Utility.Metered
import Utility.Percentage
import Utility.Aeson
-- A global lock to avoid concurrent threads emitting json at the same time.
{-# NOINLINE emitLock #-}
emitLock :: MVar ()
emitLock = unsafePerformIO $ newMVar ()
emit :: Object -> IO ()
emit o = do
takeMVar emitLock
L.hPut stdout (encode o)
putStr "\n"
putMVar emitLock ()
-- Building up a JSON object can be done by first using start,
-- then add and note any number of times, and finally complete.
type JSONBuilder = Maybe (Object, Bool) -> Maybe (Object, Bool)
none :: JSONBuilder
none = id
start :: String -> Maybe FilePath -> Maybe Key -> JSONBuilder
start command file key _ = Just (o, False)
2012-11-11 04:51:07 +00:00
where
Object o = toJSON' $ JSONActionItem
{ itemCommand = Just command
, itemKey = key
, itemFile = file
, itemAdded = Nothing
}
end :: Bool -> JSONBuilder
end b (Just (o, _)) = Just (HM.insert "success" (toJSON' b) o, True)
end _ Nothing = Nothing
finalize :: JSONOptions -> Object -> Object
finalize jsonoptions o
-- Always include error-messages field, even if empty,
-- to make the json be self-documenting.
| jsonErrorMessages jsonoptions = addErrorMessage [] o
| otherwise = o
addErrorMessage :: [String] -> Object -> Object
addErrorMessage msg o =
2018-02-19 19:39:52 +00:00
HM.insertWith combinearray "error-messages" v o
where
combinearray (Array new) (Array old) = Array (old <> new)
combinearray new _old = new
v = Array $ V.fromList $ map (String . packString) msg
note :: String -> JSONBuilder
note _ Nothing = Nothing
note s (Just (o, e)) = Just (HM.insertWith combinelines "note" (toJSON' s) o, e)
where
combinelines (String new) (String old) =
String (old <> "\n" <> new)
combinelines new _old = new
info :: String -> JSONBuilder
info s _ = Just (o, True)
where
Object o = object ["info" .= toJSON' s]
data JSONChunk v where
AesonObject :: Object -> JSONChunk Object
JSONChunk :: ToJSON' v => [(String, v)] -> JSONChunk [(String, v)]
2011-12-23 02:03:18 +00:00
add :: JSONChunk v -> JSONBuilder
add v (Just (o, e)) = Just (HM.union o' o, e)
where
Object o' = case v of
AesonObject ao -> Object ao
JSONChunk l -> object $ map mkPair l
mkPair (s, d) = (packString s, toJSON' d)
add _ Nothing = Nothing
complete :: JSONChunk v -> JSONBuilder
complete v _ = add v (Just (HM.empty, True))
-- Show JSON formatted progress, including the current state of the JSON
-- object for the action being performed.
progress :: Maybe Object -> Maybe Integer -> BytesProcessed -> IO ()
progress maction msize bytesprocessed = emit $ case maction of
Just action -> HM.insert "action" (Object action) o
Nothing -> o
where
n = fromBytesProcessed bytesprocessed :: Integer
Object o = case msize of
Just size -> object
[ "byte-progress" .= n
, "percent-progress" .= showPercentage 2 (percentage size n)
, "total-size" .= size
]
Nothing -> object
[ "byte-progress" .= n ]
-- A value that can be displayed either normally, or as JSON.
data DualDisp = DualDisp
{ dispNormal :: String
, dispJson :: String
}
instance ToJSON' DualDisp where
toJSON' = toJSON' . dispJson
instance Show DualDisp where
show = dispNormal
2016-07-26 18:10:29 +00:00
-- A Map that is serialized to JSON as an object, with each key being a
-- field of the object. This is different from Aeson's normal
-- serialization of Map, which uses "[key, value]".
data ObjectMap a = ObjectMap { fromObjectMap :: M.Map String a }
instance ToJSON' a => ToJSON' (ObjectMap a) where
toJSON' (ObjectMap m) = object $ map go $ M.toList m
where
go (k, v) = (packString k, toJSON' v)
-- An item that a git-annex command acts on, and displays a JSON object about.
data JSONActionItem a = JSONActionItem
{ itemCommand :: Maybe String
, itemKey :: Maybe Key
, itemFile :: Maybe FilePath
, itemAdded :: Maybe a -- for additional fields added by `add`
2016-07-26 18:10:29 +00:00
}
deriving (Show)
instance ToJSON' (JSONActionItem a) where
toJSON' i = object $ catMaybes
[ Just $ "command" .= itemCommand i
, case itemKey i of
Nothing -> Nothing
Just k -> Just $ "key" .= toJSON' k
, Just $ "file" .= toJSON' (itemFile i)
-- itemAdded is not included; must be added later by 'add'
]
instance FromJSON a => FromJSON (JSONActionItem a) where
parseJSON (Object v) = JSONActionItem
2016-07-26 18:10:29 +00:00
<$> (v .:? "command")
<*> (maybe (return Nothing) parseJSON =<< (v .:? "key"))
<*> (v .:? "file")
2016-07-26 18:10:29 +00:00
<*> parseadded
where
parseadded = (Just <$> parseJSON (Object v)) <|> return Nothing
parseJSON _ = mempty