Как лучше всего обращаться с целостностью данных при использовании постоянной библиотеки Haskell? - PullRequest
1 голос
/ 31 октября 2019

Допустим, в моем приложении есть базовая модель для работы с пользователями:

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'ы.

...