packages feed

api-builder (empty) → 0.1.0.0

raw patch · 9 files changed

+263/−0 lines, 9 filesdep +HTTPdep +aesondep +attoparsecsetup-changed

Dependencies added: HTTP, aeson, attoparsec, base, bytestring, either, http-conduit, text, transformers

Files

+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ api-builder.cabal view
@@ -0,0 +1,34 @@+name: api-builder+version: 0.1.0.0+synopsis: Library for easily building REST API wrappers in Haskell+license: BSD3+author: Fraser Murray+maintainer: fraser.m.murray@gmail.com+copyright: (c) Fraser Murray 2014+category: Network+build-type: Simple+cabal-version: >= 1.10++library+  exposed-modules: +    APIBuilder+    APIBuilder.API+    APIBuilder.Builder+    APIBuilder.Decoding+    APIBuilder.Error+    APIBuilder.Examples.StackOverflow+    APIBuilder.Routes+  build-depends: +    base >= 4.6 && < 4.8,+    aeson == 0.7.*,+    attoparsec == 0.11.*,+    bytestring == 0.10.*,+    either == 4.*,+    HTTP == 4000.*,+    http-conduit == 2.*,+    text == 1.*,+    transformers == 0.3.*+  hs-source-dirs: src/+  default-language: Haskell2010+  default-extensions: OverloadedStrings, RankNTypes+  ghc-options: -Wall
+ src/APIBuilder.hs view
@@ -0,0 +1,8 @@+module APIBuilder +  ( module Exports ) where++import APIBuilder.API as Exports+import APIBuilder.Builder as Exports+import APIBuilder.Decoding as Exports+import APIBuilder.Error as Exports+import APIBuilder.Routes as Exports
+ src/APIBuilder/API.hs view
@@ -0,0 +1,74 @@+module APIBuilder.API+  ( API +  , liftEither+  , liftBuilder+  , liftState+  , runAPI+  , runRoute+  , routeRequest+  , name+  , baseURL+  , customizeRoute+  , customizeRequest ) where++import APIBuilder.Builder+import APIBuilder.Decoding+import APIBuilder.Error+import APIBuilder.Routes++import Control.Exception+import Control.Monad.Trans.Either+import Control.Monad.Trans.State+import Control.Monad.Trans.Class (lift)+import Control.Monad.IO.Class (liftIO)+import Data.Aeson (FromJSON)+import Data.Text (Text)+import qualified Data.ByteString.Char8 as BS+import qualified Data.Text as T+import Network.HTTP.Conduit++type API s e a = EitherT (APIError e) (StateT Builder (StateT s IO)) a++liftEither :: API s e a -> API s e a+liftEither = id ++liftBuilder :: StateT Builder (StateT s IO) a -> API s e a+liftBuilder = lift++liftState :: StateT s IO a -> API s e a+liftState = lift . lift++runAPI :: Builder -> s -> API s e a -> IO (Either (APIError e) a)+runAPI b s api = evalStateT (evalStateT (runEitherT api) b) s++runRoute :: (FromJSON a, FromJSON e) => Route -> API s e a+runRoute route = do+  b <- liftBuilder get+  req <- hoistEither $ routeRequest b route `eitherOr` InvalidURLError+  resp <- do+    r <- liftIO $ try $ withManager (httpLbs req)+    hoistEither $ either (Left . HTTPError) Right r +  hoistEither $ decode $ responseBody resp++eitherOr :: Maybe a -> b -> Either b a+a `eitherOr` b =+  case a of +    Just x -> Right x+    Nothing -> Left b++routeRequest :: Builder -> Route -> Maybe Request+routeRequest b route = +  let initialURL = parseUrl (T.unpack $ routeURL (_baseURL b) (_customizeRoute b route)) in+  fmap (\url -> _customizeRequest b $ url { method = BS.pack (showMethod $ httpMethod route) }) initialURL++name :: Text -> API s e ()+name t = liftBuilder $ modify (\b -> b { _name = t })++baseURL :: Text -> API s e ()+baseURL t = liftBuilder $ modify (\b -> b { _baseURL = t })++customizeRoute :: (Route -> Route) -> API s e ()+customizeRoute f = liftBuilder $ modify (\b -> b { _customizeRoute = f })++customizeRequest :: (Request -> Request) -> API s e ()+customizeRequest f = liftBuilder $ modify (\b -> b { _customizeRequest = f })
+ src/APIBuilder/Builder.hs view
@@ -0,0 +1,20 @@+module APIBuilder.Builder+  ( Builder(..)+  , basicBuilder ) where++import APIBuilder.Routes++import Network.HTTP.Conduit (Request)+import Data.Text (Text)+import qualified Data.Text as T++data Builder = Builder { _name :: Text+                       , _baseURL :: Text+                       , _customizeRoute :: Route -> Route+                       , _customizeRequest :: Request -> Request }++instance Show Builder where+  show b = "Builder { name = " ++ T.unpack (_name b) ++ "}"++basicBuilder :: Text -> Text -> Builder+basicBuilder n b = Builder n b id id 
+ src/APIBuilder/Decoding.hs view
@@ -0,0 +1,22 @@+module APIBuilder.Decoding+  ( decode ) where++import APIBuilder.Error++import Data.Aeson.Types (parseEither)+import Data.Aeson.Parser (value)+import Data.Aeson (FromJSON, parseJSON)+import Data.Attoparsec.Lazy (parse, eitherResult)+import qualified Data.ByteString.Lazy as BS++decode :: (FromJSON a, FromJSON e) => BS.ByteString -> Either (APIError e) a+decode s = +  case eitherDecode s of+    Right x -> Right x+    Left err ->+      case eitherDecode s of +        Right x -> Left $ APIError x+        Left _ -> Left $ ParseError err++eitherDecode :: (FromJSON a) => BS.ByteString -> Either String a+eitherDecode s = eitherResult (parse value s) >>= parseEither parseJSON
+ src/APIBuilder/Error.hs view
@@ -0,0 +1,10 @@+module APIBuilder.Error+  ( APIError(..) ) where++import Network.HTTP.Conduit (HttpException)++data APIError a = APIError a+                | HTTPError HttpException+                | InvalidURLError+                | ParseError String+  deriving Show
+ src/APIBuilder/Examples/StackOverflow.hs view
@@ -0,0 +1,41 @@+module APIBuilder.Examples.StackOverflow where++import APIBuilder+import Control.Applicative+import Data.Aeson+import Data.Monoid (mempty)+import Data.Text (Text)++data Question = Question { title :: Text+                         , isAnswered :: Bool+                         , score :: Int+                         , tags :: [Text] }+  deriving (Show, Eq)++newtype Questions = Questions [Question]+  deriving (Show, Eq)++instance FromJSON Question where+  parseJSON (Object o) =+    Question <$> o .: "title"+             <*> o .: "is_answered"+             <*> o .: "score"+             <*> o .: "tags"+  parseJSON _ = mempty++instance FromJSON Questions where+  parseJSON (Object o) = Questions <$> o .: "items"+  parseJSON _ = mempty++stackOverflow :: Builder+stackOverflow = basicBuilder "StackOverflow API" "http://api.stackexchange.com"++answersRoute :: Route+answersRoute = Route { fragments = [ "2.2", "questions" ]+                     , urlParams = [ "order" =. Just "desc"+                                   , "sort" =. Just "activity"+                                   , "site" =. Just "stackoverflow" ]+                     , httpMethod = GET }++getAnswers :: IO (Either (APIError ()) Questions)+getAnswers = runAPI stackOverflow () $ runRoute answersRoute
+ src/APIBuilder/Routes.hs view
@@ -0,0 +1,52 @@+module APIBuilder.Routes+  ( URLFragment+  , URLParam+  , Route(..)+  , HTTPMethod(..)+  , showMethod+  , routeURL+  , (=.) ) where++import Control.Arrow ((***))+import Data.Maybe (mapMaybe)+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import qualified Network.HTTP.Base as HTTP (urlEncodeVars)++type URLFragment = Text+type URLParam = (Text, Maybe Text)++(=.) :: Text -> Maybe Text -> (Text, Maybe Text)+(=.) = (,)++data Route = Route { fragments :: [URLFragment]+                   , urlParams :: [URLParam]+                   , httpMethod :: HTTPMethod }+  deriving (Show, Read, Eq)++data HTTPMethod = GET+                | POST+                | CustomMethod Text+  deriving (Show, Read, Eq)++showMethod :: HTTPMethod -> String+showMethod GET = "GET"+showMethod POST = "POST"+showMethod (CustomMethod t) = T.unpack t++routeURL :: Text -> Route -> Text+routeURL baseURL (Route fs ps _) =+  let path = T.intercalate "/" fs+  in baseURL <> "/" <> path <> pathParamsSep fs <> buildParams ps++pathParamsSep :: [URLFragment] -> Text+pathParamsSep [] = "?"+pathParamsSep xs = if T.isInfixOf "." (last xs) then "?" else "/?"++buildParams :: [URLParam] -> Text+buildParams = T.pack . HTTP.urlEncodeVars . map (T.unpack *** T.unpack) . mapMaybe collectParams++collectParams :: URLParam -> Maybe (Text, Text)+collectParams (a, Just x) = Just (a,x)+collectParams (_, Nothing) = Nothing