packages feed

github-rest 1.0.3 → 1.1.0

raw patch · 15 files changed

+462/−375 lines, 15 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- GitHub.REST: GitHubState :: Maybe Token -> ByteString -> ByteString -> GitHubState
- GitHub.REST: [$sel:apiVersion:GitHubState] :: GitHubState -> ByteString
- GitHub.REST: [$sel:token:GitHubState] :: GitHubState -> Maybe Token
- GitHub.REST: [$sel:userAgent:GitHubState] :: GitHubState -> ByteString
- GitHub.REST: data GitHubState
- GitHub.REST: queryGitHubPage' :: (MonadGitHubREST m, FromJSON a) => GHEndpoint -> m (Either (Text, Text) (a, PageLinks))
- GitHub.REST.Monad: GitHubState :: Maybe Token -> ByteString -> ByteString -> GitHubState
- GitHub.REST.Monad: [$sel:apiVersion:GitHubState] :: GitHubState -> ByteString
- GitHub.REST.Monad: [$sel:token:GitHubState] :: GitHubState -> Maybe Token
- GitHub.REST.Monad: [$sel:userAgent:GitHubState] :: GitHubState -> ByteString
- GitHub.REST.Monad: data GitHubState
- GitHub.REST.Monad: queryGitHubPage' :: (MonadGitHubREST m, FromJSON a) => GHEndpoint -> m (Either (Text, Text) (a, PageLinks))
- GitHub.REST.Monad.Class: queryGitHubPage' :: (MonadGitHubREST m, FromJSON a) => GHEndpoint -> m (Either (Text, Text) (a, PageLinks))
+ GitHub.REST: GitHubSettings :: Maybe Token -> ByteString -> ByteString -> GitHubSettings
+ GitHub.REST: [$sel:apiVersion:GitHubSettings] :: GitHubSettings -> ByteString
+ GitHub.REST: [$sel:token:GitHubSettings] :: GitHubSettings -> Maybe Token
+ GitHub.REST: [$sel:userAgent:GitHubSettings] :: GitHubSettings -> ByteString
+ GitHub.REST: data GitHubSettings
+ GitHub.REST.Monad: GitHubSettings :: Maybe Token -> ByteString -> ByteString -> GitHubSettings
+ GitHub.REST.Monad: [$sel:apiVersion:GitHubSettings] :: GitHubSettings -> ByteString
+ GitHub.REST.Monad: [$sel:token:GitHubSettings] :: GitHubSettings -> Maybe Token
+ GitHub.REST.Monad: [$sel:userAgent:GitHubSettings] :: GitHubSettings -> ByteString
+ GitHub.REST.Monad: data GitHubManager
+ GitHub.REST.Monad: data GitHubSettings
+ GitHub.REST.Monad: initGitHubManager :: GitHubSettings -> IO GitHubManager
+ GitHub.REST.Monad: instance GHC.Exception.Type.Exception GitHub.REST.Monad.DecodeError
+ GitHub.REST.Monad: instance GHC.Show.Show GitHub.REST.Monad.DecodeError
+ GitHub.REST.Monad: queryGitHubPageIO :: FromJSON a => GitHubManager -> GHEndpoint -> IO (a, PageLinks)
- GitHub.REST: runGitHubT :: MonadIO m => GitHubState -> GitHubT m a -> m a
+ GitHub.REST: runGitHubT :: MonadIO m => GitHubSettings -> GitHubT m a -> m a
- GitHub.REST.Monad: runGitHubT :: MonadIO m => GitHubState -> GitHubT m a -> m a
+ GitHub.REST.Monad: runGitHubT :: MonadIO m => GitHubSettings -> GitHubT m a -> m a

Files

