Skip to content

Commit 034fe05

Browse files
authored
Add missing HasLink instance for DeepQuery (#1784) (#1814)
* Add missing `HasLink` instance for `DeepQuery` (#1784) * remove Data.Foldable import * add servant to doctests * is Data.Maybe evil? 🤔 * undo changes to cabal files * try and write test for DeepQuery * fix test * update test * Apply feedback by @tchoutri * other way round!
1 parent 0d679b2 commit 034fe05

File tree

4 files changed

+63
-1
lines changed

4 files changed

+63
-1
lines changed

changelog.d/issue-1784

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
synopsis: Add missing HasLink instance for DeepQuery
2+
issues: #1784
3+
4+
description {
5+
Adds missing `HasLink` instance for the `DeepQuery` type.
6+
}

servant/servant.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ test-suite spec
184184
, bytestring
185185
, http-media
186186
, mtl
187+
, network-uri
187188
, servant
188189
, text
189190

servant/src/Servant/Links.hs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ import Servant.API.NamedRoutes
162162
(NamedRoutes)
163163
import Servant.API.QueryParam
164164
(QueryFlag, QueryParam', QueryParams)
165+
import Servant.API.QueryString
166+
(ToDeepQuery, DeepQuery, generateDeepParam, toDeepQuery)
165167
import Servant.API.Raw
166168
(Raw, RawM)
167169
import Servant.API.RemoteHost
@@ -204,7 +206,7 @@ newtype Escaped = Escaped String
204206
type Fragment' = Maybe String
205207

206208
escaped :: String -> Escaped
207-
escaped = Escaped . escapeURIString isUnreserved
209+
escaped = Escaped . escape
208210

209211
getEscaped :: Escaped -> String
210212
getEscaped (Escaped s) = s
@@ -683,3 +685,27 @@ instance {-# OVERLAPPABLE #-} TypeError (NoInstanceFor (HasLink api)) => HasLink
683685
instance HasLink (MultiVerb method cs as r) where
684686
type MkLink (MultiVerb method cs as r) a = a
685687
toLink toA _ = toA
688+
689+
instance (KnownSymbol sym, ToDeepQuery record, HasLink sub) => HasLink (DeepQuery sym record :> sub) where
690+
type MkLink (DeepQuery sym record :> sub) a =
691+
record -> MkLink sub a
692+
693+
toLink :: (KnownSymbol sym, ToDeepQuery record, HasLink sub) =>
694+
(Link -> a)
695+
-> Proxy (DeepQuery sym record :> sub)
696+
-> Link
697+
-> MkLink (DeepQuery sym record :> sub) a
698+
toLink toA _ lnk record =
699+
toLink toA (Proxy @sub) $ addParams lnk
700+
where
701+
k :: Text.Text
702+
k = Text.pack $ symbolVal (Proxy @sym)
703+
704+
mkSingleParam :: ([Text.Text], Maybe Text.Text) -> Param
705+
mkSingleParam x =
706+
let (a, b) = generateDeepParam k x
707+
in SingleParam (Text.unpack a) (Text.pack $ escape $ maybe "" Text.unpack b)
708+
709+
addParams :: Link -> Link
710+
addParams link =
711+
List.foldl' (flip (addQueryParam . mkSingleParam)) link $ toDeepQuery record

servant/test/Servant/LinksSpec.hs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ import Data.Proxy
1212
(Proxy (..))
1313
import Data.String
1414
(fromString)
15+
import qualified Data.Text as T
16+
import Network.URI
17+
(unEscapeString)
1518
import Test.Hspec
1619
(Expectation, Spec, describe, it, shouldBe)
1720

1821
import Servant.API
22+
import Servant.API.QueryString
23+
(ToDeepQuery (toDeepQuery))
1924
import Servant.Links
2025
import Servant.Test.ComprehensiveAPI
2126
(comprehensiveAPIWithoutRaw)
@@ -35,6 +40,9 @@ type TestApi =
3540
-- UVerb
3641
:<|> "uverb-example" :> UVerb 'GET '[JSON] '[WithStatus 200 NoContent]
3742

43+
-- DeepQuery
44+
:<|> "books" :> DeepQuery "filter" BookQuery :> Get '[JSON] [Book]
45+
3846
-- All of the verbs
3947
:<|> "get" :> Get '[JSON] NoContent
4048
:<|> "put" :> Put '[JSON] NoContent
@@ -51,6 +59,18 @@ apiLink :: (IsElem endpoint TestApi, HasLink endpoint)
5159
=> Proxy endpoint -> MkLink endpoint Link
5260
apiLink = safeLink (Proxy :: Proxy TestApi)
5361

62+
data Book
63+
data BookQuery = BookQuery
64+
{ author :: String
65+
, year :: Int
66+
} deriving (Generic, Show, Eq)
67+
68+
instance ToDeepQuery BookQuery where
69+
toDeepQuery (BookQuery author year) =
70+
[ ([T.pack "author"], Just $ toQueryParam author)
71+
, ([T.pack "year"], Just $ toQueryParam year)
72+
]
73+
5474

5575
newtype QuuxRoutes mode = QuuxRoutes
5676
{ corge :: mode :- "corge" :> Post '[PlainText] NoContent
@@ -84,6 +104,10 @@ shouldBeLink :: Link -> String -> Expectation
84104
shouldBeLink link expected =
85105
toUrlPiece link `shouldBe` fromString expected
86106

107+
shouldBeLinkUnescaped :: Link -> String -> Expectation
108+
shouldBeLinkUnescaped link expected =
109+
unEscapeString (T.unpack $ toUrlPiece link) `shouldBe` fromString expected
110+
87111
(//) :: a -> (a -> b) -> b
88112
x // f = f x
89113
infixl 1 //
@@ -152,6 +176,11 @@ spec = describe "Servant.Links" $ do
152176
(fieldLink foo // garply /: "captureme" /: 42 // waldo)
153177
`shouldBeLink` "foo/garply/captureme/42/waldo"
154178

179+
it "generated correct links for DeepQuery" $ do
180+
let bFilter = Proxy :: Proxy ("books" :> DeepQuery "filter" BookQuery :> Get '[JSON] [Book])
181+
let exampleQuery = BookQuery { author = "Herbert", year = 1965 }
182+
apiLink bFilter exampleQuery `shouldBeLinkUnescaped` "books?filter[author]=Herbert&filter[year]=1965"
183+
155184
it "Check links from record fields" $ do
156185
let sub1 = Proxy :: Proxy ("bar" :> Get '[JSON] NoContent)
157186
recordApiLink sub1 `shouldBeLink` "bar"

0 commit comments

Comments
 (0)