Skip to content

Commit 5408b07

Browse files
authored
Merge pull request #120 from fizruk/toEncodedQueryParam
Add toEncodedQueryParam
2 parents 3bcd403 + 4eea083 commit 5408b07

File tree

4 files changed

+85
-26
lines changed

4 files changed

+85
-26
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
0.5.1
2+
-----
3+
4+
* Add `toEncodedQueryParam` to `ToHttpApiData` type class. It has default
5+
implementation using `toQueryParam`, but may be overriden with more efficient
6+
one.
7+
18
0.5
29
---
310

http-api-data.cabal

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
cabal-version: >= 1.10
22
name: http-api-data
3-
version: 0.5
4-
x-revision: 1
3+
version: 0.5.1
54

65
synopsis: Converting to/from HTTP API data like URL pieces, headers and query parameters.
76
category: Web

src/Web/Internal/HttpApiData.hs

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ class ToHttpApiData a where
9393
toUrlPiece = toQueryParam
9494

9595
-- | Convert to a URL path piece, making sure to encode any special chars.
96-
-- The default definition uses 'H.encodePathSegmentsRelative',
96+
-- The default definition uses @'H.urlEncodeBuilder' 'False'@
9797
-- but this may be overriden with a more efficient version.
9898
toEncodedUrlPiece :: a -> BS.Builder
99-
toEncodedUrlPiece = H.encodePathSegmentsRelative . (:[]) . toUrlPiece
99+
toEncodedUrlPiece = H.urlEncodeBuilder False . encodeUtf8 . toUrlPiece
100100

101101
-- | Convert to HTTP header value.
102102
toHeader :: a -> ByteString
@@ -106,6 +106,14 @@ class ToHttpApiData a where
106106
toQueryParam :: a -> Text
107107
toQueryParam = toUrlPiece
108108

109+
-- | Convert to URL query param,
110+
-- The default definition uses @'H.urlEncodeBuilder' 'True'@
111+
-- but this may be overriden with a more efficient version.
112+
--
113+
-- @since 0.5.1
114+
toEncodedQueryParam :: a -> BS.Builder
115+
toEncodedQueryParam = H.urlEncodeBuilder True . encodeUtf8 . toQueryParam
116+
109117
-- | Parse value from HTTP API data.
110118
--
111119
-- __WARNING__: Do not derive this using @DeriveAnyClass@ as the generated
@@ -422,12 +430,21 @@ parseBounded reader input = do
422430
unsafeToEncodedUrlPiece :: ToHttpApiData a => a -> BS.Builder
423431
unsafeToEncodedUrlPiece = BS.byteString . encodeUtf8 . toUrlPiece
424432

433+
-- | Convert to a URL-encoded query param using 'toQueryParam'.
434+
-- /Note/: this function does not check if the result contains unescaped characters!
435+
--
436+
-- @since 0.5.1
437+
unsafeToEncodedQueryParam :: ToHttpApiData a => a -> BS.Builder
438+
unsafeToEncodedQueryParam = BS.byteString . encodeUtf8 . toQueryParam
439+
425440
-- |
426441
-- >>> toUrlPiece ()
427442
-- "_"
428443
instance ToHttpApiData () where
429-
toUrlPiece () = "_"
430-
toEncodedUrlPiece = unsafeToEncodedUrlPiece
444+
toUrlPiece _ = "_"
445+
toHeader _ = "_"
446+
toEncodedUrlPiece _ = "_"
447+
toEncodedQueryParam _ = "_"
431448

432449
instance ToHttpApiData Char where
433450
toUrlPiece = T.singleton
@@ -438,36 +455,38 @@ instance ToHttpApiData Char where
438455
instance ToHttpApiData Version where
439456
toUrlPiece = T.pack . showVersion
440457
toEncodedUrlPiece = unsafeToEncodedUrlPiece
458+
toEncodedQueryParam = unsafeToEncodedQueryParam
441459

