{-# 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 -> "" 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