git.iko.soy/typeable/http-api-data
on master help sign in
Move FormUrlEncoded from servant.

This commit moves ToFormUrlEncoded and FromFormUrlEncoded from servant to http-api-data. The classes are renamed ToForm and FromForm, and gain generic instances.

commit1b468b014d663979aa8cff20c78a1c80b894c629 authorJulian K. Arni <jkarni@gmail.com> dateThu 21 Jan 2016, 17:25 (11 years ago) parent035ed1f
8 files changed+434−138
http-api-data.cabal+16−3 src/Web/HttpApiData.hs+8−1 src/Web/HttpApiData/Internal/FormUrlEncoded.hs+158 src/Web/HttpApiData/Internal.hs → src/Web/HttpApiData/Internal/HttpApiData.hs+2−2 test/Spec.hs+1−132 test/Web/HttpApiData/Internal/FormUrlEncodedSpec.hs+70 test/Web/HttpApiData/Internal/HttpApiDataSpec.hs+73 test/Web/HttpApiData/Internal/TestInstances.hs+106
http-api-data.cabal+16−3
@@ -9,7 +9,7 @@ description: Please see README.md
99homepage: http://github.com/fizruk/http-api-data
1010category: Web
1111stability: unstable
12−cabal-version: >= 1.8
12+cabal-version: >= 1.10
1313build-type: Custom
1414extra-source-files:
1515 test/*.hs
@@ -25,25 +25,38 @@ library
2525 build-depends: base >= 4.6 && < 4.10
2626 , text >= 0.5
2727 , bytestring
28+ , containers
29+ , mtl
2830 , time
2931 , time-locale-compat >=0.1.1.0 && <0.2
32+ , network-uri >= 2.6 && < 2.7
33+ , string-conversions
3034 if flag(use-text-show)
3135 cpp-options: -DUSE_TEXT_SHOW
3236 build-depends: text-show >= 2
3337 exposed-modules:
3438 Web.HttpApiData
35− Web.HttpApiData.Internal
39+ Web.HttpApiData.Internal.FormUrlEncoded
40+ Web.HttpApiData.Internal.HttpApiData
3641 ghc-options: -Wall
42+ default-language: Haskell2010
3743
3844test-suite spec
3945 type: exitcode-stdio-1.0
4046 main-is: Spec.hs
41− hs-source-dirs: test/
47+ other-modules:
48+ Web.HttpApiData.Internal.FormUrlEncodedSpec
49+ Web.HttpApiData.Internal.HttpApiDataSpec
50+ Web.HttpApiData.Internal.TestInstances
51+ hs-source-dirs: test
4252 ghc-options: -Wall
53+ default-language: Haskell2010
4354 build-depends: HUnit
4455 , hspec >= 1.3
4556 , base >= 4 && < 5
57+ , bytestring
4658 , QuickCheck
59+ , containers
4760 , http-api-data
4861 , text
4962 , time
src/Web/HttpApiData.hs+8−1
@@ -8,6 +8,8 @@ module Web.HttpApiData (
88 -- * Classes
99 ToHttpApiData (..),
1010 FromHttpApiData (..),
11+ ToForm (..),
12+ FromForm (..),
1113
1214 -- * @'Maybe'@ parsers
1315 parseUrlPieceMaybe,
@@ -35,12 +37,17 @@ module Web.HttpApiData (
3537 parseBoundedEnumOfI,
3638 parseBoundedTextData,
3739
40+ -- * Encoding and decoding forms,
41+ encodeForm,
42+ decodeForm,
43+
3844 -- * Other helpers
3945 showTextData,
4046 readTextData,
4147) where
4248
43−import Web.HttpApiData.Internal
49+import Web.HttpApiData.Internal.FormUrlEncoded
50+import Web.HttpApiData.Internal.HttpApiData
4451
4552-- $setup
4653--
src/Web/HttpApiData/Internal/FormUrlEncoded.hsnew file+158
@@ -0,0 +1,158 @@
1+{-# LANGUAGE DefaultSignatures #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6+{-# LANGUAGE KindSignatures #-}
7+{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE ScopedTypeVariables #-}
10+{-# LANGUAGE TypeFamilies #-}
11+{-# LANGUAGE TypeOperators #-}
12+module Web.HttpApiData.Internal.FormUrlEncoded where
13+
14+import Control.Arrow (first, left, second)
15+import Control.Monad.State
16+import qualified Data.ByteString.Lazy as BSL
17+import qualified Data.Map as M
18+import Data.Monoid
19+import Data.String.Conversions (cs)
20+import qualified Data.Text as T
21+import GHC.Exts (IsList (..))
22+import GHC.Generics
23+import Network.URI (escapeURIString, isUnreserved,
24+ unEscapeString)
25+
26+import Web.HttpApiData.Internal.HttpApiData
27+
28+-- | The contents of a form, not yet url-encoded.
29+newtype Form = Form { unForm :: M.Map T.Text T.Text }
30+ deriving (Eq, Show, Read, Generic, Monoid)
31+
32+instance IsList Form where
33+ type Item Form = (T.Text, T.Text)
34+ fromList = Form . fromList
35+ toList = toList . unForm
36+
37+-- | A type that can be converted to @application/x-www-form-urlencoded@
38+class ToForm a where
39+ toForm :: a -> Form
40+ default toForm :: (Generic a, GToForm (Rep a)) => a -> Form
41+ toForm = genericToForm
42+
43+instance ToForm [(T.Text, T.Text)] where toForm = Form . M.fromList
44+instance ToForm (M.Map T.Text T.Text) where toForm = Form
45+instance ToForm Form where toForm = id
46+
47+genericToForm :: (Generic a, GToForm (Rep a)) => a -> Form
48+genericToForm x = evalState (unGenericToFormS . gToForm $ from x) (Nothing, 0)
49+
50+-- | A datatype for generically generating forms. The state keeps track of the
51+-- record name currently being inspected (if any) and the field number.
52+newtype GenericToFormS a = GenericToFormS {
53+ unGenericToFormS :: State (Maybe T.Text, Int) a
54+ } deriving (Functor, Applicative, Generic, Monad, MonadState (Maybe T.Text, Int))
55+
56+class GToForm (f :: * -> *) where
57+ gToForm :: f x -> GenericToFormS Form
58+
59+instance (GToForm f, GToForm g) => GToForm (f :*: g) where
60+ gToForm (a :*: b) = do
61+ a' <- gToForm a
62+ modify $ second (+ 1)
63+ b' <- gToForm b
64+ return (a' <> b')
65+
66+instance (GToForm f, GToForm g) => GToForm (f :+: g) where
67+ gToForm (L1 a) = gToForm a
68+ gToForm (R1 a) = gToForm a
69+
70+instance (GToForm f) => GToForm (M1 D x f) where
71+ gToForm (M1 a) = gToForm a
72+
73+instance (GToForm f) => GToForm (M1 C x f) where
74+ gToForm (M1 a) = gToForm a
75+
76+instance {-# OVERLAPPABLE #-} (Selector sel, GToForm f)
77+ => GToForm (M1 S sel f) where
78+ gToForm (M1 a) = modify (first $ const sel) >> gToForm a
79+ where sel = Just . T.pack $ selName (Proxy3 :: Proxy3 sel g p)
80+
81+instance (ToHttpApiData f) => GToForm (K1 i f) where
82+ gToForm (K1 a) = do
83+ s <- get
84+ return . Form $ M.insert (name s) (toUrlPiece a) $ mempty
85+ where name (Nothing, i) = T.pack $ show i
86+ name (Just x, _) = x
87+
88+-- | A type that can be converted from @application/x-www-form-urlencoded@,
89+-- with the possibility of failure.
90+class FromForm a where
91+ fromForm :: Form -> Either String a
92+ default fromForm :: (Generic a, GFromForm (Rep a))
93+ => Form -> Either String a
94+ fromForm = genericFromForm
95+
96+instance FromForm Form where fromForm = return
97+instance FromForm [(T.Text, T.Text)] where fromForm = return . M.toList . unForm
98+instance FromForm (M.Map T.Text T.Text) where fromForm = return . unForm
99+
100+genericFromForm :: (Generic a, GFromForm (Rep a))
101+ => Form -> Either String a
102+genericFromForm f = to <$> gFromForm f
103+
104+class GFromForm (f :: * -> *) where
105+ gFromForm :: Form -> Either String (f x)
106+
107+instance (GFromForm f, GFromForm g)
108+ => GFromForm (f :*: g) where
109+ gFromForm f = (:*:) <$> gFromForm f <*> gFromForm f
110+
111+instance (GFromForm f, GFromForm g)
112+ => GFromForm (f :+: g) where
113+ gFromForm f = case gFromForm f of
114+ Right a -> return $ L1 a
115+ Left e1 -> case gFromForm f of
116+ Right b -> return $ R1 b
117+ Left e2 -> Left (e1 <> e2)
118+
119+instance (Selector sel, FromHttpApiData f)
120+ => GFromForm (M1 S sel (K1 i f)) where
121+ gFromForm f = case M.lookup sel (unForm f) of
122+ Nothing -> Left . T.unpack $ "Could not find key " <> sel
123+ Just v -> left T.unpack $ M1 . K1 <$> parseUrlPiece v
124+ where sel = T.pack $ selName (Proxy3 :: Proxy3 sel g p)
125+
126+instance (GFromForm f) => GFromForm (M1 D x f) where
127+ gFromForm f = M1 <$> gFromForm f
128+
129+instance (GFromForm f) => GFromForm (M1 C x f) where
130+ gFromForm f = M1 <$> gFromForm f
131+
132+encodeForm :: Form -> BSL.ByteString
133+encodeForm xs =
134+ let escape :: T.Text -> BSL.ByteString
135+ escape = cs . escapeURIString isUnreserved . cs
136+ encodePair :: (T.Text, T.Text) -> BSL.ByteString
137+ encodePair (k, "") = escape k
138+ encodePair (k, v) = escape k <> "=" <> escape v
139+ in BSL.intercalate "&" $ map encodePair $ toList xs
140+
141+decodeForm :: BSL.ByteString -> Either String Form
142+decodeForm "" = return mempty
143+decodeForm q = do
144+ let xs :: [T.Text]
145+ xs = T.splitOn "&" . cs $ q
146+ parsePair :: T.Text -> Either String (T.Text, T.Text)
147+ parsePair p =
148+ case T.splitOn "=" p of
149+ [k,v] -> return ( unescape k
150+ , unescape v
151+ )
152+ [k] -> return ( unescape k, "" )
153+ _ -> Left $ "not a valid pair: " <> cs p
154+ unescape :: T.Text -> T.Text
155+ unescape = cs . unEscapeString . cs . T.intercalate "%20" . T.splitOn "+"
156+ toForm <$> mapM parsePair xs
157+
158+data Proxy3 a b c = Proxy3
src/Web/HttpApiData/Internal.hs → src/Web/HttpApiData/Internal/HttpApiData.hsrenamed+2−2
@@ -7,7 +7,7 @@
77-- |
88-- Convert Haskell values to and from HTTP API data
99-- such as URL pieces, headers and query parameters.
10−module Web.HttpApiData.Internal where
10+module Web.HttpApiData.Internal.HttpApiData where
1111
1212#if __GLASGOW_HASKELL__ < 710
1313import Control.Applicative
@@ -157,7 +157,7 @@ parseMaybeTextData parse input =
157157--
158158-- @
159159-- data MyData = Foo | Bar | Baz deriving (Generic)
160−--
160+--
161161-- instance TextShow MyData where
162162-- showt = genericShowt
163163--
test/Spec.hs+1−132
@@ -1,132 +1 @@
1−{-# Language ScopedTypeVariables #-}
2−{-# OPTIONS_GHC -fno-warn-orphans #-}
3−module Main where
4−
5−import Control.Applicative
6−
7−import Data.Int
8−import Data.Char
9−import Data.Word
10−import Data.Time
11−import qualified Data.Text as T
12−import qualified Data.Text.Lazy as L
13−import qualified Data.ByteString as BS
14−import Data.Version
15−
16−import Test.Hspec
17−import Test.Hspec.QuickCheck(prop)
18−import Test.QuickCheck
19−
20−import Web.HttpApiData
21−
22−instance Arbitrary T.Text where
23− arbitrary = T.pack <$> arbitrary
24−
25−instance Arbitrary L.Text where
26− arbitrary = L.pack <$> arbitrary
27−
28−instance Arbitrary Day where
29− arbitrary = liftA3 fromGregorian (fmap abs arbitrary) arbitrary arbitrary
30−
31−instance Arbitrary LocalTime where
32− arbitrary = LocalTime
33− <$> arbitrary
34− <*> liftA3 TimeOfDay (choose (0, 23)) (choose (0, 59)) (fromInteger <$> choose (0, 60))
35−
36−instance Eq ZonedTime where
37− ZonedTime t (TimeZone x _ _) == ZonedTime t' (TimeZone y _ _) = t == t' && x == y
38−
39−instance Arbitrary ZonedTime where
40− arbitrary = ZonedTime
41− <$> arbitrary
42− <*> liftA3 TimeZone arbitrary arbitrary (vectorOf 3 (elements ['A'..'Z']))
43−
44−instance Arbitrary UTCTime where
45− arbitrary = UTCTime <$> arbitrary <*> fmap fromInteger (choose (0, 86400))
46−
47−instance Arbitrary NominalDiffTime where
48− arbitrary = fromInteger <$> arbitrary
49−
50−instance Arbitrary Version where
51− arbitrary = (version . map abs) <$> nonempty
52− where
53− version branch = Version branch []
54− nonempty = liftA2 (:) arbitrary arbitrary
55−
56−main :: IO ()
57−main = hspec spec
58−
59−(<=>) :: Eq a => (a -> b) -> (b -> Either T.Text a) -> a -> Bool
60−(f <=> g) x = g (f x) == Right x
61−
62−data Proxy a = Proxy
63−
64−checkUrlPiece :: forall a. (Eq a, ToHttpApiData a, FromHttpApiData a, Show a, Arbitrary a) => Proxy a -> String -> Spec
65−checkUrlPiece _ name = prop name (toUrlPiece <=> parseUrlPiece :: a -> Bool)
66−
67−data RandomCase a = RandomCase [Bool] a
68−
69−instance ToHttpApiData a => Show (RandomCase a) where
70− show rc@(RandomCase _ x) = show (toUrlPiece rc) ++ " (original: " ++ show (toUrlPiece x) ++ ")"
71−
72−instance Eq a => Eq (RandomCase a) where
73− RandomCase _ x == RandomCase _ y = x == y
74−
75−instance Arbitrary a => Arbitrary (RandomCase a) where
76− arbitrary = liftA2 RandomCase nonempty arbitrary
77− where
78− nonempty = liftA2 (:) arbitrary arbitrary
79−
80−instance ToHttpApiData a => ToHttpApiData (RandomCase a) where
81− toUrlPiece (RandomCase us x) = T.pack (zipWith (\u -> if u then toUpper else toLower) (cycle us) (T.unpack (toUrlPiece x)))
82−
83−instance FromHttpApiData a => FromHttpApiData (RandomCase a) where
84− parseUrlPiece s = RandomCase [] <$> parseUrlPiece s
85−
86−-- | Check case insensitivity for @parseUrlPiece@.
87−checkUrlPieceI :: forall a. (Eq a, ToHttpApiData a, FromHttpApiData a, Show a, Arbitrary a) => Proxy a -> String -> Spec
88−checkUrlPieceI _ = checkUrlPiece (Proxy :: Proxy (RandomCase a))
89−
90−spec :: Spec
91−spec = do
92− describe "toUrlPiece <=> parseUrlPiece" $ do
93− checkUrlPiece (Proxy :: Proxy ()) "()"
94− checkUrlPiece (Proxy :: Proxy Char) "Char"
95− checkUrlPieceI (Proxy :: Proxy Bool) "Bool"
96− checkUrlPieceI (Proxy :: Proxy Ordering) "Ordering"
97− checkUrlPiece (Proxy :: Proxy Int) "Int"
98− checkUrlPiece (Proxy :: Proxy Int8) "Int8"
99− checkUrlPiece (Proxy :: Proxy Int16) "Int16"
100− checkUrlPiece (Proxy :: Proxy Int32) "Int32"
101− checkUrlPiece (Proxy :: Proxy Int64) "Int64"
102− checkUrlPiece (Proxy :: Proxy Integer) "Integer"
103− checkUrlPiece (Proxy :: Proxy Word) "Word"
104− checkUrlPiece (Proxy :: Proxy Word8) "Word8"
105− checkUrlPiece (Proxy :: Proxy Word16) "Word16"
106− checkUrlPiece (Proxy :: Proxy Word32) "Word32"
107− checkUrlPiece (Proxy :: Proxy Word64) "Word64"
108− checkUrlPiece (Proxy :: Proxy String) "String"
109− checkUrlPiece (Proxy :: Proxy T.Text) "Text.Strict"
110− checkUrlPiece (Proxy :: Proxy L.Text) "Text.Lazy"
111− checkUrlPiece (Proxy :: Proxy Day) "Day"
112− checkUrlPiece (Proxy :: Proxy LocalTime) "LocalTime"
113− checkUrlPiece (Proxy :: Proxy ZonedTime) "ZonedTime"
114− checkUrlPiece (Proxy :: Proxy UTCTime) "UTCTime"
115− checkUrlPiece (Proxy :: Proxy NominalDiffTime) "NominalDiffTime"
116− checkUrlPiece (Proxy :: Proxy Version) "Version"
117−
118− checkUrlPiece (Proxy :: Proxy (Maybe String)) "Maybe String"
119− checkUrlPieceI (Proxy :: Proxy (Maybe Integer)) "Maybe Integer"
120− checkUrlPiece (Proxy :: Proxy (Either Integer T.Text)) "Either Integer Text"
121− checkUrlPieceI (Proxy :: Proxy (Either Version Day)) "Either Version Day"
122−
123− it "bad integers are rejected" $ do
124− parseUrlPieceMaybe (T.pack "123hello") `shouldBe` (Nothing :: Maybe Int)
125−
126− it "bounds checking works" $ do
127− parseUrlPieceMaybe (T.pack "256") `shouldBe` (Nothing :: Maybe Int8)
128− parseUrlPieceMaybe (T.pack "-10") `shouldBe` (Nothing :: Maybe Word)
129−
130− it "invalid utf8 is handled" $ do
131− parseHeaderMaybe (BS.pack [128]) `shouldBe` (Nothing :: Maybe T.Text)
132−
1+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
test/Web/HttpApiData/Internal/FormUrlEncodedSpec.hsnew file+70
@@ -0,0 +1,70 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+module Web.HttpApiData.Internal.FormUrlEncodedSpec (spec) where
4+
5+import Test.Hspec
6+import qualified Data.Map as M
7+import Test.QuickCheck
8+
9+import Web.HttpApiData.Internal.FormUrlEncoded
10+import Web.HttpApiData.Internal.HttpApiData
11+import Web.HttpApiData.Internal.TestInstances
12+
13+spec :: Spec
14+spec = do
15+ genericSpec
16+ urlEncoding
17+
18+genericSpec :: Spec
19+genericSpec = describe "Default (generic) instances" $ do
20+
21+ context "ToForm" $ do
22+
23+ it "contains the record names" $ property $ \(x :: SimpleRec) -> do
24+ let f = unForm $ toForm x
25+ M.member "rec1" f `shouldBe` True
26+ M.member "rec2" f `shouldBe` True
27+
28+ it "contains the correct record values" $ property $ \(x :: SimpleRec) -> do
29+ let f = unForm $ toForm x
30+ M.lookup "rec1" f `shouldBe` Just (rec1 x)
31+ (parseUrlPiece <$> M.lookup "rec2" f) `shouldBe` Just (Right $ rec2 x)
32+
33+ context "for sum types" $ do
34+
35+ it "contains the correct records" $ property $ \x -> do
36+ let f = unForm $ toForm x
37+ case x of
38+ SSRLeft _ _ -> do
39+ parseUrlPiece <$> M.lookup "left1" f `shouldBe` Just (Right $ left1 x)
40+ parseUrlPiece <$> M.lookup "left2" f `shouldBe` Just (Right $ left2 x)
41+ SSRRight _ _ -> do
42+ parseUrlPiece <$> M.lookup "right1" f `shouldBe` Just (Right $ right1 x)
43+ parseUrlPiece <$> M.lookup "right2" f `shouldBe` Just (Right $ right2 x)
44+
45+
46+ context "FromForm" $ do
47+
48+ it "is the left inverse of ToForm" $ property $
49+ \(x :: SimpleRec, y :: SimpleSumRec) -> do
50+ fromForm (toForm x) `shouldBe` Right x
51+ fromForm (toForm y) `shouldBe` Right y
52+
53+ it "is the right inverse of ToForm" $ property $ \x (y :: Int) -> do
54+ let f1 = Form $ M.fromList [("rec1", x), ("rec2", toUrlPiece y)]
55+ Right r1 = fromForm f1 :: Either String SimpleRec
56+ toForm r1 `shouldBe` f1
57+ let f2 = Form $ M.fromList [("right1", x), ("right2", toUrlPiece y)]
58+ Right r2 = fromForm f2 :: Either String SimpleSumRec
59+ toForm r2 `shouldBe` f2
60+
61+ it "returns the underlying error" $ do
62+ let f = Form $ M.fromList [("rec1", "anything"), ("rec2", "bad")]
63+ Left e = fromForm f :: Either String SimpleRec
64+ e `shouldContain` "input does not start with a digit"
65+
66+urlEncoding :: Spec
67+urlEncoding = describe "urlEncoding" $ do
68+
69+ it "decodeForm (encodeForm x) == Right x" $ property $ \x -> do
70+ decodeForm (encodeForm x) `shouldBe` Right x
test/Web/HttpApiData/Internal/HttpApiDataSpec.hsnew file+73
@@ -0,0 +1,73 @@
1+{-# Language ScopedTypeVariables #-}
2+module Web.HttpApiData.Internal.HttpApiDataSpec (spec) where
3+
4+import Data.Int
5+import Data.Word
6+import Data.Time
7+import qualified Data.Text as T
8+import qualified Data.Text.Lazy as L
9+import qualified Data.ByteString as BS
10+import Data.Version
11+
12+import Data.Proxy
13+import Test.Hspec
14+import Test.Hspec.QuickCheck(prop)
15+import Test.QuickCheck
16+
17+import Web.HttpApiData.Internal.HttpApiData
18+
19+import Web.HttpApiData.Internal.TestInstances
20+
21+
22+(<=>) :: Eq a => (a -> b) -> (b -> Either T.Text a) -> a -> Bool
23+(f <=> g) x = g (f x) == Right x
24+
25+checkUrlPiece :: forall a. (Eq a, ToHttpApiData a, FromHttpApiData a, Show a, Arbitrary a) => Proxy a -> String -> Spec
26+checkUrlPiece _ name = prop name (toUrlPiece <=> parseUrlPiece :: a -> Bool)
27+
28+-- | Check case insensitivity for @parseUrlPiece@.
29+checkUrlPieceI :: forall a. (Eq a, ToHttpApiData a, FromHttpApiData a, Arbitrary a) => Proxy a -> String -> Spec
30+checkUrlPieceI _ = checkUrlPiece (Proxy :: Proxy (RandomCase a))
31+
32+spec :: Spec
33+spec = do
34+ describe "toUrlPiece <=> parseUrlPiece" $ do
35+ checkUrlPiece (Proxy :: Proxy ()) "()"
36+ checkUrlPiece (Proxy :: Proxy Char) "Char"
37+ checkUrlPieceI (Proxy :: Proxy Bool) "Bool"
38+ checkUrlPieceI (Proxy :: Proxy Ordering) "Ordering"
39+ checkUrlPiece (Proxy :: Proxy Int) "Int"
40+ checkUrlPiece (Proxy :: Proxy Int8) "Int8"
41+ checkUrlPiece (Proxy :: Proxy Int16) "Int16"
42+ checkUrlPiece (Proxy :: Proxy Int32) "Int32"
43+ checkUrlPiece (Proxy :: Proxy Int64) "Int64"
44+ checkUrlPiece (Proxy :: Proxy Integer) "Integer"
45+ checkUrlPiece (Proxy :: Proxy Word) "Word"
46+ checkUrlPiece (Proxy :: Proxy Word8) "Word8"
47+ checkUrlPiece (Proxy :: Proxy Word16) "Word16"
48+ checkUrlPiece (Proxy :: Proxy Word32) "Word32"
49+ checkUrlPiece (Proxy :: Proxy Word64) "Word64"
50+ checkUrlPiece (Proxy :: Proxy String) "String"
51+ checkUrlPiece (Proxy :: Proxy T.Text) "Text.Strict"
52+ checkUrlPiece (Proxy :: Proxy L.Text) "Text.Lazy"
53+ checkUrlPiece (Proxy :: Proxy Day) "Day"
54+ checkUrlPiece (Proxy :: Proxy LocalTime) "LocalTime"
55+ checkUrlPiece (Proxy :: Proxy ZonedTime) "ZonedTime"
56+ checkUrlPiece (Proxy :: Proxy UTCTime) "UTCTime"
57+ checkUrlPiece (Proxy :: Proxy NominalDiffTime) "NominalDiffTime"
58+ checkUrlPiece (Proxy :: Proxy Version) "Version"
59+
60+ checkUrlPiece (Proxy :: Proxy (Maybe String)) "Maybe String"
61+ checkUrlPieceI (Proxy :: Proxy (Maybe Integer)) "Maybe Integer"
62+ checkUrlPiece (Proxy :: Proxy (Either Integer T.Text)) "Either Integer Text"
63+ checkUrlPieceI (Proxy :: Proxy (Either Version Day)) "Either Version Day"
64+
65+ it "bad integers are rejected" $ do
66+ parseUrlPieceMaybe (T.pack "123hello") `shouldBe` (Nothing :: Maybe Int)
67+
68+ it "bounds checking works" $ do
69+ parseUrlPieceMaybe (T.pack "256") `shouldBe` (Nothing :: Maybe Int8)
70+ parseUrlPieceMaybe (T.pack "-10") `shouldBe` (Nothing :: Maybe Word)
71+
72+ it "invalid utf8 is handled" $ do
73+ parseHeaderMaybe (BS.pack [128]) `shouldBe` (Nothing :: Maybe T.Text)
test/Web/HttpApiData/Internal/TestInstances.hsnew file+106
@@ -0,0 +1,106 @@
1+{-# LANGUAGE DeriveAnyClass #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
4+{-# OPTIONS_GHC -fno-warn-orphans #-}
5+module Web.HttpApiData.Internal.TestInstances
6+ ( RandomCase(..)
7+ , SimpleRec(..)
8+ , SimpleSumRec(..)
9+ ) where
10+
11+import Control.Applicative
12+import qualified Data.ByteString.Lazy.Char8 as BSL
13+import Data.Char
14+import qualified Data.Text as T
15+import qualified Data.Text.Lazy as L
16+import Data.Time
17+import Data.Version
18+import GHC.Exts (fromList)
19+import GHC.Generics
20+
21+import Test.QuickCheck
22+
23+import Web.HttpApiData.Internal.FormUrlEncoded
24+import Web.HttpApiData.Internal.HttpApiData
25+
26+instance Arbitrary T.Text where
27+ arbitrary = T.pack <$> arbitrary
28+
29+instance Arbitrary L.Text where
30+ arbitrary = L.pack <$> arbitrary
31+
32+instance Arbitrary BSL.ByteString where
33+ arbitrary = BSL.pack <$> arbitrary
34+
35+instance Arbitrary Day where
36+ arbitrary = liftA3 fromGregorian (fmap abs arbitrary) arbitrary arbitrary
37+
38+instance Arbitrary LocalTime where
39+ arbitrary = LocalTime
40+ <$> arbitrary
41+ <*> liftA3 TimeOfDay (choose (0, 23)) (choose (0, 59)) (fromInteger <$> choose (0, 60))
42+
43+instance Eq ZonedTime where
44+ ZonedTime t (TimeZone x _ _) == ZonedTime t' (TimeZone y _ _) = t == t' && x == y
45+
46+instance Arbitrary ZonedTime where
47+ arbitrary = ZonedTime
48+ <$> arbitrary
49+ <*> liftA3 TimeZone arbitrary arbitrary (vectorOf 3 (elements ['A'..'Z']))
50+
51+instance Arbitrary UTCTime where
52+ arbitrary = UTCTime <$> arbitrary <*> fmap fromInteger (choose (0, 86400))
53+
54+instance Arbitrary NominalDiffTime where
55+ arbitrary = fromInteger <$> arbitrary
56+
57+instance Arbitrary Version where
58+ arbitrary = (version . map abs) <$> nonempty
59+ where
60+ version branch = Version branch []
61+ nonempty = liftA2 (:) arbitrary arbitrary
62+
63+instance Arbitrary Form where
64+ arbitrary = fromList <$> arbitrary
65+
66+data RandomCase a = RandomCase [Bool] a
67+
68+instance ToHttpApiData a => Show (RandomCase a) where
69+ show rc@(RandomCase _ x) = show (toUrlPiece rc) ++ " (original: " ++ show (toUrlPiece x) ++ ")"
70+
71+instance Eq a => Eq (RandomCase a) where
72+ RandomCase _ x == RandomCase _ y = x == y
73+
74+instance Arbitrary a => Arbitrary (RandomCase a) where
75+ arbitrary = liftA2 RandomCase nonempty arbitrary
76+ where
77+ nonempty = liftA2 (:) arbitrary arbitrary
78+
79+instance ToHttpApiData a => ToHttpApiData (RandomCase a) where
80+ toUrlPiece (RandomCase us x) = T.pack (zipWith (\u -> if u then toUpper else toLower) (cycle us) (T.unpack (toUrlPiece x)))
81+
82+instance FromHttpApiData a => FromHttpApiData (RandomCase a) where
83+ parseUrlPiece s = RandomCase [] <$> parseUrlPiece s
84+
85+data SimpleRec = SimpleRec { rec1 :: T.Text, rec2 :: Int }
86+ deriving (Eq, Show, Read, Generic)
87+
88+instance ToForm SimpleRec
89+instance FromForm SimpleRec
90+
91+instance Arbitrary SimpleRec where
92+ arbitrary = SimpleRec <$> arbitrary <*> arbitrary
93+
94+data SimpleSumRec
95+ = SSRLeft { left1 :: Int, left2 :: Bool }
96+ | SSRRight { right1 :: T.Text, right2 :: Int}
97+ deriving (Eq, Show, Read, Generic)
98+
99+instance ToForm SimpleSumRec
100+instance FromForm SimpleSumRec
101+
102+instance Arbitrary SimpleSumRec where
103+ arbitrary = oneof
104+ [ SSRLeft <$> arbitrary <*> arbitrary
105+ , SSRRight <$> arbitrary <*> arbitrary
106+ ]