http-api-data 0.4.3 → 0.7
raw patch · 8 files changed
Files
- CHANGELOG.md +48/−0
- http-api-data.cabal +25/−46
- include/overlapping-compat.h +0/−8
- src/Web/Internal/FormUrlEncoded.hs +38/−39
- src/Web/Internal/HttpApiData.hs +118/−87
- test/Web/Internal/FormUrlEncodedSpec.hs +8/−14
- test/Web/Internal/HttpApiDataSpec.hs +4/−3
- test/Web/Internal/TestInstances.hs +3/−3
CHANGELOG.md view
@@ -1,3 +1,51 @@+0.7++* Change Form representation to use Map (from `containers`) instead of `HashMap`.+ This prevents possible HashDOS attacks.++0.6.3++* Add To/FromForm instances for () and Void++0.6.2++* Support GHC-9.12+* Require `cookie-0.5.1`++0.6.1++* Require at least GHC-8.6+* Support `cookie-0.5.0`, see [#137](https://github.com/fizruk/http-api-data/pull/137).++ Note that `cookie-0.5.0`'s parser now removes double quotes around cookie values.++0.6+---++* Use [`text-iso8601`](https://hackage.haskell.org/package/text-iso8601)+ to parse and serialise `time` types. (Instead of `attoparsec-iso8601`).+ Due this change some formats are slightly changed:++ - Space between timezone is not accepted+ - Timezone offset can be between -23:59..23:59+ - Timezone offset is output with colon between hours and minutes++* Require at least GHC-8.2++0.5.1+-----++* Add `toEncodedQueryParam` to `ToHttpApiData` type class. It has default+ implementation using `toQueryParam`, but may be overriden with more efficient+ one.++0.5+---++* Use `attoparsec-iso8601-1.1.0.0`.+ `Day` and `UTCTime` parsers require at least 4 digits now, which is a breaking change.+* Drop GHC-7.8 and GHC-7.10 support.+ 0.4.3 -----
http-api-data.cabal view
@@ -1,6 +1,6 @@-cabal-version: >= 1.10+cabal-version: 1.12 name: http-api-data-version: 0.4.3+version: 0.7 synopsis: Converting to/from HTTP API data like URL pieces, headers and query parameters. category: Web@@ -18,20 +18,21 @@ build-type: Simple extra-source-files:- include/overlapping-compat.h test/*.hs CHANGELOG.md README.md tested-with:- GHC==7.8.4,- GHC==7.10.3,- GHC==8.0.2,- GHC==8.2.2,- GHC==8.4.4, GHC==8.6.5, GHC==8.8.4,- GHC==8.10.3+ GHC==8.10.7,+ GHC==9.0.2,+ GHC==9.2.8,+ GHC==9.4.8,+ GHC==9.6.7,+ GHC==9.8.4,+ GHC==9.10.1,+ GHC==9.12.2 flag use-text-show description: Use text-show library for efficient ToHttpApiData implementations.@@ -40,43 +41,26 @@ library hs-source-dirs: src/- include-dirs: include/ -- GHC bundled- build-depends: base >= 4.7 && < 4.15- , bytestring >= 0.10.4.0 && < 0.12- , containers >= 0.5.5.1 && < 0.7- , text >= 1.2.3.0 && < 1.3- , transformers >= 0.3 && < 0.6-- -- so Semigroup Builder exists- if impl(ghc >= 8.0)- build-depends: bytestring >= 0.10.8.1+ build-depends: base >= 4.12.0.0 && < 4.22+ , bytestring >= 0.10.8.2 && < 0.13+ , containers >= 0.6.0.1 && < 0.8+ , text >= 1.2.3.0 && < 1.3 || >=2.0 && <2.2 -- other-dependencies build-depends:- attoparsec >= 0.13.2.2 && < 0.14- , attoparsec-iso8601 >= 1.0.2.0 && < 1.1- , base-compat >= 0.10.5 && < 0.12- , cookie >= 0.4.3 && < 0.5- , hashable >= 1.2.7.0 && < 1.4- , http-types >= 0.12.3 && < 0.13- , tagged >= 0.8.5 && < 0.9+ cookie >= 0.5.1 && < 0.6+ , hashable >= 1.4.4.0 && < 1.6+ , http-types >= 0.12.4 && < 0.13+ , text-iso8601 >= 0.1.1 && < 0.2+ , tagged >= 0.8.8 && < 0.9 , time-compat >= 1.9.5 && < 1.10- , unordered-containers >= 0.2.10.0 && < 0.3- , uuid-types >= 1.0.3 && < 1.1-- if !impl(ghc >= 7.10)- build-depends:- nats >= 1.1.2 && < 1.2,- void >= 0.7.3 && < 0.8-- if !impl(ghc >= 8.0)- build-depends: semigroups >= 0.18.5 && < 0.20+ , uuid-types >= 1.0.6 && < 1.1 if flag(use-text-show) cpp-options: -DUSE_TEXT_SHOW- build-depends: text-show >= 3.8.2 && <3.9+ build-depends: text-show >= 3.10.5 && <3.12 exposed-modules: Web.HttpApiData@@ -96,25 +80,20 @@ hs-source-dirs: test ghc-options: -Wall default-language: Haskell2010- build-tool-depends: hspec-discover:hspec-discover >= 2.7.1 && <2.8+ build-tool-depends: hspec-discover:hspec-discover >= 2.7.1 && <2.12 -- inherited depndencies build-depends: base- , base-compat , bytestring , cookie , http-api-data , text , time-compat- , unordered-containers+ , containers , uuid-types - if !impl(ghc >= 7.10)- build-depends: nats-- build-depends: HUnit >= 1.6.0.0 && <1.7- , hspec >= 2.7.1 && <2.8- , QuickCheck >= 2.13.1 && <2.15+ build-depends: hspec >= 2.7.1 && <2.12+ , QuickCheck >= 2.13.1 && <2.16 , quickcheck-instances >= 0.3.25.2 && <0.4 source-repository head
− include/overlapping-compat.h
@@ -1,8 +0,0 @@-#if __GLASGOW_HASKELL__ >= 710-#define OVERLAPPABLE_ {-# OVERLAPPABLE #-}-#define OVERLAPPING_ {-# OVERLAPPING #-}-#else-{-# LANGUAGE OverlappingInstances #-}-#define OVERLAPPABLE_-#define OVERLAPPING_-#endif
src/Web/Internal/FormUrlEncoded.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE DeriveGeneric #-}@@ -14,12 +13,8 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}-#include "overlapping-compat.h" module Web.Internal.FormUrlEncoded where -import Prelude ()-import Prelude.Compat- import Control.Applicative (Const(Const)) import Control.Arrow ((***)) import Control.Monad ((<=<))@@ -30,14 +25,11 @@ import qualified Data.Foldable as F import Data.Functor.Identity (Identity(Identity)) import Data.Hashable (Hashable)-import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap+import qualified Data.Map.Strict as Map import Data.Int (Int16, Int32, Int64, Int8) import Data.IntMap (IntMap) import qualified Data.IntMap as IntMap import Data.List (intersperse, sortBy)-import Data.Map (Map)-import qualified Data.Map as Map import Data.Monoid (All (..), Any (..), Dual (..), Product (..), Sum (..)) import Data.Ord (comparing)@@ -54,7 +46,7 @@ UTCTime, ZonedTime) import Data.Time.Calendar.Month.Compat (Month) import Data.Time.Calendar.Quarter.Compat (Quarter, QuarterOfYear (..))-import Data.Void (Void)+import Data.Void (Void, absurd) import Data.Word (Word16, Word32, Word64, Word8) import GHC.Exts (Constraint, IsList (..)) import GHC.Generics@@ -64,13 +56,11 @@ import Web.Internal.HttpApiData -- $setup--- >>> :set -XDeriveGeneric--- >>> :set -XOverloadedLists--- >>> :set -XOverloadedStrings--- >>> :set -XFlexibleContexts--- >>> :set -XScopedTypeVariables--- >>> :set -XTypeFamilies+-- >>> :set -XDeriveGeneric -XOverloadedLists -XOverloadedStrings -XFlexibleContexts -XScopedTypeVariables -XTypeFamilies+-- >>> import GHC.Generics (Generic) -- >>> import Data.Char (toLower)+-- >>> import Data.Text (Text)+-- >>> import Data.Word (Word8) -- -- >>> data Person = Person { name :: String, age :: Int } deriving (Show, Generic) -- >>> instance ToForm Person@@ -212,7 +202,7 @@ -- | The contents of a form, not yet URL-encoded. -- -- 'Form' can be URL-encoded with 'urlEncodeForm' and URL-decoded with 'urlDecodeForm'.-newtype Form = Form { unForm :: HashMap Text [Text] }+newtype Form = Form { unForm :: Map.Map Text [Text] } deriving (Eq, Read, Generic, Semigroup, Monoid) instance Show Form where@@ -223,8 +213,8 @@ -- For a stable conversion use 'toListStable'. instance IsList Form where type Item Form = (Text, Text)- fromList = Form . HashMap.fromListWith (flip (<>)) . fmap (\(k, v) -> (k, [v]))- toList = concatMap (\(k, vs) -> map ((,) k) vs) . HashMap.toList . unForm+ fromList = Form . Map.fromListWith (flip (<>)) . fmap (\(k, v) -> (k, [v]))+ toList = concatMap (\(k, vs) -> map ((,) k) vs) . Map.toList . unForm -- | A stable version of 'toList'. toListStable :: Form -> [(Text, Text)]@@ -274,15 +264,22 @@ instance ToForm Form where toForm = id +-- |+--+-- @since 0.6.3+instance ToForm Void where toForm = absurd++-- |+--+-- @since 0.6.3+instance ToForm () where toForm _ = Form Map.empty+ instance (ToFormKey k, ToHttpApiData v) => ToForm [(k, v)] where toForm = fromList . map (toFormKey *** toQueryParam) -instance (ToFormKey k, ToHttpApiData v) => ToForm (Map k [v]) where+instance (ToFormKey k, ToHttpApiData v) => ToForm (Map.Map k [v]) where toForm = fromEntriesByKey . Map.toList -instance (ToFormKey k, ToHttpApiData v) => ToForm (HashMap k [v]) where- toForm = fromEntriesByKey . HashMap.toList- instance ToHttpApiData v => ToForm (IntMap [v]) where toForm = fromEntriesByKey . IntMap.toList @@ -291,21 +288,16 @@ -- >>> fromEntriesByKey [("name",["Nick"]),("color",["red","blue"])] -- fromList [("color","red"),("color","blue"),("name","Nick")] fromEntriesByKey :: (ToFormKey k, ToHttpApiData v) => [(k, [v])] -> Form-fromEntriesByKey = Form . HashMap.fromListWith (<>) . map (toFormKey *** map toQueryParam)+fromEntriesByKey = Form . Map.fromListWith (<>) . map (toFormKey *** map toQueryParam) data Proxy3 a b c = Proxy3 type family NotSupported (cls :: k1) (a :: k2) (reason :: Symbol) :: Constraint where-#if __GLASGOW_HASKELL__ < 800- -- this is just a placeholder case for older GHCs to not freak out on an empty closed type family- NotSupported cls a "this type family is actually empty" = ()-#else NotSupported cls a reason = TypeError ( 'Text "Cannot derive a Generic-based " ':<>: 'ShowType cls ':<>: 'Text " instance for " ':<>: 'ShowType a ':<>: 'Text "." ':$$: 'ShowType a ':<>: 'Text " " ':<>: 'Text reason ':<>: 'Text "," ':$$: 'Text "but Generic-based " ':<>: 'ShowType cls ':<>: 'Text " instances can be derived only for records" ':$$: 'Text "(i.e. product types with named fields)." )-#endif -- | A 'Generic'-based implementation of 'toForm'. -- This is used as a default implementation in 'ToForm'.@@ -357,7 +349,7 @@ instance (GToForm t f) => GToForm t (M1 C x f) where gToForm p opts (M1 a) = gToForm p opts a -instance OVERLAPPABLE_ (Selector s, ToHttpApiData c) => GToForm t (M1 S s (K1 i c)) where+instance {-# OVERLAPPABLE #-} (Selector s, ToHttpApiData c) => GToForm t (M1 S s (K1 i c)) where gToForm _ opts (M1 (K1 c)) = fromList [(key, toQueryParam c)] where key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)@@ -375,7 +367,7 @@ where key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p) -instance OVERLAPPING_ (Selector s) => GToForm t (M1 S s (K1 i String)) where+instance {-# OVERLAPPING #-} (Selector s) => GToForm t (M1 S s (K1 i String)) where gToForm _ opts (M1 (K1 c)) = fromList [(key, toQueryParam c)] where key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)@@ -425,16 +417,23 @@ instance FromForm Form where fromForm = pure +-- |+--+-- @since 0.6.3+instance FromForm Void where fromForm _ = Left "fromForm: Void"++-- |+--+-- @since 0.6.3+instance FromForm () where fromForm _ = Right ()+ -- | _NOTE:_ this conversion is unstable and may result in different key order (but not values). instance (FromFormKey k, FromHttpApiData v) => FromForm [(k, v)] where fromForm = fmap (concatMap (\(k, vs) -> map ((,) k) vs)) . toEntriesByKey -instance (Ord k, FromFormKey k, FromHttpApiData v) => FromForm (Map k [v]) where+instance (Ord k, Eq k, Hashable k, FromFormKey k, FromHttpApiData v) => FromForm (Map.Map k [v]) where fromForm = fmap (Map.fromListWith (<>)) . toEntriesByKey -instance (Eq k, Hashable k, FromFormKey k, FromHttpApiData v) => FromForm (HashMap k [v]) where- fromForm = fmap (HashMap.fromListWith (<>)) . toEntriesByKey- instance FromHttpApiData v => FromForm (IntMap [v]) where fromForm = fmap (IntMap.fromListWith (<>)) . toEntriesByKey @@ -443,7 +442,7 @@ -- _NOTE:_ this conversion is unstable and may result in different key order -- (but not values). For a stable encoding see 'toEntriesByKeyStable'. toEntriesByKey :: (FromFormKey k, FromHttpApiData v) => Form -> Either Text [(k, [v])]-toEntriesByKey = traverse parseGroup . HashMap.toList . unForm+toEntriesByKey = traverse parseGroup . Map.toList . unForm where parseGroup (k, vs) = (,) <$> parseFormKey k <*> traverse parseQueryParam vs @@ -505,7 +504,7 @@ instance GFromForm t f => GFromForm t (M1 C x f) where gFromForm p opts f = M1 <$> gFromForm p opts f -instance OVERLAPPABLE_ (Selector s, FromHttpApiData c) => GFromForm t (M1 S s (K1 i c)) where+instance {-# OVERLAPPABLE #-} (Selector s, FromHttpApiData c) => GFromForm t (M1 S s (K1 i c)) where gFromForm _ opts form = M1 . K1 <$> parseUnique key form where key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)@@ -520,7 +519,7 @@ where key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p) -instance OVERLAPPING_ (Selector s) => GFromForm t (M1 S s (K1 i String)) where+instance {-# OVERLAPPING #-} (Selector s) => GFromForm t (M1 S s (K1 i String)) where gFromForm _ opts form = M1 . K1 <$> parseUnique key form where key = Text.pack $ fieldLabelModifier opts $ selName (Proxy3 :: Proxy3 s g p)@@ -670,7 +669,7 @@ -- >>> lookupAll "name" [("name", "Oleg"), ("name", "David")] -- ["Oleg","David"] lookupAll :: Text -> Form -> [Text]-lookupAll key = F.concat . HashMap.lookup key . unForm+lookupAll key = F.concat . Map.lookup key . unForm -- | Lookup an optional value for a key. -- Fail if there is more than one value.
src/Web/Internal/HttpApiData.hs view
@@ -15,14 +15,9 @@ -- such as URL pieces, headers and query parameters. module Web.Internal.HttpApiData where -import Prelude ()-import Prelude.Compat- import Control.Applicative (Const(Const)) import Control.Arrow (left, (&&&)) import Control.Monad ((<=<))-import qualified Data.Attoparsec.Text as Atto-import qualified Data.Attoparsec.Time as Atto import Data.ByteString (ByteString) import qualified Data.ByteString as BS import qualified Data.ByteString.Builder as BS@@ -32,11 +27,11 @@ import qualified Data.Fixed as F import Data.Functor.Identity (Identity(Identity)) import Data.Int (Int16, Int32, Int64, Int8)-import qualified Data.Map as Map+import Data.Kind (Type)+import qualified Data.Map.Strict as Map import Data.Monoid (All (..), Any (..), Dual (..), First (..), Last (..), Product (..), Sum (..))-import Data.Semigroup (Semigroup (..)) import qualified Data.Semigroup as Semi import Data.Tagged (Tagged (..)) import Data.Text (Text)@@ -45,19 +40,18 @@ encodeUtf8) import Data.Text.Encoding.Error (lenientDecode) import qualified Data.Text.Lazy as L+import Data.Text.Lazy.Builder (Builder, toLazyText) import Data.Text.Read (Reader, decimal, rational, signed)-import Data.Time.Compat (Day, FormatTime, LocalTime,+import qualified Data.Time.ToText as TT+import qualified Data.Time.FromText as FT+import Data.Time.Compat (Day, LocalTime, NominalDiffTime, TimeOfDay,- UTCTime, ZonedTime, formatTime,- DayOfWeek (..),+ UTCTime, ZonedTime, DayOfWeek (..), nominalDiffTimeToSeconds, secondsToNominalDiffTime)-import Data.Time.Format.Compat (defaultTimeLocale,- iso8601DateFormat) import Data.Time.Calendar.Month.Compat (Month)-import Data.Time.Calendar.Quarter.Compat (Quarter, QuarterOfYear (..),- toYearQuarter)+import Data.Time.Calendar.Quarter.Compat (Quarter, QuarterOfYear (..)) import Data.Typeable (Typeable) import qualified Data.UUID.Types as UUID import Data.Version (Version, parseVersion,@@ -71,21 +65,22 @@ import Web.Cookie (SetCookie, parseSetCookie, renderSetCookie) -#if MIN_VERSION_base(4,9,0)-import Data.Kind (Type)-#else-#define Type *-#endif- #if USE_TEXT_SHOW import TextShow (TextShow, showt) #endif -- $setup--- >>> data BasicAuthToken = BasicAuthToken Text deriving (Show)--- >>> instance FromHttpApiData BasicAuthToken where parseHeader h = BasicAuthToken <$> parseHeaderWithPrefix "Basic " h; parseQueryParam p = BasicAuthToken <$> parseQueryParam p+-- >>> :set -XOverloadedStrings+-- >>> import Data.Text (Text)+-- >>> import Data.Word (Word8)+-- >>> import Data.Text.Read (decimal) -- >>> import Data.Time.Compat+-- >>> import Data.Time.Calendar.Month.Compat+-- >>> import Data.Time.Calendar.Quarter.Compat -- >>> import Data.Version+-- >>> import Web.Cookie (SetCookie)+-- >>> data BasicAuthToken = BasicAuthToken Text deriving (Show)+-- >>> instance FromHttpApiData BasicAuthToken where parseHeader h = BasicAuthToken <$> parseHeaderWithPrefix "Basic " h; parseQueryParam p = BasicAuthToken <$> parseQueryParam p -- | Convert value to HTTP API data. --@@ -98,10 +93,10 @@ toUrlPiece = toQueryParam -- | Convert to a URL path piece, making sure to encode any special chars.- -- The default definition uses 'H.encodePathSegmentsRelative',+ -- The default definition uses @'H.urlEncodeBuilder' 'False'@ -- but this may be overriden with a more efficient version. toEncodedUrlPiece :: a -> BS.Builder- toEncodedUrlPiece = H.encodePathSegmentsRelative . (:[]) . toUrlPiece+ toEncodedUrlPiece = H.urlEncodeBuilder False . encodeUtf8 . toUrlPiece -- | Convert to HTTP header value. toHeader :: a -> ByteString@@ -111,6 +106,14 @@ toQueryParam :: a -> Text toQueryParam = toUrlPiece + -- | Convert to URL query param,+ -- The default definition uses @'H.urlEncodeBuilder' 'True'@+ -- but this may be overriden with a more efficient version.+ --+ -- @since 0.5.1+ toEncodedQueryParam :: a -> BS.Builder+ toEncodedQueryParam = H.urlEncodeBuilder True . encodeUtf8 . toQueryParam+ -- | Parse value from HTTP API data. -- -- __WARNING__: Do not derive this using @DeriveAnyClass@ as the generated@@ -427,12 +430,21 @@ unsafeToEncodedUrlPiece :: ToHttpApiData a => a -> BS.Builder unsafeToEncodedUrlPiece = BS.byteString . encodeUtf8 . toUrlPiece +-- | Convert to a URL-encoded query param using 'toQueryParam'.+-- /Note/: this function does not check if the result contains unescaped characters!+--+-- @since 0.5.1+unsafeToEncodedQueryParam :: ToHttpApiData a => a -> BS.Builder+unsafeToEncodedQueryParam = BS.byteString . encodeUtf8 . toQueryParam+ -- | -- >>> toUrlPiece () -- "_" instance ToHttpApiData () where- toUrlPiece () = "_"- toEncodedUrlPiece = unsafeToEncodedUrlPiece+ toUrlPiece _ = "_"+ toHeader _ = "_"+ toEncodedUrlPiece _ = "_"+ toEncodedQueryParam _ = "_" instance ToHttpApiData Char where toUrlPiece = T.singleton@@ -443,67 +455,74 @@ instance ToHttpApiData Version where toUrlPiece = T.pack . showVersion toEncodedUrlPiece = unsafeToEncodedUrlPiece+ toEncodedQueryParam = unsafeToEncodedQueryParam instance ToHttpApiData Void where toUrlPiece = absurd-instance ToHttpApiData Natural where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece+instance ToHttpApiData Natural where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam -instance ToHttpApiData Bool where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Ordering where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece+instance ToHttpApiData Bool where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Ordering where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam -instance ToHttpApiData Double where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Float where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Int where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Int8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Int16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Int32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Int64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Integer where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Word where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Word8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Word16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Word32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece-instance ToHttpApiData Word64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece+instance ToHttpApiData Double where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Float where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Int where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Int8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Int16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Int32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Int64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Integer where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Word where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Word8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Word16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Word32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam+instance ToHttpApiData Word64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam -- | Note: this instance is not polykinded-instance F.HasResolution a => ToHttpApiData (F.Fixed (a :: Type)) where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece+instance F.HasResolution a => ToHttpApiData (F.Fixed (a :: Type)) where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam -- | -- >>> toUrlPiece (fromGregorian 2015 10 03) -- "2015-10-03" instance ToHttpApiData Day where- toUrlPiece = T.pack . show+ toUrlPiece = runTT TT.buildDay toEncodedUrlPiece = unsafeToEncodedUrlPiece--timeToUrlPiece :: FormatTime t => String -> t -> Text-timeToUrlPiece fmt = T.pack . formatTime defaultTimeLocale (iso8601DateFormat (Just fmt))+ toEncodedQueryParam = unsafeToEncodedQueryParam -- | -- >>> toUrlPiece $ TimeOfDay 14 55 23.1--- "14:55:23.1"+-- "14:55:23.100" instance ToHttpApiData TimeOfDay where- toUrlPiece = T.pack . formatTime defaultTimeLocale "%H:%M:%S%Q"+ toUrlPiece = runTT TT.buildTimeOfDay toEncodedUrlPiece = unsafeToEncodedUrlPiece+ -- no toEncodedQueryParam as : is unsafe char. -- | -- >>> toUrlPiece $ LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 21.687) -- "2015-10-03T14:55:21.687" instance ToHttpApiData LocalTime where- toUrlPiece = timeToUrlPiece "%H:%M:%S%Q"+ toUrlPiece = runTT TT.buildLocalTime toEncodedUrlPiece = unsafeToEncodedUrlPiece+ -- no toEncodedQueryParam as : is unsafe char. -- | -- >>> toUrlPiece $ ZonedTime (LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 51.001)) utc--- "2015-10-03T14:55:51.001+0000"+-- "2015-10-03T14:55:51.001Z"+--+-- >>> toUrlPiece $ ZonedTime (LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 51.001)) (TimeZone 120 True "EET")+-- "2015-10-03T14:55:51.001+02:00"+-- instance ToHttpApiData ZonedTime where- toUrlPiece = timeToUrlPiece "%H:%M:%S%Q%z"+ toUrlPiece = runTT TT.buildZonedTime toEncodedUrlPiece = unsafeToEncodedUrlPiece+ -- no toEncodedQueryParam as : is unsafe char. -- | -- >>> toUrlPiece $ UTCTime (fromGregorian 2015 10 03) 864.5--- "2015-10-03T00:14:24.5Z"+-- "2015-10-03T00:14:24.500Z" instance ToHttpApiData UTCTime where- toUrlPiece = timeToUrlPiece "%H:%M:%S%QZ"+ toUrlPiece = runTT TT.buildUTCTime toEncodedUrlPiece = unsafeToEncodedUrlPiece+ -- no toEncodedQueryParam as : is unsafe char. -- | -- >>> toUrlPiece Monday@@ -518,16 +537,17 @@ toUrlPiece Sunday = "sunday" toEncodedUrlPiece = unsafeToEncodedUrlPiece+ toEncodedQueryParam = unsafeToEncodedQueryParam --- | +-- | -- >>> toUrlPiece Q4 -- "q4" instance ToHttpApiData QuarterOfYear where- toUrlPiece Q1 = "q1"- toUrlPiece Q2 = "q2"- toUrlPiece Q3 = "q3"- toUrlPiece Q4 = "q4"+ toUrlPiece = runTT TT.buildQuarterOfYear + toEncodedUrlPiece = unsafeToEncodedUrlPiece+ toEncodedQueryParam = unsafeToEncodedQueryParam+ -- | -- >>> import Data.Time.Calendar.Quarter.Compat (Quarter (..)) -- >>> MkQuarter 8040@@ -537,14 +557,11 @@ -- "2010-q1" -- instance ToHttpApiData Quarter where- toUrlPiece q = case toYearQuarter q of- (y, qoy) -> T.pack (show y ++ "-" ++ f qoy)- where- f Q1 = "q1"- f Q2 = "q2"- f Q3 = "q3"- f Q4 = "q4"+ toUrlPiece = runTT TT.buildQuarter + toEncodedUrlPiece = unsafeToEncodedUrlPiece+ toEncodedQueryParam = unsafeToEncodedQueryParam+ -- | -- >>> import Data.Time.Calendar.Month.Compat (Month (..)) -- >>> MkMonth 24482@@ -554,10 +571,15 @@ -- "2040-03" -- instance ToHttpApiData Month where- toUrlPiece = T.pack . formatTime defaultTimeLocale "%Y-%m"+ toUrlPiece = runTT TT.buildMonth + toEncodedUrlPiece = unsafeToEncodedUrlPiece+ toEncodedQueryParam = unsafeToEncodedQueryParam+ instance ToHttpApiData NominalDiffTime where toUrlPiece = toUrlPiece . nominalDiffTimeToSeconds++ toEncodedQueryParam = unsafeToEncodedQueryParam toEncodedUrlPiece = unsafeToEncodedUrlPiece instance ToHttpApiData String where toUrlPiece = T.pack@@ -567,46 +589,57 @@ instance ToHttpApiData All where toUrlPiece = coerce (toUrlPiece :: Bool -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Bool -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: Bool -> BS.Builder) instance ToHttpApiData Any where toUrlPiece = coerce (toUrlPiece :: Bool -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Bool -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: Bool -> BS.Builder) instance ToHttpApiData a => ToHttpApiData (Dual a) where toUrlPiece = coerce (toUrlPiece :: a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder) instance ToHttpApiData a => ToHttpApiData (Sum a) where toUrlPiece = coerce (toUrlPiece :: a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder) instance ToHttpApiData a => ToHttpApiData (Product a) where toUrlPiece = coerce (toUrlPiece :: a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder) instance ToHttpApiData a => ToHttpApiData (First a) where toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: Maybe a -> BS.Builder) instance ToHttpApiData a => ToHttpApiData (Last a) where toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: Maybe a -> BS.Builder) instance ToHttpApiData a => ToHttpApiData (Semi.Min a) where toUrlPiece = coerce (toUrlPiece :: a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder) instance ToHttpApiData a => ToHttpApiData (Semi.Max a) where toUrlPiece = coerce (toUrlPiece :: a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder) instance ToHttpApiData a => ToHttpApiData (Semi.First a) where toUrlPiece = coerce (toUrlPiece :: a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder) instance ToHttpApiData a => ToHttpApiData (Semi.Last a) where toUrlPiece = coerce (toUrlPiece :: a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder) -- | -- >>> toUrlPiece (Just "Hello")@@ -644,6 +677,7 @@ toHeader = coerce (toHeader :: a -> ByteString) toQueryParam = coerce (toQueryParam :: a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder) -- | @since 0.4.2 instance ToHttpApiData a => ToHttpApiData (Const a b) where@@ -651,6 +685,7 @@ toHeader = coerce (toHeader :: a -> ByteString) toQueryParam = coerce (toQueryParam :: a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder) -- | @since 0.4.2 instance ToHttpApiData a => ToHttpApiData (Identity a) where@@ -658,6 +693,7 @@ toHeader = coerce (toHeader :: a -> ByteString) toQueryParam = coerce (toQueryParam :: a -> Text) toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)+ toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder) -- | -- >>> parseUrlPiece "_" :: Either Text ()@@ -718,17 +754,17 @@ -- | -- >>> toGregorian <$> parseUrlPiece "2016-12-01" -- Right (2016,12,1)-instance FromHttpApiData Day where parseUrlPiece = runAtto Atto.day+instance FromHttpApiData Day where parseUrlPiece = runFT FT.parseDay -- | -- >>> parseUrlPiece "14:55:01.333" :: Either Text TimeOfDay -- Right 14:55:01.333-instance FromHttpApiData TimeOfDay where parseUrlPiece = runAtto Atto.timeOfDay+instance FromHttpApiData TimeOfDay where parseUrlPiece = runFT FT.parseTimeOfDay -- | -- >>> parseUrlPiece "2015-10-03T14:55:01" :: Either Text LocalTime -- Right 2015-10-03 14:55:01-instance FromHttpApiData LocalTime where parseUrlPiece = runAtto Atto.localTime+instance FromHttpApiData LocalTime where parseUrlPiece = runFT FT.parseLocalTime -- | -- >>> parseUrlPiece "2015-10-03T14:55:01+0000" :: Either Text ZonedTime@@ -736,12 +772,12 @@ -- -- >>> parseQueryParam "2016-12-31T01:00:00Z" :: Either Text ZonedTime -- Right 2016-12-31 01:00:00 +0000-instance FromHttpApiData ZonedTime where parseUrlPiece = runAtto Atto.zonedTime+instance FromHttpApiData ZonedTime where parseUrlPiece = runFT FT.parseZonedTime -- | -- >>> parseUrlPiece "2015-10-03T00:14:24Z" :: Either Text UTCTime -- Right 2015-10-03 00:14:24 UTC-instance FromHttpApiData UTCTime where parseUrlPiece = runAtto Atto.utcTime+instance FromHttpApiData UTCTime where parseUrlPiece = runFT FT.parseUTCTime -- | -- >>> parseUrlPiece "Monday" :: Either Text DayOfWeek@@ -760,12 +796,12 @@ -- | -- >>> parseUrlPiece "2021-01" :: Either Text Month -- Right 2021-01-instance FromHttpApiData Month where parseUrlPiece = runAtto Atto.month+instance FromHttpApiData Month where parseUrlPiece = runFT FT.parseMonth -- | -- >>> parseUrlPiece "2021-q1" :: Either Text Quarter -- Right 2021-Q1-instance FromHttpApiData Quarter where parseUrlPiece = runAtto Atto.quarter+instance FromHttpApiData Quarter where parseUrlPiece = runFT FT.parseQuarter -- | -- >>> parseUrlPiece "q2" :: Either Text QuarterOfYear@@ -773,13 +809,7 @@ -- -- >>> parseUrlPiece "Q3" :: Either Text QuarterOfYear -- Right Q3-instance FromHttpApiData QuarterOfYear where- parseUrlPiece t = case T.toLower t of- "q1" -> return Q1- "q2" -> return Q2- "q3" -> return Q3- "q4" -> return Q4- _ -> Left "Invalid quarter of year"+instance FromHttpApiData QuarterOfYear where parseUrlPiece = runFT FT.parseQuarterOfYear instance FromHttpApiData All where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text Bool) instance FromHttpApiData Any where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text Bool)@@ -839,7 +869,7 @@ -- | /Note:/ this instance works correctly for alphanumeric name and value -- -- >>> parseUrlPiece "SESSID=r2t5uvjq435r4q7ib3vtdjq120" :: Either Text SetCookie--- Right (SetCookie {setCookieName = "SESSID", setCookieValue = "r2t5uvjq435r4q7ib3vtdjq120", setCookiePath = Nothing, setCookieExpires = Nothing, setCookieMaxAge = Nothing, setCookieDomain = Nothing, setCookieHttpOnly = False, setCookieSecure = False, setCookieSameSite = Nothing})+-- Right (SetCookie {setCookieName = "SESSID", setCookieValue = "r2t5uvjq435r4q7ib3vtdjq120", setCookiePath = Nothing, setCookieExpires = Nothing, setCookieMaxAge = Nothing, setCookieDomain = Nothing, setCookieHttpOnly = False, setCookieSecure = False, setCookieSameSite = Nothing, setCookiePartitioned = False}) instance FromHttpApiData SetCookie where parseUrlPiece = parseHeader . encodeUtf8 parseHeader = Right . parseSetCookie@@ -863,12 +893,13 @@ parseQueryParam = coerce (parseQueryParam :: Text -> Either Text a) ---------------------------------------------------------------------------------- Attoparsec helpers+-- Helpers ------------------------------------------------------------------------------- -runAtto :: Atto.Parser a -> Text -> Either Text a-runAtto p t = case Atto.parseOnly (p <* Atto.endOfInput) t of+runTT :: (a -> Builder) -> a -> Text+runTT f x = L.toStrict (toLazyText (f x))++runFT :: (Text -> Either String a) -> Text -> Either Text a+runFT f t = case f t of Left err -> Left (T.pack err) Right x -> Right x--
test/Web/Internal/FormUrlEncodedSpec.hs view
@@ -1,16 +1,10 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Web.Internal.FormUrlEncodedSpec (spec) where -#if __GLASGOW_HASKELL__ < 710-import Control.Applicative-import Data.Monoid-#endif- import Control.Monad ((<=<)) import qualified Data.ByteString.Lazy.Char8 as BSL-import qualified Data.HashMap.Strict as HashMap+import qualified Data.Map.Strict as Map import Data.Text (Text, unpack) import Test.Hspec import Test.QuickCheck@@ -33,24 +27,24 @@ it "contains the record names" $ property $ \(x :: SimpleRec) -> do let f = unForm $ toForm x- HashMap.member "rec1" f `shouldBe` True- HashMap.member "rec2" f `shouldBe` True+ Map.member "rec1" f `shouldBe` True+ Map.member "rec2" f `shouldBe` True it "contains the correct record values" $ property $ \(x :: SimpleRec) -> do let f = unForm $ toForm x- HashMap.lookup "rec1" f `shouldBe` Just [rec1 x]- (parseQueryParams <$> HashMap.lookup "rec2" f) `shouldBe` Just (Right [rec2 x])+ Map.lookup "rec1" f `shouldBe` Just [rec1 x]+ (parseQueryParams <$> Map.lookup "rec2" f) `shouldBe` Just (Right [rec2 x]) context "FromForm" $ do it "is the right inverse of ToForm" $ property $ \x (y :: Int) -> do let f1 = fromList [("rec1", x), ("rec2", toQueryParam y)]- Right r1 = fromForm f1 :: Either Text SimpleRec- toForm r1 `shouldBe` f1+ r1 = fromForm f1 :: Either Text SimpleRec+ fmap toForm r1 `shouldBe` Right f1 it "returns the underlying error" $ do let f = fromList [("rec1", "anything"), ("rec2", "bad")]- Left e = fromForm f :: Either Text SimpleRec+ e = either id (const "no error") (fromForm f :: Either Text SimpleRec) unpack e `shouldContain` "input does not start with a digit" urlEncoding :: Spec
test/Web/Internal/HttpApiDataSpec.hs view
@@ -2,9 +2,6 @@ module Web.Internal.HttpApiDataSpec (spec) where -import Prelude ()-import Prelude.Compat- import qualified Data.ByteString as BS import Data.ByteString.Builder (toLazyByteString) import Data.Char@@ -46,6 +43,9 @@ encodedUrlPieceProp :: ToHttpApiData a => a -> Property encodedUrlPieceProp x = toLazyByteString (toEncodedUrlPiece (toUrlPiece x)) === toLazyByteString (toEncodedUrlPiece x) +encodedQueryParamProp :: ToHttpApiData a => a -> Property+encodedQueryParamProp x = toLazyByteString (toEncodedQueryParam (toQueryParam x)) === toLazyByteString (toEncodedQueryParam x)+ -- | Check 'ToHttpApiData' and 'FromHttpApiData' compatibility checkUrlPiece :: forall a. (Eq a, ToHttpApiData a, FromHttpApiData a, Show a, Arbitrary a) => Proxy a -> String -> Spec checkUrlPiece _ = checkUrlPiece' (arbitrary :: Gen a)@@ -56,6 +56,7 @@ prop "toQueryParam <=> parseQueryParam" $ forAll gen (toQueryParam <=> parseQueryParam :: a -> Property) prop "toHeader <=> parseHeader" $ forAll gen (toHeader <=> parseHeader :: a -> Property) prop "toEncodedUrlPiece encodes correctly" $ forAll gen encodedUrlPieceProp+ prop "toEncodedQueryParam encodes correctly" $ forAll gen encodedQueryParamProp -- | Check case insensitivity for @parseUrlPiece@. checkUrlPieceI :: forall a. (Eq a, ToHttpApiData a, FromHttpApiData a, Arbitrary a) => Proxy a -> String -> Spec
test/Web/Internal/TestInstances.hs view
@@ -8,9 +8,9 @@ , NoEmptyKeyForm(..) ) where -import Control.Applicative+import Control.Applicative -- for ghc < 9.6 import Data.Char-import qualified Data.HashMap.Strict as HashMap+import qualified Data.Map.Strict as Map import qualified Data.Text as T import Data.Time.Compat import GHC.Exts (fromList)@@ -63,4 +63,4 @@ instance Arbitrary NoEmptyKeyForm where arbitrary = NoEmptyKeyForm . removeEmptyKeys <$> arbitrary where- removeEmptyKeys (Form m) = Form (HashMap.delete "" m)+ removeEmptyKeys (Form m) = Form (Map.delete "" m)