haskell-servant / servant

Servat is a Haskell DSL for describing, serving, querying, mocking, documenting web applications and more!
https://docs.servant.dev/
1.82k stars 412 forks source link

Monadic form validation with digestive functors. #965

Open reygoch opened 6 years ago

reygoch commented 6 years ago

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.

alpmestan commented 6 years ago

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.

reygoch commented 6 years ago

@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.

reygoch commented 6 years ago

@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)).

phadej commented 6 years ago

Various observations:

Note: that forall m. Monad m => m a is the same as a, as you can pick as witnessed by Identity and return.

tonyalaribe commented 2 years ago

@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.