packages feed

ditto-0.5.0: test/Main.hs

module Main (main) where

import Control.Applicative ((<|>))
import Control.Monad (unless)
import Control.Monad.Reader (runReaderT)
import Data.Functor.Identity (Identity, runIdentity)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Text (Text)
import Ditto
import Ditto.Generalized.Named qualified as Named
import Ditto.Generalized.Unnamed qualified as Unnamed
import System.Exit (exitFailure)
import System.IO (hPutStrLn, stderr)
import Text.Read (readMaybe)

import Data.Text qualified as T

-- | Minimal backend input type for file-upload tests.
newtype TestInput = TestInput Text
  deriving stock (Show)
  deriving newtype (Eq)

instance FormInput TestInput where
  type FileType TestInput = Text
  getInputTexts (TestInput t) = [t]
  getInputFile (TestInput t) = Right t

instance FormError TestInput Text where
  commonFormError = commonFormErrorText (T.pack . show)

main :: IO ()
main = do
  testEncodeFormId
  testResultApplicative
  testValueInstances
  testViewForm
  testNamedInput
  testUnnamedInput
  testIreqIopt
  testProofs
  testProofDecode
  testHoistForm
  testAlternative
  testCatchFormError
  testInputMulti
  testInputChoice
  testInputFile
  putStrLn "OK"

assertEq :: (Eq a, Show a) => String -> a -> a -> IO ()
assertEq label expected actual =
  unless (expected == actual) $ do
    hPutStrLn stderr $ "FAIL: " <> label
    hPutStrLn stderr $ "  expected: " <> show expected
    hPutStrLn stderr $ "  got:      " <> show actual
    exitFailure

testEncodeFormId :: IO ()
testEncodeFormId = do
  assertEq "encodeFormId numbered"
    "user-val-0.1.2"
    (encodeFormId (FormId "user" (0 :| [1, 2])))
  assertEq "encodeFormId named"
    "email"
    (encodeFormId (FormIdName "email" 3))
  assertEq "formIdentifier numbered" 0 (formIdentifier (FormId "user" (0 :| [1])))
  assertEq "formIdentifier named" 3 (formIdentifier (FormIdName "email" 3))

range0 :: FormRange
range0 = FormRange (FormIdName "a" 0) (FormIdName "a" 1)

testResultApplicative :: IO ()
testResultApplicative = do
  assertEq "Result accumulates errors"
    (Error [(range0, "a" :: Text), (range0, "b")] :: Result Text Int)
    (Error [(range0, "a")] <*> Error [(range0, "b")])
  assertEq "Result Ok <*> Ok"
    (Ok (3 :: Int) :: Result Text Int)
    (Ok (+ 1) <*> Ok (2 :: Int))

testValueInstances :: IO ()
testValueInstances = do
  assertEq "Found <*> Found" (Found (3 :: Int)) (Found (+ 1) <*> Found (2 :: Int))
  assertEq "Missing <|> Found" (Found (1 :: Int)) (Missing <|> Found 1)
  assertEq "Found <> Found" (Found ("ab" :: Text)) (Found "a" <> Found "b")

testViewForm :: IO ()
testViewForm = do
  let html = runIdentity $ viewForm "f" (view ("hello" :: Text))
  assertEq "viewForm renders defaults" ("hello" :: Text) html

evalForm
  :: [(Text, Text)]
  -> Form (WithEnvironment Text Identity) Text Text view a
  -> Either view a
evalForm pairs form =
  runIdentity $ flip runReaderT lookupFn $ getWithEnvironment $ eitherForm "f" form
  where
    lookupFn fid = pure $ maybe Missing Found (lookup (encodeFormId fid) pairs)

evalFormTest
  :: [(Text, TestInput)]
  -> Form (WithEnvironment TestInput Identity) TestInput Text view a
  -> Either view a
evalFormTest pairs form =
  runIdentity $ flip runReaderT lookupFn $ getWithEnvironment $ eitherForm "f" form
  where
    lookupFn fid = pure $ maybe Missing Found (lookup (encodeFormId fid) pairs)

evalFormDefault
  :: Form (WithEnvironment Text Identity) Text Text view a
  -> Either view a
evalFormDefault form =
  runIdentity $ flip runReaderT (const (pure Default)) $ getWithEnvironment $ eitherForm "f" form

evalFormTestDefault
  :: Form (WithEnvironment TestInput Identity) TestInput Text view a
  -> Either view a
evalFormTestDefault form =
  runIdentity $ flip runReaderT (const (pure Default)) $ getWithEnvironment $ eitherForm "f" form

decodeSubmitted
  :: Text
  -> Form (WithEnvironment Text Identity) Text Text view a
  -> Either Text a
decodeSubmitted raw form =
  runIdentity $
    runReaderT
      (getWithEnvironment (formDecodeInput form raw))
      (const (pure (Found raw)))

textField
  :: Text
  -> Text
  -> Form (WithEnvironment Text Identity) Text Text Text Text
textField name initial =
  Named.input name Right (\_fid val -> val) initial

testNamedInput :: IO ()
testNamedInput = do
  let form = (,) <$> textField "first" "a" <*> textField "last" "b"
  assertEq "named input reads environment"
    (Right ("Ada", "Lovelace") :: Either Text (Text, Text))
    (evalForm [("first", "Ada"), ("last", "Lovelace")] form)
  assertEq "named input uses defaults when missing"
    (Left "ab" :: Either Text (Text, Text))
    (evalForm [] form)

unnamedText
  :: Text
  -> Form (WithEnvironment Text Identity) Text Text Text Text
unnamedText initial =
  Unnamed.input Right (\_fid val -> val) initial

