2{-# LANGUAGE DefaultSignatures #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE TypeSynonymInstances #-}
8-- Convert Haskell values to and from HTTP API data
9-- such as URL pieces, headers and query parameters.
10module Web.Internal.HttpApiData where
12#if __GLASGOW_HASKELL__ < 710
13import Control.Applicative
14import Data.Traversable (Traversable(traverse))
16import Control.Arrow ((&&&), left)
17import Control.Monad ((<=<))
20import Data.ByteString (ByteString)
21import qualified Data.ByteString as BS
26import Data.Text (Text)
27import Data.Text.Encoding (encodeUtf8, decodeUtf8')
28import Data.Text.Read (signed, decimal, rational, Reader)
29import qualified Data.Text as T
30import qualified Data.Text.Lazy as L
32import Data.Time.Locale.Compat
36#if MIN_VERSION_base(4,8,0)
40import Text.Read (readMaybe)
41import Text.ParserCombinators.ReadP (readP_to_S)
44import TextShow (TextShow, showt)
48-- >>> data BasicAuthToken = BasicAuthToken Text deriving (Show)
49-- >>> instance FromHttpApiData BasicAuthToken where parseHeader h = BasicAuthToken <$> parseHeaderWithPrefix "Basic " h; parseQueryParam p = BasicAuthToken <$> parseQueryParam p
51-- | Convert value to HTTP API data.
53-- __WARNING__: Do not derive this using @DeriveAnyClass@ as the generated
54-- instance will loop indefinitely.
55class ToHttpApiData a where
56 {-# MINIMAL toUrlPiece | toQueryParam #-}
57 -- | Convert to URL path piece.
58 toUrlPiece :: a -> Text
59 toUrlPiece = toQueryParam
61 -- | Convert to HTTP header value.
62 toHeader :: a -> ByteString
63 toHeader = encodeUtf8 . toUrlPiece
65 -- | Convert to query param value.
66 toQueryParam :: a -> Text
67 toQueryParam = toUrlPiece
69-- | Parse value from HTTP API data.
71-- __WARNING__: Do not derive this using @DeriveAnyClass@ as the generated
72-- instance will loop indefinitely.
73class FromHttpApiData a where
74 {-# MINIMAL parseUrlPiece | parseQueryParam #-}
75 -- | Parse URL path piece.
76 parseUrlPiece :: Text -> Either Text a
77 parseUrlPiece = parseQueryParam
79 -- | Parse HTTP header value.
80 parseHeader :: ByteString -> Either Text a
81 parseHeader = parseUrlPiece <=< (left (T.pack . show) . decodeUtf8')
83 -- | Parse query param value.
84 parseQueryParam :: Text -> Either Text a
85 parseQueryParam = parseUrlPiece
87-- | Convert multiple values to a list of URL pieces.
89-- >>> toUrlPieces [1, 2, 3] :: [Text]
91toUrlPieces :: (Functor t, ToHttpApiData a) => t a -> t Text
92toUrlPieces = fmap toUrlPiece
94-- | Parse multiple URL pieces.
96-- >>> parseUrlPieces ["true", "false"] :: Either Text [Bool]
98-- >>> parseUrlPieces ["123", "hello", "world"] :: Either Text [Int]
99-- Left "could not parse: `hello' (input does not start with a digit)"
100parseUrlPieces :: (Traversable t, FromHttpApiData a) => t Text -> Either Text (t a)
101parseUrlPieces = traverse parseUrlPiece
103-- | Convert multiple values to a list of query parameter values.
105-- >>> toQueryParams [fromGregorian 2015 10 03, fromGregorian 2015 12 01] :: [Text]
106-- ["2015-10-03","2015-12-01"]
107toQueryParams :: (Functor t, ToHttpApiData a) => t a -> t Text
108toQueryParams = fmap toQueryParam
110-- | Parse multiple query parameters.
112-- >>> parseQueryParams ["1", "2", "3"] :: Either Text [Int]
114-- >>> parseQueryParams ["64", "128", "256"] :: Either Text [Word8]
115-- Left "out of bounds: `256' (should be between 0 and 255)"
116parseQueryParams :: (Traversable t, FromHttpApiData a) => t Text -> Either Text (t a)
117parseQueryParams = traverse parseQueryParam
119-- | Parse URL path piece in a @'Maybe'@.
121-- >>> parseUrlPieceMaybe "12" :: Maybe Int
123parseUrlPieceMaybe :: FromHttpApiData a => Text -> Maybe a
124parseUrlPieceMaybe = either (const Nothing) Just . parseUrlPiece
126-- | Parse HTTP header value in a @'Maybe'@.
128-- >>> parseHeaderMaybe "hello" :: Maybe Text
130parseHeaderMaybe :: FromHttpApiData a => ByteString -> Maybe a
131parseHeaderMaybe = either (const Nothing) Just . parseHeader
133-- | Parse query param value in a @'Maybe'@.
135-- >>> parseQueryParamMaybe "true" :: Maybe Bool
137parseQueryParamMaybe :: FromHttpApiData a => Text -> Maybe a
138parseQueryParamMaybe = either (const Nothing) Just . parseQueryParam
140-- | Default parsing error.
141defaultParseError :: Text -> Either Text a
142defaultParseError input = Left ("could not parse: `" <> input <> "'")
144-- | Convert @'Maybe'@ parser into @'Either' 'Text'@ parser with default error message.
145parseMaybeTextData :: (Text -> Maybe a) -> (Text -> Either Text a)
146parseMaybeTextData parse input =
148 Nothing -> defaultParseError input
149 Just val -> Right val
154-- Convert to URL piece using @'TextShow'@ instance.
155-- The result is always lower cased.
157-- >>> showTextData True
160-- This can be used as a default implementation for enumeration types:
163-- data MyData = Foo | Bar | Baz deriving (Generic)
165-- instance TextShow MyData where
166-- showt = genericShowt
168-- instance ToHttpApiData MyData where
169-- toUrlPiece = showTextData
171showTextData :: TextShow a => a -> Text
172showTextData = T.toLower . showt
176-- Convert to URL piece using @'Show'@ instance.
177-- The result is always lower cased.
179-- >>> showTextData True
182-- This can be used as a default implementation for enumeration types:
184-- >>> data MyData = Foo | Bar | Baz deriving (Show)
185-- >>> instance ToHttpApiData MyData where toUrlPiece = showTextData
188showTextData :: Show a => a -> Text
189showTextData = T.toLower . showt
191-- | Like @'show'@, but returns @'Text'@.
192showt :: Show a => a -> Text
196-- | /Case insensitive/.
198-- Parse given text case insensitive and then parse the rest of the input
199-- using @'parseUrlPiece'@.
201-- >>> parseUrlPieceWithPrefix "Just " "just 10" :: Either Text Int
203-- >>> parseUrlPieceWithPrefix "Left " "left" :: Either Text Bool
204-- Left "could not parse: `left'"
206-- This can be used to implement @'FromHttpApiData'@ for single field constructors:
208-- >>> data Foo = Foo Int deriving (Show)
209-- >>> instance FromHttpApiData Foo where parseUrlPiece s = Foo <$> parseUrlPieceWithPrefix "Foo " s
210-- >>> parseUrlPiece "foo 1" :: Either Text Foo
212parseUrlPieceWithPrefix :: FromHttpApiData a => Text -> Text -> Either Text a
213parseUrlPieceWithPrefix pattern input
214 | T.toLower pattern == T.toLower prefix = parseUrlPiece rest
215 | otherwise = defaultParseError input
217 (prefix, rest) = T.splitAt (T.length pattern) input
219-- | Parse given bytestring then parse the rest of the input using @'parseHeader'@.
222-- data BasicAuthToken = BasicAuthToken Text deriving (Show)
224-- instance FromHttpApiData BasicAuthToken where
225-- parseHeader h = BasicAuthToken \<$\> parseHeaderWithPrefix "Basic " h
226-- parseQueryParam p = BasicAuthToken \<$\> parseQueryParam p
229-- >>> parseHeader "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" :: Either Text BasicAuthToken
230-- Right (BasicAuthToken "QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
231parseHeaderWithPrefix :: FromHttpApiData a => ByteString -> ByteString -> Either Text a
232parseHeaderWithPrefix pattern input
233 | pattern `BS.isPrefixOf` input = parseHeader (BS.drop (BS.length pattern) input)
234 | otherwise = defaultParseError (showt input)
236-- | /Case insensitive/.
238-- Parse given text case insensitive and then parse the rest of the input
239-- using @'parseQueryParam'@.
241-- >>> parseQueryParamWithPrefix "z" "z10" :: Either Text Int
243parseQueryParamWithPrefix :: FromHttpApiData a => Text -> Text -> Either Text a
244parseQueryParamWithPrefix pattern input
245 | T.toLower pattern == T.toLower prefix = parseQueryParam rest
246 | otherwise = defaultParseError input
248 (prefix, rest) = T.splitAt (T.length pattern) input
251-- | /Case insensitive/.
253-- Parse values case insensitively based on @'TextShow'@ instance.
255-- >>> parseBoundedTextData "true" :: Either Text Bool
257-- >>> parseBoundedTextData "FALSE" :: Either Text Bool
260-- This can be used as a default implementation for enumeration types:
263-- data MyData = Foo | Bar | Baz deriving (Show, Bounded, Enum, Generic)
265-- instance TextShow MyData where
266-- showt = genericShowt
268-- instance FromHttpApiData MyData where
269-- parseUrlPiece = parseBoundedTextData
271parseBoundedTextData :: (TextShow a, Bounded a, Enum a) => Text -> Either Text a
273-- | /Case insensitive/.
275-- Parse values case insensitively based on @'Show'@ instance.
277-- >>> parseBoundedTextData "true" :: Either Text Bool
279-- >>> parseBoundedTextData "FALSE" :: Either Text Bool
282-- This can be used as a default implementation for enumeration types:
284-- >>> data MyData = Foo | Bar | Baz deriving (Show, Bounded, Enum)
285-- >>> instance FromHttpApiData MyData where parseUrlPiece = parseBoundedTextData
286-- >>> parseUrlPiece "foo" :: Either Text MyData
288parseBoundedTextData :: (Show a, Bounded a, Enum a) => Text -> Either Text a
290parseBoundedTextData = parseBoundedEnumOfI showTextData
292-- | Lookup values based on a precalculated mapping of their representations.
293lookupBoundedEnumOf :: (Bounded a, Enum a, Eq b) => (a -> b) -> b -> Maybe a
294lookupBoundedEnumOf f = flip lookup (map (f &&& id) [minBound..maxBound])
296-- | Parse values based on a precalculated mapping of their @'Text'@ representation.
298-- >>> parseBoundedEnumOf toUrlPiece "true" :: Either Text Bool
301-- For case sensitive parser see 'parseBoundedEnumOfI'.
302parseBoundedEnumOf :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
303parseBoundedEnumOf = parseMaybeTextData . lookupBoundedEnumOf
305-- | /Case insensitive/.
307-- Parse values case insensitively based on a precalculated mapping
308-- of their @'Text'@ representations.
310-- >>> parseBoundedEnumOfI toUrlPiece "FALSE" :: Either Text Bool
313-- For case sensitive parser see 'parseBoundedEnumOf'.
314parseBoundedEnumOfI :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
315parseBoundedEnumOfI f = parseBoundedEnumOf (T.toLower . f) . T.toLower
317-- | /Case insensitive/.
319-- Parse values case insensitively based on @'ToHttpApiData'@ instance.
320-- Uses @'toUrlPiece'@ to get possible values.
321parseBoundedUrlPiece :: (ToHttpApiData a, Bounded a, Enum a) => Text -> Either Text a
322parseBoundedUrlPiece = parseBoundedEnumOfI toUrlPiece
324-- | /Case insensitive/.
326-- Parse values case insensitively based on @'ToHttpApiData'@ instance.
327-- Uses @'toQueryParam'@ to get possible values.
328parseBoundedQueryParam :: (ToHttpApiData a, Bounded a, Enum a) => Text -> Either Text a
329parseBoundedQueryParam = parseBoundedEnumOfI toQueryParam
331-- | Parse values based on @'ToHttpApiData'@ instance.
332-- Uses @'toHeader'@ to get possible values.
333parseBoundedHeader :: (ToHttpApiData a, Bounded a, Enum a) => ByteString -> Either Text a
334parseBoundedHeader bs = case lookupBoundedEnumOf toHeader bs of
335 Nothing -> defaultParseError $ T.pack $ show bs
338-- | Parse URL piece using @'Read'@ instance.
340-- Use for types which do not involve letters:
342-- >>> readTextData "1991-06-02" :: Either Text Day
345-- This parser is case sensitive and will not match @'showTextData'@
346-- in presense of letters:
348-- >>> readTextData (showTextData True) :: Either Text Bool
349-- Left "could not parse: `true'"
351-- See @'parseBoundedTextData'@.
352readTextData :: Read a => Text -> Either Text a
353readTextData = parseMaybeTextData (readMaybe . T.unpack)
355-- | Run @'Reader'@ as HTTP API data parser.
356runReader :: Reader a -> Text -> Either Text a
357runReader reader input =
359 Left err -> Left ("could not parse: `" <> input <> "' (" <> T.pack err <> ")")
361 | T.null rest -> Right x
362 | otherwise -> defaultParseError input
364-- | Run @'Reader'@ to parse bounded integral value with bounds checking.
366-- >>> parseBounded decimal "256" :: Either Text Word8
367-- Left "out of bounds: `256' (should be between 0 and 255)"
368parseBounded :: forall a. (Bounded a, Integral a) => Reader Integer -> Text -> Either Text a
369parseBounded reader input = do
370 n <- runReader reader input
372 then Left ("out of bounds: `" <> input <> "' (should be between " <> showt l <> " and " <> showt h <> ")")
373 else Right (fromInteger n)
375 l = toInteger (minBound :: a)
376 h = toInteger (maxBound :: a)
381instance ToHttpApiData () where
384instance ToHttpApiData Char where toUrlPiece = T.singleton
387-- >>> toUrlPiece (Version [1, 2, 3] [])
389instance ToHttpApiData Version where
390 toUrlPiece = T.pack . showVersion
392#if MIN_VERSION_base(4,8,0)
393instance ToHttpApiData Void where
397instance ToHttpApiData Bool where toUrlPiece = showTextData
398instance ToHttpApiData Ordering where toUrlPiece = showTextData
400instance ToHttpApiData Double where toUrlPiece = showt
401instance ToHttpApiData Float where toUrlPiece = showt
402instance ToHttpApiData Int where toUrlPiece = showt
403instance ToHttpApiData Int8 where toUrlPiece = showt
404instance ToHttpApiData Int16 where toUrlPiece = showt
405instance ToHttpApiData Int32 where toUrlPiece = showt
406instance ToHttpApiData Int64 where toUrlPiece = showt
407instance ToHttpApiData Integer where toUrlPiece = showt
408instance ToHttpApiData Word where toUrlPiece = showt
409instance ToHttpApiData Word8 where toUrlPiece = showt
410instance ToHttpApiData Word16 where toUrlPiece = showt
411instance ToHttpApiData Word32 where toUrlPiece = showt
412instance ToHttpApiData Word64 where toUrlPiece = showt
415-- >>> toUrlPiece (fromGregorian 2015 10 03)
417instance ToHttpApiData Day where toUrlPiece = T.pack . show
419timeToUrlPiece :: FormatTime t => String -> t -> Text
420timeToUrlPiece fmt = T.pack . formatTime defaultTimeLocale (iso8601DateFormat (Just fmt))
423-- >>> toUrlPiece $ LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 01)
424-- "2015-10-03T14:55:01"
425instance ToHttpApiData LocalTime where toUrlPiece = timeToUrlPiece "%H:%M:%S"
428-- >>> toUrlPiece $ ZonedTime (LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 01)) utc
429-- "2015-10-03T14:55:01+0000"
430instance ToHttpApiData ZonedTime where toUrlPiece = timeToUrlPiece "%H:%M:%S%z"
433-- >>> toUrlPiece $ UTCTime (fromGregorian 2015 10 03) 864
434-- "2015-10-03T00:14:24Z"
435instance ToHttpApiData UTCTime where toUrlPiece = timeToUrlPiece "%H:%M:%SZ"
437instance ToHttpApiData NominalDiffTime where toUrlPiece = toUrlPiece . (floor :: NominalDiffTime -> Integer)
439instance ToHttpApiData String where toUrlPiece = T.pack
440instance ToHttpApiData Text where toUrlPiece = id
441instance ToHttpApiData L.Text where toUrlPiece = L.toStrict
443instance ToHttpApiData All where toUrlPiece = toUrlPiece . getAll
444instance ToHttpApiData Any where toUrlPiece = toUrlPiece . getAny
446instance ToHttpApiData a => ToHttpApiData (Dual a) where toUrlPiece = toUrlPiece . getDual
447instance ToHttpApiData a => ToHttpApiData (Sum a) where toUrlPiece = toUrlPiece . getSum
448instance ToHttpApiData a => ToHttpApiData (Product a) where toUrlPiece = toUrlPiece . getProduct
449instance ToHttpApiData a => ToHttpApiData (First a) where toUrlPiece = toUrlPiece . getFirst
450instance ToHttpApiData a => ToHttpApiData (Last a) where toUrlPiece = toUrlPiece . getLast
453-- >>> toUrlPiece (Just "Hello")
455instance ToHttpApiData a => ToHttpApiData (Maybe a) where
456 toUrlPiece (Just x) = "just " <> toUrlPiece x
457 toUrlPiece Nothing = "nothing"
460-- >>> toUrlPiece (Left "err" :: Either String Int)
462-- >>> toUrlPiece (Right 3 :: Either String Int)
464instance (ToHttpApiData a, ToHttpApiData b) => ToHttpApiData (Either a b) where
465 toUrlPiece (Left x) = "left " <> toUrlPiece x
466 toUrlPiece (Right x) = "right " <> toUrlPiece x
469-- >>> parseUrlPiece "_" :: Either Text ()
471instance FromHttpApiData () where
472 parseUrlPiece "_" = pure ()
473 parseUrlPiece s = defaultParseError s
475instance FromHttpApiData Char where
478 Just (c, s') | T.null s' -> pure c
479 _ -> defaultParseError s
482-- >>> showVersion <$> parseUrlPiece "1.2.3"
484instance FromHttpApiData Version where
486 case reverse (readP_to_S parseVersion (T.unpack s)) of
487 ((x, ""):_) -> pure x
488 _ -> defaultParseError s
490#if MIN_VERSION_base(4,8,0)
491-- | Parsing a @'Void'@ value is always an error, considering @'Void'@ as a data type with no constructors.
492instance FromHttpApiData Void where
493 parseUrlPiece _ = Left "Void cannot be parsed!"
496instance FromHttpApiData Bool where parseUrlPiece = parseBoundedUrlPiece
497instance FromHttpApiData Ordering where parseUrlPiece = parseBoundedUrlPiece
498instance FromHttpApiData Double where parseUrlPiece = runReader rational
499instance FromHttpApiData Float where parseUrlPiece = runReader rational
500instance FromHttpApiData Int where parseUrlPiece = parseBounded (signed decimal)
501instance FromHttpApiData Int8 where parseUrlPiece = parseBounded (signed decimal)
502instance FromHttpApiData Int16 where parseUrlPiece = parseBounded (signed decimal)
503instance FromHttpApiData Int32 where parseUrlPiece = parseBounded (signed decimal)
504instance FromHttpApiData Int64 where parseUrlPiece = parseBounded (signed decimal)
505instance FromHttpApiData Integer where parseUrlPiece = runReader (signed decimal)
506instance FromHttpApiData Word where parseUrlPiece = parseBounded decimal
507instance FromHttpApiData Word8 where parseUrlPiece = parseBounded decimal
508instance FromHttpApiData Word16 where parseUrlPiece = parseBounded decimal
509instance FromHttpApiData Word32 where parseUrlPiece = parseBounded decimal
510instance FromHttpApiData Word64 where parseUrlPiece = parseBounded decimal
511instance FromHttpApiData String where parseUrlPiece = Right . T.unpack
512instance FromHttpApiData Text where parseUrlPiece = Right
513instance FromHttpApiData L.Text where parseUrlPiece = Right . L.fromStrict
516-- >>> toGregorian <$> parseUrlPiece "2016-12-01"
518instance FromHttpApiData Day where parseUrlPiece = readTextData
520timeParseUrlPiece :: ParseTime t => String -> Text -> Either Text t
521timeParseUrlPiece fmt = parseMaybeTextData (timeParseUrlPieceMaybe . T.unpack)
523 timeParseUrlPieceMaybe = parseTime defaultTimeLocale (iso8601DateFormat (Just fmt))
526-- >>> parseUrlPiece "2015-10-03T14:55:01" :: Either Text LocalTime
527-- Right 2015-10-03 14:55:01
528instance FromHttpApiData LocalTime where parseUrlPiece = timeParseUrlPiece "%H:%M:%S"
531-- >>> parseUrlPiece "2015-10-03T14:55:01+0000" :: Either Text ZonedTime
532-- Right 2015-10-03 14:55:01 +0000
533instance FromHttpApiData ZonedTime where parseUrlPiece = timeParseUrlPiece "%H:%M:%S%z"
536-- >>> parseUrlPiece "2015-10-03T00:14:24Z" :: Either Text UTCTime
537-- Right 2015-10-03 00:14:24 UTC
538instance FromHttpApiData UTCTime where parseUrlPiece = timeParseUrlPiece "%H:%M:%SZ"
540instance FromHttpApiData NominalDiffTime where parseUrlPiece = fmap fromInteger . parseUrlPiece
542instance FromHttpApiData All where parseUrlPiece = fmap All . parseUrlPiece
543instance FromHttpApiData Any where parseUrlPiece = fmap Any . parseUrlPiece
545instance FromHttpApiData a => FromHttpApiData (Dual a) where parseUrlPiece = fmap Dual . parseUrlPiece
546instance FromHttpApiData a => FromHttpApiData (Sum a) where parseUrlPiece = fmap Sum . parseUrlPiece
547instance FromHttpApiData a => FromHttpApiData (Product a) where parseUrlPiece = fmap Product . parseUrlPiece
548instance FromHttpApiData a => FromHttpApiData (First a) where parseUrlPiece = fmap First . parseUrlPiece
549instance FromHttpApiData a => FromHttpApiData (Last a) where parseUrlPiece = fmap Last . parseUrlPiece
552-- >>> parseUrlPiece "Just 123" :: Either Text (Maybe Int)
554instance FromHttpApiData a => FromHttpApiData (Maybe a) where
556 | T.toLower (T.take 7 s) == "nothing" = pure Nothing
557 | otherwise = Just <$> parseUrlPieceWithPrefix "Just " s
560-- >>> parseUrlPiece "Right 123" :: Either Text (Either String Int)
562instance (FromHttpApiData a, FromHttpApiData b) => FromHttpApiData (Either a b) where
564 Right <$> parseUrlPieceWithPrefix "Right " s
565 <!> Left <$> parseUrlPieceWithPrefix "Left " s
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