{-# 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 -- 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