{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Data.Ini.Config ( -- $main -- * Running Parsers parseIniFile -- * Parser Types , IniParser , SectionParser -- * Section-Level Parsing , section , sectionMb , sectionDef -- * Field-Level Parsing , field , fieldOf , fieldMb , fieldMbOf , fieldDef , fieldDefOf , fieldFlag , fieldFlagDef -- * Reader Functions , readable , number , string , flag ) where import Control.Applicative (Applicative(..), Alternative(..)) import Control.Monad.Trans.Except import qualified Data.HashMap.Strict as HM import Data.Ini.Config.Raw import Data.String (IsString(..)) import Data.Text (Text) import qualified Data.Text as T import Data.Typeable (Typeable, Proxy(..), typeRep) import Text.Read (readMaybe) addLineInformation :: Int -> Text -> StParser s a -> StParser s a addLineInformation lineNo sec = withExceptT go where go e = "Line " ++ show lineNo ++ ", in section " ++ show sec ++ ": " ++ e type StParser s a = ExceptT String ((->) s) a -- | An 'IniParser' value represents a computation for parsing entire -- INI-format files. newtype IniParser a = IniParser (StParser Ini a) deriving (Functor, Applicative, Alternative, Monad) -- | A 'SectionParser' value represents a computation for parsing a single -- section of an INI-format file. newtype SectionParser a = SectionParser (StParser IniSection a) deriving (Functor, Applicative, Alternative, Monad) -- | Parse a 'Text' value as an INI file and run an 'IniParser' over it parseIniFile :: Text -> IniParser a -> Either String a parseIniFile text (IniParser mote) = do ini <- parseIni text runExceptT mote ini -- | Find a named section in the INI file and parse it with the provided -- section parser, failing if the section does not exist. In order to -- support classic INI files with capitalized section names, section -- lookup is __case-insensitive__. -- -- >>> parseIniFile "[ONE]\nx = hello\n" $ section "ONE" (field "x") -- Right "hello" -- >>> parseIniFile "[ONE]\nx = hello\n" $ section "TWO" (field "x") -- Left "No top-level section named \"TWO\"" section :: Text -> SectionParser a -> IniParser a section name (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) -> case HM.lookup (T.toLower name) ini of Nothing -> Left ("No top-level section named " ++ show name) Just sec -> runExceptT thunk sec -- | Find a named section in the INI file and parse it with the provided -- section parser, returning 'Nothing' if the section does not exist. -- In order to -- support classic INI files with capitalized section names, section -- lookup is __case-insensitive__. -- -- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionMb "ONE" (field "x") -- Right (Just "hello") -- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionMb "TWO" (field "x") -- Right Nothing sectionMb :: Text -> SectionParser a -> IniParser (Maybe a) sectionMb name (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) -> case HM.lookup (T.toLower name) ini of Nothing -> return Nothing Just sec -> Just `fmap` runExceptT thunk sec -- | Find a named section in the INI file and parse it with the provided -- section parser, returning a default value if the section does not exist. -- In order to -- support classic INI files with capitalized section names, section -- lookup is __case-insensitive__. -- -- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionDef "ONE" "def" (field "x") -- Right "hello" -- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionDef "TWO" "def" (field "x") -- Right "def" sectionDef :: Text -> a -> SectionParser a -> IniParser a sectionDef name def (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) -> case HM.lookup (T.toLower name) ini of Nothing -> return def Just sec -> runExceptT thunk sec --- throw :: String -> StParser s a throw msg = ExceptT $ (\ _ -> Left msg) getSectionName :: StParser IniSection Text getSectionName = ExceptT $ (\ m -> return (isName m)) rawFieldMb :: Text -> StParser IniSection (Maybe IniValue) rawFieldMb name = ExceptT $ \m -> return (HM.lookup name (isVals m)) rawField :: Text -> StParser IniSection IniValue rawField name = do sec <- getSectionName valMb <- rawFieldMb name case valMb of Nothing -> throw ("Missing field " ++ show name ++ " in section " ++ show sec) Just x -> return x -- | Retrieve a field, failing if it doesn't exist, and return its raw value. -- -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (field "x") -- Right "hello" -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (field "y") -- Left "Missing field \"y\" in section \"MAIN\"" field :: Text -> SectionParser Text field name = SectionParser $ vValue `fmap` rawField name -- | Retrieve a field and use the supplied parser to parse it as a value, -- failing if the field does not exist, or if the parser fails to -- produce a value. -- -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldOf "x" number) -- Right 72 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldOf "x" number) -- Left "Line 2, in section \"MAIN\": Unable to parse \"hello\" as a value of type Integer" -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldOf "y" number) -- Left "Missing field \"y\" in section \"MAIN\"" fieldOf :: Text -> (Text -> Either String a) -> SectionParser a fieldOf name parse = SectionParser $ do sec <- getSectionName val <- rawField name case parse (vValue val) of Left err -> addLineInformation (vLineNo val) sec (throw err) Right x -> return x -- | Retrieve a field, returning a @Nothing@ value if it does not exist. -- -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldMb "x") -- Right (Just "hello") -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldMb "y") -- Right Nothing fieldMb :: Text -> SectionParser (Maybe Text) fieldMb name = SectionParser $ fmap vValue `fmap` rawFieldMb name -- | Retrieve a field and parse it according to the given parser, returning -- @Nothing@ if it does not exist. If the parser fails, then this will -- fail. -- -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldMbOf "x" number) -- Right (Just 72) -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldMbOf "x" number) -- Left "Line 2, in section \"MAIN\": Unable to parse \"hello\" as a value of type Integer" -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldMbOf "y" number) -- Right Nothing fieldMbOf :: Text -> (Text -> Either String a) -> SectionParser (Maybe a) fieldMbOf name parse = SectionParser $ do sec <- getSectionName mb <- rawFieldMb name case mb of Nothing -> return Nothing Just v -> case parse (vValue v) of Left err -> addLineInformation (vLineNo v) sec (throw err) Right x -> return (Just x) -- | Retrieve a field and supply a default value for if it doesn't exist. -- -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldDef "x" "def") -- Right "hello" -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldDef "y" "def") -- Right "def" fieldDef :: Text -> Text -> SectionParser Text fieldDef name def = SectionParser $ ExceptT $ \m -> case HM.lookup name (isVals m) of Nothing -> return def Just x -> return (vValue x) -- | Retrieve a field, parsing it according to the given parser, and returning -- a default value if it does not exist. If the parser fails, then this will -- fail. -- -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldDefOf "x" number 99) -- Right 72 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldDefOf "x" number 99) -- Left "Line 2, in section \"MAIN\": Unable to parse \"hello\" as a value of type Integer" -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldDefOf "y" number 99) -- Right 99 fieldDefOf :: Text -> (Text -> Either String a) -> a -> SectionParser a fieldDefOf name parse def = SectionParser $ do sec <- getSectionName mb <- rawFieldMb name case mb of Nothing -> return def Just v -> case parse (vValue v) of Left err -> addLineInformation (vLineNo v) sec (throw err) Right x -> return x -- | Retrieve a field and treat it as a boolean, failing if it -- does not exist. -- -- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlag "x") -- Right True -- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlag "y") -- Left "Missing field \"y\" in section \"MAIN\"" fieldFlag :: Text -> SectionParser Bool fieldFlag name = fieldOf name flag -- | Retrieve a field and treat it as a boolean, subsituting -- a default value if it doesn't exist. -- -- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlagDef "x" False) -- Right True -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldFlagDef "x" False) -- Left "Line 2, in section \"MAIN\": Unable to parse \"hello\" as a boolean" -- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlagDef "y" False) -- Right False fieldFlagDef :: Text -> Bool -> SectionParser Bool fieldFlagDef name def = fieldDefOf name flag def --- -- | Try to use the "Read" instance for a type to parse a value, failing -- with a human-readable error message if reading fails. -- -- >>> readable "(5, 7)" :: Either String (Int, Int) -- Right (5,7) -- >>> readable "hello" :: Either String (Int, Int) -- Left "Unable to parse \"hello\" as a value of type (Int,Int)" readable :: forall a. (Read a, Typeable a) => Text -> Either String a readable t = case readMaybe str of Just v -> Right v Nothing -> Left ("Unable to parse " ++ show str ++ " as a value of type " ++ show typ) where str = T.unpack t typ = typeRep prx prx :: Proxy a prx = Proxy -- | Try to use the "Read" instance for a numeric type to parse a value, -- failing with a human-readable error message if reading fails. -- -- >>> number "5" :: Either String Int -- Right 5 -- >>> number "hello" :: Either String Int -- Left "Unable to parse \"hello\" as a value of type Int" number :: (Num a, Read a, Typeable a) => Text -> Either String a number = readable -- | Convert a textua value to the appropriate string type. This will -- never fail. -- -- >>> string "foo" :: Either String String -- Right "foo" string :: (IsString a) => Text -> Either String a string = return . fromString . T.unpack -- | Convert a string that represents a boolean to a proper boolean. This -- is case-insensitive, and matches the words @true@, @false@, @yes@, -- @no@, as well as single-letter abbreviations for all of the above. -- If the input does not match, then this will fail with a human-readable -- error message. -- -- >>> flag "TRUE" -- Right True -- >>> flag "y" -- Right True -- >>> flag "no" -- Right False -- >>> flag "F" -- Right False -- >>> flag "That's a secret!" -- Left "Unable to parse \"That's a secret!\" as a boolean" flag :: Text -> Either String Bool flag s = case T.toLower s of "true" -> Right True "yes" -> Right True "t" -> Right True "y" -> Right True "false" -> Right False "no" -> Right False "f" -> Right False "n" -> Right False _ -> Left ("Unable to parse " ++ show s ++ " as a boolean") -- $setup -- -- >>> :{ -- data NetworkConfig = NetworkConfig -- { netHost :: String, netPort :: Int } -- deriving (Eq, Show) -- >>> :} -- -- >>> :{ -- data LocalConfig = LocalConfig -- { localUser :: Text } -- deriving (Eq, Show) -- >>> :} -- -- >>> :{ -- data Config = Config -- { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig } -- deriving (Eq, Show) -- >>> :} -- -- >>> :{ -- let configParser = do -- netCf <- section "NETWORK" $ do -- host <- fieldOf "host" string -- port <- fieldOf "port" number -- return NetworkConfig { netHost = host, netPort = port } -- locCf <- sectionMb "LOCAL" $ -- LocalConfig <$> field "user" -- return Config { cfNetwork = netCf, cfLocal = locCf } -- >>> :} -- -- >>> :{ -- let example = "[NETWORK]\nhost = example.com\nport = 7878\n\n# here is a comment\n[LOCAL]\nuser = terry\n" -- >>> :} -- $main -- The 'config-ini' library exports some simple monadic functions to -- make parsing INI-like configuration easier. INI files have a -- two-level structure: the top-level named chunks of configuration, -- and the individual key-value pairs contained within those chunks. -- For example, the following INI file has two sections, @NETWORK@ -- and @LOCAL@, and each contains its own key-value pairs. Comments, -- which begin with @#@ or @;@, are ignored: -- -- > [NETWORK] -- > host = example.com -- > port = 7878 -- > -- > # here is a comment -- > [LOCAL] -- > user = terry -- -- The combinators provided here are designed to write quick and -- idiomatic parsers for files of this form. Sections are parsed by -- 'IniParser' computations, like 'section' and its variations, -- while the fields within sections are parsed by 'SectionParser' -- computations, like 'field' and its variations. If we want to -- parse an INI file like the one above, treating the entire -- @LOCAL@ section as optional, we can write it like this: -- -- > data Config = Config -- > { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig } -- > deriving (Eq, Show) -- > -- > data NetworkConfig = NetworkConfig -- > { netHost :: String, netPort :: Int } -- > deriving (Eq, Show) -- > -- > data LocalConfig = LocalConfig -- > { localUser :: Text } -- > deriving (Eq, Show) -- > -- > configParser :: IniParser Config -- > configParser = do -- > netCf <- section "NETWORK" $ do -- > host <- fieldOf "host" string -- > port <- fieldOf "port" number -- > return NetworkConfig { netHost = host, netPort = port } -- > locCf <- sectionMb "LOCAL" $ -- > LocalConfig <$> field "user" -- > return Config { cfNetwork = netCf, cfLocal = locCf } -- -- We can run our computation with 'parseIniFile', which, -- when run on our example file above, would produce the -- following: -- -- >>> parseIniFile example configParser -- Right (Config {cfNetwork = NetworkConfig {netHost = "example.com", netPort = 7878}, cfLocal = Just (LocalConfig {localUser = "terry"})})