@@ -32,16 +32,56 @@ import Network.URI (escapeURIString, isUnreserved,
33import Web.HttpApiData.Internal.HttpApiData
33import Web.HttpApiData.Internal.HttpApiData
36+-- >>> :set -XDeriveGeneric
37+-- >>> :set -XScopedTypeVariables
38+-- >>> :set -XTypeFamilies
39+-- >>> import Data.Either (isLeft)
40+-- >>> import Data.List (sort)
35-- | The contents of a form, not yet url-encoded.
42-- | The contents of a form, not yet url-encoded.
44+-- Forms can be URL-encoded with 'encodeForm'. Url-encoded bytestrings can be
45+-- converted to a 'Form' with 'decodeForm'.
36newtype Form = Form { unForm :: M.Map T.Text T.Text }
46newtype Form = Form { unForm :: M.Map T.Text T.Text }
37 deriving (Eq, Show, Read, Generic, Monoid)
47 deriving (Eq, Show, Read, Generic, Monoid)
49+-- | Convert a list of @('T.Text', 'T.Text')@ to a 'Form'.
51+-- >>> let (form :: Form) = fromList [("baz", "qux"), ("foo", "bar")]
53+-- [("baz","qux"),("foo","bar")]
39instance IsList Form where
54instance IsList Form where
40 type Item Form = (T.Text, T.Text)
55 type Item Form = (T.Text, T.Text)
41 fromList = Form . M.fromList
56 fromList = Form . M.fromList
42 toList = M.toList . unForm
57 toList = M.toList . unForm
44−-- | A type that can be converted to @application/x-www-form-urlencoded@
59+-- | A type that can be converted to a 'Form'.
61+-- >>> let form = toForm [("foo" :: T.Text, "bar" :: T.Text)]
62+-- >>> encodeForm form
65+-- Your own datatypes can be converted to a 'Form' like the following:
67+-- >>> data MyFoo = MyFoo { myFooBar :: T.Text }
68+-- >>> instance ToForm MyFoo where toForm (MyFoo bar') = fromList [("bar", bar')]
69+-- >>> let form = toForm $ MyFoo { myFooBar = "hello" }
70+-- >>> encodeForm form
73+-- There is a default method for 'toForm' for any Haskell record type that
74+-- derives 'Generic'. The record's fields will be turned into the keys of the
75+-- 'Form'. This only works for fields with types that implement
78+-- This can be used like the following:
80+-- >>> data MyGenericBar = MyGenericBar { myGenericBar :: Int } deriving Generic
81+-- >>> instance ToForm MyGenericBar
82+-- >>> let form = toForm $ MyGenericBar { myGenericBar = 3 }
83+-- >>> encodeForm form
45class ToForm a where
85class ToForm a where
46 toForm :: a -> Form
86 toForm :: a -> Form
47 default toForm :: (Generic a, GToForm (Rep a)) => a -> Form
87 default toForm :: (Generic a, GToForm (Rep a)) => a -> Form
@@ -92,8 +132,36 @@ instance (ToHttpApiData f) => GToForm (K1 i f) where
92 where name (Nothing, i) = T.pack $ show i
132 where name (Nothing, i) = T.pack $ show i
93 name (Just x, _) = x
133 name (Just x, _) = x
95−-- | A type that can be converted from @application/x-www-form-urlencoded@,
135+-- | A type that can be converted from a 'Form', with the possibility of
96−-- with the possibility of failure.
136+-- failure.
138+-- >>> let form = fromList [("foo" :: T.Text, "bar" :: T.Text)]
139+-- >>> fromForm form :: Either T.Text [(T.Text, T.Text)]
140+-- Right [("foo","bar")]
142+-- Your own datatypes can be converted from a 'Form' like the following:
144+-- >>> data MyFoo = MyFoo { myFooBar :: T.Text } deriving Show
145+-- >>> instance FromForm MyFoo where fromForm (Form map') = maybe (Left "key \"bar\" not found") (Right . MyFoo) $ M.lookup "bar" map'
146+-- >>> let form = fromList [("bar" :: T.Text, "hello" :: T.Text)]
147+-- >>> fromForm form :: Either T.Text MyFoo
148+-- Right (MyFoo {myFooBar = "hello"})
150+-- There is a default method for 'fromForm' for any Haskell record type that
151+-- derives 'Generic'. The record's fields will be taken from the keys of the
152+-- 'Form'. This only works for fields with types that implement
153+-- 'FromHttpApiData'.
155+-- This can be used like the following:
157+-- >>> data MyGenericBar = MyGenericBar { myGenericBar :: Int } deriving (Generic, Show)
158+-- >>> instance FromForm MyGenericBar
159+-- >>> let form = fromList [("myGenericBar" :: T.Text, "1" :: T.Text)]
160+-- >>> fromForm form :: Either T.Text MyGenericBar
161+-- Right (MyGenericBar {myGenericBar = 1})
162+-- >>> let form = fromList [("some other key" :: T.Text, "2" :: T.Text)]
163+-- >>> isLeft (fromForm form :: Either T.Text MyGenericBar)
97class FromForm a where
165class FromForm a where
98 fromForm :: Form -> Either T.Text a
166 fromForm :: Form -> Either T.Text a
99 default fromForm :: (Generic a, GFromForm (Rep a))
167 default fromForm :: (Generic a, GFromForm (Rep a))
@@ -136,6 +204,33 @@ instance (GFromForm f) => GFromForm (M1 D x f) where
136instance (GFromForm f) => GFromForm (M1 C x f) where
204instance (GFromForm f) => GFromForm (M1 C x f) where
137 gFromForm f = M1 <$> gFromForm f
205 gFromForm f = M1 <$> gFromForm f
207+-- | Encode a 'Form' to a @application/x-www-form-urlencoded@ 'BSL.ByteString'.
209+-- Key-value pairs get encoded to @key=value@.
211+-- >>> encodeForm $ toForm [("foo" :: T.Text, "bar" :: T.Text)]
214+-- Keys with no values get encoded to just @key@.
216+-- >>> encodeForm $ toForm [("foo" :: T.Text, "" :: T.Text)]
219+-- Empty keys don't show up.
221+-- >>> encodeForm $ toForm [("" :: T.Text, "bar" :: T.Text)]
224+-- When both they key and value is empty, it gets encoded as nothing. (This
225+-- prevents @'decodeForm' . 'encodeForm'@ from being an isomorphism).
227+-- >>> encodeForm $ toForm [("" :: T.Text, "" :: T.Text)]
230+-- Other characters are escaped by @'escapeURIString' 'isUnreserved'@.
232+-- >>> encodeForm $ toForm [("foo" :: T.Text, "value with spaces" :: T.Text)]
233+-- "foo=value%20with%20spaces"
139encodeForm :: Form -> BSL.ByteString
234encodeForm :: Form -> BSL.ByteString
140encodeForm xs =
235encodeForm xs =
141 let escape :: T.Text -> BSL.ByteString
236 let escape :: T.Text -> BSL.ByteString
@@ -145,6 +240,39 @@ encodeForm xs =
145 encodePair (k, v) = escape k <> "=" <> escape v
240 encodePair (k, v) = escape k <> "=" <> escape v
146 in BSL.intercalate "&" $ map encodePair $ toList xs
241 in BSL.intercalate "&" $ map encodePair $ toList xs
243+-- | Decode a @application/x-www-form-urlencoded@ 'BSL.ByteString' to a 'Form'.
245+-- Key-value pairs get decoded normally.
247+-- >>> toList <$> decodeForm "foo=bar"
248+-- Right [("foo","bar")]
249+-- >>> sort . toList <$> decodeForm "foo=bar&abc=xyz"
250+-- Right [("abc","xyz"),("foo","bar")]
252+-- Keys with no values get decoded to empty values.
254+-- >>> toList <$> decodeForm "foo"
255+-- Right [("foo","")]
257+-- Empty keys get decoded as @""@.
259+-- >>> toList <$> decodeForm "=bar"
260+-- Right [("","bar")]
262+-- The empty string gets decoded as no key-value pairs.
264+-- >>> toList <$> decodeForm ""
267+-- Other characters are un-escaped with 'unEscapeString'.
269+-- >>> toList <$> decodeForm "foo=value%20with%20spaces"
270+-- Right [("foo","value with spaces")]
272+-- Improperly formed strings return 'Left'.
274+-- >>> isLeft $ decodeForm "this=has=too=many=equals"
148decodeForm :: BSL.ByteString -> Either T.Text Form
276decodeForm :: BSL.ByteString -> Either T.Text Form
149decodeForm "" = return mempty
277decodeForm "" = return mempty
150decodeForm q = do
278decodeForm q = do