{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} module Data.Yaml.Config.Internal ( Config(..) , KeyError(..) , Key -- * Work with files , load -- * Explore config , keys , subconfig , lookup , lookupDefault , fullpath ) where import Prelude hiding (lookup) import Control.DeepSeq (NFData(rnf)) import Control.Exception (Exception) 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 import Control.Failure -- | Config or field name type Key = ST.Text -- | This error can be raised if config has not target path. newtype KeyError = KeyError Key deriving (Show, Typeable) instance Exception KeyError -- | Type contains config section and path from root. data Config = Config [Key] Object deriving (Eq, Show) instance NFData Config where rnf (Config p o) = rnf p `seq` rnf o `seq` () ke :: Failure KeyError m => Key -> m a ke = failure . KeyError -- | Show full path from the root to target key. Levels are separated by dots. -- -- >>> fullpath sub "field1" -- "section1.field1" -- 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 @InvalidYaml@ if file not found. -- -- >>> config <- load "example.yaml" -- load :: FilePath -> IO Config load f = maybe err (return . Config []) =<< Yaml.decodeFile f where err = error $ "Invalid config file " <> f <> "." -- | Show all first level config field's. -- -- >>> keys config -- ["section1","section2"] -- keys :: Config -> [Key] keys (Config _ o) = HashMap.keys o -- | Get value for given key. -- May fail with @KeyError@ if key doesn't exist. -- -- >>> keys sub -- ["field1","field2"] -- >>> putStrLn =<< lookup "field1" sub -- value1 -- lookup :: (Failure KeyError m, FromJSON a) => Key -- ^ Field name -> Config -- ^ Config for find -> m a -- ^ Field value lookup path c = maybe err return $ lookupMaybe path c where err = ke $ "Field " <> fullpath c path <> " not found or has wrong type." lookupMaybe :: FromJSON a => Key -> Config -> Maybe a lookupMaybe path conf = foldM (flip subconfig) conf (init pathes) >>= look (last pathes) where look k (Config _ o) = HashMap.lookup k o >>= parseMaybe parseJSON pathes = ST.splitOn "." path -- | Find value in config and return it or return default value. -- -- >>> lookupDefault "field3" "def" sub -- "def" -- lookupDefault :: FromJSON a => Key -- ^ Field name -> a -- ^ Default value -> Config -- ^ Config for find -> a -- ^ Founded or default value lookupDefault p d = fromMaybe d . lookup p -- | Get subconfig by name. -- May fail with @KeyError@ if target key doesn't exist at current level. -- -- >>> :set -XOverloadedStrings -- >>> sub <- subconfig "section1" config -- subconfig :: Failure KeyError m => Key -- ^ Subconfig name -> Config -- ^ (Sub)Config for find -> m Config -- ^ Founded Subconfig subconfig path c@(Config parents o) = case HashMap.lookup path o of Just (Yaml.Object so) -> return $ Config (path : parents) so _ -> err where err = ke $ "Subconfig " <> fullpath c path <> " not found."