@@ -5,9 +5,7 @@
55{-# LANGUAGE DeriveFunctor #-}
66{-# LANGUAGE DeriveTraversable #-}
77{-# LANGUAGE FlexibleInstances #-}
8−{-# LANGUAGE KindSignatures #-}
98{-# LANGUAGE OverloadedStrings #-}
10−{-# LANGUAGE PolyKinds #-}
119{-# LANGUAGE ScopedTypeVariables #-}
1210{-# LANGUAGE TypeSynonymInstances #-}
1311-- |
@@ -15,72 +13,67 @@
1513-- such as URL pieces, headers and query parameters.
1614module Web.Internal.HttpApiData where
1715
18−import Prelude ()
19−import Prelude.Compat
16+#if __GLASGOW_HASKELL__ < 710
17+import Control.Applicative
18+import Data.Foldable (Foldable)
19+import Data.Traversable (Traversable (traverse))
20+#endif
2021
21−import Control.Applicative (Const(Const))
2222import Control.Arrow (left, (&&&))
2323import Control.Monad ((<=<))
24−import qualified Data.Attoparsec.Text as Atto
25−import qualified Data.Attoparsec.Time as Atto
24+
2625import Data.ByteString (ByteString)
2726import qualified Data.ByteString as BS
28−import qualified Data.ByteString.Builder as BS
2927import qualified Data.ByteString.Lazy as LBS
30−import Data.Coerce (coerce)
31−import Data.Data (Data)
28+import Data.Monoid
29+
3230import qualified Data.Fixed as F
33−import Data.Functor.Identity (Identity(Identity))
34−import Data.Int (Int16, Int32, Int64, Int8)
35−import Data.Kind (Type)
36−import qualified Data.Map as Map
37−import Data.Monoid (All (..), Any (..), Dual (..),
38− First (..), Last (..),
39− Product (..), Sum (..))
40−import Data.Semigroup (Semigroup (..))
41−import qualified Data.Semigroup as Semi
42−import Data.Tagged (Tagged (..))
31+import Data.Int
32+import Data.Word
33+
4334import Data.Text (Text)
4435import qualified Data.Text as T
45−import Data.Text.Encoding (decodeUtf8', decodeUtf8With,
46− encodeUtf8)
36+import Data.Text.Encoding (decodeUtf8With, decodeUtf8', encodeUtf8)
4737import Data.Text.Encoding.Error (lenientDecode)
4838import qualified Data.Text.Lazy as L
4939import Data.Text.Read (Reader, decimal, rational,
5040 signed)
51−import Data.Time.Compat (Day, FormatTime, LocalTime,
52− NominalDiffTime, TimeOfDay,
53− UTCTime, ZonedTime, formatTime,
54− DayOfWeek (..),
55− nominalDiffTimeToSeconds,
56− secondsToNominalDiffTime)
57−import Data.Time.Format.Compat (defaultTimeLocale,
58− iso8601DateFormat)
59−import Data.Time.Calendar.Month.Compat (Month)
60−import Data.Time.Calendar.Quarter.Compat (Quarter, QuarterOfYear (..),
61− toYearQuarter)
62−import Data.Typeable (Typeable)
63−import qualified Data.UUID.Types as UUID
64−import Data.Version (Version, parseVersion,
65− showVersion)
66−import Data.Void (Void, absurd)
67−import Data.Word (Word16, Word32, Word64, Word8)
68−import qualified Network.HTTP.Types as H
69−import Numeric.Natural (Natural)
41+
42+import Data.Time
43+#if __GLASGOW_HASKELL__ < 710
44+import Data.Time.Locale.Compat
45+#endif
46+import Data.Version
47+
48+#if MIN_VERSION_base(4,8,0)
49+import Data.Void
50+import Numeric.Natural
51+#endif
52+
7053import Text.ParserCombinators.ReadP (readP_to_S)
7154import Text.Read (readMaybe)
72−import Web.Cookie (SetCookie, parseSetCookie,
73− renderSetCookie)
7455
7556#if USE_TEXT_SHOW
7657import TextShow (TextShow, showt)
7758#endif
7859
60+import qualified Data.UUID.Types as UUID
61+
62+import qualified Data.ByteString.Builder as BS
63+import Data.Data (Data)
64+import Data.Typeable (Typeable)
65+import qualified Network.HTTP.Types as H
66+
67+import qualified Data.Attoparsec.Text as Atto
68+import qualified Data.Attoparsec.Time as Atto
69+
70+import Web.Cookie (SetCookie, parseSetCookie,
71+ renderSetCookie)
72+
73+
7974-- $setup
8075-- >>> data BasicAuthToken = BasicAuthToken Text deriving (Show)
8176-- >>> instance FromHttpApiData BasicAuthToken where parseHeader h = BasicAuthToken <$> parseHeaderWithPrefix "Basic " h; parseQueryParam p = BasicAuthToken <$> parseQueryParam p
82−-- >>> import Data.Time.Compat
83−-- >>> import Data.Version
8477
8578-- | Convert value to HTTP API data.
8679--
@@ -96,7 +89,7 @@ class ToHttpApiData a where
9689 -- The default definition uses 'H.encodePathSegmentsRelative',
9790 -- but this may be overriden with a more efficient version.
9891 toEncodedUrlPiece :: a -> BS.Builder
99− toEncodedUrlPiece = H.urlEncodeBuilder True . encodeUtf8 . toUrlPiece
92+ toEncodedUrlPiece = H.encodePathSegmentsRelative . (:[]) . toUrlPiece
10093
10194 -- | Convert to HTTP header value.
10295 toHeader :: a -> ByteString
@@ -338,7 +331,7 @@ lookupBoundedEnumOf f = flip lookup (map (f &&& id) [minBound..maxBound])
338331-- >>> parseBoundedEnumOf toUrlPiece "true" :: Either Text Bool
339332-- Right True
340333--
341−-- For case insensitive parser see 'parseBoundedEnumOfI'.
334+-- For case sensitive parser see 'parseBoundedEnumOfI'.
342335parseBoundedEnumOf :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
343336parseBoundedEnumOf = parseMaybeTextData . lookupBoundedEnumOf
344337
@@ -439,8 +432,10 @@ instance ToHttpApiData Version where
439432 toUrlPiece = T.pack . showVersion
440433 toEncodedUrlPiece = unsafeToEncodedUrlPiece
441434
435+#if MIN_VERSION_base(4,8,0)
442436instance ToHttpApiData Void where toUrlPiece = absurd
443437instance ToHttpApiData Natural where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
438+#endif
444439
445440instance ToHttpApiData Bool where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece
446441instance ToHttpApiData Ordering where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece
@@ -459,8 +454,7 @@ instance ToHttpApiData Word16 where toUrlPiece = showt; toEncodedUrlPiece = un
459454instance ToHttpApiData Word32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
460455instance ToHttpApiData Word64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
461456
462−-- | Note: this instance is not polykinded
463−instance F.HasResolution a => ToHttpApiData (F.Fixed (a :: Type)) where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
457+instance F.HasResolution a => ToHttpApiData (F.Fixed a) where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
464458
465459-- |
466460-- >>> toUrlPiece (fromGregorian 2015 10 03)
@@ -500,108 +494,36 @@ instance ToHttpApiData UTCTime where
500494 toUrlPiece = timeToUrlPiece "%H:%M:%S%QZ"
501495 toEncodedUrlPiece = unsafeToEncodedUrlPiece
502496
503−-- |
504−-- >>> toUrlPiece Monday
505−-- "monday"
506−instance ToHttpApiData DayOfWeek where
507− toUrlPiece Monday = "monday"
508− toUrlPiece Tuesday = "tuesday"
509− toUrlPiece Wednesday = "wednesday"
510− toUrlPiece Thursday = "thursday"
511− toUrlPiece Friday = "friday"
512− toUrlPiece Saturday = "saturday"
513− toUrlPiece Sunday = "sunday"
514−
515− toEncodedUrlPiece = unsafeToEncodedUrlPiece
516−
517−-- |
518−-- >>> toUrlPiece Q4
519−-- "q4"
520−instance ToHttpApiData QuarterOfYear where
521− toUrlPiece Q1 = "q1"
522− toUrlPiece Q2 = "q2"
523− toUrlPiece Q3 = "q3"
524− toUrlPiece Q4 = "q4"
525−
526−-- |
527−-- >>> import Data.Time.Calendar.Quarter.Compat (Quarter (..))
528−-- >>> MkQuarter 8040
529−-- 2010-Q1
530−--
531−-- >>> toUrlPiece $ MkQuarter 8040
532−-- "2010-q1"
533−--
534−instance ToHttpApiData Quarter where
535− toUrlPiece q = case toYearQuarter q of
536− (y, qoy) -> T.pack (show y ++ "-" ++ f qoy)
537− where
538− f Q1 = "q1"
539− f Q2 = "q2"
540− f Q3 = "q3"
541− f Q4 = "q4"
542−
543−-- |
544−-- >>> import Data.Time.Calendar.Month.Compat (Month (..))
545−-- >>> MkMonth 24482
546−-- 2040-03
547−--
548−-- >>> toUrlPiece $ MkMonth 24482
549−-- "2040-03"
550−--
551−instance ToHttpApiData Month where
552− toUrlPiece = T.pack . formatTime defaultTimeLocale "%Y-%m"
553−
554497instance ToHttpApiData NominalDiffTime where
555− toUrlPiece = toUrlPiece . nominalDiffTimeToSeconds
498+ toUrlPiece = toUrlPiece . (floor :: NominalDiffTime -> Integer)
556499 toEncodedUrlPiece = unsafeToEncodedUrlPiece
557500
558501instance ToHttpApiData String where toUrlPiece = T.pack
559502instance ToHttpApiData Text where toUrlPiece = id
560503instance ToHttpApiData L.Text where toUrlPiece = L.toStrict
561504
562−instance ToHttpApiData All where
563− toUrlPiece = coerce (toUrlPiece :: Bool -> Text)
564− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Bool -> BS.Builder)
565−
566−instance ToHttpApiData Any where
567− toUrlPiece = coerce (toUrlPiece :: Bool -> Text)
568− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Bool -> BS.Builder)
505+instance ToHttpApiData All where toUrlPiece = toUrlPiece . getAll; toEncodedUrlPiece = toEncodedUrlPiece . getAll
506+instance ToHttpApiData Any where toUrlPiece = toUrlPiece . getAny; toEncodedUrlPiece = toEncodedUrlPiece . getAny
569507
570508instance ToHttpApiData a => ToHttpApiData (Dual a) where
571− toUrlPiece = coerce (toUrlPiece :: a -> Text)
572− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
509+ toUrlPiece = toUrlPiece . getDual
510+ toEncodedUrlPiece = toEncodedUrlPiece . getDual
573511
574512instance ToHttpApiData a => ToHttpApiData (Sum a) where
575− toUrlPiece = coerce (toUrlPiece :: a -> Text)
576− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
513+ toUrlPiece = toUrlPiece . getSum
514+ toEncodedUrlPiece = toEncodedUrlPiece . getSum
577515
578516instance ToHttpApiData a => ToHttpApiData (Product a) where
579− toUrlPiece = coerce (toUrlPiece :: a -> Text)
580− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
517+ toUrlPiece = toUrlPiece . getProduct
518+ toEncodedUrlPiece = toEncodedUrlPiece . getProduct
581519
582520instance ToHttpApiData a => ToHttpApiData (First a) where
583− toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text)
584− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)
521+ toUrlPiece = toUrlPiece . getFirst
522+ toEncodedUrlPiece = toEncodedUrlPiece . getFirst
585523
586524instance ToHttpApiData a => ToHttpApiData (Last a) where
587− toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text)
588− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)
589−
590−instance ToHttpApiData a => ToHttpApiData (Semi.Min a) where
591− toUrlPiece = coerce (toUrlPiece :: a -> Text)
592− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
593−
594−instance ToHttpApiData a => ToHttpApiData (Semi.Max a) where
595− toUrlPiece = coerce (toUrlPiece :: a -> Text)
596− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
597−
598−instance ToHttpApiData a => ToHttpApiData (Semi.First a) where
599− toUrlPiece = coerce (toUrlPiece :: a -> Text)
600− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
601−
602−instance ToHttpApiData a => ToHttpApiData (Semi.Last a) where
603− toUrlPiece = coerce (toUrlPiece :: a -> Text)
604− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
525+ toUrlPiece = toUrlPiece . getLast
526+ toEncodedUrlPiece = toEncodedUrlPiece . getLast
605527
606528-- |
607529-- >>> toUrlPiece (Just "Hello")
@@ -633,27 +555,6 @@ instance ToHttpApiData SetCookie where
633555 toHeader = LBS.toStrict . BS.toLazyByteString . renderSetCookie
634556 -- toEncodedUrlPiece = renderSetCookie -- doesn't do things.
635557
636−-- | Note: this instance is not polykinded
637−instance ToHttpApiData a => ToHttpApiData (Tagged (b :: Type) a) where
638− toUrlPiece = coerce (toUrlPiece :: a -> Text)
639− toHeader = coerce (toHeader :: a -> ByteString)
640− toQueryParam = coerce (toQueryParam :: a -> Text)
641− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
642−
643−-- | @since 0.4.2
644−instance ToHttpApiData a => ToHttpApiData (Const a b) where
645− toUrlPiece = coerce (toUrlPiece :: a -> Text)
646− toHeader = coerce (toHeader :: a -> ByteString)
647− toQueryParam = coerce (toQueryParam :: a -> Text)
648− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
649−
650−-- | @since 0.4.2
651−instance ToHttpApiData a => ToHttpApiData (Identity a) where
652− toUrlPiece = coerce (toUrlPiece :: a -> Text)
653− toHeader = coerce (toHeader :: a -> ByteString)
654− toQueryParam = coerce (toQueryParam :: a -> Text)
655− toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
656−
657558-- |
658559-- >>> parseUrlPiece "_" :: Either Text ()
659560-- Right ()
@@ -665,7 +566,7 @@ instance FromHttpApiData Char where
665566 parseUrlPiece s =
666567 case T.uncons s of
667568 Just (c, s') | T.null s' -> pure c
668− _ -> defaultParseError s
569+ _ -> defaultParseError s
669570
670571-- |
671572-- >>> showVersion <$> parseUrlPiece "1.2.3"
@@ -676,6 +577,7 @@ instance FromHttpApiData Version where
676577 ((x, ""):_) -> pure x
677578 _ -> defaultParseError s
678579
580+#if MIN_VERSION_base(4,8,0)
679581-- | Parsing a @'Void'@ value is always an error, considering @'Void'@ as a data type with no constructors.
680582instance FromHttpApiData Void where
681583 parseUrlPiece _ = Left "Void cannot be parsed!"
@@ -686,6 +588,7 @@ instance FromHttpApiData Natural where
686588 if n < 0
687589 then Left ("underflow: " <> s <> " (should be a non-negative integer)")
688590 else Right (fromInteger n)
591+#endif
689592
690593instance FromHttpApiData Bool where parseUrlPiece = parseBoundedUrlPiece
691594instance FromHttpApiData Ordering where parseUrlPiece = parseBoundedUrlPiece
@@ -706,10 +609,6 @@ instance FromHttpApiData String where parseUrlPiece = Right . T.unpack
706609instance FromHttpApiData Text where parseUrlPiece = Right
707610instance FromHttpApiData L.Text where parseUrlPiece = Right . L.fromStrict
708611
709−-- | Note: this instance is not polykinded
710−instance F.HasResolution a => FromHttpApiData (F.Fixed (a :: Type)) where
711− parseUrlPiece = runReader rational
712−
713612-- |
714613-- >>> toGregorian <$> parseUrlPiece "2016-12-01"
715614-- Right (2016,12,1)
@@ -738,57 +637,16 @@ instance FromHttpApiData ZonedTime where parseUrlPiece = runAtto Atto.zonedTime
738637-- Right 2015-10-03 00:14:24 UTC
739638instance FromHttpApiData UTCTime where parseUrlPiece = runAtto Atto.utcTime
740639
741−-- |
742−-- >>> parseUrlPiece "Monday" :: Either Text DayOfWeek
743−-- Right Monday
744−instance FromHttpApiData DayOfWeek where
745− parseUrlPiece t = case Map.lookup (T.toLower t) m of
746− Just dow -> Right dow
747− Nothing -> Left $ "Incorrect DayOfWeek: " <> T.take 10 t
748− where
749− m :: Map.Map Text DayOfWeek
750− m = Map.fromList [ (toUrlPiece dow, dow) | dow <- [Monday .. Sunday] ]
751−
752−
753−instance FromHttpApiData NominalDiffTime where parseUrlPiece = fmap secondsToNominalDiffTime . parseUrlPiece
754−
755−-- |
756−-- >>> parseUrlPiece "2021-01" :: Either Text Month
757−-- Right 2021-01
758−instance FromHttpApiData Month where parseUrlPiece = runAtto Atto.month
640+instance FromHttpApiData NominalDiffTime where parseUrlPiece = fmap fromInteger . parseUrlPiece
759641
760−-- |
761−-- >>> parseUrlPiece "2021-q1" :: Either Text Quarter
762−-- Right 2021-Q1
763−instance FromHttpApiData Quarter where parseUrlPiece = runAtto Atto.quarter
642+instance FromHttpApiData All where parseUrlPiece = fmap All . parseUrlPiece
643+instance FromHttpApiData Any where parseUrlPiece = fmap Any . parseUrlPiece
764644
765−-- |
766−-- >>> parseUrlPiece "q2" :: Either Text QuarterOfYear
767−-- Right Q2
768−--
769−-- >>> parseUrlPiece "Q3" :: Either Text QuarterOfYear
770−-- Right Q3
771−instance FromHttpApiData QuarterOfYear where
772− parseUrlPiece t = case T.toLower t of
773− "q1" -> return Q1
774− "q2" -> return Q2
775− "q3" -> return Q3
776− "q4" -> return Q4
777− _ -> Left "Invalid quarter of year"
778−
779−instance FromHttpApiData All where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text Bool)
780−instance FromHttpApiData Any where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text Bool)
781−
782−instance FromHttpApiData a => FromHttpApiData (Dual a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
783−instance FromHttpApiData a => FromHttpApiData (Sum a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
784−instance FromHttpApiData a => FromHttpApiData (Product a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
785−instance FromHttpApiData a => FromHttpApiData (First a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text (Maybe a))
786−instance FromHttpApiData a => FromHttpApiData (Last a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text (Maybe a))
787−
788−instance FromHttpApiData a => FromHttpApiData (Semi.Min a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
789−instance FromHttpApiData a => FromHttpApiData (Semi.Max a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
790−instance FromHttpApiData a => FromHttpApiData (Semi.First a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
791−instance FromHttpApiData a => FromHttpApiData (Semi.Last a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
645+instance FromHttpApiData a => FromHttpApiData (Dual a) where parseUrlPiece = fmap Dual . parseUrlPiece
646+instance FromHttpApiData a => FromHttpApiData (Sum a) where parseUrlPiece = fmap Sum . parseUrlPiece
647+instance FromHttpApiData a => FromHttpApiData (Product a) where parseUrlPiece = fmap Product . parseUrlPiece
648+instance FromHttpApiData a => FromHttpApiData (First a) where parseUrlPiece = fmap First . parseUrlPiece
649+instance FromHttpApiData a => FromHttpApiData (Last a) where parseUrlPiece = fmap Last . parseUrlPiece
792650
793651-- |
794652-- >>> parseUrlPiece "Just 123" :: Either Text (Maybe Int)
@@ -839,24 +697,6 @@ instance FromHttpApiData SetCookie where
839697 parseUrlPiece = parseHeader . encodeUtf8
840698 parseHeader = Right . parseSetCookie
841699
842−-- | Note: this instance is not polykinded
843−instance FromHttpApiData a => FromHttpApiData (Tagged (b :: Type) a) where
844− parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
845− parseHeader = coerce (parseHeader :: ByteString -> Either Text a)
846− parseQueryParam = coerce (parseQueryParam :: Text -> Either Text a)
847−
848−-- | @since 0.4.2
849−instance FromHttpApiData a => FromHttpApiData (Const a b) where
850− parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
851− parseHeader = coerce (parseHeader :: ByteString -> Either Text a)
852− parseQueryParam = coerce (parseQueryParam :: Text -> Either Text a)
853−
854−-- | @since 0.4.2
855−instance FromHttpApiData a => FromHttpApiData (Identity a) where
856− parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
857− parseHeader = coerce (parseHeader :: ByteString -> Either Text a)
858− parseQueryParam = coerce (parseQueryParam :: Text -> Either Text a)
859−
860700-------------------------------------------------------------------------------
861701-- Attoparsec helpers
862702-------------------------------------------------------------------------------