Web.FormUrlEncoded

http-api-data-0.3.5: Converting to/from HTTP API data like URL pieces, headers and query parameters.

Safe Haskell | None
Language | Haskell2010

Web.FormUrlEncoded

Contents

- Classes
  - Keys for `Form` entries
- `Form` type
- Encoding and decoding ``Form``s
- `Generic`s
  - Encoding options
- Helpers

Description

Convert Haskell values to and from `application/xxx-form-urlencoded` format.

Synopsis

- class ToForm a where
- class FromForm a where
- class ToFormKey k where
- class FromFormKey k where
- newtype Form = Form {
  - unForm :: HashMap Text [Text]
  }
- urlEncodeAsForm :: ToForm a => a -> ByteString
- urlDecodeAsForm :: FromForm a => ByteString -> Either Text a
- urlEncodeForm :: Form -> ByteString
- urlDecodeForm :: ByteString -> Either Text Form
- genericToForm :: forall a. (Generic a, GToForm a (Rep a)) => FormOptions -> a -> Form
- genericFromForm :: forall a. (Generic a, GFromForm a (Rep a)) => FormOptions -> Form -> Either Text a
- data FormOptions = FormOptions {
  - fieldLabelModifier :: String -> String
  }
- defaultFormOptions :: FormOptions
- toEntriesByKey :: (FromFormKey k, FromHttpApiData v) => Form -> Either Text [(k, [v])]
- fromEntriesByKey :: (ToFormKey k, ToHttpApiData v) => [(k, [v])] -> Form
- lookupAll :: Text -> Form -> [Text]
- lookupMaybe :: Text -> Form -> Either Text (Maybe Text)
- lookupUnique :: Text -> Form -> Either Text Text
- parseAll :: FromHttpApiData v => Text -> Form -> Either Text [v]
- parseMaybe :: FromHttpApiData v => Text -> Form -> Either Text (Maybe v)
- parseUnique :: FromHttpApiData v => Text -> Form -> Either Text v

# Classes

class ToForm a where #

Convert a value into `Form`.

