git.iko.soy/typeable/http-api-data
on master help sign in
Add comments and doctests to the FormUrlEncoded module.
commit8ab1a1fb462b1446797acbbfd74abf5c5c8eaa30 author(cdep)illabout <cdep.illabout@gmail.com> dateFri 26 Aug 2016, 21:24 (10 years ago) parentdd1baa5
1 file changed+131−3
src/Web/HttpApiData/Internal/FormUrlEncoded.hs+131−3
src/Web/HttpApiData/Internal/FormUrlEncoded.hs+131−3
@@ -32,16 +32,56 @@ import Network.URI (escapeURIString, isUnreserved,
3232
3333import Web.HttpApiData.Internal.HttpApiData
3434
35+-- $setup
36+-- >>> :set -XDeriveGeneric
37+-- >>> :set -XScopedTypeVariables
38+-- >>> :set -XTypeFamilies
39+-- >>> import Data.Either (isLeft)
40+-- >>> import Data.List (sort)
41+
3542-- | The contents of a form, not yet url-encoded.
43+--
44+-- Forms can be URL-encoded with 'encodeForm'. Url-encoded bytestrings can be
45+-- converted to a 'Form' with 'decodeForm'.
3646newtype Form = Form { unForm :: M.Map T.Text T.Text }
3747 deriving (Eq, Show, Read, Generic, Monoid)
3848
49+-- | Convert a list of @('T.Text', 'T.Text')@ to a 'Form'.
50+--
51+-- >>> let (form :: Form) = fromList [("baz", "qux"), ("foo", "bar")]
52+-- >>> toList form
53+-- [("baz","qux"),("foo","bar")]
3954instance IsList Form where
4055 type Item Form = (T.Text, T.Text)
4156 fromList = Form . M.fromList
4257 toList = M.toList . unForm
4358
44−-- | A type that can be converted to @application/x-www-form-urlencoded@
59+-- | A type that can be converted to a 'Form'.
60+--
61+-- >>> let form = toForm [("foo" :: T.Text, "bar" :: T.Text)]
62+-- >>> encodeForm form
63+-- "foo=bar"
64+--
65+-- Your own datatypes can be converted to a 'Form' like the following:
66+--
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
71+-- "bar=hello"
72+--
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
76+-- 'ToHttpApiData'.
77+--
78+-- This can be used like the following:
79+--
80+-- >>> data MyGenericBar = MyGenericBar { myGenericBar :: Int } deriving Generic
81+-- >>> instance ToForm MyGenericBar
82+-- >>> let form = toForm $ MyGenericBar { myGenericBar = 3 }
83+-- >>> encodeForm form
84+-- "myGenericBar=3"
4585class ToForm a where
4686 toForm :: a -> Form
4787 default toForm :: (Generic a, GToForm (Rep a)) => a -> Form
@@ -92,8 +132,36 @@ instance (ToHttpApiData f) => GToForm (K1 i f) where
92132 where name (Nothing, i) = T.pack $ show i
93133 name (Just x, _) = x
94134
95−-- | A type that can be converted from @application/x-www-form-urlencoded@,
96−-- with the possibility of failure.
135+-- | A type that can be converted from a 'Form', with the possibility of
136+-- failure.
137+--
138+-- >>> let form = fromList [("foo" :: T.Text, "bar" :: T.Text)]
139+-- >>> fromForm form :: Either T.Text [(T.Text, T.Text)]
140+-- Right [("foo","bar")]
141+--
142+-- Your own datatypes can be converted from a 'Form' like the following:
143+--
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"})
149+--
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'.
154+--
155+-- This can be used like the following:
156+--
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)
164+-- True
97165class FromForm a where
98166 fromForm :: Form -> Either T.Text a
99167 default fromForm :: (Generic a, GFromForm (Rep a))
@@ -136,6 +204,33 @@ instance (GFromForm f) => GFromForm (M1 D x f) where
136204instance (GFromForm f) => GFromForm (M1 C x f) where
137205 gFromForm f = M1 <$> gFromForm f
138206
207+-- | Encode a 'Form' to a @application/x-www-form-urlencoded@ 'BSL.ByteString'.
208+--
209+-- Key-value pairs get encoded to @key=value@.
210+--
211+-- >>> encodeForm $ toForm [("foo" :: T.Text, "bar" :: T.Text)]
212+-- "foo=bar"
213+--
214+-- Keys with no values get encoded to just @key@.
215+--
216+-- >>> encodeForm $ toForm [("foo" :: T.Text, "" :: T.Text)]
217+-- "foo"
218+--
219+-- Empty keys don't show up.
220+--
221+-- >>> encodeForm $ toForm [("" :: T.Text, "bar" :: T.Text)]
222+-- "=bar"
223+--
224+-- When both they key and value is empty, it gets encoded as nothing. (This
225+-- prevents @'decodeForm' . 'encodeForm'@ from being an isomorphism).
226+--
227+-- >>> encodeForm $ toForm [("" :: T.Text, "" :: T.Text)]
228+-- ""
229+--
230+-- Other characters are escaped by @'escapeURIString' 'isUnreserved'@.
231+--
232+-- >>> encodeForm $ toForm [("foo" :: T.Text, "value with spaces" :: T.Text)]
233+-- "foo=value%20with%20spaces"
139234encodeForm :: Form -> BSL.ByteString
140235encodeForm xs =
141236 let escape :: T.Text -> BSL.ByteString
@@ -145,6 +240,39 @@ encodeForm xs =
145240 encodePair (k, v) = escape k <> "=" <> escape v
146241 in BSL.intercalate "&" $ map encodePair $ toList xs
147242
243+-- | Decode a @application/x-www-form-urlencoded@ 'BSL.ByteString' to a 'Form'.
244+--
245+-- Key-value pairs get decoded normally.
246+--
247+-- >>> toList <$> decodeForm "foo=bar"
248+-- Right [("foo","bar")]
249+-- >>> sort . toList <$> decodeForm "foo=bar&abc=xyz"
250+-- Right [("abc","xyz"),("foo","bar")]
251+--
252+-- Keys with no values get decoded to empty values.
253+--
254+-- >>> toList <$> decodeForm "foo"
255+-- Right [("foo","")]
256+--
257+-- Empty keys get decoded as @""@.
258+--
259+-- >>> toList <$> decodeForm "=bar"
260+-- Right [("","bar")]
261+--
262+-- The empty string gets decoded as no key-value pairs.
263+--
264+-- >>> toList <$> decodeForm ""
265+-- Right []
266+--
267+-- Other characters are un-escaped with 'unEscapeString'.
268+--
269+-- >>> toList <$> decodeForm "foo=value%20with%20spaces"
270+-- Right [("foo","value with spaces")]
271+--
272+-- Improperly formed strings return 'Left'.
273+--
274+-- >>> isLeft $ decodeForm "this=has=too=many=equals"
275+-- True
148276decodeForm :: BSL.ByteString -> Either T.Text Form
149277decodeForm "" = return mempty
150278decodeForm q = do