{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE OverloadedStrings #-} module Data.Yaml.Config.Internal ( Config(..) , Key , load , keys , lookupSubconfig , subconfig , lookup , lookupDefault , require , fullpath ) where import Prelude hiding (lookup) import Control.DeepSeq (NFData(rnf)) import Control.Exception (Exception, throwIO) import Control.Monad (foldM) import Data.Maybe (fromMaybe) import Data.Monoid ((<>)) import Data.Typeable (Typeable) import qualified Data.Text as ST import Data.Yaml (Object, FromJSON(parseJSON), parseMaybe) import qualified Data.HashMap.Strict as HashMap import qualified Data.Yaml as Yaml -- | Subconfig or field name type Key = ST.Text -- | Throwable exception newtype KeyError = KeyError Key deriving (Show, Typeable) instance Exception KeyError -- | (Sub)Config type data Config = Config [Key] Object deriving (Eq, Show) instance NFData Config where rnf (Config p o) = rnf p `seq` rnf o `seq` () ke :: Key -> IO a ke = throwIO . KeyError -- | Show full path from the root to target key fullpath :: Config -> Key -> Key fullpath (Config parents _) path = ST.intercalate "." $ reverse $ path : parents -- | Find file in filesystem and try to load it as YAML config -- May fail with @KeyError@ load :: FilePath -> IO Config load f = maybe err (return . Config []) =<< Yaml.decodeFile f where err = error $ "Invalid config file " <> f <> "." -- | Show all (sub)config first level filed's name keys :: Config -> [Key] keys (Config _ o) = HashMap.keys o -- | Field value wrapped into @Maybe@ (sub)config lookup :: FromJSON a => Config -- ^ (Sub)Config for find -> Key -- ^ Field name -> Maybe a -- ^ Field value lookup conf path = foldM lookupSubconfig conf (init pathes) >>= look (last pathes) where look k (Config _ o) = HashMap.lookup k o >>= parseMaybe parseJSON pathes = ST.splitOn "." path -- | Subconfig wrapped into @Maybe@ lookupSubconfig :: Config -- ^ (Sub)Config for find -> Key -- ^ Field name -> Maybe Config -- ^ Maybe Subconfig lookupSubconfig (Config parents o) k = HashMap.lookup k o >>= \s -> case s of (Yaml.Object so) -> Just $ Config (k : parents) so _ -> Nothing -- | Find value in (sub)config and return it or default value lookupDefault :: FromJSON a => Config -- ^ (Sub)Config for find -> Key -- ^ Field name -> a -- ^ Default value -> a -- ^ Return value lookupDefault c p d = fromMaybe d $ lookup c p -- | Find subconfig -- May fail with @KeyError@ subconfig :: Config -- ^ (Sub)Config for find -> Key -- ^ Subconfig name -> IO Config -- ^ Subconfig subconfig c path = maybe err return $ lookupSubconfig c path where err = ke $ "Subconfig " <> fullpath c path <> " not found." -- | Same as @lookup@ buf fail with @KeyError@ -- if there is no field with target name require :: FromJSON a => Config -> Key -> IO a require c path = maybe err return $ lookup c path where err = ke $ "Field " <> fullpath c path <> " not found or has wrong type."