2{-# LANGUAGE DefaultSignatures #-}
3{-# LANGUAGE DeriveDataTypeable #-}
4{-# LANGUAGE DeriveFunctor #-}
5{-# LANGUAGE DeriveFoldable #-}
6{-# LANGUAGE DeriveTraversable #-}
7{-# LANGUAGE FlexibleInstances #-}
8{-# LANGUAGE OverloadedStrings #-}
9{-# LANGUAGE ScopedTypeVariables #-}
10{-# LANGUAGE TypeSynonymInstances #-}
12-- Convert Haskell values to and from HTTP API data
13-- such as URL pieces, headers and query parameters.
14module Web.Internal.HttpApiData where
16#if __GLASGOW_HASKELL__ < 710
17import Control.Applicative
18import Data.Foldable (Foldable)
19import Data.Traversable (Traversable(traverse))
22import Control.Arrow ((&&&), left)
23import Control.Monad ((<=<))
26import Data.ByteString (ByteString)
27import qualified Data.ByteString as BS
32import Data.Text (Text)
33import Data.Text.Encoding (encodeUtf8, decodeUtf8')
34import Data.Text.Read (signed, decimal, rational, Reader)
35import qualified Data.Text as T
36import qualified Data.Text.Lazy as L
38import Data.Time.Locale.Compat
42#if MIN_VERSION_base(4,8,0)
47import Text.Read (readMaybe)
48import Text.ParserCombinators.ReadP (readP_to_S)
51import TextShow (TextShow, showt)
54import qualified Data.UUID.Types as UUID
56import Data.Typeable (Typeable)
57import Data.Data (Data)
58import qualified Data.ByteString.Builder as BS
59import qualified Network.HTTP.Types as H
63-- >>> data BasicAuthToken = BasicAuthToken Text deriving (Show)
64-- >>> instance FromHttpApiData BasicAuthToken where parseHeader h = BasicAuthToken <$> parseHeaderWithPrefix "Basic " h; parseQueryParam p = BasicAuthToken <$> parseQueryParam p
66-- | Convert value to HTTP API data.
68-- __WARNING__: Do not derive this using @DeriveAnyClass@ as the generated
69-- instance will loop indefinitely.
70class ToHttpApiData a where
71 {-# MINIMAL toUrlPiece | toQueryParam #-}
72 -- | Convert to URL path piece.
73 toUrlPiece :: a -> Text
74 toUrlPiece = toQueryParam
76 -- | Convert to a URL path piece, making sure to encode any special chars
77 toEncodedUrlPiece :: a -> BS.Builder
78 toEncodedUrlPiece = H.encodePathSegmentsRelative . (:[]) . toUrlPiece
80 -- | Convert to HTTP header value.
81 toHeader :: a -> ByteString
82 toHeader = encodeUtf8 . toUrlPiece
84 -- | Convert to query param value.
85 toQueryParam :: a -> Text
86 toQueryParam = toUrlPiece
88-- | Parse value from HTTP API data.
90-- __WARNING__: Do not derive this using @DeriveAnyClass@ as the generated
91-- instance will loop indefinitely.
92class FromHttpApiData a where
93 {-# MINIMAL parseUrlPiece | parseQueryParam #-}
94 -- | Parse URL path piece.
95 parseUrlPiece :: Text -> Either Text a
96 parseUrlPiece = parseQueryParam
98 -- | Parse HTTP header value.
99 parseHeader :: ByteString -> Either Text a
100 parseHeader = parseUrlPiece <=< (left (T.pack . show) . decodeUtf8')
102 -- | Parse query param value.
103 parseQueryParam :: Text -> Either Text a
104 parseQueryParam = parseUrlPiece
106-- | Convert multiple values to a list of URL pieces.
108-- >>> toUrlPieces [1, 2, 3] :: [Text]
110toUrlPieces :: (Functor t, ToHttpApiData a) => t a -> t Text
111toUrlPieces = fmap toUrlPiece
113-- | Parse multiple URL pieces.
115-- >>> parseUrlPieces ["true", "false"] :: Either Text [Bool]
117-- >>> parseUrlPieces ["123", "hello", "world"] :: Either Text [Int]
118-- Left "could not parse: `hello' (input does not start with a digit)"
119parseUrlPieces :: (Traversable t, FromHttpApiData a) => t Text -> Either Text (t a)
120parseUrlPieces = traverse parseUrlPiece
122-- | Convert multiple values to a list of query parameter values.
124-- >>> toQueryParams [fromGregorian 2015 10 03, fromGregorian 2015 12 01] :: [Text]
125-- ["2015-10-03","2015-12-01"]
126toQueryParams :: (Functor t, ToHttpApiData a) => t a -> t Text
127toQueryParams = fmap toQueryParam
129-- | Parse multiple query parameters.
131-- >>> parseQueryParams ["1", "2", "3"] :: Either Text [Int]
133-- >>> parseQueryParams ["64", "128", "256"] :: Either Text [Word8]
134-- Left "out of bounds: `256' (should be between 0 and 255)"
135parseQueryParams :: (Traversable t, FromHttpApiData a) => t Text -> Either Text (t a)
136parseQueryParams = traverse parseQueryParam
138-- | Parse URL path piece in a @'Maybe'@.
140-- >>> parseUrlPieceMaybe "12" :: Maybe Int
142parseUrlPieceMaybe :: FromHttpApiData a => Text -> Maybe a
143parseUrlPieceMaybe = either (const Nothing) Just . parseUrlPiece
145-- | Parse HTTP header value in a @'Maybe'@.
147-- >>> parseHeaderMaybe "hello" :: Maybe Text
149parseHeaderMaybe :: FromHttpApiData a => ByteString -> Maybe a
150parseHeaderMaybe = either (const Nothing) Just . parseHeader
152-- | Parse query param value in a @'Maybe'@.
154-- >>> parseQueryParamMaybe "true" :: Maybe Bool
156parseQueryParamMaybe :: FromHttpApiData a => Text -> Maybe a
157parseQueryParamMaybe = either (const Nothing) Just . parseQueryParam
159-- | Default parsing error.
160defaultParseError :: Text -> Either Text a
161defaultParseError input = Left ("could not parse: `" <> input <> "'")
163-- | Convert @'Maybe'@ parser into @'Either' 'Text'@ parser with default error message.
164parseMaybeTextData :: (Text -> Maybe a) -> (Text -> Either Text a)
165parseMaybeTextData parse input =
167 Nothing -> defaultParseError input
168 Just val -> Right val
173-- Convert to URL piece using @'TextShow'@ instance.
174-- The result is always lower cased.
176-- >>> showTextData True
179-- This can be used as a default implementation for enumeration types:
182-- data MyData = Foo | Bar | Baz deriving (Generic)
184-- instance TextShow MyData where
185-- showt = genericShowt
187-- instance ToHttpApiData MyData where
188-- toUrlPiece = showTextData
190showTextData :: TextShow a => a -> Text
191showTextData = T.toLower . showt
195-- Convert to URL piece using @'Show'@ instance.
196-- The result is always lower cased.
198-- >>> showTextData True
201-- This can be used as a default implementation for enumeration types:
203-- >>> data MyData = Foo | Bar | Baz deriving (Show)
204-- >>> instance ToHttpApiData MyData where toUrlPiece = showTextData
207showTextData :: Show a => a -> Text
208showTextData = T.toLower . showt
210-- | Like @'show'@, but returns @'Text'@.
211showt :: Show a => a -> Text
215-- | /Case insensitive/.
217-- Parse given text case insensitive and then parse the rest of the input
218-- using @'parseUrlPiece'@.
220-- >>> parseUrlPieceWithPrefix "Just " "just 10" :: Either Text Int
222-- >>> parseUrlPieceWithPrefix "Left " "left" :: Either Text Bool
223-- Left "could not parse: `left'"
225-- This can be used to implement @'FromHttpApiData'@ for single field constructors:
227-- >>> data Foo = Foo Int deriving (Show)
228-- >>> instance FromHttpApiData Foo where parseUrlPiece s = Foo <$> parseUrlPieceWithPrefix "Foo " s
229-- >>> parseUrlPiece "foo 1" :: Either Text Foo
231parseUrlPieceWithPrefix :: FromHttpApiData a => Text -> Text -> Either Text a
232parseUrlPieceWithPrefix pattern input
233 | T.toLower pattern == T.toLower prefix = parseUrlPiece rest
234 | otherwise = defaultParseError input
236 (prefix, rest) = T.splitAt (T.length pattern) input
238-- | Parse given bytestring then parse the rest of the input using @'parseHeader'@.
241-- data BasicAuthToken = BasicAuthToken Text deriving (Show)
243-- instance FromHttpApiData BasicAuthToken where
244-- parseHeader h = BasicAuthToken \<$\> parseHeaderWithPrefix "Basic " h
245-- parseQueryParam p = BasicAuthToken \<$\> parseQueryParam p
248-- >>> parseHeader "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" :: Either Text BasicAuthToken
249-- Right (BasicAuthToken "QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
250parseHeaderWithPrefix :: FromHttpApiData a => ByteString -> ByteString -> Either Text a
251parseHeaderWithPrefix pattern input
252 | pattern `BS.isPrefixOf` input = parseHeader (BS.drop (BS.length pattern) input)
253 | otherwise = defaultParseError (showt input)
255-- | /Case insensitive/.
257-- Parse given text case insensitive and then parse the rest of the input
258-- using @'parseQueryParam'@.
260-- >>> parseQueryParamWithPrefix "z" "z10" :: Either Text Int
262parseQueryParamWithPrefix :: FromHttpApiData a => Text -> Text -> Either Text a
263parseQueryParamWithPrefix pattern input
264 | T.toLower pattern == T.toLower prefix = parseQueryParam rest
265 | otherwise = defaultParseError input
267 (prefix, rest) = T.splitAt (T.length pattern) input
270-- | /Case insensitive/.
272-- Parse values case insensitively based on @'TextShow'@ instance.
274-- >>> parseBoundedTextData "true" :: Either Text Bool
276-- >>> parseBoundedTextData "FALSE" :: Either Text Bool
279-- This can be used as a default implementation for enumeration types:
282-- data MyData = Foo | Bar | Baz deriving (Show, Bounded, Enum, Generic)
284-- instance TextShow MyData where
285-- showt = genericShowt
287-- instance FromHttpApiData MyData where
288-- parseUrlPiece = parseBoundedTextData
290parseBoundedTextData :: (TextShow a, Bounded a, Enum a) => Text -> Either Text a
292-- | /Case insensitive/.
294-- Parse values case insensitively based on @'Show'@ instance.
296-- >>> parseBoundedTextData "true" :: Either Text Bool
298-- >>> parseBoundedTextData "FALSE" :: Either Text Bool
301-- This can be used as a default implementation for enumeration types:
303-- >>> data MyData = Foo | Bar | Baz deriving (Show, Bounded, Enum)
304-- >>> instance FromHttpApiData MyData where parseUrlPiece = parseBoundedTextData
305-- >>> parseUrlPiece "foo" :: Either Text MyData
307parseBoundedTextData :: (Show a, Bounded a, Enum a) => Text -> Either Text a
309parseBoundedTextData = parseBoundedEnumOfI showTextData
311-- | Lookup values based on a precalculated mapping of their representations.
312lookupBoundedEnumOf :: (Bounded a, Enum a, Eq b) => (a -> b) -> b -> Maybe a
313lookupBoundedEnumOf f = flip lookup (map (f &&& id) [minBound..maxBound])
315-- | Parse values based on a precalculated mapping of their @'Text'@ representation.
317-- >>> parseBoundedEnumOf toUrlPiece "true" :: Either Text Bool
320-- For case sensitive parser see 'parseBoundedEnumOfI'.
321parseBoundedEnumOf :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
322parseBoundedEnumOf = parseMaybeTextData . lookupBoundedEnumOf
324-- | /Case insensitive/.
326-- Parse values case insensitively based on a precalculated mapping
327-- of their @'Text'@ representations.
329-- >>> parseBoundedEnumOfI toUrlPiece "FALSE" :: Either Text Bool
332-- For case sensitive parser see 'parseBoundedEnumOf'.
333parseBoundedEnumOfI :: (Bounded a, Enum a) => (a -> Text) -> Text -> Either Text a
334parseBoundedEnumOfI f = parseBoundedEnumOf (T.toLower . f) . T.toLower
336-- | /Case insensitive/.
338-- Parse values case insensitively based on @'ToHttpApiData'@ instance.
339-- Uses @'toUrlPiece'@ to get possible values.
340parseBoundedUrlPiece :: (ToHttpApiData a, Bounded a, Enum a) => Text -> Either Text a
341parseBoundedUrlPiece = parseBoundedEnumOfI toUrlPiece
343-- | /Case insensitive/.
345-- Parse values case insensitively based on @'ToHttpApiData'@ instance.
346-- Uses @'toQueryParam'@ to get possible values.
347parseBoundedQueryParam :: (ToHttpApiData a, Bounded a, Enum a) => Text -> Either Text a
348parseBoundedQueryParam = parseBoundedEnumOfI toQueryParam
350-- | Parse values based on @'ToHttpApiData'@ instance.
351-- Uses @'toHeader'@ to get possible values.
352parseBoundedHeader :: (ToHttpApiData a, Bounded a, Enum a) => ByteString -> Either Text a
353parseBoundedHeader bs = case lookupBoundedEnumOf toHeader bs of
354 Nothing -> defaultParseError $ T.pack $ show bs
357-- | Parse URL piece using @'Read'@ instance.
359-- Use for types which do not involve letters:
361-- >>> readTextData "1991-06-02" :: Either Text Day
364-- This parser is case sensitive and will not match @'showTextData'@
365-- in presense of letters:
367-- >>> readTextData (showTextData True) :: Either Text Bool
368-- Left "could not parse: `true'"
370-- See @'parseBoundedTextData'@.
371readTextData :: Read a => Text -> Either Text a
372readTextData = parseMaybeTextData (readMaybe . T.unpack)
374-- | Run @'Reader'@ as HTTP API data parser.
375runReader :: Reader a -> Text -> Either Text a
376runReader reader input =
378 Left err -> Left ("could not parse: `" <> input <> "' (" <> T.pack err <> ")")
380 | T.null rest -> Right x
381 | otherwise -> defaultParseError input
383-- | Run @'Reader'@ to parse bounded integral value with bounds checking.
385-- >>> parseBounded decimal "256" :: Either Text Word8
386-- Left "out of bounds: `256' (should be between 0 and 255)"
387parseBounded :: forall a. (Bounded a, Integral a) => Reader Integer -> Text -> Either Text a
388parseBounded reader input = do
389 n <- runReader reader input
391 then Left ("out of bounds: `" <> input <> "' (should be between " <> showt l <> " and " <> showt h <> ")")
392 else Right (fromInteger n)
394 l = toInteger (minBound :: a)
395 h = toInteger (maxBound :: a)
400instance ToHttpApiData () where
403instance ToHttpApiData Char where toUrlPiece = T.singleton
406-- >>> toUrlPiece (Version [1, 2, 3] [])
408instance ToHttpApiData Version where
409 toUrlPiece = T.pack . showVersion
411#if MIN_VERSION_base(4,8,0)
412instance ToHttpApiData Void where toUrlPiece = absurd
413instance ToHttpApiData Natural where toUrlPiece = showt
416instance ToHttpApiData Bool where toUrlPiece = showTextData
417instance ToHttpApiData Ordering where toUrlPiece = showTextData
419instance ToHttpApiData Double where toUrlPiece = showt
420instance ToHttpApiData Float where toUrlPiece = showt
421instance ToHttpApiData Int where toUrlPiece = showt
422instance ToHttpApiData Int8 where toUrlPiece = showt
423instance ToHttpApiData Int16 where toUrlPiece = showt
424instance ToHttpApiData Int32 where toUrlPiece = showt
425instance ToHttpApiData Int64 where toUrlPiece = showt
426instance ToHttpApiData Integer where toUrlPiece = showt
427instance ToHttpApiData Word where toUrlPiece = showt
428instance ToHttpApiData Word8 where toUrlPiece = showt
429instance ToHttpApiData Word16 where toUrlPiece = showt
430instance ToHttpApiData Word32 where toUrlPiece = showt
431instance ToHttpApiData Word64 where toUrlPiece = showt
434-- >>> toUrlPiece (fromGregorian 2015 10 03)
436instance ToHttpApiData Day where toUrlPiece = T.pack . show
438timeToUrlPiece :: FormatTime t => String -> t -> Text
439timeToUrlPiece fmt = T.pack . formatTime defaultTimeLocale (iso8601DateFormat (Just fmt))
442-- >>> toUrlPiece $ LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 01)
443-- "2015-10-03T14:55:01"
444instance ToHttpApiData LocalTime where toUrlPiece = timeToUrlPiece "%H:%M:%S"
447-- >>> toUrlPiece $ ZonedTime (LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 01)) utc
448-- "2015-10-03T14:55:01+0000"
449instance ToHttpApiData ZonedTime where toUrlPiece = timeToUrlPiece "%H:%M:%S%z"
452-- >>> toUrlPiece $ UTCTime (fromGregorian 2015 10 03) 864
453-- "2015-10-03T00:14:24Z"
454instance ToHttpApiData UTCTime where toUrlPiece = timeToUrlPiece "%H:%M:%SZ"
456instance ToHttpApiData NominalDiffTime where toUrlPiece = toUrlPiece . (floor :: NominalDiffTime -> Integer)
458instance ToHttpApiData String where toUrlPiece = T.pack
459instance ToHttpApiData Text where toUrlPiece = id
460instance ToHttpApiData L.Text where toUrlPiece = L.toStrict
462instance ToHttpApiData All where toUrlPiece = toUrlPiece . getAll
463instance ToHttpApiData Any where toUrlPiece = toUrlPiece . getAny
465instance ToHttpApiData a => ToHttpApiData (Dual a) where toUrlPiece = toUrlPiece . getDual
466instance ToHttpApiData a => ToHttpApiData (Sum a) where toUrlPiece = toUrlPiece . getSum
467instance ToHttpApiData a => ToHttpApiData (Product a) where toUrlPiece = toUrlPiece . getProduct
468instance ToHttpApiData a => ToHttpApiData (First a) where toUrlPiece = toUrlPiece . getFirst
469instance ToHttpApiData a => ToHttpApiData (Last a) where toUrlPiece = toUrlPiece . getLast
472-- >>> toUrlPiece (Just "Hello")
474instance ToHttpApiData a => ToHttpApiData (Maybe a) where
475 toUrlPiece (Just x) = "just " <> toUrlPiece x
476 toUrlPiece Nothing = "nothing"
479-- >>> toUrlPiece (Left "err" :: Either String Int)
481-- >>> toUrlPiece (Right 3 :: Either String Int)
483instance (ToHttpApiData a, ToHttpApiData b) => ToHttpApiData (Either a b) where
484 toUrlPiece (Left x) = "left " <> toUrlPiece x
485 toUrlPiece (Right x) = "right " <> toUrlPiece x
488-- >>> parseUrlPiece "_" :: Either Text ()
490instance FromHttpApiData () where
491 parseUrlPiece "_" = pure ()
492 parseUrlPiece s = defaultParseError s
494instance FromHttpApiData Char where
497 Just (c, s') | T.null s' -> pure c
498 _ -> defaultParseError s
501-- >>> showVersion <$> parseUrlPiece "1.2.3"
503instance FromHttpApiData Version where
505 case reverse (readP_to_S parseVersion (T.unpack s)) of
506 ((x, ""):_) -> pure x
507 _ -> defaultParseError s
509#if MIN_VERSION_base(4,8,0)
510-- | Parsing a @'Void'@ value is always an error, considering @'Void'@ as a data type with no constructors.
511instance FromHttpApiData Void where
512 parseUrlPiece _ = Left "Void cannot be parsed!"
514instance FromHttpApiData Natural where
516 n <- runReader (signed decimal) s
518 then Left ("underflow: " <> s <> " (should be a non-negative integer)")
519 else Right (fromInteger n)
522instance FromHttpApiData Bool where parseUrlPiece = parseBoundedUrlPiece
523instance FromHttpApiData Ordering where parseUrlPiece = parseBoundedUrlPiece
524instance FromHttpApiData Double where parseUrlPiece = runReader rational
525instance FromHttpApiData Float where parseUrlPiece = runReader rational
526instance FromHttpApiData Int where parseUrlPiece = parseBounded (signed decimal)
527instance FromHttpApiData Int8 where parseUrlPiece = parseBounded (signed decimal)
528instance FromHttpApiData Int16 where parseUrlPiece = parseBounded (signed decimal)
529instance FromHttpApiData Int32 where parseUrlPiece = parseBounded (signed decimal)
530instance FromHttpApiData Int64 where parseUrlPiece = parseBounded (signed decimal)
531instance FromHttpApiData Integer where parseUrlPiece = runReader (signed decimal)
532instance FromHttpApiData Word where parseUrlPiece = parseBounded decimal
533instance FromHttpApiData Word8 where parseUrlPiece = parseBounded decimal
534instance FromHttpApiData Word16 where parseUrlPiece = parseBounded decimal
535instance FromHttpApiData Word32 where parseUrlPiece = parseBounded decimal
536instance FromHttpApiData Word64 where parseUrlPiece = parseBounded decimal
537instance FromHttpApiData String where parseUrlPiece = Right . T.unpack
538instance FromHttpApiData Text where parseUrlPiece = Right
539instance FromHttpApiData L.Text where parseUrlPiece = Right . L.fromStrict
542-- >>> toGregorian <$> parseUrlPiece "2016-12-01"
544instance FromHttpApiData Day where parseUrlPiece = readTextData
546timeParseUrlPiece :: ParseTime t => String -> Text -> Either Text t
547timeParseUrlPiece fmt = parseMaybeTextData (timeParseUrlPieceMaybe . T.unpack)
549 timeParseUrlPieceMaybe = parseTime defaultTimeLocale (iso8601DateFormat (Just fmt))
552-- >>> parseUrlPiece "2015-10-03T14:55:01" :: Either Text LocalTime
553-- Right 2015-10-03 14:55:01
554instance FromHttpApiData LocalTime where parseUrlPiece = timeParseUrlPiece "%H:%M:%S"
557-- >>> parseUrlPiece "2015-10-03T14:55:01+0000" :: Either Text ZonedTime
558-- Right 2015-10-03 14:55:01 +0000
559instance FromHttpApiData ZonedTime where parseUrlPiece = timeParseUrlPiece "%H:%M:%S%z"
562-- >>> parseUrlPiece "2015-10-03T00:14:24Z" :: Either Text UTCTime
563-- Right 2015-10-03 00:14:24 UTC
564instance FromHttpApiData UTCTime where parseUrlPiece = timeParseUrlPiece "%H:%M:%SZ"
566instance FromHttpApiData NominalDiffTime where parseUrlPiece = fmap fromInteger . parseUrlPiece
568instance FromHttpApiData All where parseUrlPiece = fmap All . parseUrlPiece
569instance FromHttpApiData Any where parseUrlPiece = fmap Any . parseUrlPiece
571instance FromHttpApiData a => FromHttpApiData (Dual a) where parseUrlPiece = fmap Dual . parseUrlPiece
572instance FromHttpApiData a => FromHttpApiData (Sum a) where parseUrlPiece = fmap Sum . parseUrlPiece
573instance FromHttpApiData a => FromHttpApiData (Product a) where parseUrlPiece = fmap Product . parseUrlPiece
574instance FromHttpApiData a => FromHttpApiData (First a) where parseUrlPiece = fmap First . parseUrlPiece
575instance FromHttpApiData a => FromHttpApiData (Last a) where parseUrlPiece = fmap Last . parseUrlPiece
578-- >>> parseUrlPiece "Just 123" :: Either Text (Maybe Int)
580instance FromHttpApiData a => FromHttpApiData (Maybe a) where
582 | T.toLower (T.take 7 s) == "nothing" = pure Nothing
583 | otherwise = Just <$> parseUrlPieceWithPrefix "Just " s
586-- >>> parseUrlPiece "Right 123" :: Either Text (Either String Int)
588instance (FromHttpApiData a, FromHttpApiData b) => FromHttpApiData (Either a b) where
590 Right <$> parseUrlPieceWithPrefix "Right " s
591 <!> Left <$> parseUrlPieceWithPrefix "Left " s
597instance ToHttpApiData UUID.UUID where
598 toUrlPiece = UUID.toText
599 toHeader = UUID.toASCIIBytes
601instance FromHttpApiData UUID.UUID where
602 parseUrlPiece = maybe (Left "invalid UUID") Right . UUID.fromText
603 parseHeader = maybe (Left "invalid UUID") Right . UUID.fromASCIIBytes
606-- | Lenient parameters. 'FromHttpApiData' combinators always return `Right`.
609newtype LenientData a = LenientData { getLenientData :: Either Text a }
610 deriving (Eq, Ord, Show, Read, Typeable, Data, Functor, Foldable, Traversable)
612instance FromHttpApiData a => FromHttpApiData (LenientData a) where
613 parseUrlPiece = Right . LenientData . parseUrlPiece
614 parseHeader = Right . LenientData . parseHeader
615 parseQueryParam = Right . LenientData . parseQueryParam
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