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 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
#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
→ α
→ [ConfigFile]
→ μ α
parseConfigFiles conf = foldM $ \val file →
readConfigFile conf file <*> pure val
readConfigFile
∷ (ConfigFileParser μ, FromJSON (α → α))
⇒ ConfigFilesConfig
→ ConfigFile
→ μ (α → α)
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
→ μ (α → α)
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 μ, FromJSON (α → α))
⇒ ConfigFilesConfig
→ ConfigFile
→ μ (α → α)
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