CHANGELOG.md view
@@ -1,5 +1,12 @@ ## Upcoming +## 1.1.0++* Rename `GitHubState` to `GitHubSettings`+* Remove `queryGitHubPage'` -- implement `queryGitHubPage` in `MonadGitHubREST` instead.+* Expose `queryGitHubPageIO` if users want to manually implement `MonadGitHubREST`+* Add `DecodeError` error+ ## 1.0.3  * Fix goldens after GitHub changed documentation URL
README.md view
@@ -23,7 +23,7 @@ default (Text)  main = do-  let state = GitHubState+  let state = GitHubSettings         { token = Nothing           -- ^ An authentication token to use, if any.         , userAgent = "alice/my-project"
github-rest.cabal view
@@ -1,13 +1,13 @@ cabal-version: >= 1.10 --- This file has been generated from package.yaml by hpack version 0.33.0.+-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack ----- hash: 9626757e084e5f6d0d88ae12dee832d693aa6ae2dea9283634eb783be14cfe9f+-- hash: 800fa4f169210c967d917bacbc61e12d696ad192e8744af6798c68552fd8c05c  name:           github-rest-version:        1.0.3+version:        1.1.0 synopsis:       Query the GitHub REST API programmatically description:    Query the GitHub REST API programmatically, which can provide a more                 flexible and clear interface than if all of the endpoints and their types
src/GitHub/REST.hs view
@@ -1,4 +1,4 @@-{-|+{- | Module      :  GitHub.REST Maintainer  :  Brandon Chinn <brandon@leapyear.io> Stability   :  experimental@@ -6,38 +6,44 @@  Definitions for querying the GitHub REST API. See README.md for an example. -}--module GitHub.REST-  (+module GitHub.REST (   -- * Monad transformer and type-class for querying the GitHub REST API-    MonadGitHubREST(..)-  , GitHubT-  , GitHubState(..)-  , runGitHubT+  MonadGitHubREST (..),+  GitHubT,+  GitHubSettings (..),+  runGitHubT,+   -- * GitHub authentication-  , Token(..)+  Token (..),+   -- * GitHub Endpoints-  , GHEndpoint(..)-  , GitHubData-  , EndpointVals+  GHEndpoint (..),+  GitHubData,+  EndpointVals,+   -- * KeyValue pairs-  , KeyValue(..)+  KeyValue (..),+   -- * Helpers-  , githubTry-  , githubTry'-  , (.:)+  githubTry,+  githubTry',+  (.:),+   -- * Re-exports-  , StdMethod(..)-  ) where+  StdMethod (..),+) where  import Control.Monad.IO.Unlift (MonadUnliftIO)-import Data.Aeson (FromJSON, Value(..), decode, withObject)+import Data.Aeson (FromJSON, Value (..), decode, withObject) import Data.Aeson.Types (parseEither, parseField) import qualified Data.ByteString.Lazy as ByteStringL import Data.Text (Text)-import Network.HTTP.Client-    (HttpException(..), HttpExceptionContent(..), Response(..))-import Network.HTTP.Types (Status, StdMethod(..), status422)+import Network.HTTP.Client (+  HttpException (..),+  HttpExceptionContent (..),+  Response (..),+ )+import Network.HTTP.Types (Status, StdMethod (..), status422) import UnliftIO.Exception (handleJust)  import GitHub.REST.Auth@@ -47,12 +53,13 @@  {- HTTP exception handling -} --- | Handle 422 exceptions thrown by the GitHub REST API.------ Most client errors are 422, since we should always be sending valid JSON. If an endpoint--- throws different error codes, use githubTry'.------ https://developer.github.com/v3/#client-errors+{- | Handle 422 exceptions thrown by the GitHub REST API.++ Most client errors are 422, since we should always be sending valid JSON. If an endpoint+ throws different error codes, use githubTry'.++ https://developer.github.com/v3/#client-errors+-} githubTry :: MonadUnliftIO m => m a -> m (Either Value a) githubTry = githubTry' status422 
src/GitHub/REST/Auth.hs view
@@ -1,4 +1,8 @@-{-|+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++{- | Module      :  GitHub.REST.Auth Maintainer  :  Brandon Chinn <brandon@leapyear.io> Stability   :  experimental@@ -6,20 +10,18 @@  Definitions for handling authentication with the GitHub REST API. -}-{-# LANGUAGE CPP #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}+module GitHub.REST.Auth (+  Token (..),+  fromToken, -module GitHub.REST.Auth-  ( Token(..)-  , fromToken   -- * Helpers for using JWT tokens with the GitHub API-  , getJWTToken-  , loadSigner-  ) where+  getJWTToken,+  loadSigner,+) where  import Data.ByteString (ByteString) import qualified Data.ByteString as ByteString+ #if !MIN_VERSION_base(4,11,0) import Data.Monoid ((<>)) #endif@@ -31,10 +33,10 @@  -- | The token to use to authenticate with GitHub. data Token-  = AccessToken ByteString-    -- ^ https://developer.github.com/v3/#authentication-  | BearerToken ByteString-    -- ^ https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app+  = -- | https://developer.github.com/v3/#authentication+    AccessToken ByteString+  | -- | https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app+    BearerToken ByteString   deriving (Show)  fromToken :: Token -> ByteString@@ -55,12 +57,13 @@     signToken = JWT.encodeSigned #endif     mkToken now =-      let claims = mempty-            { JWT.iat = JWT.numericDate $ utcTimeToPOSIXSeconds now-            , JWT.exp = JWT.numericDate $ utcTimeToPOSIXSeconds now + (10 * 60)-            , JWT.iss = JWT.stringOrURI $ Text.pack $ show appId-            }-      in BearerToken . Text.encodeUtf8 $ signToken signer claims+      let claims =+            mempty+              { JWT.iat = JWT.numericDate $ utcTimeToPOSIXSeconds now+              , JWT.exp = JWT.numericDate $ utcTimeToPOSIXSeconds now + (10 * 60)+              , JWT.iss = JWT.stringOrURI $ Text.pack $ show appId+              }+       in BearerToken . Text.encodeUtf8 $ signToken signer claims     -- lose a second in the case of rounding     -- https://github.community/t5/GitHub-API-Development-and/quot-Expiration-time-claim-exp-is-too-far-in-the-future-quot/m-p/20457/highlight/true#M1127     getNow = addUTCTime (-1) <$> getCurrentTime
src/GitHub/REST/Endpoint.hs view
@@ -1,4 +1,8 @@-{-|+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++{- | Module      :  GitHub.REST.Endpoint Maintainer  :  Brandon Chinn <brandon@leapyear.io> Stability   :  experimental@@ -6,19 +10,16 @@  Define the 'GHEndpoint' helper type for defining a call to a GitHub API endpoint. -}-{-# LANGUAGE CPP #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}--module GitHub.REST.Endpoint-  ( GHEndpoint(..)-  , EndpointVals-  , GitHubData-  , endpointPath-  , renderMethod-  ) where+module GitHub.REST.Endpoint (+  GHEndpoint (..),+  EndpointVals,+  GitHubData,+  endpointPath,+  renderMethod,+) where  import Data.Maybe (fromMaybe)+ #if !MIN_VERSION_base(4,11,0) import Data.Monoid ((<>)) #endif@@ -33,16 +34,16 @@  -- | A call to a GitHub API endpoint. data GHEndpoint = GHEndpoint-  { method       :: StdMethod-  , endpoint     :: Text-    -- ^ The GitHub API endpoint, with colon-prefixed components that will be replaced; e.g.+  { method :: StdMethod+  , -- | The GitHub API endpoint, with colon-prefixed components that will be replaced; e.g.     -- @"\/users\/:username\/repos"@-  , endpointVals :: EndpointVals-    -- ^ Key-value pairs to replace colon-prefixed components in 'endpoint'; e.g.+    endpoint :: Text+  , -- | Key-value pairs to replace colon-prefixed components in 'endpoint'; e.g.     -- @[ "username" := ("alice" :: Text) ]@-  , ghData       :: GitHubData-    -- ^ Key-value pairs to send in the request body; e.g.+    endpointVals :: EndpointVals+  , -- | Key-value pairs to send in the request body; e.g.     -- @[ "sort" := ("created" :: Text), "direction" := ("asc" :: Text) ]@+    ghData :: GitHubData   }  -- | Return the endpoint path, populated by the values in 'endpointVals'.@@ -51,9 +52,10 @@   where     values = map kvToText endpointVals     populate t = case Text.uncons t of-      Just (':', key) -> fromMaybe-        (fail' $ "Could not find value for key '" <> key <> "'")-        $ lookup key values+      Just (':', key) ->+        fromMaybe+          (fail' $ "Could not find value for key '" <> key <> "'")+          $ lookup key values       _ -> t     fail' msg = error . Text.unpack $ msg <> ": " <> endpoint 
src/GitHub/REST/KeyValue.hs view
@@ -1,4 +1,7 @@-{-|+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}++{- | Module      :  GitHub.REST.KeyValue Maintainer  :  Brandon Chinn <brandon@leapyear.io> Stability   :  experimental@@ -6,16 +9,13 @@  Define the 'KeyValue' helper type. -}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}--module GitHub.REST.KeyValue-  ( KeyValue(..)-  , kvToValue-  , kvToText-  ) where+module GitHub.REST.KeyValue (+  KeyValue (..),+  kvToValue,+  kvToText,+) where -import Data.Aeson (ToJSON(..), Value(..), object)+import Data.Aeson (ToJSON (..), Value (..), object) import Data.Aeson.Types (Pair) import Data.Scientific (floatingOrInteger) import Data.Text (Text)@@ -24,6 +24,7 @@ -- | A type representing a key-value pair. data KeyValue where   (:=) :: (Show v, ToJSON v) => Text -> v -> KeyValue+ infixr 1 :=  instance Show KeyValue where
src/GitHub/REST/Monad.hs view
@@ -1,4 +1,11 @@-{-|+{-# LANGUAGE CPP #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TupleSections #-}++{- | Module      :  GitHub.REST.Monad Maintainer  :  Brandon Chinn <brandon@leapyear.io> Stability   :  experimental@@ -7,67 +14,142 @@ Defines 'GitHubT' and 'MonadGitHubREST', a monad transformer and type class that gives a monad @m@ the capability to query the GitHub REST API. -}-{-# LANGUAGE CPP #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TupleSections #-}+module GitHub.REST.Monad (+  -- * MonadGitHubREST API+  MonadGitHubREST (..),+  queryGitHubPageIO, -module GitHub.REST.Monad-  ( MonadGitHubREST(..)-  , GitHubT-  , GitHubState(..)-  , runGitHubT-  ) where+  -- * GitHubManager+  GitHubManager,+  initGitHubManager, -#if !MIN_VERSION_base(4,13,0)-import Control.Monad.Fail (MonadFail)-#endif-import Control.Monad.IO.Class (MonadIO(..))-import Control.Monad.IO.Unlift (MonadUnliftIO(..))+  -- * GitHubSettings+  GitHubSettings (..),++  -- * GitHubT+  GitHubT,+  runGitHubT,+) where++import Control.Monad.IO.Class (MonadIO (..))+import Control.Monad.IO.Unlift (MonadUnliftIO (..)) import Control.Monad.Reader (ReaderT, ask, runReaderT) import Control.Monad.Trans (MonadTrans)-import Data.Aeson (eitherDecode, encode)+import Data.Aeson (FromJSON, eitherDecode, encode) import Data.ByteString (ByteString) import qualified Data.ByteString.Lazy as ByteStringL-#if !MIN_VERSION_base(4,11,0)-import Data.Monoid ((<>))-#endif+import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text-import Network.HTTP.Client-    ( Manager-    , Request(..)-    , RequestBody(..)-    , Response(..)-    , httpLbs-    , newManager-    , parseRequest_-    , throwErrorStatusCodes-    )+import Network.HTTP.Client (+  Manager,+  Request (..),+  RequestBody (..),+  Response (..),+  httpLbs,+  newManager,+  parseRequest_,+  throwErrorStatusCodes,+ ) import Network.HTTP.Client.TLS (tlsManagerSettings) import Network.HTTP.Types (hAccept, hAuthorization, hUserAgent)+import UnliftIO.Exception (Exception, throwIO) +#if !MIN_VERSION_base(4,13,0)+import Control.Monad.Fail (MonadFail)+#endif+#if !MIN_VERSION_base(4,11,0)+import Data.Monoid ((<>))+#endif+ import GitHub.REST.Auth (Token, fromToken)-import GitHub.REST.Endpoint (GHEndpoint(..), endpointPath, renderMethod)+import GitHub.REST.Endpoint (GHEndpoint (..), endpointPath, renderMethod) import GitHub.REST.KeyValue (kvToValue) import GitHub.REST.Monad.Class-import GitHub.REST.PageLinks (parsePageLinks)+import GitHub.REST.PageLinks (PageLinks, parsePageLinks) -data GitHubState = GitHubState-  { token      :: Maybe Token-    -- ^ The token to use to authenticate with the API.-  , userAgent  :: ByteString-    -- ^ The user agent to use when interacting with the API: https://developer.github.com/v3/#user-agent-required-  , apiVersion :: ByteString-    -- ^ The media type will be sent as: application/vnd.github.VERSION+json. For the standard+{- GitHubSettings -}++data GitHubSettings = GitHubSettings+  { -- | The token to use to authenticate with the API.+    token :: Maybe Token+  , -- | The user agent to use when interacting with the API: https://developer.github.com/v3/#user-agent-required+    userAgent :: ByteString+  , -- | The media type will be sent as: application/vnd.github.VERSION+json. For the standard     -- API endpoints, "v3" should be sufficient here. See https://developer.github.com/v3/media/+    apiVersion :: ByteString   } +{- GitHubManager -}++data GitHubManager = GitHubManager+  { ghSettings :: GitHubSettings+  , ghManager :: Manager+  }++-- | Initialize a 'GitHubManager'.+initGitHubManager :: GitHubSettings -> IO GitHubManager+initGitHubManager ghSettings = do+  ghManager <- newManager tlsManagerSettings+  return GitHubManager{..}++{- | Same as 'queryGitHubPage', except explicitly taking in 'GitHubManager' and running+ in IO.++ Useful for implementing 'MonadGitHubREST' outside of 'GitHubT'.+-}+queryGitHubPageIO :: FromJSON a => GitHubManager -> GHEndpoint -> IO (a, PageLinks)+queryGitHubPageIO GitHubManager{..} ghEndpoint = do+  let GitHubSettings{..} = ghSettings++  let request =+        (parseRequest_ $ Text.unpack $ ghUrl <> endpointPath ghEndpoint)+          { method = renderMethod ghEndpoint+          , requestHeaders =+              [ (hAccept, "application/vnd.github." <> apiVersion <> "+json")+              , (hUserAgent, userAgent)+              ]+                ++ maybe [] ((: []) . (hAuthorization,) . fromToken) token+          , requestBody = RequestBodyLBS $ encode $ kvToValue $ ghData ghEndpoint+          , checkResponse = throwErrorStatusCodes+          }++  response <- httpLbs request ghManager++  let body = responseBody response+      -- empty body always errors when decoding, even if the end user doesn't care about the+      -- result, like creating a branch, when the endpoint doesn't return anything.+      --+      -- In this case, pretend like the server sent back an encoded version of the unit type,+      -- so that `queryGitHub endpoint` would be typed to `m ()`.+      nonEmptyBody = if ByteStringL.null body then encode () else body+      pageLinks = maybe mempty parsePageLinks . lookupHeader "Link" $ response++  case eitherDecode nonEmptyBody of+    Right payload -> return (payload, pageLinks)+    Left e ->+      throwIO $+        DecodeError+          { decodeErrorMessage = Text.pack e+          , decodeErrorResponse = Text.decodeUtf8 $ ByteStringL.toStrict body+          }+  where+    ghUrl = "https://api.github.com"+    lookupHeader headerName = fmap Text.decodeUtf8 . lookup headerName . responseHeaders++data DecodeError = DecodeError+  { decodeErrorMessage :: Text+  , decodeErrorResponse :: Text+  }+  deriving (Show)++instance Exception DecodeError++{- GitHubT -}+ -- | A simple monad that can run REST calls. newtype GitHubT m a = GitHubT-  { unGitHubT :: ReaderT (Manager, GitHubState) m a+  { unGitHubT :: ReaderT GitHubManager m a   }   deriving     ( Functor@@ -84,42 +166,16 @@       inner (run . unGitHubT)  instance MonadIO m => MonadGitHubREST (GitHubT m) where-  queryGitHubPage' ghEndpoint = do-    (manager, GitHubState{..}) <- GitHubT ask--    let request = (parseRequest_ $ Text.unpack $ ghUrl <> endpointPath ghEndpoint)-          { method = renderMethod ghEndpoint-          , requestHeaders =-              [ (hAccept, "application/vnd.github." <> apiVersion <> "+json")-              , (hUserAgent, userAgent)-              ] ++ maybe [] ((:[]) . (hAuthorization,) . fromToken) token-          , requestBody = RequestBodyLBS $ encode $ kvToValue $ ghData ghEndpoint-          , checkResponse = throwErrorStatusCodes-          }--    response <- liftIO $ httpLbs request manager--    let body = responseBody response-        -- empty body always errors when decoding, even if the end user doesn't care about the-        -- result, like creating a branch, when the endpoint doesn't return anything.-        ---        -- In this case, pretend like the server sent back an encoded version of the unit type,-        -- so that `queryGitHub endpoint` would be typed to `m ()`.-        nonEmptyBody = if ByteStringL.null body then encode () else body-        pageLinks = maybe mempty parsePageLinks . lookupHeader "Link" $ response+  queryGitHubPage ghEndpoint = do+    manager <- GitHubT ask+    liftIO $ queryGitHubPageIO manager ghEndpoint -    return $ case eitherDecode nonEmptyBody of-      Right payload -> Right (payload, pageLinks)-      Left e -> Left (Text.pack e, Text.decodeUtf8 $ ByteStringL.toStrict body)-    where-      ghUrl = "https://api.github.com"-      lookupHeader headerName = fmap Text.decodeUtf8 . lookup headerName . responseHeaders+{- | Run the given 'GitHubT' action with the given token and user agent. --- | Run the given 'GitHubT' action with the given token and user agent.------ The token will be sent with each API request -- see 'Token'. The user agent is also required for--- each API request -- see https://developer.github.com/v3/#user-agent-required.-runGitHubT :: MonadIO m => GitHubState -> GitHubT m a -> m a-runGitHubT state action = do-  manager <- liftIO $ newManager tlsManagerSettings-  (`runReaderT` (manager, state)) . unGitHubT $ action+ The token will be sent with each API request -- see 'Token'. The user agent is also required for+ each API request -- see https://developer.github.com/v3/#user-agent-required.+-}+runGitHubT :: MonadIO m => GitHubSettings -> GitHubT m a -> m a+runGitHubT settings action = do+  manager <- liftIO $ initGitHubManager settings+  (`runReaderT` manager) . unGitHubT $ action
src/GitHub/REST/Monad/Class.hs view
@@ -1,4 +1,7 @@-{-|+{-# LANGUAGE CPP #-}+{-# LANGUAGE TypeApplications #-}++{- | Module      :  GitHub.REST.Monad.Class Maintainer  :  Brandon Chinn <brandon@leapyear.io> Stability   :  experimental@@ -6,84 +9,73 @@  Defines 'MonadGitHubREST' that gives a monad @m@ the capability to query the GitHub REST API. -}-{-# LANGUAGE CPP #-}-{-# LANGUAGE TypeApplications #-}--module GitHub.REST.Monad.Class-  ( MonadGitHubREST(..)-  ) where+module GitHub.REST.Monad.Class (+  MonadGitHubREST (..),+) where -import Control.Monad (void, (<=<))+import Control.Monad (void) import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Except (ExceptT) import Control.Monad.Trans.Identity (IdentityT) import Control.Monad.Trans.Maybe (MaybeT)-import Control.Monad.Trans.Reader (ReaderT) import qualified Control.Monad.Trans.RWS.Lazy as Lazy import qualified Control.Monad.Trans.RWS.Strict as Strict+import Control.Monad.Trans.Reader (ReaderT) import qualified Control.Monad.Trans.State.Lazy as Lazy import qualified Control.Monad.Trans.State.Strict as Strict import qualified Control.Monad.Trans.Writer.Lazy as Lazy import qualified Control.Monad.Trans.Writer.Strict as Strict import Data.Aeson (FromJSON, Value)+ #if !MIN_VERSION_base(4,11,0) import Data.Monoid ((<>)) #endif-import Data.Text (Text)-import qualified Data.Text as Text  import GitHub.REST.Endpoint-import GitHub.REST.PageLinks (PageLinks(..))+import GitHub.REST.PageLinks (PageLinks (..)) --- | A type class for monads that can query the GitHub REST API.------ Example:------ > -- create the "foo" branch--- > queryGitHub GHEndpoint--- >   { method = POST--- >   , endpoint = "/repos/:owner/:repo/git/refs"--- >   , endpointVals =--- >     [ "owner" := "alice"--- >     , "repo" := "my-project"--- >     ]--- >   , ghData =--- >     [ "ref" := "refs/heads/foo"--- >     , "sha" := "1234567890abcdef"--- >     ]--- >   }------ It's recommended that you create functions for the API endpoints you're using:------ > deleteBranch branch = queryGitHub GHEndpoint--- >   { method = DELETE--- >   , endpoint = "/repos/:owner/:repo/git/refs/:ref"--- >   , endpointVals =--- >     [ "owner" := "alice"--- >     , "repo" := "my-project"--- >     , "ref" := "heads/" <> branch--- >     ]--- >   , ghData = []--- >   }+{- | A type class for monads that can query the GitHub REST API.++ Example:++ > -- create the "foo" branch+ > queryGitHub GHEndpoint+ >   { method = POST+ >   , endpoint = "/repos/:owner/:repo/git/refs"+ >   , endpointVals =+ >     [ "owner" := "alice"+ >     , "repo" := "my-project"+ >     ]+ >   , ghData =+ >     [ "ref" := "refs/heads/foo"+ >     , "sha" := "1234567890abcdef"+ >     ]+ >   }++ It's recommended that you create functions for the API endpoints you're using:++ > deleteBranch branch = queryGitHub GHEndpoint+ >   { method = DELETE+ >   , endpoint = "/repos/:owner/:repo/git/refs/:ref"+ >   , endpointVals =+ >     [ "owner" := "alice"+ >     , "repo" := "my-project"+ >     , "ref" := "heads/" <> branch+ >     ]+ >   , ghData = []+ >   }+-} class Monad m => MonadGitHubREST m where-  {-# MINIMAL queryGitHubPage' #-}+  {-# MINIMAL queryGitHubPage #-} -  -- | Query GitHub, returning @Right (payload, links)@ if successful, where @payload@ is the+  -- | Query GitHub, returning @(payload, links)@ if successful, where @payload@ is the   -- response that GitHub sent back and @links@ containing any pagination links GitHub may have   -- sent back. If the response could not be decoded as JSON, returns   -- @Left (error message, response from server)@.   ---  -- Errors on network connection failures or if GitHub sent back an error message. Use `githubTry`-  -- if you wish to handle GitHub errors.-  queryGitHubPage' :: FromJSON a => GHEndpoint -> m (Either (Text, Text) (a, PageLinks))--  -- | 'queryGitHubPage'', except calls 'fail' if JSON decoding fails.+  -- Errors on network connection failures, if GitHub sent back an error message, or if the response+  -- could not be decoded as JSON. Use `githubTry` if you wish to handle GitHub errors.   queryGitHubPage :: FromJSON a => GHEndpoint -> m (a, PageLinks)-  queryGitHubPage = either fail' pure <=< queryGitHubPage'-    where-      fail' (message, response) =-        let ellipses s = if Text.length s > 100 then take 100 (Text.unpack s) ++ "..." else Text.unpack s-        in error $ "Could not decode response:\nmessage = " ++ ellipses message ++ "\nresponse = " ++ ellipses response    -- | 'queryGitHubPage', except ignoring pagination links.   queryGitHub :: FromJSON a => GHEndpoint -> m a@@ -96,7 +88,7 @@     (payload, pageLinks) <- queryGitHubPage ghEndpoint     case pageNext pageLinks of       Just next -> do-        rest <- queryGitHubAll ghEndpoint { endpoint = next, endpointVals = [] }+        rest <- queryGitHubAll ghEndpoint{endpoint = next, endpointVals = []}         return $ payload <> rest       Nothing -> return payload @@ -107,31 +99,31 @@ {- Instances for common monad transformers -}  instance MonadGitHubREST m => MonadGitHubREST (ReaderT r m) where-  queryGitHubPage' = lift . queryGitHubPage'+  queryGitHubPage = lift . queryGitHubPage  instance MonadGitHubREST m => MonadGitHubREST (ExceptT e m) where-  queryGitHubPage' = lift . queryGitHubPage'+  queryGitHubPage = lift . queryGitHubPage  instance MonadGitHubREST m => MonadGitHubREST (IdentityT m) where-  queryGitHubPage' = lift . queryGitHubPage'+  queryGitHubPage = lift . queryGitHubPage  instance MonadGitHubREST m => MonadGitHubREST (MaybeT m) where-  queryGitHubPage' = lift . queryGitHubPage'+  queryGitHubPage = lift . queryGitHubPage  instance (Monoid w, MonadGitHubREST m) => MonadGitHubREST (Lazy.RWST r w s m) where-  queryGitHubPage' = lift . queryGitHubPage'+  queryGitHubPage = lift . queryGitHubPage  instance (Monoid w, MonadGitHubREST m) => MonadGitHubREST (Strict.RWST r w s m) where-  queryGitHubPage' = lift . queryGitHubPage'+  queryGitHubPage = lift . queryGitHubPage  instance MonadGitHubREST m => MonadGitHubREST (Lazy.StateT s m) where-  queryGitHubPage' = lift . queryGitHubPage'+  queryGitHubPage = lift . queryGitHubPage  instance MonadGitHubREST m => MonadGitHubREST (Strict.StateT s m) where-  queryGitHubPage' = lift . queryGitHubPage'+  queryGitHubPage = lift . queryGitHubPage  instance (Monoid w, MonadGitHubREST m) => MonadGitHubREST (Lazy.WriterT w m) where-  queryGitHubPage' = lift . queryGitHubPage'+  queryGitHubPage = lift . queryGitHubPage  instance (Monoid w, MonadGitHubREST m) => MonadGitHubREST (Strict.WriterT w m) where-  queryGitHubPage' = lift . queryGitHubPage'+  queryGitHubPage = lift . queryGitHubPage
src/GitHub/REST/PageLinks.hs view
@@ -1,34 +1,38 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} -module GitHub.REST.PageLinks-  ( PageLinks(..)-  , parsePageLinks-  ) where+module GitHub.REST.PageLinks (+  PageLinks (..),+  parsePageLinks,+) where  import Data.Maybe (fromMaybe)+ #if !MIN_VERSION_base(4,11,0) import Data.Semigroup (Semigroup(..)) #endif import Data.Text (Text) import qualified Data.Text as Text --- | Helper type for GitHub pagination.------ https://developer.github.com/v3/guides/traversing-with-pagination/+{- | Helper type for GitHub pagination.++ https://developer.github.com/v3/guides/traversing-with-pagination/+-} data PageLinks = PageLinks   { pageFirst :: Maybe Text-  , pagePrev  :: Maybe Text-  , pageNext  :: Maybe Text-  , pageLast  :: Maybe Text-  } deriving (Eq,Show)+  , pagePrev :: Maybe Text+  , pageNext :: Maybe Text+  , pageLast :: Maybe Text+  }+  deriving (Eq, Show)  instance Semigroup PageLinks where-  links1 <> links2 = PageLinks-    (pageFirst links1 <> pageFirst links2)-    (pagePrev links1 <> pagePrev links2)-    (pageNext links1 <> pageNext links2)-    (pageLast links1 <> pageLast links2)+  links1 <> links2 =+    PageLinks+      (pageFirst links1 <> pageFirst links2)+      (pagePrev links1 <> pagePrev links2)+      (pageNext links1 <> pageNext links2)+      (pageLast links1 <> pageLast links2)  instance Monoid PageLinks where   mempty = PageLinks Nothing Nothing Nothing Nothing@@ -44,18 +48,19 @@     resolve pageLinks "" = pageLinks     resolve pageLinks link =       let (rel, url) = parsePageLink link-      in case rel of-        "first" -> pageLinks { pageFirst = Just url }-        "prev" -> pageLinks { pagePrev = Just url }-        "next" -> pageLinks { pageNext = Just url }-        "last" -> pageLinks { pageLast = Just url }-        _ -> error $ "Unknown rel in page link: " ++ show link+       in case rel of+            "first" -> pageLinks{pageFirst = Just url}+            "prev" -> pageLinks{pagePrev = Just url}+            "next" -> pageLinks{pageNext = Just url}+            "last" -> pageLinks{pageLast = Just url}+            _ -> error $ "Unknown rel in page link: " ++ show link --- | Parse a single page link, like:------ <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2>; rel="next"------ Returns ("next", "/search/code?q=addClass+user%3Amozilla&page=2")+{- | Parse a single page link, like:++ <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2>; rel="next"++ Returns ("next", "/search/code?q=addClass+user%3Amozilla&page=2")+-} parsePageLink :: Text -> (Text, Text) parsePageLink link = fromMaybe (error $ "Unknown page link: " ++ show link) $ do   (linkUrl, linkRel) <- case split ";" link of
test/Endpoint.hs view
@@ -8,27 +8,27 @@ import Data.Function (on) import Data.List (nubBy) import Data.Maybe (fromMaybe)+ #if !MIN_VERSION_base(4,11,0) import Data.Monoid ((<>)) import Data.Semigroup (Semigroup) #endif-import Data.String (IsString(..))+import Data.String (IsString (..)) import Data.Text (Text) import qualified Data.Text as Text-import Network.HTTP.Types (StdMethod(..))+import Network.HTTP.Types (StdMethod (..)) import Test.Tasty (TestTree) import Test.Tasty.QuickCheck  import GitHub.REST.Endpoint-import GitHub.REST.KeyValue (KeyValue(..))+import GitHub.REST.KeyValue (KeyValue (..))  tests :: [TestTree] tests =   [ testProperty "endpointPath with no substitutions" $       forAll (listOf genNoSubstitute) $ \endpointComponents ->         let ghEndpoint = mkGHEndpoint endpointComponents []-        in endpointPath ghEndpoint === concatComponents endpointComponents-+         in endpointPath ghEndpoint === concatComponents endpointComponents   , testProperty "endpointPath with substitutions" $       forAll (listOf genNoSubstitute) $ \withoutColons ->         forAll (listOf1 genSubstitute) $ \withColonAndVals' -> do@@ -47,26 +47,27 @@ {- Helpers -}  mkGHEndpoint :: [EndpointComponent] -> EndpointVals -> GHEndpoint-mkGHEndpoint endpointComponents endpointVals = GHEndpoint-  { method = GET-  , endpoint = concatComponents endpointComponents-  , endpointVals-  , ghData = []-  }+mkGHEndpoint endpointComponents endpointVals =+  GHEndpoint+    { method = GET+    , endpoint = concatComponents endpointComponents+    , endpointVals+    , ghData = []+    }  concatComponents :: [EndpointComponent] -> Text concatComponents = ("/" <>) . Text.intercalate "/" . map unComponent  -- | A component in an endpoint path; e.g. does not contain forward slashes.-newtype EndpointComponent = EndpointComponent { unComponent :: Text }-  deriving (Show,Eq,IsString,Monoid,Semigroup)+newtype EndpointComponent = EndpointComponent {unComponent :: Text}+  deriving (Show, Eq, IsString, Monoid, Semigroup)  -- | Generate a non-substitutionable EndpointComponent. genNoSubstitute :: Gen EndpointComponent genNoSubstitute = fmap fromString $ listOf1 $ elements allowedUrlCharacters   where     -- https://stackoverflow.com/a/41353282-    allowedUrlCharacters = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ "-_.~"+    allowedUrlCharacters = ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['0' .. '9'] ++ "-_.~"  -- | Generate a substitutionable EndpointComponent and a value to interpolate. genSubstitute :: Gen (EndpointComponent, EndpointComponent)
test/Main.hs view
@@ -7,10 +7,13 @@ import qualified Query  main :: IO ()-main = defaultMain $ testGroup "github-rest"-  [ testGroup "GitHub.REST (Helpers)" Helpers.tests-  , testGroup "GitHub.REST.Endpoint" Endpoint.tests-  , testGroup "GitHub.REST.Monad.Class" MockQuery.tests-  , testGroup "GitHub.REST.PageLinks" PageLinks.tests-  , testGroup "GitHub.REST (End-to-End)" Query.tests-  ]+main =+  defaultMain $+    testGroup+      "github-rest"+      [ testGroup "GitHub.REST (Helpers)" Helpers.tests+      , testGroup "GitHub.REST.Endpoint" Endpoint.tests+      , testGroup "GitHub.REST.Monad.Class" MockQuery.tests+      , testGroup "GitHub.REST.PageLinks" PageLinks.tests+      , testGroup "GitHub.REST (End-to-End)" Query.tests+      ]
test/MockQuery.hs view
@@ -7,109 +7,113 @@  import Control.Applicative ((<|>)) import Control.Exception (SomeException, try)+ #if !MIN_VERSION_base(4,13,0) import Control.Monad.Fail (MonadFail) #endif import Control.Monad.State.Strict (StateT, evalStateT, get, put)-import Data.Aeson (Result(..), Value(..), fromJSON)+import Data.Aeson (Result (..), Value (..), fromJSON) import Data.Either (isLeft) import Data.List (uncons) import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text)+import qualified Data.Text as Text import GHC.Exts (fromList)-import Network.HTTP.Types (StdMethod(..))+import Network.HTTP.Types (StdMethod (..)) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit -import GitHub.REST.Endpoint (GHEndpoint(..))+import GitHub.REST.Endpoint (GHEndpoint (..)) import GitHub.REST.Monad.Class-import GitHub.REST.PageLinks (PageLinks(..))+import GitHub.REST.PageLinks (PageLinks (..))  tests :: [TestTree] tests =-  [ testGroup "queryGitHubPage"-    [ testCase "success single" $ do-        (result, pageLinks) <- runMockREST (queryGitHubPage @_ @Text) [Right "a"]-        result @?= "a"-        isNothing (pageNext pageLinks) @? "PageLinks 'next' rel is not Nothing"-    , testCase "success multiple" $ do-        (result, pageLinks) <- runMockREST (queryGitHubPage @_ @Text) [Right "a", Right "b"]-        result @?= "a"-        isJust (pageNext pageLinks) @? "PageLinks 'next' rel is Nothing"-    , testCase "error single" $ do-        result <- try @SomeException $ runMockREST (queryGitHubPage @_ @Text) [Left "error message!"]-        isLeft result @? "query did not error"-    , testCase "success with error in second page" $ do-        (result, pageLinks) <- runMockREST (queryGitHubPage @_ @Text) [Right "a", Left "error message!"]-        result @?= "a"-        isJust (pageNext pageLinks) @? "PageLinks 'next' rel is Nothing"-    ]--  , testGroup "queryGitHub"-    [ testCase "success single" $ do-        result <- runMockREST (queryGitHub @_ @Text) [Right "a"]-        result @?= "a"-    , testCase "success multiple" $ do-        result <- runMockREST (queryGitHub @_ @Text) [Right "a", Right "b"]-        result @?= "a"-    , testCase "error single" $ do-        result <- try @SomeException $ runMockREST (queryGitHub @_ @Text) [Left "error message!"]-        isLeft result @? "query did not error"-    , testCase "success with error in second page" $ do-        result <- runMockREST (queryGitHub @_ @Text) [Right "a", Left "error message!"]-        result @?= "a"-    ]--  , testGroup "queryGitHubAll"-    [ testCase "success single" $ do-        result <- runMockREST (queryGitHubAll @_ @[Text]) [Right "a"]-        result @?= ["a"]-    , testCase "success multiple" $ do-        result <- runMockREST (queryGitHubAll @_ @[Text]) [Right "a", Right "b"]-        result @?= ["a", "b"]-    , testCase "error single" $ do-        result <- try @SomeException $ runMockREST (queryGitHubAll @_ @[Text]) [Left "error message!"]-        isLeft result @? "query did not error"-    , testCase "error in second page" $ do-        result <- try @SomeException $ runMockREST (queryGitHubAll @_ @[Text]) [Right "a", Left "error message!"]-        isLeft result @? "query did not error"-    ]--  , testGroup "queryGitHub_"-    [ testCase "success single" $-        runMockREST queryGitHub_ [Right "a"]-    , testCase "success multiple" $-        runMockREST queryGitHub_ [Right "a", Right "b"]-    , testCase "error single" $ do-        result <- try @SomeException $ runMockREST queryGitHub_ [Left "error message!"]-        isLeft result @? "query did not error"-    , testCase "success with error in second page" $-        runMockREST queryGitHub_ [Right "a", Left "error message!"]-    ]+  [ testGroup+      "queryGitHubPage"+      [ testCase "success single" $ do+          (result, pageLinks) <- runMockREST (queryGitHubPage @_ @Text) [Right "a"]+          result @?= "a"+          isNothing (pageNext pageLinks) @? "PageLinks 'next' rel is not Nothing"+      , testCase "success multiple" $ do+          (result, pageLinks) <- runMockREST (queryGitHubPage @_ @Text) [Right "a", Right "b"]+          result @?= "a"+          isJust (pageNext pageLinks) @? "PageLinks 'next' rel is Nothing"+      , testCase "error single" $ do+          result <- try @SomeException $ runMockREST (queryGitHubPage @_ @Text) [Left "error message!"]+          isLeft result @? "query did not error"+      , testCase "success with error in second page" $ do+          (result, pageLinks) <- runMockREST (queryGitHubPage @_ @Text) [Right "a", Left "error message!"]+          result @?= "a"+          isJust (pageNext pageLinks) @? "PageLinks 'next' rel is Nothing"+      ]+  , testGroup+      "queryGitHub"+      [ testCase "success single" $ do+          result <- runMockREST (queryGitHub @_ @Text) [Right "a"]+          result @?= "a"+      , testCase "success multiple" $ do+          result <- runMockREST (queryGitHub @_ @Text) [Right "a", Right "b"]+          result @?= "a"+      , testCase "error single" $ do+          result <- try @SomeException $ runMockREST (queryGitHub @_ @Text) [Left "error message!"]+          isLeft result @? "query did not error"+      , testCase "success with error in second page" $ do+          result <- runMockREST (queryGitHub @_ @Text) [Right "a", Left "error message!"]+          result @?= "a"+      ]+  , testGroup+      "queryGitHubAll"+      [ testCase "success single" $ do+          result <- runMockREST (queryGitHubAll @_ @[Text]) [Right "a"]+          result @?= ["a"]+      , testCase "success multiple" $ do+          result <- runMockREST (queryGitHubAll @_ @[Text]) [Right "a", Right "b"]+          result @?= ["a", "b"]+      , testCase "error single" $ do+          result <- try @SomeException $ runMockREST (queryGitHubAll @_ @[Text]) [Left "error message!"]+          isLeft result @? "query did not error"+      , testCase "error in second page" $ do+          result <- try @SomeException $ runMockREST (queryGitHubAll @_ @[Text]) [Right "a", Left "error message!"]+          isLeft result @? "query did not error"+      ]+  , testGroup+      "queryGitHub_"+      [ testCase "success single" $+          runMockREST queryGitHub_ [Right "a"]+      , testCase "success multiple" $+          runMockREST queryGitHub_ [Right "a", Right "b"]+      , testCase "error single" $ do+          result <- try @SomeException $ runMockREST queryGitHub_ [Left "error message!"]+          isLeft result @? "query did not error"+      , testCase "success with error in second page" $+          runMockREST queryGitHub_ [Right "a", Left "error message!"]+      ]   ]  {- Mock implementation -} -newtype MockREST a = MockREST { unMockREST :: StateT [Either Text Text] IO a }-  deriving (Functor,Applicative,Monad,MonadFail)+newtype MockREST a = MockREST {unMockREST :: StateT [Either Text Text] IO a}+  deriving (Functor, Applicative, Monad, MonadFail)  runMockREST :: (GHEndpoint -> MockREST a) -> [Either Text Text] -> IO a runMockREST f results = (`evalStateT` results) $ unMockREST $ f ghEndpoint   where-    ghEndpoint = GHEndpoint-      { method = GET-      , endpoint = "/"-      , endpointVals = []-      , ghData = []-      }+    ghEndpoint =+      GHEndpoint+        { method = GET+        , endpoint = "/"+        , endpointVals = []+        , ghData = []+        }  instance MonadGitHubREST MockREST where-  queryGitHubPage' _ = do+  queryGitHubPage _ = do     (curr, rest) <- fromMaybe (error "Did you forget to mock a query?") . uncons <$> MockREST get     MockREST $ put rest      case curr of-      Left e -> return $ Left (e, e)+      Left e -> error $ "Mocked error: " ++ Text.unpack e       Right v -> do         result <- case fromJSON (String v) <|> fromJSON (Array $ fromList [String v]) of           Success a -> return a@@ -117,6 +121,6 @@          let pageLinks = case rest of               [] -> mempty-              _ -> mempty { pageNext = Just "/" }+              _ -> mempty{pageNext = Just "/"} -        return $ Right (result, pageLinks)+        return (result, pageLinks)
test/PageLinks.hs view
@@ -5,6 +5,7 @@ module PageLinks (tests) where  import Data.Maybe (catMaybes)+ #if !MIN_VERSION_base(4,11,0) import Data.Monoid ((<>)) #endif@@ -19,12 +20,14 @@ tests =   [ testProperty "parsePageLinks" $       forAll genPageLinks $ \(pageFirst, pagePrev, pageNext, pageLast) -> do-        pageLinks <- shuffle $ catMaybes-          [ mkPageLink "first" <$> pageFirst-          , mkPageLink "prev" <$> pagePrev-          , mkPageLink "next" <$> pageNext-          , mkPageLink "last" <$> pageLast-          ]+        pageLinks <-+          shuffle $+            catMaybes+              [ mkPageLink "first" <$> pageFirst+              , mkPageLink "prev" <$> pagePrev+              , mkPageLink "next" <$> pageNext+              , mkPageLink "last" <$> pageLast+              ]          return $ parsePageLinks (Text.intercalate ", " pageLinks) === PageLinks{..}   ]@@ -37,4 +40,4 @@   where     genPageLink = liftArbitrary $ Text.pack <$> genUrl     genUrl = ('/' :) <$> listOf (elements urlChars)-    urlChars = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ "-_.~?=+%&/"+    urlChars = ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['0' .. '9'] ++ "-_.~?=+%&/"
test/Query.hs view
@@ -30,24 +30,27 @@ goldens :: TestName -> String -> GitHubT IO ByteString -> TestTree goldens name fp action = goldenVsString name ("test/goldens/" ++ fp) $ runGitHubT state action   where-    state = GitHubState-      { token = Nothing-      , userAgent = "github-rest"-      , apiVersion = "v3"-      }+    state =+      GitHubSettings+        { token = Nothing+        , userAgent = "github-rest"+        , apiVersion = "v3"+        }  showResult :: Monad m => m (Either Value Value) -> m ByteString-showResult m = m >>= \case-  Right v -> error $ "Got back invalid result: " ++ show v-  Left e -> return $ Char8.pack $ show e+showResult m =+  m >>= \case+    Right v -> error $ "Got back invalid result: " ++ show v+    Left e -> return $ Char8.pack $ show e  getGist :: Int -> String -> GHEndpoint-getGist gistId gistSha = GHEndpoint-  { method = GET-  , endpoint = "/gists/:gist_id/:sha"-  , endpointVals =-      [ "gist_id" := gistId-      , "sha" := gistSha-      ]-  , ghData = []-  }+getGist gistId gistSha =+  GHEndpoint+    { method = GET+    , endpoint = "/gists/:gist_id/:sha"+    , endpointVals =+        [ "gist_id" := gistId+        , "sha" := gistSha+        ]+    , ghData = []+    }