{-# OPTIONS_GHC -Wall #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveDataTypeable #-} {-| Module : Language.Libconfig.Encode Copyright : (c) Matthew Peddie 2014 License : BSD3 Maintainer : mpeddie@gmail.com Stability : experimental Portability : GHC Converting from "Language.Libconfig.Types" structures to native libconfig 'C.Configuration's. -} module Language.Libconfig.Encode ( -- $setup -- * Encoding libconfig native data encode , encodeAt , encodeValue , encodeTo -- * Encoding errors , EncodeError(..) -- * Helpers , valueType , scalarType ) where import Control.Applicative import Data.Data (Data) import Data.Typeable (Typeable) import GHC.Generics (Generic) import Control.DeepSeq (NFData) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Maybe import Control.Monad.Trans.Except import Control.Monad (when, replicateM_) import qualified Data.Text as T (unpack, pack, empty) import Data.Monoid ((<>)) import Language.Libconfig.Types import Language.Libconfig.Bindings (ConfigType(..), ConfigFormat(..)) import qualified Language.Libconfig.Bindings as C -- $setup -- To run these usage examples, you must tell GHC it's allowed to -- parse string literals as 'Text' values: -- -- >>> :set -XOverloadedStrings -- -- We also pre-define the @test@ 'Group' to be the example from the -- @libconfig@ manual. -- -- >>> let Just [version, application, window, title, size, w, h, pos, x, y, list, books, author, price, qty, misc, pi, columns, bigint, bitmask] = mapM textToName ["version", "application", "window", "title", "size", "w", "h", "pos", "x", "y", "list", "books", "author", "price", "qty", "misc", "pi", "columns", "bigint", "bitmask"] -- >>> let test = [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)]]] -- | Any of these problems can occur while encoding a @libconfig@ -- 'C.Configuration'. data EncodeError = EncoderRoot -- ^ No root setting was found -- (possibly this configuration is -- invalid?) | TypeMismatch { encodeErrSetting :: Text -- ^ The Haskell structure contained -- invalid libconfig types for this setting. } | FileOutput { encodeErrFilename :: Text -- ^ Failed to open this file } | AddSetting { encodeErrParent :: Text -- ^ Failed to create a new 'C.Setting' , encodeErrValue :: Value -- ^ This was the value we -- were making a spot for } | RemoveOldValue { encodeErrSetting :: Text -- ^ Failed to remove an old value -- from this setting } | SetValue { encodeErrSetting :: Text -- ^ Failed to assign a value -- to this 'C.Setting' , encodeErrValue :: Value -- ^ This was the value we -- tried to assign } | SetIndex { encodeErrParent :: Text -- ^ Failed to assign an element -- within this collection , encodeErrIndex :: Int -- ^ This is the index of the element , encodeErrValue :: Value -- ^ This is the value we -- tried to assign } deriving (Eq, Ord, Show, Read, Data, Typeable, Generic) instance NFData EncodeError type Encoder a = ExceptT EncodeError IO a withErr :: Maybe a -> e -> Either e a withErr Nothing e = Left e withErr (Just x) _ = Right x addTrace :: C.Setting -> Encoder a -> Encoder a addTrace s = flip catchE handler where mapSetting _ e@(FileOutput _) = e mapSetting f (SetIndex p i v) = SetIndex (f p) i v mapSetting f (AddSetting p v) = AddSetting (f p) v mapSetting f e = e { encodeErrSetting = f (encodeErrSetting e) } repair name path | path == T.empty = name | otherwise = name <> "." <> path handler e = do name <- liftIO $ getName s throwE $ mapSetting (repair name) e getName :: C.Setting -> IO Text getName s = do name <- C.configSettingName s return $ case name of Nothing -> "" Just x -> T.pack x scalarSet :: C.Setting -> Scalar -> Encoder () scalarSet sp v = addTrace sp $ ExceptT $ (`withErr` SetValue "" (Scalar v)) <$> go v where go (Boolean b) = C.configSettingSetBool sp b go (Integer i) = C.configSettingSetInt sp (fromIntegral i) go (Integer64 i) = C.configSettingSetInt64 sp i go (Float f) = C.configSettingSetFloat sp f go (String s) = C.configSettingSetString sp (T.unpack s) go (Hex h) = runMaybeT $ do MaybeT $ C.configSettingSetFormat sp HexFormat MaybeT $ C.configSettingSetInt sp (fromIntegral h) go (Hex64 h) = runMaybeT $ do MaybeT $ C.configSettingSetFormat sp HexFormat MaybeT $ C.configSettingSetInt64 sp (fromIntegral h) addValue :: Text -> C.Setting -> Value -> Encoder C.Setting addValue nm parent value = do newset <- ExceptT $ (`withErr` AddSetting "" value) <$> add addTrace newset $ setValue newset value return newset where add = C.configSettingAdd parent (T.unpack nm) (valueType value) addSetting :: C.Setting -> Setting -> Encoder C.Setting addSetting parent (name := value) = addValue (nameToText name) parent value setValue :: C.Setting -> Value -> Encoder () setValue sp (Scalar s) = scalarSet sp s setValue sp (Group g) = mapM_ (addSetting sp) g setValue sp (List l) = mapM_ (addValue "" sp) l setValue sp (Array a) = if arrayCheck a then mapM_ (addValue "" sp . Scalar) a else throwE $ TypeMismatch "" -- | Convert a top-level 'Group' of 'Setting's into a native -- 'C.Configuration'. This allocates a new 'C.Configuration'. -- -- >>> Right conf <- encode test -- >>> C.configWriteFile conf "/tmp/encode_output_test.conf" -- Just () -- -- >>> Just newconf <- C.configNew "/tmp/encode_output_test.conf" encode :: Group -> IO (Either EncodeError C.Configuration) encode g = runExceptT $ do conf <- liftIO C.configInit ExceptT $ encodeAt conf g return conf -- | Convert a top-level 'Group' of 'Setting's into a native -- libconfig structure and output it to the specified file path. -- -- >>> encodeTo test "/tmp/encode_output_test_2.conf" -- Right () -- -- >>> Just newconf <- C.configNew "/tmp/encode_output_test_2.conf" encodeTo :: Group -> String -> IO (Either EncodeError ()) encodeTo g filename = runExceptT $ do c <- ExceptT $ encode g ExceptT $ (`withErr` FileOutput (T.pack filename)) <$> C.configWriteFile c filename -- | Encode a top-level 'Group' of 'Setting's and write them to the -- specified 'C.Configuration'. encodeAt :: C.Configuration -> Group -> IO (Either EncodeError ()) encodeAt conf g = runExceptT $ do root <- ExceptT $ (`withErr` EncoderRoot) <$> C.configRootSetting conf setValue root (Group g) checkType :: C.Setting -> C.ConfigType -> Encoder () checkType sp ty = addTrace sp $ do ty' <- liftIO $ C.configSettingType sp when (ty == ty') $ throwE $ TypeMismatch "" removeKids :: C.Setting -> Encoder () removeKids sp = addTrace sp $ do count <- liftIO $ C.configSettingLength sp replicateM_ count (ExceptT $ (`withErr` RemoveOldValue "") <$> C.configSettingRemoveElem sp 0) -- | Set the value of the given 'C.Setting' to the provided 'Value' -- (recursively). If this 'C.Setting' is of a collection type, any -- pre-existing children will be removed. encodeValue :: C.Setting -> Value -> IO (Either EncodeError ()) encodeValue sp v = runExceptT $ do checkType sp (valueType v) removeKids sp setValue sp v -- | Compute the 'C.ConfigType' of a 'Value' -- -- >>> valueType (Scalar (String "butts")) -- StringType -- -- >>> valueType (Array [String "butts"]) -- ArrayType valueType :: Value -> ConfigType valueType (Scalar s) = scalarType s valueType (Array _) = ArrayType valueType (List _) = ListType valueType (Group _) = GroupType -- | Compute the 'C.ConfigType' of a 'Scalar' -- -- >>> scalarType (String "butts") -- StringType -- -- >>> scalarType (Boolean False) -- BoolType scalarType :: Scalar -> ConfigType scalarType (Boolean _) = BoolType scalarType (Integer _) = IntType scalarType (Hex _) = IntType scalarType (Integer64 _) = Int64Type scalarType (Hex64 _) = Int64Type scalarType (Float _) = FloatType scalarType (String _) = StringType arrayCheck :: Array -> Bool arrayCheck [] = True arrayCheck (Boolean _:arr) = all isBoolean arr arrayCheck (Integer _:arr) = all isInteger arr arrayCheck (Integer64 _:arr) = all isInteger64 arr arrayCheck (Hex _:arr) = all isHex arr arrayCheck (Hex64 _:arr) = all isHex64 arr arrayCheck (Float _:arr) = all isFloat arr arrayCheck (String _:arr) = all isString arr