servant
servant copied to clipboard
Monadic form validation with digestive functors.
Hi,
So, I'm trying to validate a form on my application with digestive functors but I can't find a good way to successfully do that.
I've read #236 but I can't find FromFormUrlEncoded class. Was it removed? If so, what is the alternative?
I have a Credentials data type that carries username and password. It should be validated to make sure password is strong enough and username is not empty, and it should also check if user with that username already exists.
I've made a simple wrapper type called 'Page' that contains some meta data about the page that is being rendered and the main content of the page.
data Meta = Meta
{ title :: Text
} deriving ( Show, Generic )
data Page content = Page
{ meta :: Meta
, content :: content
} deriving ( Show, Generic )
And I have a Credentials type with the accompanying form.
data Credentials = Credentials
{ username :: Text
, password :: Text
} deriving ( Show, Generic )
credentialsForm :: Monad m => Form Html m Credentials
credentialsForm = Credentials
<$> "username" .: checkIfUserExistsInDB
<*> "password" .: nonEmptyText
where
checkIfUserExistsInDB = undefined
nonEmptyText = check "Can't be empty!" (not . null) $ text Nothing
credentialsFormView :: View Html -> Html
credentialsFormView v = form v "/user/new" $ do
div_ $ do
label "username" v "Username"
inputText "username" v
div_ $ do
label "password" v "Password"
inputPassword "password" v
div_ $ do
inputSubmit "Register"
Idea is to have something like this:
...
:<|> "user" :> "new" :> Get '[HTML] Page (GetForm Credentials)
:<|> "user" :> "new" :> ReqBody '[FormUrlEncoded] Credentials :>
(Post '[HTML] (Page (BadForm User)) :<|> (Post '[HTML] (Page (GoodForm User))
...
where user first gets the empty form, and than it can do a post request to the server where input data is validated (within a custom monad if possible) and than result is either a GoodForm which than inserts a new user into the database and returns a success page, or a BadForm which renders HTML with form containing error warnings.
I haven't really seen many (or any) tutorials about form validation in servant which seems like a very important thing, so I'm a bit overwhelmed with this task.
As you can see in the haddocks for FormUrlEncoded (in the list of instances), we now require that haskell types implement the FromForm/ToForm classes, from the http-api-data package.
As you can see, FromForm only wants an Either out. So you can do all the validation you want as part of your instance as long as you can come back to an Either in the end. I'm not an expert in the fancy validation libraries but this sounds like something you should be able to do with those, no? The decoding necessarily has to be pure so you can't perform arbitrary monadic validation there, unless you can turn it all back into a good old Either. If you need some effects for the validation code or other fancy things, you're better off doing some dummy decoding in the FromForm instance and doing the fancy validation as part of your handler. You could so something like:
data Validation = Validated | NotValidated
data Person (v :: Validation) = Person String Int
-- we don't want to decode a "validated person", it has to go through
-- the validation process
instance FromForm (Person 'NotValidated) where
...
-- validate a person, erroring out if validation fails.
-- this assumes the monad has some notion of error. if not, return
-- Either ValidationError (Person 'Validated).
validatePerson :: Person 'NotValidated -> SomeMonad (Person 'Validated)
validatePerson (Person name age)
| age >= 0 = pure (Person name age)
| otherwise = ... -- error out
Note that https://github.com/search?o=desc&q=%22FormUrlEncoded%22+servant&s=indexed&type=Code shows quite a few examples of writing code that uses the FormUrlEncoded content type but I agree that ideally we would have a little cookbook recipe dedicated to this topic. If that's something you're interested in writing once this is all done and figured out, we would be delighted to take a PR :)
Hopefully some things I said will help but let us know if that's not enough.
@alpmestan thanks, I missed the FromForm, but the thing is it's not very useful for my usecase. I can't really check the database since its result is Either and also, Either has to be of type Either Text a.
When I evaluate my form I get Monad m => m (View v, Maybe a) so ideally I'd be able to get Either (View v) a from FromForm where v is a structured tree of fields and corresponding errors that can be rendered as an html form and display errors to the user or I can process the input further. This would mean that my handler signature should look something like this : Either (View v) a -> Handler (Page (Credentials)).
Is it by any chance possible to "chain" handlers? That way I'd be able to Post raw request data into one handler, process it in my custom monad and than forward that result to the final handler in the chain.
Can you maybe point me in the right direction with this? I'd be glad to write a comprehensive tutorial / cookbook once I figure a nice way to do this.
@alpmestan on second thought, I guess I can just write a combinator that gives me a raw form data which I can then pass to my handler and process. Handler signature will be something like RawForm -> Page (Either (Bad Credentials) (Good Credentials)).
Various observations:
- Monadic, did you mean "with IO"? I assume, yes. (because of it should also check if user with that username already exists. and check the database)
- We are in general against doing IO when parsing (headers, form data, query params, ...) as then GET requests could easily violate HTTP assumptions (they shouldn't change the internal state of the server)
- Even IO would be allowed, passing data to it (database connection, e.g.) is not simple.
- So the way you found is indeed the recommended one, you could do "pure" (form is non empty, maybe password strength) while pre-validating form, but stateful checks (non-duplicate passwords, uniqueness of username) have to be done in the handler.
Note: that forall m. Monad m => m a is the same as a, as you can pick as witnessed by Identity and return.
@reygoch do you have any complete example for how you integrated servant with digestiv-functors? I can't figure this out, and I can't find any servant web form code examples.