442460
instance ToHttpApiData Void where toUrlPiece = absurd
443-
instance ToHttpApiData Natural where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
444-
445-
instance ToHttpApiData Bool where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece
446-
instance ToHttpApiData Ordering where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece
447-
448-
instance ToHttpApiData Double where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
449-
instance ToHttpApiData Float where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
450-
instance ToHttpApiData Int where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
451-
instance ToHttpApiData Int8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
452-
instance ToHttpApiData Int16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
453-
instance ToHttpApiData Int32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
454-
instance ToHttpApiData Int64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
455-
instance ToHttpApiData Integer where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
456-
instance ToHttpApiData Word where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
457-
instance ToHttpApiData Word8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
458-
instance ToHttpApiData Word16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
459-
instance ToHttpApiData Word32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
460-
instance ToHttpApiData Word64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
461+
instance ToHttpApiData Natural where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
462+
463+
instance ToHttpApiData Bool where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
464+
instance ToHttpApiData Ordering where toUrlPiece = showTextData; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
465+
466+
instance ToHttpApiData Double where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
467+
instance ToHttpApiData Float where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
468+
instance ToHttpApiData Int where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
469+
instance ToHttpApiData Int8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
470+
instance ToHttpApiData Int16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
471+
instance ToHttpApiData Int32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
472+
instance ToHttpApiData Int64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
473+
instance ToHttpApiData Integer where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
474+
instance ToHttpApiData Word where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
475+
instance ToHttpApiData Word8 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
476+
instance ToHttpApiData Word16 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
477+
instance ToHttpApiData Word32 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
478+
instance ToHttpApiData Word64 where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
461479

462480
-- | Note: this instance is not polykinded
463-
instance F.HasResolution a => ToHttpApiData (F.Fixed (a :: Type)) where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece
481+
instance F.HasResolution a => ToHttpApiData (F.Fixed (a :: Type)) where toUrlPiece = showt; toEncodedUrlPiece = unsafeToEncodedUrlPiece; toEncodedQueryParam = unsafeToEncodedQueryParam
464482

465483
-- |
466484
-- >>> toUrlPiece (fromGregorian 2015 10 03)
467485
-- "2015-10-03"
468486
instance ToHttpApiData Day where
469487
toUrlPiece = T.pack . show
470488
toEncodedUrlPiece = unsafeToEncodedUrlPiece
489+
toEncodedQueryParam = unsafeToEncodedQueryParam
471490

