packages feed

configuration-tools 0.2.6 → 0.2.7

raw patch · 9 files changed

+569/−53 lines, 9 filesdep +dlistdep +mtldep +network-uriPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: dlist, mtl, network-uri

API changes (from Hackage documentation)

- Configuration.Utils: data ProgramInfo α
+ Configuration.Utils: data ConfigValidationFunction α λ
+ Configuration.Utils: data ProgramInfoValidate α λ
+ Configuration.Utils: piValidateConfiguration :: Lens' (ProgramInfoValidate α λ) (ConfigValidationFunction α λ)
+ Configuration.Utils: programInfoValidate :: String -> MParser α -> α -> ConfigValidation α λ -> ProgramInfoValidate α λ
+ Configuration.Utils: type ProgramInfo α = ProgramInfoValidate α []
+ Configuration.Utils: type ConfigValidation α λ = (MonadIO μ, Functor μ, Applicative μ, MonadError Text μ, MonadWriter (λ Text) μ) => α -> μ ()
+ Configuration.Utils.Http: defaultHttpServiceTLSConfiguration :: HttpServiceTLSConfiguration
+ Configuration.Utils.Http: pHttpServiceTLSConfiguration :: String -> MParser HttpServiceTLSConfiguration
+ Configuration.Utils.Http: validateHttpClientConfiguration :: ConfigValidation HttpClientConfiguration λ
+ Configuration.Utils.Http: validateHttpServiceConfiguration :: ConfigValidation HttpServiceConfiguration DList
+ Configuration.Utils.Http: validateHttpServiceTLSConfiguration :: ConfigValidation HttpServiceTLSConfiguration λ
+ Configuration.Utils.Validation: validateAbsoluteUri :: Text -> ConfigValidation String λ
+ Configuration.Utils.Validation: validateAbsoluteUriFragment :: Text -> ConfigValidation String λ
+ Configuration.Utils.Validation: validateDirectory :: Text -> ConfigValidation FilePath λ
+ Configuration.Utils.Validation: validateExecutable :: Text -> ConfigValidation FilePath λ
+ Configuration.Utils.Validation: validateFile :: Text -> ConfigValidation FilePath λ
+ Configuration.Utils.Validation: validateFilePath :: Text -> ConfigValidation FilePath λ
+ Configuration.Utils.Validation: validateFileReadable :: Text -> ConfigValidation FilePath λ
+ Configuration.Utils.Validation: validateFileWritable :: Text -> ConfigValidation FilePath λ
+ Configuration.Utils.Validation: validateHttpOrHttpsUrl :: Text -> ConfigValidation String λ
+ Configuration.Utils.Validation: validateHttpUrl :: Text -> ConfigValidation String λ
+ Configuration.Utils.Validation: validateHttpsUrl :: Text -> ConfigValidation String λ
+ Configuration.Utils.Validation: validateIPv4 :: Text -> ConfigValidation String λ
+ Configuration.Utils.Validation: validateIPv6 :: Text -> ConfigValidation String λ
+ Configuration.Utils.Validation: validateLength :: Foldable φ => Text -> Int -> ConfigValidation (φ α) λ
+ Configuration.Utils.Validation: validateMaxLength :: Foldable φ => Text -> Int -> ConfigValidation (φ α) λ
+ Configuration.Utils.Validation: validateMinLength :: Foldable φ => Text -> Int -> ConfigValidation (φ α) λ
+ Configuration.Utils.Validation: validateMinMaxLength :: Foldable φ => Text -> Int -> Int -> ConfigValidation (φ α) λ
+ Configuration.Utils.Validation: validateNonEmpty :: (Eq α, Monoid α) => Text -> ConfigValidation α λ
+ Configuration.Utils.Validation: validatePort :: Text -> ConfigValidation Int λ
+ Configuration.Utils.Validation: validateUri :: Text -> ConfigValidation String λ
- Configuration.Utils: piDefaultConfiguration :: Lens' (ProgramInfo α) α
+ Configuration.Utils: piDefaultConfiguration :: Lens' (ProgramInfoValidate α λ) α
- Configuration.Utils: piDescription :: Lens' (ProgramInfo α) String
+ Configuration.Utils: piDescription :: Lens' (ProgramInfoValidate α λ) String
- Configuration.Utils: piHelpFooter :: Lens' (ProgramInfo α) (Maybe String)
+ Configuration.Utils: piHelpFooter :: Lens' (ProgramInfoValidate α λ) (Maybe String)
- Configuration.Utils: piHelpHeader :: Lens' (ProgramInfo α) (Maybe String)
+ Configuration.Utils: piHelpHeader :: Lens' (ProgramInfoValidate α λ) (Maybe String)
- Configuration.Utils: piOptionParser :: Lens' (ProgramInfo α) (MParser α)
+ Configuration.Utils: piOptionParser :: Lens' (ProgramInfoValidate α λ) (MParser α)
- Configuration.Utils: piOptionParserAndDefaultConfiguration :: Lens (ProgramInfo α) (ProgramInfo β) (MParser α, α) (MParser β, β)
+ Configuration.Utils: piOptionParserAndDefaultConfiguration :: Lens (ProgramInfoValidate α λ) (ProgramInfoValidate β γ) (MParser α, α, ConfigValidationFunction α λ) (MParser β, β, ConfigValidationFunction β γ)
- Configuration.Utils: runWithConfiguration :: (FromJSON (α -> α), ToJSON α) => ProgramInfo α -> (α -> IO ()) -> IO ()
+ Configuration.Utils: runWithConfiguration :: (FromJSON (α -> α), ToJSON α, Foldable λ, Monoid (λ Text)) => ProgramInfoValidate α λ -> (α -> IO ()) -> IO ()
- Configuration.Utils: runWithPkgInfoConfiguration :: (FromJSON (α -> α), ToJSON α) => ProgramInfo α -> PkgInfo -> (α -> IO ()) -> IO ()
+ Configuration.Utils: runWithPkgInfoConfiguration :: (FromJSON (α -> α), ToJSON α, Foldable λ, Monoid (λ Text)) => ProgramInfoValidate α λ -> PkgInfo -> (α -> IO ()) -> IO ()