testUnnamedInput :: IO ()
testUnnamedInput = do
  let form = (,) <$> unnamedText "a" <*> unnamedText "b"
  assertEq "unnamed input enumerates ids"
    (Right ("Ada", "Lovelace") :: Either Text (Text, Text))
    (evalForm [("f-val-0", "Ada"), ("f-val-1", "Lovelace")] form)

readInt :: Text -> Either Text Int
readInt t = maybe (Left "not an int") Right (readMaybe (T.unpack t))

testIreqIopt :: IO ()
testIreqIopt = do
  assertEq "ireq success"
    (Right (42 :: Int) :: Either Text Int)
    (evalForm [("age", "42")] (Named.ireq "age" readInt 0))
  assertEq "ireq missing"
    (Left (mempty :: Text) :: Either Text Int)
    (evalForm [] (Named.ireq "age" readInt 0))
  assertEq "iopt missing is Nothing"
    (Right Nothing :: Either Text (Maybe Int))
    (evalForm [] (Named.iopt "age" readInt Nothing))
  assertEq "iopt found"
    (Right (Just (7 :: Int)) :: Either Text (Maybe Int))
    (evalForm [("age", "7")] (Named.iopt "age" readInt Nothing))

testProofs :: IO ()
testProofs = do
  let parsed =
        transformEither
          (pure "12" :: Form Identity Text Text Text String)
          (\s -> if s == "12" then Right (12 :: Int) else Left ("bad" :: Text))
          (const 0)
  assertEq "transformEither success"
    (Right (12 :: Int))
    (runIdentity $ eitherForm "f" parsed)
  let dec = prove (pure "42" :: Form Identity Text Text Text String) (decimal T.pack 0)
  assertEq "decimal proof"
    (Right (42 :: Int))
    (runIdentity $ eitherForm "f" dec)
  let nonempty = prove (pure [1, 2 :: Int] :: Form Identity Text Text Text [Int]) (notNullProof ("empty" :: Text))
  assertEq "notNullProof"
    (Right [1, 2 :: Int])
    (runIdentity $ eitherForm "f" nonempty)

testProofDecode :: IO ()
testProofDecode = do
  let decoded = transformEither (textField "amount" "0") readInt (const 0)
  assertEq "prove decode valid"
    (Right (7 :: Int))
    (decodeSubmitted "7" decoded)
  assertEq "prove decode invalid"
    (Left ("not an int" :: Text))
    (decodeSubmitted "nope" decoded)
  assertEq "prove submit valid"
    (Right (9 :: Int))
    (evalForm [("amount", "9")] decoded)

pickLabels :: FormId -> [Named.Choice Text Text] -> Text
pickLabels _ = T.intercalate "," . map Named.choiceLabel

testHoistForm :: IO ()
testHoistForm = do
  let inner = view ("ok" :: Text) :: Form Identity Text Text Text ()
      outer = hoistForm id inner
  assertEq "hoistForm Identity"
    ("ok" :: Text)
    (runIdentity $ viewForm "f" outer)

testAlternative :: IO ()
testAlternative = do
  let failing = Named.ireq "x" readInt 0 :: Form (WithEnvironment Text Identity) Text Text Text Int
      fallback = pure (99 :: Int) :: Form (WithEnvironment Text Identity) Text Text Text Int
      combined = failing <|> fallback
  assertEq "Alternative fallback"
    (Right (99 :: Int))
    (evalForm [] combined)

testCatchFormError :: IO ()
testCatchFormError = do
  let form =
        catchFormError
          (const (0 :: Int))
          (Named.ireq "x" readInt 999 :: Form (WithEnvironment Text Identity) Text Text Text Int)
  assertEq "catchFormError recovery"
    (Right (0 :: Int))
    (evalForm [] form)

parseTags :: Text -> Either Text [Text]
parseTags inp = Right (T.words inp)

testInputMulti :: IO ()
testInputMulti = do
  let form =
        Named.inputMulti
          "tags"
          [("a", "alpha"), ("b", "beta")]
          parseTags
          pickLabels
          (== "a")
  assertEq "inputMulti default selection"
    (Right ["a"] :: Either Text [Text])
    (evalFormDefault form)
  assertEq "inputMulti submitted values"
    (Right ["b"] :: Either Text [Text])
    (evalForm [("tags", "b")] form)
  assertEq "inputMulti multiple submitted values"
    (Right ["a", "b"] :: Either Text [Text])
    (evalForm [("tags", "a b")] form)

testInputChoice :: IO ()
testInputChoice = do
  let form =
        Named.inputChoice
          "mode"
          (== "b")
          (("a", "A") :| [("b", "B")])
          (\inp -> Right inp)
          pickLabels
  assertEq "inputChoice default"
    (Right ("b" :: Text))
    (evalForm [] form)
  assertEq "inputChoice submitted"
    (Right ("a" :: Text))
    (evalForm [("mode", "a")] form)

testInputFile :: IO ()
testInputFile = do
  let form = Named.inputFile "upload" (\_ -> "widget" :: Text)
  assertEq "inputFile initial render"
    (Right (mempty :: Text))
    (evalFormTestDefault form)
  assertEq "inputFile missing on submit"
    (Left ("widget" :: Text))
    (evalFormTest [] form)
  assertEq "inputFile submitted"
    (Right ("payload" :: Text))
    (evalFormTest [("upload", TestInput "payload")] form)
  assertEq "inputFile decode"
    (Right ("payload" :: Text))
    (runIdentity $ runReaderT (getWithEnvironment (formDecodeInput form (TestInput "payload"))) (const (pure Default)))