api-builder 0.11.0.0 → 0.17.0.0
raw patch · 10 files changed
Files
- api-builder.cabal +71/−66
- src/Network/API/Builder/API.hs +0/−1
- src/Network/API/Builder/Error.hs +7/−3
- src/Network/API/Builder/Examples/StackOverflow.hs +7/−1
- src/Network/API/Builder/Send.hs +6/−3
- test-io/Network/API/Builder/Examples/StackOverflowSpec.hs +40/−0
- test/Network/API/Builder/ErrorSpec.hs +40/−0
- test/Network/API/Builder/QuerySpec.hs +33/−0
- test/Network/API/Builder/RoutesSpec.hs +24/−0
- test/Network/API/Builder/SendSpec.hs +100/−0
api-builder.cabal view
@@ -1,90 +1,95 @@-name: api-builder-version: 0.11.0.0-synopsis: Library for easily building REST API wrappers in Haskell-license: BSD3-license-file: LICENSE-author: Fraser Murray-maintainer: fraser.m.murray@gmail.com-homepage: https://github.com/intolerable/api-builder-copyright: (c) Fraser Murray 2014-category: Network-build-type: Simple-cabal-version: >= 1.10+name: api-builder+version: 0.17.0.0+synopsis: Library for easily building REST API wrappers in Haskell+category: Network+homepage: https://github.com/intolerable/api-builder+author: Fraser Murray+maintainer: fraser.m.murray@gmail.com+copyright: (c) Fraser Murray 2014+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10 source-repository head type: git location: git://github.com/intolerable/api-builder.git -source-repository this- type: git- location: git://github.com/intolerable/api-builder.git- tag: v0.11.0.0- library- exposed-modules:- Network.API.Builder- Network.API.Builder.API- Network.API.Builder.Builder- Network.API.Builder.Error- Network.API.Builder.Examples.StackOverflow- Network.API.Builder.Query- Network.API.Builder.Receive- Network.API.Builder.Routes- Network.API.Builder.Send- Network.API.Builder.Send.Multipart- build-depends:- base >= 4.6 && < 4.9,- aeson >= 0.9 && < 0.10,- bifunctors >= 4.0 && < 6.0,- bytestring == 0.10.*,- HTTP == 4000.*,- http-client >= 0.4.11 && < 0.4.21,- http-client-tls >= 0.2 && < 0.2.3,- http-types == 0.8.*,- text == 1.*,- transformers >= 0.4 && < 0.5 hs-source-dirs: src/- default-language: Haskell2010 default-extensions:- FlexibleInstances OverloadedStrings+ FlexibleInstances ghc-options: -Wall+ build-depends:+ HTTP >=4000,+ aeson >=0.9,+ base >=4.6,+ bifunctors >=4.0,+ bytestring >=0.10,+ http-client >=0.4.30,+ http-client-tls >=0.2,+ http-types >=0.8,+ text >=1.0,+ tls >=1.3,+ transformers >=0.4+ exposed-modules:+ Network.API.Builder+ Network.API.Builder.API+ Network.API.Builder.Builder+ Network.API.Builder.Error+ Network.API.Builder.Examples.StackOverflow+ Network.API.Builder.Query+ Network.API.Builder.Receive+ Network.API.Builder.Routes+ Network.API.Builder.Send+ Network.API.Builder.Send.Multipart+ other-modules:+ Paths_api_builder+ default-language: Haskell2010 test-suite test- hs-source-dirs: test- main-is: Spec.hs- default-extensions:- OverloadedStrings- ScopedTypeVariables- default-language: Haskell2010 type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs: test/+ default-extensions: OverloadedStrings FlexibleInstances ScopedTypeVariables+ ghc-options: -Wall build-depends:- base == 4.*,- Cabal >= 1.16.0,- api-builder,+ Cabal >=1.16.0, aeson,+ api-builder,+ base ==4.*, bytestring, hspec, http-client,+ QuickCheck, text, transformers- GHC-options: -Wall+ other-modules:+ Network.API.Builder.ErrorSpec+ Network.API.Builder.QuerySpec+ Network.API.Builder.RoutesSpec+ Network.API.Builder.SendSpec+ Paths_api_builder+ default-language: Haskell2010 test-suite test-io- hs-source-dirs: test-io- main-is: Spec.hs- default-extensions:- LambdaCase- default-language: Haskell2010 type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs: test-io/+ default-extensions: OverloadedStrings FlexibleInstances LambdaCase+ ghc-options: -Wall build-depends:- base == 4.*,- Cabal >= 1.16.0,- api-builder,- aeson,- bytestring,- containers,- hspec,- text,- transformers- GHC-options: -Wall+ Cabal >=1.16.0+ , aeson+ , api-builder+ , base ==4.*+ , bytestring+ , containers+ , hspec+ , text+ , transformers+ other-modules:+ Network.API.Builder.Examples.StackOverflowSpec+ Paths_api_builder+ default-language: Haskell2010
src/Network/API/Builder/API.hs view
@@ -81,7 +81,6 @@ execAPI b s api = do m <- liftIO $ newManager tlsManagerSettings (res, _, _) <- runAPI b m s api- liftIO $ closeManager m return res -- | Runs an @API@ by executing its transformer stack and dumping it all into @IO@.
src/Network/API/Builder/Error.hs view
@@ -1,7 +1,7 @@ module Network.API.Builder.Error ( APIError(..) ) where -import Data.Monoid+import Data.Semigroup import Network.HTTP.Client (HttpException) import Prelude @@ -23,9 +23,13 @@ (APIError a) == (APIError b) = a == b InvalidURLError == InvalidURLError = True (ParseError a) == (ParseError b) = a == b+ EmptyError == EmptyError = True _ == _ = False +instance Semigroup (APIError a) where+ EmptyError <> x = x+ x <> _ = x+ instance Monoid (APIError a) where mempty = EmptyError- EmptyError `mappend` x = x- x `mappend` _ = x+ mappend = (<>)
src/Network/API/Builder/Examples/StackOverflow.hs view
@@ -3,7 +3,7 @@ module Network.API.Builder.Examples.StackOverflow where import Network.API.Builder-import Control.Applicative+import Control.Applicative ((<$>), (<*>)) import Data.Aeson import Data.Monoid (mempty) import Data.Text (Text)@@ -35,6 +35,9 @@ stackOverflow :: Builder stackOverflow = basicBuilder "StackOverflow API" "http://api.stackexchange.com" +stackOverflowSSL :: Builder+stackOverflowSSL = basicBuilder "StackOverflow API" "https://api.stackexchange.com"+ answersRoute :: Route answersRoute = Route { urlPieces = [ "2.2", "questions" ] , urlParams = [ "order" =. ("desc" :: Text)@@ -44,3 +47,6 @@ getAnswers :: IO (Either (APIError ()) Questions) getAnswers = execAPI stackOverflow () $ runRoute answersRoute++getAnswersSSL :: IO (Either (APIError ()) Questions)+getAnswersSSL = execAPI stackOverflowSSL () $ runRoute answersRoute
src/Network/API/Builder/Send.hs view
@@ -18,7 +18,7 @@ send builder r () = case httpMethod r of "POST" -> do- req <- parseUrl $ Text.unpack $ routeURL (_baseURL builder) (_customizeRoute builder r)+ req <- parseUrlThrow $ Text.unpack $ routeURL (_baseURL builder) (_customizeRoute builder r) return $ _customizeRequest builder $ req { requestHeaders = ("Content-Type", "application/x-www-form-urlencoded") : requestHeaders req , requestBody = RequestBodyBS (dropQuestion $ queryString req)@@ -29,19 +29,22 @@ basicSend :: Builder -> Route -> Maybe Request basicSend builder r = do- req <- parseUrl $ Text.unpack $ routeURL (_baseURL builder) (_customizeRoute builder r)+ req <- parseUrlThrow $ Text.unpack $ routeURL (_baseURL builder) (_customizeRoute builder r) return $ _customizeRequest builder $ req { method = httpMethod r } instance Sendable Value where send builder r value = case httpMethod r of "POST" -> do- req <- parseUrl $ Text.unpack $ routeURL (_baseURL builder) (_customizeRoute builder r)+ req <- parseUrlThrow $ Text.unpack $ routeURL (_baseURL builder) (_customizeRoute builder r) return $ _customizeRequest builder $ req { requestBody = RequestBodyLBS (encode value) , requestHeaders = ("Content-Type", "application/json") : requestHeaders req , method = httpMethod r } _ -> Nothing++useToJSON :: ToJSON a => Builder -> Route -> a -> Maybe Request+useToJSON b r v = send b r (toJSON v) instance Sendable ByteString where send builder r bs =
+ test-io/Network/API/Builder/Examples/StackOverflowSpec.hs view
@@ -0,0 +1,40 @@+module Network.API.Builder.Examples.StackOverflowSpec where++import Network.API.Builder.Examples.StackOverflow++import Control.Monad+import Data.List (nub)+import Test.Hspec+import qualified Data.Set as Set+import qualified Data.Text as Text++main :: IO ()+main = hspec spec++spec :: Spec+spec =+ describe "getAnswers" $ do++ it "can get answers" $+ getAnswers >>= \case+ Left _ -> expectationFailure "getAnswers failed"+ Right (Questions qs) -> do+ nub qs `shouldMatchList` qs+ forM_ qs $ \q -> do+ tags q `shouldSatisfy` isNubbed+ title q `shouldSatisfy` (not . Text.null)++ it "can get answers via https" $+ getAnswersSSL >>= \case+ Left _ -> expectationFailure "getAnswers failed"+ Right (Questions qs) -> do+ nub qs `shouldMatchList` qs+ forM_ qs $ \q -> do+ tags q `shouldSatisfy` isNubbed+ title q `shouldSatisfy` (not . Text.null)++isNubbed :: Ord a => [a] -> Bool+isNubbed = f Set.empty+ where+ f _ [] = True+ f s (x:xs) = not (Set.member x s) && f (Set.insert x s) xs
+ test/Network/API/Builder/ErrorSpec.hs view
@@ -0,0 +1,40 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+module Network.API.Builder.ErrorSpec where++import Data.Semigroup ((<>))+import Network.API.Builder.Error++import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck++main :: IO ()+main = hspec spec++instance Arbitrary a => Arbitrary (APIError a) where+ arbitrary = oneof+ [ APIError <$> arbitrary+ , pure InvalidURLError+ , ParseError <$> arbitrary+ , pure EmptyError+ ]++spec :: Spec+spec =+ describe "APIError" $ do+ describe "Eq" $ do+ it "should check for equality properly" $ do+ HTTPError undefined == (HTTPError undefined :: APIError ())+ `shouldBe` False+ APIError () `shouldBe` APIError ()+ InvalidURLError `shouldBe` (InvalidURLError :: APIError ())++ prop "ParseError s == ParseError s" $ \s ->+ ParseError s == (ParseError s :: APIError ())++ prop "APIError x == APIError x" $ \(x :: String) ->+ APIError x == APIError x++ describe "Semigroup" $ do+ prop "(x <> y) <> z == x <> (y <> z)" $ \(x :: APIError ()) y z ->+ (x <> y) <> z == x <> (y <> z)
+ test/Network/API/Builder/QuerySpec.hs view
@@ -0,0 +1,33 @@+module Network.API.Builder.QuerySpec where++import Network.API.Builder.Query++import Test.Hspec+import Test.Hspec.QuickCheck++main :: IO ()+main = hspec spec++spec :: Spec+spec =+ describe "ToQuery" $ do++ it "can convert an Integer" $ do+ toQuery "integer" (5 :: Integer) `shouldBe`+ [("integer", "5")]++ it "can convert a Boolean" $ do+ toQuery "bool" False `shouldBe` [("bool", "false")]++ it "can convert a list of Ints" $ do+ toQuery "list" ([1,2,3,4,5] :: [Int]) `shouldBe` [("list", "1,2,3,4,5")]++ it "can convert Maybe values" $ do+ toQuery "maybe" (Just False) `shouldBe` [("maybe", "false")]+ toQuery "maybe" (Nothing :: Maybe Bool) `shouldBe` []++ prop "toQuery x y == toQuery x (Just y)" $ \(y :: Int) ->+ toQuery "x" y == toQuery "x" (Just y)++skip :: String -> Expectation -> Expectation+skip _ _ = return ()
+ test/Network/API/Builder/RoutesSpec.hs view
@@ -0,0 +1,24 @@+module Network.API.Builder.RoutesSpec where++import Network.API.Builder.Routes++import Test.Hspec++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+ describe "routeURL" $ do+ it "should be able to create a basic url" $ do+ routeURL "" (Route [ "api", "index" ] [ ] "GET")+ `shouldBe` "/api/index"+ routeURL "" (Route [ "api", "index.json" ] [ ] "GET")+ `shouldBe` "/api/index.json"+ routeURL "" (Route [ ] [ "test" =. False ] "GET")+ `shouldBe` "?test=false"+ routeURL "" (Route [ "about.json" ] [ "test" =. True ] "GET")+ `shouldBe` "/about.json?test=true"+ routeURL "" (Route [ "about.json" ] [ "test" =. True ] "GET")+ `shouldBe` "/about.json?test=true"+
+ test/Network/API/Builder/SendSpec.hs view
@@ -0,0 +1,100 @@+module Network.API.Builder.SendSpec where++import Network.API.Builder.Builder+import Network.API.Builder.Routes+import Network.API.Builder.Send++import Data.Aeson+import Data.ByteString.Lazy (ByteString)+import qualified Network.HTTP.Client as HTTP+import Test.Hspec++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+ describe "Sendable" $ do++ describe "()" $ do+ it "should be able to construct a basic request" $ do+ case send exampleBuilder (Route ["api.json"] [] "GET") () of+ Just req -> do+ HTTP.secure req `shouldBe` True+ HTTP.host req `shouldBe` "example.com"+ HTTP.method req `shouldBe` "GET"+ HTTP.port req `shouldBe` 443+ HTTP.queryString req `shouldBe` ""+ HTTP.path req `shouldBe` "/api.json"+ Nothing -> expectationFailure "req construction failed"+ it "should be able to construct request with query params" $ do+ case send exampleBuilder (Route ["api.json"] ["a_query" =. True] "GET") () of+ Just req -> do+ HTTP.secure req `shouldBe` True+ HTTP.host req `shouldBe` "example.com"+ HTTP.method req `shouldBe` "GET"+ HTTP.port req `shouldBe` 443+ HTTP.queryString req `shouldBe` "?a_query=true"+ HTTP.path req `shouldBe` "/api.json"+ Nothing -> expectationFailure "req construction failed"+ it "should be able to construct a POST request" $ do+ case send exampleBuilder (Route ["api.json"] ["query" =. False] "POST") () of+ Just req -> do+ HTTP.secure req `shouldBe` True+ HTTP.host req `shouldBe` "example.com"+ HTTP.method req `shouldBe` "POST"+ HTTP.port req `shouldBe` 443+ HTTP.queryString req `shouldBe` ""+ HTTP.path req `shouldBe` "/api.json"+ case HTTP.requestBody req of+ HTTP.RequestBodyBS "query=false" -> return ()+ _ -> expectationFailure "incorrect POST body"+ Nothing -> expectationFailure "req construction failed"++ describe "PostQuery" $ do+ it "should be able to construct a POST request with no body" $ do+ case send exampleBuilder (Route ["api.json"] ["query" =. False] "POST") PostQuery of+ Just req -> do+ HTTP.secure req `shouldBe` True+ HTTP.host req `shouldBe` "example.com"+ HTTP.method req `shouldBe` "POST"+ HTTP.port req `shouldBe` 443+ HTTP.queryString req `shouldBe` "?query=false"+ HTTP.path req `shouldBe` "/api.json"+ case HTTP.requestBody req of+ HTTP.RequestBodyLBS "" -> return ()+ _ -> expectationFailure "incorrect POST body"+ Nothing -> expectationFailure "req construction failed"++ describe "Value" $ do+ it "should be able to construct a POST request with the correct JSON body" $ do+ case send exampleBuilder (Route ["api.json"] ["query" =. False] "POST") $ object ["hello" .= Null] of+ Just req -> do+ HTTP.secure req `shouldBe` True+ HTTP.host req `shouldBe` "example.com"+ HTTP.method req `shouldBe` "POST"+ HTTP.port req `shouldBe` 443+ HTTP.queryString req `shouldBe` "?query=false"+ HTTP.path req `shouldBe` "/api.json"+ case HTTP.requestBody req of+ HTTP.RequestBodyLBS "{\"hello\":null}" -> return ()+ _ -> expectationFailure "incorrect POST body"+ Nothing -> expectationFailure "req construction failed"++ describe "ByteString" $ do+ it "should be able to construct a POST request with the correct ByteString body" $ do+ case send exampleBuilder (Route ["api.json"] ["query" =. False] "POST") ("hello world" :: ByteString) of+ Just req -> do+ HTTP.secure req `shouldBe` True+ HTTP.host req `shouldBe` "example.com"+ HTTP.method req `shouldBe` "POST"+ HTTP.port req `shouldBe` 443+ HTTP.queryString req `shouldBe` "?query=false"+ HTTP.path req `shouldBe` "/api.json"+ case HTTP.requestBody req of+ HTTP.RequestBodyLBS "hello world" -> return ()+ _ -> expectationFailure "incorrect POST body"+ Nothing -> expectationFailure "req construction failed"++exampleBuilder :: Builder+exampleBuilder = basicBuilder "example" "https://example.com"