Files

CHANGELOG.md view
@@ -1,3 +1,13 @@+0.2.7+=====++*   Added `view` funtion for lenses to `Configuration.Utils.Internal`.++*   Added support for validation of configuration values.++*   Added module `Configuration.Utils.Validation` that provides primitives+    for validating different basic configuration values.+ 0.2.6 ===== 
configuration-tools.cabal view
@@ -3,7 +3,7 @@ -- ------------------------------------------------------ --  Name:                configuration-tools-Version:             0.2.6+Version:             0.2.7 Synopsis:            Tools for specifying and parsing configurations description:     Tools for specifying and parsing configurations@@ -54,7 +54,7 @@ source-repository this     type: git     location: https://github.com/alephcloud/hs-configuration-tools.git-    tag: 0.2.6+    tag: 0.2.7  Library     hs-source-dirs: src@@ -65,6 +65,7 @@         Configuration.Utils         Configuration.Utils.Http         Configuration.Utils.Setup+        Configuration.Utils.Validation      other-modules:         Configuration.Utils.Internal@@ -78,7 +79,10 @@         bytestring >= 0.10.0.2,         case-insensitive >= 1.2,         directory >= 1.2.1.0,+        dlist >= 0.7.1,         errors >= 1.4.3,+        mtl >= 2.2,+        network-uri >= 2.6.0.1,         optparse-applicative >= 0.8.1,         process >= 1.2.0.0,         text >= 1.0,@@ -89,18 +93,24 @@      ghc-options: -Wall -Test-Suite url-example+Test-Suite url-example-test     type: exitcode-stdio-1.0     default-language: Haskell2010-    main-is: Example.hs-    hs-source-dirs: examples+    main-is: TestExample.hs+    hs-source-dirs: examples, test +    other-modules:+        Example+     build-depends:         base >= 4.6 && < 5.0,         base-unicode-symbols >= 0.2.2.4,-        configuration-tools+        configuration-tools,+        errors >= 1.4.3,+        mtl >= 2.2      ghc-options: -Wall+    cpp-options: -DMain=Example  Test-Suite trivial     type: exitcode-stdio-1.0
constraints view
@@ -12,7 +12,7 @@              case-insensitive ==1.2.0.0,              comonad ==4.2.2,              conduit ==1.2.0.2,-             configuration-tools ==0.2.6,+             configuration-tools ==0.2.7,              containers ==0.5.5.1,              contravariant ==1.2,              deepseq ==1.3.0.2,@@ -32,8 +32,10 @@              monad-control ==0.3.3.0,              mtl ==2.2.1,              nats ==0.2,+             network-uri ==2.6.0.1,              old-locale ==1.0.0.6,              optparse-applicative ==0.11.0.1,+             parsec ==3.1.7,              prelude-extras ==0.4,              pretty ==1.1.1.1,              primitive ==0.5.3.0,
examples/Example.hs view
@@ -2,21 +2,30 @@ -- Copyright © 2014 AlephCloud Systems, Inc. -- ------------------------------------------------------ -- +{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE UnicodeSyntax #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleInstances #-}  module Main ( main+, main_ ) where  import Configuration.Utils++import Control.Monad+import Control.Monad.Except+import Control.Monad.Writer+ import Data.Monoid.Unicode+ import Prelude.Unicode --- This assume usage of cabal with custom Setup.hs+-- This assumes usage of cabal with custom Setup.hs ---import PkgInfo_url_example+import PkgInfo_url_example_test  -- | Specification of the authentication section of a URL. --@@ -42,6 +51,10 @@     , _pwd = ""     } +validateAuth ∷ ConfigValidation Auth []+validateAuth conf =+    when (_user conf ≠ "" && _pwd conf ≡ "") $ tell ["password is empty"]+ instance FromJSON (Auth → Auth) where     parseJSON = withObject "Auth" $ \o → id         <$< user ..: "user" × o@@ -86,6 +99,12 @@     , _path = ""     } +validateHttpURL ∷ ConfigValidation HttpURL []+validateHttpURL conf = do+    validateAuth $ _auth conf+    when (_domain conf ≡ "" && _path conf ≡ "") $+        throwError "domain and path must not both be null"+ instance FromJSON (HttpURL → HttpURL) where     parseJSON = withObject "HttpURL" $ \o → id         <$< auth %.: "auth" × o@@ -112,13 +131,13 @@  -- | Information about the main Application ---mainInfo ∷ ProgramInfo HttpURL-mainInfo = programInfo "HTTP URL" pHttpURL defaultHttpURL+mainInfo ∷ ProgramInfoValidate HttpURL []+mainInfo = programInfoValidate "HTTP URL" pHttpURL defaultHttpURL validateHttpURL  -- This version assumes usage of cabal with custom Setup.hs -- main ∷ IO ()-main = runWithPkgInfoConfiguration mainInfo pkgInfo $ \conf → do+main = runWithPkgInfoConfiguration mainInfo pkgInfo $ \conf →     putStrLn         $ "http://"         ⊕ (_user ∘ _auth) conf@@ -132,7 +151,7 @@ -- This version does not rely on cabal -- main_ ∷ IO ()-main_ = runWithConfiguration mainInfo $ \conf → do+main_ = runWithConfiguration mainInfo $ \conf →     putStrLn         $ "http://"         ⊕ (_user ∘ _auth) conf
src/Configuration/Utils.hs view
@@ -36,6 +36,10 @@ -- 4. an options parser that yields a function that takes a value and updates --    that value with the values provided as command line options. --+-- In addition to the above optionally a validation function may be provided+-- that (recursively) validates a configuration value and returns either+-- an error or a --possibly empty-- list-like structure of warnings.+-- -- The module provides operators and functions that make the implmentation of -- these entities easy for the common case that the configurations are encoded -- mainly as nested records.@@ -56,6 +60,15 @@ , piHelpFooter , piOptionParser , piDefaultConfiguration++-- * Program Configurations with Validation of Configuration Values+, ConfigValidation+, programInfoValidate++-- ** Low-level Config Validation+, ProgramInfoValidate+, piValidateConfiguration+, ConfigValidationFunction , piOptionParserAndDefaultConfiguration  -- * Running an Configured Application@@ -106,18 +119,21 @@ import Configuration.Utils.Internal  import Control.Error (fmapL)+import Control.Monad.Except hiding (mapM_)+import Control.Monad.Writer hiding (mapM_)  import Data.Aeson import Data.Aeson.Types (Parser) import qualified Data.ByteString.Char8 as B8 import Data.Char import qualified Data.CaseInsensitive as CI+import Data.Foldable import qualified Data.HashMap.Strict as H import Data.Maybe-import Data.Monoid import Data.Monoid.Unicode import Data.String import qualified Data.Text as T+import qualified Data.Text.IO as T import qualified Data.Yaml as Yaml  #if MIN_VERSION_optparse_applicative(0,10,0)@@ -128,8 +144,10 @@  import qualified Options.Applicative as O +import Prelude hiding (concatMap, mapM_, any) import Prelude.Unicode +import System.IO import System.IO.Unsafe (unsafePerformIO)  import qualified Text.ParserCombinators.ReadP as P@@ -453,7 +471,22 @@ -- -------------------------------------------------------------------------- -- -- Main Configuration -data ProgramInfo α = ProgramInfo+-- | A validation function. The type in the 'MonadWriter' is excpected to+-- be a 'Foldable' structure for collecting warnings.+--+type ConfigValidation α λ = (MonadIO μ, Functor μ, Applicative μ, MonadError T.Text μ, MonadWriter (λ T.Text) μ) ⇒ α → μ ()++-- | A newtype wrapper around a validation function. The only purpose of+-- this type is to avoid ImpredicativeTypes when storing the function+-- in the 'ProgramInfoValidate' record.+--+newtype ConfigValidationFunction α λ = ConfigValidationFunction+    { runConfigValidation ∷ ConfigValidation α λ+    }++type ProgramInfo α = ProgramInfoValidate α []++data ProgramInfoValidate α λ = ProgramInfo     { _piDescription ∷ !String       -- ^ Program Description     , _piHelpHeader ∷ !(Maybe String)@@ -464,65 +497,101 @@       -- ^ options parser for configuration (TODO consider using a typeclass for this)     , _piDefaultConfiguration ∷ !α       -- ^ default configuration+    , _piValidateConfiguration ∷ !(ConfigValidationFunction α λ)+      -- ^ a validation function. The 'Right' result is interpreted as a 'Foldable'+      -- structure of warnings.     }  -- | Program Description ---piDescription ∷ Lens' (ProgramInfo α) String+piDescription ∷ Lens' (ProgramInfoValidate α λ) String piDescription = lens _piDescription $ \s a → s { _piDescription = a } {-# INLINE piDescription #-}  -- | Help header ---piHelpHeader ∷ Lens' (ProgramInfo α) (Maybe String)+piHelpHeader ∷ Lens' (ProgramInfoValidate α λ) (Maybe String) piHelpHeader = lens _piHelpHeader $ \s a → s { _piHelpHeader = a } {-# INLINE piHelpHeader #-}  -- | Help footer ---piHelpFooter ∷ Lens' (ProgramInfo α) (Maybe String)+piHelpFooter ∷ Lens' (ProgramInfoValidate α λ) (Maybe String) piHelpFooter = lens _piHelpFooter $ \s a → s { _piHelpFooter = a } {-# INLINE piHelpFooter #-}  -- | options parser for configuration (TODO consider using a typeclass for this) ---piOptionParser ∷ Lens' (ProgramInfo α) (MParser α)+piOptionParser ∷ Lens' (ProgramInfoValidate α λ) (MParser α) piOptionParser = lens _piOptionParser $ \s a → s { _piOptionParser = a } {-# INLINE piOptionParser #-}  -- | default configuration ---piDefaultConfiguration ∷ Lens' (ProgramInfo α) α+piDefaultConfiguration ∷ Lens' (ProgramInfoValidate α λ) α piDefaultConfiguration = lens _piDefaultConfiguration $ \s a → s { _piDefaultConfiguration = a } {-# INLINE piDefaultConfiguration #-} +-- | Validation Function+--+-- The 'Right' result is interpreted as a 'Foldable' structure of warnings.+--+piValidateConfiguration ∷ Lens' (ProgramInfoValidate α λ) (ConfigValidationFunction α λ)+piValidateConfiguration = lens _piValidateConfiguration $ \s a → s { _piValidateConfiguration = a }+{-# INLINE piValidateConfiguration #-}+ -- | 'Lens' for simultaneous query and update of 'piOptionParser' and -- 'piDefaultConfiguration'. This supports to change the type of 'ProgramInfo' -- with 'over' and 'set'. ---piOptionParserAndDefaultConfiguration ∷ Lens (ProgramInfo α) (ProgramInfo β) (MParser α, α) (MParser β, β)-piOptionParserAndDefaultConfiguration = lens g $ \s (a,b) → ProgramInfo+piOptionParserAndDefaultConfiguration+    ∷ Lens+        (ProgramInfoValidate α λ)+        (ProgramInfoValidate β γ)+        (MParser α, α, ConfigValidationFunction α λ)+        (MParser β, β, ConfigValidationFunction β γ)+piOptionParserAndDefaultConfiguration = lens g $ \s (a,b,c) → ProgramInfo     { _piDescription = _piDescription s     , _piHelpHeader = _piHelpHeader s     , _piHelpFooter = _piHelpFooter s     , _piOptionParser = a     , _piDefaultConfiguration = b+    , _piValidateConfiguration = c     }   where-    g s = (_piOptionParser s, _piDefaultConfiguration s)+    g s = (_piOptionParser s, _piDefaultConfiguration s, _piValidateConfiguration s) {-# INLINE piOptionParserAndDefaultConfiguration #-}  -- | Smart constructor for 'ProgramInfo'. -- -- 'piHelpHeader' and 'piHelpFooter' are set to 'Nothing'.+-- The function 'piValidateConfiguration' is set to @const (return [])@ ---programInfo ∷ String → MParser α → α → ProgramInfo α-programInfo desc parser defaultConfig = ProgramInfo+programInfo+    ∷ String+    → MParser α+    → α+    → ProgramInfo α+programInfo desc parser defaultConfig =+    programInfoValidate desc parser defaultConfig $ const (return ())++-- | Smart constructor for 'ProgramInfo'.+--+-- 'piHelpHeader' and 'piHelpFooter' are set to 'Nothing'.+--+programInfoValidate+    ∷ String+    → MParser α+    → α+    → ConfigValidation α λ+    → ProgramInfoValidate α λ+programInfoValidate desc parser defaultConfig valFunc = ProgramInfo     { _piDescription = desc     , _piHelpHeader = Nothing     , _piHelpFooter = Nothing     , _piOptionParser = parser     , _piDefaultConfiguration = defaultConfig+    , _piValidateConfiguration = ConfigValidationFunction valFunc     }  data AppConfiguration α = AppConfiguration@@ -570,8 +639,8 @@         $ unsafePerformIO (Yaml.decodeFileEither file)  mainOptions-    ∷ ∀ α . FromJSON (α → α)-    ⇒ ProgramInfo α+    ∷ ∀ α λ . FromJSON (α → α)+    ⇒ ProgramInfoValidate α λ     → (∀ β . Maybe (MParser β))     → O.ParserInfo (AppConfiguration α) mainOptions ProgramInfo{..} pkgInfoParser = O.info optionParser@@ -610,12 +679,13 @@ --     Print a help message and exit. -- runWithConfiguration-    ∷ (FromJSON (α → α), ToJSON α)-    ⇒ ProgramInfo α+    ∷ (FromJSON (α → α), ToJSON α, Foldable λ, Monoid (λ T.Text))+    ⇒ ProgramInfoValidate α λ     → (α → IO ())     → IO () runWithConfiguration appInfo mainFunction = do     conf ← O.customExecParser parserPrefs mainOpts+    validateConfig appInfo $ _mainConfig conf     if _printConfig conf         then B8.putStrLn ∘ Yaml.encode ∘ _mainConfig $ conf         else mainFunction ∘ _mainConfig $ conf@@ -623,6 +693,30 @@     mainOpts = mainOptions appInfo Nothing     parserPrefs = O.prefs O.disambiguate +-- | Validates a configuration value. Throws an user error+-- if there is an error. If there are warnings they are+-- printed to 'stderr'.+--+validateConfig+    ∷ (Foldable λ, Monoid (λ T.Text))+    ⇒ ProgramInfoValidate α λ+    → α+    → IO ()+validateConfig appInfo conf = do+    warnings ← execWriterT . exceptT (error . T.unpack) return $ do+        runConfigValidation (view piValidateConfiguration appInfo) conf+    when (any (const True) warnings) $ do+        T.hPutStrLn stderr "WARNINGS:"+        mapM_ (\w → T.hPutStrLn stderr $ "warning: " ⊕ w) warnings++exceptT+    ∷ Monad μ+    ⇒ (ε → μ β)+    → (α → μ β)+    → ExceptT ε μ α+    → μ β+exceptT a b = runExceptT >=> either a b+ -- -------------------------------------------------------------------------- -- -- Main Configuration with Package Info @@ -690,20 +784,21 @@ -- [@--info, -i@] --     Print a short info message for the application and exit. ----- [@--long-inf@]+-- [@--long-info@] --     Print a detailed info message for the application and exit. -- -- [@--license@] --     Print the text of the lincense of the application and exit. -- runWithPkgInfoConfiguration-    ∷ (FromJSON (α → α), ToJSON α)-    ⇒ ProgramInfo α+    ∷ (FromJSON (α → α), ToJSON α, Foldable λ, Monoid (λ T.Text))+    ⇒ ProgramInfoValidate α λ     → PkgInfo     → (α → IO ())     → IO () runWithPkgInfoConfiguration appInfo pkgInfo mainFunction = do     conf ← O.customExecParser parserPrefs mainOpts+    validateConfig appInfo $ _mainConfig conf     if _printConfig conf         then B8.putStrLn ∘ Yaml.encode ∘ _mainConfig $ conf         else mainFunction ∘ _mainConfig $ conf@@ -832,14 +927,14 @@ -- == Example: -- -- > data Setting = Setting--- >     { _setA :: !Int--- >     , _setB :: !String+-- >     { _setA ∷ !Int+-- >     , _setB ∷ !String -- >     } -- >     deriving (Show, Read, Eq, Ord, Typeable) -- > -- > $(makeLenses ''Setting) -- >--- > defaultSetting :: Setting+-- > defaultSetting ∷ Setting -- > defaultSetting = Setting -- >     { _setA = 0 -- >     , _setB = 1@@ -851,15 +946,15 @@ -- >        , "b" .= _setB setting -- >        ] -- >--- > instance FromJSON (Setting -> Setting) where--- >     parseJSON = withObject "Setting" $ \o -> id+-- > instance FromJSON (Setting → Setting) where+-- >     parseJSON = withObject "Setting" $ \o → id -- >         <$< setA ..: "a" % o -- >         <*< setB ..: "b" % o -- > -- > instance FromJSON Setting where -- >    parseJSON v = parseJSON v <*> pure defaultSetting -- >--- > pSetting :: MParser Setting+-- > pSetting ∷ MParser Setting -- > pSetting = id -- >     <$< setA .:: option auto -- >         % short 'a'@@ -873,13 +968,13 @@ -- > -- | Use 'Setting' as 'Maybe' in a configuration: -- > -- -- > data Config = Config--- >     { _maybeSetting :: !(Maybe Setting)+-- >     { _maybeSetting ∷ !(Maybe Setting) -- >     } -- >     deriving (Show, Read, Eq, Ord, Typeable) -- > -- > $(makeLenses ''Config) -- >--- > defaultConfig :: Config+-- > defaultConfig ∷ Config -- > defaultConfig = Config -- >     { _maybeSetting = defaultSetting -- >     }@@ -889,11 +984,11 @@ -- >         [ "setting" .= maybeSetting -- >         ] -- >--- > instance FromJSON (Config -> Config) where--- >     parseJSON = withObject "Config" $ \o -> id+-- > instance FromJSON (Config → Config) where+-- >     parseJSON = withObject "Config" $ \o → id -- >         <$< maybeSetting %.: "setting" % o -- >--- > pConfig :: MParser Config+-- > pConfig ∷ MParser Config -- > pConfig = id -- >     <$< maybeSetting %:: (maybeOption defaultSetting -- >         <$> pEnableSetting@@ -905,15 +1000,15 @@ -- >         <> help "Enable configuration flags for setting" -- maybeOption-    :: a+    ∷ a         -- ^ default value that is used if base configuration is 'Nothing'-    -> Bool+    → Bool         -- ^ whether to enable this parser or not (usually is a boolean option parser)-    -> (a -> a)+    → (a → a)         -- ^ update function (usually given as applicative 'MParser a')-    -> Maybe a+    → Maybe a         -- ^ the base value that is updated (usually the result of parsing the configuraton file)-    -> Maybe a+    → Maybe a maybeOption _ False _ Nothing = Nothing -- not enabled maybeOption defA True update Nothing = Just $ update defA -- disabled in config file but enabled by command line maybeOption _ _ update (Just val) = Just $ update val -- enabled by config file and possibly by command line
src/Configuration/Utils/Http.hs view
@@ -2,12 +2,16 @@ -- Copyright © 2014 AlephCloud Systems, Inc. -- ------------------------------------------------------ -- -{-# LANGUAGE UnicodeSyntax #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+#if __GLASGOW_HASKELL__>=708+{-# LANGUAGE OverloadedLists #-}+#endif {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE CPP #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnicodeSyntax #-}  {-# OPTIONS_HADDOCK show-extensions #-} @@ -17,6 +21,9 @@   HttpServiceTLSConfiguration , hstcCertFile , hstcKeyFile+, defaultHttpServiceTLSConfiguration+, pHttpServiceTLSConfiguration+, validateHttpServiceTLSConfiguration  -- * HTTP Service Configuration , HttpServiceConfiguration@@ -25,6 +32,7 @@ , hscUseTLS , defaultHttpServiceConfiguration , pHttpServiceConfiguration+, validateHttpServiceConfiguration  -- * Http Client Configuration , HttpClientConfiguration@@ -33,13 +41,21 @@ , hccUseTLS , defaultHttpClientConfiguration , pHttpClientConfiguration+, validateHttpClientConfiguration , httpService2clientConfiguration ) where  import Configuration.Utils import Configuration.Utils.Internal+import Configuration.Utils.Validation +import Control.Monad (when)+#if __GLASGOW_HASKELL__>=708+import Control.Monad.Writer.Class (tell)+#endif+ import qualified Data.ByteString.Char8 as B8+import qualified Data.DList as DL import Data.Maybe (isJust) import Data.Monoid.Unicode @@ -69,6 +85,12 @@     , _hstcKeyFile = "key.pem"     } +validateHttpServiceTLSConfiguration+    ∷ ConfigValidation HttpServiceTLSConfiguration λ+validateHttpServiceTLSConfiguration conf = do+    validateFileReadable "cert-file" $ _hstcCertFile conf+    validateFileReadable "key-file" $ _hstcKeyFile conf+ instance FromJSON (HttpServiceTLSConfiguration → HttpServiceTLSConfiguration) where     parseJSON = withObject "HttpServiceTLSConfiguration" $ \o → id         <$< hstcCertFile ..: "cert-file" × o@@ -136,7 +158,6 @@ hscUseTLS ∷ Lens' HttpServiceConfiguration (Maybe HttpServiceTLSConfiguration) hscUseTLS = lens _hscUseTLS $ \s a → s { _hscUseTLS = a} - defaultHttpServiceConfiguration ∷ HttpServiceConfiguration defaultHttpServiceConfiguration = HttpServiceConfiguration     { _hscHost = "localhost"@@ -145,6 +166,17 @@     , _hscUseTLS = Nothing     } +validateHttpServiceConfiguration ∷ ConfigValidation HttpServiceConfiguration DL.DList+validateHttpServiceConfiguration conf = do+    maybe (return ()) validateHttpServiceTLSConfiguration $ _hscUseTLS conf+    validatePort "port" $ _hscPort conf+#if __GLASGOW_HASKELL__>=708+    when (_hscPort conf < 1024) $+        tell ["listening on a priviledged port requires super user rights"]+#endif+    validateNonEmpty "host" $ _hscHost conf+    validateIPv4 "interface" . B8.unpack $ _hscInterface conf+ instance FromJSON (HttpServiceConfiguration → HttpServiceConfiguration) where     parseJSON = withObject "HttpServiceConfiguration" $ \o → id         <$< hscHost ∘ bs ..: "host" × o@@ -212,6 +244,11 @@     , _hccPort = 80     , _hccUseTLS = False     }++validateHttpClientConfiguration ∷ ConfigValidation HttpClientConfiguration λ+validateHttpClientConfiguration conf = do+    validatePort "port" $ _hccPort conf+    validateNonEmpty "host" $ _hccHost conf  instance FromJSON (HttpClientConfiguration → HttpClientConfiguration) where     parseJSON = withObject "HttpClientConfiguration" $ \o → id
src/Configuration/Utils/Internal.hs view
@@ -10,14 +10,19 @@ ( lens , over , set+, view , Lens' , Lens , Iso' , iso ) where +import Control.Applicative (Const(..))+import Control.Monad.Reader.Class+ import Data.Functor.Identity import Data.Profunctor+import Data.Profunctor.Unsafe  -- -------------------------------------------------------------------------- -- -- Lenses@@ -51,6 +56,9 @@ set ∷ ((α → Identity α) → β → Identity β) → α → β → β set s a = runIdentity . s (const $ Identity a) {-# INLINE set #-}++view ∷ MonadReader σ μ ⇒ ((α → Const α α) → σ → Const α σ) → μ α+view l = asks (getConst #. l Const)  -- | This is the same type as the type from the lens library with the same name. --
+ src/Configuration/Utils/Validation.hs view
@@ -0,0 +1,302 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnicodeSyntax #-}++-- |+-- Module: Configuration.Utils.Validation+-- Copyright: Copyright © 2014 AlephCloud Systems, Inc.+-- License: MIT+-- Maintainer: Lars Kuhtz <lars@alephcloud.com>+-- Stability: experimental+--+-- Utilities for validating configuration values+--+module Configuration.Utils.Validation+(+-- * Networking+  validateHttpOrHttpsUrl+, validateHttpUrl+, validateHttpsUrl+, validateUri+, validateAbsoluteUri+, validateAbsoluteUriFragment+, validateIPv4+, validateIPv6+, validatePort++-- * Monoids, Foldables and Co+, validateNonEmpty+, validateLength+, validateMinLength+, validateMaxLength+, validateMinMaxLength++-- * Files+, validateFilePath+, validateFile+, validateFileReadable+, validateFileWritable+, validateExecutable+, validateDirectory+) where++import Configuration.Utils++import Control.Monad.Error.Class+import Control.Monad+import Control.Monad.IO.Class++import qualified Data.Foldable as F+import Data.Monoid+import Data.Monoid.Unicode+import Data.String+import qualified Data.Text as T++import Network.URI++import Prelude.Unicode++import System.Directory++-- -------------------------------------------------------------------------- --+-- Utils++sshow+    ∷ (Show α, IsString τ)+    ⇒ α+    → τ+sshow = fromString ∘ show++-- -------------------------------------------------------------------------- --+-- Networking++-- | Validates that a value is an HTTP or HTTPS URL+--+validateHttpOrHttpsUrl+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation String λ+validateHttpOrHttpsUrl configName uri =+    case parseURI uri of+        Nothing → throwError $+            "the value " ⊕ T.pack uri ⊕ " for " ⊕ configName ⊕ " is not a valid URI"+        Just u → unless (uriScheme u ≡ "http:" || uriScheme u ≡ "https:") ∘ throwError $+            "the value " ⊕ T.pack uri ⊕ " for " ⊕ configName ⊕ " is not an HTTP or HTTPS URL"++-- | Validates that a value is an HTTP URL+--+validateHttpUrl+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation String λ+validateHttpUrl configName uri =+    case parseURI uri of+        Nothing → throwError $+            "the value " ⊕ T.pack uri ⊕ " for " ⊕ configName ⊕ " is not a valid URI"+        Just u → unless (uriScheme u ≡ "http:") ∘ throwError $+            "the value " ⊕ T.pack uri ⊕ " for " ⊕ configName ⊕ " is not an HTTP URL"++-- | Validates that a value is an HTTPS URL+--+validateHttpsUrl+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation String λ+validateHttpsUrl configName uri =+    case parseURI uri of+        Nothing → throwError $+            "the value " ⊕ T.pack uri ⊕ " for " ⊕ configName ⊕ " is not a valid URI"+        Just u → unless (uriScheme u ≡ "https:") ∘ throwError $+            "the value " ⊕ T.pack uri ⊕ " for " ⊕ configName ⊕ " is not an HTTPS URL"++-- | Validates that a value is an URI without a fragment identifier+--+validateUri+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation String λ+validateUri configName uri =+    unless (isURIReference uri) ∘ throwError $+        "The value " ⊕ T.pack uri ⊕ " for " ⊕ configName ⊕ " is not a valid URI"++-- | Validates that a value is an absolute URI without a fragment identifier+--+validateAbsoluteUri+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation String λ+validateAbsoluteUri configName uri =+    unless (isAbsoluteURI uri) ∘ throwError $+        "The value " ⊕ T.pack uri ⊕ " for " ⊕ configName ⊕ " is not a valid URI"++-- | Validates that a value is an absolute URI with an optional fragment+-- identifier+--+validateAbsoluteUriFragment+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation String λ+validateAbsoluteUriFragment configName uri =+    unless (isURI uri) ∘ throwError $+        "The value " ⊕ T.pack uri ⊕ " for " ⊕ configName ⊕ " is not a valid URI"++validateIPv4+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation String λ+validateIPv4 configName ipv4 =+    unless (isIPv4address ipv4) ∘ throwError $+        "The value " ⊕ T.pack ipv4 ⊕ " for " ⊕ configName ⊕ " is not a valid IPv4 address"++validateIPv6+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation String λ+validateIPv6 configName ipv6 =+    unless (isIPv6address ipv6) ∘ throwError $+        "The value " ⊕ T.pack ipv6 ⊕ " for " ⊕ configName ⊕ " is not a valid IPv6 address"++validatePort+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation Int λ+validatePort configName p =+    unless (p > 1 && p < 65535) ∘ throwError $+        "port value " ⊕ T.pack (show p) ⊕ " for " ⊕ configName ⊕ " is not valid port number"++-- -------------------------------------------------------------------------- --+-- Monoids, Foldables, and Co++validateNonEmpty+    ∷ (Eq α, Monoid α)+    ⇒ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation α λ+validateNonEmpty configName x =+    when (x ≡ mempty) ∘ throwError $+        "value for " ⊕ configName ⊕ " must not be empty"++validateLength+    ∷ (F.Foldable φ)+    ⇒ T.Text+        -- ^ configuration property name that is used in the error message+    → Int+        -- ^ exact length of the validated value+    → ConfigValidation (φ α) λ+validateLength configName len x =+    unless (length (F.toList x) ≡ len) ∘ throwError $+        "value for " ⊕ configName ⊕ " must be of length exactly " ⊕ sshow len++validateMaxLength+    ∷ (F.Foldable φ)+    ⇒ T.Text+        -- ^ configuration property name that is used in the error message+    → Int+        -- ^ maximum length of the validated value+    → ConfigValidation (φ α) λ+validateMaxLength configName u x =+    unless (length (F.toList x) ≤ u) ∘ throwError $+        "value for " ⊕ configName ⊕ " must be of length at most " ⊕ sshow u++validateMinLength+    ∷ (F.Foldable φ)+    ⇒ T.Text+        -- ^ configuration property name that is used in the error message+    → Int+        -- ^ minimum length of the validated value+    → ConfigValidation (φ α) λ+validateMinLength configName l x =+    unless (length (F.toList x) ≥ l) ∘ throwError $+        "value for " ⊕ configName ⊕ " must be of length at least " ⊕ sshow l++validateMinMaxLength+    ∷ (F.Foldable φ)+    ⇒ T.Text+        -- ^ configuration property name that is used in the error message+    → Int+        -- ^ minimum length of the validated value+    → Int+        -- ^ maximum length of the validated value+    → ConfigValidation (φ α) λ+validateMinMaxLength configName l u x =+    unless (len ≥ l && len ≤ u) ∘ throwError $+        "the length of the value for " ⊕ configName ⊕+        " must be at least " ⊕ sshow l ⊕ " and at most " ⊕ sshow u+  where+    len = length $ F.toList x++-- -------------------------------------------------------------------------- --+-- Files++validateFilePath+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation FilePath λ+validateFilePath configName file =+    when (null file) ∘ throwError $+        "file path for " ⊕ configName ⊕ " must not be empty"++validateFile+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation FilePath λ+validateFile configName file = do+    exists ← liftIO $ doesFileExist file+    unless exists ∘ throwError $+        "the file " ⊕ T.pack file ⊕ " for " ⊕ configName ⊕ " does not exist"++validateFileReadable+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation FilePath λ+validateFileReadable configName file = do+    validateFile configName file+    liftIO (getPermissions file) >>= \x → unless (readable x) ∘ throwError $+        "the file " ⊕ T.pack file ⊕ " for " ⊕ configName ⊕ " is not readable"++validateFileWritable+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation FilePath λ+validateFileWritable configName file = do+    validateFile configName file+    liftIO (getPermissions file) >>= \x → unless (writable x) ∘ throwError $+        "the file " ⊕ T.pack file ⊕ " for " ⊕ configName ⊕ " is not writable"++validateFileExecutable+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation FilePath λ+validateFileExecutable configName file = do+    validateFile configName file+    liftIO (getPermissions file) >>= \x → unless (executable x) ∘ throwError $+        "the file " ⊕ T.pack file ⊕ " for " ⊕ configName ⊕ " is not excutable"++validateDirectory+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation FilePath λ+validateDirectory configName dir = do+    exists ← liftIO $ doesDirectoryExist dir+    unless exists ∘ throwError $+        "the directory " ⊕ T.pack dir ⊕ " for " ⊕ configName ⊕ " does not exist"++-- | Validates if the given executable name can be found in the system+-- and can be executed.+--+validateExecutable+    ∷ T.Text+        -- ^ configuration property name that is used in the error message+    → ConfigValidation FilePath λ+validateExecutable configName file = do+    execFile ← (file <$ validateFile configName file) `catchError` \_ ->+        liftIO (findExecutable file) >>= \case+            Nothing → throwError $+                "the executable " ⊕ T.pack file ⊕ " for " ⊕ configName ⊕ " could not be found in the system;"+                ⊕ " you may check your SearchPath and PATH variable settings"+            Just f → return f+    validateFileExecutable configName execFile+
+ test/TestExample.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnicodeSyntax #-}++-- |+-- Module: Main+-- Copyright: Copyright © 2014 AlephCloud Systems, Inc.+-- License: MIT+-- Maintainer: Lars Kuhtz <lars@alephcloud.com>+-- Stability: experimental+--+-- Tests for Example+--+module Main+( main+) where++import Control.Exception++import qualified Example++import System.Environment++main ∷ IO ()+main = do+    withArgs ["--domain=localhost"] Example.main+    withArgs ["--path=abc"] Example.main+    withArgs ["--path=abc", "--user=u"] Example.main+    handle (\(_ ∷ SomeException) → return ()) $ do+        withArgs [] Example.main+        error "expected failure but got success"+    return ()+