packages feed

servant-client 0.12.0.1 → 0.20.3.0

raw patch · 23 files changed

Files

CHANGELOG.md view
@@ -1,6 +1,351 @@ [The latest version of this document is on GitHub.](https://github.com/haskell-servant/servant/blob/master/servant-client/CHANGELOG.md) [Changelog for `servant` package contains significant entries for all core packages.](https://github.com/haskell-servant/servant/blob/master/servant/CHANGELOG.md) +Package versions follow the [Package Versioning Policy](https://pvp.haskell.org/): in A.B.C, bumps to either A or B represent major versions.++0.20.3.0+--------++- Server-sent events (SSE) for client-side [#1811](https://github.com/haskell-servant/servant/issues/1811)++  Implement Server-sent events (SSE) for the Servant client using a new+  combinator "ServerSentEvents". The raw event messages, accumulated events and+  JSON-processed events can be exposed.++- Integrate MultiVerb [#1766](https://github.com/haskell-servant/servant/pull/1766) [#1804](https://github.com/haskell-servant/servant/pull/1804)++  Expose MultiVerb, a more ergonomic way of defining endpoints that return+  many kinds of responses. Read the cookbook https://docs.servant.dev/en/master/cookbook/multiverb/MultiVerb.html++- Add public re-export of renderCurlBasePath lens [#1706](https://github.com/haskell-servant/servant/pull/1706)+- Remove GHC <= 8.10.7 from the support window [#1778](https://github.com/haskell-servant/servant/pull/1778)+- Add Servant.API.Range type [#1805](https://github.com/haskell-servant/servant/pull/1805)+- Add missing HasLink instance for DeepQuery [#1784](https://github.com/haskell-servant/servant/issues/1784) [#1814](https://github.com/haskell-servant/servant/pull/1814)+- Add Host API combinator [#1800](https://github.com/haskell-servant/servant/pull/1800)++  Adding a Host combinator allows servant users to select APIs according+  to the Host header provided by clients.++0.20.2+------++- Client Middleware [#1720](https://github.com/haskell-servant/servant/pull/1720)++  Clients now support real middleware of type `(Request -> ClientM Response) -> Request -> ClientM Response` which can be configured in `ClientEnv`.+  This allows access to raw request and response data. It can also be used to control how/when/if actual requests are performed.+  Middleware can be chained with function composition `mid1 . mid2 . mid3`.++0.20+----++- Escape special chars in QueryParams. [#1584](https://github.com/haskell-servant/servant/issues/1584) [#1597](https://github.com/haskell-servant/servant/pull/1597)++  Escape special chars in QueryParam (`:@&=+$`) in servant-client. Note that this+  mean binary data will not work as is, and so reverts the functionality in+  [#1432](https://github.com/haskell-servant/servant/pull/1432).++- Handle Cookies correctly for RunStreamingClient [#1605](https://github.com/haskell-servant/servant/issues/1605) [#1606](https://github.com/haskell-servant/servant/pull/1606)++  Makes `performWithStreamingRequest` take into consideration the+  CookieJar, which it previously didn't.++- Fix the handling of multiple headers with the same name. [#1666](https://github.com/haskell-servant/servant/pull/1666)++  servant-client no longer concatenates the values of response headers with the same name.+  This fixes an issue with parsing multiple `Set-Cookie` headers.++0.19+----++### Significant changes++- Drop support for GHC < 8.6.+- Support GHC 9.0 (GHC 9.2 should work as well, but isn't fully tested yet).+- Support Aeson 2 ([#1475](https://github.com/haskell-servant/servant/pull/1475)),+  which fixes a [DOS vulnerability](https://github.com/haskell/aeson/issues/864)+  related to hash collisions.+- Add `NamedRoutes` combinator, making support for records first-class in Servant+  ([#1388](https://github.com/haskell-servant/servant/pull/1388)).+- Add custom type errors for partially applied combinators+  ([#1289](https://github.com/haskell-servant/servant/pull/1289),+  [#1486](https://github.com/haskell-servant/servant/pull/1486)).+- *servant-client* / *servant-client-core* / *servant-http-streams*: Fix+  erroneous behavior, where only 2XX status codes would be considered+  successful, irrelevant of the status parameter specified by the verb+  combinator. ([#1469](https://github.com/haskell-servant/servant/pull/1469))+- *servant-client* / *servant-client-core*: Fix `Show` instance for+  `Servant.Client.Core.Request`.+- *servant-client* /  *servant-client-core*: Allow passing arbitrary binary data+  in Query parameters.+  ([#1432](https://github.com/haskell-servant/servant/pull/1432)).++### Other changes++- Various version bumps.++0.18.3+------++### Significant changes++- Add response header support to UVerb (#1420)++### Other changes++- Support GHC-9.0.1.+- Bump `bytestring`, `hspec`, `http-client` and `QuickCheck` dependencies.++0.18.2+------++### Significant changes++- Support `Fragment` combinator.++0.18.1+------++### Significant changes++- Union verbs++### Other changes++- Bump "tested-with" ghc versions++0.18+----++### Significant changes++- Support for ghc8.8 (#1318, #1326, #1327)+++0.17+----++### Significant changes++- Add NoContentVerb [#1028](https://github.com/haskell-servant/servant/issues/1028) [#1219](https://github.com/haskell-servant/servant/pull/1219) [#1228](https://github.com/haskell-servant/servant/pull/1228)++  The `NoContent` API endpoints should now use `NoContentVerb` combinator.+  The API type changes are usually of the kind++  ```diff+  - :<|> PostNoContent '[JSON] NoContent+  + :<|> PostNoContent+  ```++  i.e. one doesn't need to specify the content-type anymore. There is no content.++- `Capture` can be `Lenient` [#1155](https://github.com/haskell-servant/servant/issues/1155) [#1156](https://github.com/haskell-servant/servant/pull/1156)++  You can specify a lenient capture as++  ```haskell+  :<|> "capture-lenient"  :> Capture' '[Lenient] "foo" Int :> GET+  ```++  which will make the capture always succeed. Handlers will be of the+  type `Either String CapturedType`, where `Left err` represents+  the possible parse failure.++- *servant-client* Added a function to create Client.Request in ClientEnv [#1213](https://github.com/haskell-servant/servant/pull/1213) [#1255](https://github.com/haskell-servant/servant/pull/1255)++  The new member `makeClientRequest` of `ClientEnv` is used to create+  `http-client` `Request` from `servant-client-core` `Request`.+  This functionality can be used for example to set+  dynamic timeouts for each request.++### Other changes++- *servant-client* *servant-client-core* *servant-http-streams* Fix Verb with headers checking content type differently [#1200](https://github.com/haskell-servant/servant/issues/1200) [#1204](https://github.com/haskell-servant/servant/pull/1204)++  For `Verb`s with response `Headers`, the implementation didn't check+  for the content-type of the response. Now it does.++- *servant-client* *servant-http-streams* `HasClient` instance for `Stream` with `Headers` [#1170](https://github.com/haskell-servant/servant/issues/1170) [#1197](https://github.com/haskell-servant/servant/pull/1197)+- *servant-client* Redact the authorization header in Show and exceptions [#1238](https://github.com/haskell-servant/servant/pull/1238)++++0.16.0.1+--------++- Allow `base-compat-0.11`++0.16+----++- Rename `ServantError` to `ClientError`, `ServantErr` to `ServerError`+  [#1131](https://github.com/haskell-servant/servant/pull/1131)++- *servant-client-core* Rearrange modules. No more `Internal` modules, whole+  API is versioned.+  [#1130](https://github.com/haskell-servant/servant/pull/1130)++- *servant-client-core* `RequestBody` is now++    ```haskell+    = RequestBodyLBS LBS.ByteString+    | RequestBodyBS BS.ByteString+    | RequestBodySource (SourceIO LBS.ByteString)+    ```++  i.e. no more replicates `http-client`s API.+  [#1117](https://github.com/haskell-servant/servant/pull/1117)++- *servant-client-core* Keep structured exceptions in `ConnectionError`+  constructor of `ClientError`+  [#1115](https://github.com/haskell-servant/servant/pull/1115)++    ```diff+    -| ConnectionError Text+    +| ConnectionError SomeException+    ```++- *servant-client-core* Preserve failing request in `FailureResponse`+  constructor of `ClientError`+  [#1114](https://github.com/haskell-servant/servant/pull/1114)++    ```diff+    -FailureResponse Response+    +-- | The server returned an error response including the+    +-- failing request. 'requestPath' includes the 'BaseUrl' and the+    +-- path of the request.+    +FailureResponse (RequestF () (BaseUrl, BS.ByteString)) Response+    ```++- *servant-client* Fix (implement) `StreamBody` instance+  [#1110](https://github.com/haskell-servant/servant/pull/1110)++- *servant-client* Update CookieJar with intermediate request/responses (redirects)+  [#1104](https://github.com/haskell-servant/servant/pull/1104)++0.15+----++- Streaming refactoring.+  [#991](https://github.com/haskell-servant/servant/pull/991)+  [#1076](https://github.com/haskell-servant/servant/pull/1076)+  [#1077](https://github.com/haskell-servant/servant/pull/1077)++  The streaming functionality (`Servant.API.Stream`) is refactored to use+  `servant`'s own `SourceIO` type (see `Servant.Types.SourceT` documentation),+  which replaces both `StreamGenerator` and `ResultStream` types.++  New conversion type-classes are `ToSourceIO` and `FromSourceIO`+  (replacing `ToStreamGenerator` and `BuildFromStream`).+  There are instances for *conduit*, *pipes* and *machines* in new packages:+  [servant-conduit](https://hackage.haskell.org/package/servant-conduit)+  [servant-pipes](https://hackage.haskell.org/package/servant-pipes) and+  [servant-machines](https://hackage.haskell.org/package/servant-machines)+  respectively.++  Writing new framing strategies is simpler. Check existing strategies for examples.++  This change shouldn't affect you, if you don't use streaming endpoints.++- *servant-client* Separate streaming client.+  [#1066](https://github.com/haskell-servant/servant/pull/1066)++  We now have two `http-client` based clients,+  in `Servant.Client` and `Servant.Client.Streaming`.++  Their API is the same, except for+  - `Servant.Client` **cannot** request `Stream` endpoints.+  - `Servant.Client` is *run* by direct+    `runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)`+  - `Servant.Client.Streaming` **can** request `Stream` endpoints.+  - `Servant.Client.Streaming` is *used* by CPSised+    `withClientM :: ClientM a -> ClientEnv -> (Either ServantError a -> IO b) -> IO b`++  To access `Stream` endpoints use `Servant.Client.Streaming` with+  `withClientM`; otherwise you can continue using `Servant.Client` with `runClientM`.+  You can use both too, `ClientEnv` and `BaseUrl` types are same for both.++  **Note:** `Servant.Client.Streaming` doesn't *stream* non-`Stream` endpoints.+  Requesting ordinary `Verb` endpoints (e.g. `Get`) will block until+  the whole response is received.++  There is `Servant.Client.Streaming.runClientM` function, but it has+  restricted type. `NFData a` constraint prevents using it with+  `SourceT`, `Conduit` etc. response types.++  ```haskell+  runClientM :: NFData a => ClientM a -> ClientEnv -> IO (Either ServantError a)+  ```++  This change shouldn't affect you, if you don't use streaming endpoints.++- Drop support for GHC older than 8.0+  [#1008](https://github.com/haskell-servant/servant/pull/1008)+  [#1009](https://github.com/haskell-servant/servant/pull/1009)++- *servant-client-core* Add `NFData (GenResponse a)` and `NFData ServantError` instances.+  [#1076](https://github.com/haskell-servant/servant/pull/1076)++ *servant-client-core* Add `aeson` and `Lift BaseUrl` instances+  [#1037](https://github.com/haskell-servant/servant/pull/1037)++0.14+----++- `Stream` takes a status code argument++  ```diff+  -Stream method        framing ctype a+  +Stream method status framing ctype a+  ```++  ([#966](https://github.com/haskell-servant/servant/pull/966)+   [#972](https://github.com/haskell-servant/servant/pull/972))++- `ToStreamGenerator` definition changed, so it's possible to write an instance+  for conduits.++  ```diff+  -class ToStreamGenerator f a where+  -   toStreamGenerator :: f a -> StreamGenerator a+  +class ToStreamGenerator a b | a -> b where+  +   toStreamGenerator :: a -> StreamGenerator b+  ```++  ([#959](https://github.com/haskell-servant/servant/pull/959))++- Added `NoFraming` streaming strategy+  ([#959](https://github.com/haskell-servant/servant/pull/959))++- *servant-client-core* Add `hoistClient` to `HasClient`.+  Just like `hoistServer` allows us to change the monad in which request handlers+  of a web application live, we also have `hoistClient` for changing the monad+  in which *client functions* live.+  Read [tutorial section for more information](https://docs.servant.dev/en/release-0.14/tutorial/Client.html#changing-the-monad-the-client-functions-live-in).+  ([#936](https://github.com/haskell-servant/servant/pull/936))++- *servant-client* Add more constructors to `RequestBody`, including+  `RequestBodyStream`.+  *Note:* we are looking for http-library agnostic API,+  so the might change again soon.+  Tell us which constructors are useful for you!+  ([#913](https://github.com/haskell-servant/servant/pull/913))++0.13.0.1+--------++- Support `base-compat-0.10`++0.13+----++- Streaming endpoint support.+  ([#836](https://github.com/haskell-servant/servant/pull/836))+- *servant* Add `Servant.API.Modifiers`+  ([#873](https://github.com/haskell-servant/servant/pull/873))+- *servant-client* Support `http-client`’s `CookieJar`+  ([#897](https://github.com/haskell-servant/servant/pull/897)+   [#883](https://github.com/haskell-servant/servant/pull/883))+ 0.12.0.1 -------- 
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2014-2016, Zalora South East Asia Pte Ltd, Servant Contributors+Copyright (c) 2014-2016, Zalora South East Asia Pte Ltd, 2016-2018 Servant Contributors  All rights reserved. 
+ README.lhs view
@@ -0,0 +1,41 @@+# servant-client++![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)++This library lets you automatically derive Haskell functions that let you query each endpoint of a *servant* webservice.++## Example++``` haskell+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}++import Data.Proxy+import Data.Text (Text)+import Network.HTTP.Client (newManager, defaultManagerSettings)+import Servant.API+import Servant.Client+++type Book = Text++type MyApi = "books" :> Get '[JSON] [Book] -- GET /books+        :<|> "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book -- POST /books++myApi :: Proxy MyApi+myApi = Proxy++-- 'client' allows you to produce operations to query an API from a client.+postNewBook :: Book -> ClientM Book+getAllBooks :: ClientM [Book]+(getAllBooks :<|> postNewBook) = client myApi+++main :: IO ()+main = do+  manager' <- newManager defaultManagerSettings+  res <- runClientM getAllBooks (mkClientEnv manager' (BaseUrl Http "localhost" 8081 ""))+  case res of+    Left err -> putStrLn $ "Error: " ++ show err+    Right books -> print books+```
README.md view
@@ -7,14 +7,35 @@ ## Example  ``` haskell+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}++import Data.Proxy+import Data.Text (Text)+import Network.HTTP.Client (newManager, defaultManagerSettings)+import Servant.API+import Servant.Client+++type Book = Text+ type MyApi = "books" :> Get '[JSON] [Book] -- GET /books-        :<|> "books" :> ReqBody Book :> Post '[JSON] Book -- POST /books+        :<|> "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book -- POST /books  myApi :: Proxy MyApi myApi = Proxy -getAllBooks :: Manager -> BaseUrl -> ExceptT String IO [Book]-postNewBook :: Book -> Manager -> BaseUrl -> ExceptT String IO Book -- 'client' allows you to produce operations to query an API from a client.+postNewBook :: Book -> ClientM Book+getAllBooks :: ClientM [Book] (getAllBooks :<|> postNewBook) = client myApi+++main :: IO ()+main = do+  manager' <- newManager defaultManagerSettings+  res <- runClientM getAllBooks (mkClientEnv manager' (BaseUrl Http "localhost" 8081 ""))+  case res of+    Left err -> putStrLn $ "Error: " ++ show err+    Right books -> print books ```
− include/overlapping-compat.h
@@ -1,8 +0,0 @@-#if __GLASGOW_HASKELL__ >= 710-#define OVERLAPPABLE_ {-# OVERLAPPABLE #-}-#define OVERLAPPING_  {-# OVERLAPPING #-}-#else-{-# LANGUAGE OverlappingInstances #-}-#define OVERLAPPABLE_-#define OVERLAPPING_-#endif
servant-client.cabal view
@@ -1,100 +1,200 @@-name:                servant-client-version:             0.12.0.1-synopsis: automatical derivation of querying functions for servant webservices+cabal-version:      3.0+name:               servant-client+version:            0.20.3.0+synopsis:           Automatic derivation of querying functions for servant+category:           Servant, Web description:   This library lets you derive automatically Haskell functions that   let you query each endpoint of a <http://hackage.haskell.org/package/servant servant> webservice.   .-  See <http://haskell-servant.readthedocs.org/en/stable/tutorial/Client.html the client section of the tutorial>.+  See <http://docs.servant.dev/en/stable/tutorial/Client.html the client section of the tutorial>.   .   <https://github.com/haskell-servant/servant/blob/master/servant-client/CHANGELOG.md CHANGELOG>-license:             BSD3-license-file:        LICENSE-author:              Servant Contributors-maintainer:          haskell-servant-maintainers@googlegroups.com-copyright:           2014-2016 Zalora South East Asia Pte Ltd, 2016-2017 Servant Contributors-category:            Servant, Web-build-type:          Simple-cabal-version:       >=1.10-tested-with:-  GHC==7.8.4-  GHC==7.10.3-  GHC==8.0.2-  GHC==8.2.1-homepage:            http://haskell-servant.readthedocs.org/-Bug-reports:         http://github.com/haskell-servant/servant/issues++homepage:           http://docs.servant.dev/+bug-reports:        http://github.com/haskell-servant/servant/issues+license:            BSD-3-Clause+license-file:       LICENSE+author:             Servant Contributors+maintainer:         haskell-servant-maintainers@googlegroups.com+copyright:+  2014-2016 Zalora South East Asia Pte Ltd, 2016-2019 Servant Contributors++build-type:         Simple+tested-with:        GHC ==9.2.8 || ==9.4.8 || ==9.6.6 || ==9.8.4 || ==9.10.1 || ==9.12.1+ extra-source-files:-  include/*.h   CHANGELOG.md   README.md+ source-repository head-  type: git+  type:     git   location: http://github.com/haskell-servant/servant.git +common extensions+  default-extensions:+    AllowAmbiguousTypes+    ConstraintKinds+    DataKinds+    DeriveAnyClass+    DeriveDataTypeable+    DeriveFunctor+    DeriveGeneric+    DerivingStrategies+    DerivingVia+    DuplicateRecordFields+    ExplicitNamespaces+    FlexibleContexts+    FlexibleInstances+    FunctionalDependencies+    GADTs+    GeneralizedNewtypeDeriving+    InstanceSigs+    KindSignatures+    LambdaCase+    MultiParamTypeClasses+    NamedFieldPuns+    NoStarIsType+    OverloadedLabels+    OverloadedStrings+    PackageImports+    PolyKinds+    QuantifiedConstraints+    RankNTypes+    RecordWildCards+    ScopedTypeVariables+    StrictData+    TupleSections+    TypeApplications+    TypeFamilies+    TypeOperators+    UndecidableInstances+    ViewPatterns++  default-language:   Haskell2010++common ghc-options+  ghc-options:+    -Wall -Wcompat -Widentities -Wincomplete-record-updates+    -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints+    -fhide-source-paths -Wno-unused-do-bind -fdicts-strict+    -Wno-unticked-promoted-constructors -Werror=unused-imports+    -Wunused-packages+ library+  import:          extensions+  import:          ghc-options   exposed-modules:     Servant.Client     Servant.Client.Internal.HttpClient+    Servant.Client.Internal.HttpClient.Streaming+    Servant.Client.Streaming++  -- Bundled with GHC: Lower bound to not force re-installs+  -- text and mtl are bundled starting with GHC-8.4   build-depends:-      base                  >= 4.7      && < 4.11-    , base-compat           >= 0.9.1    && < 0.10-    , bytestring            >= 0.10     && < 0.11-    , aeson                 >= 0.7      && < 1.3-    , attoparsec            >= 0.12     && < 0.14-    , containers            >= 0.5      && < 0.6-    , http-client           >= 0.4.30   && < 0.6-    , http-client-tls       >= 0.2.2    && < 0.4-    , http-media            >= 0.6.2    && < 0.8-    , http-types            >= 0.8.6    && < 0.11-    , exceptions            >= 0.8      && < 0.9-    , monad-control         >= 1.0.0.4  && < 1.1-    , mtl                   >= 2.1      && < 2.3-    , semigroupoids         >= 4.3      && < 5.3-    , servant-client-core   == 0.12.*-    , text                  >= 1.2      && < 1.3-    , transformers          >= 0.3      && < 0.6-    , transformers-base     >= 0.4.4    && < 0.5-    , transformers-compat   >= 0.4      && < 0.6-  hs-source-dirs: src-  default-language: Haskell2010-  ghc-options: -Wall-  if impl(ghc >= 8.0)-    ghc-options: -Wno-redundant-constraints-  include-dirs: include+    , base          >= 4.16.4.0 && < 4.22+    , bytestring    >=0.11 && <0.13+    , containers    >=0.6.5.1  && <0.9+    , deepseq       >=1.4.2.0  && <1.6+    , mtl           ^>=2.2.2   || ^>=2.3.1+    , stm           >=2.4.5.1  && <2.6+    , time          >=1.6.0.1  && <1.15+    , transformers  >=0.5.2.0  && <0.7 +  -- Servant dependencies.+  -- Strict dependency on `servant-client-core` as we re-export things.+  build-depends:+    , servant              >=0.20.2 && <0.21+    , servant-client-core  >=0.20.2 && <0.21++  -- Other dependencies: Lower bound around what is in the latest Stackage LTS.+  -- Here can be exceptions if we really need features from the newer versions.+  build-depends:+    , base-compat          >=0.10.5   && <0.15+    , exceptions           >=0.10.0   && <0.11+    , http-client          >=0.5.13.1 && <0.8+    , http-media           >=0.7.1.3  && <0.9+    , http-types           >=0.12.2   && <0.13+    , kan-extensions       >=5.2      && <5.3+    , monad-control        >=1.0.2.3  && <1.1+    , semigroupoids        >=5.3.1    && <6.1+    , transformers-base    >=0.4.5.2  && <0.5++  hs-source-dirs:  src+ test-suite spec-  type: exitcode-stdio-1.0-  ghc-options: -Wall-  default-language: Haskell2010-  hs-source-dirs: test-  main-is: Spec.hs-  build-tool-depends:-    hspec-discover:hspec-discover+  import:             extensions+  import:             ghc-options+  type:               exitcode-stdio-1.0+  ghc-options:        -Wall -rtsopts -threaded "-with-rtsopts=-T -N2"++  if impl(ghcjs)+    buildable: False++  hs-source-dirs:     test+  main-is:            Spec.hs   other-modules:-      Servant.ClientSpec+    Servant.BasicAuthSpec+    Servant.BrokenSpec+    Servant.ClientTestUtils+    Servant.ConnectionErrorSpec+    Servant.FailSpec+    Servant.GenAuthSpec+    Servant.GenericSpec+    Servant.HoistClientSpec+    Servant.MiddlewareSpec+    Servant.StreamSpec+    Servant.SuccessSpec+    Servant.WrappedApiSpec++  -- Dependencies inherited from the library. No need to specify bounds.   build-depends:-      base == 4.*     , aeson+    , base     , base-compat     , bytestring-    , containers-    , deepseq-    , hspec == 2.*     , http-api-data     , http-client-    , http-media     , http-types-    , HUnit     , mtl-    , network >= 2.6-    , QuickCheck >= 2.7-    , servant     , servant-client     , servant-client-core-    , servant-server == 0.12.*+    , sop-core+    , generics-sop+    , stm     , text     , transformers-    , transformers-compat     , wai     , warp-    , generics-sop++  -- Additional dependencies+  build-depends:+    , entropy         >=0.4.1.3  && <0.5+    , hspec           >=2.6.0    && <2.12+    , HUnit           >=1.6.0.0  && <1.7+    , network         >=2.8.0.0  && <3.3+    , QuickCheck      >=2.12.6.1 && <2.16+    , servant         >=0.20.2   && <0.21+    , servant-server  >=0.20.2   && <0.21++  build-tool-depends: hspec-discover:hspec-discover >=2.6.0 && <2.12++test-suite readme+  import:             extensions+  import:             ghc-options+  type:               exitcode-stdio-1.0+  main-is:            README.lhs+  build-depends:+    , base+    , http-client+    , markdown-unlit+    , servant+    , servant-client+    , text++  build-tool-depends: markdown-unlit:markdown-unlit+  ghc-options:        -pgmL markdown-unlit++  if impl(ghcjs)+    buildable: False
src/Servant/Client.hs view
@@ -1,3 +1,5 @@++ -- | This module provides 'client' which can automatically generate -- querying functions for each endpoint just from the type representing your -- API.@@ -6,8 +8,11 @@   , ClientM   , runClientM   , ClientEnv(..)+  , mkClientEnv+  , defaultMakeClientRequest+  , hoistClient   , module Servant.Client.Core.Reexport   ) where -import Servant.Client.Internal.HttpClient-import Servant.Client.Core.Reexport+import           Servant.Client.Core.Reexport+import           Servant.Client.Internal.HttpClient
src/Servant/Client/Internal/HttpClient.hs view
@@ -1,54 +1,97 @@ {-# LANGUAGE CPP                        #-}-{-# LANGUAGE DeriveDataTypeable         #-}-{-# LANGUAGE DeriveGeneric              #-}-{-# LANGUAGE FlexibleContexts           #-}-{-# LANGUAGE FlexibleInstances          #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses      #-}-{-# LANGUAGE OverloadedStrings          #-}-{-# LANGUAGE ScopedTypeVariables        #-}-{-# LANGUAGE TypeFamilies               #-}---- | @http-client@-based client requests executor module Servant.Client.Internal.HttpClient where --import           Prelude                     ()+import           Prelude () import           Prelude.Compat +import           Control.Concurrent.MVar+                 (modifyMVar, newMVar)+import           Control.Concurrent.STM.TVar import           Control.Exception+                 (SomeException (..), catch) import           Control.Monad-import           Control.Monad.Base          (MonadBase (..))-import           Control.Monad.Catch         (MonadCatch, MonadThrow)-import           Control.Monad.Error.Class   (MonadError (..))+                 (unless)+import           Control.Monad.Base+                 (MonadBase (..))+import           Control.Monad.Catch+                 (MonadCatch, MonadThrow, MonadMask)+import           Control.Monad.Error.Class+                 (MonadError (..))+import           Control.Monad.IO.Class+                 (MonadIO (..)) import           Control.Monad.Reader-import           Control.Monad.Trans.Control (MonadBaseControl (..))+                 (MonadReader, ReaderT, ask, runReaderT)+import           Control.Monad.STM+                 (STM, atomically)+import           Control.Monad.Trans.Control+                 (MonadBaseControl (..)) import           Control.Monad.Trans.Except-import           Data.ByteString.Builder     (toLazyByteString)+                 (ExceptT, runExceptT)+import           Data.Bifunctor+                 (bimap)+import qualified Data.ByteString             as BS+import           Data.ByteString.Builder+                 (toLazyByteString) import qualified Data.ByteString.Lazy        as BSL-import           Data.Foldable               (toList)-import           Data.Functor.Alt            (Alt (..))-import           Data.Maybe                  (maybeToList)-import           Data.Monoid                 ((<>))-import           Data.Proxy                  (Proxy (..))-import           Data.Sequence               (fromList)-import           Data.String                 (fromString)-import qualified Data.Text                   as T+import qualified Data.List as List+import           Data.Foldable+                 (toList)+import           Data.Functor.Alt+                 (Alt (..))+import           Data.Maybe+                 (maybeToList)+import           Data.Proxy+                 (Proxy (..))+import           Data.Sequence+                 (fromList)+import           Data.String+                 (fromString)+import           Data.Time.Clock+                 (UTCTime, getCurrentTime) import           GHC.Generics-import           Network.HTTP.Media          (renderHeader)-import           Network.HTTP.Types          (hContentType, renderQuery,-                                              statusCode)+import           Network.HTTP.Media+                 (renderHeader)+import           Network.HTTP.Types+                 (hContentType, statusIsSuccessful, urlEncode, Status) import           Servant.Client.Core  import qualified Network.HTTP.Client         as Client+import qualified Servant.Types.SourceT       as S  -- | The environment in which a request is run.+--   The 'baseUrl' and 'makeClientRequest' function are used to create a @http-client@ request.+--   Cookies are then added to that request if a 'CookieJar' is set on the environment.+--   Finally the request is executed with the 'manager'.+--   The 'makeClientRequest' function can be used to modify the request to execute and set values which+--   are not specified on a @servant@ 'Request' like 'responseTimeout' or 'redirectCount' data ClientEnv   = ClientEnv   { manager :: Client.Manager   , baseUrl :: BaseUrl+  , cookieJar :: Maybe (TVar Client.CookieJar)+  , makeClientRequest :: BaseUrl -> Request -> IO Client.Request+  -- ^ this function can be used to customize the creation of @http-client@ requests from @servant@ requests. Default value: 'defaultMakeClientRequest'+  --   Note that:+  --      1. 'makeClientRequest' exists to allow overriding operational semantics e.g. 'responseTimeout' per request,+  --          If you need global modifications, you should use 'managerModifyRequest'+  --      2. the 'cookieJar', if defined, is being applied after 'makeClientRequest' is called.+  , middleware :: ClientMiddleware    } +type ClientApplication = Request -> ClientM Response++type ClientMiddleware = ClientApplication -> ClientApplication++-- | 'ClientEnv' smart constructor.+mkClientEnv :: Client.Manager -> BaseUrl -> ClientEnv+mkClientEnv manager baseUrl = ClientEnv +  { manager+  , baseUrl+  , cookieJar = Nothing+  , makeClientRequest = defaultMakeClientRequest+  , middleware = id+  }+ -- | Generates a set of client functions for an API. -- -- Example:@@ -65,21 +108,43 @@ client :: HasClient ClientM api => Proxy api -> Client ClientM api client api = api `clientIn` (Proxy :: Proxy ClientM) +-- | Change the monad the client functions live in, by+--   supplying a conversion function+--   (a natural transformation to be precise).+--+--   For example, assuming you have some @manager :: 'Manager'@ and+--   @baseurl :: 'BaseUrl'@ around:+--+--   > type API = Get '[JSON] Int :<|> Capture "n" Int :> Post '[JSON] Int+--   > api :: Proxy API+--   > api = Proxy+--   > getInt :: IO Int+--   > postInt :: Int -> IO Int+--   > getInt :<|> postInt = hoistClient api (flip runClientM cenv) (client api)+--   >   where cenv = mkClientEnv manager baseurl+hoistClient+  :: HasClient ClientM api+  => Proxy api+  -> (forall a. m a -> n a)+  -> Client m api+  -> Client n api+hoistClient = hoistClientMonad (Proxy :: Proxy ClientM)+ -- | @ClientM@ is the monad in which client functions run. Contains the -- 'Client.Manager' and 'BaseUrl' used for requests in the reader environment. newtype ClientM a = ClientM-  { runClientM' :: ReaderT ClientEnv (ExceptT ServantError IO) a }-  deriving ( Functor, Applicative, Monad, MonadIO, Generic-           , MonadReader ClientEnv, MonadError ServantError, MonadThrow-           , MonadCatch)+  { unClientM :: ReaderT ClientEnv (ExceptT ClientError IO) a }+  deriving newtype ( Functor, Applicative, Monad, MonadIO, Generic+           , MonadReader ClientEnv, MonadError ClientError, MonadThrow+           , MonadCatch, MonadMask)  instance MonadBase IO ClientM where   liftBase = ClientM . liftBase  instance MonadBaseControl IO ClientM where-  type StM ClientM a = Either ServantError a+  type StM ClientM a = Either ClientError a -  liftBaseWith f = ClientM (liftBaseWith (\g -> f (g . runClientM')))+  liftBaseWith f = ClientM (liftBaseWith (\g -> f (g . unClientM)))    restoreM st = ClientM (restoreM st) @@ -88,60 +153,110 @@   a <!> b = a `catchError` \_ -> b  instance RunClient ClientM where-  runRequest = performRequest-  throwServantError = throwError-  catchServantError = catchError+  runRequestAcceptStatus statuses req = do+    ClientEnv {middleware} <- ask+    let oldApp = performRequest statuses+    middleware oldApp req+  throwClientError = throwError -instance ClientLike (ClientM a) (ClientM a) where-  mkClient = id+runClientM :: ClientM a -> ClientEnv -> IO (Either ClientError a)+runClientM cm env = runExceptT $ flip runReaderT env $ unClientM cm -runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)-runClientM cm env = runExceptT $ (flip runReaderT env) $ runClientM' cm+performRequest :: Maybe [Status] -> Request -> ClientM Response+performRequest acceptStatus req = do+  ClientEnv m burl cookieJar' createClientRequest _ <- ask+  clientRequest <- liftIO $ createClientRequest burl req+  request <- case cookieJar' of+    Nothing -> pure clientRequest+    Just cj -> liftIO $ do+      now <- getCurrentTime+      atomically $ do+        oldCookieJar <- readTVar cj+        let (newRequest, newCookieJar) =+              Client.insertCookiesIntoRequest+                clientRequest+                oldCookieJar+                now+        writeTVar cj newCookieJar+        pure newRequest +  response <- maybe (requestWithoutCookieJar m request) (requestWithCookieJar m request) cookieJar'+  let status = Client.responseStatus response+      ourResponse = clientResponseToResponse id response+      goodStatus = case acceptStatus of+        Nothing -> statusIsSuccessful status+        Just good -> status `elem` good+  unless goodStatus $ do+    throwError $ mkFailureResponse burl req ourResponse+  return ourResponse+  where+    requestWithoutCookieJar :: Client.Manager -> Client.Request -> ClientM (Client.Response BSL.ByteString)+    requestWithoutCookieJar m' request' = do+        eResponse <- liftIO . catchConnectionError $ Client.httpLbs request' m'+        either throwError return eResponse -performRequest :: Request -> ClientM Response-performRequest req = do-  m <- asks manager-  burl <- asks baseUrl-  let request = requestToClientRequest burl req+    requestWithCookieJar :: Client.Manager -> Client.Request -> TVar Client.CookieJar -> ClientM (Client.Response BSL.ByteString)+    requestWithCookieJar m' request' cj = do+        eResponse <- liftIO . catchConnectionError . Client.withResponseHistory request' m' $ updateWithResponseCookies cj+        either throwError return eResponse -  eResponse <- liftIO $ catchConnectionError $ Client.httpLbs request m-  case eResponse of-    Left err -> throwError $ err-    Right response -> do-      let status = Client.responseStatus response-          status_code = statusCode status-          ourResponse = clientResponseToReponse response-      unless (status_code >= 200 && status_code < 300) $-        throwError $ FailureResponse ourResponse-      return ourResponse+    updateWithResponseCookies :: TVar Client.CookieJar -> Client.HistoriedResponse Client.BodyReader -> IO (Client.Response BSL.ByteString)+    updateWithResponseCookies cj responses = do+        now <- getCurrentTime+        bss <- Client.brConsume $ Client.responseBody fRes+        let fRes'        = fRes { Client.responseBody = BSL.fromChunks bss }+            allResponses = Client.hrRedirects responses <> [(fReq, fRes')]+        atomically $ mapM_ (updateCookieJar now) allResponses+        return fRes'+      where+          updateCookieJar :: UTCTime -> (Client.Request, Client.Response BSL.ByteString) -> STM ()+          updateCookieJar now' (req', res') = modifyTVar' cj (fst . Client.updateCookieJar res' req' now') -clientResponseToReponse :: Client.Response BSL.ByteString -> Response-clientResponseToReponse r = Response-  { responseStatusCode = Client.responseStatus r-  , responseBody = Client.responseBody r-  , responseHeaders = fromList $ Client.responseHeaders r-  , responseHttpVersion = Client.responseVersion r-  }+          fReq = Client.hrFinalRequest responses+          fRes = Client.hrFinalResponse responses -requestToClientRequest :: BaseUrl -> Request -> Client.Request-requestToClientRequest burl r = Client.defaultRequest-  { Client.method = requestMethod r-  , Client.host = fromString $ baseUrlHost burl-  , Client.port = baseUrlPort burl-  , Client.path = BSL.toStrict-                $ fromString (baseUrlPath burl)-               <> toLazyByteString (requestPath r)-  , Client.queryString = renderQuery True . toList $ requestQueryString r-  , Client.requestHeaders =-    maybeToList acceptHdr ++ maybeToList contentTypeHdr ++ headers-  , Client.requestBody = body-  , Client.secure = isSecure-  }+mkFailureResponse :: BaseUrl -> Request -> ResponseF BSL.ByteString -> ClientError+mkFailureResponse burl request =+    FailureResponse (bimap (const ()) f request)   where+    f b = (burl, BSL.toStrict $ toLazyByteString b)++clientResponseToResponse :: (a -> b) -> Client.Response a -> ResponseF b+clientResponseToResponse f r = Response+    { responseStatusCode  = Client.responseStatus r+    , responseBody        = f (Client.responseBody r)+    , responseHeaders     = fromList $ Client.responseHeaders r+    , responseHttpVersion = Client.responseVersion r+    }++-- | Create a @http-client@ 'Client.Request' from a @servant@ 'Request'+--    The 'Client.host', 'Client.path' and 'Client.port' fields are extracted from the 'BaseUrl'+--    otherwise the body, headers and query string are derived from the @servant@ 'Request'+--+-- Note that @Applicative@ dependency is not really needed for this function+-- implementation. But in the past the return type was wrapped into @IO@+-- without a necessity breaking the API backward-compatibility. In order to not+-- break the API again it was changed to @Applicative@ so that you can just use+-- something like @Data.Functor.Identity@ without a need to involve @IO@ but+-- still keeping it compatible with the code written when it was typed as @IO@.+defaultMakeClientRequest :: Applicative f => BaseUrl -> Request -> f Client.Request+defaultMakeClientRequest burl r = pure Client.defaultRequest+    { Client.method = requestMethod r+    , Client.host = fromString $ baseUrlHost burl+    , Client.port = baseUrlPort burl+    , Client.path = BSL.toStrict+                  $ fromString (baseUrlPath burl)+                 <> toLazyByteString (requestPath r)+    , Client.queryString = buildQueryString . toList $ requestQueryString r+    , Client.requestHeaders =+      maybeToList acceptHdr ++ maybeToList contentTypeHdr ++ headers+    , Client.requestBody = body+    , Client.secure = isSecure+    }+  where     -- Content-Type and Accept are specified by requestBody and requestAccept     headers = filter (\(h, _) -> h /= "Accept" && h /= "Content-Type") $-        toList $requestHeaders r+        toList $ requestHeaders r      acceptHdr         | null hs   = Nothing@@ -149,16 +264,49 @@       where         hs = toList $ requestAccept r +    convertBody bd = case bd of+        RequestBodyLBS body'       -> Client.RequestBodyLBS body'+        RequestBodyBS body'        -> Client.RequestBodyBS body'+        RequestBodySource sourceIO -> Client.RequestBodyStreamChunked givesPopper+          where+            givesPopper :: (IO BS.ByteString -> IO ()) -> IO ()+            givesPopper needsPopper = S.unSourceT sourceIO $ \step0 -> do+                ref <- newMVar step0++                -- Note sure we need locking, but it's feels safer.+                let popper :: IO BS.ByteString+                    popper = modifyMVar ref nextBs++                needsPopper popper++            nextBs S.Stop          = return (S.Stop, BS.empty)+            nextBs (S.Error err)   = fail err+            nextBs (S.Skip s)      = nextBs s+            nextBs (S.Effect ms)   = ms >>= nextBs+            nextBs (S.Yield lbs s) = case BSL.toChunks lbs of+                []     -> nextBs s+                (x:xs) | BS.null x -> nextBs step'+                       | otherwise -> return (step', x)+                    where+                      step' = S.Yield (BSL.fromChunks xs) s+     (body, contentTypeHdr) = case requestBody r of-      Nothing -> (Client.RequestBodyLBS "", Nothing)-      Just (RequestBodyLBS body', typ)-        -> (Client.RequestBodyLBS body', Just (hContentType, renderHeader typ))+        Nothing           -> (Client.RequestBodyBS "", Nothing)+        Just (body', typ) -> (convertBody body', Just (hContentType, renderHeader typ))      isSecure = case baseUrlScheme burl of-      Http -> False-      Https -> True+        Http -> False+        Https -> True -catchConnectionError :: IO a -> IO (Either ServantError a)+    -- Query string builder which does not do any encoding+    buildQueryString [] = mempty+    buildQueryString qps = "?" <> List.foldl' addQueryParam mempty qps++    addQueryParam qs (k, v) =+          qs <> (if BS.null qs then mempty else "&") <> urlEncode True k <> foldMap ("=" <>) v+++catchConnectionError :: IO a -> IO (Either ClientError a) catchConnectionError action =   catch (Right <$> action) $ \e ->-    pure . Left . ConnectionError . T.pack $ show (e :: Client.HttpException)+    pure . Left . ConnectionError $ SomeException (e :: Client.HttpException)
+ src/Servant/Client/Internal/HttpClient/Streaming.hs view
@@ -0,0 +1,204 @@+{-# LANGUAGE CPP                        #-}+{-# LANGUAGE FlexibleContexts           #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE RankNTypes                 #-}+{-# LANGUAGE ScopedTypeVariables        #-}+{-# LANGUAGE TypeFamilies               #-}+module Servant.Client.Internal.HttpClient.Streaming (+    module Servant.Client.Internal.HttpClient.Streaming,+    ClientEnv (..),+    mkClientEnv,+    clientResponseToResponse,+    defaultMakeClientRequest,+    catchConnectionError,+    ) where++import           Prelude ()+import           Prelude.Compat++import           Control.Concurrent.STM.TVar+import           Control.DeepSeq+                 (NFData, force)+import           Control.Exception+                 (evaluate, throwIO)+import           Control.Monad+                 (unless)+import           Control.Monad.Base+                 (MonadBase (..))+import           Control.Monad.Codensity+                 (Codensity (..))+import           Control.Monad.Error.Class+                 (MonadError (..))+import           Control.Monad.IO.Class (MonadIO(..))+import           Control.Monad.Reader (MonadReader(..), ReaderT(..))+import           Control.Monad.STM+                 (atomically)+import           Control.Monad.Trans.Except+import qualified Data.ByteString                    as BS+import qualified Data.ByteString.Lazy               as BSL+import           Data.Foldable+                 (for_)+import           Data.Functor.Alt+                 (Alt (..))+import           Data.Proxy+                 (Proxy (..))+import           Data.Time.Clock+                 (getCurrentTime)+import           GHC.Generics+import           Network.HTTP.Types+                 (Status, statusIsSuccessful)++import qualified Network.HTTP.Client                as Client++import           Servant.Client.Core+import           Servant.Client.Internal.HttpClient+                 (ClientEnv (..), catchConnectionError,+                 clientResponseToResponse, mkClientEnv, mkFailureResponse,+                 defaultMakeClientRequest)+import qualified Servant.Types.SourceT              as S+import Control.Monad.Trans.Class (MonadTrans(..))+++-- | Generates a set of client functions for an API.+--+-- Example:+--+-- > type API = Capture "no" Int :> Get '[JSON] Int+-- >        :<|> Get '[JSON] [Bool]+-- >+-- > api :: Proxy API+-- > api = Proxy+-- >+-- > getInt :: Int -> ClientM Int+-- > getBools :: ClientM [Bool]+-- > getInt :<|> getBools = client api+client :: HasClient ClientM api => Proxy api -> Client ClientM api+client api = api `clientIn` (Proxy :: Proxy ClientM)++-- | Change the monad the client functions live in, by+--   supplying a conversion function+--   (a natural transformation to be precise).+--+--   For example, assuming you have some @manager :: 'Manager'@ and+--   @baseurl :: 'BaseUrl'@ around:+--+--   > type API = Get '[JSON] Int :<|> Capture "n" Int :> Post '[JSON] Int+--   > api :: Proxy API+--   > api = Proxy+--   > getInt :: IO Int+--   > postInt :: Int -> IO Int+--   > getInt :<|> postInt = hoistClient api (flip runClientM cenv) (client api)+--   >   where cenv = mkClientEnv manager baseurl+hoistClient+  :: HasClient ClientM api+  => Proxy api+  -> (forall a. m a -> n a)+  -> Client m api+  -> Client n api+hoistClient = hoistClientMonad (Proxy :: Proxy ClientM)++-- | @ClientM@ is the monad in which client functions run. Contains the+-- 'Client.Manager' and 'BaseUrl' used for requests in the reader environment.+newtype ClientM a = ClientM+  { unClientM :: ReaderT ClientEnv (ExceptT ClientError (Codensity IO)) a }+  deriving newtype ( Functor, Applicative, Monad, MonadIO, Generic+           , MonadReader ClientEnv, MonadError ClientError)++instance MonadBase IO ClientM where+  liftBase = ClientM . liftIO++-- | Try clients in order, last error is preserved.+instance Alt ClientM where+  a <!> b = a `catchError` \_ -> b++instance RunClient ClientM where+  runRequestAcceptStatus = performRequest+  throwClientError = throwError++instance RunStreamingClient ClientM where+  withStreamingRequest = performWithStreamingRequest++withClientM :: ClientM a -> ClientEnv -> (Either ClientError a -> IO b) -> IO b+withClientM cm env k =+    let Codensity f = runExceptT $ flip runReaderT env $ unClientM cm+    in f k++-- | A 'runClientM' variant for streaming client.+--+-- It allows using this module's 'ClientM' in a direct style.+-- The 'NFData' constraint however prevents using this function with genuine+-- streaming response types ('SourceT', 'Conduit', pipes 'Proxy' or 'Machine').+-- For those you have to use 'withClientM'.+--+-- /Note:/ we 'force' the result, so the likelihood of accidentally leaking a+-- connection is smaller. Use with care.+--+runClientM :: NFData a => ClientM a -> ClientEnv -> IO (Either ClientError a)+runClientM cm env = withClientM cm env (evaluate . force)++performRequest :: Maybe [Status] -> Request -> ClientM Response+performRequest acceptStatus req = do+    -- TODO: should use Client.withResponse here too+  ClientEnv m burl cookieJar' createClientRequest _ <- ask+  clientRequest <- liftIO $ createClientRequest burl req+  request <- case cookieJar' of+    Nothing -> pure clientRequest+    Just cj -> liftIO $ do+      now <- getCurrentTime+      atomically $ do+        oldCookieJar <- readTVar cj+        let (newRequest, newCookieJar) =+              Client.insertCookiesIntoRequest+                clientRequest+                oldCookieJar+                now+        writeTVar cj newCookieJar+        pure newRequest++  eResponse <- liftIO $ catchConnectionError $ Client.httpLbs request m+  case eResponse of+    Left err -> throwError err+    Right response -> do+      for_ cookieJar' $ \cj -> liftIO $ do+        now' <- getCurrentTime+        atomically $ modifyTVar' cj (fst . Client.updateCookieJar response request now')+      let status = Client.responseStatus response+          ourResponse = clientResponseToResponse id response+          goodStatus = case acceptStatus of+            Nothing -> statusIsSuccessful status+            Just good -> status `elem` good+      unless goodStatus $ do+        throwError $ mkFailureResponse burl req ourResponse+      return ourResponse++-- | TODO: support UVerb ('acceptStatus' argument, like in 'performRequest' above).+performWithStreamingRequest :: Request -> (StreamingResponse -> IO a) -> ClientM a+performWithStreamingRequest req k = do+  ClientEnv m burl cookieJar' createClientRequest _ <- ask+  clientRequest <- liftIO $ createClientRequest burl req+  request <- case cookieJar' of+    Nothing -> pure clientRequest+    Just cj -> liftIO $ do+      now <- getCurrentTime+      atomically $ do+        oldCookieJar <- readTVar cj+        let (newRequest, newCookieJar) =+              Client.insertCookiesIntoRequest+                clientRequest+                oldCookieJar+                now+        writeTVar cj newCookieJar+        pure newRequest+  ClientM $ lift $ lift $ Codensity $ \k1 ->+      Client.withResponse request m $ \res -> do+          let status = Client.responseStatus res++          -- we throw FailureResponse in IO :(+          unless (statusIsSuccessful status) $ do+              b <- BSL.fromChunks <$> Client.brConsume (Client.responseBody res)+              throwIO $ mkFailureResponse burl req (clientResponseToResponse (const b) res)++          x <- k (clientResponseToResponse (S.fromAction BS.null) res)+          k1 x
+ src/Servant/Client/Streaming.hs view
@@ -0,0 +1,19 @@+-- | This module provides 'client' which can automatically generate+-- querying functions for each endpoint just from the type representing your+-- API.+--+-- This client supports streaming operations.+module Servant.Client.Streaming+    ( client+    , ClientM+    , withClientM+    , runClientM+    , ClientEnv(..)+    , mkClientEnv+    , defaultMakeClientRequest+    , hoistClient+    , module Servant.Client.Core.Reexport+    ) where++import           Servant.Client.Core.Reexport+import           Servant.Client.Internal.HttpClient.Streaming
+ test/Servant/BasicAuthSpec.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.BasicAuthSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Control.Arrow+                 (left)+import           Data.Monoid ()+import qualified Network.HTTP.Types                   as HTTP+import           Test.Hspec++import           Servant.API+                 (BasicAuthData (..))+import           Servant.Client+import           Servant.ClientTestUtils++spec :: Spec+spec = describe "Servant.BasicAuthSpec" $ do+    basicAuthSpec++basicAuthSpec :: Spec+basicAuthSpec = beforeAll (startWaiApp basicAuthServer) $ afterAll endWaiApp $ do+  context "Authentication works when requests are properly authenticated" $ do++    it "Authenticates a BasicAuth protected server appropriately" $ \(_,baseUrl) -> do+      let getBasic = client basicAuthAPI+      let basicAuthData = BasicAuthData "servant" "server"+      left show <$> runClient (getBasic basicAuthData) baseUrl `shouldReturn` Right alice++  context "Authentication is rejected when requests are not authenticated properly" $ do++    it "Authenticates a BasicAuth protected server appropriately" $ \(_,baseUrl) -> do+      let getBasic = client basicAuthAPI+      let basicAuthData = BasicAuthData "not" "password"+      Left (FailureResponse _ r) <- runClient (getBasic basicAuthData) baseUrl+      responseStatusCode r `shouldBe` HTTP.Status 403 "Forbidden"
+ test/Servant/BrokenSpec.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE DataKinds                   #-}+{-# LANGUAGE TypeOperators               #-}+{-# OPTIONS_GHC -freduction-depth=100    #-}+{-# OPTIONS_GHC -fno-warn-orphans        #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.BrokenSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Data.Monoid ()+import           Data.Proxy+import qualified Network.HTTP.Types as HTTP+import           Test.Hspec++import           Servant.API+                 ((:<|>) ((:<|>)), (:>), JSON, Verb, Get, StdMethod (GET))+import           Servant.Client+import           Servant.ClientTestUtils+import           Servant.Server++-- * api for testing inconsistencies between client and server++type Get201 = Verb 'GET 201+type Get301 = Verb 'GET 301++type BrokenAPI =+  -- the server should respond with 200, but returns 201+       "get200" :> Get201 '[JSON] ()+  -- the server should respond with 307, but returns 301+  :<|> "get307" :> Get301 '[JSON] ()++brokenApi :: Proxy BrokenAPI+brokenApi = Proxy++brokenServer :: Application+brokenServer = serve brokenApi (pure () :<|> pure ())++type PublicAPI =+  -- the client expects 200+       "get200" :> Get '[JSON] ()+  -- the client expects 307+  :<|> "get307" :> Get307 '[JSON] ()++publicApi :: Proxy PublicAPI+publicApi = Proxy++get200Client :: ClientM ()+get307Client :: ClientM ()+get200Client :<|> get307Client = client publicApi+++spec :: Spec+spec = describe "Servant.BrokenSpec" $ do+    brokenSpec++brokenSpec :: Spec+brokenSpec = beforeAll (startWaiApp brokenServer) $ afterAll endWaiApp $ do+    context "client returns errors for inconsistencies between client and server api" $ do+      it "reports FailureResponse with wrong 2xx status code" $ \(_, baseUrl) -> do+        res <- runClient get200Client baseUrl+        case res of+          Left (FailureResponse _ r) | responseStatusCode r == HTTP.status201 -> return ()+          _ -> fail $ "expected 201 broken response, but got " <> show res++      it "reports FailureResponse with wrong 3xx status code" $ \(_, baseUrl) -> do+        res <- runClient get307Client baseUrl+        case res of+          Left (FailureResponse _ r) | responseStatusCode r == HTTP.status301 -> return ()+          _ -> fail $ "expected 301 broken response, but got " <> show res
− test/Servant/ClientSpec.hs
@@ -1,519 +0,0 @@-{-# LANGUAGE CPP                    #-}-{-# LANGUAGE ConstraintKinds        #-}-{-# LANGUAGE DataKinds              #-}-{-# LANGUAGE DeriveGeneric          #-}-{-# LANGUAGE FlexibleContexts       #-}-{-# LANGUAGE FlexibleInstances      #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE GADTs                  #-}-{-# LANGUAGE MultiParamTypeClasses  #-}-{-# LANGUAGE OverloadedStrings      #-}-{-# LANGUAGE PolyKinds              #-}-{-# LANGUAGE RecordWildCards        #-}-{-# LANGUAGE ScopedTypeVariables    #-}-{-# LANGUAGE StandaloneDeriving     #-}-{-# LANGUAGE TypeFamilies           #-}-{-# LANGUAGE TypeOperators          #-}-{-# LANGUAGE UndecidableInstances   #-}-#if __GLASGOW_HASKELL__ >= 800-{-# OPTIONS_GHC -freduction-depth=100 #-}-#else-{-# OPTIONS_GHC -fcontext-stack=100 #-}-#endif-{-# OPTIONS_GHC -fno-warn-orphans #-}-{-# OPTIONS_GHC -fno-warn-name-shadowing #-}--#include "overlapping-compat.h"-module Servant.ClientSpec (spec) where--import           Prelude                                    ()-import           Prelude.Compat--import           Control.Arrow                              (left)-import           Control.Concurrent                         (ThreadId, forkIO,-                                                             killThread)-import           Control.Exception                          (bracket)-import           Control.Monad.Error.Class                  (throwError)-import           Data.Aeson-import           Data.Char                                  (chr, isPrint)-import           Data.Foldable                              (forM_)-import           Data.Monoid                                hiding (getLast)-import           Data.Proxy-import qualified Generics.SOP                               as SOP-import           GHC.Generics                               (Generic)-import qualified Network.HTTP.Client                        as C-import qualified Network.HTTP.Types                         as HTTP-import           Network.Socket-import qualified Network.Wai                                as Wai-import           Network.Wai.Handler.Warp-import           System.IO.Unsafe                           (unsafePerformIO)-import           Test.Hspec-import           Test.Hspec.QuickCheck-import           Test.HUnit-import           Test.QuickCheck-import           Web.FormUrlEncoded                         (FromForm, ToForm)--import           Servant.API                                ((:<|>) ((:<|>)),-                                                             (:>), AuthProtect,-                                                             BasicAuth,-                                                             BasicAuthData (..),-                                                             Capture,-                                                             CaptureAll, Delete,-                                                             DeleteNoContent,-                                                             EmptyAPI, addHeader,-                                                             FormUrlEncoded,-                                                             Get, Header,-                                                             Headers, JSON,-                                                             NoContent (NoContent),-                                                             Post, Put, Raw,-                                                             QueryFlag,-                                                             QueryParam,-                                                             QueryParams,-                                                             ReqBody,-                                                             getHeaders)-import           Servant.API.Internal.Test.ComprehensiveAPI-import           Servant.Client-import qualified Servant.Client.Core.Internal.Request as Req-import qualified Servant.Client.Core.Internal.Auth as Auth-import           Servant.Server-import           Servant.Server.Experimental.Auth---- This declaration simply checks that all instances are in place.-_ = client comprehensiveAPI--spec :: Spec-spec = describe "Servant.Client" $ do-    sucessSpec-    failSpec-    wrappedApiSpec-    basicAuthSpec-    genAuthSpec-    genericClientSpec---- * test data types--data Person = Person-  { _name :: String-  , _age  :: Integer-  } deriving (Eq, Show, Generic)--instance ToJSON Person-instance FromJSON Person--instance ToForm Person-instance FromForm Person--alice :: Person-alice = Person "Alice" 42--carol :: Person-carol = Person "Carol" 17--type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]--type Api =-  Get '[JSON] Person-  :<|> "get" :> Get '[JSON] Person-  :<|> "deleteEmpty" :> DeleteNoContent '[JSON] NoContent-  :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person-  :<|> "captureAll" :> CaptureAll "names" String :> Get '[JSON] [Person]-  :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person-  :<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person-  :<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]-  :<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool-  :<|> "rawSuccess" :> Raw-  :<|> "rawFailure" :> Raw-  :<|> "multiple" :>-            Capture "first" String :>-            QueryParam "second" Int :>-            QueryFlag "third" :>-            ReqBody '[JSON] [(String, [Rational])] :>-            Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])-  :<|> "headers" :> Get '[JSON] (Headers TestHeaders Bool)-  :<|> "deleteContentType" :> DeleteNoContent '[JSON] NoContent-  :<|> "empty" :> EmptyAPI--api :: Proxy Api-api = Proxy--getRoot         :: ClientM Person-getGet          :: ClientM Person-getDeleteEmpty  :: ClientM NoContent-getCapture      :: String -> ClientM Person-getCaptureAll   :: [String] -> ClientM [Person]-getBody         :: Person -> ClientM Person-getQueryParam   :: Maybe String -> ClientM Person-getQueryParams  :: [String] -> ClientM [Person]-getQueryFlag    :: Bool -> ClientM Bool-getRawSuccess   :: HTTP.Method -> ClientM Response-getRawFailure   :: HTTP.Method -> ClientM Response-getMultiple     :: String -> Maybe Int -> Bool -> [(String, [Rational])]-  -> ClientM (String, Maybe Int, Bool, [(String, [Rational])])-getRespHeaders  :: ClientM (Headers TestHeaders Bool)-getDeleteContentType :: ClientM NoContent--getRoot-  :<|> getGet-  :<|> getDeleteEmpty-  :<|> getCapture-  :<|> getCaptureAll-  :<|> getBody-  :<|> getQueryParam-  :<|> getQueryParams-  :<|> getQueryFlag-  :<|> getRawSuccess-  :<|> getRawFailure-  :<|> getMultiple-  :<|> getRespHeaders-  :<|> getDeleteContentType-  :<|> EmptyClient = client api--server :: Application-server = serve api (-       return carol-  :<|> return alice-  :<|> return NoContent-  :<|> (\ name -> return $ Person name 0)-  :<|> (\ names -> return (zipWith Person names [0..]))-  :<|> return-  :<|> (\ name -> case name of-                   Just "alice" -> return alice-                   Just n -> throwError $ ServantErr 400 (n ++ " not found") "" []-                   Nothing -> throwError $ ServantErr 400 "missing parameter" "" [])-  :<|> (\ names -> return (zipWith Person names [0..]))-  :<|> return-  :<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "rawSuccess")-  :<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.badRequest400 [] "rawFailure")-  :<|> (\ a b c d -> return (a, b, c, d))-  :<|> (return $ addHeader 1729 $ addHeader "eg2" True)-  :<|> return NoContent-  :<|> emptyServer)---type FailApi =-       "get" :> Raw-  :<|> "capture" :> Capture "name" String :> Raw-  :<|> "body" :> Raw-failApi :: Proxy FailApi-failApi = Proxy--failServer :: Application-failServer = serve failApi (-       (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "")-  :<|> (\ _capture -> Tagged $ \_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "application/json")] "")-  :<|> (Tagged $ \_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "fooooo")] "")- )---- * basic auth stuff--type BasicAuthAPI =-       BasicAuth "foo-realm" () :> "private" :> "basic" :> Get '[JSON] Person--basicAuthAPI :: Proxy BasicAuthAPI-basicAuthAPI = Proxy--basicAuthHandler :: BasicAuthCheck ()-basicAuthHandler =-  let check (BasicAuthData username password) =-        if username == "servant" && password == "server"-        then return (Authorized ())-        else return Unauthorized-  in BasicAuthCheck check--basicServerContext :: Context '[ BasicAuthCheck () ]-basicServerContext = basicAuthHandler :. EmptyContext--basicAuthServer :: Application-basicAuthServer = serveWithContext basicAuthAPI basicServerContext (const (return alice))---- * general auth stuff--type GenAuthAPI =-  AuthProtect "auth-tag" :> "private" :> "auth" :> Get '[JSON] Person--genAuthAPI :: Proxy GenAuthAPI-genAuthAPI = Proxy--type instance AuthServerData (AuthProtect "auth-tag") = ()-type instance Auth.AuthClientData (AuthProtect "auth-tag") = ()--genAuthHandler :: AuthHandler Wai.Request ()-genAuthHandler =-  let handler req = case lookup "AuthHeader" (Wai.requestHeaders req) of-        Nothing -> throwError (err401 { errBody = "Missing auth header" })-        Just _ -> return ()-  in mkAuthHandler handler--genAuthServerContext :: Context '[ AuthHandler Wai.Request () ]-genAuthServerContext = genAuthHandler :. EmptyContext--genAuthServer :: Application-genAuthServer = serveWithContext genAuthAPI genAuthServerContext (const (return alice))---- * generic client stuff--type GenericClientAPI-    = QueryParam "sqr" Int :> Get '[JSON] Int- :<|> Capture "foo" String :> NestedAPI1--data GenericClient = GenericClient-  { getSqr          :: Maybe Int -> ClientM Int-  , mkNestedClient1 :: String -> NestedClient1-  } deriving Generic-instance SOP.Generic GenericClient-instance (Client ClientM GenericClientAPI ~ client) => ClientLike client GenericClient--type NestedAPI1-    = QueryParam "int" Int :> NestedAPI2- :<|> QueryParam "id" Char :> Get '[JSON] Char--data NestedClient1 = NestedClient1-  { mkNestedClient2 :: Maybe Int -> NestedClient2-  , idChar          :: Maybe Char -> ClientM Char-  } deriving Generic-instance SOP.Generic NestedClient1-instance (Client ClientM NestedAPI1 ~ client) => ClientLike client NestedClient1--type NestedAPI2-    = "sum"  :> Capture "first" Int :> Capture "second" Int :> Get '[JSON] Int- :<|> "void" :> Post '[JSON] ()--data NestedClient2 = NestedClient2-  { getSum    :: Int -> Int -> ClientM Int-  , doNothing :: ClientM ()-  } deriving Generic-instance SOP.Generic NestedClient2-instance (Client ClientM NestedAPI2 ~ client) => ClientLike client NestedClient2--genericClientServer :: Application-genericClientServer = serve (Proxy :: Proxy GenericClientAPI) (-       (\ mx -> case mx of-                  Just x -> return (x*x)-                  Nothing -> throwError $ ServantErr 400 "missing parameter" "" []-       )-  :<|> nestedServer1- )-  where-    nestedServer1 _str = nestedServer2 :<|> (maybe (throwError $ ServantErr 400 "missing parameter" "" []) return)-    nestedServer2 _int = (\ x y -> return (x + y)) :<|> return ()--{-# NOINLINE manager' #-}-manager' :: C.Manager-manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings--runClient :: ClientM a -> BaseUrl -> IO (Either ServantError a)-runClient x baseUrl' = runClientM x (ClientEnv manager' baseUrl')--sucessSpec :: Spec-sucessSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do-    it "Servant.API.Get root" $ \(_, baseUrl) -> do-      left show <$> runClient getRoot baseUrl  `shouldReturn` Right carol--    it "Servant.API.Get" $ \(_, baseUrl) -> do-      left show <$> runClient getGet baseUrl  `shouldReturn` Right alice--    describe "Servant.API.Delete" $ do-      it "allows empty content type" $ \(_, baseUrl) -> do-        left show <$> runClient getDeleteEmpty baseUrl `shouldReturn` Right NoContent--      it "allows content type" $ \(_, baseUrl) -> do-        left show <$> runClient getDeleteContentType baseUrl `shouldReturn` Right NoContent--    it "Servant.API.Capture" $ \(_, baseUrl) -> do-      left show <$> runClient (getCapture "Paula") baseUrl `shouldReturn` Right (Person "Paula" 0)--    it "Servant.API.CaptureAll" $ \(_, baseUrl) -> do-      let expected = [(Person "Paula" 0), (Person "Peta" 1)]-      left show <$> runClient (getCaptureAll ["Paula", "Peta"]) baseUrl `shouldReturn` Right expected--    it "Servant.API.ReqBody" $ \(_, baseUrl) -> do-      let p = Person "Clara" 42-      left show <$> runClient (getBody p) baseUrl `shouldReturn` Right p--    it "Servant.API.QueryParam" $ \(_, baseUrl) -> do-      left show <$> runClient (getQueryParam (Just "alice")) baseUrl `shouldReturn` Right alice-      Left (FailureResponse r) <- runClient (getQueryParam (Just "bob")) baseUrl-      responseStatusCode r `shouldBe` HTTP.Status 400 "bob not found"--    it "Servant.API.QueryParam.QueryParams" $ \(_, baseUrl) -> do-      left show <$> runClient (getQueryParams []) baseUrl `shouldReturn` Right []-      left show <$> runClient (getQueryParams ["alice", "bob"]) baseUrl-        `shouldReturn` Right [Person "alice" 0, Person "bob" 1]--    context "Servant.API.QueryParam.QueryFlag" $-      forM_ [False, True] $ \ flag -> it (show flag) $ \(_, baseUrl) -> do-        left show <$> runClient (getQueryFlag flag) baseUrl `shouldReturn` Right flag--    it "Servant.API.Raw on success" $ \(_, baseUrl) -> do-      res <- runClient (getRawSuccess HTTP.methodGet) baseUrl-      case res of-        Left e -> assertFailure $ show e-        Right r -> do-          responseStatusCode r `shouldBe` HTTP.status200-          responseBody r `shouldBe` "rawSuccess"--    it "Servant.API.Raw should return a Left in case of failure" $ \(_, baseUrl) -> do-      res <- runClient (getRawFailure HTTP.methodGet) baseUrl-      case res of-        Right _ -> assertFailure "expected Left, but got Right"-        Left (FailureResponse r) -> do-          responseStatusCode r `shouldBe` HTTP.status400-          responseBody r `shouldBe` "rawFailure"-        Left e -> assertFailure $ "expected FailureResponse, but got " ++ show e--    it "Returns headers appropriately" $ \(_, baseUrl) -> do-      res <- runClient getRespHeaders baseUrl-      case res of-        Left e -> assertFailure $ show e-        Right val -> getHeaders val `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")]--    modifyMaxSuccess (const 20) $ do-      it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $ \(_, baseUrl) ->-        property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body ->-          ioProperty $ do-            result <- left show <$> runClient (getMultiple cap num flag body) baseUrl-            return $-              result === Right (cap, num, flag, body)---wrappedApiSpec :: Spec-wrappedApiSpec = describe "error status codes" $ do-  let serveW api = serve api $ throwError $ ServantErr 500 "error message" "" []-  context "are correctly handled by the client" $-    let test :: (WrappedApi, String) -> Spec-        test (WrappedApi api, desc) =-          it desc $ bracket (startWaiApp $ serveW api) endWaiApp $ \(_, baseUrl) -> do-            let getResponse :: ClientM ()-                getResponse = client api-            Left (FailureResponse r) <- runClient getResponse baseUrl-            responseStatusCode r `shouldBe` (HTTP.Status 500 "error message")-    in mapM_ test $-        (WrappedApi (Proxy :: Proxy (Delete '[JSON] ())), "Delete") :-        (WrappedApi (Proxy :: Proxy (Get '[JSON] ())), "Get") :-        (WrappedApi (Proxy :: Proxy (Post '[JSON] ())), "Post") :-        (WrappedApi (Proxy :: Proxy (Put '[JSON] ())), "Put") :-        []--failSpec :: Spec-failSpec = beforeAll (startWaiApp failServer) $ afterAll endWaiApp $ do--    context "client returns errors appropriately" $ do-      it "reports FailureResponse" $ \(_, baseUrl) -> do-        let (_ :<|> _ :<|> getDeleteEmpty :<|> _) = client api-        Left res <- runClient getDeleteEmpty baseUrl-        case res of-          FailureResponse r | responseStatusCode r == HTTP.status404 -> return ()-          _ -> fail $ "expected 404 response, but got " <> show res--      it "reports DecodeFailure" $ \(_, baseUrl) -> do-        let (_ :<|> _ :<|> _ :<|> getCapture :<|> _) = client api-        Left res <- runClient (getCapture "foo") baseUrl-        case res of-          DecodeFailure _ _ -> return ()-          _ -> fail $ "expected DecodeFailure, but got " <> show res--      it "reports ConnectionError" $ \_ -> do-        let (getGetWrongHost :<|> _) = client api-        Left res <- runClient getGetWrongHost (BaseUrl Http "127.0.0.1" 19872 "")-        case res of-          ConnectionError _ -> return ()-          _ -> fail $ "expected ConnectionError, but got " <> show res--      it "reports UnsupportedContentType" $ \(_, baseUrl) -> do-        let (_ :<|> getGet :<|> _ ) = client api-        Left res <- runClient getGet baseUrl-        case res of-          UnsupportedContentType ("application/octet-stream") _ -> return ()-          _ -> fail $ "expected UnsupportedContentType, but got " <> show res--      it "reports InvalidContentTypeHeader" $ \(_, baseUrl) -> do-        let (_ :<|> _ :<|> _ :<|> _ :<|> _ :<|> getBody :<|> _) = client api-        Left res <- runClient (getBody alice) baseUrl-        case res of-          InvalidContentTypeHeader _ -> return ()-          _ -> fail $ "expected InvalidContentTypeHeader, but got " <> show res--data WrappedApi where-  WrappedApi :: (HasServer (api :: *) '[], Server api ~ Handler a,-                 HasClient ClientM api, Client ClientM api ~ ClientM ()) =>-    Proxy api -> WrappedApi--basicAuthSpec :: Spec-basicAuthSpec = beforeAll (startWaiApp basicAuthServer) $ afterAll endWaiApp $ do-  context "Authentication works when requests are properly authenticated" $ do--    it "Authenticates a BasicAuth protected server appropriately" $ \(_,baseUrl) -> do-      let getBasic = client basicAuthAPI-      let basicAuthData = BasicAuthData "servant" "server"-      left show <$> runClient (getBasic basicAuthData) baseUrl `shouldReturn` Right alice--  context "Authentication is rejected when requests are not authenticated properly" $ do--    it "Authenticates a BasicAuth protected server appropriately" $ \(_,baseUrl) -> do-      let getBasic = client basicAuthAPI-      let basicAuthData = BasicAuthData "not" "password"-      Left (FailureResponse r) <- runClient (getBasic basicAuthData) baseUrl-      responseStatusCode r `shouldBe` HTTP.Status 403 "Forbidden"--genAuthSpec :: Spec-genAuthSpec = beforeAll (startWaiApp genAuthServer) $ afterAll endWaiApp $ do-  context "Authentication works when requests are properly authenticated" $ do--    it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do-      let getProtected = client genAuthAPI-      let authRequest = Auth.mkAuthenticatedRequest () (\_ req -> Req.addHeader "AuthHeader" ("cool" :: String) req)-      left show <$> runClient (getProtected authRequest) baseUrl `shouldReturn` Right alice--  context "Authentication is rejected when requests are not authenticated properly" $ do--    it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do-      let getProtected = client genAuthAPI-      let authRequest = Auth.mkAuthenticatedRequest () (\_ req -> Req.addHeader "Wrong" ("header" :: String) req)-      Left (FailureResponse r) <- runClient (getProtected authRequest) baseUrl-      responseStatusCode r `shouldBe` (HTTP.Status 401 "Unauthorized")--genericClientSpec :: Spec-genericClientSpec = beforeAll (startWaiApp genericClientServer) $ afterAll endWaiApp $ do-  describe "Servant.Client.Generic" $ do--    let GenericClient{..} = mkClient (client (Proxy :: Proxy GenericClientAPI))-        NestedClient1{..} = mkNestedClient1 "example"-        NestedClient2{..} = mkNestedClient2 (Just 42)--    it "works for top-level client inClientM function" $ \(_, baseUrl) -> do-      left show <$> runClient (getSqr (Just 5)) baseUrl `shouldReturn` Right 25--    it "works for nested clients" $ \(_, baseUrl) -> do-      left show <$> runClient (idChar (Just 'c')) baseUrl `shouldReturn` Right 'c'-      left show <$> runClient (getSum 3 4) baseUrl `shouldReturn` Right 7-      left show <$> runClient doNothing baseUrl `shouldReturn` Right ()---- * utils--startWaiApp :: Application -> IO (ThreadId, BaseUrl)-startWaiApp app = do-    (port, socket) <- openTestSocket-    let settings = setPort port $ defaultSettings-    thread <- forkIO $ runSettingsSocket settings socket app-    return (thread, BaseUrl Http "localhost" port "")---endWaiApp :: (ThreadId, BaseUrl) -> IO ()-endWaiApp (thread, _) = killThread thread--openTestSocket :: IO (Port, Socket)-openTestSocket = do-  s <- socket AF_INET Stream defaultProtocol-  localhost <- inet_addr "127.0.0.1"-  bind s (SockAddrInet aNY_PORT localhost)-  listen s 1-  port <- socketPort s-  return (fromIntegral port, s)--pathGen :: Gen (NonEmptyList Char)-pathGen = fmap NonEmpty path- where-  path = listOf1 $ elements $-    filter (not . (`elem` ("?%[]/#;" :: String))) $-    filter isPrint $-    map chr [0..127]
+ test/Servant/ClientTestUtils.hs view
@@ -0,0 +1,483 @@+{-# LANGUAGE ConstraintKinds       #-}+{-# LANGUAGE DataKinds             #-}+{-# LANGUAGE DeriveGeneric         #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE GADTs                 #-}+{-# LANGUAGE LambdaCase            #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE PolyKinds             #-}+{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE TypeApplications      #-}+{-# LANGUAGE TypeFamilies          #-}+{-# LANGUAGE TypeOperators         #-}+{-# LANGUAGE UndecidableInstances  #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.ClientTestUtils where++import           Prelude ()+import           Prelude.Compat++import           Control.Concurrent               (ThreadId, forkIO, killThread)+import           Control.Monad                    (join)+import           Control.Monad.Error.Class        (throwError)+import           Data.Aeson+import           Data.ByteString                  (ByteString)+import           Data.ByteString.Builder          (byteString)+import qualified Data.ByteString.Char8            as C8+import qualified Data.ByteString.Lazy             as LazyByteString+import           Data.Char                        (chr, isPrint)+import           Data.Maybe                       (fromMaybe)+import           Data.Monoid ()+import           Data.Proxy+import           Data.SOP+import           Data.Text                        (Text)+import qualified Data.Text                        as Text+import           Data.Text.Encoding               (decodeUtf8, encodeUtf8)+import qualified Generics.SOP                     as GSOP+import           GHC.Generics                     (Generic)+import qualified Network.HTTP.Client              as C+import qualified Network.HTTP.Types               as HTTP+import           Network.Socket+import qualified Network.Wai                      as Wai+import           Network.Wai.Handler.Warp+import           System.IO.Unsafe                 (unsafePerformIO)+import           Test.QuickCheck+import           Text.Read                        (readMaybe)+import           Web.FormUrlEncoded               (FromForm, ToForm)++import           Servant.API+                 (AuthProtect, BasicAuth, BasicAuthData (..), Capture,+                 CaptureAll, DeepQuery, DeleteNoContent, EmptyAPI,+                 FormUrlEncoded, Fragment, FromHttpApiData (..), Get, Header,+                 Headers, Host, JSON, MimeRender (mimeRender),+                 MimeUnrender (mimeUnrender), NamedRoutes,+                 NoContent (NoContent), PlainText, Post, QueryFlag, QueryParam,+                 QueryParams, QueryString, Raw, ReqBody, StdMethod (GET),+                 ToHttpApiData (..), UVerb, Union, Verb,+                 WithStatus (WithStatus), addHeader, (:<|>) ((:<|>)), (:>))+import           Servant.API.Generic              ((:-))+import           Servant.API.MultiVerb+import           Servant.API.QueryString+                 (FromDeepQuery (..), ToDeepQuery (..))+import           Servant.API.Range+import           Servant.Client+import qualified Servant.Client.Core.Auth         as Auth+import           Servant.Server+import           Servant.Server.Experimental.Auth+import           Servant.Test.ComprehensiveAPI++-- This declaration simply checks that all instances are in place.+_ = client comprehensiveAPIWithoutStreaming++-- * test data types++data Person = Person+  { _name :: String+  , _age  :: Integer+  } deriving (Eq, Show, Read, Generic)++instance ToJSON Person+instance FromJSON Person++instance ToForm Person+instance FromForm Person++instance Arbitrary Person where+  arbitrary = Person <$> arbitrary <*> arbitrary++instance MimeRender PlainText Person where+  mimeRender _ = LazyByteString.fromStrict . encodeUtf8 . Text.pack . show++instance MimeUnrender PlainText Person where+  mimeUnrender _ =+    -- This does not handle any errors, but it should be fine for tests+    Right . read . Text.unpack . decodeUtf8 . LazyByteString.toStrict+++alice :: Person+alice = Person "Alice" 42++carol :: Person+carol = Person "Carol" 17++type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]+type TestSetCookieHeaders = '[Header "Set-Cookie" String, Header "Set-Cookie" String]++data RecordRoutes mode = RecordRoutes+  { version :: mode :- "version" :> Get '[JSON] Int+  , echo :: mode :- "echo" :> Capture "string" String :> Get '[JSON] String+  , otherRoutes :: mode :- "other" :> Capture "someParam" Int :> NamedRoutes OtherRoutes+  } deriving Generic++newtype OtherRoutes mode = OtherRoutes+  { something :: mode :- "something" :> Get '[JSON] [String]+  } deriving Generic++-- Get for HTTP 307 Temporary Redirect+type Get307 = Verb 'GET 307++data Filter = Filter+  { ageFilter :: Integer+  , nameFilter :: String+  }+  deriving Show++instance FromDeepQuery Filter where+  fromDeepQuery params = do+    let maybeToRight l = maybe (Left l) Right+    age' <- maybeToRight "missing age" $ readMaybe . Text.unpack =<< join (lookup ["age"] params)+    name' <- maybeToRight "missing name" $ join $ lookup ["name"] params+    return $ Filter age' (Text.unpack name')++instance ToDeepQuery Filter where+  toDeepQuery (Filter age' name') =+    [ (["age"], Just (Text.pack $ show age'))+    , (["name"], Just (Text.pack name'))+    ]++-----------------------------+-- MultiVerb test endpoint --+-----------------------------++-- This is the list of all possible responses+type MultipleChoicesIntResponses =+  '[ RespondEmpty 400 "Negative"+   , Respond 200 "Even number" Bool+   , Respond 200 "Odd number" Int+   ]++data MultipleChoicesIntResult+  = NegativeNumber+  | Even Bool+  | Odd Int+  deriving stock (Generic)+  deriving (AsUnion MultipleChoicesIntResponses)+    via GenericAsUnion MultipleChoicesIntResponses MultipleChoicesIntResult++instance GSOP.Generic MultipleChoicesIntResult++-- This is our endpoint description+type MultipleChoicesInt =+  Capture "int" Int+  :> MultiVerb+    'GET+    '[JSON]+    MultipleChoicesIntResponses+    MultipleChoicesIntResult++type Api =+  Get '[JSON] Person+  :<|> "get" :> Get '[JSON] Person+  -- This endpoint returns a response with status code 307 Temporary Redirect,+  -- different from the ones in the 2xx successful class, to test derivation+  -- of clients' api.+  :<|> "get307" :> Get307 '[PlainText] Text+  :<|> "deleteEmpty" :> DeleteNoContent+  :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person+  :<|> "captureAll" :> CaptureAll "names" String :> Get '[JSON] [Person]+  :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person+  :<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person+  -- This endpoint makes use of a 'Raw' server because it is not currently+  -- possible to handle arbitrary binary query param values with+  -- @servant-server@+  :<|> "param-binary" :> QueryParam "payload" UrlEncodedByteString :> Raw+  :<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]+  :<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool+  :<|> "query-string" :> QueryString :> Get '[JSON] Person+  :<|> "deep-query" :> DeepQuery "filter" Filter :> Get '[JSON] Person+  :<|> "fragment" :> Fragment String :> Get '[JSON] Person+  :<|> "rawSuccess" :> Raw+  :<|> "rawSuccessPassHeaders" :> Raw+  :<|> "rawFailure" :> Raw+  :<|> "multiple" :>+            Capture "first" String :>+            QueryParam "second" Int :>+            QueryFlag "third" :>+            ReqBody '[JSON] [(String, [Rational])] :>+            Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])+  :<|> "headers" :> Get '[JSON] (Headers TestHeaders Bool)+  :<|> "uverb-headers" :> UVerb 'GET '[JSON] '[ WithStatus 200 (Headers TestHeaders Bool), WithStatus 204 String ]+  :<|> "set-cookie-headers" :> Get '[JSON] (Headers TestSetCookieHeaders Bool)+  :<|> "deleteContentType" :> DeleteNoContent+  :<|> "redirectWithCookie" :> Raw+  :<|> "empty" :> EmptyAPI+  :<|> "uverb-success-or-redirect" :>+            Capture "bool" Bool :>+            UVerb 'GET '[PlainText] '[WithStatus 200 Person,+                                      WithStatus 301 Text]+  :<|> "uverb-get-created" :> UVerb 'GET '[PlainText] '[WithStatus 201 Person]+  :<|> NamedRoutes RecordRoutes+  :<|> "multiple-choices-int" :> MultipleChoicesInt+  :<|> "captureVerbatim" :> Capture "someString" Verbatim :> Get '[PlainText] Text+  :<|> "host-test" :> Host "servant.example" :> Get '[JSON] Bool+  :<|> PaginatedAPI++api :: Proxy Api+api = Proxy++getRoot         :: ClientM Person+getGet          :: ClientM Person+getGet307       :: ClientM Text+getDeleteEmpty  :: ClientM NoContent+getCapture      :: String -> ClientM Person+getCaptureAll   :: [String] -> ClientM [Person]+getBody         :: Person -> ClientM Person+getQueryParam   :: Maybe String -> ClientM Person+getQueryParamBinary :: Maybe UrlEncodedByteString -> HTTP.Method -> ClientM Response+getQueryParams  :: [String] -> ClientM [Person]+getQueryFlag    :: Bool -> ClientM Bool+getQueryString  :: [(ByteString, Maybe ByteString)] -> ClientM Person+getDeepQuery    :: Filter -> ClientM Person+getFragment     :: ClientM Person+getRawSuccess   :: HTTP.Method -> ClientM Response+getRawSuccessPassHeaders :: HTTP.Method -> ClientM Response+getRawFailure   :: HTTP.Method -> ClientM Response+getMultiple     :: String -> Maybe Int -> Bool -> [(String, [Rational])]+  -> ClientM (String, Maybe Int, Bool, [(String, [Rational])])+getRespHeaders  :: ClientM (Headers TestHeaders Bool)+getUVerbRespHeaders  :: ClientM (Union '[ WithStatus 200 (Headers TestHeaders Bool), WithStatus 204 String ])+getSetCookieHeaders  :: ClientM (Headers TestSetCookieHeaders Bool)+getDeleteContentType :: ClientM NoContent+getRedirectWithCookie :: HTTP.Method -> ClientM Response+uverbGetSuccessOrRedirect :: Bool+                          -> ClientM (Union '[WithStatus 200 Person,+                                              WithStatus 301 Text])+uverbGetCreated :: ClientM (Union '[WithStatus 201 Person])+recordRoutes :: RecordRoutes (AsClientT ClientM)+multiChoicesInt :: Int -> ClientM MultipleChoicesIntResult+captureVerbatim :: Verbatim -> ClientM Text+getHost :: ClientM Bool+getPaginatedPerson :: Maybe (Range 1 100) -> ClientM [Person]++getRoot+  :<|> getGet+  :<|> getGet307+  :<|> getDeleteEmpty+  :<|> getCapture+  :<|> getCaptureAll+  :<|> getBody+  :<|> getQueryParam+  :<|> getQueryParamBinary+  :<|> getQueryParams+  :<|> getQueryFlag+  :<|> getQueryString+  :<|> getDeepQuery+  :<|> getFragment+  :<|> getRawSuccess+  :<|> getRawSuccessPassHeaders+  :<|> getRawFailure+  :<|> getMultiple+  :<|> getRespHeaders+  :<|> getUVerbRespHeaders+  :<|> getSetCookieHeaders+  :<|> getDeleteContentType+  :<|> getRedirectWithCookie+  :<|> EmptyClient+  :<|> uverbGetSuccessOrRedirect+  :<|> uverbGetCreated+  :<|> recordRoutes+  :<|> multiChoicesInt+  :<|> captureVerbatim+  :<|> getHost+  :<|> getPaginatedPerson = client api++server :: Application+server = serve api (+       return carol+  :<|> return alice+  :<|> return "redirecting"+  :<|> return NoContent+  :<|> (\ name -> return $ Person name 0)+  :<|> (\ names -> return (zipWith Person names [0..]))+  :<|> return+  :<|> (\case+          Just "alice" -> return alice+          Just n -> throwError $ ServerError 400 (n ++ " not found") "" []+          Nothing -> throwError $ ServerError 400 "missing parameter" "" [])+  :<|> const (Tagged $ \request respond ->+          respond . maybe (Wai.responseLBS HTTP.notFound404 [] "Missing: payload")+                          (Wai.responseLBS HTTP.ok200 [] . LazyByteString.fromStrict)+                  . join+                  . lookup "payload"+                  $ Wai.queryString request+       )+  :<|> (\ names -> return (zipWith Person names [0..]))+  :<|> return+  :<|> (\ q -> return alice { _name = maybe mempty C8.unpack $ join (lookup "name" q)+                            , _age = fromMaybe 0 (readMaybe . C8.unpack =<< join (lookup "age" q))+                            }+       )+  :<|> (\ filter'  -> return alice { _name = nameFilter filter'+                                   , _age = ageFilter filter'+                                   }+       )+  :<|> return alice+  :<|> Tagged (\ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "rawSuccess")+  :<|> Tagged (\ request respond -> respond $ Wai.responseLBS HTTP.ok200 (Wai.requestHeaders request) "rawSuccess")+  :<|> Tagged (\ _request respond -> respond $ Wai.responseLBS HTTP.badRequest400 [] "rawFailure")+  :<|> (\ a b c d -> return (a, b, c, d))+  :<|> return (addHeader 1729 $ addHeader "eg2" True)+  :<|> (pure . Z . I . WithStatus $ addHeader 1729 $ addHeader "eg2" True)+  :<|> return (addHeader "cookie1" $ addHeader "cookie2" True)+  :<|> return NoContent+  :<|> Tagged (\ _request respond -> respond $ Wai.responseLBS HTTP.found302 [("Location", "testlocation"), ("Set-Cookie", "testcookie=test")] "")+  :<|> emptyServer+  :<|> (\shouldRedirect -> if shouldRedirect+                              then respond (WithStatus @301 ("redirecting" :: Text))+                              else respond (WithStatus @200 alice ))+  :<|> respond (WithStatus @201 carol)+  :<|> RecordRoutes+         { version = pure 42+         , echo = pure+         , otherRoutes = const OtherRoutes+             { something = pure ["foo", "bar", "pweet"]+             }+         }+  :<|> (\param ->+          if param < 0+          then pure NegativeNumber+          else+            if even param+            then pure $ Odd 3+            else pure $ Even True+            )++  :<|> pure . decodeUtf8 . unVerbatim+  :<|> pure True+  :<|> usersServer+  )++-- * api for testing failures++type FailApi =+       "get" :> Raw+  :<|> "capture" :> Capture "name" String :> Raw+  :<|> "body" :> Raw+  :<|> "headers" :> Raw+failApi :: Proxy FailApi+failApi = Proxy++failServer :: Application+failServer = serve failApi (+       Tagged (\ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "")+  :<|> (\ _capture -> Tagged $ \_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "application/json")] "")+  :<|> Tagged (\_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "fooooo")] "")+  :<|> Tagged (\_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "application/x-www-form-urlencoded"), ("X-Example1", "1"), ("X-Example2", "foo")] "")+  )++-- * basic auth stuff++type BasicAuthAPI =+       BasicAuth "foo-realm" () :> "private" :> "basic" :> Get '[JSON] Person++basicAuthAPI :: Proxy BasicAuthAPI+basicAuthAPI = Proxy++basicAuthHandler :: BasicAuthCheck ()+basicAuthHandler =+  let check (BasicAuthData username password) =+        if username == "servant" && password == "server"+        then return (Authorized ())+        else return Unauthorized+  in BasicAuthCheck check++basicServerContext :: Context '[ BasicAuthCheck () ]+basicServerContext = basicAuthHandler :. EmptyContext++basicAuthServer :: Application+basicAuthServer = serveWithContext basicAuthAPI basicServerContext (const (return alice))++-- * general auth stuff++type GenAuthAPI =+  AuthProtect "auth-tag" :> "private" :> "auth" :> Get '[JSON] Person++genAuthAPI :: Proxy GenAuthAPI+genAuthAPI = Proxy++type instance AuthServerData (AuthProtect "auth-tag") = ()+type instance Auth.AuthClientData (AuthProtect "auth-tag") = ()++genAuthHandler :: AuthHandler Wai.Request ()+genAuthHandler =+  let handler req = case lookup "AuthHeader" (Wai.requestHeaders req) of+        Nothing -> throwError (err401 { errBody = "Missing auth header" })+        Just _ -> return ()+  in mkAuthHandler handler++genAuthServerContext :: Context '[ AuthHandler Wai.Request () ]+genAuthServerContext = genAuthHandler :. EmptyContext++genAuthServer :: Application+genAuthServer = serveWithContext genAuthAPI genAuthServerContext (const (return alice))++{-# NOINLINE manager' #-}+manager' :: C.Manager+manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings++runClient :: ClientM a -> BaseUrl -> IO (Either ClientError a)+runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')++-- * utils++startWaiApp :: Application -> IO (ThreadId, BaseUrl)+startWaiApp app = do+    (port, socket) <- openTestSocket+    let settings = setPort port defaultSettings+    thread <- forkIO $ runSettingsSocket settings socket app+    return (thread, BaseUrl Http "localhost" port "")+++endWaiApp :: (ThreadId, BaseUrl) -> IO ()+endWaiApp (thread, _) = killThread thread++openTestSocket :: IO (Port, Socket)+openTestSocket = do+  s <- socket AF_INET Stream defaultProtocol+  let localhost = tupleToHostAddress (127, 0, 0, 1)+  bind s (SockAddrInet defaultPort localhost)+  listen s 1+  port <- socketPort s+  return (fromIntegral port, s)++pathGen :: Gen (NonEmptyList Char)+pathGen = fmap NonEmpty path+ where+  path = listOf1 $ elements $+    filter (not . (`elem` ("?%[]/#;" :: String))) $+    filter isPrint $+    map chr [0..127]++newtype UrlEncodedByteString = UrlEncodedByteString { unUrlEncodedByteString :: ByteString }++instance ToHttpApiData UrlEncodedByteString where+    toEncodedUrlPiece = byteString . HTTP.urlEncode True . unUrlEncodedByteString+    toUrlPiece = decodeUtf8 . HTTP.urlEncode True . unUrlEncodedByteString++instance FromHttpApiData UrlEncodedByteString where+    parseUrlPiece = pure . UrlEncodedByteString . HTTP.urlDecode True . encodeUtf8++newtype Verbatim = Verbatim { unVerbatim :: ByteString }++instance ToHttpApiData Verbatim where+    toEncodedUrlPiece = byteString . unVerbatim+    toUrlPiece = decodeUtf8 . unVerbatim++instance FromHttpApiData Verbatim where+    parseUrlPiece = pure . Verbatim . encodeUtf8++-- * range type example++type PaginatedAPI =+  "users" :> QueryParam "page" (Range 1 100) :> Get '[JSON] [Person]++usersServer :: Maybe (Range 1 100) -> Handler [Person]+usersServer mpage = do+  let pageNum = maybe 1 unRange mpage+  -- pageNum is guaranteed to be between 1 and 100+  return [Person "Example" $ fromIntegral pageNum]
+ test/Servant/ConnectionErrorSpec.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeApplications       #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.ConnectionErrorSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Control.Exception+                 (fromException)+import           Data.Maybe+                 (isJust)+import           Data.Monoid ()+import           Data.Proxy+import qualified Network.HTTP.Client                  as C+import           Test.Hspec++import           Servant.API+                 (Get, JSON)+import           Servant.Client+import           Servant.ClientTestUtils+++spec :: Spec+spec = describe "Servant.ConnectionErrorSpec" $ do+    connectionErrorSpec++type ConnectionErrorAPI = Get '[JSON] Int++connectionErrorAPI :: Proxy ConnectionErrorAPI+connectionErrorAPI = Proxy++connectionErrorSpec :: Spec+connectionErrorSpec = describe "Servant.Client.ClientError" $+    it "correctly catches ConnectionErrors when the HTTP request can't go through" $ do+        let getInt = client connectionErrorAPI+        let baseUrl' = BaseUrl Http "example.invalid" 80 ""+        let isHttpError (Left (ConnectionError e)) = isJust $ fromException @C.HttpException e+            isHttpError _ = False+        (isHttpError <$> runClient getInt baseUrl') `shouldReturn` True
+ test/Servant/FailSpec.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.FailSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Data.Monoid ()+import qualified Network.HTTP.Types                   as HTTP+import           Test.Hspec++import           Servant.API+                 ((:<|>) ((:<|>)))+import           Servant.Client+import           Servant.ClientTestUtils++spec :: Spec+spec = describe "Servant.FailSpec" $ do+    failSpec++failSpec :: Spec+failSpec = beforeAll (startWaiApp failServer) $ afterAll endWaiApp $ do++    context "client returns errors appropriately" $ do+      it "reports FailureResponse" $ \(_, baseUrl) -> do+        let (_ :<|> _ :<|> _ :<|> getDeleteEmpty :<|> _) = client api+        Left res <- runClient getDeleteEmpty baseUrl+        case res of+          FailureResponse _ r | responseStatusCode r == HTTP.status404 -> return ()+          _ -> fail $ "expected 404 response, but got " <> show res++      it "reports DecodeFailure" $ \(_, baseUrl) -> do+        let (_ :<|> _ :<|> _ :<|> _ :<|> getCapture :<|> _) = client api+        Left res <- runClient (getCapture "foo") baseUrl+        case res of+          DecodeFailure _ _ -> return ()+          _ -> fail $ "expected DecodeFailure, but got " <> show res++      it "reports ConnectionError" $ \_ -> do+        let (getGetWrongHost :<|> _) = client api+        Left res <- runClient getGetWrongHost (BaseUrl Http "127.0.0.1" 19872 "")+        case res of+          ConnectionError _ -> return ()+          _ -> fail $ "expected ConnectionError, but got " <> show res++      it "reports UnsupportedContentType" $ \(_, baseUrl) -> do+        let (_ :<|> getGet :<|> _ ) = client api+        Left res <- runClient getGet baseUrl+        case res of+          UnsupportedContentType "application/octet-stream" _ -> return ()+          _ -> fail $ "expected UnsupportedContentType, but got " <> show res++      it "reports UnsupportedContentType when there are response headers" $ \(_, baseUrl) -> do+        Left res <- runClient getRespHeaders baseUrl+        case res of+          UnsupportedContentType "application/x-www-form-urlencoded" _ -> return ()+          _ -> fail $ "expected UnsupportedContentType, but got " <> show res++      it "reports InvalidContentTypeHeader" $ \(_, baseUrl) -> do+        let (_ :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> getBody :<|> _) = client api+        Left res <- runClient (getBody alice) baseUrl+        case res of+          InvalidContentTypeHeader _ -> return ()+          _ -> fail $ "expected InvalidContentTypeHeader, but got " <> show res
+ test/Servant/GenAuthSpec.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.GenAuthSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Control.Arrow+                 (left)+import           Data.Monoid ()+import qualified Network.HTTP.Types                   as HTTP+import           Test.Hspec++import           Servant.Client+import qualified Servant.Client.Core.Auth    as Auth+import qualified Servant.Client.Core.Request as Req+import           Servant.ClientTestUtils++spec :: Spec+spec = describe "Servant.GenAuthSpec" $ do+    genAuthSpec++genAuthSpec :: Spec+genAuthSpec = beforeAll (startWaiApp genAuthServer) $ afterAll endWaiApp $ do+  context "Authentication works when requests are properly authenticated" $ do++    it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do+      let getProtected = client genAuthAPI+      let authRequest = Auth.mkAuthenticatedRequest () (\_ req -> Req.addHeader "AuthHeader" ("cool" :: String) req)+      left show <$> runClient (getProtected authRequest) baseUrl `shouldReturn` Right alice++  context "Authentication is rejected when requests are not authenticated properly" $ do++    it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do+      let getProtected = client genAuthAPI+      let authRequest = Auth.mkAuthenticatedRequest () (\_ req -> Req.addHeader "Wrong" ("header" :: String) req)+      Left (FailureResponse _ r) <- runClient (getProtected authRequest) baseUrl+      responseStatusCode r `shouldBe` HTTP.Status 401 "Unauthorized"+
+ test/Servant/GenericSpec.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.GenericSpec (spec) where++import           Test.Hspec++import           Servant.Client ((//), (/:))+import           Servant.ClientTestUtils++spec :: Spec+spec = describe "Servant.GenericSpec" $ do+    genericSpec++genericSpec :: Spec+genericSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do+  context "Record clients work as expected" $ do++    it "Client functions return expected values" $ \(_,baseUrl) -> do+      runClient (recordRoutes // version) baseUrl `shouldReturn` Right 42+      runClient (recordRoutes // echo /: "foo") baseUrl `shouldReturn` Right "foo"+    it "Clients can be nested" $ \(_,baseUrl) -> do+      runClient (recordRoutes // otherRoutes /: 42 // something) baseUrl `shouldReturn` Right ["foo", "bar", "pweet"]
+ test/Servant/HoistClientSpec.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE RecordWildCards        #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.HoistClientSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Data.Monoid ()+import           Data.Proxy+import           Test.Hspec++import           Servant.API+                 ((:<|>) ((:<|>)), (:>), Capture,+                 Get, JSON, Post)+import           Servant.Client+import           Servant.Server+import           Servant.ClientTestUtils+++spec :: Spec+spec = describe "Servant.HoistClientSpec" $ do+    hoistClientSpec++type HoistClientAPI = Get '[JSON] Int :<|> Capture "n" Int :> Post '[JSON] Int++hoistClientAPI :: Proxy HoistClientAPI+hoistClientAPI = Proxy++hoistClientServer :: Application -- implements HoistClientAPI+hoistClientServer = serve hoistClientAPI $ return 5 :<|> return ++hoistClientSpec :: Spec+hoistClientSpec = beforeAll (startWaiApp hoistClientServer) $ afterAll endWaiApp $ do+  describe "Servant.Client.hoistClient" $ do+    it "allows us to GET/POST/... requests in IO instead of ClientM" $ \(_, baseUrl) -> do+      let (getInt :<|> postInt)+            = hoistClient hoistClientAPI+                          (fmap (either (error . show) id) . flip runClient baseUrl)+                          (client hoistClientAPI)++      getInt `shouldReturn` 5+      postInt 5 `shouldReturn` 5
+ test/Servant/MiddlewareSpec.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}++module Servant.MiddlewareSpec (spec) where++import Control.Arrow (left)+import Control.Concurrent (newEmptyMVar, putMVar, takeMVar)+import Control.Exception (Exception, throwIO, try)+import Control.Monad.IO.Class+import Data.ByteString.Builder (toLazyByteString)+import Data.IORef (modifyIORef, newIORef, readIORef)+import Data.Monoid ()+import Prelude.Compat+import Servant.Client+import Servant.Client.Core (RequestF (..))+import Servant.Client.Internal.HttpClient (ClientMiddleware)+import Servant.ClientTestUtils+import Test.Hspec+import Prelude ()++runClientWithMiddleware :: ClientM a -> ClientMiddleware -> BaseUrl -> IO (Either ClientError a)+runClientWithMiddleware x mid baseUrl' =+  runClientM x ((mkClientEnv manager' baseUrl') {middleware = mid})++data CustomException = CustomException deriving (Show, Eq)++instance Exception CustomException++spec :: Spec+spec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do+  it "Raw request and response can be accessed in middleware" $ \(_, baseUrl) -> do+    mvarReq <- newEmptyMVar+    mvarResp <- newEmptyMVar++    let mid :: ClientMiddleware+        mid oldApp req = do+          -- "Log" request+          liftIO $ putMVar mvarReq req+          -- perform request+          resp <- oldApp req+          -- "Log" response+          liftIO $ putMVar mvarResp resp+          pure resp++    -- Same as without middleware+    left show <$> runClientWithMiddleware getGet mid baseUrl `shouldReturn` Right alice++    -- Access some raw request data+    req <- takeMVar mvarReq+    toLazyByteString (requestPath req) `shouldBe` "/get"++    -- Access some raw response data+    resp <- takeMVar mvarResp+    responseBody resp `shouldBe` "{\"_age\":42,\"_name\":\"Alice\"}"++  it "errors can be thrown in middleware" $ \(_, baseUrl) -> do+    let mid :: ClientMiddleware+        mid oldApp req = do+          -- perform request+          resp <- oldApp req+          -- throw error+          _ <- liftIO $ throwIO CustomException+          pure resp++    try (runClientWithMiddleware getGet mid baseUrl) `shouldReturn` Left CustomException++  it "runs in the expected order" $ \(_, baseUrl) -> do+    ref <- newIORef []++    let mid1 :: ClientMiddleware+        mid1 oldApp req = do+          liftIO $ modifyIORef ref (\xs -> xs <> ["req1"])+          resp <- oldApp req+          liftIO $ modifyIORef ref (\xs -> xs <> ["resp1"])+          pure resp++    let mid2 :: ClientMiddleware+        mid2 oldApp req = do+          liftIO $ modifyIORef ref (\xs -> xs <> ["req2"])+          resp <- oldApp req+          liftIO $ modifyIORef ref (\xs -> xs <> ["resp2"])+          pure resp++    let mid3 :: ClientMiddleware+        mid3 oldApp req = do+          liftIO $ modifyIORef ref (\xs -> xs <> ["req3"])+          resp <- oldApp req+          liftIO $ modifyIORef ref (\xs -> xs <> ["resp3"])+          pure resp++    let mid :: ClientMiddleware+        mid = mid1 . mid2 . mid3+        -- \^ Composition in "reverse order".+        -- It is equivalent to the following, which is more intuitive:+        -- mid :: ClientMiddleware+        -- mid oldApp = mid1 (mid2 (mid3 oldApp))++    -- Same as without middleware+    left show <$> runClientWithMiddleware getGet mid baseUrl `shouldReturn` Right alice++    ref <- readIORef ref+    ref `shouldBe` ["req1", "req2", "req3", "resp3", "resp2", "resp1"]
+ test/Servant/StreamSpec.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.StreamSpec (spec) where++import           Control.Monad.Trans.Except+import qualified Data.ByteString               as BS+import           Data.Proxy+import qualified Network.HTTP.Client           as C+import           Prelude ()+import           Prelude.Compat+import           Servant.API+                 ((:<|>) ((:<|>)), (:>), JSON, NetstringFraming, StreamBody,+                 NewlineFraming, NoFraming, OctetStream, SourceIO, StreamGet,+                 )+import           Servant.Client.Streaming+import           Servant.Server+import           Servant.Test.ComprehensiveAPI+import           Servant.Types.SourceT+import           System.Entropy+                 (getEntropy, getHardwareEntropy)+import           System.IO.Unsafe+                 (unsafePerformIO)+import           Test.Hspec+import           Servant.ClientTestUtils (Person(..))+import qualified Servant.ClientTestUtils as CT++-- This declaration simply checks that all instances are in place.+-- Note: this is streaming client+_ = client comprehensiveAPI++spec :: Spec+spec = describe "Servant.Client.Streaming" $ do+    streamSpec++type StreamApi =+         "streamGetNewline" :> StreamGet NewlineFraming JSON (SourceIO Person)+    :<|> "streamGetNetstring" :> StreamGet NetstringFraming JSON (SourceIO Person)+    :<|> "streamALot" :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)+    :<|> "streamBody" :> StreamBody NoFraming OctetStream (SourceIO BS.ByteString) :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)++api :: Proxy StreamApi+api = Proxy++getGetNL, getGetNS :: ClientM (SourceIO Person)+getGetALot :: ClientM (SourceIO BS.ByteString)+getStreamBody :: SourceT IO BS.ByteString -> ClientM (SourceIO BS.ByteString)+getGetNL :<|> getGetNS :<|> getGetALot :<|> getStreamBody = client api++alice :: Person+alice = Person "Alice" 42++bob :: Person+bob = Person "Bob" 25++server :: Application+server = serve api+    $    return (source [alice, bob, alice])+    :<|> return (source [alice, bob, alice])+    -- 2 ^ (18 + 10) = 256M+    :<|> return (SourceT ($ lots (powerOfTwo 18)))+    :<|> return+  where+    lots n+        | n < 0     = Stop+        | otherwise = Effect $ do+            let size = powerOfTwo 10+            mbs <- getHardwareEntropy size+            bs <- maybe (getEntropy size) pure mbs+            return (Yield bs (lots (n - 1)))++powerOfTwo :: Int -> Int+powerOfTwo = (2 ^)++{-# NOINLINE manager' #-}+manager' :: C.Manager+manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings++withClient :: ClientM a -> BaseUrl -> (Either ClientError a -> IO r) -> IO r+withClient x baseUrl' = withClientM x (mkClientEnv manager' baseUrl')++testRunSourceIO :: SourceIO a+    -> IO (Either String [a])+testRunSourceIO = runExceptT . runSourceT++streamSpec :: Spec+streamSpec = beforeAll (CT.startWaiApp server) $ afterAll CT.endWaiApp $ do+    it "works with Servant.API.StreamGet.Newline" $ \(_, baseUrl) -> do+        withClient getGetNL baseUrl $ \(Right res) ->+            testRunSourceIO res `shouldReturn` Right [alice, bob, alice]++    it "works with Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do+        withClient getGetNS baseUrl $ \(Right res) ->+            testRunSourceIO res `shouldReturn` Right [alice, bob, alice]++    it "works with Servant.API.StreamBody" $ \(_, baseUrl) -> do+        withClient (getStreamBody (source input)) baseUrl $ \(Right res) ->+            testRunSourceIO res `shouldReturn` Right output+        where+          input = ["foo", "", "bar"]+          output = ["foo", "bar"]
+ test/Servant/SuccessSpec.hs view
@@ -0,0 +1,202 @@+{-# LANGUAGE CPP                   #-}+{-# LANGUAGE ConstraintKinds       #-}+{-# LANGUAGE DataKinds             #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE GADTs                 #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE PolyKinds             #-}+{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE TypeApplications      #-}+{-# LANGUAGE TypeFamilies          #-}+{-# LANGUAGE UndecidableInstances  #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.SuccessSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Control.Arrow+                 (left)+import           Control.Concurrent.STM+                 (atomically)+import           Control.Concurrent.STM.TVar+                 (newTVar, readTVar)+import           Data.Foldable+                 (forM_, toList)+import           Data.Maybe+                 (listToMaybe)+import           Data.Monoid ()+import           Data.Text+                 (Text)+import           Data.Text.Encoding+                 (encodeUtf8)+import qualified Network.HTTP.Client                as C+import qualified Network.HTTP.Types                 as HTTP+import           Test.Hspec+import           Test.Hspec.QuickCheck+import           Test.HUnit+import           Test.QuickCheck++import           Servant.API+                 (NoContent (NoContent), WithStatus (WithStatus), getHeaders, Headers(..))+import           Servant.Client+import qualified Servant.Client.Core.Request        as Req+import           Servant.ClientTestUtils+import           Servant.Test.ComprehensiveAPI++-- This declaration simply checks that all instances are in place.+_ = client comprehensiveAPIWithoutStreaming++spec :: Spec+spec = describe "Servant.SuccessSpec" $ successSpec++successSpec :: Spec+successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do+    describe "Servant.API.Get" $ do+      it "get root endpoint" $ \(_, baseUrl) -> left show <$> runClient getRoot baseUrl  `shouldReturn` Right carol++      it "get simple endpoint" $ \(_, baseUrl) -> left show <$> runClient getGet baseUrl  `shouldReturn` Right alice++      it "get redirection endpoint" $ \(_, baseUrl) -> left show <$> runClient getGet307 baseUrl `shouldReturn` Right "redirecting"++    describe "Servant.API.Delete" $ do+      it "allows empty content type" $ \(_, baseUrl) -> left show <$> runClient getDeleteEmpty baseUrl `shouldReturn` Right NoContent++      it "allows content type" $ \(_, baseUrl) -> left show <$> runClient getDeleteContentType baseUrl `shouldReturn` Right NoContent++    it "Servant.API.Capture" $ \(_, baseUrl) -> left show <$> runClient (getCapture "Paula") baseUrl `shouldReturn` Right (Person "Paula" 0)++    it "Servant.API.CaptureAll" $ \(_, baseUrl) -> do+      let expected = [Person "Paula" 0, Person "Peta" 1]+      left show <$> runClient (getCaptureAll ["Paula", "Peta"]) baseUrl `shouldReturn` Right expected++    it "Servant.API.ReqBody" $ \(_, baseUrl) -> do+      let p = Person "Clara" 42+      left show <$> runClient (getBody p) baseUrl `shouldReturn` Right p++    it "Servant.API FailureResponse" $ \(_, baseUrl) -> do+      left show <$> runClient (getQueryParam (Just "alice")) baseUrl `shouldReturn` Right alice+      Left (FailureResponse req _) <- runClient (getQueryParam (Just "bob")) baseUrl+      Req.requestPath req `shouldBe` (baseUrl, "/param")+      toList (Req.requestQueryString req) `shouldBe` [("name", Just "bob")]+      Req.requestMethod req `shouldBe` HTTP.methodGet++    it "Servant.API.QueryParam" $ \(_, baseUrl) -> do+      left show <$> runClient (getQueryParam (Just "alice")) baseUrl `shouldReturn` Right alice+      Left (FailureResponse _ r) <- runClient (getQueryParam (Just "bob")) baseUrl+      responseStatusCode r `shouldBe` HTTP.Status 400 "bob not found"++    it "Servant.API.QueryParam.QueryParams" $ \(_, baseUrl) -> do+      left show <$> runClient (getQueryParams []) baseUrl `shouldReturn` Right []+      left show <$> runClient (getQueryParams ["alice", "bob"]) baseUrl+        `shouldReturn` Right [Person "alice" 0, Person "bob" 1]++    context "Servant.API.QueryParam.QueryFlag" $+      forM_ [False, True] $ \ flag -> it (show flag) $ \(_, baseUrl) -> left show <$> runClient (getQueryFlag flag) baseUrl `shouldReturn` Right flag++    it "Servant.API.QueryParam.QueryString" $ \(_, baseUrl) -> do+      let qs = [("name", Just "bob"), ("age", Just "1")]+      left show <$> runClient (getQueryString qs) baseUrl `shouldReturn` Right (Person "bob" 1)++    it "Servant.API.QueryParam.DeepQuery" $ \(_, baseUrl) -> left show <$> runClient (getDeepQuery $ Filter 1 "bob") baseUrl `shouldReturn` (Right (Person "bob" 1))++    it "Servant.API.Fragment" $ \(_, baseUrl) -> left id <$> runClient getFragment baseUrl `shouldReturn` Right alice++    it "Servant.API.Raw on success" $ \(_, baseUrl) -> do+      res <- runClient (getRawSuccess HTTP.methodGet) baseUrl+      case res of+        Left e -> assertFailure $ show e+        Right r -> do+          responseStatusCode r `shouldBe` HTTP.status200+          responseBody r `shouldBe` "rawSuccess"++    it "Servant.API.Raw should return a Left in case of failure" $ \(_, baseUrl) -> do+      res <- runClient (getRawFailure HTTP.methodGet) baseUrl+      case res of+        Right _ -> assertFailure "expected Left, but got Right"+        Left (FailureResponse _ r) -> do+          responseStatusCode r `shouldBe` HTTP.status400+          responseBody r `shouldBe` "rawFailure"+        Left e -> assertFailure $ "expected FailureResponse, but got " ++ show e++    it "Returns headers appropriately" $ \(_, baseUrl) -> do+      res <- runClient getRespHeaders baseUrl+      case res of+        Left e -> assertFailure $ show e+        Right val -> getHeaders val `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")]++    it "Returns headers on UVerb requests" $ \(_, baseUrl) -> do+      res <- runClient getUVerbRespHeaders baseUrl+      case res of+        Left e -> assertFailure $ show e+        Right val -> case matchUnion val of+          Just (WithStatus val' :: WithStatus 200 (Headers TestHeaders Bool))+            -> getHeaders val' `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")]+          Nothing -> assertFailure "unexpected alternative of union"++    it "Returns multiple Set-Cookie headers appropriately" $ \(_, baseUrl) -> do+      res <- runClient getSetCookieHeaders baseUrl+      case res of+        Left e -> assertFailure $ show e+        Right val -> getHeaders val `shouldBe` [("Set-Cookie", "cookie1"), ("Set-Cookie", "cookie2")]++    it "Stores Cookie in CookieJar after a redirect" $ \(_, baseUrl) -> do+      mgr <- C.newManager C.defaultManagerSettings+      cj <- atomically . newTVar $ C.createCookieJar []+      _ <- runClientM (getRedirectWithCookie HTTP.methodGet) (ClientEnv mgr baseUrl (Just cj) defaultMakeClientRequest id)+      cookie <- listToMaybe . C.destroyCookieJar <$> atomically (readTVar cj)+      C.cookie_name <$> cookie `shouldBe` Just "testcookie"+      C.cookie_value <$> cookie `shouldBe` Just "test"++    it "Can modify the outgoing Request using the ClientEnv" $ \(_, baseUrl) -> do+      mgr <- C.newManager C.defaultManagerSettings+      -- In proper situation, extra headers should probably be visible in API type.+      -- However, testing for response timeout is difficult, so we test with something which is easy to observe+      let createClientRequest url r = fmap (\req -> req { C.requestHeaders = [("X-Added-Header", "XXX")] })+                                           (defaultMakeClientRequest url r)+          clientEnv = (mkClientEnv mgr baseUrl) { makeClientRequest = createClientRequest }+      res <- runClientM (getRawSuccessPassHeaders HTTP.methodGet) clientEnv+      case res of+        Left e ->+          assertFailure $ show e+        Right r ->+          ("X-Added-Header", "XXX") `elem` toList (responseHeaders r) `shouldBe` True++    modifyMaxSuccess (const 20) $ it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $ \(_, baseUrl) ->+      property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body ->+        ioProperty $ do+          result <- left show <$> runClient (getMultiple cap num flag body) baseUrl+          return $+            result === Right (cap, num, flag, body)++    context "With a route that can either return success or redirect" $ do+      it "Redirects when appropriate" $ \(_, baseUrl) -> do+        eitherResponse <- runClient (uverbGetSuccessOrRedirect True) baseUrl+        case eitherResponse of+          Left clientError -> fail $ show clientError+          Right response -> matchUnion response `shouldBe` Just (WithStatus @301 @Text "redirecting")++      it "Returns a proper response when appropriate" $ \(_, baseUrl) -> do+        eitherResponse <- runClient (uverbGetSuccessOrRedirect False) baseUrl+        case eitherResponse of+          Left clientError -> fail $ show clientError+          Right response -> matchUnion response `shouldBe` Just (WithStatus @200 alice)++    context "with a route that uses uverb but only has a single response" $+      it "returns the expected response" $ \(_, baseUrl) -> do+        eitherResponse <- runClient uverbGetCreated baseUrl+        case eitherResponse of+          Left clientError -> fail $ show clientError+          Right response -> matchUnion response `shouldBe` Just (WithStatus @201 carol)++    it "encodes URL pieces following ToHttpApiData instance" $ \(_, baseUrl) -> do+      let textOrig = "*"+      eitherResponse <- runClient (captureVerbatim $ Verbatim $ encodeUtf8 textOrig) baseUrl+      case eitherResponse of+        Left clientError -> fail $ show clientError+        Right textBack -> textBack `shouldBe` textOrig
+ test/Servant/WrappedApiSpec.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.WrappedApiSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Data.Kind (Type)+import           Control.Exception+                 (bracket)+import           Control.Monad.Error.Class+                 (throwError)+import           Data.Monoid ()+import           Data.Proxy+import qualified Network.HTTP.Types                   as HTTP+import           Test.Hspec++import           Servant.API+                 (Delete, Get, JSON, Post, Put)+import           Servant.Client+import           Servant.Server+import           Servant.ClientTestUtils++spec :: Spec+spec = describe "Servant.WrappedApiSpec" $ do+    wrappedApiSpec++data WrappedApi where+  WrappedApi :: (HasServer (api :: Type) '[], Server api ~ Handler a,+                 HasClient ClientM api, Client ClientM api ~ ClientM ()) =>+    Proxy api -> WrappedApi++wrappedApiSpec :: Spec+wrappedApiSpec = describe "error status codes" $ do+  let serveW api = serve api $ throwError $ ServerError 500 "error message" "" []+  context "are correctly handled by the client" $+    let test :: (WrappedApi, String) -> Spec+        test (WrappedApi api, desc) =+          it desc $ bracket (startWaiApp $ serveW api) endWaiApp $ \(_, baseUrl) -> do+            let getResponse :: ClientM ()+                getResponse = client api+            Left (FailureResponse _ r) <- runClient getResponse baseUrl+            responseStatusCode r `shouldBe` HTTP.Status 500 "error message"+    in mapM_ test $+        (WrappedApi (Proxy :: Proxy (Delete '[JSON] ())), "Delete") :+        (WrappedApi (Proxy :: Proxy (Get '[JSON] ())), "Get") :+        (WrappedApi (Proxy :: Proxy (Post '[JSON] ())), "Post") :+        (WrappedApi (Proxy :: Proxy (Put '[JSON] ())), "Put") :+        []