packages feed

api-builder 0.1.0.0 → 0.1.0.1

raw patch · 8 files changed

+96/−19 lines, 8 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- APIBuilder.API: liftEither :: API s e a -> API s e a
+ APIBuilder.API: liftEither :: EitherT (APIError e) (StateT Builder (StateT s IO)) a -> API s e a

Files

api-builder.cabal view
@@ -1,5 +1,5 @@ name: api-builder-version: 0.1.0.0+version: 0.1.0.1 synopsis: Library for easily building REST API wrappers in Haskell license: BSD3 author: Fraser Murray
src/APIBuilder.hs view
@@ -1,3 +1,5 @@+-- | Exports a bunch of stuff. Check 'APIBuilder.API', 'APIBuilder.Builder',+--   'APIBuilder.Decoding', 'APIBuilder.Error', and 'APIBuilder.Routes'. module APIBuilder    ( module Exports ) where 
src/APIBuilder/API.hs view
@@ -1,11 +1,15 @@-module APIBuilder.API-  ( API -  , liftEither-  , liftBuilder-  , liftState+module APIBuilder.API (+  -- * API+    API +  -- ** Running the API   , runAPI   , runRoute   , routeRequest+  -- ** Lifting+  , liftEither+  , liftBuilder+  , liftState+  -- ** Changing the @Builder@ within the API   , name   , baseURL   , customizeRoute@@ -27,20 +31,35 @@ import qualified Data.Text as T import Network.HTTP.Conduit +-- | Main API type. @s@ is the API's internal state, @e@ is the API's custom error type,+--   and @a@ is the result when the API runs. 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 +-- | Lifts an action that works on an @API@ to an action that works on an @API@.+--   This function is provided solely for future-proofing in the case that more transformers+--   need to be stacked on top - it's implemented simply as @id@ for the moment.+liftEither :: EitherT (APIError e) (StateT Builder (StateT s IO)) a -> API s e a+liftEither = id +-- | Lifts an action that operates on a @Builder@ to one that works on an @API@. Useful+--   mainly for gaining access to a @Builder@ from inside an @API@. liftBuilder :: StateT Builder (StateT s IO) a -> API s e a liftBuilder = lift +-- | Lifts an action on an @API@'s state type @s@ to one that works on the @API@. Good+--   for messing with the state from inside the @API@.  liftState :: StateT s IO a -> API s e a liftState = lift . lift -runAPI :: Builder -> s -> API s e a -> IO (Either (APIError e) a)+-- | Runs an @API@ by executing its transformer stack and dumping it all into @IO@.+runAPI :: Builder -- ^ initial @Builder@ for the @API@ +       -> s -- ^ initial state @s@ for the @API@+       -> API s e a -- ^ the actual @API@ to run+       -> IO (Either (APIError e) a) -- ^ IO action that returns either an error or the result runAPI b s api = evalStateT (evalStateT (runEitherT api) b) s +-- | Runs a @Route@. Infers the type to convert to from the JSON with the @a@ in @API@,+--   and infers the error type from @e@. runRoute :: (FromJSON a, FromJSON e) => Route -> API s e a runRoute route = do   b <- liftBuilder get@@ -56,19 +75,30 @@     Just x -> Right x     Nothing -> Left b +-- | Try to construct a @Request@ from a @Route@ (with the help of the @Builder@). Returns @Nothing@ if+--   the URL is invalid or there is another error with the @Route@. 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 +-- | Modify the @name@ of the @Builder@ from inside an API. Using this is probably not the best idea,+--   it's nice if the @Builder@'s name is stable at least. name :: Text -> API s e () name t = liftBuilder $ modify (\b -> b { _name = t }) +-- | Modify the @baseURL@ of the @Builder@ from inside an API.+--   Can be useful for changing the API's endpoints for certain requests. baseURL :: Text -> API s e () baseURL t = liftBuilder $ modify (\b -> b { _baseURL = t }) +-- | Modify every @Route@ before it runs. Useful for adding extra params to every query,+--   for example. customizeRoute :: (Route -> Route) -> API s e () customizeRoute f = liftBuilder $ modify (\b -> b { _customizeRoute = f }) ++-- | Modify every @Request@ before the API fetches it. Useful for adding headers to every request,+--   for example. customizeRequest :: (Request -> Request) -> API s e () customizeRequest f = liftBuilder $ modify (\b -> b { _customizeRequest = f })
src/APIBuilder/Builder.hs view
@@ -8,6 +8,8 @@ import Data.Text (Text) import qualified Data.Text as T +-- | Builder type for the API. Keeps track of the API's name and base URL, and how+--   to modify Routes and Requests before they're run. data Builder = Builder { _name :: Text                        , _baseURL :: Text                        , _customizeRoute :: Route -> Route@@ -16,5 +18,9 @@ instance Show Builder where   show b = "Builder { name = " ++ T.unpack (_name b) ++ "}" -basicBuilder :: Text -> Text -> Builder+-- | Makes a basic builder, i.e. one that simply has a name and base URL+--   and doesn't fiddle with Routes / Requests.+basicBuilder :: Text -- ^ name+             -> Text -- ^ base url+             -> Builder -- ^ a simple @Builder@ basicBuilder n b = Builder n b id id 
src/APIBuilder/Decoding.hs view
@@ -9,6 +9,9 @@ import Data.Attoparsec.Lazy (parse, eitherResult) import qualified Data.ByteString.Lazy as BS +-- | Try to parse a value from a JSON @ByteString@, and if not, try to+--   parse a useful error from the JSON. If we can't do that, complain about+--   a @ParseError@. decode :: (FromJSON a, FromJSON e) => BS.ByteString -> Either (APIError e) a decode s =    case eitherDecode s of@@ -18,5 +21,9 @@         Right x -> Left $ APIError x         Left _ -> Left $ ParseError err +-- | @Data.Aeson@'s @decode@ function will only parse a value if it's +--   an array or an object, in accordance with some RFC somewhere. Unfortunately+--   not all JSON APIs respect this, so we have to mess with parsers and values and +--   whatnot. eitherDecode :: (FromJSON a) => BS.ByteString -> Either String a eitherDecode s = eitherResult (parse value s) >>= parseEither parseJSON
src/APIBuilder/Error.hs view
@@ -3,8 +3,15 @@  import Network.HTTP.Conduit (HttpException) -data APIError a = APIError a-                | HTTPError HttpException-                | InvalidURLError-                | ParseError String+-- | Error type for the @API@, where @a@ is the type that should be returned when+--   something goes wrong on the other end - i.e. any error that isn't directly related+--   to this library.+data APIError a = APIError a -- ^ A type that represents any error that happens on the API end.+                             --   Define your own custom type with a @FromJSON@ instance if you+                             --   want to handle them, or you can use @()@ if you just want to+                             --   ignore them all.+                | HTTPError HttpException -- ^ Something went wrong when we tried to do a HTTP operation.+                | InvalidURLError -- ^ You're trying to create an invalid URL somewhere - check your+                                  --   @Builder@'s base URL and your @Route@s.+                | ParseError String -- ^ Failed when parsing the response, and it wasn't an error on their end.   deriving Show
src/APIBuilder/Examples/StackOverflow.hs view
@@ -1,3 +1,5 @@+-- | Defines a basic example of API use - check the readme for more detail+--   or check the tutorial at <https://github.com/intolerable/api-builder> module APIBuilder.Examples.StackOverflow where  import APIBuilder
src/APIBuilder/Routes.hs view
@@ -1,11 +1,11 @@ module APIBuilder.Routes-  ( URLFragment+  ( Route(..)+  , URLFragment   , URLParam-  , Route(..)+  , (=.)   , HTTPMethod(..)   , showMethod-  , routeURL-  , (=.) ) where+  , routeURL ) where  import Control.Arrow ((***)) import Data.Maybe (mapMaybe)@@ -14,28 +14,51 @@ import qualified Data.Text as T import qualified Network.HTTP.Base as HTTP (urlEncodeVars) +-- | Alias for @Text@ to store the URL fragments for each @Route@. type URLFragment = Text++-- | Alias to @(Text, Maybe Text)@ used to store each query that gets+--   tacked onto the request. type URLParam = (Text, Maybe Text) +-- | Convenience function for building @URLParam@s.+-- +-- >>> "api_type" =. Just "json"+-- ("api_type", Just "json") (=.) :: Text -> Maybe Text -> (Text, Maybe Text) (=.) = (,) +-- | Main type for routes in the API. Used to represent the URL minus the actual +--   endpoint URL as well as the query string and the HTTP method used to communicate+--   with the server. data Route = Route { fragments :: [URLFragment]                    , urlParams :: [URLParam]                    , httpMethod :: HTTPMethod }   deriving (Show, Read, Eq) +-- | Type for different HTTP request methods. Has the two most common ones (GET and POST)+--   as well as support for custom methods. data HTTPMethod = GET                 | POST                 | CustomMethod Text   deriving (Show, Read, Eq) +-- | Get a @String@ from a HTTPMethod. Used mostly for creating @Request@s.+--+-- >>> showMethod GET+-- "GET"+-- >>> showMethod (CustomMethod "PATCH")+-- "PATCH" showMethod :: HTTPMethod -> String showMethod GET = "GET" showMethod POST = "POST" showMethod (CustomMethod t) = T.unpack t -routeURL :: Text -> Route -> Text+-- | Converts a Route to a URL. Drops any @Nothing@ values from the query, separates the+--   fragments with "/" and tacks them onto the end of the base URL.+routeURL :: Text -- ^ base URL for the @Route@ (you can usually get this from the @Builder@)+         -> Route -- ^ the @Route@ to process+         -> Text -- ^ the finalized URL as a @Text@ routeURL baseURL (Route fs ps _) =   let path = T.intercalate "/" fs   in baseURL <> "/" <> path <> pathParamsSep fs <> buildParams ps