@@ -55,6 +55,9 @@ import qualified Data.UUID.Types as UUID
5555
5656import Data.Typeable (Typeable)
5757import Data.Data (Data)
58+import qualified Data.ByteString.Builder as BS
59+import qualified Network.HTTP.Types as H
60+
5861
5962-- $setup
6063-- >>> data BasicAuthToken = BasicAuthToken Text deriving (Show)
@@ -70,6 +73,12 @@ class ToHttpApiData a where
7073 toUrlPiece :: a -> Text
7174 toUrlPiece = toQueryParam
7275
76+ -- | Convert to a URL path piece, making sure to encode any special chars.
77+ -- The default definition uses 'H.encodePathSegmentsRelative',
78+ -- but this may be overriden with a more efficient version.
79+ toEncodedUrlPiece :: a -> BS.Builder
80+ toEncodedUrlPiece = H.encodePathSegmentsRelative . (:[]) . toUrlPiece
81+
7382 -- | Convert to HTTP header value.
7483 toHeader :: a -> ByteString
7584 toHeader = encodeUtf8 . toUrlPiece
@@ -387,46 +396,58 @@ parseBounded reader input = do
387396 l = toInteger (minBound :: a)
388397 h = toInteger (maxBound :: a)
389398
399+-- | Convert to a URL-encoded path piece using 'toUrlPiece'.
400+-- /Note/: this function does not check if the result contains unescaped characters!
401+-- This function can be used to override 'toEncodedUrlPiece' as a more efficient implementation
402+-- when the resulting URL piece /never/ has to be escaped.
403+unsafeToEncodedUrlPiece :: ToHttpApiData a => a -> BS.Builder
404+unsafeToEncodedUrlPiece = BS.byteString . encodeUtf8 . toUrlPiece
405+
390406-- |
391407-- >>> toUrlPiece ()
392408-- "_"
393409instance ToHttpApiData () where
394410 toUrlPiece () = "_"
411+ toEncodedUrlPiece = unsafeToEncodedUrlPiece
395412
396−instance ToHttpApiData Char where toUrlPiece = T.singleton
413+instance ToHttpApiData Char where
414+ toUrlPiece = T.singleton
397415
398416-- |
399417-- >>> toUrlPiece (Version [1, 2, 3] [])
400418-- "1.2.3"
401419instance ToHttpApiData Version where
402420 toUrlPiece = T.pack . showVersion
421+ toEncodedUrlPiece = unsafeToEncodedUrlPiece
403422
404423#if MIN_VERSION_base(4,8,0)
405424instance ToHttpApiData Void where toUrlPiece = absurd
406−instance ToHttpApiData Natural where toUrlPiece = showt
425+instance ToHttpApiData Natural where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
407426#endif
408427
409−instance ToHttpApiData Bool where toUrlPiece = showTextData
410−instance ToHttpApiData Ordering where toUrlPiece = showTextData
411−
412−instance ToHttpApiData Double where toUrlPiece = showt
413−instance ToHttpApiData Float where toUrlPiece = showt
414−instance ToHttpApiData Int where toUrlPiece = showt
415−instance ToHttpApiData Int8 where toUrlPiece = showt
416−instance ToHttpApiData Int16 where toUrlPiece = showt
417−instance ToHttpApiData Int32 where toUrlPiece = showt
418−instance ToHttpApiData Int64 where toUrlPiece = showt
419−instance ToHttpApiData Integer where toUrlPiece = showt
420−instance ToHttpApiData Word where toUrlPiece = showt
421−instance ToHttpApiData Word8 where toUrlPiece = showt
422−instance ToHttpApiData Word16 where toUrlPiece = showt
423−instance ToHttpApiData Word32 where toUrlPiece = showt
424−instance ToHttpApiData Word64 where toUrlPiece = showt
428+instance ToHttpApiData Bool where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece
429+instance ToHttpApiData Ordering where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece
430+
431+instance ToHttpApiData Double where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
432+instance ToHttpApiData Float where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
433+instance ToHttpApiData Int where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
434+instance ToHttpApiData Int8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
435+instance ToHttpApiData Int16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
436+instance ToHttpApiData Int32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
437+instance ToHttpApiData Int64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
438+instance ToHttpApiData Integer where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
439+instance ToHttpApiData Word where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
440+instance ToHttpApiData Word8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
441+instance ToHttpApiData Word16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
442+instance ToHttpApiData Word32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
443+instance ToHttpApiData Word64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
425444
426445-- |
427446-- >>> toUrlPiece (fromGregorian 2015 10 03)
428447-- "2015-10-03"
429−instance ToHttpApiData Day where toUrlPiece = T.pack . show
448+instance ToHttpApiData Day where
449+ toUrlPiece = T.pack . show
450+ toEncodedUrlPiece = unsafeToEncodedUrlPiece
430451
431452timeToUrlPiece :: FormatTime t => String -> t -> Text
432453timeToUrlPiece fmt = T.pack . formatTime defaultTimeLocale (iso8601DateFormat (Just fmt))
@@ -434,32 +455,54 @@ timeToUrlPiece fmt = T.pack . formatTime defaultTimeLocale (iso8601DateFormat (J
434455-- |
435456-- >>> toUrlPiece $ LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 01)
436457-- "2015-10-03T14:55:01"
437−instance ToHttpApiData LocalTime where toUrlPiece = timeToUrlPiece "%H:%M:%S"
458+instance ToHttpApiData LocalTime where
459+ toUrlPiece = timeToUrlPiece "%H:%M:%S"
460+ toEncodedUrlPiece = unsafeToEncodedUrlPiece
438461
439462-- |
440463-- >>> toUrlPiece $ ZonedTime (LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 01)) utc
441464-- "2015-10-03T14:55:01+0000"
442−instance ToHttpApiData ZonedTime where toUrlPiece = timeToUrlPiece "%H:%M:%S%z"
465+instance ToHttpApiData ZonedTime where
466+ toUrlPiece = timeToUrlPiece "%H:%M:%S%z"
467+ toEncodedUrlPiece = unsafeToEncodedUrlPiece
443468
444469-- |
445470-- >>> toUrlPiece $ UTCTime (fromGregorian 2015 10 03) 864
446471-- "2015-10-03T00:14:24Z"
447−instance ToHttpApiData UTCTime where toUrlPiece = timeToUrlPiece "%H:%M:%SZ"
472+instance ToHttpApiData UTCTime where
473+ toUrlPiece = timeToUrlPiece "%H:%M:%SZ"
474+ toEncodedUrlPiece = unsafeToEncodedUrlPiece
448475
449−instance ToHttpApiData NominalDiffTime where toUrlPiece = toUrlPiece . (floor :: NominalDiffTime -> Integer)
476+instance ToHttpApiData NominalDiffTime where
477+ toUrlPiece = toUrlPiece . (floor :: NominalDiffTime -> Integer)
478+ toEncodedUrlPiece = unsafeToEncodedUrlPiece
450479
451480instance ToHttpApiData String where toUrlPiece = T.pack
452481instance ToHttpApiData Text where toUrlPiece = id
453482instance ToHttpApiData L.Text where toUrlPiece = L.toStrict
454483
455−instance ToHttpApiData All where toUrlPiece = toUrlPiece . getAll
456−instance ToHttpApiData Any where toUrlPiece = toUrlPiece . getAny
484+instance ToHttpApiData All where toUrlPiece = toUrlPiece . getAll; toEncodedUrlPiece = toEncodedUrlPiece . getAll
485+instance ToHttpApiData Any where toUrlPiece = toUrlPiece . getAny; toEncodedUrlPiece = toEncodedUrlPiece . getAny
486+
487+instance ToHttpApiData a => ToHttpApiData (Dual a) where
488+ toUrlPiece = toUrlPiece . getDual
489+ toEncodedUrlPiece = toEncodedUrlPiece . getDual
490+
491+instance ToHttpApiData a => ToHttpApiData (Sum a) where
492+ toUrlPiece = toUrlPiece . getSum
493+ toEncodedUrlPiece = toEncodedUrlPiece . getSum
494+
495+instance ToHttpApiData a => ToHttpApiData (Product a) where
496+ toUrlPiece = toUrlPiece . getProduct
497+ toEncodedUrlPiece = toEncodedUrlPiece . getProduct
498+
499+instance ToHttpApiData a => ToHttpApiData (First a) where
500+ toUrlPiece = toUrlPiece . getFirst
501+ toEncodedUrlPiece = toEncodedUrlPiece . getFirst
457502
458−instance ToHttpApiData a => ToHttpApiData (Dual a) where toUrlPiece = toUrlPiece . getDual
459−instance ToHttpApiData a => ToHttpApiData (Sum a) where toUrlPiece = toUrlPiece . getSum
460−instance ToHttpApiData a => ToHttpApiData (Product a) where toUrlPiece = toUrlPiece . getProduct
461−instance ToHttpApiData a => ToHttpApiData (First a) where toUrlPiece = toUrlPiece . getFirst
462−instance ToHttpApiData a => ToHttpApiData (Last a) where toUrlPiece = toUrlPiece . getLast
503+instance ToHttpApiData a => ToHttpApiData (Last a) where
504+ toUrlPiece = toUrlPiece . getLast
505+ toEncodedUrlPiece = toEncodedUrlPiece . getLast
463506
464507-- |
465508-- >>> toUrlPiece (Just "Hello")
@@ -590,6 +633,7 @@ instance (FromHttpApiData a, FromHttpApiData b) => FromHttpApiData (Either a b)
590633instance ToHttpApiData UUID.UUID where
591634 toUrlPiece = UUID.toText
592635 toHeader = UUID.toASCIIBytes
636+ toEncodedUrlPiece = unsafeToEncodedUrlPiece
593637
594638instance FromHttpApiData UUID.UUID where
595639 parseUrlPiece = maybe (Left "invalid UUID") Right . UUID.fromText