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 #-}
12{-# LANGUAGE TypeSynonymInstances #-}
14-- Convert Haskell values to and from HTTP API data
15-- such as URL pieces, headers and query parameters.
16module Web.Internal.HttpApiData where
21import Control.Applicative (Const(Const))
22import Control.Arrow (left, (&&&))
23import Control.Monad ((<=<))
24import qualified Data.Attoparsec.Text as Atto
25import qualified Data.Attoparsec.Time as Atto
26import Data.ByteString (ByteString)
27import qualified Data.ByteString as BS
28import qualified Data.ByteString.Builder as BS
29import qualified Data.ByteString.Lazy as LBS
30import Data.Coerce (coerce)
31import Data.Data (Data)
32import qualified Data.Fixed as F
33import Data.Functor.Identity (Identity(Identity))
34import Data.Int (Int16, Int32, Int64, Int8)
35import qualified Data.Map as Map
36import Data.Monoid (All (..), Any (..), Dual (..),
37 First (..), Last (..),
38 Product (..), Sum (..))
39import Data.Semigroup (Semigroup (..))
40import qualified Data.Semigroup as Semi
41import Data.Tagged (Tagged (..))
42import Data.Text (Text)
43import qualified Data.Text as T
44import Data.Text.Encoding (decodeUtf8', decodeUtf8With,
46import Data.Text.Encoding.Error (lenientDecode)
47import qualified Data.Text.Lazy as L
48import Data.Text.Read (Reader, decimal, rational,
50import Data.Time.Compat (Day, FormatTime, LocalTime,
51 NominalDiffTime, TimeOfDay,
52 UTCTime, ZonedTime, formatTime,
54 nominalDiffTimeToSeconds,
55 secondsToNominalDiffTime)
56import Data.Time.Format.Compat (defaultTimeLocale,
58import Data.Typeable (Typeable)
59import qualified Data.UUID.Types as UUID
60import Data.Version (Version, parseVersion,
62import Data.Void (Void, absurd)
63import Data.Word (Word16, Word32, Word64, Word8)
64import qualified Network.HTTP.Types as H
65import Numeric.Natural (Natural)
66import Text.ParserCombinators.ReadP (readP_to_S)
67import Text.Read (readMaybe)
68import Web.Cookie (SetCookie, parseSetCookie,
71#if MIN_VERSION_base(4,9,0)
72import Data.Kind (Type)
78import TextShow (TextShow, showt)
82-- >>> data BasicAuthToken = BasicAuthToken Text deriving (Show)
83-- >>> instance FromHttpApiData BasicAuthToken where parseHeader h = BasicAuthToken <$> parseHeaderWithPrefix "Basic " h; parseQueryParam p = BasicAuthToken <$> parseQueryParam p
84-- >>> import Data.Time.Compat
85-- >>> import Data.Version
87-- | Convert value to HTTP API data.
89-- __WARNING__: Do not derive this using @DeriveAnyClass@ as the generated
90-- instance will loop indefinitely.
91class ToHttpApiData a where
92 {-# MINIMAL toUrlPiece | toQueryParam #-}
93 -- | Convert to URL path piece.
94 toUrlPiece :: a -> Text
95 toUrlPiece = toQueryParam
97 -- | Convert to a URL path piece, making sure to encode any special chars.
98 -- The default definition uses 'H.encodePathSegmentsRelative',
99 -- but this may be overriden with a more efficient version.
100 toEncodedUrlPiece :: a -> BS.Builder
101 toEncodedUrlPiece = H.encodePathSegmentsRelative . (:[]) . toUrlPiece
103 -- | Convert to HTTP header value.
104 toHeader :: a -> ByteString
105 toHeader = encodeUtf8 . toUrlPiece
107 -- | Convert to query param value.
108 toQueryParam :: a -> Text
109 toQueryParam = toUrlPiece
111-- | Parse value from HTTP API data.
113-- __WARNING__: Do not derive this using @DeriveAnyClass@ as the generated
114-- instance will loop indefinitely.
115class FromHttpApiData a where
116 {-# MINIMAL parseUrlPiece | parseQueryParam #-}
117 -- | Parse URL path piece.
118 parseUrlPiece :: Text -> Either Text a
119 parseUrlPiece = parseQueryParam
121 -- | Parse HTTP header value.
122 parseHeader :: ByteString -> Either Text a
123 parseHeader = parseUrlPiece <=< (left (T.pack . show) . decodeUtf8')
125 -- | Parse query param value.
126 parseQueryParam :: Text -> Either Text a
127 parseQueryParam = parseUrlPiece
129-- | Convert multiple values to a list of URL pieces.
131-- >>> toUrlPieces [1, 2, 3] :: [Text]
133toUrlPieces :: (Functor t, ToHttpApiData a) => t a -> t Text
134toUrlPieces = fmap toUrlPiece
136-- | Parse multiple URL pieces.
138-- >>> parseUrlPieces ["true", "false"] :: Either Text [Bool]
140-- >>> parseUrlPieces ["123", "hello", "world"] :: Either Text [Int]
141-- Left "could not parse: `hello' (input does not start with a digit)"
142parseUrlPieces :: (Traversable t, FromHttpApiData a) => t Text -> Either Text (t a)
143parseUrlPieces = traverse parseUrlPiece
145-- | Convert multiple values to a list of query parameter values.
147-- >>> toQueryParams [fromGregorian 2015 10 03, fromGregorian 2015 12 01] :: [Text]
148-- ["2015-10-03","2015-12-01"]
149toQueryParams :: (Functor t, ToHttpApiData a) => t a -> t Text
150toQueryParams = fmap toQueryParam
152-- | Parse multiple query parameters.
154-- >>> parseQueryParams ["1", "2", "3"] :: Either Text [Int]
156-- >>> parseQueryParams ["64", "128", "256"] :: Either Text [Word8]
157-- Left "out of bounds: `256' (should be between 0 and 255)"
158parseQueryParams :: (Traversable t, FromHttpApiData a) => t Text -> Either Text (t a)
159parseQueryParams = traverse parseQueryParam
161-- | Parse URL path piece in a @'Maybe'@.
163-- >>> parseUrlPieceMaybe "12" :: Maybe Int
165parseUrlPieceMaybe :: FromHttpApiData a => Text -> Maybe a
166parseUrlPieceMaybe = either (const Nothing) Just . parseUrlPiece
168-- | Parse HTTP header value in a @'Maybe'@.
170-- >>> parseHeaderMaybe "hello" :: Maybe Text
172parseHeaderMaybe :: FromHttpApiData a => ByteString -> Maybe a
173parseHeaderMaybe = either (const Nothing) Just . parseHeader
175-- | Parse query param value in a @'Maybe'@.
177-- >>> parseQueryParamMaybe "true" :: Maybe Bool
179parseQueryParamMaybe :: FromHttpApiData a => Text -> Maybe a
180parseQueryParamMaybe = either (const Nothing) Just . parseQueryParam
182-- | Default parsing error.
183defaultParseError :: Text -> Either Text a
184defaultParseError input = Left ("could not parse: `" <> input <> "'")
186-- | Convert @'Maybe'@ parser into @'Either' 'Text'@ parser with default error message.
187parseMaybeTextData :: (Text -> Maybe a) -> (Text -> Either Text a)
188parseMaybeTextData parse input =
190 Nothing -> defaultParseError input
191 Just val -> Right val
196-- Convert to URL piece using @'TextShow'@ instance.
197-- The result is always lower cased.
199-- >>> showTextData True
202-- This can be used as a default implementation for enumeration types:
205-- data MyData = Foo | Bar | Baz deriving (Generic)
207-- instance TextShow MyData where
208-- showt = genericShowt
210-- instance ToHttpApiData MyData where
211-- toUrlPiece = showTextData
213showTextData :: TextShow a => a -> Text
214showTextData = T.toLower . showt
218-- Convert to URL piece using @'Show'@ instance.
219-- The result is always lower cased.
221-- >>> showTextData True
224-- This can be used as a default implementation for enumeration types:
226-- >>> data MyData = Foo | Bar | Baz deriving (Show)
227-- >>> instance ToHttpApiData MyData where toUrlPiece = showTextData
230showTextData :: Show a => a -> Text
231showTextData = T.toLower . showt
233-- | Like @'show'@, but returns @'Text'@.
234showt :: Show a => a -> Text
238-- | /Case insensitive/.
240-- Parse given text case insensitive and then parse the rest of the input
241-- using @'parseUrlPiece'@.
243-- >>> parseUrlPieceWithPrefix "Just " "just 10" :: Either Text Int
245-- >>> parseUrlPieceWithPrefix "Left " "left" :: Either Text Bool
246-- Left "could not parse: `left'"
248-- This can be used to implement @'FromHttpApiData'@ for single field constructors:
250-- >>> data Foo = Foo Int deriving (Show)
251-- >>> instance FromHttpApiData Foo where parseUrlPiece s = Foo <$> parseUrlPieceWithPrefix "Foo " s
252-- >>> parseUrlPiece "foo 1" :: Either Text Foo
254parseUrlPieceWithPrefix :: FromHttpApiData a => Text -> Text -> Either Text a
255parseUrlPieceWithPrefix pattern input
256 | T.toLower pattern == T.toLower prefix = parseUrlPiece rest
257 | otherwise = defaultParseError input
259 (prefix, rest) = T.splitAt (T.length pattern) input
261-- | Parse given bytestring then parse the rest of the input using @'parseHeader'@.
264-- data BasicAuthToken = BasicAuthToken Text deriving (Show)
266-- instance FromHttpApiData BasicAuthToken where
267-- parseHeader h = BasicAuthToken \<$\> parseHeaderWithPrefix "Basic " h
268-- parseQueryParam p = BasicAuthToken \<$\> parseQueryParam p
271-- >>> parseHeader "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" :: Either Text BasicAuthToken
272-- Right (BasicAuthToken "QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
273parseHeaderWithPrefix :: FromHttpApiData a => ByteString -> ByteString -> Either Text a
274parseHeaderWithPrefix pattern input
275 | pattern `BS.isPrefixOf` input = parseHeader (BS.drop (BS.length pattern) input)
276 | otherwise = defaultParseError (showt input)
278-- | /Case insensitive/.
280-- Parse given text case insensitive and then parse the rest of the input
281-- using @'parseQueryParam'@.
283-- >>> parseQueryParamWithPrefix "z" "z10" :: Either Text Int
285parseQueryParamWithPrefix :: FromHttpApiData a => Text -> Text -> Either Text a
286parseQueryParamWithPrefix pattern input
287 | T.toLower pattern == T.toLower prefix = parseQueryParam rest
288 | otherwise = defaultParseError input
290 (prefix, rest) = T.splitAt (T.length pattern) input
293-- | /Case insensitive/.
295-- Parse values case insensitively based on @'TextShow'@ instance.
297-- >>> parseBoundedTextData "true" :: Either Text Bool
299-- >>> parseBoundedTextData "FALSE" :: Either Text Bool
302-- This can be used as a default implementation for enumeration types:
305-- data MyData = Foo | Bar | Baz deriving (Show, Bounded, Enum, Generic)
307-- instance TextShow MyData where
308-- showt = genericShowt
310-- instance FromHttpApiData MyData where
311-- parseUrlPiece = parseBoundedTextData
313parseBoundedTextData :: (TextShow a, Bounded a, Enum a) => Text -> Either Text a
315-- | /Case insensitive/.
317-- Parse values case insensitively based on @'Show'@ instance.
319-- >>> parseBoundedTextData "true" :: Either Text Bool
321-- >>> parseBoundedTextData "FALSE" :: Either Text Bool
324-- This can be used as a default implementation for enumeration types:
326-- >>> data MyData = Foo | Bar | Baz deriving (Show, Bounded, Enum)
327-- >>> instance FromHttpApiData MyData where parseUrlPiece = parseBoundedTextData
328-- >>> parseUrlPiece "foo" :: Either Text MyData
330parseBoundedTextData :: (Show a, Bounded a, Enum a) => Text -> Either Text a
332parseBoundedTextData = parseBoundedEnumOfI showTextData
334-- | Lookup values based on a precalculated mapping of their representations.
335lookupBoundedEnumOf :: (Bounded a, Enum a, Eq b) => (a -> b) -> b -> Maybe a
336lookupBoundedEnumOf f = flip lookup (map (f &&& id) [minBound..maxBound])
338-- | Parse values based on a precalculated mapping of their @'Text'@ representation.
340-- >>> parseBoundedEnumOf toUrlPiece "true" :: Either Text Bool
343-- For case insensitive parser see 'parseBoundedEnumOfI'.
344parseBoundedEnumOf :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
345parseBoundedEnumOf = parseMaybeTextData . lookupBoundedEnumOf
347-- | /Case insensitive/.
349-- Parse values case insensitively based on a precalculated mapping
350-- of their @'Text'@ representations.
352-- >>> parseBoundedEnumOfI toUrlPiece "FALSE" :: Either Text Bool
355-- For case sensitive parser see 'parseBoundedEnumOf'.
356parseBoundedEnumOfI :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
357parseBoundedEnumOfI f = parseBoundedEnumOf (T.toLower . f) . T.toLower
359-- | /Case insensitive/.
361-- Parse values case insensitively based on @'ToHttpApiData'@ instance.
362-- Uses @'toUrlPiece'@ to get possible values.
363parseBoundedUrlPiece :: (ToHttpApiData a, Bounded a, Enum a) => Text -> Either Text a
364parseBoundedUrlPiece = parseBoundedEnumOfI toUrlPiece
366-- | /Case insensitive/.
368-- Parse values case insensitively based on @'ToHttpApiData'@ instance.
369-- Uses @'toQueryParam'@ to get possible values.
370parseBoundedQueryParam :: (ToHttpApiData a, Bounded a, Enum a) => Text -> Either Text a
371parseBoundedQueryParam = parseBoundedEnumOfI toQueryParam
373-- | Parse values based on @'ToHttpApiData'@ instance.
374-- Uses @'toHeader'@ to get possible values.
375parseBoundedHeader :: (ToHttpApiData a, Bounded a, Enum a) => ByteString -> Either Text a
376parseBoundedHeader bs = case lookupBoundedEnumOf toHeader bs of
377 Nothing -> defaultParseError $ T.pack $ show bs
380-- | Parse URL piece using @'Read'@ instance.
382-- Use for types which do not involve letters:
384-- >>> readTextData "1991-06-02" :: Either Text Day
387-- This parser is case sensitive and will not match @'showTextData'@
388-- in presence of letters:
390-- >>> readTextData (showTextData True) :: Either Text Bool
391-- Left "could not parse: `true'"
393-- See @'parseBoundedTextData'@.
394readTextData :: Read a => Text -> Either Text a
395readTextData = parseMaybeTextData (readMaybe . T.unpack)
397-- | Run @'Reader'@ as HTTP API data parser.
398runReader :: Reader a -> Text -> Either Text a
399runReader reader input =
401 Left err -> Left ("could not parse: `" <> input <> "' (" <> T.pack err <> ")")
403 | T.null rest -> Right x
404 | otherwise -> defaultParseError input
406-- | Run @'Reader'@ to parse bounded integral value with bounds checking.
408-- >>> parseBounded decimal "256" :: Either Text Word8
409-- Left "out of bounds: `256' (should be between 0 and 255)"
410parseBounded :: forall a. (Bounded a, Integral a) => Reader Integer -> Text -> Either Text a
411parseBounded reader input = do
412 n <- runReader reader input
414 then Left ("out of bounds: `" <> input <> "' (should be between " <> showt l <> " and " <> showt h <> ")")
415 else Right (fromInteger n)
417 l = toInteger (minBound :: a)
418 h = toInteger (maxBound :: a)
420-- | Convert to a URL-encoded path piece using 'toUrlPiece'.
421-- /Note/: this function does not check if the result contains unescaped characters!
422-- This function can be used to override 'toEncodedUrlPiece' as a more efficient implementation
423-- when the resulting URL piece /never/ has to be escaped.
424unsafeToEncodedUrlPiece :: ToHttpApiData a => a -> BS.Builder
425unsafeToEncodedUrlPiece = BS.byteString . encodeUtf8 . toUrlPiece
430instance ToHttpApiData () where
432 toEncodedUrlPiece = unsafeToEncodedUrlPiece
434instance ToHttpApiData Char where
435 toUrlPiece = T.singleton
438-- >>> toUrlPiece (Version [1, 2, 3] [])
440instance ToHttpApiData Version where
441 toUrlPiece = T.pack . showVersion
442 toEncodedUrlPiece = unsafeToEncodedUrlPiece
444instance ToHttpApiData Void where toUrlPiece = absurd
445instance ToHttpApiData Natural where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
447instance ToHttpApiData Bool where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece
448instance ToHttpApiData Ordering where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece
450instance ToHttpApiData Double where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
451instance ToHttpApiData Float where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
452instance ToHttpApiData Int where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
453instance ToHttpApiData Int8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
454instance ToHttpApiData Int16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
455instance ToHttpApiData Int32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
456instance ToHttpApiData Int64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
457instance ToHttpApiData Integer where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
458instance ToHttpApiData Word where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
459instance ToHttpApiData Word8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
460instance ToHttpApiData Word16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
461instance ToHttpApiData Word32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
462instance ToHttpApiData Word64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
464-- | Note: this instance is not polykinded
465instance F.HasResolution a => ToHttpApiData (F.Fixed (a :: Type)) where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
468-- >>> toUrlPiece (fromGregorian 2015 10 03)
470instance ToHttpApiData Day where
471 toUrlPiece = T.pack . show
472 toEncodedUrlPiece = unsafeToEncodedUrlPiece
474timeToUrlPiece :: FormatTime t => String -> t -> Text
475timeToUrlPiece fmt = T.pack . formatTime defaultTimeLocale (iso8601DateFormat (Just fmt))
478-- >>> toUrlPiece $ TimeOfDay 14 55 23.1
480instance ToHttpApiData TimeOfDay where
481 toUrlPiece = T.pack . formatTime defaultTimeLocale "%H:%M:%S%Q"
482 toEncodedUrlPiece = unsafeToEncodedUrlPiece
485-- >>> toUrlPiece $ LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 21.687)
486-- "2015-10-03T14:55:21.687"
487instance ToHttpApiData LocalTime where
488 toUrlPiece = timeToUrlPiece "%H:%M:%S%Q"
489 toEncodedUrlPiece = unsafeToEncodedUrlPiece
492-- >>> toUrlPiece $ ZonedTime (LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 51.001)) utc
493-- "2015-10-03T14:55:51.001+0000"
494instance ToHttpApiData ZonedTime where
495 toUrlPiece = timeToUrlPiece "%H:%M:%S%Q%z"
496 toEncodedUrlPiece = unsafeToEncodedUrlPiece
499-- >>> toUrlPiece $ UTCTime (fromGregorian 2015 10 03) 864.5
500-- "2015-10-03T00:14:24.5Z"
501instance ToHttpApiData UTCTime where
502 toUrlPiece = timeToUrlPiece "%H:%M:%S%QZ"
503 toEncodedUrlPiece = unsafeToEncodedUrlPiece
506-- >>> toUrlPiece Monday
508instance ToHttpApiData DayOfWeek where
509 toUrlPiece Monday = "monday"
510 toUrlPiece Tuesday = "tuesday"
511 toUrlPiece Wednesday = "wednesday"
512 toUrlPiece Thursday = "thursday"
513 toUrlPiece Friday = "friday"
514 toUrlPiece Saturday = "saturday"
515 toUrlPiece Sunday = "sunday"
517 toEncodedUrlPiece = unsafeToEncodedUrlPiece
520instance ToHttpApiData NominalDiffTime where
521 toUrlPiece = toUrlPiece . nominalDiffTimeToSeconds
522 toEncodedUrlPiece = unsafeToEncodedUrlPiece
524instance ToHttpApiData String where toUrlPiece = T.pack
525instance ToHttpApiData Text where toUrlPiece = id
526instance ToHttpApiData L.Text where toUrlPiece = L.toStrict
528instance ToHttpApiData All where
529 toUrlPiece = coerce (toUrlPiece :: Bool -> Text)
530 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Bool -> BS.Builder)
532instance ToHttpApiData Any where
533 toUrlPiece = coerce (toUrlPiece :: Bool -> Text)
534 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Bool -> BS.Builder)
536instance ToHttpApiData a => ToHttpApiData (Dual a) where
537 toUrlPiece = coerce (toUrlPiece :: a -> Text)
538 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
540instance ToHttpApiData a => ToHttpApiData (Sum a) where
541 toUrlPiece = coerce (toUrlPiece :: a -> Text)
542 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
544instance ToHttpApiData a => ToHttpApiData (Product a) where
545 toUrlPiece = coerce (toUrlPiece :: a -> Text)
546 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
548instance ToHttpApiData a => ToHttpApiData (First a) where
549 toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text)
550 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)
552instance ToHttpApiData a => ToHttpApiData (Last a) where
553 toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text)
554 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)
556instance ToHttpApiData a => ToHttpApiData (Semi.Min a) where
557 toUrlPiece = coerce (toUrlPiece :: a -> Text)
558 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
560instance ToHttpApiData a => ToHttpApiData (Semi.Max a) where
561 toUrlPiece = coerce (toUrlPiece :: a -> Text)
562 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
564instance ToHttpApiData a => ToHttpApiData (Semi.First a) where
565 toUrlPiece = coerce (toUrlPiece :: a -> Text)
566 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
568instance ToHttpApiData a => ToHttpApiData (Semi.Last a) where
569 toUrlPiece = coerce (toUrlPiece :: a -> Text)
570 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
573-- >>> toUrlPiece (Just "Hello")
575instance ToHttpApiData a => ToHttpApiData (Maybe a) where
576 toUrlPiece (Just x) = "just " <> toUrlPiece x
577 toUrlPiece Nothing = "nothing"
580-- >>> toUrlPiece (Left "err" :: Either String Int)
582-- >>> toUrlPiece (Right 3 :: Either String Int)
584instance (ToHttpApiData a, ToHttpApiData b) => ToHttpApiData (Either a b) where
585 toUrlPiece (Left x) = "left " <> toUrlPiece x
586 toUrlPiece (Right x) = "right " <> toUrlPiece x
588-- | /Note:/ this instance works correctly for alphanumeric name and value
590-- >>> let Right c = parseUrlPiece "SESSID=r2t5uvjq435r4q7ib3vtdjq120" :: Either Text SetCookie
592-- "SESSID=r2t5uvjq435r4q7ib3vtdjq120"
595-- "SESSID=r2t5uvjq435r4q7ib3vtdjq120"
597instance ToHttpApiData SetCookie where
598 toUrlPiece = decodeUtf8With lenientDecode . toHeader
599 toHeader = LBS.toStrict . BS.toLazyByteString . renderSetCookie
600 -- toEncodedUrlPiece = renderSetCookie -- doesn't do things.
602-- | Note: this instance is not polykinded
603instance ToHttpApiData a => ToHttpApiData (Tagged (b :: Type) a) where
604 toUrlPiece = coerce (toUrlPiece :: a -> Text)
605 toHeader = coerce (toHeader :: a -> ByteString)
606 toQueryParam = coerce (toQueryParam :: a -> Text)
607 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
610instance ToHttpApiData a => ToHttpApiData (Const a b) where
611 toUrlPiece = coerce (toUrlPiece :: a -> Text)
612 toHeader = coerce (toHeader :: a -> ByteString)
613 toQueryParam = coerce (toQueryParam :: a -> Text)
614 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
617instance ToHttpApiData a => ToHttpApiData (Identity a) where
618 toUrlPiece = coerce (toUrlPiece :: a -> Text)
619 toHeader = coerce (toHeader :: a -> ByteString)
620 toQueryParam = coerce (toQueryParam :: a -> Text)
621 toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
624-- >>> parseUrlPiece "_" :: Either Text ()
626instance FromHttpApiData () where
627 parseUrlPiece "_" = pure ()
628 parseUrlPiece s = defaultParseError s
630instance FromHttpApiData Char where
633 Just (c, s') | T.null s' -> pure c
634 _ -> defaultParseError s
637-- >>> showVersion <$> parseUrlPiece "1.2.3"
639instance FromHttpApiData Version where
641 case reverse (readP_to_S parseVersion (T.unpack s)) of
642 ((x, ""):_) -> pure x
643 _ -> defaultParseError s
645-- | Parsing a @'Void'@ value is always an error, considering @'Void'@ as a data type with no constructors.
646instance FromHttpApiData Void where
647 parseUrlPiece _ = Left "Void cannot be parsed!"
649instance FromHttpApiData Natural where
651 n <- runReader (signed decimal) s
653 then Left ("underflow: " <> s <> " (should be a non-negative integer)")
654 else Right (fromInteger n)
656instance FromHttpApiData Bool where parseUrlPiece = parseBoundedUrlPiece
657instance FromHttpApiData Ordering where parseUrlPiece = parseBoundedUrlPiece
658instance FromHttpApiData Double where parseUrlPiece = runReader rational
659instance FromHttpApiData Float where parseUrlPiece = runReader rational
660instance FromHttpApiData Int where parseUrlPiece = parseBounded (signed decimal)
661instance FromHttpApiData Int8 where parseUrlPiece = parseBounded (signed decimal)
662instance FromHttpApiData Int16 where parseUrlPiece = parseBounded (signed decimal)
663instance FromHttpApiData Int32 where parseUrlPiece = parseBounded (signed decimal)
664instance FromHttpApiData Int64 where parseUrlPiece = parseBounded (signed decimal)
665instance FromHttpApiData Integer where parseUrlPiece = runReader (signed decimal)
666instance FromHttpApiData Word where parseUrlPiece = parseBounded decimal
667instance FromHttpApiData Word8 where parseUrlPiece = parseBounded decimal
668instance FromHttpApiData Word16 where parseUrlPiece = parseBounded decimal
669instance FromHttpApiData Word32 where parseUrlPiece = parseBounded decimal
670instance FromHttpApiData Word64 where parseUrlPiece = parseBounded decimal
671instance FromHttpApiData String where parseUrlPiece = Right . T.unpack
672instance FromHttpApiData Text where parseUrlPiece = Right
673instance FromHttpApiData L.Text where parseUrlPiece = Right . L.fromStrict
675-- | Note: this instance is not polykinded
676instance F.HasResolution a => FromHttpApiData (F.Fixed (a :: Type)) where
677 parseUrlPiece = runReader rational
680-- >>> toGregorian <$> parseUrlPiece "2016-12-01"
682instance FromHttpApiData Day where parseUrlPiece = runAtto Atto.day
685-- >>> parseUrlPiece "14:55:01.333" :: Either Text TimeOfDay
687instance FromHttpApiData TimeOfDay where parseUrlPiece = runAtto Atto.timeOfDay
690-- >>> parseUrlPiece "2015-10-03T14:55:01" :: Either Text LocalTime
691-- Right 2015-10-03 14:55:01
692instance FromHttpApiData LocalTime where parseUrlPiece = runAtto Atto.localTime
695-- >>> parseUrlPiece "2015-10-03T14:55:01+0000" :: Either Text ZonedTime
696-- Right 2015-10-03 14:55:01 +0000
698-- >>> parseQueryParam "2016-12-31T01:00:00Z" :: Either Text ZonedTime
699-- Right 2016-12-31 01:00:00 +0000
700instance FromHttpApiData ZonedTime where parseUrlPiece = runAtto Atto.zonedTime
703-- >>> parseUrlPiece "2015-10-03T00:14:24Z" :: Either Text UTCTime
704-- Right 2015-10-03 00:14:24 UTC
705instance FromHttpApiData UTCTime where parseUrlPiece = runAtto Atto.utcTime
707instance FromHttpApiData DayOfWeek where
708 parseUrlPiece t = case Map.lookup (T.toLower t) m of
709 Just dow -> Right dow
710 Nothing -> Left $ "Incorrect DayOfWeek: " <> T.take 10 t
712 m :: Map.Map Text DayOfWeek
713 m = Map.fromList [ (toUrlPiece dow, dow) | dow <- [Monday .. Sunday] ]
716instance FromHttpApiData NominalDiffTime where parseUrlPiece = fmap secondsToNominalDiffTime . parseUrlPiece
718instance FromHttpApiData All where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text Bool)
719instance FromHttpApiData Any where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text Bool)
721instance FromHttpApiData a => FromHttpApiData (Dual a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
722instance FromHttpApiData a => FromHttpApiData (Sum a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
723instance FromHttpApiData a => FromHttpApiData (Product a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
724instance FromHttpApiData a => FromHttpApiData (First a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text (Maybe a))
725instance FromHttpApiData a => FromHttpApiData (Last a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text (Maybe a))
727instance FromHttpApiData a => FromHttpApiData (Semi.Min a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
728instance FromHttpApiData a => FromHttpApiData (Semi.Max a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
729instance FromHttpApiData a => FromHttpApiData (Semi.First a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
730instance FromHttpApiData a => FromHttpApiData (Semi.Last a) where parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
733-- >>> parseUrlPiece "Just 123" :: Either Text (Maybe Int)
735instance FromHttpApiData a => FromHttpApiData (Maybe a) where
737 | T.toLower (T.take 7 s) == "nothing" = pure Nothing
738 | otherwise = Just <$> parseUrlPieceWithPrefix "Just " s
741-- >>> parseUrlPiece "Right 123" :: Either Text (Either String Int)
743instance (FromHttpApiData a, FromHttpApiData b) => FromHttpApiData (Either a b) where
745 Right <$> parseUrlPieceWithPrefix "Right " s
746 <!> Left <$> parseUrlPieceWithPrefix "Left " s
752instance ToHttpApiData UUID.UUID where
753 toUrlPiece = UUID.toText
754 toHeader = UUID.toASCIIBytes
755 toEncodedUrlPiece = unsafeToEncodedUrlPiece
757instance FromHttpApiData UUID.UUID where
758 parseUrlPiece = maybe (Left "invalid UUID") Right . UUID.fromText
759 parseHeader = maybe (Left "invalid UUID") Right . UUID.fromASCIIBytes
762-- | Lenient parameters. 'FromHttpApiData' combinators always return `Right`.
765newtype LenientData a = LenientData { getLenientData :: Either Text a }
766 deriving (Eq, Ord, Show, Read, Typeable, Data, Functor, Foldable, Traversable)
768instance FromHttpApiData a => FromHttpApiData (LenientData a) where
769 parseUrlPiece = Right . LenientData . parseUrlPiece
770 parseHeader = Right . LenientData . parseHeader
771 parseQueryParam = Right . LenientData . parseQueryParam
773-- | /Note:/ this instance works correctly for alphanumeric name and value
775-- >>> parseUrlPiece "SESSID=r2t5uvjq435r4q7ib3vtdjq120" :: Either Text SetCookie
776-- Right (SetCookie {setCookieName = "SESSID", setCookieValue = "r2t5uvjq435r4q7ib3vtdjq120", setCookiePath = Nothing, setCookieExpires = Nothing, setCookieMaxAge = Nothing, setCookieDomain = Nothing, setCookieHttpOnly = False, setCookieSecure = False, setCookieSameSite = Nothing})
777instance FromHttpApiData SetCookie where
778 parseUrlPiece = parseHeader . encodeUtf8
779 parseHeader = Right . parseSetCookie
781-- | Note: this instance is not polykinded
782instance FromHttpApiData a => FromHttpApiData (Tagged (b :: Type) a) where
783 parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
784 parseHeader = coerce (parseHeader :: ByteString -> Either Text a)
785 parseQueryParam = coerce (parseQueryParam :: Text -> Either Text a)
788instance FromHttpApiData a => FromHttpApiData (Const a b) where
789 parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
790 parseHeader = coerce (parseHeader :: ByteString -> Either Text a)
791 parseQueryParam = coerce (parseQueryParam :: Text -> Either Text a)
794instance FromHttpApiData a => FromHttpApiData (Identity a) where
795 parseUrlPiece = coerce (parseUrlPiece :: Text -> Either Text a)
796 parseHeader = coerce (parseHeader :: ByteString -> Either Text a)
797 parseQueryParam = coerce (parseQueryParam :: Text -> Either Text a)
799-------------------------------------------------------------------------------
801-------------------------------------------------------------------------------
803runAtto :: Atto.Parser a -> Text -> Either Text a
804runAtto p t = case Atto.parseOnly (p <* Atto.endOfInput) t of
805 Left err -> Left (T.pack err)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list