├── .gitignore ├── Examples └── Simple.hs ├── LICENSE ├── README.md ├── Setup.hs ├── circle.yml ├── deploy.sh ├── docs └── tutorial.md ├── http-dispatch.cabal ├── src └── Network │ └── HTTP │ ├── Dispatch.hs │ └── Dispatch │ ├── Core.hs │ ├── Request.hs │ └── Types.hs ├── stack.yaml └── test ├── Network └── HTTP │ └── Dispatch │ └── RequestSpec.hs └── Spec.hs /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work 2 | -------------------------------------------------------------------------------- /Examples/Simple.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Network.HTTP.Dispatch.Examples.Simple where 3 | 4 | import Network.HTTP.Dispatch as Dispatch 5 | 6 | example1 = 7 | let req = Dispatch.get "http://google.com" in 8 | Dispatch.http req 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Owain Lewis (c) 2016 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Owain Lewis nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP Dispatch 2 | 3 | > This is the HTTP library I wish I had when first learning Haskell 4 | 5 | [![CircleCI](https://circleci.com/gh/owainlewis/http-dispatch.svg?style=svg)](https://circleci.com/gh/owainlewis/http-dispatch) 6 | 7 | Available on Hackage: You'll want to use a version at 0.6.2.0 or greater 8 | 9 | https://hackage.haskell.org/package/http-dispatch-0.6.2.0 10 | 11 | A high level Haskell HTTP client with a friendly and consistent API. 12 | This library builds upon the http-client library, providing an (IMO) easier and more intuative API 13 | 14 | *There are only two types (HTTPRequest and HTTPResponse). Everything else is sugar for constructing these types* 15 | 16 | ### Differences from http-client 17 | 18 | * Simple DSL (only two types HTTPRequest and HTTPResponse) 19 | * Higher level API 20 | * No exceptions thrown on non 200 status codes 21 | * Supports TLS out of the box 22 | 23 | ### Differences from wreq 24 | 25 | * Lighter 26 | * Doesn't require lens package 27 | 28 | ## Motivation 29 | 30 | There are already a couple of really good HTTP clients for Haskell ([Wreq](http://www.serpentine.com/wreq/), [HTTP Client](https://github.com/snoyberg/http-client)), but typically I'd need to go hunting through documentation just to do even the simplest thing (or having to import a different package for https). 31 | This is the HTTP library I wish I had when first learning Haskell. 32 | 33 | This library strips back everything to be as simple as possible. 34 | It will transparently support HTTPS and has a very consistent DSL for making requests. 35 | 36 | There are only two types. A HTTPRequest and a HTTPResponse. That's all there is to know about this library. 37 | 38 | Some utility functions are provided to make constructing requests easier but it's nothing more than sugar for creating types. 39 | 40 | ### HTTP Request 41 | 42 | A HTTP request has a method, url, a list of headers and an optional body. Header is a type synonym for a ByteString pair i.e (S.ByteString, S.ByteString). The body is a strict ByteString. 43 | 44 | ```haskell 45 | data HTTPRequest = HTTPRequest { 46 | method :: RequestMethod 47 | , url :: String 48 | , headers :: [(S.ByteString, S.ByteString)] 49 | , body :: Maybe S.ByteString 50 | } deriving ( Eq, Ord, Show ) 51 | ``` 52 | 53 | ### HTTP Response 54 | 55 | A HTTP response has a status, a list of headers, and a response body. Header is a type synonym for a ByteString pair i.e (S.ByteString, S.ByteString). The body is a strict ByteString. 56 | 57 | ```haskell 58 | data HTTPResponse = HTTPResponse { 59 | responseStatus :: Int 60 | , responseHeaders :: [(S.ByteString, S.ByteString)] 61 | , resposeBody :: S.ByteString 62 | } deriving ( Eq, Show ) 63 | ``` 64 | 65 | ## Examples 66 | 67 | Some examples to help you get started. 68 | 69 | Remember that *everything* is just sugar for constructing the HTTPRequest type and calling run on it to convert it to a IO HttpResponse. 70 | 71 | ```haskell 72 | {-# LANGUAGE OverloadedStrings #-} 73 | module Example 74 | 75 | import qualified Network.HTTP.Dispatch as Dispatch 76 | 77 | response :: IO Dispatch.HttpResponse 78 | response = http request where reqeuest = get "http://google.com" 79 | 80 | ``` 81 | 82 | If you have any questions comments or feedback let me know 83 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | cache_directories: 3 | - "~/.stack" 4 | pre: 5 | - wget -q -O- https://s3.amazonaws.com/download.fpcomplete.com/ubuntu/fpco.key | sudo apt-key add - 6 | - echo 'deb http://download.fpcomplete.com/ubuntu/precise stable main'|sudo tee /etc/apt/sources.list.d/fpco.list 7 | - sudo apt-get update && sudo apt-get install stack -y 8 | - rm -rf .stack-work 9 | override: 10 | - stack setup 11 | - rm -rf $(stack path --dist-dir) $(stack path --local-install-root) 12 | - stack build 13 | - stack build --test --only-dependencies 14 | 15 | test: 16 | override: 17 | - stack test 18 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | stack test 2 | 3 | cabal clean && cabal sdist 4 | -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | ## Getting started 4 | 5 | ## Basic Requests 6 | 7 | ### GET 8 | 9 | ### POST 10 | 11 | ### PUT / PATCH 12 | 13 | ### DELETE 14 | 15 | ## Adding Headers 16 | 17 | ## Processing Responses 18 | -------------------------------------------------------------------------------- /http-dispatch.cabal: -------------------------------------------------------------------------------- 1 | name: http-dispatch 2 | version: 1.0.0 3 | synopsis: High level HTTP client for Haskell 4 | description: A high level HTTP client with a simple API 5 | homepage: http://github.com/owainlewis/http-dispatch#readme 6 | license: BSD3 7 | license-file: LICENSE 8 | author: Owain Lewis 9 | maintainer: owain@owainlewis.com 10 | copyright: 2016 Owain Lewis 11 | category: Network 12 | build-type: Simple 13 | cabal-version: >=1.10 14 | 15 | library 16 | hs-source-dirs: src 17 | exposed-modules: Network.HTTP.Dispatch 18 | , Network.HTTP.Dispatch.Request 19 | , Network.HTTP.Dispatch.Core 20 | , Network.HTTP.Dispatch.Types 21 | build-depends: base >= 4.5 && < 5 22 | , http-client >= 0.5.7.0 23 | , http-client-tls >= 0.3.5.1 24 | , http-types >= 0.9.1 25 | , bytestring 26 | , base64-bytestring 27 | , case-insensitive 28 | default-language: Haskell2010 29 | 30 | test-suite http-dispatch-test 31 | type: exitcode-stdio-1.0 32 | hs-source-dirs: test 33 | main-is: Spec.hs 34 | build-depends: base 35 | , http-dispatch 36 | , hspec 37 | , aeson 38 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 39 | default-language: Haskell2010 40 | 41 | source-repository head 42 | type: git 43 | location: https://github.com/owainlewis/http-dispatch 44 | -------------------------------------------------------------------------------- /src/Network/HTTP/Dispatch.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Network.HTTP.Dispatch where 3 | 4 | import Network.HTTP.Dispatch.Types 5 | import Network.HTTP.Dispatch.Request 6 | import Network.HTTP.Dispatch.Core 7 | -------------------------------------------------------------------------------- /src/Network/HTTP/Dispatch/Core.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | -- | 3 | -- Module : Network.HTTP.Dispatch.Core 4 | -- Copyright : (c) 2016 Owain Lewis 5 | -- 6 | -- License : BSD-style 7 | -- Maintainer : owain@owainlewis.com 8 | -- Stability : experimental 9 | -- Portability : GHC 10 | -- 11 | -- A transformation layer between Dispatch types and http client 12 | -- 13 | module Network.HTTP.Dispatch.Core 14 | ( http 15 | , httpManager 16 | ) where 17 | 18 | import Control.Applicative ((<$>)) 19 | import qualified Data.ByteString.Char8 as C 20 | import qualified Data.ByteString.Lazy as LBS 21 | import qualified Data.CaseInsensitive as CI 22 | import Data.List (isPrefixOf) 23 | import Network.HTTP.Client as Client 24 | import Network.HTTP.Client.TLS 25 | import Network.HTTP.Dispatch.Types (Header) 26 | import Network.HTTP.Types (RequestHeaders, Status (..)) 27 | 28 | import Network.HTTP.Dispatch.Types 29 | 30 | -- | Transforms a dispatch request into a low level http-client request 31 | -- 32 | toRequest :: HTTPRequest -> IO Client.Request 33 | toRequest (HTTPRequest method url headers body) = do 34 | initReq <- parseRequest url 35 | let hdrs = map (\(k, v) -> (CI.mk k, v)) headers 36 | req = initReq 37 | { Client.method = C.pack . show $ method 38 | , requestHeaders = hdrs 39 | } 40 | return $ maybe req (\lbs -> req { requestBody = RequestBodyBS lbs }) body 41 | 42 | -- | Get the correct Manager depending on the URL (i.e https vs http) 43 | -- 44 | getManagerForUrl :: String -> IO Manager 45 | getManagerForUrl url = 46 | if ("https" `isPrefixOf` url) then newManager tlsManagerSettings 47 | else newManager defaultManagerSettings 48 | 49 | -- | Transforms an http-client response into a dispatch response 50 | -- 51 | toResponse :: Client.Response LBS.ByteString -> HTTPResponse 52 | toResponse resp = 53 | let rStatus = statusCode . Client.responseStatus $ resp 54 | rHdrs = Client.responseHeaders resp 55 | rBody = LBS.toStrict $ Client.responseBody resp 56 | in 57 | HTTPResponse rStatus (map (\(k,v) -> 58 | let hk = CI.original k 59 | in 60 | (hk, v)) rHdrs) rBody 61 | 62 | class Runnable a where 63 | http :: a -> IO HTTPResponse 64 | httpManager :: a -> ManagerSettings -> IO HTTPResponse 65 | 66 | instance Runnable HTTPRequest where 67 | http req = do 68 | manager <- getManagerForUrl (url req) 69 | request <- toRequest req 70 | toResponse <$> httpLbs request manager 71 | 72 | httpManager req settings = do 73 | manager <- newManager settings 74 | request <- toRequest req 75 | toResponse <$> httpLbs request manager 76 | -------------------------------------------------------------------------------- /src/Network/HTTP/Dispatch/Request.hs: -------------------------------------------------------------------------------- 1 | module Network.HTTP.Dispatch.Request 2 | ( raw 3 | , get 4 | , post 5 | , put 6 | , patch 7 | , delete 8 | , options 9 | ) where 10 | 11 | import Network.HTTP.Dispatch.Types 12 | 13 | import qualified Data.ByteString as S 14 | 15 | -- | Make a raw HTTP request 16 | -- 17 | -- @ 18 | -- raw GET "http://google.com" [header "Content-Type" "application/json"] Nothing 19 | -- 20 | -- HTTPRequest { method = GET 21 | -- , url = "http://google.com" 22 | -- , headers = [("Content-Type","application/json")] 23 | -- , body = Nothing 24 | -- } 25 | -- @ 26 | raw :: RequestMethod -> String -> [Header] -> Maybe S.ByteString -> HTTPRequest 27 | raw method url headers body = HTTPRequest method url headers body 28 | 29 | -- A HTTP GET request 30 | -- 31 | get :: String -> HTTPRequest 32 | get url = raw GET url [] Nothing 33 | 34 | -- A HTTP POST request 35 | -- 36 | post :: Url -> Maybe S.ByteString -> HTTPRequest 37 | post url body = raw POST url [] body 38 | 39 | -- A HTTP PUT request 40 | -- 41 | put :: Url -> Maybe S.ByteString -> HTTPRequest 42 | put url body = raw PUT url [] body 43 | 44 | -- A HTTP PATCH request 45 | -- 46 | patch :: Url -> Maybe S.ByteString -> HTTPRequest 47 | patch url body = raw PATCH url [] body 48 | 49 | -- A HTTP DELETE request 50 | -- 51 | delete :: Url -> HTTPRequest 52 | delete url = raw DELETE url [] Nothing 53 | 54 | -- A HTTP OPTIONS request 55 | -- 56 | options :: Url -> HTTPRequest 57 | options url = raw OPTIONS url [] Nothing 58 | -------------------------------------------------------------------------------- /src/Network/HTTP/Dispatch/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | -- | 3 | -- Module : Network.HTTP.Dispatch.Request 4 | -- Copyright : (c) 2016 Owain Lewis 5 | -- 6 | -- License : BSD-style 7 | -- Maintainer : owain@owainlewis.com 8 | -- Stability : experimental 9 | -- Portability : GHC 10 | -- 11 | -- HTTP request type 12 | -- 13 | module Network.HTTP.Dispatch.Types 14 | ( RequestMethod(..) 15 | , HTTPRequest(..) 16 | , HTTPResponse(..) 17 | , Header 18 | , Headers 19 | , Url 20 | , header 21 | , transformHeaders 22 | , withHeader 23 | , withHeaders 24 | , withBody 25 | , withMethod 26 | ) where 27 | 28 | import Prelude hiding (head) 29 | 30 | import qualified Data.ByteString as S 31 | import qualified Data.ByteString.Char8 as SC 32 | 33 | type Header = (S.ByteString, S.ByteString) 34 | 35 | type Headers = [Header] 36 | 37 | type Url = String 38 | 39 | data RequestMethod = 40 | HEAD 41 | | GET 42 | | POST 43 | | PUT 44 | | PATCH 45 | | DELETE 46 | | TRACE 47 | | OPTIONS 48 | | CONNECT deriving ( Eq, Ord, Show ) 49 | 50 | data HTTPRequest = HTTPRequest { 51 | method :: RequestMethod 52 | , url :: String 53 | , headers :: [(S.ByteString, S.ByteString)] 54 | , body :: Maybe S.ByteString 55 | } deriving ( Eq, Ord, Show ) 56 | 57 | data HTTPResponse = HTTPResponse { 58 | responseStatus :: Int 59 | , responseHeaders :: [(S.ByteString, S.ByteString)] 60 | , resposeBody :: S.ByteString 61 | } deriving ( Eq, Show ) 62 | 63 | header :: String -> String -> (S.ByteString, S.ByteString) 64 | header k v = (SC.pack k , SC.pack v) 65 | 66 | transformHeaders :: [(String, String)] -> [(S.ByteString, S.ByteString)] 67 | transformHeaders = map (\(k,v) -> header k v) 68 | 69 | withHeader :: HTTPRequest -> (S.ByteString, S.ByteString) -> HTTPRequest 70 | withHeader req header = req { headers = header : (headers req) } 71 | 72 | withHeaders :: HTTPRequest -> [(S.ByteString, S.ByteString)] -> HTTPRequest 73 | withHeaders req headers = req { headers = headers } 74 | 75 | withBody :: HTTPRequest -> S.ByteString -> HTTPRequest 76 | withBody req body = req { body = Just body } 77 | 78 | withMethod :: HTTPRequest -> RequestMethod -> HTTPRequest 79 | withMethod req method = req { method = method } 80 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # http://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # resolver: ghcjs-0.1.0_ghc-7.10.2 15 | # resolver: 16 | # name: custom-snapshot 17 | # location: "./custom-snapshot.yaml" 18 | resolver: lts-9.2 19 | 20 | # User packages to be built. 21 | # Various formats can be used as shown in the example below. 22 | # 23 | # packages: 24 | # - some-directory 25 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 26 | # - location: 27 | # git: https://github.com/commercialhaskell/stack.git 28 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 29 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a 30 | # extra-dep: true 31 | # subdirs: 32 | # - auto-update 33 | # - wai 34 | # 35 | # A package marked 'extra-dep: true' will only be built if demanded by a 36 | # non-dependency (i.e. a user package), and its test suites and benchmarks 37 | # will not be run. This is useful for tweaking upstream packages. 38 | packages: 39 | - '.' 40 | # Dependency packages to be pulled from upstream that are not in the resolver 41 | # (e.g., acme-missiles-0.3) 42 | extra-deps: [] 43 | 44 | # Override default flag values for local packages and extra-deps 45 | flags: {} 46 | 47 | # Extra package databases containing global packages 48 | extra-package-dbs: [] 49 | 50 | # Control whether we use the GHC we find on the path 51 | # system-ghc: true 52 | # 53 | # Require a specific version of stack, using version ranges 54 | # require-stack-version: -any # Default 55 | # require-stack-version: ">=1.4" 56 | # 57 | # Override the architecture used by stack, especially useful on Windows 58 | # arch: i386 59 | # arch: x86_64 60 | # 61 | # Extra directories used by stack for building 62 | # extra-include-dirs: [/path/to/dir] 63 | # extra-lib-dirs: [/path/to/dir] 64 | # 65 | # Allow a newer minor version of GHC than the snapshot specifies 66 | # compiler-check: newer-minor -------------------------------------------------------------------------------- /test/Network/HTTP/Dispatch/RequestSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Network.HTTP.Dispatch.RequestSpec where 3 | 4 | import Network.HTTP.Dispatch.Request 5 | import Network.HTTP.Dispatch.Types 6 | import Test.Hspec 7 | 8 | main :: IO () 9 | main = hspec spec 10 | 11 | spec :: Spec 12 | spec = do 13 | describe "building GET requests" $ do 14 | it "constructs a GET request" $ do 15 | let actual = get "http://google.com" 16 | expected = HTTPRequest GET "http://google.com" [] Nothing 17 | actual `shouldBe` expected 18 | 19 | describe "building POST requests" $ do 20 | it "constructs a POST request" $ do 21 | let actual = post "http://google.com" (Just "OK") 22 | expected = HTTPRequest POST "http://google.com" [] (Just "OK") 23 | actual `shouldBe` expected 24 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} 2 | --------------------------------------------------------------------------------