git.iko.soy/typeable/http-api-data
on e9c728b help sign in
570 lines 21 kB history raw
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE DefaultSignatures #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE TypeSynonymInstances #-}
7-- |
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))
15#endif
16import Control.Arrow ((&&&), left)
17import Control.Monad ((<=<))
19import Data.Monoid
20import Data.ByteString (ByteString)
21import qualified Data.ByteString as BS
23import Data.Int
24import Data.Word
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
33import Data.Time
34import Data.Version
36#if MIN_VERSION_base(4,8,0)
37import Data.Void
38#endif
40import Text.Read (readMaybe)
41import Text.ParserCombinators.ReadP (readP_to_S)
43#if USE_TEXT_SHOW
44import TextShow (TextShow, showt)
45#endif
47-- $setup
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.
52--
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.
70--
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.
88--
89-- >>> toUrlPieces [1, 2, 3] :: [Text]
90-- ["1","2","3"]
91toUrlPieces :: (Functor t, ToHttpApiData a) => t a -> t Text
92toUrlPieces = fmap toUrlPiece
94-- | Parse multiple URL pieces.
95--
96-- >>> parseUrlPieces ["true", "false"] :: Either Text [Bool]
97-- Right [True,False]
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.
104--
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.
111--
112-- >>> parseQueryParams ["1", "2", "3"] :: Either Text [Int]
113-- Right [1,2,3]
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'@.
120--
121-- >>> parseUrlPieceMaybe "12" :: Maybe Int
122-- Just 12
123parseUrlPieceMaybe :: FromHttpApiData a => Text -> Maybe a
124parseUrlPieceMaybe = either (const Nothing) Just . parseUrlPiece
126-- | Parse HTTP header value in a @'Maybe'@.
127--
128-- >>> parseHeaderMaybe "hello" :: Maybe Text
129-- Just "hello"
130parseHeaderMaybe :: FromHttpApiData a => ByteString -> Maybe a
131parseHeaderMaybe = either (const Nothing) Just . parseHeader
133-- | Parse query param value in a @'Maybe'@.
134--
135-- >>> parseQueryParamMaybe "true" :: Maybe Bool
136-- Just True
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 =
147 case parse input of
148 Nothing -> defaultParseError input
149 Just val -> Right val
151#if USE_TEXT_SHOW
152-- | /Lower case/.
153--
154-- Convert to URL piece using @'TextShow'@ instance.
155-- The result is always lower cased.
156--
157-- >>> showTextData True
158-- "true"
159--
160-- This can be used as a default implementation for enumeration types:
161--
162-- @
163-- data MyData = Foo | Bar | Baz deriving (Generic)
164--
165-- instance TextShow MyData where
166-- showt = genericShowt
167--
168-- instance ToHttpApiData MyData where
169-- toUrlPiece = showTextData
170-- @
171showTextData :: TextShow a => a -> Text
172showTextData = T.toLower . showt
173#else
174-- | /Lower case/.
175--
176-- Convert to URL piece using @'Show'@ instance.
177-- The result is always lower cased.
178--
179-- >>> showTextData True
180-- "true"
181--
182-- This can be used as a default implementation for enumeration types:
183--
184-- >>> data MyData = Foo | Bar | Baz deriving (Show)
185-- >>> instance ToHttpApiData MyData where toUrlPiece = showTextData
186-- >>> toUrlPiece Foo
187-- "foo"
188showTextData :: Show a => a -> Text
189showTextData = T.toLower . showt
191-- | Like @'show'@, but returns @'Text'@.
192showt :: Show a => a -> Text
193showt = T.pack . show
194#endif
196-- | /Case insensitive/.
197--
198-- Parse given text case insensitive and then parse the rest of the input
199-- using @'parseUrlPiece'@.
200--
201-- >>> parseUrlPieceWithPrefix "Just " "just 10" :: Either Text Int
202-- Right 10
203-- >>> parseUrlPieceWithPrefix "Left " "left" :: Either Text Bool
204-- Left "could not parse: `left'"
205--
206-- This can be used to implement @'FromHttpApiData'@ for single field constructors:
207--
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
211-- Right (Foo 1)
212parseUrlPieceWithPrefix :: FromHttpApiData a => Text -> Text -> Either Text a
213parseUrlPieceWithPrefix pattern input
214 | T.toLower pattern == T.toLower prefix = parseUrlPiece rest
215 | otherwise = defaultParseError input
216 where
217 (prefix, rest) = T.splitAt (T.length pattern) input
219-- | Parse given bytestring then parse the rest of the input using @'parseHeader'@.
220--
221-- @
222-- data BasicAuthToken = BasicAuthToken Text deriving (Show)
223--
224-- instance FromHttpApiData BasicAuthToken where
225-- parseHeader h = BasicAuthToken \<$\> parseHeaderWithPrefix "Basic " h
226-- parseQueryParam p = BasicAuthToken \<$\> parseQueryParam p
227-- @
228--
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/.
237--
238-- Parse given text case insensitive and then parse the rest of the input
239-- using @'parseQueryParam'@.
240--
241-- >>> parseQueryParamWithPrefix "z" "z10" :: Either Text Int
242-- Right 10
243parseQueryParamWithPrefix :: FromHttpApiData a => Text -> Text -> Either Text a
244parseQueryParamWithPrefix pattern input
245 | T.toLower pattern == T.toLower prefix = parseQueryParam rest
246 | otherwise = defaultParseError input
247 where
248 (prefix, rest) = T.splitAt (T.length pattern) input
250#if USE_TEXT_SHOW
251-- | /Case insensitive/.
252--
253-- Parse values case insensitively based on @'TextShow'@ instance.
254--
255-- >>> parseBoundedTextData "true" :: Either Text Bool
256-- Right True
257-- >>> parseBoundedTextData "FALSE" :: Either Text Bool
258-- Right False
259--
260-- This can be used as a default implementation for enumeration types:
261--
262-- @
263-- data MyData = Foo | Bar | Baz deriving (Show, Bounded, Enum, Generic)
264--
265-- instance TextShow MyData where
266-- showt = genericShowt
267--
268-- instance FromHttpApiData MyData where
269-- parseUrlPiece = parseBoundedTextData
270-- @
271parseBoundedTextData :: (TextShow a, Bounded a, Enum a) => Text -> Either Text a
272#else
273-- | /Case insensitive/.
274--
275-- Parse values case insensitively based on @'Show'@ instance.
276--
277-- >>> parseBoundedTextData "true" :: Either Text Bool
278-- Right True
279-- >>> parseBoundedTextData "FALSE" :: Either Text Bool
280-- Right False
281--
282-- This can be used as a default implementation for enumeration types:
283--
284-- >>> data MyData = Foo | Bar | Baz deriving (Show, Bounded, Enum)
285-- >>> instance FromHttpApiData MyData where parseUrlPiece = parseBoundedTextData
286-- >>> parseUrlPiece "foo" :: Either Text MyData
287-- Right Foo
288parseBoundedTextData :: (Show a, Bounded a, Enum a) => Text -> Either Text a
289#endif
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.
297--
298-- >>> parseBoundedEnumOf toUrlPiece "true" :: Either Text Bool
299-- Right True
300--
301-- For case sensitive parser see 'parseBoundedEnumOfI'.
302parseBoundedEnumOf :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
303parseBoundedEnumOf = parseMaybeTextData . lookupBoundedEnumOf
305-- | /Case insensitive/.
306--
307-- Parse values case insensitively based on a precalculated mapping
308-- of their @'Text'@ representations.
309--
310-- >>> parseBoundedEnumOfI toUrlPiece "FALSE" :: Either Text Bool
311-- Right False
312--
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/.
318--
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/.
325--
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
336 Just x -> return x
338-- | Parse URL piece using @'Read'@ instance.
339--
340-- Use for types which do not involve letters:
341--
342-- >>> readTextData "1991-06-02" :: Either Text Day
343-- Right 1991-06-02
344--
345-- This parser is case sensitive and will not match @'showTextData'@
346-- in presense of letters:
347--
348-- >>> readTextData (showTextData True) :: Either Text Bool
349-- Left "could not parse: `true'"
350--
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 =
358 case reader input of
359 Left err -> Left ("could not parse: `" <> input <> "' (" <> T.pack err <> ")")
360 Right (x, rest)
361 | T.null rest -> Right x
362 | otherwise -> defaultParseError input
364-- | Run @'Reader'@ to parse bounded integral value with bounds checking.
365--
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
371 if (n > h || n < l)
372 then Left ("out of bounds: `" <> input <> "' (should be between " <> showt l <> " and " <> showt h <> ")")
373 else Right (fromInteger n)
374 where
375 l = toInteger (minBound :: a)
376 h = toInteger (maxBound :: a)
378-- |
379-- >>> toUrlPiece ()
380-- "_"
381instance ToHttpApiData () where
382 toUrlPiece () = "_"
384instance ToHttpApiData Char where toUrlPiece = T.singleton
386-- |
387-- >>> toUrlPiece (Version [1, 2, 3] [])
388-- "1.2.3"
389instance ToHttpApiData Version where
390 toUrlPiece = T.pack . showVersion
392#if MIN_VERSION_base(4,8,0)
393instance ToHttpApiData Void where
394 toUrlPiece = absurd
395#endif
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
414-- |
415-- >>> toUrlPiece (fromGregorian 2015 10 03)
416-- "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))
422-- |
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"
427-- |
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"
432-- |
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
452-- |
453-- >>> toUrlPiece (Just "Hello")
454-- "just Hello"
455instance ToHttpApiData a => ToHttpApiData (Maybe a) where
456 toUrlPiece (Just x) = "just " <> toUrlPiece x
457 toUrlPiece Nothing = "nothing"
459-- |
460-- >>> toUrlPiece (Left "err" :: Either String Int)
461-- "left err"
462-- >>> toUrlPiece (Right 3 :: Either String Int)
463-- "right 3"
464instance (ToHttpApiData a, ToHttpApiData b) => ToHttpApiData (Either a b) where
465 toUrlPiece (Left x) = "left " <> toUrlPiece x
466 toUrlPiece (Right x) = "right " <> toUrlPiece x
468-- |
469-- >>> parseUrlPiece "_" :: Either Text ()
470-- Right ()
471instance FromHttpApiData () where
472 parseUrlPiece "_" = pure ()
473 parseUrlPiece s = defaultParseError s
475instance FromHttpApiData Char where
476 parseUrlPiece s =
477 case T.uncons s of
478 Just (c, s') | T.null s' -> pure c
479 _ -> defaultParseError s
481-- |
482-- >>> showVersion <$> parseUrlPiece "1.2.3"
483-- Right "1.2.3"
484instance FromHttpApiData Version where
485 parseUrlPiece s =
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!"
494#endif
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
515-- |
516-- >>> toGregorian <$> parseUrlPiece "2016-12-01"
517-- Right (2016,12,1)
518instance FromHttpApiData Day where parseUrlPiece = readTextData
520timeParseUrlPiece :: ParseTime t => String -> Text -> Either Text t
521timeParseUrlPiece fmt = parseMaybeTextData (timeParseUrlPieceMaybe . T.unpack)
522 where
523 timeParseUrlPieceMaybe = parseTime defaultTimeLocale (iso8601DateFormat (Just fmt))
525-- |
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"
530-- |
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"
535-- |
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
551-- |
552-- >>> parseUrlPiece "Just 123" :: Either Text (Maybe Int)
553-- Right (Just 123)
554instance FromHttpApiData a => FromHttpApiData (Maybe a) where
555 parseUrlPiece s
556 | T.toLower (T.take 7 s) == "nothing" = pure Nothing
557 | otherwise = Just <$> parseUrlPieceWithPrefix "Just " s
559-- |
560-- >>> parseUrlPiece "Right 123" :: Either Text (Either String Int)
561-- Right (Right 123)
562instance (FromHttpApiData a, FromHttpApiData b) => FromHttpApiData (Either a b) where
563 parseUrlPiece s =
564 Right <$> parseUrlPieceWithPrefix "Right " s
565 <!> Left <$> parseUrlPieceWithPrefix "Left " s
566 where
567 infixl 3 <!>
568 Left _ <!> y = y
569 x <!> _ = x