libconfig-0.3.0.0: src/Language/Libconfig/Decode.hs
{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-|
Module : Language.Libconfig.Decode
Copyright : (c) Matthew Peddie 2014
License : BSD3
Maintainer : mpeddie@gmail.com
Stability : experimental
Portability : GHC
Converting libconfig native data into friendly Haskell structures from
"Language.Libconfig.Types".
-}
module Language.Libconfig.Decode (
-- * Decoding libconfig native data
decode
, decodeFrom
-- * Decoding errors
, DecodeError(..)
) where
import Control.Applicative
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except
import Control.Monad.Trans.Reader
import Data.Data (Data)
import Data.Typeable (Typeable)
import GHC.Generics (Generic)
import Control.DeepSeq (NFData)
import qualified Data.Text as T (pack)
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
import Language.Libconfig.Types
import Language.Libconfig.Bindings (ConfigType(..), ConfigFormat(..))
import qualified Language.Libconfig.Bindings as C
-- | Any of these problems can occur while decoding a @libconfig@
-- 'C.Configuration'.
data DecodeError = DecoderRoot -- ^ No root setting was found (possibly this
-- configuration is invalid?)
| Name {
decodeErrSetting :: Text -- ^ This setting had no name
-- but was in a 'Group'.
}
| GetNone {
decodeErrSetting :: Text -- ^ This setting was of type
-- 'NoneType', but it should
-- have a type.
}
| GetIndex {
decodeErrParent :: Text -- ^ Failed to get a child of
-- this 'C.Setting'
, decodeErrIndex :: Int -- ^ This was the index we
-- tried to look up
}
| Parse {
decodeErrFilename :: Text -- ^ The file in which
-- parsing failed
, decodeErrLine :: Word32 -- ^ The line of the file on
-- which parsing failed
, decodeErrDescription :: Text -- ^ @libconfig@'s description
-- of the parsing failure
}
| FileInput {
decodeErrFilename :: Text -- ^ Failed to open this file
} deriving (Eq, Ord, Show, Read, Data, Typeable, Generic)
instance NFData DecodeError
withErr :: Maybe a -> e -> Either e a
withErr Nothing e = Left e
withErr (Just x) _ = Right x
decoder :: IO (Either DecodeError a) -> Decoder a
decoder = lift . ExceptT
throw :: DecodeError -> Decoder a
throw = lift . throwE
catchD :: (DecodeError -> ExceptT DecodeError IO a) -> Decoder a -> Decoder a
catchD handler action =
ReaderT $ \conf -> catchE (runReaderT action conf) handler
type Decoder a = ReaderT ConfigFormat (ExceptT DecodeError IO) a
textToNameErr :: Text -> Name
textToNameErr text = fromMaybe err $ textToName text
where
err = error $ "Language.Libconfig.Decode.textToNameErr: " ++
"C library passed an invalid 'Name' value " ++ show text ++ "!"
toScalar :: C.Setting -> Decoder Scalar
toScalar s = do
ty <- liftIO $ C.configSettingType s
localFormat <- liftIO $ C.configSettingGetFormat s
format <- case localFormat of
HexFormat -> return HexFormat
DefaultFormat -> ask
decoder $ go format ty
where
go :: ConfigFormat -> ConfigType -> IO (Either DecodeError Scalar)
go DefaultFormat IntType =
Right . Integer . fromIntegral <$> C.configSettingGetInt s
go DefaultFormat Int64Type =
Right . Integer64 . fromIntegral <$> C.configSettingGetInt64 s
go HexFormat IntType =
Right . Hex . fromIntegral <$> C.configSettingGetInt s
go HexFormat Int64Type =
Right . Hex64 . fromIntegral <$> C.configSettingGetInt64 s
go _ FloatType = Right . Float <$> C.configSettingGetFloat s
go _ BoolType = Right . Boolean <$> C.configSettingGetBool s
go _ StringType = Right . String . T.pack <$> C.configSettingGetString s
go _ t =
error $ "Language.Libconfig.Decode.toScalar: internal error (bug!): expected " ++
"a type in [IntType, Int64Type, FloatType, BoolType, StringType], but got '" ++
show t ++ "'!"
toList :: C.Setting -> Decoder List
toList s = do
ty <- liftIO $ C.configSettingType s
addParent s $ go ty
where
go ListType = do
l <- liftIO $ C.configSettingLength s
mapM get [0 .. l - 1]
go ty =
error $ "Language.Libconfig.Decode.toList: internal error (bug!): expected " ++
"a value with 'ListType', but got '" ++ show ty ++ "'!"
get :: Int -> Decoder Value
get i = do
el <- decoder $ (`withErr` GetIndex "" i) <$> C.configSettingGetElem s i
toValue el
toArray :: C.Setting -> Decoder Array
toArray s = addParent s $ liftIO (C.configSettingType s) >>= go
where
go ArrayType = do
l <- liftIO $ C.configSettingLength s
mapM get [0 .. l - 1]
go ty =
error $ "Language.Libconfig.Decode.toArray: internal error (bug!): expected " ++
"a value with 'ArrayType', but got '" ++ show ty ++ "'!"
get i = do
el <- decoder $ (`withErr` GetIndex "" i) <$> C.configSettingGetElem s i
toScalar el
toGroup :: C.Setting -> Decoder Group
toGroup s = addParent s $ liftIO (C.configSettingType s) >>= go
where
go GroupType = do
l <- liftIO $ C.configSettingLength s
mapM get [0 .. l - 1]
go ty =
error $ "Language.Libconfig.Decode.toGroup: internal error (bug!): expected " ++
"a value with 'GroupType', but got '" ++ show ty ++ "'!"
get i = do
el <- decoder $ (`withErr` GetIndex "" i) <$> C.configSettingGetElem s i
decodeSetting el
toValue :: C.Setting -> Decoder Value
toValue s = addParent s $ liftIO (C.configSettingType s) >>= go
where
go NoneType = throw $ GetNone ""
go ListType = List <$> toList s
go ArrayType = Array <$> toArray s
go GroupType = Group <$> toGroup s
go _ = Scalar <$> toScalar s
addParent :: C.Setting -> Decoder a -> Decoder a
addParent s = catchD handler
where
mapSetting _ e@(Parse{}) = e
mapSetting _ e@(FileInput _) = e
mapSetting f (GetIndex p i) = GetIndex (f p) i
mapSetting f e = e { decodeErrSetting = f (decodeErrSetting e) }
handler e = do
name <- liftIO $ getName s
throwE $ mapSetting ((name <> ".") <>) e
getName :: C.Setting -> IO Text
getName s = do
name <- C.configSettingName s
return $ case name of
Nothing -> "<no name>"
Just x -> T.pack x
decodeSetting :: C.Setting -> Decoder Setting
decodeSetting s = addParent s $ liftIO (C.configSettingType s) >>= go
where
go NoneType = throw $ GetNone ""
go _ =
(:=) <$>
fmap (textToNameErr . T.pack)
(decoder $ (`withErr` Name "") <$> C.configSettingName s) <*>
toValue s
-- | Convert a native 'C.Configuration' into a top-level 'Group' of
-- 'Setting's.
--
-- >>> Just conf <- C.configNew "test/test.conf"
-- >>> decode conf
-- Right ["version" := Scalar (String "1.0"),"application" := Group ["window" := Group ["title" := Scalar (String "My Application"),"size" := Group ["w" := Scalar (Integer 640),"h" := Scalar (Integer 480)],"pos" := Group ["x" := Scalar (Integer 350),"y" := Scalar (Integer 250)]],"list" := List [List [Scalar (String "abc"),Scalar (Integer 123),Scalar (Boolean True)],Scalar (Float 1.234),List []],"books" := List [Group ["title" := Scalar (String "Treasure Island"),"author" := Scalar (String "Robert Louis Stevenson"),"price" := Scalar (Float 29.95),"qty" := Scalar (Integer 5)],Group ["title" := Scalar (String "Snow Crash"),"author" := Scalar (String "Neal Stephenson"),"price" := Scalar (Float 9.99),"qty" := Scalar (Integer 8)]],"misc" := Group ["pi" := Scalar (Float 3.141592654),"bigint" := Scalar (Integer64 9223372036854775807),"columns" := Array [String "Last Name",String "First Name",String "MI"],"bitmask" := Scalar (Hex 8131)]]]
decode :: C.Configuration -> IO (Either DecodeError Group)
decode c = do
format <- C.configGetDefaultFormat c
res <- runExceptT $ runReaderT (getRoot c >>= toGroup) format
C.touchConfiguration c
return res
where
getRoot cnf = decoder $ (`withErr` DecoderRoot) <$> C.configRootSetting cnf
-- | Load the libconfig configuration file at the given path and try
-- to convert it to a top-level 'Group' of 'Setting's.
--
-- >>> decodeFrom "test/test.conf"
-- Right ["version" := Scalar (String "1.0"),"application" := Group ["window" := Group ["title" := Scalar (String "My Application"),"size" := Group ["w" := Scalar (Integer 640),"h" := Scalar (Integer 480)],"pos" := Group ["x" := Scalar (Integer 350),"y" := Scalar (Integer 250)]],"list" := List [List [Scalar (String "abc"),Scalar (Integer 123),Scalar (Boolean True)],Scalar (Float 1.234),List []],"books" := List [Group ["title" := Scalar (String "Treasure Island"),"author" := Scalar (String "Robert Louis Stevenson"),"price" := Scalar (Float 29.95),"qty" := Scalar (Integer 5)],Group ["title" := Scalar (String "Snow Crash"),"author" := Scalar (String "Neal Stephenson"),"price" := Scalar (Float 9.99),"qty" := Scalar (Integer 8)]],"misc" := Group ["pi" := Scalar (Float 3.141592654),"bigint" := Scalar (Integer64 9223372036854775807),"columns" := Array [String "Last Name",String "First Name",String "MI"],"bitmask" := Scalar (Hex 8131)]]]
decodeFrom :: String -> IO (Either DecodeError Group)
decodeFrom filename = do
c <- C.configInit
red <- C.configReadFile c filename
case red of
Nothing -> do
ty <- C.configErrorType c
fn <- maybe (T.pack filename) T.pack <$> C.configErrorFile c
case ty of
C.ConfigErrFileIo -> return . Left $ FileInput fn
C.ConfigErrParse -> do
err <- Parse fn <$>
(fromIntegral <$> C.configErrorLine c) <*>
(maybe "" T.pack <$> C.configErrorText c)
return $ Left err
_ ->
error "Language.Libconfig.Decode.decodeFrom: something is really broken!"
Just _ -> decode c