-- Hoogle documentation, generated by Haddock
-- See Hoogle, http://www.haskell.org/hoogle/
-- | Process forms sent to you in JSON format like a man
--
-- Process forms sent to you in JSON format like a man.
@package forma
@version 0.1.0
-- | This module provides a tool for validation of forms that are
-- represented in the JSON format. Sending forms in JSON format via an
-- AJAX request instead of traditional submitting of forms has a number
-- of advantages:
--
--
-- - Smoother user experience: no need to reload the whole page.
-- - Form rendering is separated and lives only in GET handler, POST
-- (or whatever method you deem appropriate for your use case) handler
-- only handles validation and actual effects that form submission should
-- initiate.
-- - You get a chance to organize form input just like you want.
--
--
-- The task of validation of a form in the JSON format may seem simple,
-- but it's not trivial to get it right. The library allows you to:
--
--
-- - Define form parser using type-safe applicative notation with field
-- labels being stored on the type label which excludes any possibility
-- of typos and will force all your field labels be always up to
-- date.
-- - Parse JSON Value according to the definition of form you
-- created.
-- - Stop parsing immediately if given form is malformed and cannot be
-- processed.
-- - Validate forms using any number of composable checkers that
-- you write for your specific problem domain. Once you have a vocabulary
-- of checkers, creation of new forms is just a matter of combining them,
-- and yes they do combine nicely.
-- - Collect validation errors from multiple branches of parsing (one
-- branch per form field) in parallel, so validation errors in one branch
-- do not prevent us from collecting validation errors from other
-- branches. This allows for a better user experience as the user can see
-- all validation errors at the same time.
-- - Use optional and (<|>) from
-- Control.Applicative in your form definitions instead of ugly
-- ad-hoc stuff (yes digestive-functors, I'm looking at
-- you).
-- - When individual validation of fields is done, you get a chance to
-- perform some actions and either decide that form submission has
-- succeeded, or indeed perform additional checks that may depend on
-- several form fields at once and signal a validation error assigned to
-- a specific field(s). This constitute the “second level” of validation,
-- so to speak.
--
--
-- This library requires at least GHC 8 to work.
--
-- You need to enable at least DataKinds and
-- TypeApplications language extensions to use this library.
module Web.Forma
-- | Construct a parser for a field. Combine multiple fields using
-- applicative syntax like so:
--
--
-- type LoginFields = '["username", "password", "remember_me"]
--
-- data LoginForm = LoginForm
-- { loginUsername :: Text
-- , loginPassword :: Text
-- , loginRememberMe :: Bool
-- }
--
-- loginForm :: Monad m => FormParser LoginFields m LoginForm
-- loginForm = LoginForm
-- <$> field @"username" notEmpty
-- <*> field @"password" notEmpty
-- <*> field' @"remember_me"
--
-- notEmpty :: Monad m => Text -> ExceptT Text m Text
-- notEmpty txt =
-- if T.null txt
-- then throwError "This field cannot be empty"
-- else return txt
--
--
-- Referring to the types in the function's signature, s is
-- extracted from JSON Value for you automatically using its
-- FromJSON instance. The field value is taken in assumption that
-- top level Value is a dictionary, and field name is a key in
-- that dictionary. So for example a valid JSON input for the form shown
-- above could be this:
--
--
-- {
-- "username": "Bob",
-- "password": "123",
-- "remember_me": true
-- }
--
--
-- Once value of type s is extracted, validation phase beings.
-- The supplied checker (you can easy compose them with
-- (>=>), as they are Kleisli arrows) is applied to
-- the s value and validation either succeeds producing an
-- a value, or we collect an error in the form of a value of
-- e type, which is fed into mkFieldError internally.
--
-- To run a form composed from fields, see runForm.
field :: forall (name :: Symbol) (names :: [Symbol]) m e s a. (KnownSymbol name, InSet name names, Monad m, ToJSON e, FromJSON s) => (s -> ExceptT e m a) -> FormParser names m a
-- | The same as field, but does not require a checker.
field' :: forall (name :: Symbol) (names :: [Symbol]) m a. (KnownSymbol name, InSet name names, Monad m, FromJSON a) => FormParser names m a
-- | Run the supplied parser on given input and call the specified callback
-- that uses the result of parsing on success.
--
-- The callback can either report a FieldError (one or more), or
-- report success providing a value that will be converted to JSON and
-- including in the resulting Value (response).
--
-- The resulting Value has the following format:
--
--
-- {
-- "parse_error": "Text or null."
-- "field_errors":
-- {
-- "foo": "Foo's error serialized to JSON.",
-- "bar": "Bar's error…"
-- }
-- "result": "What you return from the callback in FormResultSuccess."
-- }
--
runForm :: (Monad m, ToJSON b) => FormParser names m a -> Value -> (a -> m (FormResult names b)) -> m Value
-- | Pick a name from a given collection of names.
--
-- Typical usage:
--
--
-- type Fields = '["foo", "bar", "baz"]
--
-- myName :: SelectedName Fields
-- myName = pick @"foo" @Fields
--
--
-- It's a good idea to use pick to get field names not only where
-- this approach is imposed by the library, but everywhere you need to
-- use the field names, in your templates for example.
pick :: forall (name :: Symbol) (names :: [Symbol]). (KnownSymbol name, InSet name names) => SelectedName names
-- | Extract a Text value from SelectedName.
unSelectedName :: SelectedName names -> Text
-- | This is a smart constructor for the FieldError type, and the
-- only way to obtain values of that type.
--
-- Typical usage:
--
--
-- type Fields = '["foo", "bar", "baz"]
--
-- myError :: FieldError Fields
-- myError = mkFieldError (pick @"foo" @Fields) "That's all wrong."
--
--
-- See also: pick (to create SelectedName).
mkFieldError :: ToJSON e => SelectedName names -> e -> FieldError names
-- | The type represents the parser that you can run on a Value with
-- the help of runForm. The only way for the user of the library
-- to create a parser is via the field function. Users can combine
-- existing parsers using the applicative notation.
--
-- FormParser is parametrized by three type variables:
--
--
-- - names — collection of field names we can use in a form to
-- be parsed with this parser.
-- - m — underlying monad, FormParser is not a monad
-- itself, so it's not a monad transformer, but validation can make use
-- of the m monad.
-- - a — result of parsing.
--
--
-- FormParser is not a monad because it's not possible to write a
-- Monad instance with the properties that we want (validation
-- errors should not lead to short-cutting behavior).
data FormParser (names :: [Symbol]) m a
-- | This a type that user must return in the callback passed to the
-- runForm function. Quite simply, it allows you either report a
-- error or finish successfully.
data FormResult (names :: [Symbol]) a
-- | Form submission failed, here are the validation errors.
FormResultError :: (FieldError names) -> FormResult a
-- | Form submission succeeded, send this info.
FormResultSuccess :: a -> FormResult a
-- | SelectedName names represents a name (Text
-- value) that is guaranteed to be in the names, which is a set
-- of strings on type level. The purpose if this type is to avoid typos
-- and to force users to update field names everywhere when they decide
-- to change them. The only way to obtain a value of type
-- SelectedName is via the pick function, which see.
data SelectedName (names :: [Symbol])
-- | The type function computes a Constraint which is satisfied when
-- its first argument is contained in its second argument. Otherwise a
-- friendly type error is displayed.
-- | Error info in JSON format associated with a particular form field.
-- Parametrized by names, which is a collection of field names
-- (on type level) the target field belongs to. FieldError is an
-- instance of Semigroup and that's how you combine values of that
-- type. Note that it's not a Monoid, because we do not want to
-- allow empty FieldErrors.
data FieldError (names :: [Symbol])
instance GHC.Base.Functor (Web.Forma.BranchState names)
instance GHC.Show.Show a => GHC.Show.Show (Web.Forma.FormResult names a)
instance GHC.Classes.Eq a => GHC.Classes.Eq (Web.Forma.FormResult names a)
instance GHC.Show.Show (Web.Forma.FieldError names)
instance GHC.Classes.Eq (Web.Forma.FieldError names)
instance GHC.Show.Show (Web.Forma.SelectedName names)
instance GHC.Classes.Eq (Web.Forma.SelectedName names)
instance GHC.Base.Applicative (Web.Forma.BranchState names)
instance GHC.Base.Functor m => GHC.Base.Functor (Web.Forma.FormParser names m)
instance GHC.Base.Applicative m => GHC.Base.Applicative (Web.Forma.FormParser names m)
instance GHC.Base.Applicative m => GHC.Base.Alternative (Web.Forma.FormParser names m)
instance Data.Semigroup.Semigroup (Web.Forma.FieldError names)
instance Data.Aeson.Types.ToJSON.ToJSON (Web.Forma.FieldError names)
instance Data.Default.Class.Default (Web.Forma.Response names)
instance Data.Aeson.Types.ToJSON.ToJSON (Web.Forma.Response names)