@@ -1,50 +1,67 @@
1+{-# LANGUAGE ConstraintKinds #-}
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE CPP #-}
3+{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE DefaultSignatures #-}
4{-# LANGUAGE DefaultSignatures #-}
3{-# LANGUAGE DeriveGeneric #-}
5{-# LANGUAGE DeriveGeneric #-}
4{-# LANGUAGE FlexibleContexts #-}
6{-# LANGUAGE FlexibleContexts #-}
5{-# LANGUAGE FlexibleInstances #-}
7{-# LANGUAGE FlexibleInstances #-}
6{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8{-# LANGUAGE GeneralizedNewtypeDeriving #-}
7{-# LANGUAGE KindSignatures #-}
9{-# LANGUAGE KindSignatures #-}
10+{-# LANGUAGE MultiParamTypeClasses #-}
8{-# LANGUAGE OverloadedStrings #-}
11{-# LANGUAGE OverloadedStrings #-}
9{-# LANGUAGE PolyKinds #-}
12{-# LANGUAGE PolyKinds #-}
10{-# LANGUAGE ScopedTypeVariables #-}
13{-# LANGUAGE ScopedTypeVariables #-}
11{-# LANGUAGE TypeFamilies #-}
14{-# LANGUAGE TypeFamilies #-}
12{-# LANGUAGE TypeOperators #-}
15{-# LANGUAGE TypeOperators #-}
16+{-# LANGUAGE UndecidableInstances #-}
17+#include "overlapping-compat.h"
13module Web.Internal.FormUrlEncoded where
18module Web.Internal.FormUrlEncoded where
15−#if __GLASGOW_HASKELL__ < 710
20+import Prelude ()
16−import Control.Applicative
21+import Prelude.Compat
23+import Control.Applicative (Const(Const))
19import Control.Arrow ((***))
24import Control.Arrow ((***))
20import Control.Monad ((<=<))
25import Control.Monad ((<=<))
26+import Data.ByteString.Builder (shortByteString, toLazyByteString)
21import qualified Data.ByteString.Lazy as BSL
27import qualified Data.ByteString.Lazy as BSL
22import qualified Data.ByteString.Lazy.Char8 as BSL8
28import qualified Data.ByteString.Lazy.Char8 as BSL8
29+import Data.Coerce (coerce)
30+import qualified Data.Foldable as F
31+import Data.Functor.Identity (Identity(Identity))
23import Data.Hashable (Hashable)
32import Data.Hashable (Hashable)
24import Data.HashMap.Strict (HashMap)
33import Data.HashMap.Strict (HashMap)
25import qualified Data.HashMap.Strict as HashMap
34import qualified Data.HashMap.Strict as HashMap
26−import Data.Int
35+import Data.Int (Int16, Int32, Int64, Int8)
27import Data.IntMap (IntMap)
36import Data.IntMap (IntMap)
28import qualified Data.IntMap as IntMap
37import qualified Data.IntMap as IntMap
38+import Data.List (intersperse, sortBy)
29import Data.Map (Map)
39import Data.Map (Map)
30import qualified Data.Map as Map
40import qualified Data.Map as Map
31−import Data.Monoid
41+import Data.Monoid (All (..), Any (..), Dual (..),
32−
42+ Product (..), Sum (..))
43+import Data.Ord (comparing)
44+import Data.Proxy (Proxy (..))
45+import Data.Semigroup (Semigroup (..))
46+import qualified Data.Semigroup as Semi
47+import Data.Tagged (Tagged (..))
33import Data.Text (Text)
48import Data.Text (Text)
34import qualified Data.Text as Text
49import qualified Data.Text as Text
35−import qualified Data.Text.Lazy as Lazy
50+import qualified Data.Text.Encoding as Text
36−import Data.Text.Encoding as Text
37import Data.Text.Encoding.Error (lenientDecode)
51import Data.Text.Encoding.Error (lenientDecode)
38−
52+import qualified Data.Text.Lazy as Lazy
53+import Data.Time.Compat (Day, LocalTime, NominalDiffTime,
55+import Data.Time.Calendar.Month.Compat (Month)
39−import Data.Time
56+import Data.Time.Calendar.Quarter.Compat (Quarter, QuarterOfYear (..))
57+import Data.Void (Void)
40−import Data.Word
58+import Data.Word (Word16, Word32, Word64, Word8)
42−import GHC.Exts (IsList (..))
59+import GHC.Exts (Constraint, IsList (..))
43import GHC.Generics
60import GHC.Generics
44−import Network.URI (escapeURIString, isUnreserved,
61+import GHC.TypeLits
45− unEscapeString)
62+import Network.HTTP.Types.URI (urlDecode, urlEncodeBuilder)
46−
63+import Numeric.Natural (Natural)
47−import Web.Internal.HttpApiData
64+import Web.Internal.HttpApiData
50-- >>> :set -XDeriveGeneric
67-- >>> :set -XDeriveGeneric
@@ -53,11 +70,20 @@ import Web.Internal.HttpApiData
53-- >>> :set -XFlexibleContexts
70-- >>> :set -XFlexibleContexts
54-- >>> :set -XScopedTypeVariables
71-- >>> :set -XScopedTypeVariables
55-- >>> :set -XTypeFamilies
72-- >>> :set -XTypeFamilies
56−-- >>> import Data.Either (isLeft)
73+-- >>> import Data.Char (toLower)
57−-- >>> import Data.List (sort)
74+--
58-- >>> data Person = Person { name :: String, age :: Int } deriving (Show, Generic)
75-- >>> data Person = Person { name :: String, age :: Int } deriving (Show, Generic)
59-- >>> instance ToForm Person
76-- >>> instance ToForm Person
60-- >>> instance FromForm Person
77-- >>> instance FromForm Person
79+-- >>> data Post = Post { title :: String, subtitle :: Maybe String, comments :: [String]} deriving (Generic, Show)
80+-- >>> instance ToForm Post
81+-- >>> instance FromForm Post
83+-- >>> data Project = Project { projectName :: String, projectSize :: Int } deriving (Generic, Show)
84+-- >>> let myOptions = FormOptions { fieldLabelModifier = map toLower . drop (length ("project" :: String)) }
85+-- >>> instance ToForm Project where toForm = genericToForm myOptions
86+-- >>> instance FromForm Project where fromForm = genericFromForm myOptions
62-- | Typeclass for types that can be used as keys in a 'Form'-like container (like 'Map').
88-- | Typeclass for types that can be used as keys in a 'Form'-like container (like 'Map').
63class ToFormKey k where
89class ToFormKey k where
@@ -89,6 +115,9 @@ instance ToFormKey LocalTime where toFormKey = toQueryParam
89instance ToFormKey ZonedTime where toFormKey = toQueryParam
115instance ToFormKey ZonedTime where toFormKey = toQueryParam
90instance ToFormKey UTCTime where toFormKey = toQueryParam
116instance ToFormKey UTCTime where toFormKey = toQueryParam
91instance ToFormKey NominalDiffTime where toFormKey = toQueryParam
117instance ToFormKey NominalDiffTime where toFormKey = toQueryParam
118+instance ToFormKey Quarter where toFormKey = toQueryParam
119+instance ToFormKey QuarterOfYear where toFormKey = toQueryParam
120+instance ToFormKey Month where toFormKey = toQueryParam
93instance ToFormKey String where toFormKey = toQueryParam
122instance ToFormKey String where toFormKey = toQueryParam
94instance ToFormKey Text where toFormKey = toQueryParam
123instance ToFormKey Text where toFormKey = toQueryParam
@@ -97,9 +126,26 @@ instance ToFormKey Lazy.Text where toFormKey = toQueryParam
97instance ToFormKey All where toFormKey = toQueryParam
126instance ToFormKey All where toFormKey = toQueryParam
98instance ToFormKey Any where toFormKey = toQueryParam
127instance ToFormKey Any where toFormKey = toQueryParam
100−instance ToFormKey a => ToFormKey (Dual a) where toFormKey = toFormKey . getDual
129+instance ToFormKey a => ToFormKey (Dual a) where toFormKey = coerce (toFormKey :: a -> Text)
101−instance ToFormKey a => ToFormKey (Sum a) where toFormKey = toFormKey . getSum
130+instance ToFormKey a => ToFormKey (Sum a) where toFormKey = coerce (toFormKey :: a -> Text)
102−instance ToFormKey a => ToFormKey (Product a) where toFormKey = toFormKey . getProduct
131+instance ToFormKey a => ToFormKey (Product a) where toFormKey = coerce (toFormKey :: a -> Text)
133+instance ToFormKey a => ToFormKey (Semi.Min a) where toFormKey = coerce (toFormKey :: a -> Text)
134+instance ToFormKey a => ToFormKey (Semi.Max a) where toFormKey = coerce (toFormKey :: a -> Text)
135+instance ToFormKey a => ToFormKey (Semi.First a) where toFormKey = coerce (toFormKey :: a -> Text)
136+instance ToFormKey a => ToFormKey (Semi.Last a) where toFormKey = coerce (toFormKey :: a -> Text)
138+instance ToFormKey a => ToFormKey (Tagged b a) where toFormKey = coerce (toFormKey :: a -> Text)
141+instance ToFormKey a => ToFormKey (Identity a) where toFormKey = coerce (toFormKey :: a -> Text)
144+instance ToFormKey a => ToFormKey (Const a b) where
145+ toFormKey = coerce (toFormKey :: a -> Text)
147+instance ToFormKey Void where toFormKey = toQueryParam
148+instance ToFormKey Natural where toFormKey = toQueryParam
104-- | Typeclass for types that can be parsed from keys of a 'Form'. This is the reverse of 'ToFormKey'.
150-- | Typeclass for types that can be parsed from keys of a 'Form'. This is the reverse of 'ToFormKey'.
105class FromFormKey k where
151class FromFormKey k where
@@ -131,6 +177,9 @@ instance FromFormKey LocalTime where parseFormKey = parseQueryParam
131instance FromFormKey ZonedTime where parseFormKey = parseQueryParam
177instance FromFormKey ZonedTime where parseFormKey = parseQueryParam
132instance FromFormKey UTCTime where parseFormKey = parseQueryParam
178instance FromFormKey UTCTime where parseFormKey = parseQueryParam
133instance FromFormKey NominalDiffTime where parseFormKey = parseQueryParam
179instance FromFormKey NominalDiffTime where parseFormKey = parseQueryParam
180+instance FromFormKey Quarter where parseFormKey = parseQueryParam
181+instance FromFormKey QuarterOfYear where parseFormKey = parseQueryParam
182+instance FromFormKey Month where parseFormKey = parseQueryParam
135instance FromFormKey String where parseFormKey = parseQueryParam
184instance FromFormKey String where parseFormKey = parseQueryParam
136instance FromFormKey Text where parseFormKey = parseQueryParam
185instance FromFormKey Text where parseFormKey = parseQueryParam
@@ -139,25 +188,48 @@ instance FromFormKey Lazy.Text where parseFormKey = parseQueryParam
139instance FromFormKey All where parseFormKey = parseQueryParam
188instance FromFormKey All where parseFormKey = parseQueryParam
140instance FromFormKey Any where parseFormKey = parseQueryParam
189instance FromFormKey Any where parseFormKey = parseQueryParam
142−instance FromFormKey a => FromFormKey (Dual a) where parseFormKey = fmap Dual . parseFormKey
191+instance FromFormKey a => FromFormKey (Dual a) where parseFormKey = coerce (parseFormKey :: Text -> Either Text a)
192+instance FromFormKey a => FromFormKey (Sum a) where parseFormKey = coerce (parseFormKey :: Text -> Either Text a)
193+instance FromFormKey a => FromFormKey (Product a) where parseFormKey = coerce (parseFormKey :: Text -> Either Text a)
195+instance FromFormKey a => FromFormKey (Semi.Min a) where parseFormKey = coerce (parseFormKey :: Text -> Either Text a)
196+instance FromFormKey a => FromFormKey (Semi.Max a) where parseFormKey = coerce (parseFormKey :: Text -> Either Text a)
143−instance FromFormKey a => FromFormKey (Sum a) where parseFormKey = fmap Sum . parseFormKey
197+instance FromFormKey a => FromFormKey (Semi.First a) where parseFormKey = coerce (parseFormKey :: Text -> Either Text a)
198+instance FromFormKey a => FromFormKey (Semi.Last a) where parseFormKey = coerce (parseFormKey :: Text -> Either Text a)
200+instance FromFormKey a => FromFormKey (Tagged b a) where parseFormKey = coerce (parseFormKey :: Text -> Either Text a)
144−instance FromFormKey a => FromFormKey (Product a) where parseFormKey = fmap Product . parseFormKey
203+instance FromFormKey a => FromFormKey (Identity a) where parseFormKey = coerce (parseFormKey :: Text -> Either Text a)
206+instance FromFormKey a => FromFormKey (Const a b) where
207+ parseFormKey = coerce (parseFormKey :: Text -> Either Text a)
209+instance FromFormKey Void where parseFormKey = parseQueryParam
210+instance FromFormKey Natural where parseFormKey = parseQueryParam
146-- | The contents of a form, not yet URL-encoded.
212-- | The contents of a form, not yet URL-encoded.
148−-- 'Form' can be URL-encoded with 'encodeForm' and URL-decoded with 'decodeForm'.
214+-- 'Form' can be URL-encoded with 'urlEncodeForm' and URL-decoded with 'urlDecodeForm'.
149newtype Form = Form { unForm :: HashMap Text [Text] }
215newtype Form = Form { unForm :: HashMap Text [Text] }
150− deriving (Eq, Read, Generic, Monoid)
216+ deriving (Eq, Read, Generic, Semigroup, Monoid)
152instance Show Form where
218instance Show Form where
153 showsPrec d form = showParen (d > 10) $
219 showsPrec d form = showParen (d > 10) $
154− showString "fromList " . shows (toList form)
220+ showString "fromList " . shows (toListStable form)
222+-- | _NOTE:_ 'toList' is unstable and may result in different key order (but not values).
223+-- For a stable conversion use 'toListStable'.
156instance IsList Form where
224instance IsList Form where
157 type Item Form = (Text, Text)
225 type Item Form = (Text, Text)
158 fromList = Form . HashMap.fromListWith (flip (<>)) . fmap (\(k, v) -> (k, [v]))
226 fromList = Form . HashMap.fromListWith (flip (<>)) . fmap (\(k, v) -> (k, [v]))
159 toList = concatMap (\(k, vs) -> map ((,) k) vs) . HashMap.toList . unForm
227 toList = concatMap (\(k, vs) -> map ((,) k) vs) . HashMap.toList . unForm
229+-- | A stable version of 'toList'.
230+toListStable :: Form -> [(Text, Text)]
231+toListStable = sortOn fst . toList
161-- | Convert a value into 'Form'.
233-- | Convert a value into 'Form'.
163-- An example type and instance:
235-- An example type and instance:
@@ -193,12 +265,12 @@ instance IsList Form where
193-- instance 'ToForm' Person
265-- instance 'ToForm' Person
196−-- The default implementation will use 'toQueryParam' for each field's value.
268+-- The default implementation of 'toForm' is 'genericToForm'.
197class ToForm a where
269class ToForm a where
198 -- | Convert a value into 'Form'.
270 -- | Convert a value into 'Form'.
199 toForm :: a -> Form
271 toForm :: a -> Form
200− default toForm :: (Generic a, GToForm (Rep a)) => a -> Form
272+ default toForm :: (Generic a, GToForm a (Rep a)) => a -> Form
201− toForm = genericToForm
273+ toForm = genericToForm defaultFormOptions
203instance ToForm Form where toForm = id
275instance ToForm Form where toForm = id
@@ -215,34 +287,100 @@ instance ToHttpApiData v => ToForm (IntMap [v]) where
215 toForm = fromEntriesByKey . IntMap.toList
287 toForm = fromEntriesByKey . IntMap.toList
217-- | Convert a list of entries groupped by key into a 'Form'.
289-- | Convert a list of entries groupped by key into a 'Form'.
291+-- >>> fromEntriesByKey [("name",["Nick"]),("color",["red","blue"])]
292+-- fromList [("color","red"),("color","blue"),("name","Nick")]
218fromEntriesByKey :: (ToFormKey k, ToHttpApiData v) => [(k, [v])] -> Form
293fromEntriesByKey :: (ToFormKey k, ToHttpApiData v) => [(k, [v])] -> Form
219fromEntriesByKey = Form . HashMap.fromListWith (<>) . map (toFormKey *** map toQueryParam)
294fromEntriesByKey = Form . HashMap.fromListWith (<>) . map (toFormKey *** map toQueryParam)
296+data Proxy3 a b c = Proxy3
298+type family NotSupported (cls :: k1) (a :: k2) (reason :: Symbol) :: Constraint where
299+#if __GLASGOW_HASKELL__ < 800
300+ -- this is just a placeholder case for older GHCs to not freak out on an empty closed type family
301+ NotSupported cls a "this type family is actually empty" = ()
303+ NotSupported cls a reason = TypeError
304+ ( 'Text "Cannot derive a Generic-based " ':<>: 'ShowType cls ':<>: 'Text " instance for " ':<>: 'ShowType a ':<>: 'Text "." ':$$:
305+ 'ShowType a ':<>: 'Text " " ':<>: 'Text reason ':<>: 'Text "," ':$$:
306+ 'Text "but Generic-based " ':<>: 'ShowType cls ':<>: 'Text " instances can be derived only for records" ':$$:
307+ 'Text "(i.e. product types with named fields)." )
221-- | A 'Generic'-based implementation of 'toForm'.
310-- | A 'Generic'-based implementation of 'toForm'.
222-- This is used as a default implementation in 'ToForm'.
311-- This is used as a default implementation in 'ToForm'.
313+-- Note that this only works for records (i.e. product data types with named fields):
316+-- data Person = Person
319+-- } deriving ('Generic')
322+-- In this implementation each field's value gets encoded using `toQueryParam`.
323+-- Two field types are exceptions:
325+-- - for values of type @'Maybe' a@ an entry is added to the 'Form' only when it is @'Just' x@
326+-- and the encoded value is @'toQueryParam' x@; 'Nothing' values are omitted from the 'Form';
328+-- - for values of type @[a]@ (except @['Char']@) an entry is added for every item in the list;
329+-- if the list is empty no entries are added to the 'Form';
331+-- Here's an example:
336+-- , subtitle :: Maybe String
337+-- , comments :: [String]
338+-- } deriving ('Generic', 'Show')
340+-- instance 'ToForm' Post
343+-- >>> urlEncodeAsFormStable Post { title = "Test", subtitle = Nothing, comments = ["Nice post!", "+1"] }
344+-- "comments=Nice%20post%21&comments=%2B1&title=Test"
223−genericToForm :: (Generic a, GToForm (Rep a)) => a -> Form
345+genericToForm :: forall a. (Generic a, GToForm a (Rep a)) => FormOptions -> a -> Form
224−genericToForm = gToForm . from
346+genericToForm opts = gToForm (Proxy :: Proxy a) opts . from
226−class GToForm (f :: * -> *) where
348+class GToForm t (f :: * -> *) where
227− gToForm :: f x -> Form
349+ gToForm :: Proxy t -> FormOptions -> f x -> Form
229−instance (GToForm f, GToForm g) => GToForm (f :*: g) where
351+instance (GToForm t f, GToForm t g) => GToForm t (f :*: g) where
230− gToForm (a :*: b) = gToForm a <> gToForm b
352+ gToForm p opts (a :*: b) = gToForm p opts a <> gToForm p opts b
232−instance (GToForm f, GToForm g) => GToForm (f :+: g) where
354+instance (GToForm t f) => GToForm t (M1 D x f) where
233− gToForm (L1 a) = gToForm a
355+ gToForm p opts (M1 a) = gToForm p opts a
234− gToForm (R1 a) = gToForm a
236−instance (GToForm f) => GToForm (M1 D x f) where
357+instance (GToForm t f) => GToForm t (M1 C x f) where
237− gToForm (M1 a) = gToForm a
358+ gToForm p opts (M1 a) = gToForm p opts a
239−instance (GToForm f) => GToForm (M1 C x f) where
360+instance OVERLAPPABLE_ (Selector s, ToHttpApiData c) => GToForm t (M1 S s (K1 i c)) where
240− gToForm (M1 a) = gToForm a
361+ gToForm _ opts (M1 (K1 c)) = fromList [(key, toQueryParam c)]
363+ key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)
365+instance (Selector s, ToHttpApiData c) => GToForm t (M1 S s (K1 i (Maybe c))) where
366+ gToForm _ opts (M1 (K1 c)) =
369+ Just x -> fromList [(key, toQueryParam x)]
371+ key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)
373+instance (Selector s, ToHttpApiData c) => GToForm t (M1 S s (K1 i [c])) where
374+ gToForm _ opts (M1 (K1 cs)) = fromList (map (\c -> (key, toQueryParam c)) cs)
376+ key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)
242−instance (Selector s, ToHttpApiData c) => GToForm (M1 S s (K1 i c)) where
378+instance OVERLAPPING_ (Selector s) => GToForm t (M1 S s (K1 i String)) where
243− gToForm (M1 (K1 c)) = fromList [(key, toQueryParam c)]
379+ gToForm _ opts (M1 (K1 c)) = fromList [(key, toQueryParam c)]
245− key = Text.pack $ selName (Proxy3 :: Proxy3 s g p)
381+ key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)
383+instance NotSupported ToForm t "is a sum type" => GToForm t (f :+: g) where gToForm = error "impossible"
247-- | Parse 'Form' into a value.
385-- | Parse 'Form' into a value.
@@ -254,9 +392,9 @@ instance (Selector s, ToHttpApiData c) => GToForm (M1 S s (K1 i c)) where
254-- , age :: Int }
392-- , age :: Int }
256-- instance 'FromForm' Person where
394-- instance 'FromForm' Person where
257−-- 'fromForm' (Form m) = Person
395+-- 'fromForm' f = Person
258−-- '<$>' 'parseUnique' "name" m
396+-- '<$>' 'parseUnique' "name" f
259−-- '<*>' 'parseUnique' "age" m
397+-- '<*>' 'parseUnique' "age" f
262-- Instead of manually writing @'FromForm'@ instances you can
400-- Instead of manually writing @'FromForm'@ instances you can
@@ -277,16 +415,17 @@ instance (Selector s, ToHttpApiData c) => GToForm (M1 S s (K1 i c)) where
277-- instance 'FromForm' Person
415-- instance 'FromForm' Person
418+-- The default implementation of 'fromForm' is 'genericFromForm'.
280−-- The default implementation will use 'parseQueryParam' for each field's value.
419+-- It only works for records and it will use 'parseQueryParam' for each field's value.
281class FromForm a where
420class FromForm a where
282 -- | Parse 'Form' into a value.
421 -- | Parse 'Form' into a value.
283 fromForm :: Form -> Either Text a
422 fromForm :: Form -> Either Text a
284− default fromForm :: (Generic a, GFromForm (Rep a))
423+ default fromForm :: (Generic a, GFromForm a (Rep a)) => Form -> Either Text a
285− => Form -> Either Text a
286− fromForm = genericFromForm
424+ fromForm = genericFromForm defaultFormOptions
288instance FromForm Form where fromForm = pure
426instance FromForm Form where fromForm = pure
428+-- | _NOTE:_ this conversion is unstable and may result in different key order (but not values).
290instance (FromFormKey k, FromHttpApiData v) => FromForm [(k, v)] where
429instance (FromFormKey k, FromHttpApiData v) => FromForm [(k, v)] where
291 fromForm = fmap (concatMap (\(k, vs) -> map ((,) k) vs)) . toEntriesByKey
430 fromForm = fmap (concatMap (\(k, vs) -> map ((,) k) vs)) . toEntriesByKey
@@ -300,157 +439,254 @@ instance FromHttpApiData v => FromForm (IntMap [v]) where
300 fromForm = fmap (IntMap.fromListWith (<>)) . toEntriesByKey
439 fromForm = fmap (IntMap.fromListWith (<>)) . toEntriesByKey
302-- | Parse a 'Form' into a list of entries groupped by key.
441-- | Parse a 'Form' into a list of entries groupped by key.
443+-- _NOTE:_ this conversion is unstable and may result in different key order
444+-- (but not values). For a stable encoding see 'toEntriesByKeyStable'.
303toEntriesByKey :: (FromFormKey k, FromHttpApiData v) => Form -> Either Text [(k, [v])]
445toEntriesByKey :: (FromFormKey k, FromHttpApiData v) => Form -> Either Text [(k, [v])]
304toEntriesByKey = traverse parseGroup . HashMap.toList . unForm
446toEntriesByKey = traverse parseGroup . HashMap.toList . unForm
306 parseGroup (k, vs) = (,) <$> parseFormKey k <*> traverse parseQueryParam vs
448 parseGroup (k, vs) = (,) <$> parseFormKey k <*> traverse parseQueryParam vs
450+-- | Parse a 'Form' into a list of entries groupped by key.
452+-- >>> toEntriesByKeyStable [("name", "Nick"), ("color", "red"), ("color", "white")] :: Either Text [(Text, [Text])]
453+-- Right [("color",["red","white"]),("name",["Nick"])]
455+-- For an unstable (but faster) conversion see 'toEntriesByKey'.
456+toEntriesByKeyStable :: (Ord k, FromFormKey k, FromHttpApiData v) => Form -> Either Text [(k, [v])]
457+toEntriesByKeyStable = fmap (sortOn fst) . toEntriesByKey
308-- | A 'Generic'-based implementation of 'fromForm'.
459-- | A 'Generic'-based implementation of 'fromForm'.
309-- This is used as a default implementation in 'FromForm'.
460-- This is used as a default implementation in 'FromForm'.
462+-- Note that this only works for records (i.e. product data types with named fields):
465+-- data Person = Person
468+-- } deriving ('Generic')
471+-- In this implementation each field's value gets decoded using `parseQueryParam`.
472+-- Two field types are exceptions:
474+-- - for values of type @'Maybe' a@ an entry is parsed if present in the 'Form'
475+-- and the is decoded with 'parseQueryParam'; if no entry is present result is 'Nothing';
477+-- - for values of type @[a]@ (except @['Char']@) all entries are parsed to produce a list of parsed values;
479+-- Here's an example:
484+-- , subtitle :: Maybe String
485+-- , comments :: [String]
486+-- } deriving ('Generic', 'Show')
488+-- instance 'FromForm' Post
491+-- >>> urlDecodeAsForm "comments=Nice%20post%21&comments=%2B1&title=Test" :: Either Text Post
492+-- Right (Post {title = "Test", subtitle = Nothing, comments = ["Nice post!","+1"]})
310−genericFromForm :: (Generic a, GFromForm (Rep a))
493+genericFromForm :: forall a. (Generic a, GFromForm a (Rep a)) => FormOptions -> Form -> Either Text a
311− => Form -> Either Text a
312−genericFromForm f = to <$> gFromForm f
494+genericFromForm opts f = to <$> gFromForm (Proxy :: Proxy a) opts f
496+class GFromForm t (f :: * -> *) where
497+ gFromForm :: Proxy t -> FormOptions -> Form -> Either Text (f x)
499+instance (GFromForm t f, GFromForm t g) => GFromForm t (f :*: g) where
500+ gFromForm p opts f = (:*:) <$> gFromForm p opts f <*> gFromForm p opts f
314−class GFromForm (f :: * -> *) where
502+instance GFromForm t f => GFromForm t (M1 D x f) where
315− gFromForm :: Form -> Either Text (f x)
503+ gFromForm p opts f = M1 <$> gFromForm p opts f
317−instance (GFromForm f, GFromForm g) => GFromForm (f :*: g) where
505+instance GFromForm t f => GFromForm t (M1 C x f) where
318− gFromForm f = (:*:) <$> gFromForm f <*> gFromForm f
506+ gFromForm p opts f = M1 <$> gFromForm p opts f
320−instance (GFromForm f, GFromForm g) => GFromForm (f :+: g) where
508+instance OVERLAPPABLE_ (Selector s, FromHttpApiData c) => GFromForm t (M1 S s (K1 i c)) where
321− gFromForm f
509+ gFromForm _ opts form = M1 . K1 <$> parseUnique key form
322− = fmap L1 (gFromForm f)
323− <!> fmap R1 (gFromForm f)
325− Left _ <!> y = y
511+ key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)
328−instance (Selector s, FromHttpApiData f) => GFromForm (M1 S s (K1 i f)) where
513+instance (Selector s, FromHttpApiData c) => GFromForm t (M1 S s (K1 i (Maybe c))) where
329− gFromForm form = M1 . K1 <$> parseUnique key form
514+ gFromForm _ opts form = M1 . K1 <$> parseMaybe key form
331− key = Text.pack $ selName (Proxy3 :: Proxy3 s g p)
516+ key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)
333−instance (GFromForm f) => GFromForm (M1 D x f) where
518+instance (Selector s, FromHttpApiData c) => GFromForm t (M1 S s (K1 i [c])) where
334− gFromForm f = M1 <$> gFromForm f
519+ gFromForm _ opts form = M1 . K1 <$> parseAll key form
521+ key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)
523+instance OVERLAPPING_ (Selector s) => GFromForm t (M1 S s (K1 i String)) where
524+ gFromForm _ opts form = M1 . K1 <$> parseUnique key form
526+ key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)
336−instance (GFromForm f) => GFromForm (M1 C x f) where
528+instance NotSupported FromForm t "is a sum type" => GFromForm t (f :+: g) where gFromForm = error "impossible"
337− gFromForm f = M1 <$> gFromForm f
339-- | Encode a 'Form' to an @application/x-www-form-urlencoded@ 'BSL.ByteString'.
530-- | Encode a 'Form' to an @application/x-www-form-urlencoded@ 'BSL.ByteString'.
532+-- _NOTE:_ this encoding is unstable and may result in different key order
533+-- (but not values). For a stable encoding see 'urlEncodeFormStable'.
534+urlEncodeForm :: Form -> BSL.ByteString
535+urlEncodeForm = urlEncodeParams . toList
537+-- | Encode a 'Form' to an @application/x-www-form-urlencoded@ 'BSL.ByteString'.
539+-- For an unstable (but faster) encoding see 'urlEncodeForm'.
341-- Key-value pairs get encoded to @key=value@ and separated by @&@:
541-- Key-value pairs get encoded to @key=value@ and separated by @&@:
343−-- >>> encodeForm [("name", "Julian"), ("lastname", "Arni")]
543+-- >>> urlEncodeFormStable [("name", "Julian"), ("lastname", "Arni")]
344-- "lastname=Arni&name=Julian"
544-- "lastname=Arni&name=Julian"
346-- Keys with empty values get encoded to just @key@ (without the @=@ sign):
546-- Keys with empty values get encoded to just @key@ (without the @=@ sign):
348−-- >>> encodeForm [("is_test", "")]
548+-- >>> urlEncodeFormStable [("is_test", "")]
349-- "is_test"
549-- "is_test"
351-- Empty keys are allowed too:
551-- Empty keys are allowed too:
353−-- >>> encodeForm [("", "foobar")]
553+-- >>> urlEncodeFormStable [("", "foobar")]
354-- "=foobar"
554-- "=foobar"
356−-- However, if not key and value are empty, the key-value pair is ignored.
556+-- However, if both key and value are empty, the key-value pair is ignored.
357−-- (This prevents @'decodeForm' . 'encodeForm'@ from being a true isomorphism).
557+-- (This prevents @'urlDecodeForm' . 'urlEncodeFormStable'@ from being a true isomorphism).
359−-- >>> encodeForm [("", "")]
559+-- >>> urlEncodeFormStable [("", "")]
362-- Everything is escaped with @'escapeURIString' 'isUnreserved'@:
562-- Everything is escaped with @'escapeURIString' 'isUnreserved'@:
364−-- >>> encodeForm [("fullname", "Andres Löh")]
564+-- >>> urlEncodeFormStable [("fullname", "Andres Löh")]
365-- "fullname=Andres%20L%C3%B6h"
565-- "fullname=Andres%20L%C3%B6h"
366−encodeForm :: Form -> BSL.ByteString
566+urlEncodeFormStable :: Form -> BSL.ByteString
367−encodeForm xs = BSL.intercalate "&" $ map (BSL.fromStrict . encodePair) $ toList xs
567+urlEncodeFormStable = urlEncodeParams . sortOn fst . toList
569+-- | Encode a list of key-value pairs to an @application/x-www-form-urlencoded@ 'BSL.ByteString'.
571+-- See also 'urlEncodeFormStable'.
572+urlEncodeParams :: [(Text, Text)] -> BSL.ByteString
573+urlEncodeParams = toLazyByteString . mconcat . intersperse (shortByteString "&") . map encodePair
369− escape = Text.encodeUtf8 . Text.pack . escapeURIString isUnreserved . Text.unpack
575+ escape = urlEncodeBuilder True . Text.encodeUtf8
371 encodePair (k, "") = escape k
577 encodePair (k, "") = escape k
372− encodePair (k, v) = escape k <> "=" <> escape v
578+ encodePair (k, v) = escape k <> shortByteString "=" <> escape v
375-- | Decode an @application/x-www-form-urlencoded@ 'BSL.ByteString' to a 'Form'.
580-- | Decode an @application/x-www-form-urlencoded@ 'BSL.ByteString' to a 'Form'.
377-- Key-value pairs get decoded normally:
582-- Key-value pairs get decoded normally:
379−-- >>> decodeForm "name=Greg&lastname=Weber"
584+-- >>> urlDecodeForm "name=Greg&lastname=Weber"
380-- Right (fromList [("lastname","Weber"),("name","Greg")])
585-- Right (fromList [("lastname","Weber"),("name","Greg")])
382-- Keys with no values get decoded to pairs with empty values.
587-- Keys with no values get decoded to pairs with empty values.
384−-- >>> decodeForm "is_test"
589+-- >>> urlDecodeForm "is_test"
385-- Right (fromList [("is_test","")])
590-- Right (fromList [("is_test","")])
387-- Empty keys are allowed:
592-- Empty keys are allowed:
389−-- >>> decodeForm "=foobar"
594+-- >>> urlDecodeForm "=foobar"
390-- Right (fromList [("","foobar")])
595-- Right (fromList [("","foobar")])
392-- The empty string gets decoded into an empty 'Form':
597-- The empty string gets decoded into an empty 'Form':
394−-- >>> decodeForm ""
599+-- >>> urlDecodeForm ""
395-- Right (fromList [])
600-- Right (fromList [])
397-- Everything is un-escaped with 'unEscapeString':
602-- Everything is un-escaped with 'unEscapeString':
399−-- >>> decodeForm "fullname=Andres%20L%C3%B6h"
604+-- >>> urlDecodeForm "fullname=Andres%20L%C3%B6h"
400-- Right (fromList [("fullname","Andres L\246h")])
605-- Right (fromList [("fullname","Andres L\246h")])
402-- Improperly formed strings result in an error:
607-- Improperly formed strings result in an error:
404−-- >>> decodeForm "this=has=too=many=equals"
609+-- >>> urlDecodeForm "this=has=too=many=equals"
405-- Left "not a valid pair: this=has=too=many=equals"
610-- Left "not a valid pair: this=has=too=many=equals"
406−decodeForm :: BSL.ByteString -> Either Text Form
611+urlDecodeForm :: BSL.ByteString -> Either Text Form
612+urlDecodeForm = fmap toForm . urlDecodeParams
614+-- | Decode an @application/x-www-form-urlencoded@ 'BSL.ByteString' to a list of key-value pairs.
616+-- See also 'urlDecodeForm'.
617+urlDecodeParams :: BSL.ByteString -> Either Text [(Text, Text)]
407−decodeForm bs = toForm <$> traverse parsePair pairs
618+urlDecodeParams bs = traverse parsePair pairs
409− pairs = map (Text.decodeUtf8With lenientDecode . BSL.toStrict) (BSL8.split '&' bs)
620+ pairs = map (BSL8.split '=') (BSL8.split '&' bs)
411− unescape = Text.pack . unEscapeString . Text.unpack . Text.replace "+" "%20"
622+ unescape = Text.decodeUtf8With lenientDecode . urlDecode True . BSL.toStrict
413− parsePair :: Text -> Either Text (Text, Text)
414 parsePair p =
624 parsePair p =
415− case Text.splitOn "=" p of
625+ case map unescape p of
416− [k, v] -> return (unescape k, unescape v)
626+ [k, v] -> return (k, v)
417− [k] -> return (unescape k, "" )
627+ [k] -> return (k, "")
418− _ -> Left $ "not a valid pair: " <> p
628+ xs -> Left $ "not a valid pair: " <> Text.intercalate "=" xs
420−data Proxy3 a b c = Proxy3
422-- | This is a convenience function for decoding a
631-- | This is a convenience function for decoding a
423-- @application/x-www-form-urlencoded@ 'BSL.ByteString' directly to a datatype
632-- @application/x-www-form-urlencoded@ 'BSL.ByteString' directly to a datatype
424-- that has an instance of 'FromForm'.
633-- that has an instance of 'FromForm'.
426−-- This is effectively @'fromForm' '<=<' 'decodeForm'@.
635+-- This is effectively @'fromForm' '<=<' 'urlDecodeForm'@.
428−-- >>> decodeAsForm "name=Dennis&age=22" :: Either Text Person
637+-- >>> urlDecodeAsForm "name=Dennis&age=22" :: Either Text Person
429-- Right (Person {name = "Dennis", age = 22})
638-- Right (Person {name = "Dennis", age = 22})
430−decodeAsForm :: FromForm a => BSL.ByteString -> Either Text a
639+urlDecodeAsForm :: FromForm a => BSL.ByteString -> Either Text a
431−decodeAsForm = fromForm <=< decodeForm
640+urlDecodeAsForm = fromForm <=< urlDecodeForm
433-- | This is a convenience function for encoding a datatype that has instance
642-- | This is a convenience function for encoding a datatype that has instance
434-- of 'ToForm' directly to a @application/x-www-form-urlencoded@
643-- of 'ToForm' directly to a @application/x-www-form-urlencoded@
435-- 'BSL.ByteString'.
644-- 'BSL.ByteString'.
437−-- This is effectively @'encodeForm' . 'toForm'@.
646+-- This is effectively @'urlEncodeForm' . 'toForm'@.
648+-- _NOTE:_ this encoding is unstable and may result in different key order
649+-- (but not values). For a stable encoding see 'urlEncodeAsFormStable'.
650+urlEncodeAsForm :: ToForm a => a -> BSL.ByteString
651+urlEncodeAsForm = urlEncodeForm . toForm
653+-- | This is a convenience function for encoding a datatype that has instance
654+-- of 'ToForm' directly to a @application/x-www-form-urlencoded@
657+-- This is effectively @'urlEncodeFormStable' . 'toForm'@.
439−-- >>> encodeAsForm Person {name = "Dennis", age = 22}
659+-- >>> urlEncodeAsFormStable Person {name = "Dennis", age = 22}
440-- "age=22&name=Dennis"
660-- "age=22&name=Dennis"
441−encodeAsForm :: ToForm a => a -> BSL.ByteString
661+urlEncodeAsFormStable :: ToForm a => a -> BSL.ByteString
442−encodeAsForm = encodeForm . toForm
662+urlEncodeAsFormStable = urlEncodeFormStable . toForm
444-- | Find all values corresponding to a given key in a 'Form'.
664-- | Find all values corresponding to a given key in a 'Form'.
446−-- >>> lookupKey "name" []
666+-- >>> lookupAll "name" []
448−-- >>> lookupKey "name" [("name", "Oleg")]
668+-- >>> lookupAll "name" [("name", "Oleg")]
449-- ["Oleg"]
669-- ["Oleg"]
450−-- >>> lookupKey "name" [("name", "Oleg"), ("name", "David")]
670+-- >>> lookupAll "name" [("name", "Oleg"), ("name", "David")]
451-- ["Oleg","David"]
671-- ["Oleg","David"]
452−lookupKey :: Text -> Form -> [Text]
672+lookupAll :: Text -> Form -> [Text]
453−lookupKey key = concat . HashMap.lookup key . unForm
673+lookupAll key = F.concat . HashMap.lookup key . unForm
675+-- | Lookup an optional value for a key.
676+-- Fail if there is more than one value.
678+-- >>> lookupMaybe "name" []
680+-- >>> lookupMaybe "name" [("name", "Oleg")]
681+-- Right (Just "Oleg")
682+-- >>> lookupMaybe "name" [("name", "Oleg"), ("name", "David")]
683+-- Left "Duplicate key \"name\""
684+lookupMaybe :: Text -> Form -> Either Text (Maybe Text)
685+lookupMaybe key form =
686+ case lookupAll key form of
688+ [v] -> pure (Just v)
689+ _ -> Left $ "Duplicate key " <> Text.pack (show key)
455-- | Lookup a unique value for a key.
691-- | Lookup a unique value for a key.
456-- Fail if there is zero or more than one value.
692-- Fail if there is zero or more than one value.
@@ -462,13 +698,43 @@ lookupKey key = concat . HashMap.lookup key . unForm
462-- >>> lookupUnique "name" [("name", "Oleg"), ("name", "David")]
698-- >>> lookupUnique "name" [("name", "Oleg"), ("name", "David")]
463-- Left "Duplicate key \"name\""
699-- Left "Duplicate key \"name\""
464lookupUnique :: Text -> Form -> Either Text Text
700lookupUnique :: Text -> Form -> Either Text Text
465−lookupUnique key form =
701+lookupUnique key form = do
702+ mv <- lookupMaybe key form
466− case lookupKey key form of
703+ case mv of
467− [v] -> pure v
704+ Just v -> pure v
468− [] -> Left $ "Could not find key " <> Text.pack (show key)
705+ Nothing -> Left $ "Could not find key " <> Text.pack (show key)
707+-- | Lookup all values for a given key in a 'Form' and parse them with 'parseQueryParams'.
709+-- >>> parseAll "age" [] :: Either Text [Word8]
711+-- >>> parseAll "age" [("age", "8"), ("age", "seven")] :: Either Text [Word8]
712+-- Left "could not parse: `seven' (input does not start with a digit)"
713+-- >>> parseAll "age" [("age", "8"), ("age", "777")] :: Either Text [Word8]
714+-- Left "out of bounds: `777' (should be between 0 and 255)"
715+-- >>> parseAll "age" [("age", "12"), ("age", "25")] :: Either Text [Word8]
717+parseAll :: FromHttpApiData v => Text -> Form -> Either Text [v]
718+parseAll key = parseQueryParams . lookupAll key
720+-- | Lookup an optional value for a given key and parse it with 'parseQueryParam'.
721+-- Fail if there is more than one value for the key.
723+-- >>> parseMaybe "age" [] :: Either Text (Maybe Word8)
725+-- >>> parseMaybe "age" [("age", "12"), ("age", "25")] :: Either Text (Maybe Word8)
469− _ -> Left $ "Duplicate key " <> Text.pack (show key)
726+-- Left "Duplicate key \"age\""
727+-- >>> parseMaybe "age" [("age", "seven")] :: Either Text (Maybe Word8)
728+-- Left "could not parse: `seven' (input does not start with a digit)"
729+-- >>> parseMaybe "age" [("age", "777")] :: Either Text (Maybe Word8)
730+-- Left "out of bounds: `777' (should be between 0 and 255)"
731+-- >>> parseMaybe "age" [("age", "7")] :: Either Text (Maybe Word8)
733+parseMaybe :: FromHttpApiData v => Text -> Form -> Either Text (Maybe v)
734+parseMaybe key = parseQueryParams <=< lookupMaybe key
471-- | Lookup a unique value for a given key and parse it with 'parseQueryParam'.
736-- | Lookup a unique value for a given key and parse it with 'parseQueryParam'.
737+-- Fail if there is zero or more than one value for the key.
473-- >>> parseUnique "age" [] :: Either Text Word8
739-- >>> parseUnique "age" [] :: Either Text Word8
474-- Left "Could not find key \"age\""
740-- Left "Could not find key \"age\""
@@ -483,16 +749,48 @@ lookupUnique key form =
483parseUnique :: FromHttpApiData v => Text -> Form -> Either Text v
749parseUnique :: FromHttpApiData v => Text -> Form -> Either Text v
484parseUnique key form = lookupUnique key form >>= parseQueryParam
750parseUnique key form = lookupUnique key form >>= parseQueryParam
486−-- | Lookup all values for a given key in a 'Form' and parse them with 'parseQueryParams'.
752+-- | 'Generic'-based deriving options for 'ToForm' and 'FromForm'.
488−-- >>> parseAll "age" [] :: Either Text [Word8]
754+-- A common use case for non-default 'FormOptions'
489−-- Right []
755+-- is to strip a prefix off of field labels:
490−-- >>> parseAll "age" [("age", "8"), ("age", "seven")] :: Either Text [Word8]
756+--
491−-- Left "could not parse: `seven' (input does not start with a digit)"
757+-- @
492−-- >>> parseAll "age" [("age", "8"), ("age", "777")] :: Either Text [Word8]
758+-- data Project = Project
493−-- Left "out of bounds: `777' (should be between 0 and 255)"
759+-- { projectName :: String
494−-- >>> parseAll "age" [("age", "12"), ("age", "25")] :: Either Text [Word8]
760+-- , projectSize :: Int
495−-- Right [12,25]
761+-- } deriving ('Generic', 'Show')
496−parseAll :: FromHttpApiData v => Text -> Form -> Either Text [v]
762+--
497−parseAll key = parseQueryParams . lookupKey key
763+-- myOptions :: 'FormOptions'
764+-- myOptions = 'FormOptions'
765+-- { 'fieldLabelModifier' = 'map' 'toLower' . 'drop' ('length' \"project\") }
767+-- instance 'ToForm' Project where
768+-- 'toForm' = 'genericToForm' myOptions
770+-- instance 'FromForm' Project where
771+-- 'fromForm' = 'genericFromForm' myOptions
774+-- >>> urlEncodeAsFormStable Project { projectName = "http-api-data", projectSize = 172 }
775+-- "name=http-api-data&size=172"
776+-- >>> urlDecodeAsForm "name=http-api-data&size=172" :: Either Text Project
777+-- Right (Project {projectName = "http-api-data", projectSize = 172})
778+data FormOptions = FormOptions
779+ { -- | Function applied to field labels. Handy for removing common record prefixes for example.
780+ fieldLabelModifier :: String -> String
783+-- | Default encoding 'FormOptions'.
787+-- { 'fieldLabelModifier' = id
790+defaultFormOptions :: FormOptions
791+defaultFormOptions = FormOptions
792+ { fieldLabelModifier = id
795+sortOn :: Ord b => (a -> b) -> [a] -> [a]
796+sortOn f = sortBy (comparing f)