An example type and instance:

    {-# LANGUAGE OverloadedLists #-}

    data Person = Person
      { name :: String
      , age  :: Int }

    instance ToForm Person where
      toForm person =
        [ ("name", toQueryParam (name person))
        , ("age", toQueryParam (age person)) ]

Instead of manually writing ``ToForm`` instances you can use a default generic implementation of ``toForm``.

To do that, simply add `deriving `Generic`` clause to your datatype and declare a `ToForm` instance for your datatype without giving definition for `toForm`.

For instance, the previous example can be simplified into this:

    data Person = Person
      { name :: String
      , age  :: Int
      } deriving (Generic)

    instance ToForm Person

The default implementation of `toForm` is `genericToForm`.

Methods

toForm :: a -> Form #

Convert a value into `Form`.

toForm :: (Generic a, GToForm a (Rep a)) => a -> Form #

Convert a value into `Form`.

Instances

ToForm Form # |
Methods toForm :: Form -> Form #
(ToFormKey k, ToHttpApiData v) => ToForm [(k, v)] # |
Methods toForm :: [(k, v)] -> Form #
ToHttpApiData v => ToForm (IntMap [v]) # |
Methods toForm :: IntMap [v] -> Form #
(ToFormKey k, ToHttpApiData v) => ToForm (Map k [v]) # |
Methods toForm :: Map k [v] -> Form #
(ToFormKey k, ToHttpApiData v) => ToForm (HashMap k [v]) # |
Methods toForm :: HashMap k [v] -> Form #

class FromForm a where #

Parse `Form` into a value.

An example type and instance:

    data Person = Person
      { name :: String
      , age  :: Int }

    instance FromForm Person where
      fromForm f = Person
        <$> parseUnique "name" f
        <*> parseUnique "age"  f

Instead of manually writing ``FromForm`` instances you can use a default generic implementation of ``fromForm``.

To do that, simply add `deriving `Generic`` clause to your datatype and declare a `FromForm` instance for your datatype without giving definition for `fromForm`.

For instance, the previous example can be simplified into this:

    data Person = Person
      { name :: String
      , age  :: Int
      } deriving (Generic)

    instance FromForm Person

The default implementation of `fromForm` is `genericFromForm`. It only works for records and it will use `parseQueryParam` for each field's value.

Methods

fromForm :: Form -> Either Text a #

Parse `Form` into a value.

fromForm :: (Generic a, GFromForm a (Rep a)) => Form -> Either Text a #

Parse `Form` into a value.

Instances

FromForm Form # |
Methods fromForm :: Form -> Either Text Form #
(FromFormKey k, FromHttpApiData v) => FromForm [(k, v)] # |
Methods fromForm :: Form -> Either Text [(k, v)] #
FromHttpApiData v => FromForm (IntMap [v]) # |
Methods fromForm :: Form -> Either Text (IntMap [v]) #
(Ord k, FromFormKey k, FromHttpApiData v) => FromForm (Map k [v]) # |
Methods fromForm :: Form -> Either Text (Map k [v]) #
(Eq k, Hashable k, FromFormKey k, FromHttpApiData v) => FromForm (HashMap k [v]) # |
Methods fromForm :: Form -> Either Text (HashMap k [v]) #

## Keys for `Form` entries

class ToFormKey k where #

Typeclass for types that can be used as keys in a `Form`-like container (like `Map`).

Minimal complete definition

toFormKey

Methods

toFormKey :: k -> Text #

Render a key for a `Form`.

Instances

ToFormKey Bool # |
Methods toFormKey :: Bool -> Text #
ToFormKey Char # |
Methods toFormKey :: Char -> Text #
ToFormKey Double # |
Methods toFormKey :: Double -> Text #
ToFormKey Float # |
Methods toFormKey :: Float -> Text #
ToFormKey Int # |
Methods toFormKey :: Int -> Text #
ToFormKey Int8 # |
Methods toFormKey :: Int8 -> Text #
ToFormKey Int16 # |
Methods toFormKey :: Int16 -> Text #
ToFormKey Int32 # |
Methods toFormKey :: Int32 -> Text #
ToFormKey Int64 # |
Methods toFormKey :: Int64 -> Text #
ToFormKey Integer # |
Methods toFormKey :: Integer -> Text #
ToFormKey Ordering # |
Methods toFormKey :: Ordering -> Text #
ToFormKey Word # |
Methods toFormKey :: Word -> Text #
ToFormKey Word8 # |
Methods toFormKey :: Word8 -> Text #
ToFormKey Word16 # |
Methods toFormKey :: Word16 -> Text #
ToFormKey Word32 # |
Methods toFormKey :: Word32 -> Text #
ToFormKey Word64 # |
Methods toFormKey :: Word64 -> Text #
ToFormKey () # |
Methods toFormKey :: () -> Text #
ToFormKey Text # |
Methods toFormKey :: Text -> Text #
ToFormKey Text # |
Methods toFormKey :: Text -> Text #
ToFormKey String # |
Methods toFormKey :: String -> Text #
ToFormKey Natural # |
Methods toFormKey :: Natural -> Text #
ToFormKey Void # |
Methods toFormKey :: Void -> Text #
ToFormKey All # |
Methods toFormKey :: All -> Text #
ToFormKey Any # |
Methods toFormKey :: Any -> Text #
ToFormKey LocalTime # |
Methods toFormKey :: LocalTime -> Text #
ToFormKey ZonedTime # |
Methods toFormKey :: ZonedTime -> Text #
ToFormKey UTCTime # |
Methods toFormKey :: UTCTime -> Text #
ToFormKey NominalDiffTime # |
Methods toFormKey :: NominalDiffTime -> Text #
ToFormKey Day # |
Methods toFormKey :: Day -> Text #
ToFormKey a => ToFormKey (Dual a) # |
Methods toFormKey :: Dual a -> Text #
ToFormKey a => ToFormKey (Sum a) # |
Methods toFormKey :: Sum a -> Text #
ToFormKey a => ToFormKey (Product a) # |
Methods toFormKey :: Product a -> Text #

class FromFormKey k where #

Typeclass for types that can be parsed from keys of a `Form`. This is the reverse of `ToFormKey`.

Minimal complete definition

parseFormKey

Methods

parseFormKey :: Text -> Either Text k #

Parse a key of a `Form`.

Instances

FromFormKey Bool # |
Methods parseFormKey :: Text -> Either Text Bool #
FromFormKey Char # |
Methods parseFormKey :: Text -> Either Text Char #
FromFormKey Double # |
Methods parseFormKey :: Text -> Either Text Double #
FromFormKey Float # |
Methods parseFormKey :: Text -> Either Text Float #
FromFormKey Int # |
Methods parseFormKey :: Text -> Either Text Int #
FromFormKey Int8 # |
Methods parseFormKey :: Text -> Either Text Int8 #
FromFormKey Int16 # |
Methods parseFormKey :: Text -> Either Text Int16 #
FromFormKey Int32 # |
Methods parseFormKey :: Text -> Either Text Int32 #
FromFormKey Int64 # |
Methods parseFormKey :: Text -> Either Text Int64 #
FromFormKey Integer # |
Methods parseFormKey :: Text -> Either Text Integer #
FromFormKey Ordering # |
Methods parseFormKey :: Text -> Either Text Ordering #
FromFormKey Word # |
Methods parseFormKey :: Text -> Either Text Word #
FromFormKey Word8 # |
Methods parseFormKey :: Text -> Either Text Word8 #
FromFormKey Word16 # |
Methods parseFormKey :: Text -> Either Text Word16 #
FromFormKey Word32 # |
Methods parseFormKey :: Text -> Either Text Word32 #
FromFormKey Word64 # |
Methods parseFormKey :: Text -> Either Text Word64 #
FromFormKey () # |
Methods parseFormKey :: Text -> Either Text () #
FromFormKey Text # |
Methods parseFormKey :: Text -> Either Text Text #
FromFormKey Text # |
Methods parseFormKey :: Text -> Either Text Text #
FromFormKey String # |
Methods parseFormKey :: Text -> Either Text String #
FromFormKey Natural # |
Methods parseFormKey :: Text -> Either Text Natural #
FromFormKey Void # |
Methods parseFormKey :: Text -> Either Text Void #
FromFormKey All # |
Methods parseFormKey :: Text -> Either Text All #
FromFormKey Any # |
Methods parseFormKey :: Text -> Either Text Any #
FromFormKey LocalTime # |
Methods parseFormKey :: Text -> Either Text LocalTime #
FromFormKey ZonedTime # |
Methods parseFormKey :: Text -> Either Text ZonedTime #
FromFormKey UTCTime # |
Methods parseFormKey :: Text -> Either Text UTCTime #
FromFormKey NominalDiffTime # |
Methods parseFormKey :: Text -> Either Text NominalDiffTime #
FromFormKey Day # |
Methods parseFormKey :: Text -> Either Text Day #
FromFormKey a => FromFormKey (Dual a) # |
Methods parseFormKey :: Text -> Either Text (Dual a) #
FromFormKey a => FromFormKey (Sum a) # |
Methods parseFormKey :: Text -> Either Text (Sum a) #
FromFormKey a => FromFormKey (Product a) # |
Methods parseFormKey :: Text -> Either Text (Product a) #

# `Form` type

newtype Form #

The contents of a form, not yet URL-encoded.

`Form` can be URL-encoded with `urlEncodeForm` and URL-decoded with `urlDecodeForm`.

Constructors

Form |
Fields unForm :: HashMap Text [Text]

Instances

IsList Form # |
Associated Types type Item Form :: * # Methods fromList :: [Item Form] -> Form # fromListN :: Int -> [Item Form] -> Form # toList :: Form -> [Item Form] #
Eq Form # |
Methods (==) :: Form -> Form -> Bool # (/=) :: Form -> Form -> Bool #
Read Form # |
Methods readsPrec :: Int -> ReadS Form # readList :: ReadS [Form] # readPrec :: ReadPrec Form # readListPrec :: ReadPrec [Form] #
Show Form # |
Methods showsPrec :: Int -> Form -> ShowS # show :: Form -> String # showList :: [Form] -> ShowS #
Generic Form # |
Associated Types type Rep Form :: * -> * # Methods from :: Form -> Rep Form x # to :: Rep Form x -> Form #
Monoid Form # |
Methods mempty :: Form # mappend :: Form -> Form -> Form # mconcat :: [Form] -> Form #
FromForm Form # |
Methods fromForm :: Form -> Either Text Form #
ToForm Form # |
Methods toForm :: Form -> Form #
type Rep Form # |
type Rep Form = D1 (MetaData "Form" "Web.Internal.FormUrlEncoded" "http-api-data-0.3.5-F5OI65ALGA2HMQIdkF75QL" True) (C1 (MetaCons "Form" PrefixI True) (S1 (MetaSel (Just Symbol "unForm") NoSourceUnpackedness NoSourceStrictness DecidedLazy) (Rec0 (HashMap Text [Text]))))
type Item Form # |
type Item Form = (Text, Text)

# Encoding and decoding ``Form``s

urlEncodeAsForm :: ToForm a => a -> ByteString #

This is a convenience function for encoding a datatype that has instance of `ToForm` directly to a `application/x-www-form-urlencoded` `ByteString`.

This is effectively ``urlEncodeForm` . `toForm``.

    >>> urlEncodeAsForm Person {name = "Dennis", age = 22}
    "age=22&name=Dennis"

urlDecodeAsForm :: FromForm a => ByteString -> Either Text a #

This is a convenience function for decoding a `application/x-www-form-urlencoded` `ByteString` directly to a datatype that has an instance of `FromForm`.

This is effectively ``fromForm` `<=<` `urlDecodeForm``.

    >>> urlDecodeAsForm "name=Dennis&age=22" :: Either Text Person
    Right (Person {name = "Dennis", age = 22})

urlEncodeForm :: Form -> ByteString #

Encode a `Form` to an `application/x-www-form-urlencoded` `ByteString`.

Key-value pairs get encoded to `key=value` and separated by `&`:

    >>> urlEncodeForm [("name", "Julian"), ("lastname", "Arni")]
    "lastname=Arni&name=Julian"

Keys with empty values get encoded to just `key` (without the `=` sign):

    >>> urlEncodeForm [("is_test", "")]
    "is_test"

Empty keys are allowed too:

    >>> urlEncodeForm [("", "foobar")]
    "=foobar"

However, if not key and value are empty, the key-value pair is ignored. (This prevents ``urlDecodeForm` . `urlEncodeForm`` from being a true isomorphism).

    >>> urlEncodeForm [("", "")]
    ""

Everything is escaped with ``escapeURIString` `isUnreserved``:

    >>> urlEncodeForm [("fullname", "Andres Löh")]
    "fullname=Andres%20L%C3%B6h"

urlDecodeForm :: ByteString -> Either Text Form #

Decode an `application/x-www-form-urlencoded` `ByteString` to a `Form`.

Key-value pairs get decoded normally:

    >>> urlDecodeForm "name=Greg&lastname=Weber"
    Right (fromList [("lastname","Weber"),("name","Greg")])

Keys with no values get decoded to pairs with empty values.

    >>> urlDecodeForm "is_test"
    Right (fromList [("is_test","")])

Empty keys are allowed:

    >>> urlDecodeForm "=foobar"
    Right (fromList [("","foobar")])

The empty string gets decoded into an empty `Form`:

    >>> urlDecodeForm ""
    Right (fromList [])

Everything is un-escaped with `unEscapeString`:

    >>> urlDecodeForm "fullname=Andres%20L%C3%B6h"
    Right (fromList [("fullname","Andres L\246h")])

Improperly formed strings result in an error:

    >>> urlDecodeForm "this=has=too=many=equals"
    Left "not a valid pair: this=has=too=many=equals"

# `Generic`s

genericToForm :: forall a. (Generic a, GToForm a (Rep a)) => FormOptions -> a -> Form #

A `Generic`-based implementation of `toForm`. This is used as a default implementation in `ToForm`.

Note that this only works for records (i.e. product data types with named fields):

    data Person = Person
      { name :: String
      , age  :: Int
      } deriving (Generic)

In this implementation each field's value gets encoded using `toQueryParam`. Two field types are exceptions:

- for values of type ``Maybe` a` an entry is added to the `Form` only when it is ``Just` x` and the encoded value is ``toQueryParam` x`; `Nothing` values are omitted from the `Form`;
- for values of type `[a]` (except `[`Char`]`) an entry is added for every item in the list; if the list is empty no entries are added to the `Form`;

Here's an example:

    data Post = Post
      { title    :: String
      , subtitle :: Maybe String
      , comments :: [String]
      } deriving (Generic, Show)

    instance ToForm Post

    >>> urlEncodeAsForm Post { title = "Test", subtitle = Nothing, comments = ["Nice post!", "+1"] }
    "comments=Nice%20post%21&comments=%2B1&title=Test"

genericFromForm :: forall a. (Generic a, GFromForm a (Rep a)) => FormOptions -> Form -> Either Text a #

A `Generic`-based implementation of `fromForm`. This is used as a default implementation in `FromForm`.

Note that this only works for records (i.e. product data types with named fields):

    data Person = Person
      { name :: String
      , age  :: Int
      } deriving (Generic)

In this implementation each field's value gets decoded using `parseQueryParam`. Two field types are exceptions:

- for values of type ``Maybe` a` an entry is parsed if present in the `Form` and the is decoded with `parseQueryParam`; if no entry is present result is `Nothing`;
- for values of type `[a]` (except `[`Char`]`) all entries are parsed to produce a list of parsed values;

Here's an example:

    data Post = Post
      { title    :: String
      , subtitle :: Maybe String
      , comments :: [String]
      } deriving (Generic, Show)

    instance FromForm Post

    >>> urlDecodeAsForm "comments=Nice%20post%21&comments=%2B1&title=Test" :: Either Text Post
    Right (Post {title = "Test", subtitle = Nothing, comments = ["Nice post!","+1"]})

## Encoding options

data FormOptions #

`Generic`-based deriving options for `ToForm` and `FromForm`.

A common use case for non-default `FormOptions` is to strip a prefix off of field labels:

    data Project = Project
      { projectName :: String
      , projectSize :: Int
      } deriving (Generic, Show)

    myOptions :: FormOptions
    myOptions = FormOptions
     { fieldLabelModifier = map toLower . drop (length "project") }

    instance ToForm Project where
      toForm = genericToForm myOptions

    instance FromForm Project where
      fromForm = genericFromForm myOptions

    >>> urlEncodeAsForm Project { projectName = "http-api-data", projectSize = 172 }
    "size=172&name=http-api-data"
    >>> urlDecodeAsForm "name=http-api-data&size=172" :: Either Text Project
    Right (Project {projectName = "http-api-data", projectSize = 172})

Constructors

FormOptions |
Fields fieldLabelModifier :: String -> String Function applied to field labels. Handy for removing common record prefixes for example.

defaultFormOptions :: FormOptions #

Default encoding `FormOptions`.

    FormOptions
    { fieldLabelModifier = id
    }

# Helpers

toEntriesByKey :: (FromFormKey k, FromHttpApiData v) => Form -> Either Text [(k, [v])] #

Parse a `Form` into a list of entries groupped by key.

    >>> toEntriesByKey [("name", "Nick"), ("color", "red"), ("color", "white")] :: Either Text [(Text, [Text])]
    Right [("color",["red","white"]),("name",["Nick"])]

fromEntriesByKey :: (ToFormKey k, ToHttpApiData v) => [(k, [v])] -> Form #

Convert a list of entries groupped by key into a `Form`.

    >>> fromEntriesByKey [("name",["Nick"]),("color",["red","blue"])]
    fromList [("color","red"),("color","blue"),("name","Nick")]

lookupAll :: Text -> Form -> [Text] #

Find all values corresponding to a given key in a `Form`.

    >>> lookupAll "name" []
    []
    >>> lookupAll "name" [("name", "Oleg")]
    ["Oleg"]
    >>> lookupAll "name" [("name", "Oleg"), ("name", "David")]
    ["Oleg","David"]

lookupMaybe :: Text -> Form -> Either Text (Maybe Text) #

Lookup an optional value for a key. Fail if there is more than one value.

    >>> lookupMaybe "name" []
    Right Nothing
    >>> lookupMaybe "name" [("name", "Oleg")]
    Right (Just "Oleg")
    >>> lookupMaybe "name" [("name", "Oleg"), ("name", "David")]
    Left "Duplicate key \"name\""

lookupUnique :: Text -> Form -> Either Text Text #

Lookup a unique value for a key. Fail if there is zero or more than one value.

    >>> lookupUnique "name" []
    Left "Could not find key \"name\""
    >>> lookupUnique "name" [("name", "Oleg")]
    Right "Oleg"
    >>> lookupUnique "name" [("name", "Oleg"), ("name", "David")]
    Left "Duplicate key \"name\""

parseAll :: FromHttpApiData v => Text -> Form -> Either Text [v] #

Lookup all values for a given key in a `Form` and parse them with `parseQueryParams`.

    >>> parseAll "age" [] :: Either Text [Word8]
    Right []
    >>> parseAll "age" [("age", "8"), ("age", "seven")] :: Either Text [Word8]
    Left "could not parse: `seven' (input does not start with a digit)"
    >>> parseAll "age" [("age", "8"), ("age", "777")] :: Either Text [Word8]
    Left "out of bounds: `777' (should be between 0 and 255)"
    >>> parseAll "age" [("age", "12"), ("age", "25")] :: Either Text [Word8]
    Right [12,25]

parseMaybe :: FromHttpApiData v => Text -> Form -> Either Text (Maybe v) #

Lookup an optional value for a given key and parse it with `parseQueryParam`. Fail if there is more than one value for the key.

    >>> parseMaybe "age" [] :: Either Text (Maybe Word8)
    Right Nothing
    >>> parseMaybe "age" [("age", "12"), ("age", "25")] :: Either Text (Maybe Word8)
    Left "Duplicate key \"age\""
    >>> parseMaybe "age" [("age", "seven")] :: Either Text (Maybe Word8)
    Left "could not parse: `seven' (input does not start with a digit)"
    >>> parseMaybe "age" [("age", "777")] :: Either Text (Maybe Word8)
    Left "out of bounds: `777' (should be between 0 and 255)"
    >>> parseMaybe "age" [("age", "7")] :: Either Text (Maybe Word8)
    Right (Just 7)

parseUnique :: FromHttpApiData v => Text -> Form -> Either Text v #

Lookup a unique value for a given key and parse it with `parseQueryParam`. Fail if there is zero or more than one value for the key.

    >>> parseUnique "age" [] :: Either Text Word8
    Left "Could not find key \"age\""
    >>> parseUnique "age" [("age", "12"), ("age", "25")] :: Either Text Word8
    Left "Duplicate key \"age\""
    >>> parseUnique "age" [("age", "seven")] :: Either Text Word8
    Left "could not parse: `seven' (input does not start with a digit)"
    >>> parseUnique "age" [("age", "777")] :: Either Text Word8
    Left "out of bounds: `777' (should be between 0 and 255)"
    >>> parseUnique "age" [("age", "7")] :: Either Text Word8
    Right 7