{-# LANGUAGE ConstraintKinds , OverloadedStrings , TypeFamilies , MultiParamTypeClasses , GeneralizedNewtypeDeriving , LambdaCase , StandaloneDeriving , RankNTypes , OverloadedStrings , LambdaCase , DataKinds , PolyKinds , GADTs , TypeOperators , TypeFamilies , RankNTypes , ScopedTypeVariables , DeriveFunctor , GeneralizedNewtypeDeriving #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Trasa.Form ( FormData(..) , FormError(..) , TrasaFormT(..) , FormType(..) , TrasaForm , TrasaFormError(..) , bodyFormData , liftParser , liftToForm , reform , reformPost , trasaFormView , textError ) where import Control.Applicative (Alternative(..)) import Control.Monad.Except import Control.Monad.Reader -- import Data.Functor.Identity import Data.String (IsString(..)) import Data.Text (Text) import Data.Bifunctor import Ditto.Backend import Ditto.Core hiding (view) import Ditto.Types import Lucid import Trasa.Core hiding (optional) import Trasa.Server import Trasa.Url import qualified Data.HashMap.Strict as HM import qualified Data.Text as T import qualified Web.FormUrlEncoded as HTTP instance FormError QueryParam TrasaFormError where commonFormError e = TrasaFormError (Just e) (commonFormErrorText encQP e) where encQP QueryParamFlag = "" encQP (QueryParamSingle x) = x encQP (QueryParamList xs) = tshow xs instance FormInput QueryParam where type FileType QueryParam = () getInputStrings (QueryParamSingle x) = [T.unpack x] getInputStrings (QueryParamList xs) = fmap T.unpack xs getInputStrings (QueryParamFlag) = [] getInputFile _ = Left $ commonFormError $ (NoFileFound (QueryParamSingle "No support for file uploads") :: CommonFormError QueryParam) -- | @trasa-form@'s form error type: may have a @CommonFormError@, will have an error message data TrasaFormError = TrasaFormError { trasaFormErrorType :: Maybe (CommonFormError QueryParam) , trasaFormErrorText :: Text } deriving Show deriving instance Show QueryParam instance ToHtml TrasaFormError where toHtml = toHtml . trasaFormErrorText toHtmlRaw = toHtmlRaw . trasaFormErrorText instance IsString TrasaFormError where fromString = TrasaFormError Nothing . fromString -- | convert @Text@ to a @TrasaFormError@ textError :: Text -> TrasaFormError textError = TrasaFormError Nothing -- | lift a function which parses @Text@ into a function which parses a @QueryParam@ liftParser :: (Text -> Either Text a) -> (QueryParam -> Either TrasaFormError a) liftParser f q = case q of QueryParamSingle x -> first textError $ f x QueryParamList [x] -> first textError $ f x QueryParamFlag -> Left $ textError "Unexpected query flag" QueryParamList [] -> Left $ textError "Unexpect empty query list" QueryParamList (_:_:_) -> Left $ textError "Unexpected query string list" tshow :: Show a => a -> Text tshow = T.pack . show -- | a type alias for the most common type of form using @trasa-form@ type TrasaForm a = Form (TrasaFormT IO) QueryParam TrasaFormError (Html ()) a -- | run a @Form@ with the @GET@ method reform :: (MonadIO m, Monoid view) => ([(Text, Text)] -> view -> view) -- ^ wrap raw form html inside a
tag -> Text -- ^ form name prefix -> Form (TrasaFormT m) QueryParam err view a -- ^ the formlet -> TrasaT m (Result err a, view) reform toForm prefix formlet = do (View viewf, res) <- flip runReaderT Get $ getTrasaFormT $ runForm prefix formlet case res of Error errs -> pure (Error errs, toForm [] $ viewf errs) Ok (Proved _ unProved') -> pure (Ok unProved', toForm [] $ viewf []) instance MonadIO m => Environment (TrasaT m) QueryParam where environment formId = do QueryString queryString <- trasaQueryString <$> ask let val = HM.lookup (encodeFormId formId) queryString case val of Nothing -> pure Missing Just x -> pure (Found x) -- | run a @Form@ with the @POST@ method reformPost :: (MonadIO m, Monoid view) => ([(Text, Text)] -> view -> view) -- ^ wrap raw form html inside a tag -> Text -- ^ form name prefix -> Maybe (FormData a) -> Form (TrasaFormT m) QueryParam err view a -- ^ the formlet -> TrasaT m (Result err a, view) reformPost toForm prefix formData formlet = do (View viewf, res) <- flip runReaderT (Post $ getFormData <$> formData) $ getTrasaFormT $ runForm prefix formlet case res of Error errs -> pure (Error errs, toForm [] $ viewf errs) Ok (Proved _ unProved') -> pure (Ok unProved', toForm [] $ viewf []) newtype FormData a = FormData { getFormData :: HM.HashMap Text [Text] } deriving (Monoid, Semigroup, Eq, Ord, Show) -- | this is a @BodyCodec@, the intended use is to get the request body -- without using @wai@ black magic to pass to @POST@ forms bodyFormData :: BodyCodec (FormData a) bodyFormData = BodyCodec (pure "application/x-www-form-urlencoded") (HTTP.urlEncodeAsFormStable . getFormData) (fmap (FormData . HTTP.unForm) . HTTP.urlDecodeForm) -- | a newtype over TrasaT which allows it to have -- different environments for @GET@ and @POST@ requests newtype TrasaFormT m a = TrasaFormT { getTrasaFormT :: ReaderT FormType (TrasaT m) a } deriving (Monad, Applicative, Functor, Alternative) instance MonadTrans (TrasaFormT) where lift = TrasaFormT . lift . lift deriving instance Monad m => MonadReader FormType (TrasaFormT m) -- | No environment, a GET request, or a @POST@ request which may hav the untyped @FormData@ data FormType = None | Get | Post (Maybe (HM.HashMap Text [Text])) instance Monad m => Environment (TrasaFormT m) QueryParam where environment formId = ask >>= \case Post (Just urlEncoded) -> do case HM.lookup (encodeFormId formId) urlEncoded of Nothing -> pure Missing Just [] -> pure $ Found QueryParamFlag Just [x] -> pure $ Found $ QueryParamSingle x Just xs -> pure $ Found $ QueryParamList xs Post Nothing -> pure Default Get -> TrasaFormT $ lift $ do QueryString queryString <- trasaQueryString <$> ask case HM.lookup (encodeFormId formId) queryString of Nothing -> pure Missing Just x -> pure (Found x) None -> pure Default -- | lift a TrasaT to a TrasaFormT liftToForm :: Monad m => TrasaT m a -> TrasaFormT m a liftToForm = TrasaFormT . lift -- | @viewForm@ helper function trasaFormView :: Monad m => Text -> Form (TrasaFormT m) QueryParam err view a -> TrasaT m view trasaFormView name form = flip runReaderT None $ getTrasaFormT $ viewForm name form