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 +1/−1
- src/APIBuilder.hs +2/−0
- src/APIBuilder/API.hs +38/−8
- src/APIBuilder/Builder.hs +7/−1
- src/APIBuilder/Decoding.hs +7/−0
- src/APIBuilder/Error.hs +11/−4
- src/APIBuilder/Examples/StackOverflow.hs +2/−0
- src/APIBuilder/Routes.hs +28/−5
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