packages feed

octohat (empty) → 0.1

raw patch · 10 files changed

+600/−0 lines, 10 filesdep +aesondep +basedep +base-compatsetup-changed

Dependencies added: aeson, base, base-compat, base16-bytestring, base64-bytestring, bytestring, containers, cryptohash, dotenv, either, errors, hspec, hspec-expectations, http-client, http-types, lens, mtl, octohat, text, time, transformers, unordered-containers, wreq, xmlhtml

Files

+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ octohat.cabal view
@@ -0,0 +1,63 @@+Name:                octohat+Version:             0.1+Synopsis:            A tested, minimal wrapper around GitHub's API. Very incomplete at the moment+License:             MIT+Author:              Stack Builders+Maintainer:          hackage@stackbuilders.com+Stability:           Experimental+Category:            Network+Build-type:          Simple+Cabal-version:       >=1.10++Library+  hs-source-dirs: src++  Build-depends:+      aeson                == 0.8.*+    , base                 >= 4.5 && < 4.8+    , base-compat          == 0.5.*+    , base16-bytestring    == 0.1.1.*+    , base64-bytestring    == 1.0.*+    , bytestring           >= 0.9+    , containers           >= 0.4+    , cryptohash           == 0.11.*+    , dotenv               == 0.1.*+    , either               == 4.3.*+    , errors               == 1.4.*+    , http-client          == 0.4.*+    , http-types           == 0.8.*+    , lens                 >= 4.0 && <= 4.7+    , mtl                  == 2.*+    , text                 == 1.2.*+    , time                 == 1.4.*+    , transformers         == 0.3.*+    , unordered-containers == 0.2.*+    , wreq                 == 0.3.*+    , xmlhtml              == 0.2.*++  ghc-options:       -Wall+  exposed-modules:   Network.Octohat.Types+                   , Network.Octohat.Members+                   , Network.Octohat.Keys+                   , Network.Octohat++  other-modules:     Network.Octohat.Internal++  default-language: Haskell2010++test-suite spec+  ghc-options     :   -Wall+  type            :   exitcode-stdio-1.0+  hs-source-dirs  :   spec+  main-is         :   Spec.hs+  build-depends   :   base               >= 4.5 && <4.8+                    , base-compat        == 0.5.*+                    , hspec              == 2.1.*+                    , hspec-expectations == 0.6.*+                    , text               == 1.2.*+                    , dotenv             == 0.1.*+                    , transformers       == 0.3.*+                    , octohat+  other-modules:    Network.Octohat.TestData,+                    Network.Octohat.TestUtil+  default-language:   Haskell2010
+ spec/Network/Octohat/TestData.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.Octohat.TestData ( loadTestOrganizationName+                                , loadOwnerTeam+                                , loadTestAccountOne+                                , loadTestAccountTwo+                                , loadTestAccountThree+                                , publicKeyFixture+                                , publicKeyHostnameFixture+                                , fingerprintFixture+                                , fullPublicKeyFixture+                                ) where++import Network.Octohat.Types+import Network.Octohat (teamForTeamNameInOrg)+import Network.Octohat.Members (userForUsername)++import Control.Monad.IO.Class (liftIO)+import Control.Arrow (second)+import System.Environment.Compat (getEnvironment)+import Configuration.Dotenv (loadFile)+import Control.Applicative ((<$>), (<*>))+import qualified Data.Text as T++data TestEnvironment =+  TestEnvironment {+    organization :: T.Text,+    accountOne   :: T.Text,+    accountTwo   :: T.Text,+    accountThree :: T.Text+  }++readEnv :: [(String, String)] -> Maybe TestEnvironment+readEnv environment =+  TestEnvironment <$> lookup "SANDBOX_ORGANIZATION" env+                  <*> lookup "TEST_ACCOUNT_ONE" env+                  <*> lookup "TEST_ACCOUNT_TWO" env+                  <*> lookup "TEST_ACCOUNT_THREE" env+    where env = map (second T.pack) environment++loadEnv :: IO TestEnvironment+loadEnv = do+  loadFile False ".github-sandbox"+  env <- getEnvironment+  case readEnv env of+    Just res -> return res+    Nothing  -> fail "Environment variables not set correctly, please read README.md"++loadTestOrganizationName :: IO T.Text+loadTestOrganizationName = organization `fmap` loadEnv++loadOwnerTeam :: GitHub Team+loadOwnerTeam = liftIO (organization `fmap` loadEnv) >>= teamForTeamNameInOrg "Owners"++loadTestAccountOne :: GitHub Member+loadTestAccountOne = liftIO (accountOne `fmap` loadEnv) >>= userForUsername++loadTestAccountTwo :: GitHub Member+loadTestAccountTwo = liftIO (accountTwo `fmap` loadEnv) >>= userForUsername++loadTestAccountThree :: GitHub Member+loadTestAccountThree = liftIO (accountThree `fmap` loadEnv) >>= userForUsername++publicKeyHostnameFixture :: T.Text+publicKeyHostnameFixture = "octohat@stackbuilders"++fingerprintFixture :: T.Text+fingerprintFixture = "42:59:20:02:6f:df:b4:4a:1c:0e:fd:1b:86:58:f6:06"++publicKeyFixture :: T.Text+publicKeyFixture = "AAAAB3NzaC1yc2EAAAADAQABAAABAQC1Dopc3yxLWlzJwFqSoj0nAzRCU93R5DwNlogtRr/7NsnUVf443wl/vpRDRNscR0dV/VeNWYCqiZA0wGrXiVJ7HYi9XaWtHrUutLqrLe47aFFvAIdp15+RHkM0sXr963Kb9XMkmqswyXJ2TaZ0cgZfMNgl1ND248Y8fMDBx8elHwdZvyG2onG5aSVtOuKB4dWnmIb+uSQCN1K2kLYwHvQOjmqCiZ2XOP9u+ScphVdp6x4uAczH67CCRSUhI6U2fxSNf6YaDXyCWcqxj1agHUKdskb5rzxPaz5XZ2BgQscjoVo93M338HLmkyvbuP4yl2X6ZdLfE5mk2ZFfWQogxLGd"++fullPublicKeyFixture :: T.Text+fullPublicKeyFixture = T.concat ["ssh-rsa ", publicKeyFixture, " ", publicKeyHostnameFixture]
+ spec/Network/Octohat/TestUtil.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.Octohat.TestUtil (findUserId, setupToken, removeTeams) where+++import Network.Octohat.Members+import Network.Octohat.Types+import Network.Octohat.TestData++import Control.Monad.IO.Class (liftIO)+import Configuration.Dotenv (loadFile)+import qualified Data.Text as T++findUserId :: T.Text -> GitHub Integer+findUserId username = memberId `fmap` userForUsername username++deleteAllTeams :: GitHub [DidDelete]+deleteAllTeams = do+  testOrganization <- liftIO loadTestOrganizationName+  allTeams <- teamsForOrganization testOrganization+  let teamsToDelete = map teamId $ filter (\t -> teamName t /= "Owners") allTeams+  mapM deleteTeamFromOrganization teamsToDelete++setupToken :: IO ()+setupToken = do+  loadFile False ".github-sandbox"+  result <- runGitHub deleteAllTeams+  case result of+    Left  _ -> fail "Clean-up failed"+    Right _ -> return ()++removeTeams :: () -> IO ()+removeTeams _ = do+  result <- runGitHub deleteAllTeams+  case result of+    Left  _ -> fail "Clean-up failed"+    Right _ -> return ()
+ spec/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ src/Network/Octohat.hs view
@@ -0,0 +1,62 @@+-- | Convenience functions for some common operations with teams. Execute the result of these+--   functions using 'runGitHub' or 'runGitHub''++module Network.Octohat ( addUserToTeam+                       , membersOfTeamInOrganization+                       , keysOfTeamInOrganization+                       , teamForTeamNameInOrg) where++import Control.Error.Safe (tryHead)+import Control.Monad (liftM)+import qualified Data.Text as T++import Network.Octohat.Keys+import Network.Octohat.Members+import Network.Octohat.Types+++-- | Gets all the members of the organization+membersOfTeamInOrganization :: T.Text -- ^ GitHub organization name+                            -> T.Text -- ^ GitHub team name+                            -> GitHub [Member]+membersOfTeamInOrganization nameOfOrg nameOfTeam = teamId `liftM` teamForTeamNameInOrg nameOfOrg nameOfTeam >>= membersForTeam++-- | Adds a user with @nameOfUser@ to the team named @nameOfTeam@ within the organization named `nameOfOrg`+addUserToTeam :: T.Text -- ^ GitHub username+              -> T.Text -- ^ GitHub organization name+              -> T.Text -- ^ GitHub team name+              -> GitHub StatusInTeam+addUserToTeam nameOfUser nameOfOrg nameOfTeam = teamId `liftM` teamForTeamNameInOrg nameOfOrg nameOfTeam >>= addMemberToTeam nameOfUser++-- | Retrieves a list of members in a given team within an organization together with their public keys+keysOfTeamInOrganization :: T.Text -- ^ GitHub organization name+                         -> T.Text -- ^ GitHub team name+                         -> GitHub [MemberWithKey]+keysOfTeamInOrganization nameOfOrg nameOfTeam = do+  members <- membersOfTeamInOrganization nameOfOrg nameOfTeam+  pubKeys <- mapM keysForMember members+  let memberFingerprints = publicKeySetToFingerprints pubKeys+  return $ makeMembersWithKey members pubKeys memberFingerprints++teamForTeamNameInOrg :: T.Text -- ^ Team name+                     -> T.Text -- ^ Organization name+                     -> GitHub Team+teamForTeamNameInOrg nameOfTeam nameOfOrg = do+  teams <- teamsForOrganization nameOfOrg+  tryHead NotFound (teamsWithName nameOfTeam teams)++teamsWithName :: T.Text -> [Team] -> [Team]+teamsWithName nameOfTeam = filter (hasName nameOfTeam)++hasName :: T.Text -> Team -> Bool+hasName name team = name == teamName team+keysForMember :: Member -> GitHub [PublicKey]+keysForMember = publicKeysForUser . memberLogin++makeMembersWithKey :: [Member] -> [[PublicKey]] -> [[PublicKeyFingerprint]] -> [MemberWithKey]+makeMembersWithKey = zipWith3 MemberWithKey++publicKeySetToFingerprints :: [[PublicKey]] -> [[PublicKeyFingerprint]]+publicKeySetToFingerprints = (map.map) fingerprintFor++
+ src/Network/Octohat/Internal.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns      #-}++module Network.Octohat.Internal+  ( putRequestTo+  , getRequestTo+  , postRequestTo+  , deleteRequestTo+  , composeEndpoint) where++import Control.Error.Safe+import Control.Lens (set, view)+import Control.Monad.Reader+import Data.Aeson+import Data.List+import Data.Text.Encoding (encodeUtf8)+import Network.Wreq+import qualified Network.Wreq.Types as WT+import qualified Data.ByteString.Lazy as BSL+import qualified Data.Text as T++import Network.Octohat.Types++composeEndpoint :: [T.Text] -> T.Text+composeEndpoint pathChunks = T.concat $ intersperse "/" ("https://api.github.com" : pathChunks)++getResponseEntity :: FromJSON a => Response BSL.ByteString -> Either GitHubReturnStatus a+getResponseEntity resp =+  case eitherDecode (view responseBody resp) of+    Left errorMessage -> Left (UnexpectedJSON errorMessage)+    Right decoded     -> Right decoded++requestOptions :: GitHub Options+requestOptions = do+  bearerToken <- ask+  let opts   = set auth (Just $ oauth2Bearer (encodeUtf8 $ unBearerToken bearerToken)) defaults+  let opts'  = set checkStatus (Just (\_ _ _ -> Nothing)) opts+  let opts'' = set (header "User-Agent") ["octohat v0.1"] opts'+  return opts''+++postRequestTo :: (ToJSON b, WT.Postable b, FromJSON a) => T.Text -> b -> GitHub a+postRequestTo uri body = do+  opts     <- requestOptions+  response <- liftIO $ postWith opts (T.unpack uri) (toJSON body)+  checkForStatus response+  tryRight $ getResponseEntity response++getRequestTo :: FromJSON a => T.Text -> GitHub a+getRequestTo uri = do+  opts     <- requestOptions+  response <- liftIO $ getWith opts (T.unpack uri)+  checkForStatus response+  tryRight $ getResponseEntity response++putRequestTo :: FromJSON a => T.Text -> GitHub a+putRequestTo uri = do+  opts     <- requestOptions+  response <- liftIO $ putWith opts (T.unpack uri) EmptyBody+  checkForStatus response+  tryRight $ getResponseEntity response++deleteRequestTo :: T.Text -> GitHub DidDelete+deleteRequestTo uri = do+  opts     <- requestOptions+  response <- liftIO $ deleteWith opts (T.unpack uri)+  checkForStatus response+  return $ isDeleted (viewResponse response)++checkForStatus :: Response a -> GitHub ()+checkForStatus (viewResponse -> 404) = tryAssert NotFound False+checkForStatus (viewResponse -> 403) = tryAssert NotAllowed False+checkForStatus (viewResponse -> 401) = tryAssert RequiresAuthentication False+checkForStatus (viewResponse -> 422) = tryAssert ValidationFailed False+checkForStatus (viewResponse -> 500) = tryAssert InternalError False+checkForStatus (viewResponse -> 400) = tryAssert InvalidJSON False+checkForStatus (viewResponse -> _)   = tryAssert AllOk True++viewResponse :: Response a -> Int+viewResponse = view (responseStatus . statusCode)++isDeleted :: Int -> DidDelete+isDeleted 204 = Deleted+isDeleted 200 = Deleted+isDeleted _   = NotDeleted
+ src/Network/Octohat/Keys.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.Octohat.Keys (fingerprintFor) where++import qualified Data.List as L+import qualified Data.ByteString.Base64 as B64+import qualified Data.ByteString.Base16 as B16+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified Crypto.Hash.MD5 as MD5 -- eww++import Network.Octohat.Types++type RSAPublicKey = T.Text+type Fingerprint  = T.Text++-- | Computes a fingerprint from its Base 64 encoded representation. Assumes leading @ssh-rsa @ (mind the space)+--   prefix and no user@hostname suffix+fingerprintFor :: PublicKey -> PublicKeyFingerprint+fingerprintFor PublicKey{..} = PublicKeyFingerprint publicKeyId (digestToHex publicKey)++digestToHex :: RSAPublicKey -> Fingerprint+digestToHex = T.concat          .+              L.intersperse ":" .+              T.chunksOf 2      .+              T.toLower         .+              TE.decodeUtf8     .+              B16.encode        .+              MD5.hash          .+              B64.decodeLenient .+              TE.encodeUtf8     .+              T.replace "ssh-rsa " ""
+ src/Network/Octohat/Members.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Execute the result of these functions using 'runGitHub' or 'runGitHub''++module Network.Octohat.Members+  ( membersForOrganization+  , membersForTeam+  , teamsForOrganization+  , addMemberToTeam+  , deleteMemberFromTeam+  , deleteTeamFromOrganization+  , publicKeysForUser+  , addTeamToOrganization+  , userForUsername+  , addPublicKey+  ) where++import Network.Octohat.Internal+import Network.Octohat.Types++import qualified Data.Text as T+import Data.Monoid ((<>))++-- | Takes a new team name, the description of a team and the organization where to create the team+--   and creates a new team. Regular GitHub authorization/authentication applies.+addTeamToOrganization :: T.Text  -- ^ Name of new team+                      -> T.Text  -- ^ Description of new team+                      -> T.Text  -- ^ Organization name where the team will be created+                      -> GitHub Team+addTeamToOrganization nameOfNewTeam descOfTeam orgName =+  postRequestTo (composeEndpoint ["orgs", orgName, "teams"]) (TeamCreateRequest nameOfNewTeam descOfTeam)++-- | Deletes a team from an organization using its team ID.+deleteTeamFromOrganization :: Integer          -- ^ ID of Team to delete+                           -> GitHub DidDelete+deleteTeamFromOrganization idOfTeam = deleteRequestTo (composeEndpoint ["teams", T.pack $ show idOfTeam])++-- | Returns a list of members of an organization with the given name.+membersForOrganization :: T.Text          -- ^ The organization name+                       -> GitHub [Member]+membersForOrganization nameOfOrg = getRequestTo (composeEndpoint ["orgs", nameOfOrg, "members"])++-- | Returns a list of members of a team with the given team ID.+membersForTeam :: Integer         -- ^ The team ID+               -> GitHub [Member]+membersForTeam idOfTeam = getRequestTo (composeEndpoint ["teams", T.pack $ show idOfTeam, "members"])++-- | Returns a list of teams for the organization with the given name+teamsForOrganization :: T.Text        -- ^ The organization name+                     -> GitHub [Team]+teamsForOrganization nameOfOrg = getRequestTo (composeEndpoint ["orgs", nameOfOrg, "teams"])++-- | Adds a member to a team, might invite or add the member. Refer to 'StatusInTeam'+addMemberToTeam :: T.Text               -- ^ The GitHub username to add to a team+                -> Integer              -- ^ The Team ID+                -> GitHub StatusInTeam+addMemberToTeam nameOfUser idOfTeam =+  putRequestTo (composeEndpoint ["teams", T.pack $ show idOfTeam, "memberships", nameOfUser])++-- | Deletes a member with the given name from a team with the given ID. Might or might not delete+deleteMemberFromTeam :: T.Text            -- ^ GitHub username+                     -> Integer           -- ^ GitHub team ID+                     -> GitHub DidDelete+deleteMemberFromTeam nameOfUser idOfTeam =+  deleteRequestTo (composeEndpoint ["teams", T.pack $ show idOfTeam, "memberships", nameOfUser])++-- | Returns the public keys of the user with the given name+publicKeysForUser :: T.Text             -- ^ GitHub username+                  -> GitHub [PublicKey]+publicKeysForUser nameOfUser = getRequestTo (composeEndpoint ["users", nameOfUser, "keys"])++-- | Finds a user ID given their username+userForUsername :: T.Text        -- ^ GitHub username+                -> GitHub Member+userForUsername username = getRequestTo (composeEndpoint ["users", username])++-- | Add a key for the currently authenticated user+addPublicKey :: T.Text -- ^ Base64 RSA Key (ssh-rsa AA..)+             -> T.Text -- ^ Key title, e.g @octocat@stackbuilders@+             -> GitHub PublicKey+addPublicKey newKey newTitle =+  postRequestTo (composeEndpoint ["user", "keys"]) (AddPublicKeyRequest ("ssh-rsa " <> newKey) newTitle)
+ src/Network/Octohat/Types.hs view
@@ -0,0 +1,161 @@+{-# LANGUAGE TemplateHaskell   #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.Octohat.Types ( Member(..)+                             , MemberWithKey(..)+                             , Team(..)+                             , BearerToken(..)+                             , StatusInTeam(..)+                             , EmptyBody(..)+                             , DidDelete(..)+                             , PublicKey(..)+                             , PublicKeyFingerprint(..)+                             , TeamCreateRequest(..)+                             , GitHubReturnStatus(..)+                             , DidAddKey(..)+                             , AddPublicKeyRequest(..)+                             , runGitHub+                             , runGitHub'+                             , GitHub) where+import Control.Applicative+import Control.Monad.Reader (ReaderT(..))+import Control.Monad.Trans.Either+import Data.Aeson+import Data.Aeson.TH+import Data.Char (toLower)+import Network.HTTP.Client+import Network.Wreq.Types+import System.Environment.Compat (lookupEnv)+import qualified Data.HashMap.Strict as HS+import qualified Data.Text as T++-- | Represents a user in GitHub. Contains no more than login and user ID+data Member =+  Member { memberLogin :: T.Text+         , memberId    :: Integer+         } deriving (Show, Eq)++-- | Represents a team in GitHub. Contains the team's ID, the team's name and an optional description+data Team =+  Team { teamId          :: Integer+       , teamName        :: T.Text+       , teamDescription :: Maybe T.Text+       } deriving (Show, Eq)++-- | Represents a request to create a new team within an organization. The rest of the paramaters+--   are passed in the URL. Refer to <https://developer.github.com/v3/orgs/teams/#create-team>+data TeamCreateRequest =+  TeamCreateRequest { newTeamName        :: T.Text+                    , newTeamDescription :: T.Text+                    } deriving (Show, Eq)++-- | Represents a GitHub user with its public keys and fingerprints. A GitHub user might or might not+--   have any public keys+data MemberWithKey =+  MemberWithKey { member               :: Member+                , memberKey            :: [PublicKey]+                , memberKeyFingerprint :: [PublicKeyFingerprint]+                } deriving (Show, Eq)++-- | Represents a PublicKey within GitHub. It includes its ID and the public key encoded as base 64+data PublicKey =+  PublicKey { publicKeyId    :: Integer+            , publicKey      :: T.Text+            } deriving (Show, Eq)++-- | Represents a Fingerprint. The `fingerprintId` field should match the fingerprint's public key ID+--   within GitHub+data PublicKeyFingerprint =+  PublicKeyFingerprint { fingerprintId        :: Integer+                       , publicKeyFingerprint :: T.Text+                       } deriving (Show, Eq)++-- | Some Wreq functions expect a body, but often GitHub's API will request no body. The PUT verb+--   and its implementation in Wreq is an example of this.+data EmptyBody = EmptyBody deriving (Show, Eq)++-- | When adding a user to a team GitHub will add it immediately if the user already belongs to the+--   to the organization the team is in. Otherwise it will send an email for the user to accept the+--   request to join the team. Functions related adding or removing teams will return either Active+--   or Pending correspondingly.+data StatusInTeam = Active  | Pending deriving (Show, Eq)+++-- | Sum type to represent the success or failure of deletion of a resource within GitHub's API+data DidDelete = Deleted | NotDeleted deriving (Show, Eq)++instance FromJSON PublicKey where+  parseJSON (Object o) = PublicKey <$> o .: "id" <*> o .: "key"+  parseJSON _          = fail "Could not find public keys in document"++data DidAddKey = KeyAdded | KeyNotAdded++data AddPublicKeyRequest =+  AddPublicKeyRequest {+      addPublicKeyRequestKey   :: T.Text,+      addPublicKeyRequestTitle :: T.Text+  }++instance FromJSON StatusInTeam where+  parseJSON (Object o) =+    case HS.lookup "state" o of+      Just "active"  -> pure Active+      Just "pending" -> pure Pending+      Just _         -> fail "\"state\" key not \"active\" or \"pending\""+      Nothing        -> (fail . maybe "No error message from GitHub" show) (HS.lookup "message" o)+  parseJSON _         = fail "Expected a membership document, got something else"++$(deriveJSON defaultOptions { fieldLabelModifier = drop 6  . map toLower } ''Member)+$(deriveJSON defaultOptions { fieldLabelModifier = drop 4  . map toLower } ''Team)+$(deriveJSON defaultOptions { fieldLabelModifier = drop 7  . map toLower } ''TeamCreateRequest)+$(deriveJSON defaultOptions { fieldLabelModifier = drop 19 . map toLower } ''AddPublicKeyRequest)++-- | Error codes GitHub might return when attempting to use an API endpoint+data GitHubReturnStatus =   InvalidJSON             -- ^ GitHub could not parse the JSON document sent+                          | ValidationFailed        -- ^ Validation failed, an example of this error+                                                    --   is trying to create teams with the same name+                                                    --   within one organization+                          | InternalError           -- ^ In case GitHub returns 500 Internal Server Error+                                                    --   to some request+                          | NotFound                -- ^ When a resource has not been found. It does not+                                                    --   imply the resource does not exist+                          | NotAllowed              -- ^ Usually returned after GitHub replies with 403 Forbidden.+                                                    --   The user might not have permission to access/modify+                                                    --   that resource+                          | AllOk                   -- ^ This should never be returned+                          | RequiresAuthentication  -- ^ Accesing this resource requires authentication+                          | UnexpectedJSON String   -- ^ This library has failed to fulfill its purpose and could not+                                                    --   handle GitHub's response+                          deriving (Show, Eq)++-- | Instance that does not add anything to the body or headers of a PUT request+instance Putable EmptyBody where+  putPayload EmptyBody req = return $ req {requestBody = RequestBodyLBS ""}++instance Postable TeamCreateRequest where+  postPayload createRequest req = return $ req { requestBody = RequestBodyLBS (encode createRequest)}++instance Postable AddPublicKeyRequest where+  postPayload createRequest req = return $ req { requestBody = RequestBodyLBS (encode createRequest)}++-- | GitHub's OAuth 2.0 bearer token. This is simply added in an+--   Authorization header+newtype BearerToken = BearerToken { unBearerToken :: T.Text }++-- | The monad transformer where all operations run. Supports initial configuration+--   through a Reader monad and the possibility of failure through Either+type GitHub = EitherT GitHubReturnStatus (ReaderT BearerToken IO)++-- | Executes a computation built within the GitHub monad returning an Either within+--   the IO data type using the provided token+runGitHub' :: GitHub a -> BearerToken -> IO (Either GitHubReturnStatus a)+runGitHub' comp = runReaderT (runEitherT comp)++-- | Executes a computation built within the GitHub monad returning an Either within+--   the IO data type. Reads an API token from an environment variable named GITHUB_TOKEN+runGitHub :: GitHub a -> IO (Either GitHubReturnStatus a)+runGitHub comp = do+  maybeToken <- lookupEnv "GITHUB_TOKEN"+  case maybeToken of+    Just acquiredToken -> runGitHub' comp (BearerToken $ T.pack acquiredToken)+    Nothing            -> fail "Couldn't find GITHUB_TOKEN in environment"