@@ -1,70 +1,86 @@
1−{-# LANGUAGE CPP #-}
2−{-# LANGUAGE DefaultSignatures #-}
3−{-# LANGUAGE DeriveDataTypeable #-}
4−{-# LANGUAGE DeriveFunctor #-}
5−{-# LANGUAGE DeriveFoldable #-}
6−{-# LANGUAGE DeriveTraversable #-}
7−{-# LANGUAGE FlexibleInstances #-}
8−{-# LANGUAGE OverloadedStrings #-}
9−{-# LANGUAGE ScopedTypeVariables #-}
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DefaultSignatures #-}
3+{-# LANGUAGE DeriveDataTypeable #-}
4+{-# LANGUAGE DeriveFoldable #-}
5+{-# LANGUAGE DeriveFunctor #-}
6+{-# LANGUAGE DeriveTraversable #-}
7+{-# LANGUAGE FlexibleInstances #-}
8+{-# LANGUAGE KindSignatures #-}
9+{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE PolyKinds #-}
11+{-# LANGUAGE ScopedTypeVariables #-}
1012{-# LANGUAGE TypeSynonymInstances #-}
1113-- |
1214-- Convert Haskell values to and from HTTP API data
1315-- such as URL pieces, headers and query parameters.
1416module Web.Internal.HttpApiData where
1517
16−#if __GLASGOW_HASKELL__ < 710
17−import Control.Applicative
18−import Data.Foldable (Foldable)
19−import Data.Traversable (Traversable(traverse))
20−#endif
21−
22−import Control.Arrow ((&&&), left)
23−import Control.Monad ((<=<))
24−
25−import Data.Monoid
26−import Data.ByteString (ByteString)
27−import qualified Data.ByteString as BS
28−
29−import Data.Int
30−import Data.Word
31−
32−import Data.Text (Text)
33−import Data.Text.Encoding (encodeUtf8, decodeUtf8')
34−import Data.Text.Read (signed, decimal, rational, Reader)
35−import qualified Data.Text as T
36−import qualified Data.Text.Lazy as L
37−
38−import Data.Time.Locale.Compat
39−import Data.Time
40−import Data.Version
41−
42−#if MIN_VERSION_base(4,8,0)
43−import Data.Void
44−import Numeric.Natural
45−#endif
46−
47−import Text.Read (readMaybe)
48−import Text.ParserCombinators.ReadP (readP_to_S)
18+import Prelude ()
19+import Prelude.Compat
20+
21+import Control.Applicative (Const(Const))
22+import Control.Arrow (left, (&&&))
23+import Control.Monad ((<=<))
24+import qualified Data.Attoparsec.Text as Atto
25+import qualified Data.Attoparsec.Time as Atto
26+import Data.ByteString (ByteString)
27+import qualified Data.ByteString as BS
28+import qualified Data.ByteString.Builder as BS
29+import qualified Data.ByteString.Lazy as LBS
30+import Data.Coerce (coerce)
31+import Data.Data (Data)
32+import 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 (..))
43+import Data.Text (Text)
44+import qualified Data.Text as T
45+import Data.Text.Encoding (decodeUtf8', decodeUtf8With,
46+ encodeUtf8)
47+import Data.Text.Encoding.Error (lenientDecode)
48+import qualified Data.Text.Lazy as L
49+import Data.Text.Read (Reader, decimal, rational,
50+ 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)
70+import Text.ParserCombinators.ReadP (readP_to_S)
71+import Text.Read (readMaybe)
72+import Web.Cookie (SetCookie, parseSetCookie,
73+ renderSetCookie)
4974
5075#if USE_TEXT_SHOW
51−import TextShow (TextShow, showt)
76+import TextShow (TextShow, showt)
5277#endif
5378
54−import qualified Data.UUID.Types as UUID
55−
56−import Data.Typeable (Typeable)
57−import Data.Data (Data)
58−import qualified Data.ByteString.Builder as BS
59−import qualified Network.HTTP.Types as H
60−
61−import qualified Data.Attoparsec.Text as Atto
62−import qualified Data.Attoparsec.Time as Atto
63−
64−
6579-- $setup
6680-- >>> data BasicAuthToken = BasicAuthToken Text deriving (Show)
6781-- >>> instance FromHttpApiData BasicAuthToken where parseHeader h = BasicAuthToken <$> parseHeaderWithPrefix "Basic " h; parseQueryParam p = BasicAuthToken <$> parseQueryParam p
82+-- >>> import Data.Time.Compat
83+-- >>> import Data.Version
6884
6985-- | Convert value to HTTP API data.
7086--
@@ -80,7 +96,7 @@ class ToHttpApiData a where
8096 -- The default definition uses 'H.encodePathSegmentsRelative',
8197 -- but this may be overriden with a more efficient version.
8298 toEncodedUrlPiece :: a -> BS.Builder
83− toEncodedUrlPiece = H.encodePathSegmentsRelative . (:[]) . toUrlPiece
99+ toEncodedUrlPiece = H.urlEncodeBuilder True . encodeUtf8 . toUrlPiece
84100
85101 -- | Convert to HTTP header value.
86102 toHeader :: a -> ByteString
@@ -322,7 +338,7 @@ lookupBoundedEnumOf f = flip lookup (map (f &&& id) [minBound..maxBound])
322338-- >>> parseBoundedEnumOf toUrlPiece "true" :: Either Text Bool
323339-- Right True
324340--
325−-- For case sensitive parser see 'parseBoundedEnumOfI'.
341+-- For case insensitive parser see 'parseBoundedEnumOfI'.
326342parseBoundedEnumOf :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
327343parseBoundedEnumOf = parseMaybeTextData . lookupBoundedEnumOf
328344
@@ -367,7 +383,7 @@ parseBoundedHeader bs = case lookupBoundedEnumOf toHeader bs of
367383-- Right 1991-06-02
368384--
369385-- This parser is case sensitive and will not match @'showTextData'@
370−-- in presense of letters:
386+-- in presence of letters:
371387--
372388-- >>> readTextData (showTextData True) :: Either Text Bool
373389-- Left "could not parse: `true'"
@@ -423,10 +439,8 @@ instance ToHttpApiData Version where
423439 toUrlPiece = T.pack . showVersion
424440 toEncodedUrlPiece = unsafeToEncodedUrlPiece
425441
426−#if MIN_VERSION_base(4,8,0)
427442instance ToHttpApiData Void where toUrlPiece = absurd
428443instance ToHttpApiData Natural where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
429−#endif
430444
431445instance ToHttpApiData Bool where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece
432446instance ToHttpApiData Ordering where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece
@@ -445,6 +459,9 @@ instance ToHttpApiData Word16 where toUrlPiece = showt; toEncodedUrlPiece = un
445459instance ToHttpApiData Word32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
446460instance ToHttpApiData Word64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
447461
462+-- | Note: this instance is not polykinded
463+instance F.HasResolution a => ToHttpApiData (F.Fixed (a :: Type)) where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
464+
448465-- |
449466-- >>> toUrlPiece (fromGregorian 2015 10 03)
450467-- "2015-10-03"
@@ -456,56 +473,135 @@ timeToUrlPiece :: FormatTime t => String -> t -> Text
456473timeToUrlPiece fmt = T.pack . formatTime defaultTimeLocale (iso8601DateFormat (Just fmt))
457474
458475-- |
459−-- >>> toUrlPiece $ LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 01)
460−-- "2015-10-03T14:55:01"
476+-- >>> toUrlPiece $ TimeOfDay 14 55 23.1
477+-- "14:55:23.1"
478+instance ToHttpApiData TimeOfDay where
479+ toUrlPiece = T.pack . formatTime defaultTimeLocale "%H:%M:%S%Q"
480+ toEncodedUrlPiece = unsafeToEncodedUrlPiece
481+
482+-- |
483+-- >>> toUrlPiece $ LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 21.687)
484+-- "2015-10-03T14:55:21.687"
461485instance ToHttpApiData LocalTime where
462− toUrlPiece = timeToUrlPiece "%H:%M:%S"
486+ toUrlPiece = timeToUrlPiece "%H:%M:%S%Q"
463487 toEncodedUrlPiece = unsafeToEncodedUrlPiece
464488
465489-- |
466−-- >>> toUrlPiece $ ZonedTime (LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 01)) utc
467−-- "2015-10-03T14:55:01+0000"
490+-- >>> toUrlPiece $ ZonedTime (LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 51.001)) utc
491+-- "2015-10-03T14:55:51.001+0000"
468492instance ToHttpApiData ZonedTime where
469− toUrlPiece = timeToUrlPiece "%H:%M:%S%z"
493+ toUrlPiece = timeToUrlPiece "%H:%M:%S%Q%z"
470494 toEncodedUrlPiece = unsafeToEncodedUrlPiece
471495
472496-- |
473−-- >>> toUrlPiece $ UTCTime (fromGregorian 2015 10 03) 864
474−-- "2015-10-03T00:14:24Z"
497+-- >>> toUrlPiece $ UTCTime (fromGregorian 2015 10 03) 864.5
498+-- "2015-10-03T00:14:24.5Z"
475499instance ToHttpApiData UTCTime where
476− toUrlPiece = timeToUrlPiece "%H:%M:%SZ"
500+ toUrlPiece = timeToUrlPiece "%H:%M:%S%QZ"
477501 toEncodedUrlPiece = unsafeToEncodedUrlPiece
478502
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+
479554instance ToHttpApiData NominalDiffTime where
480− toUrlPiece = toUrlPiece . (floor :: NominalDiffTime -> Integer)
555+ toUrlPiece = toUrlPiece . nominalDiffTimeToSeconds
481556 toEncodedUrlPiece = unsafeToEncodedUrlPiece
482557
483558instance ToHttpApiData String where toUrlPiece = T.pack
484559instance ToHttpApiData Text where toUrlPiece = id
485560instance ToHttpApiData L.Text where toUrlPiece = L.toStrict
486561
487−instance ToHttpApiData All where toUrlPiece = toUrlPiece . getAll; toEncodedUrlPiece = toEncodedUrlPiece . getAll
488−instance ToHttpApiData Any where toUrlPiece = toUrlPiece . getAny; toEncodedUrlPiece = toEncodedUrlPiece . getAny
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)
489569
490570instance ToHttpApiData a => ToHttpApiData (Dual a) where
491− toUrlPiece = toUrlPiece . getDual
492− toEncodedUrlPiece = toEncodedUrlPiece . getDual
571+ toUrlPiece = coerce (toUrlPiece :: a -> Text)
572+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
493573
494574instance ToHttpApiData a => ToHttpApiData (Sum a) where
495− toUrlPiece = toUrlPiece . getSum
496− toEncodedUrlPiece = toEncodedUrlPiece . getSum
575+ toUrlPiece = coerce (toUrlPiece :: a -> Text)
576+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
497577
498578instance ToHttpApiData a => ToHttpApiData (Product a) where
499− toUrlPiece = toUrlPiece . getProduct
500− toEncodedUrlPiece = toEncodedUrlPiece . getProduct
579+ toUrlPiece = coerce (toUrlPiece :: a -> Text)
580+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
501581
502582instance ToHttpApiData a => ToHttpApiData (First a) where
503− toUrlPiece = toUrlPiece . getFirst
504− toEncodedUrlPiece = toEncodedUrlPiece . getFirst
583+ toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text)
584+ toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)
505585
506586instance ToHttpApiData a => ToHttpApiData (Last a) where
507− toUrlPiece = toUrlPiece . getLast
508− toEncodedUrlPiece = toEncodedUrlPiece . getLast
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)
509605
510606-- |
511607-- >>> toUrlPiece (Just "Hello")
@@ -523,6 +619,41 @@ instance (ToHttpApiData a, ToHttpApiData b) => ToHttpApiData (Either a b) where
523619 toUrlPiece (Left x) = "left " <> toUrlPiece x
524620 toUrlPiece (Right x) = "right " <> toUrlPiece x
525621
622+-- | /Note:/ this instance works correctly for alphanumeric name and value
623+--
624+-- >>> let Right c = parseUrlPiece "SESSID=r2t5uvjq435r4q7ib3vtdjq120" :: Either Text SetCookie
625+-- >>> toUrlPiece c
626+-- "SESSID=r2t5uvjq435r4q7ib3vtdjq120"
627+--
628+-- >>> toHeader c
629+-- "SESSID=r2t5uvjq435r4q7ib3vtdjq120"
630+--
631+instance ToHttpApiData SetCookie where
632+ toUrlPiece = decodeUtf8With lenientDecode . toHeader
633+ toHeader = LBS.toStrict . BS.toLazyByteString . renderSetCookie
634+ -- toEncodedUrlPiece = renderSetCookie -- doesn't do things.
635+
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+
526657-- |
527658-- >>> parseUrlPiece "_" :: Either Text ()
528659-- Right ()
@@ -534,7 +665,7 @@ instance FromHttpApiData Char where
534665 parseUrlPiece s =
535666 case T.uncons s of
536667 Just (c, s') | T.null s' -> pure c
537− _ -> defaultParseError s
668+ _ -> defaultParseError s
538669
539670-- |
540671-- >>> showVersion <$> parseUrlPiece "1.2.3"
@@ -545,7 +676,6 @@ instance FromHttpApiData Version where
545676 ((x, ""):_) -> pure x
546677 _ -> defaultParseError s
547678
548−#if MIN_VERSION_base(4,8,0)
549679-- | Parsing a @'Void'@ value is always an error, considering @'Void'@ as a data type with no constructors.
550680instance FromHttpApiData Void where
551681 parseUrlPiece _ = Left "Void cannot be parsed!"
@@ -556,7 +686,6 @@ instance FromHttpApiData Natural where
556686 if n < 0
557687 then Left ("underflow: " <> s <> " (should be a non-negative integer)")
558688 else Right (fromInteger n)
559−#endif
560689
561690instance FromHttpApiData Bool where parseUrlPiece = parseBoundedUrlPiece
562691instance FromHttpApiData Ordering where parseUrlPiece = parseBoundedUrlPiece
@@ -577,10 +706,19 @@ instance FromHttpApiData String where parseUrlPiece = Right . T.unpack
577706instance FromHttpApiData Text where parseUrlPiece = Right
578707instance FromHttpApiData L.Text where parseUrlPiece = Right . L.fromStrict
579708
709+-- | Note: this instance is not polykinded
710+instance F.HasResolution a => FromHttpApiData (F.Fixed (a :: Type)) where
711+ parseUrlPiece = runReader rational
712+
580713-- |
581714-- >>> toGregorian <$> parseUrlPiece "2016-12-01"
582715-- Right (2016,12,1)
583−instance FromHttpApiData Day where parseUrlPiece = runAtto Atto.day
716+instance FromHttpApiData Day where parseUrlPiece = runAtto Atto.day
717+
718+-- |
719+-- >>> parseUrlPiece "14:55:01.333" :: Either Text TimeOfDay
720+-- Right 14:55:01.333
721+instance FromHttpApiData TimeOfDay where parseUrlPiece = runAtto Atto.timeOfDay
584722
585723-- |
586724-- >>> parseUrlPiece "2015-10-03T14:55:01" :: Either Text LocalTime
@@ -600,16 +738,57 @@ instance FromHttpApiData ZonedTime where parseUrlPiece = runAtto Atto.zonedTime
600738-- Right 2015-10-03 00:14:24 UTC
601739instance FromHttpApiData UTCTime where parseUrlPiece = runAtto Atto.utcTime
602740
603−instance FromHttpApiData NominalDiffTime where parseUrlPiece = fmap fromInteger . parseUrlPiece
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] ]
604751
605−instance FromHttpApiData All where parseUrlPiece = fmap All . parseUrlPiece
606−instance FromHttpApiData Any where parseUrlPiece = fmap Any . parseUrlPiece
607752
608−instance FromHttpApiData a => FromHttpApiData (Dual a) where parseUrlPiece = fmap Dual . parseUrlPiece
609−instance FromHttpApiData a => FromHttpApiData (Sum a) where parseUrlPiece = fmap Sum . parseUrlPiece
610−instance FromHttpApiData a => FromHttpApiData (Product a) where parseUrlPiece = fmap Product . parseUrlPiece
611−instance FromHttpApiData a => FromHttpApiData (First a) where parseUrlPiece = fmap First . parseUrlPiece
612−instance FromHttpApiData a => FromHttpApiData (Last a) where parseUrlPiece = fmap Last . parseUrlPiece
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
759+
760+-- |
761+-- >>> parseUrlPiece "2021-q1" :: Either Text Quarter
762+-- Right 2021-Q1
763+instance FromHttpApiData Quarter where parseUrlPiece = runAtto Atto.quarter
764+
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)
613792
614793-- |
615794-- >>> parseUrlPiece "Just 123" :: Either Text (Maybe Int)
@@ -652,6 +831,32 @@ instance FromHttpApiData a => FromHttpApiData (LenientData a) where
652831 parseHeader = Right . LenientData . parseHeader
653832 parseQueryParam = Right . LenientData . parseQueryParam
654833
834+-- | /Note:/ this instance works correctly for alphanumeric name and value
835+--
836+-- >>> parseUrlPiece "SESSID=r2t5uvjq435r4q7ib3vtdjq120" :: Either Text SetCookie
837+-- Right (SetCookie {setCookieName = "SESSID", setCookieValue = "r2t5uvjq435r4q7ib3vtdjq120", setCookiePath = Nothing, setCookieExpires = Nothing, setCookieMaxAge = Nothing, setCookieDomain = Nothing, setCookieHttpOnly = False, setCookieSecure = False, setCookieSameSite = Nothing})
838+instance FromHttpApiData SetCookie where
839+ parseUrlPiece = parseHeader . encodeUtf8
840+ parseHeader = Right . parseSetCookie
841+
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+
655860-------------------------------------------------------------------------------
656861-- Attoparsec helpers
657862-------------------------------------------------------------------------------