{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UnicodeSyntax #-}
module Configuration.Utils.Internal.ConfigFileReader
(
parseConfigFiles
, readConfigFile
, ConfigFileFormat(..)
, loadLocal
#ifdef REMOTE_CONFIGS
, 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 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.CaseInsensitive as CI
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
#ifdef REMOTE_CONFIGS
type ConfigFileParser m =
( Functor m
, Applicative m
, MonadIO m
, MonadBaseControl IO m
, MonadError T.Text m
)
#else
type ConfigFileParser m =
( Functor m
, Applicative m
, MonadIO m
, MonadError T.Text m
)
#endif
parseConfigFiles
∷ (ConfigFileParser m, FromJSON (a → a))
⇒ ConfigFilesConfig
→ a
→ [ConfigFile]
→ m a
parseConfigFiles conf = foldM $ \val file →
readConfigFile conf file <*> pure val
readConfigFile
∷ (ConfigFileParser m, FromJSON (a → a))
⇒ ConfigFilesConfig
→ ConfigFile
→ m (a → a)
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
| ".yaml" `T.isSuffixOf` T.toLower f = Yaml
| ".yml" `T.isSuffixOf` T.toLower f = Yaml
| ".json" `T.isSuffixOf` T.toLower f = Json
| ".js" `T.isSuffixOf` T.toLower f = Json
| otherwise = Other
loadLocal
∷ (Functor m, MonadIO m, MonadError T.Text m, FromJSON (a → a))
⇒ ConfigFile
→ m (a → a)
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"]
jsonMimeType ∷ IsString s ⇒ [s]
jsonMimeType = map fromString ["application/json"]
contentType
∷ B8.ByteString
→ 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 m, FromJSON (a → a))
⇒ ConfigFilesConfig
→ ConfigFile
→ m (a → a)
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