@@ -5,7 +5,9 @@
5{-# LANGUAGE DeriveFunctor #-}
5{-# LANGUAGE DeriveFunctor #-}
6{-# LANGUAGE DeriveTraversable #-}
6{-# LANGUAGE DeriveTraversable #-}
7{-# LANGUAGE FlexibleInstances #-}
7{-# LANGUAGE FlexibleInstances #-}
8+{-# LANGUAGE KindSignatures #-}
8{-# LANGUAGE OverloadedStrings #-}
9{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE PolyKinds #-}
9{-# LANGUAGE ScopedTypeVariables #-}
11{-# LANGUAGE ScopedTypeVariables #-}
10{-# LANGUAGE TypeSynonymInstances #-}
12{-# LANGUAGE TypeSynonymInstances #-}
@@ -13,21 +15,31 @@
13-- such as URL pieces, headers and query parameters.
15-- such as URL pieces, headers and query parameters.
14module Web.Internal.HttpApiData where
16module Web.Internal.HttpApiData where
16−import Prelude ()
18+import Prelude ()
17import Prelude.Compat
19import Prelude.Compat
21+import Control.Applicative (Const(Const))
19import Control.Arrow (left, (&&&))
22import Control.Arrow (left, (&&&))
20import Control.Monad ((<=<))
23import Control.Monad ((<=<))
21−
24+import qualified Data.Attoparsec.Text as Atto
25+import qualified Data.Attoparsec.Time as Atto
22import Data.ByteString (ByteString)
26import Data.ByteString (ByteString)
23import qualified Data.ByteString as BS
27import qualified Data.ByteString as BS
28+import qualified Data.ByteString.Builder as BS
24import qualified Data.ByteString.Lazy as LBS
29import qualified Data.ByteString.Lazy as LBS
25−import Data.Monoid
30+import Data.Coerce (coerce)
26−
31+import Data.Data (Data)
27import qualified Data.Fixed as F
32import qualified Data.Fixed as F
28−import Data.Int
33+import Data.Functor.Identity (Identity(Identity))
34+import Data.Int (Int16, Int32, Int64, Int8)
29−import Data.Word
35+import Data.Kind (Type)
30−
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 (..))
31import Data.Text (Text)
43import Data.Text (Text)
32import qualified Data.Text as T
44import qualified Data.Text as T
33import Data.Text.Encoding (decodeUtf8', decodeUtf8With,
45import Data.Text.Encoding (decodeUtf8', decodeUtf8With,
@@ -36,43 +48,39 @@ import Data.Text.Encoding.Error (lenientDecode)
36import qualified Data.Text.Lazy as L
48import qualified Data.Text.Lazy as L
37import Data.Text.Read (Reader, decimal, rational,
49import Data.Text.Read (Reader, decimal, rational,
41−import Data.Time.Locale.Compat
51+import Data.Time.Compat (Day, FormatTime, LocalTime,
42−
52+ NominalDiffTime, TimeOfDay,
43−#if MIN_VERSION_time(1,9,1)
53+ UTCTime, ZonedTime, formatTime,
44−import Data.Time (nominalDiffTimeToSeconds, secondsToNominalDiffTime)
55+ nominalDiffTimeToSeconds,
45−#endif
56+ secondsToNominalDiffTime)
46−
57+import Data.Time.Format.Compat (defaultTimeLocale,
59+import Data.Time.Calendar.Month.Compat (Month)
60+import Data.Time.Calendar.Quarter.Compat (Quarter, QuarterOfYear (..),
62+import Data.Typeable (Typeable)
63+import qualified Data.UUID.Types as UUID
47−import Data.Version
64+import Data.Version (Version, parseVersion,
49−import Data.Void
66+import Data.Void (Void, absurd)
67+import Data.Word (Word16, Word32, Word64, Word8)
68+import qualified Network.HTTP.Types as H
50−import Numeric.Natural
69+import Numeric.Natural (Natural)
52import Text.ParserCombinators.ReadP (readP_to_S)
70import Text.ParserCombinators.ReadP (readP_to_S)
53import Text.Read (readMaybe)
71import Text.Read (readMaybe)
72+import Web.Cookie (SetCookie, parseSetCookie,
55#if USE_TEXT_SHOW
75#if USE_TEXT_SHOW
56import TextShow (TextShow, showt)
76import TextShow (TextShow, showt)
59−import qualified Data.UUID.Types as UUID
61−import qualified Data.ByteString.Builder as BS
62−import Data.Data (Data)
63−import Data.Typeable (Typeable)
64−import qualified Network.HTTP.Types as H
66−import qualified Data.Attoparsec.Text as Atto
67−import qualified Data.Attoparsec.Time as Atto
69−import Web.Cookie (SetCookie, parseSetCookie,
74-- >>> data BasicAuthToken = BasicAuthToken Text deriving (Show)
80-- >>> data BasicAuthToken = BasicAuthToken Text deriving (Show)
75-- >>> instance FromHttpApiData BasicAuthToken where parseHeader h = BasicAuthToken <$> parseHeaderWithPrefix "Basic " h; parseQueryParam p = BasicAuthToken <$> parseQueryParam p
81-- >>> instance FromHttpApiData BasicAuthToken where parseHeader h = BasicAuthToken <$> parseHeaderWithPrefix "Basic " h; parseQueryParam p = BasicAuthToken <$> parseQueryParam p
82+-- >>> import Data.Time.Compat
83+-- >>> import Data.Version
77-- | Convert value to HTTP API data.
85-- | Convert value to HTTP API data.
@@ -88,7 +96,7 @@ class ToHttpApiData a where
88 -- The default definition uses 'H.encodePathSegmentsRelative',
96 -- The default definition uses 'H.encodePathSegmentsRelative',
89 -- but this may be overriden with a more efficient version.
97 -- but this may be overriden with a more efficient version.
90 toEncodedUrlPiece :: a -> BS.Builder
98 toEncodedUrlPiece :: a -> BS.Builder
91− toEncodedUrlPiece = H.encodePathSegmentsRelative . (:[]) . toUrlPiece
99+ toEncodedUrlPiece = H.urlEncodeBuilder True . encodeUtf8 . toUrlPiece
93 -- | Convert to HTTP header value.
101 -- | Convert to HTTP header value.
94 toHeader :: a -> ByteString
102 toHeader :: a -> ByteString
@@ -330,7 +338,7 @@ lookupBoundedEnumOf f = flip lookup (map (f &&& id) [minBound..maxBound])
330-- >>> parseBoundedEnumOf toUrlPiece "true" :: Either Text Bool
338-- >>> parseBoundedEnumOf toUrlPiece "true" :: Either Text Bool
331-- Right True
339-- Right True
333−-- For case sensitive parser see 'parseBoundedEnumOfI'.
341+-- For case insensitive parser see 'parseBoundedEnumOfI'.
334parseBoundedEnumOf :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
342parseBoundedEnumOf :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
335parseBoundedEnumOf = parseMaybeTextData . lookupBoundedEnumOf
343parseBoundedEnumOf = parseMaybeTextData . lookupBoundedEnumOf
@@ -451,7 +459,8 @@ instance ToHttpApiData Word16 where toUrlPiece = showt; toEncodedUrlPiece = un
451instance ToHttpApiData Word32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
459instance ToHttpApiData Word32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
452instance ToHttpApiData Word64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
460instance ToHttpApiData Word64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
462+-- | Note: this instance is not polykinded
454−instance F.HasResolution a => ToHttpApiData (F.Fixed a) where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
463+instance F.HasResolution a => ToHttpApiData (F.Fixed (a :: Type)) where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
457-- >>> toUrlPiece (fromGregorian 2015 10 03)
466-- >>> toUrlPiece (fromGregorian 2015 10 03)
@@ -491,56 +500,108 @@ instance ToHttpApiData UTCTime where
491 toUrlPiece = timeToUrlPiece "%H:%M:%S%QZ"
500 toUrlPiece = timeToUrlPiece "%H:%M:%S%QZ"
492 toEncodedUrlPiece = unsafeToEncodedUrlPiece
501 toEncodedUrlPiece = unsafeToEncodedUrlPiece
494−-- The CPP in both this function and the function after it are to avoid
503+-- |
495−-- exporting @nominalDiffTimeToSeconds@ and @secondsToNominalDiffTime@,
504+-- >>> toUrlPiece Monday
496−-- since these names are already used by @Data.Time@ from the @time@ library
505+-- "monday"
497−-- starting in version @1.9.1@.
506+instance ToHttpApiData DayOfWeek where
498−nominalDiffTimeToSecs :: NominalDiffTime -> F.Pico
507+ toUrlPiece Monday = "monday"
499−nominalDiffTimeToSecs =
508+ toUrlPiece Tuesday = "tuesday"
500−#if !MIN_VERSION_time(1,9,1)
509+ toUrlPiece Wednesday = "wednesday"
501− realToFrac
510+ toUrlPiece Thursday = "thursday"
502−#else
511+ toUrlPiece Friday = "friday"
503− nominalDiffTimeToSeconds
512+ toUrlPiece Saturday = "saturday"
504−#endif
513+ toUrlPiece Sunday = "sunday"
506−secsToNominalDiffTime :: F.Pico -> NominalDiffTime
515+ toEncodedUrlPiece = unsafeToEncodedUrlPiece
507−secsToNominalDiffTime =
516+
508−#if !MIN_VERSION_time(1,9,1)
517+-- |
509− realToFrac
518+-- >>> toUrlPiece Q4
511− secondsToNominalDiffTime
520+instance ToHttpApiData QuarterOfYear where
512−#endif
521+ toUrlPiece Q1 = "q1"
522+ toUrlPiece Q2 = "q2"
523+ toUrlPiece Q3 = "q3"
524+ toUrlPiece Q4 = "q4"
527+-- >>> import Data.Time.Calendar.Quarter.Compat (Quarter (..))
528+-- >>> MkQuarter 8040
531+-- >>> toUrlPiece $ MkQuarter 8040
534+instance ToHttpApiData Quarter where
535+ toUrlPiece q = case toYearQuarter q of
536+ (y, qoy) -> T.pack (show y ++ "-" ++ f qoy)
544+-- >>> import Data.Time.Calendar.Month.Compat (Month (..))
548+-- >>> toUrlPiece $ MkMonth 24482
551+instance ToHttpApiData Month where
552+ toUrlPiece = T.pack . formatTime defaultTimeLocale "%Y-%m"
514instance ToHttpApiData NominalDiffTime where
554instance ToHttpApiData NominalDiffTime where
515− toUrlPiece = toUrlPiece . nominalDiffTimeToSecs
555+ toUrlPiece = toUrlPiece . nominalDiffTimeToSeconds
516 toEncodedUrlPiece = unsafeToEncodedUrlPiece
556 toEncodedUrlPiece = unsafeToEncodedUrlPiece
518instance ToHttpApiData String where toUrlPiece = T.pack
558instance ToHttpApiData String where toUrlPiece = T.pack
519instance ToHttpApiData Text where toUrlPiece = id
559instance ToHttpApiData Text where toUrlPiece = id
520instance ToHttpApiData L.Text where toUrlPiece = L.toStrict
560instance ToHttpApiData L.Text where toUrlPiece = L.toStrict
522−instance ToHttpApiData All where toUrlPiece = toUrlPiece . getAll; toEncodedUrlPiece = toEncodedUrlPiece . getAll
562+instance ToHttpApiData All where
563+ toUrlPiece = coerce (toUrlPiece :: Bool -> Text)
564+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Bool -> BS.Builder)
523−instance ToHttpApiData Any where toUrlPiece = toUrlPiece . getAny; toEncodedUrlPiece = toEncodedUrlPiece . getAny
566+instance ToHttpApiData Any where
567+ toUrlPiece = coerce (toUrlPiece :: Bool -> Text)
568+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Bool -> BS.Builder)
525instance ToHttpApiData a => ToHttpApiData (Dual a) where
570instance ToHttpApiData a => ToHttpApiData (Dual a) where
526− toUrlPiece = toUrlPiece . getDual
571+ toUrlPiece = coerce (toUrlPiece :: a -> Text)
527− toEncodedUrlPiece = toEncodedUrlPiece . getDual
572+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
529instance ToHttpApiData a => ToHttpApiData (Sum a) where
574instance ToHttpApiData a => ToHttpApiData (Sum a) where
530− toUrlPiece = toUrlPiece . getSum
575+ toUrlPiece = coerce (toUrlPiece :: a -> Text)
531− toEncodedUrlPiece = toEncodedUrlPiece . getSum
576+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
533instance ToHttpApiData a => ToHttpApiData (Product a) where
578instance ToHttpApiData a => ToHttpApiData (Product a) where
534− toUrlPiece = toUrlPiece . getProduct
579+ toUrlPiece = coerce (toUrlPiece :: a -> Text)
535− toEncodedUrlPiece = toEncodedUrlPiece . getProduct
580+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
537instance ToHttpApiData a => ToHttpApiData (First a) where
582instance ToHttpApiData a => ToHttpApiData (First a) where
538− toUrlPiece = toUrlPiece . getFirst
583+ toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text)
539− toEncodedUrlPiece = toEncodedUrlPiece . getFirst
584+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)
541instance ToHttpApiData a => ToHttpApiData (Last a) where
586instance ToHttpApiData a => ToHttpApiData (Last a) where
542− toUrlPiece = toUrlPiece . getLast
587+ toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text)
588+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)
590+instance ToHttpApiData a => ToHttpApiData (Semi.Min a) where
591+ toUrlPiece = coerce (toUrlPiece :: a -> Text)
592+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
594+instance ToHttpApiData a => ToHttpApiData (Semi.Max a) where
595+ toUrlPiece = coerce (toUrlPiece :: a -> Text)
596+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
598+instance ToHttpApiData a => ToHttpApiData (Semi.First a) where
599+ toUrlPiece = coerce (toUrlPiece :: a -> Text)
543− toEncodedUrlPiece = toEncodedUrlPiece . getLast
600+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
602+instance ToHttpApiData a => ToHttpApiData (Semi.Last a) where
603+ toUrlPiece = coerce (toUrlPiece :: a -> Text)
604+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
546-- >>> toUrlPiece (Just "Hello")
607-- >>> toUrlPiece (Just "Hello")
@@ -572,6 +633,27 @@ instance ToHttpApiData SetCookie where
572 toHeader = LBS.toStrict . BS.toLazyByteString . renderSetCookie
633 toHeader = LBS.toStrict . BS.toLazyByteString . renderSetCookie
573 -- toEncodedUrlPiece = renderSetCookie -- doesn't do things.
634 -- toEncodedUrlPiece = renderSetCookie -- doesn't do things.
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)
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)
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)
576-- >>> parseUrlPiece "_" :: Either Text ()
658-- >>> parseUrlPiece "_" :: Either Text ()
577-- Right ()
659-- Right ()
@@ -624,7 +706,8 @@ instance FromHttpApiData String where parseUrlPiece = Right . T.unpack
624instance FromHttpApiData Text where parseUrlPiece = Right
706instance FromHttpApiData Text where parseUrlPiece = Right
625instance FromHttpApiData L.Text where parseUrlPiece = Right . L.fromStrict
707instance FromHttpApiData L.Text where parseUrlPiece = Right . L.fromStrict
709+-- | Note: this instance is not polykinded
627−instance F.HasResolution a => FromHttpApiData (F.Fixed a) where
710+instance F.HasResolution a => FromHttpApiData (F.Fixed (a :: Type)) where
628 parseUrlPiece = runReader rational
711 parseUrlPiece = runReader rational
@@ -655,16 +738,57 @@ instance FromHttpApiData ZonedTime where parseUrlPiece = runAtto Atto.zonedTime
655-- Right 2015-10-03 00:14:24 UTC
738-- Right 2015-10-03 00:14:24 UTC
656instance FromHttpApiData UTCTime where parseUrlPiece = runAtto Atto.utcTime
739instance FromHttpApiData UTCTime where parseUrlPiece = runAtto Atto.utcTime
742+-- >>> parseUrlPiece "Monday" :: Either Text DayOfWeek
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
749+ m :: Map.Map Text DayOfWeek
750+ m = Map.fromList [ (toUrlPiece dow, dow) | dow <- [Monday .. Sunday] ]
658−instance FromHttpApiData NominalDiffTime where parseUrlPiece = fmap secsToNominalDiffTime . parseUrlPiece
753+instance FromHttpApiData NominalDiffTime where parseUrlPiece = fmap secondsToNominalDiffTime . parseUrlPiece
756+-- >>> parseUrlPiece "2021-01" :: Either Text Month
758+instance FromHttpApiData Month where parseUrlPiece = runAtto Atto.month
660−instance FromHttpApiData All where parseUrlPiece = fmap All . parseUrlPiece
760+-- |
761+-- >>> parseUrlPiece "2021-q1" :: Either Text Quarter
661−instance FromHttpApiData Any where parseUrlPiece = fmap Any . parseUrlPiece
763+instance FromHttpApiData Quarter where parseUrlPiece = runAtto Atto.quarter
766+-- >>> parseUrlPiece "q2" :: Either Text QuarterOfYear
769+-- >>> parseUrlPiece "Q3" :: Either Text QuarterOfYear
771+instance FromHttpApiData QuarterOfYear where
772+ parseUrlPiece t = case T.toLower t of
777+ _ -> Left "Invalid quarter of year"
779+instance FromHttpApiData All where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text Bool)
780+instance FromHttpApiData Any where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text Bool)
663−instance FromHttpApiData a => FromHttpApiData (Dual a) where parseUrlPiece = fmap Dual . parseUrlPiece
782+instance FromHttpApiData a => FromHttpApiData (Dual a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
664−instance FromHttpApiData a => FromHttpApiData (Sum a) where parseUrlPiece = fmap Sum . parseUrlPiece
783+instance FromHttpApiData a => FromHttpApiData (Sum a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
665−instance FromHttpApiData a => FromHttpApiData (Product a) where parseUrlPiece = fmap Product . parseUrlPiece
784+instance FromHttpApiData a => FromHttpApiData (Product a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
666−instance FromHttpApiData a => FromHttpApiData (First a) where parseUrlPiece = fmap First . parseUrlPiece
785+instance FromHttpApiData a => FromHttpApiData (First a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text (Maybe a))
667−instance FromHttpApiData a => FromHttpApiData (Last a) where parseUrlPiece = fmap Last . parseUrlPiece
786+instance FromHttpApiData a => FromHttpApiData (Last a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text (Maybe a))
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)
670-- >>> parseUrlPiece "Just 123" :: Either Text (Maybe Int)
794-- >>> parseUrlPiece "Just 123" :: Either Text (Maybe Int)
@@ -715,6 +839,24 @@ instance FromHttpApiData SetCookie where
715 parseUrlPiece = parseHeader . encodeUtf8
839 parseUrlPiece = parseHeader . encodeUtf8
716 parseHeader = Right . parseSetCookie
840 parseHeader = Right . parseSetCookie
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)
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)
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)
718-------------------------------------------------------------------------------
860-------------------------------------------------------------------------------
719-- Attoparsec helpers
861-- Attoparsec helpers
720-------------------------------------------------------------------------------
862-------------------------------------------------------------------------------