git.iko.soy/typeable/http-api-data
on master help sign in
Add comments and doctests to the FormUrlEncoded module.
8ab1a1f(cdep)illabout
src/Web/HttpApiData/Internal/FormUrlEncoded.hs+131−3
@@ -32,16 +32,56 @@ import Network.URI (escapeURIString, isUnreserved,
32
32
33import Web.HttpApiData.Internal.HttpApiData
33import Web.HttpApiData.Internal.HttpApiData
34
34
35+-- $setup
36+-- >>> :set -XDeriveGeneric
37+-- >>> :set -XScopedTypeVariables
38+-- >>> :set -XTypeFamilies
39+-- >>> import Data.Either (isLeft)
40+-- >>> import Data.List (sort)
41+
35-- | The contents of a form, not yet url-encoded.
42-- | 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'.
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)
38
48
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")]
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
43
58
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"
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
94
134
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.
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
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
138
206
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"
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
147
242
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
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