472491
timeToUrlPiece :: FormatTime t => String -> t -> Text
473492
timeToUrlPiece fmt = T.pack . formatTime defaultTimeLocale (iso8601DateFormat (Just fmt))
@@ -478,27 +497,31 @@ timeToUrlPiece fmt = T.pack . formatTime defaultTimeLocale (iso8601DateFormat (J
478497
instance ToHttpApiData TimeOfDay where
479498
toUrlPiece = T.pack . formatTime defaultTimeLocale "%H:%M:%S%Q"
480499
toEncodedUrlPiece = unsafeToEncodedUrlPiece
500+
-- no toEncodedQueryParam as : is unsafe char.
481501

482502
-- |
483503
-- >>> toUrlPiece $ LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 21.687)
484504
-- "2015-10-03T14:55:21.687"
485505
instance ToHttpApiData LocalTime where
486506
toUrlPiece = timeToUrlPiece "%H:%M:%S%Q"
487507
toEncodedUrlPiece = unsafeToEncodedUrlPiece
508+
-- no toEncodedQueryParam as : is unsafe char.
488509

489510
-- |
490511
-- >>> toUrlPiece $ ZonedTime (LocalTime (fromGregorian 2015 10 03) (TimeOfDay 14 55 51.001)) utc
491512
-- "2015-10-03T14:55:51.001+0000"
492513
instance ToHttpApiData ZonedTime where
493514
toUrlPiece = timeToUrlPiece "%H:%M:%S%Q%z"
494515
toEncodedUrlPiece = unsafeToEncodedUrlPiece
516+
-- no toEncodedQueryParam as : is unsafe char.
495517

496518
-- |
497519
-- >>> toUrlPiece $ UTCTime (fromGregorian 2015 10 03) 864.5
498520
-- "2015-10-03T00:14:24.5Z"
499521
instance ToHttpApiData UTCTime where
500522
toUrlPiece = timeToUrlPiece "%H:%M:%S%QZ"
501523
toEncodedUrlPiece = unsafeToEncodedUrlPiece
524+
-- no toEncodedQueryParam as : is unsafe char.
502525

503526
-- |
504527
-- >>> toUrlPiece Monday
@@ -513,8 +536,9 @@ instance ToHttpApiData DayOfWeek where
513536
toUrlPiece Sunday = "sunday"
514537

515538
toEncodedUrlPiece = unsafeToEncodedUrlPiece
539+
toEncodedQueryParam = unsafeToEncodedQueryParam
516540

517-
-- |
541+
-- |
518542
-- >>> toUrlPiece Q4
519543
-- "q4"
520544
instance ToHttpApiData QuarterOfYear where
@@ -523,6 +547,9 @@ instance ToHttpApiData QuarterOfYear where
523547
toUrlPiece Q3 = "q3"
524548
toUrlPiece Q4 = "q4"
525549

550+
toEncodedUrlPiece = unsafeToEncodedUrlPiece
551+
toEncodedQueryParam = unsafeToEncodedQueryParam
552+
526553
-- |
527554
-- >>> import Data.Time.Calendar.Quarter.Compat (Quarter (..))
528555
-- >>> MkQuarter 8040
@@ -540,6 +567,9 @@ instance ToHttpApiData Quarter where
540567
f Q3 = "q3"
541568
f Q4 = "q4"
542569

570+
toEncodedUrlPiece = unsafeToEncodedUrlPiece
571+
toEncodedQueryParam = unsafeToEncodedQueryParam
572+
543573
-- |
544574
-- >>> import Data.Time.Calendar.Month.Compat (Month (..))
545575
-- >>> MkMonth 24482
@@ -551,8 +581,13 @@ instance ToHttpApiData Quarter where
551581
instance ToHttpApiData Month where
552582
toUrlPiece = T.pack . formatTime defaultTimeLocale "%Y-%m"
553583

584+
toEncodedUrlPiece = unsafeToEncodedUrlPiece
585+
toEncodedQueryParam = unsafeToEncodedQueryParam
586+
554587
instance ToHttpApiData NominalDiffTime where
555588
toUrlPiece = toUrlPiece . nominalDiffTimeToSeconds
589+
590+
toEncodedQueryParam = unsafeToEncodedQueryParam
556591
toEncodedUrlPiece = unsafeToEncodedUrlPiece
557592

558593
instance ToHttpApiData String where toUrlPiece = T.pack
@@ -562,46 +597,57 @@ instance ToHttpApiData L.Text where toUrlPiece = L.toStrict
562597
instance ToHttpApiData All where
563598
toUrlPiece = coerce (toUrlPiece :: Bool -> Text)
564599
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Bool -> BS.Builder)
600+
toEncodedQueryParam = coerce (toEncodedQueryParam :: Bool -> BS.Builder)
565601

566602
instance ToHttpApiData Any where
567603
toUrlPiece = coerce (toUrlPiece :: Bool -> Text)
568604
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Bool -> BS.Builder)
605+
toEncodedQueryParam = coerce (toEncodedQueryParam :: Bool -> BS.Builder)
569606

570607
instance ToHttpApiData a => ToHttpApiData (Dual a) where
571608
toUrlPiece = coerce (toUrlPiece :: a -> Text)
572609
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
610+
toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder)
573611

574612
instance ToHttpApiData a => ToHttpApiData (Sum a) where
575613
toUrlPiece = coerce (toUrlPiece :: a -> Text)
576614
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
615+
toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder)
577616

578617
instance ToHttpApiData a => ToHttpApiData (Product a) where
579618
toUrlPiece = coerce (toUrlPiece :: a -> Text)
580619
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
620+
toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder)
581621

582622
instance ToHttpApiData a => ToHttpApiData (First a) where
583623
toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text)
584624
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)
625+
toEncodedQueryParam = coerce (toEncodedQueryParam :: Maybe a -> BS.Builder)
585626

586627
instance ToHttpApiData a => ToHttpApiData (Last a) where
587628
toUrlPiece = coerce (toUrlPiece :: Maybe a -> Text)
588629
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: Maybe a -> BS.Builder)
630+
toEncodedQueryParam = coerce (toEncodedQueryParam :: Maybe a -> BS.Builder)
589631

