module IHP.ValidationSupport.ValidateField where
import ClassyPrelude
import Data.Proxy
import IHP.ValidationSupport.Types
import GHC.TypeLits (KnownSymbol)
import GHC.Records
import IHP.ModelSupport
import IHP.HaskellSupport
import Text.Regex.TDFA
import Data.List ((!!))
import Network.URI (parseURI, uriScheme)
type Validator valueType = valueType -> ValidatorResult
validateField :: forall field fieldValue model. (
KnownSymbol field
, HasField field model fieldValue
, HasField "meta" model MetaBag
, SetField "meta" model MetaBag
) => Proxy field -> Validator fieldValue -> model -> model
validateField :: forall (field :: Symbol) fieldValue model.
(KnownSymbol field, HasField field model fieldValue,
HasField "meta" model MetaBag, SetField "meta" model MetaBag) =>
Proxy field -> Validator fieldValue -> model -> model
validateField Proxy field
field Validator fieldValue
validator model
model = Proxy field -> ValidatorResult -> model -> model
forall (field :: Symbol) model.
(KnownSymbol field, HasField "meta" model MetaBag,
SetField "meta" model MetaBag) =>
Proxy field -> ValidatorResult -> model -> model
attachValidatorResult Proxy field
field (Validator fieldValue
validator (forall {k} (x :: k) r a. HasField x r a => r -> a
forall (x :: Symbol) r a. HasField x r a => r -> a
getField @field model
model)) model
model
{-# INLINE validateField #-}
type ValidatorIO value = value -> IO ValidatorResult
validateFieldIO :: forall field model fieldValue. (
?modelContext :: ModelContext
, KnownSymbol field
, HasField field model fieldValue
, HasField "meta" model MetaBag
, SetField "meta" model MetaBag
) => Proxy field -> ValidatorIO fieldValue -> model -> IO model
validateFieldIO :: forall (field :: Symbol) model fieldValue.
(?modelContext::ModelContext, KnownSymbol field,
HasField field model fieldValue, HasField "meta" model MetaBag,
SetField "meta" model MetaBag) =>
Proxy field -> ValidatorIO fieldValue -> model -> IO model
validateFieldIO Proxy field
fieldProxy ValidatorIO fieldValue
customValidation model
model = do
let fieldValue
value :: fieldValue = forall {k} (x :: k) r a. HasField x r a => r -> a
forall (x :: Symbol) r a. HasField x r a => r -> a
getField @field model
model
ValidatorResult
result <- ValidatorIO fieldValue
customValidation fieldValue
value
model -> IO model
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Proxy field -> ValidatorResult -> model -> model
forall (field :: Symbol) model.
(KnownSymbol field, HasField "meta" model MetaBag,
SetField "meta" model MetaBag) =>
Proxy field -> ValidatorResult -> model -> model
attachValidatorResult Proxy field
fieldProxy ValidatorResult
result model
model)
{-# INLINE validateFieldIO #-}
validateMaybe :: (val -> ValidatorResult) -> Maybe val -> ValidatorResult
validateMaybe :: forall val.
(val -> ValidatorResult) -> Maybe val -> ValidatorResult
validateMaybe val -> ValidatorResult
_ Maybe val
Nothing = ValidatorResult
Success
validateMaybe val -> ValidatorResult
validator (Just val
value) = val -> ValidatorResult
validator val
value
{-# INLINE validateMaybe #-}
withCustomErrorMessage :: Text -> (value -> ValidatorResult) -> value -> ValidatorResult
withCustomErrorMessage :: forall value.
Text -> (value -> ValidatorResult) -> value -> ValidatorResult
withCustomErrorMessage Text
errorMessage value -> ValidatorResult
validator value
value =
case value -> ValidatorResult
validator value
value of
Failure Text
_ -> Text -> ValidatorResult
Failure Text
errorMessage
ValidatorResult
Success -> ValidatorResult
Success
{-# INLINABLE withCustomErrorMessage #-}
validateAny :: [value -> ValidatorResult] -> value -> ValidatorResult
validateAny :: forall value.
[value -> ValidatorResult] -> value -> ValidatorResult
validateAny [value -> ValidatorResult]
validators value
text =
case (Element [ValidatorResult] -> Bool) -> [ValidatorResult] -> Bool
forall mono.
MonoFoldable mono =>
(Element mono -> Bool) -> mono -> Bool
any Element [ValidatorResult] -> Bool
ValidatorResult -> Bool
isSuccess ([ValidatorResult] -> Bool) -> [ValidatorResult] -> Bool
forall a b. (a -> b) -> a -> b
$ ((value -> ValidatorResult) -> ValidatorResult)
-> [value -> ValidatorResult] -> [ValidatorResult]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
map ((value -> ValidatorResult) -> value -> ValidatorResult
forall a b. (a -> b) -> a -> b
$ value
text) [value -> ValidatorResult]
validators of
Bool
True -> ValidatorResult
Success
Bool
False -> Text -> ValidatorResult
Failure Text
"did not pass any validators"
{-# INLINABLE validateAny #-}
validateAll :: [value -> ValidatorResult] -> value -> ValidatorResult
validateAll :: forall value.
[value -> ValidatorResult] -> value -> ValidatorResult
validateAll [value -> ValidatorResult]
validators value
text =
let results :: [ValidatorResult]
results = ((value -> ValidatorResult) -> ValidatorResult)
-> [value -> ValidatorResult] -> [ValidatorResult]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
map ((value -> ValidatorResult) -> value -> ValidatorResult
forall a b. (a -> b) -> a -> b
$ value
text) [value -> ValidatorResult]
validators
in case (Element [ValidatorResult] -> Bool) -> [ValidatorResult] -> Bool
forall mono.
MonoFoldable mono =>
(Element mono -> Bool) -> mono -> Bool
all Element [ValidatorResult] -> Bool
ValidatorResult -> Bool
isSuccess [ValidatorResult]
results of
Bool
True -> ValidatorResult
Success
Bool
False -> ((Element [ValidatorResult] -> Bool)
-> [ValidatorResult] -> [ValidatorResult]
forall seq. IsSequence seq => (Element seq -> Bool) -> seq -> seq
filter Element [ValidatorResult] -> Bool
ValidatorResult -> Bool
isFailure [ValidatorResult]
results) [ValidatorResult] -> Int -> ValidatorResult
forall a. HasCallStack => [a] -> Int -> a
!! Int
0
{-# INLINABLE validateAll #-}
nonEmpty :: IsEmpty value => value -> ValidatorResult
nonEmpty :: forall value. IsEmpty value => value -> ValidatorResult
nonEmpty value
value | value -> Bool
forall value. IsEmpty value => value -> Bool
isEmpty value
value = Text -> ValidatorResult
Failure Text
"This field cannot be empty"
nonEmpty value
_ = ValidatorResult
Success
{-# INLINABLE nonEmpty #-}
isEmptyValue :: IsEmpty value => value -> ValidatorResult
isEmptyValue :: forall value. IsEmpty value => value -> ValidatorResult
isEmptyValue value
value | value -> Bool
forall value. IsEmpty value => value -> Bool
isEmpty value
value = ValidatorResult
Success
isEmptyValue value
_ = Text -> ValidatorResult
Failure Text
"This field must be empty"
{-# INLINABLE isEmptyValue #-}
isPhoneNumber :: Text -> ValidatorResult
isPhoneNumber :: Text -> ValidatorResult
isPhoneNumber Text
text | Text
"+" Text -> Text -> Bool
forall seq.
(IsSequence seq, Eq (Element seq)) =>
seq -> seq -> Bool
`isPrefixOf` Text
text Bool -> Bool -> Bool
&& Text -> Int
forall mono. MonoFoldable mono => mono -> Int
length Text
text Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
5 = ValidatorResult
Success
isPhoneNumber Text
text = Text -> ValidatorResult
Failure Text
"is not a valid phone number (has to start with +, at least 5 characters)"
{-# INLINABLE isPhoneNumber #-}
isEmail :: Text -> ValidatorResult
isEmail :: Text -> ValidatorResult
isEmail Text
text | Text
text Text -> Text -> Bool
forall source source1 target.
(RegexMaker Regex CompOption ExecOption source,
RegexContext Regex source1 target) =>
source1 -> source -> target
=~ (Text
"^[^ @]+@[^ @_+]+\\.?[^ @_+-]+$" :: Text) = ValidatorResult
Success
isEmail Text
text = Text -> ValidatorResult
Failure Text
"is not a valid email"
{-# INLINABLE isEmail #-}
isInRange :: (Show value, Ord value) => (value, value) -> value -> ValidatorResult
isInRange :: forall value.
(Show value, Ord value) =>
(value, value) -> value -> ValidatorResult
isInRange (value
min, value
max) value
value | value
value value -> value -> Bool
forall a. Ord a => a -> a -> Bool
>= value
min Bool -> Bool -> Bool
&& value
value value -> value -> Bool
forall a. Ord a => a -> a -> Bool
<= value
max = ValidatorResult
Success
isInRange (value
min, value
max) value
value = Text -> ValidatorResult
Failure (Text
"has to be between " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> value -> Text
forall a. Show a => a -> Text
tshow value
min Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" and " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> value -> Text
forall a. Show a => a -> Text
tshow value
max)
{-# INLINABLE isInRange #-}
isLessThan :: (Show value, Ord value) => value -> value -> ValidatorResult
isLessThan :: forall value.
(Show value, Ord value) =>
value -> value -> ValidatorResult
isLessThan value
max value
value | value
value value -> value -> Bool
forall a. Ord a => a -> a -> Bool
< value
max = ValidatorResult
Success
isLessThan value
max value
value = Text -> ValidatorResult
Failure (Text
"has to be less than " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> value -> Text
forall a. Show a => a -> Text
tshow value
max)
{-# INLINABLE isLessThan #-}
isGreaterThan :: (Show value, Ord value) => value -> value -> ValidatorResult
isGreaterThan :: forall value.
(Show value, Ord value) =>
value -> value -> ValidatorResult
isGreaterThan value
min value
value | value
value value -> value -> Bool
forall a. Ord a => a -> a -> Bool
> value
min = ValidatorResult
Success
isGreaterThan value
min value
value = Text -> ValidatorResult
Failure (Text
"has to be greater than " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> value -> Text
forall a. Show a => a -> Text
tshow value
min)
{-# INLINABLE isGreaterThan #-}
isEqual :: (Show value, Eq value) => value -> value -> ValidatorResult
isEqual :: forall value.
(Show value, Eq value) =>
value -> value -> ValidatorResult
isEqual value
expected value
value | value
value value -> value -> Bool
forall a. Eq a => a -> a -> Bool
== value
expected = ValidatorResult
Success
isEqual value
expected value
value = Text -> ValidatorResult
Failure (Text
"has to be equal to " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> value -> Text
forall a. Show a => a -> Text
tshow value
expected)
{-# INLINABLE isEqual #-}
hasMaxLength :: Int -> Text -> ValidatorResult
hasMaxLength :: Int -> Text -> ValidatorResult
hasMaxLength Int
max Text
text | Text -> Int
forall mono. MonoFoldable mono => mono -> Int
length Text
text Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
max = ValidatorResult
Success
hasMaxLength Int
max Text
text = Text -> ValidatorResult
Failure (Text
"is longer than " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall a. Show a => a -> Text
tshow Int
max Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" characters")
{-# INLINABLE hasMaxLength #-}
hasMinLength :: Int -> Text -> ValidatorResult
hasMinLength :: Int -> Text -> ValidatorResult
hasMinLength Int
min Text
text | Text -> Int
forall mono. MonoFoldable mono => mono -> Int
length Text
text Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
min = ValidatorResult
Success
hasMinLength Int
min Text
text = Text -> ValidatorResult
Failure (Text
"is shorter than " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall a. Show a => a -> Text
tshow Int
min Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" characters")
{-# INLINABLE hasMinLength #-}
isRgbHexColor :: Text -> ValidatorResult
isRgbHexColor :: Text -> ValidatorResult
isRgbHexColor Text
text | Text
text Text -> Text -> Bool
forall source source1 target.
(RegexMaker Regex CompOption ExecOption source,
RegexContext Regex source1 target) =>
source1 -> source -> target
=~ (Text
"^#([0-9a-f]{3}|[0-9a-f]{6})$" :: Text) = ValidatorResult
Success
isRgbHexColor Text
text = Text -> ValidatorResult
Failure Text
"is not a valid rgb hex color"
{-# INLINABLE isRgbHexColor #-}
isRgbaHexColor :: Text -> ValidatorResult
isRgbaHexColor :: Text -> ValidatorResult
isRgbaHexColor Text
text | Text
text Text -> Text -> Bool
forall source source1 target.
(RegexMaker Regex CompOption ExecOption source,
RegexContext Regex source1 target) =>
source1 -> source -> target
=~ (Text
"^#([0-9a-f]{4}|[0-9a-f]{8})$" :: Text) = ValidatorResult
Success
isRgbaHexColor Text
text = Text -> ValidatorResult
Failure Text
"is not a valid rgba hex color"
{-# INLINABLE isRgbaHexColor #-}
isHexColor :: Text -> ValidatorResult
isHexColor :: Text -> ValidatorResult
isHexColor = [Text -> ValidatorResult] -> Text -> ValidatorResult
forall value.
[value -> ValidatorResult] -> value -> ValidatorResult
validateAny [Text -> ValidatorResult
isRgbHexColor, Text -> ValidatorResult
isRgbaHexColor]
(Text -> ValidatorResult)
-> ((Text -> ValidatorResult) -> Text -> ValidatorResult)
-> Text
-> ValidatorResult
forall {t1} {t2}. t1 -> (t1 -> t2) -> t2
|> Text -> (Text -> ValidatorResult) -> Text -> ValidatorResult
forall value.
Text -> (value -> ValidatorResult) -> value -> ValidatorResult
withCustomErrorMessage Text
"is not a valid hex color"
{-# INLINABLE isHexColor #-}
isRgbColor :: Text -> ValidatorResult
isRgbColor :: Text -> ValidatorResult
isRgbColor Text
text | Text
text Text -> Text -> Bool
forall source source1 target.
(RegexMaker Regex CompOption ExecOption source,
RegexContext Regex source1 target) =>
source1 -> source -> target
=~ (Text
"^rgb\\( *([0-9]*\\.)?[0-9]+ *, *([0-9]*\\.)?[0-9]+ *, *([0-9]*\\.)?[0-9]+ *\\)$" :: Text) = ValidatorResult
Success
isRgbColor Text
text = Text -> ValidatorResult
Failure Text
"is not a valid rgb() color"
{-# INLINABLE isRgbColor #-}
isRgbaColor :: Text -> ValidatorResult
isRgbaColor :: Text -> ValidatorResult
isRgbaColor Text
text | Text
text Text -> Text -> Bool
forall source source1 target.
(RegexMaker Regex CompOption ExecOption source,
RegexContext Regex source1 target) =>
source1 -> source -> target
=~ (Text
"^rgba\\( *([0-9]*\\.)?[0-9]+ *, *([0-9]*\\.)?[0-9]+ *, *([0-9]*\\.)?[0-9]+ *, *([0-9]*\\.)?[0-9]+ *\\)$" :: Text) = ValidatorResult
Success
isRgbaColor Text
text = Text -> ValidatorResult
Failure Text
"is not a valid rgba() color"
{-# INLINABLE isRgbaColor #-}
isColor :: Text -> ValidatorResult
isColor :: Text -> ValidatorResult
isColor = [Text -> ValidatorResult] -> Text -> ValidatorResult
forall value.
[value -> ValidatorResult] -> value -> ValidatorResult
validateAny [Text -> ValidatorResult
isRgbHexColor, Text -> ValidatorResult
isRgbaHexColor, Text -> ValidatorResult
isRgbColor, Text -> ValidatorResult
isRgbaColor]
(Text -> ValidatorResult)
-> ((Text -> ValidatorResult) -> Text -> ValidatorResult)
-> Text
-> ValidatorResult
forall {t1} {t2}. t1 -> (t1 -> t2) -> t2
|> Text -> (Text -> ValidatorResult) -> Text -> ValidatorResult
forall value.
Text -> (value -> ValidatorResult) -> value -> ValidatorResult
withCustomErrorMessage Text
"is not a valid color"
{-# INLINABLE isColor #-}
isUrl :: Text -> ValidatorResult
isUrl :: Text -> ValidatorResult
isUrl Text
url =
case String -> Maybe URI
parseURI (Text -> [Element Text]
forall mono. MonoFoldable mono => mono -> [Element mono]
unpack Text
url) of
Just URI
uri ->
if URI -> String
uriScheme URI
uri Element [String] -> [String] -> Bool
forall mono.
(MonoFoldable mono, Eq (Element mono)) =>
Element mono -> mono -> Bool
`elem` [String
"http:", String
"https:"]
then ValidatorResult
Success
else Text -> ValidatorResult
Failure Text
"URL must start with http:// or https://"
Maybe URI
Nothing -> Text -> ValidatorResult
Failure Text
"Invalid URL"
{-# INLINABLE isUrl #-}
isInList :: (Eq value, Show value) => [value] -> value -> ValidatorResult
isInList :: forall value.
(Eq value, Show value) =>
[value] -> value -> ValidatorResult
isInList [value]
list value
value | [value]
list [value] -> ([value] -> Bool) -> Bool
forall {t1} {t2}. t1 -> (t1 -> t2) -> t2
|> Element [value] -> [value] -> Bool
forall mono.
(MonoFoldable mono, Eq (Element mono)) =>
Element mono -> mono -> Bool
includes value
Element [value]
value = ValidatorResult
Success
isInList [value]
list value
value = Text -> ValidatorResult
Failure (Text
"is not allowed. It needs to be one of the following: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> ([value] -> Text
forall a. Show a => a -> Text
tshow [value]
list))
{-# INLINABLE isInList #-}
isTrue :: Bool -> ValidatorResult
isTrue :: Bool -> ValidatorResult
isTrue Bool
value = if Bool
value then ValidatorResult
Success else Text -> ValidatorResult
Failure Text
"This field cannot be false"
{-# INLINABLE isTrue #-}
isFalse :: Bool -> ValidatorResult
isFalse :: Bool -> ValidatorResult
isFalse Bool
value = if Bool -> Bool
not Bool
value then ValidatorResult
Success else Text -> ValidatorResult
Failure Text
"This field cannot be true"
{-# INLINABLE isFalse #-}
matchesRegex :: Text -> Text -> ValidatorResult
matchesRegex :: Text -> Text -> ValidatorResult
matchesRegex Text
regex Text
text = if Text
text Text -> Text -> Bool
forall source source1 target.
(RegexMaker Regex CompOption ExecOption source,
RegexContext Regex source1 target) =>
source1 -> source -> target
=~ Text
regex then ValidatorResult
Success else Text -> ValidatorResult
Failure (Text -> ValidatorResult) -> Text -> ValidatorResult
forall a b. (a -> b) -> a -> b
$ Text
"This field does not match the regular expression \"" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
regex Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"\""
{-# INLINABLE matchesRegex #-}
isSlug :: Text -> ValidatorResult
isSlug :: Text -> ValidatorResult
isSlug Text
text | Text
text Text -> Text -> Bool
forall source source1 target.
(RegexMaker Regex CompOption ExecOption source,
RegexContext Regex source1 target) =>
source1 -> source -> target
=~ (Text
"^[a-zA-Z0-9_-]+$" :: Text) = ValidatorResult
Success
isSlug Text
text = Text -> ValidatorResult
Failure Text
"is not a valid slug (consisting of only letters, numbers, underscores or hyphens)"
{-# INLINABLE isSlug #-}