{-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE UnicodeSyntax #-} -- | -- Module: Configuration.Utils.Internal.ConfigFileReader -- Description: Internal Tools for Parsing Configuration Files -- Copyright: Copyright © 2015 PivotCloud, Inc. -- License: MIT -- Maintainer: Lars Kuhtz -- Stability: experimental -- module Configuration.Utils.Internal.ConfigFileReader ( parseConfigFiles , readConfigFile , ConfigFileFormat(..) -- * Local Config Files , loadLocal #ifdef REMOTE_CONFIGS -- * Remote Config Files , isRemote , loadRemote , yamlMimeType , jsonMimeType , contentType , requestHeaders #endif ) where import Configuration.Utils.ConfigFile import Configuration.Utils.Internal import Configuration.Utils.Validation import Control.Applicative import Control.DeepSeq (NFData) import Control.Monad import Control.Monad.IO.Class import Control.Monad.Error.Class import qualified Data.ByteString.Char8 as B8 import qualified Data.CaseInsensitive as CI import Data.Monoid.Unicode import qualified Data.Text as T import Data.Typeable import qualified Data.Yaml as Yaml import GHC.Generics import Prelude hiding (concatMap, mapM_, any) import Prelude.Unicode #ifdef REMOTE_CONFIGS import Configuration.Utils.Internal.HttpsCertPolicy import Control.Exception.Enclosed import Control.Monad.Trans.Control import qualified Data.ByteString.Lazy as LB import qualified Data.List as L import Data.String import qualified Data.Text.IO as T import qualified Network.HTTP.Client as HTTP import qualified Network.HTTP.Types.Header as HTTP import System.IO #endif -- -------------------------------------------------------------------------- -- -- Tools for parsing configuration files #ifdef REMOTE_CONFIGS type ConfigFileParser μ = ( Functor μ , Applicative μ , MonadIO μ , MonadBaseControl IO μ , MonadError T.Text μ ) #else type ConfigFileParser μ = ( Functor μ , Applicative μ , MonadIO μ , MonadError T.Text μ ) #endif parseConfigFiles ∷ (ConfigFileParser μ, FromJSON (α → α)) ⇒ ConfigFilesConfig → α -- ^ default configuration value → [ConfigFile] -- ^ list of configuration file paths → μ α parseConfigFiles conf = foldM $ \val file → readConfigFile conf file <*> pure val readConfigFile ∷ (ConfigFileParser μ, FromJSON (α → α)) ⇒ ConfigFilesConfig → ConfigFile -- ^ file path → μ (α → α) readConfigFile _conf file = #ifdef REMOTE_CONFIGS if isRemote file then loadRemote _conf file else loadLocal file #else loadLocal file #endif fileType ∷ T.Text → ConfigFileFormat fileType f | CI.foldCase ".yaml" `T.isSuffixOf` CI.foldCase f = Yaml | CI.foldCase ".yml" `T.isSuffixOf` CI.foldCase f = Yaml | CI.foldCase ".json" `T.isSuffixOf` CI.foldCase f = Json | CI.foldCase ".js" `T.isSuffixOf` CI.foldCase f = Json | otherwise = Other loadLocal ∷ (Functor μ, MonadIO μ, MonadError T.Text μ, FromJSON (α → α)) ⇒ ConfigFile -- ^ file path → μ (α → α) loadLocal path = do validateFilePath "config-file" (T.unpack file) exists ← (True <$ validateFile "config-file" (T.unpack file)) `catchError` \e → case path of ConfigFileOptional _ → return False ConfigFileRequired _ → throwError $ "failed to read config file: " ⊕ e if exists then liftIO (parser (fileType file) file) >>= \case Left e → throwError $ "failed to parse configuration file " ⊕ file ⊕ ": " ⊕ sshow e Right r → return r else return id where file = getConfigFile path parser Json f = fmapL T.pack ∘ eitherDecodeStrict' <$> B8.readFile (T.unpack f) parser _ f = fmapL sshow <$> Yaml.decodeFileEither (T.unpack f) data ConfigFileFormat = Yaml | Json | Other deriving (Show, Read, Eq, Ord, Enum, Bounded, Typeable, Generic) instance NFData ConfigFileFormat #ifdef REMOTE_CONFIGS isRemote ∷ ConfigFile → Bool isRemote path = L.any (`T.isPrefixOf` getConfigFile path) ["http://", "https://"] yamlMimeType ∷ IsString s ⇒ [s] yamlMimeType = map fromString ["application/x-yaml", "text/yaml"] -- | Defined in RFC 4627 -- jsonMimeType ∷ IsString s ⇒ [s] jsonMimeType = map fromString ["application/json"] contentType ∷ B8.ByteString -- ^ value of an HTTP @Content-Type@ header → ConfigFileFormat contentType headerValue | CI.foldCase "yaml" `B8.isInfixOf` CI.foldCase headerValue = Yaml | CI.foldCase "json" `B8.isInfixOf` CI.foldCase headerValue = Json | otherwise = Other loadRemote ∷ (ConfigFileParser μ, FromJSON (α → α)) ⇒ ConfigFilesConfig → ConfigFile -- ^ URL → μ (α → α) loadRemote conf path = do validateHttpOrHttpsUrl "config-file" (T.unpack url) result ← (Just <$> doHttp) `catchAnyDeep` \e → case path of ConfigFileOptional _ → do liftIO ∘ T.hPutStrLn stderr $ "WARNING: failed to download remote configuration file " ⊕ url ⊕ ": " ⊕ sshow e return Nothing ConfigFileRequired _ → throwError $ "failed to download remote configuration file " ⊕ url ⊕ ": " ⊕ sshow e case result of Nothing → return id Just (format, d) → case (parser format) d of Left e → throwError $ "failed to parse remote configuration " ⊕ url ⊕ ": " ⊕ e Right r → return r where parser Json = fmapL T.pack ∘ eitherDecodeStrict' parser _ = fmapL sshow ∘ Yaml.decodeEither' url = getConfigFile path policy = _cfcHttpsPolicy conf doHttp = liftIO $ do request ← (HTTP.parseUrl $ T.unpack url) <&> over requestHeaders ((:) acceptHeader) resp ← httpWithValidationPolicy request policy let format = maybe Other contentType ∘ L.lookup HTTP.hContentType $ HTTP.responseHeaders resp return (format, LB.toStrict (HTTP.responseBody resp)) acceptHeader = (HTTP.hAccept, B8.intercalate "," (yamlMimeType ⊕ jsonMimeType)) requestHeaders ∷ Lens' HTTP.Request HTTP.RequestHeaders requestHeaders = lens HTTP.requestHeaders $ \s a → s { HTTP.requestHeaders = a } #endif