590632
instance ToHttpApiData a => ToHttpApiData (Semi.Min a) where
591633
toUrlPiece = coerce (toUrlPiece :: a -> Text)
592634
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
635+
toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder)
593636

594637
instance ToHttpApiData a => ToHttpApiData (Semi.Max a) where
595638
toUrlPiece = coerce (toUrlPiece :: a -> Text)
596639
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
640+
toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder)
597641

598642
instance ToHttpApiData a => ToHttpApiData (Semi.First a) where
599643
toUrlPiece = coerce (toUrlPiece :: a -> Text)
600644
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
645+
toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder)
601646

602647
instance ToHttpApiData a => ToHttpApiData (Semi.Last a) where
603648
toUrlPiece = coerce (toUrlPiece :: a -> Text)
604649
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
650+
toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder)
605651

606652
-- |
607653
-- >>> toUrlPiece (Just "Hello")
@@ -639,20 +685,23 @@ instance ToHttpApiData a => ToHttpApiData (Tagged (b :: Type) a) where
639685
toHeader = coerce (toHeader :: a -> ByteString)
640686
toQueryParam = coerce (toQueryParam :: a -> Text)
641687
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
688+
toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder)
642689

643690
-- | @since 0.4.2
644691
instance ToHttpApiData a => ToHttpApiData (Const a b) where
645692
toUrlPiece = coerce (toUrlPiece :: a -> Text)
646693
toHeader = coerce (toHeader :: a -> ByteString)
647694
toQueryParam = coerce (toQueryParam :: a -> Text)
648695
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
696+
toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder)
649697

650698
-- | @since 0.4.2
651699
instance ToHttpApiData a => ToHttpApiData (Identity a) where
652700
toUrlPiece = coerce (toUrlPiece :: a -> Text)
653701
toHeader = coerce (toHeader :: a -> ByteString)
654702
toQueryParam = coerce (toQueryParam :: a -> Text)
655703
toEncodedUrlPiece = coerce (toEncodedUrlPiece :: a -> BS.Builder)
704+
toEncodedQueryParam = coerce (toEncodedQueryParam :: a -> BS.Builder)
656705

657706
-- |
658707
-- >>> parseUrlPiece "_" :: Either Text ()

test/Web/Internal/HttpApiDataSpec.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ import Web.Internal.TestInstances
4646
encodedUrlPieceProp :: ToHttpApiData a => a -> Property
4747
encodedUrlPieceProp x = toLazyByteString (toEncodedUrlPiece (toUrlPiece x)) === toLazyByteString (toEncodedUrlPiece x)
4848

49+
encodedQueryParamProp :: ToHttpApiData a => a -> Property
50+
encodedQueryParamProp x = toLazyByteString (toEncodedQueryParam (toQueryParam x)) === toLazyByteString (toEncodedQueryParam x)
51+
4952
-- | Check 'ToHttpApiData' and 'FromHttpApiData' compatibility
5053
checkUrlPiece :: forall a. (Eq a, ToHttpApiData a, FromHttpApiData a, Show a, Arbitrary a) => Proxy a -> String -> Spec
5154
checkUrlPiece _ = checkUrlPiece' (arbitrary :: Gen a)
@@ -56,6 +59,7 @@ checkUrlPiece' gen name = describe name $ do
5659
prop "toQueryParam <=> parseQueryParam" $ forAll gen (toQueryParam <=> parseQueryParam :: a -> Property)
5760
prop "toHeader <=> parseHeader" $ forAll gen (toHeader <=> parseHeader :: a -> Property)
5861
prop "toEncodedUrlPiece encodes correctly" $ forAll gen encodedUrlPieceProp
62+
prop "toEncodedQueryParam encodes correctly" $ forAll gen encodedQueryParamProp
5963

6064
-- | Check case insensitivity for @parseUrlPiece@.
6165
checkUrlPieceI :: forall a. (Eq a, ToHttpApiData a, FromHttpApiData a, Arbitrary a) => Proxy a -> String -> Spec

0 commit comments

Comments
 (0)