Допустим, в моем приложении есть базовая модель для работы с пользователями:
User
username Text
password Text
email Text
На основе этих типов я могу хранить все, что захочу (если, конечно, Text
) для любого изэти поля. Скажем, я хочу начать применять некоторые правила, касающиеся того, что можно и нельзя считать электронной почтой. Я создаю этот модуль электронной почты:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Model.EmailAddress
( EmailAddress
, parseEmailAddress
) where
import Data.Aeson
import Data.Text (Text)
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TL
import Database.Persist
import Database.Persist.Sql
import Text.Shakespeare.Text (ToText (..))
newtype EmailAddress = EmailAddress (Either InvalidEmailAddress ValidEmailAddress)
deriving (Show, Eq)
instance ToJSON EmailAddress where
toJSON = String . toStrictText
instance FromJSON EmailAddress where
parseJSON = withText "EmailAddress" (pure . parseEmailAddress)
instance ToText EmailAddress where
toText (EmailAddress e) = either toText toText e
newtype ValidEmailAddress = ValidEmailAddress Text
deriving (Show, Eq)
instance ToText ValidEmailAddress where
toText (ValidEmailAddress t) = toText t
data InvalidEmailAddress = InvalidEmailAddress
{ invalidEmailAddressValue :: Text
, invalidEmailAddressReason :: Text
} deriving (Show, Eq)
instance ToText InvalidEmailAddress where
toText = toText . invalidEmailAddressValue
parseEmailAddress :: Text -> EmailAddress
parseEmailAddress t =
-- super basic validation example
EmailAddress $ case T.count "@" t of
0 -> Left (InvalidEmailAddress t "Must contain @ symbol")
1 -> Right (ValidEmailAddress t)
_ -> Left (InvalidEmailAddress t "Contains more than one @ symbol")
instance PersistFieldSql EmailAddress where
sqlType _ = SqlString
instance PersistField EmailAddress where
fromPersistValue =
decodePersistTextValue parseEmailAddress
toPersistValue =
PersistText . toStrictText
decodePersistTextValue :: PersistField a => (Text -> a) -> PersistValue -> Either Text a
decodePersistTextValue f = \case
PersistByteString bs ->
fromPersistValue $ PersistText (T.decodeUtf8 bs)
v ->
f <$> fromPersistValue v
toStrictText :: ToText t => t -> Text
toStrictText = builderToStrictText . toText
builderToStrictText :: TL.Builder -> Text
builderToStrictText = TL.toStrict . TL.toLazyText
Теперь я могу определить свою модель следующим образом:
User
username Text
password Text
email EmailAddress
Однако это не мешает мне записать неверный адрес электронной почты в базу данных. Если изменятся правила относительно того, что составляет действительный адрес электронной почты, я все равно смогу прочитать их из БД, и ранее действительные электронные письма будут выходить из БД как недействительные, и это нормально, мне это нравится. Однако я не могу принудить (на все время постоянном уровне) предотвратить запись недопустимого электронного письма в базу данных, потому что toPersistValue
имеет тип a -> PersistValue
, я полагаю, мне нужно было бы принудительно установить этот уровень вверх (когда данные поступают в приложение). , декодеры форм, JSON-декодеры и т. д.), чтобы при появлении Left InvalidEmailAddress
пользователю сообщалось об ошибке.
Это заставляет меня задуматься, является ли это наилучшим способом обеспечения целостности данных при использовании постоянных данных? есть ли лучший подход? Должен ли постоянный вообще не участвовать? то есть поле остается как Text
, и приложение должно преобразовывать эти поля при обращении к ним?
Мне бы очень хотелось, чтобы способ записи моих сущностей в базу данных в безопасном видеспособ, позволяющий мне определить, что делать в ситуации, когда для сохранения моего типа User
используется недопустимая электронная почта, я могу выбросить error
в реализации toPersistValue
, но мне это кажется немного грязным,и не очень Haskell'ы.