Functions.hs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. {-# LANGUAGE QuasiQuotes #-}
  2. {-# LANGUAGE TupleSections #-}
  3. {-# LANGUAGE TypeFamilies #-}
  4. {-# LANGUAGE OverloadedStrings #-}
  5. {-# LANGUAGE RecordWildCards #-}
  6. {-# LANGUAGE FlexibleContexts #-}
  7. {-# LANGUAGE CPP #-}
  8. module Yesod.Form.Functions
  9. ( -- * Running in MForm monad
  10. newFormIdent
  11. , askParams
  12. , askFiles
  13. -- * Applicative/Monadic conversion
  14. , formToAForm
  15. , aFormToForm
  16. -- * Fields to Forms
  17. , mreq
  18. , mopt
  19. , areq
  20. , aopt
  21. -- * Run a form
  22. , runFormPost
  23. , runFormPostNoToken
  24. , runFormGet
  25. -- * Generate a blank form
  26. , generateFormPost
  27. , generateFormGet'
  28. , generateFormGet
  29. -- * More than one form on a handler
  30. , identifyForm
  31. -- * Rendering
  32. , FormRender
  33. , renderTable
  34. , renderDivs
  35. , renderDivsNoLabels
  36. , renderBootstrap
  37. , renderBootstrap2
  38. -- * Validation
  39. , check
  40. , checkBool
  41. , checkM
  42. , checkMMap
  43. , customErrorMessage
  44. -- * Utilities
  45. , fieldSettingsLabel
  46. , parseHelper
  47. , parseHelperGen
  48. , convertField
  49. ) where
  50. import Yesod.Form.Types
  51. import Data.Text (Text, pack)
  52. import Control.Arrow (second)
  53. import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST, local)
  54. import Control.Monad.Trans.Class
  55. import Control.Monad (liftM, join)
  56. import Data.Byteable (constEqBytes)
  57. import Text.Blaze (Markup, toMarkup)
  58. #define Html Markup
  59. #define toHtml toMarkup
  60. import Yesod.Core
  61. import Yesod.Core.Handler (defaultCsrfParamName)
  62. import Network.Wai (requestMethod)
  63. import Text.Hamlet (shamlet)
  64. import Data.Monoid (mempty)
  65. import Data.Maybe (listToMaybe, fromMaybe)
  66. import qualified Data.Map as Map
  67. import qualified Data.Text.Encoding as TE
  68. import Control.Arrow (first)
  69. -- | Get a unique identifier.
  70. newFormIdent :: Monad m => MForm m Text
  71. newFormIdent = do
  72. i <- get
  73. let i' = incrInts i
  74. put i'
  75. return $ pack $ 'f' : show i'
  76. where
  77. incrInts (IntSingle i) = IntSingle $ i + 1
  78. incrInts (IntCons i is) = (i + 1) `IntCons` is
  79. formToAForm :: (HandlerSite m ~ site, Monad m)
  80. => MForm m (FormResult a, [FieldView site])
  81. -> AForm m a
  82. formToAForm form = AForm $ \(site, langs) env ints -> do
  83. ((a, xmls), ints', enc) <- runRWST form (env, site, langs) ints
  84. return (a, (++) xmls, ints', enc)
  85. aFormToForm :: (Monad m, HandlerSite m ~ site)
  86. => AForm m a
  87. -> MForm m (FormResult a, [FieldView site] -> [FieldView site])
  88. aFormToForm (AForm aform) = do
  89. ints <- get
  90. (env, site, langs) <- ask
  91. (a, xml, ints', enc) <- lift $ aform (site, langs) env ints
  92. put ints'
  93. tell enc
  94. return (a, xml)
  95. askParams :: Monad m => MForm m (Maybe Env)
  96. askParams = do
  97. (x, _, _) <- ask
  98. return $ liftM fst x
  99. askFiles :: Monad m => MForm m (Maybe FileEnv)
  100. askFiles = do
  101. (x, _, _) <- ask
  102. return $ liftM snd x
  103. -- | Converts a form field into monadic form. This field requires a value
  104. -- and will return 'FormFailure' if left empty.
  105. mreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
  106. => Field m a -- ^ form field
  107. -> FieldSettings site -- ^ settings for this field
  108. -> Maybe a -- ^ optional default value
  109. -> MForm m (FormResult a, FieldView site)
  110. mreq field fs mdef = mhelper field fs mdef (\m l -> FormFailure [renderMessage m l MsgValueRequired]) FormSuccess True
  111. -- | Converts a form field into monadic form. This field is optional, i.e.
  112. -- if filled in, it returns 'Just a', if left empty, it returns 'Nothing'.
  113. -- Arguments are the same as for 'mreq' (apart from type of default value).
  114. mopt :: (site ~ HandlerSite m, MonadHandler m)
  115. => Field m a
  116. -> FieldSettings site
  117. -> Maybe (Maybe a)
  118. -> MForm m (FormResult (Maybe a), FieldView site)
  119. mopt field fs mdef = mhelper field fs (join mdef) (const $ const $ FormSuccess Nothing) (FormSuccess . Just) False
  120. mhelper :: (site ~ HandlerSite m, MonadHandler m)
  121. => Field m a
  122. -> FieldSettings site
  123. -> Maybe a
  124. -> (site -> [Text] -> FormResult b) -- ^ on missing
  125. -> (a -> FormResult b) -- ^ on success
  126. -> Bool -- ^ is it required?
  127. -> MForm m (FormResult b, FieldView site)
  128. mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do
  129. tell fieldEnctype
  130. mp <- askParams
  131. name <- maybe newFormIdent return fsName
  132. theId <- lift $ maybe newIdent return fsId
  133. (_, site, langs) <- ask
  134. let mr2 = renderMessage site langs
  135. (res, val) <-
  136. case mp of
  137. Nothing -> return (FormMissing, maybe (Left "") Right mdef)
  138. Just p -> do
  139. mfs <- askFiles
  140. let mvals = fromMaybe [] $ Map.lookup name p
  141. files = fromMaybe [] $ mfs >>= Map.lookup name
  142. emx <- lift $ fieldParse mvals files
  143. return $ case emx of
  144. Left (SomeMessage e) -> (FormFailure [renderMessage site langs e], maybe (Left "") Left (listToMaybe mvals))
  145. Right mx ->
  146. case mx of
  147. Nothing -> (onMissing site langs, Left "")
  148. Just x -> (onFound x, Right x)
  149. return (res, FieldView
  150. { fvLabel = toHtml $ mr2 fsLabel
  151. , fvTooltip = fmap toHtml $ fmap mr2 fsTooltip
  152. , fvId = theId
  153. , fvInput = fieldView theId name fsAttrs val isReq
  154. , fvErrors =
  155. case res of
  156. FormFailure [e] -> Just $ toHtml e
  157. _ -> Nothing
  158. , fvRequired = isReq
  159. })
  160. -- | Applicative equivalent of 'mreq'.
  161. areq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
  162. => Field m a
  163. -> FieldSettings site
  164. -> Maybe a
  165. -> AForm m a
  166. areq a b = formToAForm . liftM (second return) . mreq a b
  167. -- | Applicative equivalent of 'mopt'.
  168. aopt :: MonadHandler m
  169. => Field m a
  170. -> FieldSettings (HandlerSite m)
  171. -> Maybe (Maybe a)
  172. -> AForm m (Maybe a)
  173. aopt a b = formToAForm . liftM (second return) . mopt a b
  174. runFormGeneric :: Monad m
  175. => MForm m a
  176. -> HandlerSite m
  177. -> [Text]
  178. -> Maybe (Env, FileEnv)
  179. -> m (a, Enctype)
  180. runFormGeneric form site langs env = evalRWST form (env, site, langs) (IntSingle 0)
  181. -- | This function is used to both initially render a form and to later extract
  182. -- results from it. Note that, due to CSRF protection and a few other issues,
  183. -- forms submitted via GET and POST are slightly different. As such, be sure to
  184. -- call the relevant function based on how the form will be submitted, /not/
  185. -- the current request method.
  186. --
  187. -- For example, a common case is displaying a form on a GET request and having
  188. -- the form submit to a POST page. In such a case, both the GET and POST
  189. -- handlers should use 'runFormPost'.
  190. runFormPost :: (RenderMessage (HandlerSite m) FormMessage, MonadResource m, MonadHandler m)
  191. => (Html -> MForm m (FormResult a, xml))
  192. -> m ((FormResult a, xml), Enctype)
  193. runFormPost form = do
  194. env <- postEnv
  195. postHelper form env
  196. postHelper :: (MonadHandler m, RenderMessage (HandlerSite m) FormMessage)
  197. => (Html -> MForm m (FormResult a, xml))
  198. -> Maybe (Env, FileEnv)
  199. -> m ((FormResult a, xml), Enctype)
  200. postHelper form env = do
  201. req <- getRequest
  202. let tokenKey = defaultCsrfParamName
  203. let token =
  204. case reqToken req of
  205. Nothing -> mempty
  206. Just n -> [shamlet|<input type=hidden name=#{tokenKey} value=#{n}>|]
  207. m <- getYesod
  208. langs <- languages
  209. ((res, xml), enctype) <- runFormGeneric (form token) m langs env
  210. let res' =
  211. case (res, env) of
  212. (_, Nothing) -> FormMissing
  213. (FormSuccess{}, Just (params, _))
  214. | not (Map.lookup tokenKey params === reqToken req) ->
  215. FormFailure [renderMessage m langs MsgCsrfWarning]
  216. _ -> res
  217. -- It's important to use constant-time comparison (constEqBytes) in order to avoid timing attacks.
  218. where (Just [t1]) === (Just t2) = TE.encodeUtf8 t1 `constEqBytes` TE.encodeUtf8 t2
  219. Nothing === Nothing = True
  220. _ === _ = False
  221. return ((res', xml), enctype)
  222. -- | Similar to 'runFormPost', except it always ignores the currently available
  223. -- environment. This is necessary in cases like a wizard UI, where a single
  224. -- page will both receive and incoming form and produce a new, blank form. For
  225. -- general usage, you can stick with @runFormPost@.
  226. generateFormPost
  227. :: (RenderMessage (HandlerSite m) FormMessage, MonadHandler m)
  228. => (Html -> MForm m (FormResult a, xml))
  229. -> m (xml, Enctype)
  230. generateFormPost form = first snd `liftM` postHelper form Nothing
  231. postEnv :: (MonadHandler m, MonadResource m)
  232. => m (Maybe (Env, FileEnv))
  233. postEnv = do
  234. req <- getRequest
  235. if requestMethod (reqWaiRequest req) == "GET"
  236. then return Nothing
  237. else do
  238. (p, f) <- runRequestBody
  239. let p' = Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) p
  240. return $ Just (p', Map.unionsWith (++) $ map (\(k, v) -> Map.singleton k [v]) f)
  241. runFormPostNoToken :: MonadHandler m
  242. => (Html -> MForm m a)
  243. -> m (a, Enctype)
  244. runFormPostNoToken form = do
  245. langs <- languages
  246. m <- getYesod
  247. env <- postEnv
  248. runFormGeneric (form mempty) m langs env
  249. runFormGet :: MonadHandler m
  250. => (Html -> MForm m a)
  251. -> m (a, Enctype)
  252. runFormGet form = do
  253. gets <- liftM reqGetParams getRequest
  254. let env =
  255. case lookup getKey gets of
  256. Nothing -> Nothing
  257. Just _ -> Just (Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) gets, Map.empty)
  258. getHelper form env
  259. {- FIXME: generateFormGet' "Will be renamed to generateFormGet in next version of Yesod" -}
  260. -- |
  261. --
  262. -- Since 1.3.11
  263. generateFormGet'
  264. :: (RenderMessage (HandlerSite m) FormMessage, MonadHandler m)
  265. => (Html -> MForm m (FormResult a, xml))
  266. -> m (xml, Enctype)
  267. generateFormGet' form = first snd `liftM` getHelper form Nothing
  268. {-# DEPRECATED generateFormGet "Will require RenderMessage in next version of Yesod" #-}
  269. generateFormGet :: MonadHandler m
  270. => (Html -> MForm m a)
  271. -> m (a, Enctype)
  272. generateFormGet form = getHelper form Nothing
  273. getKey :: Text
  274. getKey = "_hasdata"
  275. getHelper :: MonadHandler m
  276. => (Html -> MForm m a)
  277. -> Maybe (Env, FileEnv)
  278. -> m (a, Enctype)
  279. getHelper form env = do
  280. let fragment = [shamlet|<input type=hidden name=#{getKey}>|]
  281. langs <- languages
  282. m <- getYesod
  283. runFormGeneric (form fragment) m langs env
  284. -- | Creates a hidden field on the form that identifies it. This
  285. -- identification is then used to distinguish between /missing/
  286. -- and /wrong/ form data when a single handler contains more than
  287. -- one form.
  288. --
  289. -- For instance, if you have the following code on your handler:
  290. --
  291. -- > ((fooRes, fooWidget), fooEnctype) <- runFormPost fooForm
  292. -- > ((barRes, barWidget), barEnctype) <- runFormPost barForm
  293. --
  294. -- Then replace it with
  295. --
  296. -- > ((fooRes, fooWidget), fooEnctype) <- runFormPost $ identifyForm "foo" fooForm
  297. -- > ((barRes, barWidget), barEnctype) <- runFormPost $ identifyForm "bar" barForm
  298. --
  299. -- Note that it's your responsibility to ensure that the
  300. -- identification strings are unique (using the same one twice on a
  301. -- single handler will not generate any errors). This allows you
  302. -- to create a variable number of forms and still have them work
  303. -- even if their number or order change between the HTML
  304. -- generation and the form submission.
  305. identifyForm
  306. :: Monad m
  307. => Text -- ^ Form identification string.
  308. -> (Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))
  309. -> (Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))
  310. identifyForm identVal form = \fragment -> do
  311. -- Create hidden <input>.
  312. let fragment' =
  313. [shamlet|
  314. <input type=hidden name=#{identifyFormKey} value=#{identVal}>
  315. #{fragment}
  316. |]
  317. -- Check if we got its value back.
  318. mp <- askParams
  319. let missing = (mp >>= Map.lookup identifyFormKey) /= Just [identVal]
  320. -- Run the form proper (with our hidden <input>). If the
  321. -- data is missing, then do not provide any params to the
  322. -- form, which will turn its result into FormMissing. Also,
  323. -- doing this avoids having lots of fields with red errors.
  324. let eraseParams | missing = local (\(_, h, l) -> (Nothing, h, l))
  325. | otherwise = id
  326. eraseParams (form fragment')
  327. identifyFormKey :: Text
  328. identifyFormKey = "_formid"
  329. type FormRender m a =
  330. AForm m a
  331. -> Html
  332. -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ())
  333. renderTable, renderDivs, renderDivsNoLabels :: Monad m => FormRender m a
  334. -- | Render a form into a series of tr tags. Note that, in order to allow
  335. -- you to add extra rows to the table, this function does /not/ wrap up
  336. -- the resulting HTML in a table tag; you must do that yourself.
  337. renderTable aform fragment = do
  338. (res, views') <- aFormToForm aform
  339. let views = views' []
  340. let widget = [whamlet|
  341. $newline never
  342. $if null views
  343. \#{fragment}
  344. $forall (isFirst, view) <- addIsFirst views
  345. <tr :fvRequired view:.required :not $ fvRequired view:.optional>
  346. <td>
  347. $if isFirst
  348. \#{fragment}
  349. <label for=#{fvId view}>#{fvLabel view}
  350. $maybe tt <- fvTooltip view
  351. <div .tooltip>#{tt}
  352. <td>^{fvInput view}
  353. $maybe err <- fvErrors view
  354. <td .errors>#{err}
  355. |]
  356. return (res, widget)
  357. where
  358. addIsFirst [] = []
  359. addIsFirst (x:y) = (True, x) : map (False, ) y
  360. -- | render a field inside a div
  361. renderDivs = renderDivsMaybeLabels True
  362. -- | render a field inside a div, not displaying any label
  363. renderDivsNoLabels = renderDivsMaybeLabels False
  364. renderDivsMaybeLabels :: Monad m => Bool -> FormRender m a
  365. renderDivsMaybeLabels withLabels aform fragment = do
  366. (res, views') <- aFormToForm aform
  367. let views = views' []
  368. let widget = [whamlet|
  369. $newline never
  370. \#{fragment}
  371. $forall view <- views
  372. <div :fvRequired view:.required :not $ fvRequired view:.optional>
  373. $if withLabels
  374. <label for=#{fvId view}>#{fvLabel view}
  375. $maybe tt <- fvTooltip view
  376. <div .tooltip>#{tt}
  377. ^{fvInput view}
  378. $maybe err <- fvErrors view
  379. <div .errors>#{err}
  380. |]
  381. return (res, widget)
  382. -- | Render a form using Bootstrap v2-friendly shamlet syntax.
  383. -- If you're using Bootstrap v3, then you should use the
  384. -- functions from module "Yesod.Form.Bootstrap3".
  385. --
  386. -- Sample Hamlet:
  387. --
  388. -- > <form .form-horizontal method=post action=@{ActionR} enctype=#{formEnctype}>
  389. -- > <fieldset>
  390. -- > <legend>_{MsgLegend}
  391. -- > $case result
  392. -- > $of FormFailure reasons
  393. -- > $forall reason <- reasons
  394. -- > <div .alert .alert-error>#{reason}
  395. -- > $of _
  396. -- > ^{formWidget}
  397. -- > <div .form-actions>
  398. -- > <input .btn .primary type=submit value=_{MsgSubmit}>
  399. --
  400. -- Since 1.3.14
  401. renderBootstrap2 :: Monad m => FormRender m a
  402. renderBootstrap2 aform fragment = do
  403. (res, views') <- aFormToForm aform
  404. let views = views' []
  405. has (Just _) = True
  406. has Nothing = False
  407. let widget = [whamlet|
  408. $newline never
  409. \#{fragment}
  410. $forall view <- views
  411. <div .control-group .clearfix :fvRequired view:.required :not $ fvRequired view:.optional :has $ fvErrors view:.error>
  412. <label .control-label for=#{fvId view}>#{fvLabel view}
  413. <div .controls .input>
  414. ^{fvInput view}
  415. $maybe tt <- fvTooltip view
  416. <span .help-block>#{tt}
  417. $maybe err <- fvErrors view
  418. <span .help-block>#{err}
  419. |]
  420. return (res, widget)
  421. -- | Deprecated synonym for 'renderBootstrap2'.
  422. renderBootstrap :: Monad m => FormRender m a
  423. renderBootstrap = renderBootstrap2
  424. {-# DEPRECATED renderBootstrap "Please use the Yesod.Form.Bootstrap3 module." #-}
  425. check :: (Monad m, RenderMessage (HandlerSite m) msg)
  426. => (a -> Either msg a)
  427. -> Field m a
  428. -> Field m a
  429. check f = checkM $ return . f
  430. -- | Return the given error message if the predicate is false.
  431. checkBool :: (Monad m, RenderMessage (HandlerSite m) msg)
  432. => (a -> Bool) -> msg -> Field m a -> Field m a
  433. checkBool b s = check $ \x -> if b x then Right x else Left s
  434. checkM :: (Monad m, RenderMessage (HandlerSite m) msg)
  435. => (a -> m (Either msg a))
  436. -> Field m a
  437. -> Field m a
  438. checkM f = checkMMap f id
  439. -- | Same as 'checkM', but modifies the datatype.
  440. --
  441. -- In order to make this work, you must provide a function to convert back from
  442. -- the new datatype to the old one (the second argument to this function).
  443. --
  444. -- Since 1.1.2
  445. checkMMap :: (Monad m, RenderMessage (HandlerSite m) msg)
  446. => (a -> m (Either msg b))
  447. -> (b -> a)
  448. -> Field m a
  449. -> Field m b
  450. checkMMap f inv field = field
  451. { fieldParse = \ts fs -> do
  452. e1 <- fieldParse field ts fs
  453. case e1 of
  454. Left msg -> return $ Left msg
  455. Right Nothing -> return $ Right Nothing
  456. Right (Just a) -> liftM (either (Left . SomeMessage) (Right . Just)) $ f a
  457. , fieldView = \i n a eres req -> fieldView field i n a (fmap inv eres) req
  458. }
  459. -- | Allows you to overwrite the error message on parse error.
  460. customErrorMessage :: Monad m => SomeMessage (HandlerSite m) -> Field m a -> Field m a
  461. customErrorMessage msg field = field
  462. { fieldParse = \ts fs ->
  463. liftM (either (const $ Left msg) Right)
  464. $ fieldParse field ts fs
  465. }
  466. -- | Generate a 'FieldSettings' from the given label.
  467. fieldSettingsLabel :: RenderMessage site msg => msg -> FieldSettings site
  468. fieldSettingsLabel msg = FieldSettings (SomeMessage msg) Nothing Nothing Nothing []
  469. -- | A helper function for creating custom fields.
  470. --
  471. -- This is intended to help with the common case where a single input value is
  472. -- required, such as when parsing a text field.
  473. --
  474. -- Since 1.1
  475. parseHelper :: (Monad m, RenderMessage site FormMessage)
  476. => (Text -> Either FormMessage a)
  477. -> [Text] -> [FileInfo] -> m (Either (SomeMessage site) (Maybe a))
  478. parseHelper = parseHelperGen
  479. -- | A generalized version of 'parseHelper', allowing any type for the message
  480. -- indicating a bad parse.
  481. --
  482. -- Since 1.3.6
  483. parseHelperGen :: (Monad m, RenderMessage site msg)
  484. => (Text -> Either msg a)
  485. -> [Text] -> [FileInfo] -> m (Either (SomeMessage site) (Maybe a))
  486. parseHelperGen _ [] _ = return $ Right Nothing
  487. parseHelperGen _ ("":_) _ = return $ Right Nothing
  488. parseHelperGen f (x:_) _ = return $ either (Left . SomeMessage) (Right . Just) $ f x
  489. -- | Since a 'Field' cannot be a 'Functor', it is not obvious how to "reuse" a Field
  490. -- on a @newtype@ or otherwise equivalent type. This function allows you to convert
  491. -- a @Field m a@ to a @Field m b@ assuming you provide a bidireccional
  492. -- convertion among the two, through the first two functions.
  493. --
  494. -- A simple example:
  495. --
  496. -- > import Data.Monoid
  497. -- > sumField :: (Functor m, Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m (Sum Int)
  498. -- > sumField = convertField Sum getSum intField
  499. --
  500. -- Another example, not using a newtype, but instead creating a Lazy Text field:
  501. --
  502. -- > import qualified Data.Text.Lazy as TL
  503. -- > TextField :: (Functor m, Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m TL.Text
  504. -- > lazyTextField = convertField TL.fromStrict TL.toStrict textField
  505. --
  506. -- Since 1.3.16
  507. convertField :: (Functor m)
  508. => (a -> b) -> (b -> a)
  509. -> Field m a -> Field m b
  510. convertField to from (Field fParse fView fEnctype) = let
  511. fParse' ts = fmap (fmap (fmap to)) . fParse ts
  512. fView' ti tn at ei = fView ti tn at (fmap from ei)
  513. in Field fParse' fView' fEnctype