graphql-client 1.1.1 → 1.2.0
raw patch · 18 files changed
+433/−332 lines, 18 filesdep ~basedep ~template-haskellPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: base, template-haskell
API changes (from Hackage documentation)
- Data.GraphQL.Monad: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader Data.GraphQL.Monad.QueryState (Data.GraphQL.Monad.GraphQLQueryT m)
- Data.GraphQL.Query: type family ResultSchema query :: Schema;
+ Data.GraphQL.Monad: class Monad m => MonadGraphQLQuery m
+ Data.GraphQL.Monad: data GraphQLManager
+ Data.GraphQL.Monad: initGraphQLManager :: GraphQLSettings -> IO GraphQLManager
+ Data.GraphQL.Monad: runQuery :: (MonadIO m, MonadGraphQLQuery m, GraphQLQuery query, schema ~ ResultSchema query) => query -> m (Object schema)
+ Data.GraphQL.Monad: runQuerySafe :: (MonadGraphQLQuery m, GraphQLQuery query, schema ~ ResultSchema query) => query -> m (GraphQLResult (Object schema))
+ Data.GraphQL.Monad: runQuerySafeIO :: (GraphQLQuery query, schema ~ ResultSchema query) => GraphQLManager -> query -> IO (GraphQLResult (Object schema))
+ Data.GraphQL.Query: type ResultSchema query :: Schema;
- Data.GraphQL.Bootstrap: (.=) :: (KeyValue kv, ToJSON v) => Text -> v -> kv
+ Data.GraphQL.Bootstrap: (.=) :: (KeyValue kv, ToJSON v) => Key -> v -> kv
Files
- CHANGELOG.md +12/−4
- README.md +3/−3
- exe/Codegen.hs +48/−35
- graphql-client.cabal +23/−25
- js/graphql-codegen-haskell-fallback.js +4/−4
- js/graphql-codegen-haskell.js +31/−15
- src/Data/GraphQL.hs +1/−2
- src/Data/GraphQL/Bootstrap.hs +4/−5
- src/Data/GraphQL/Error.hs +19/−17
- src/Data/GraphQL/Monad.hs +132/−90
- src/Data/GraphQL/Monad/Class.hs +25/−22
- src/Data/GraphQL/Query.hs +29/−26
- src/Data/GraphQL/Result.hs +13/−12
- src/Data/GraphQL/TestUtils.hs +27/−24
- test/Data/GraphQL/Test/Monad/Class.hs +17/−17
- test/Data/GraphQL/Test/TestQuery.hs +4/−2
- test/Data/GraphQL/Test/TestUtils.hs +34/−25
- test/Main.hs +7/−4
CHANGELOG.md view
@@ -1,12 +1,20 @@-## Upcoming+# v1.2.0 -## 1.1.1+Breaking changes: +* Remove support for GHC < 8.10++New features:++* Added `runQuerySafeIO` and expose `GraphQLManager` for applications that want to manually implement `MonadGraphQLQuery`++# v1.1.1+ Bug fixes: * Generate enums that only appear in query arguments ([#59](https://github.com/LeapYear/graphql-client/pull/59)) -## 1.1.0+# v1.1.0 Breaking changes: @@ -22,7 +30,7 @@ * Add `Show` instance for `AnyResultMock` * Add `MonadTrans` instance for `MockQueryT` -## 1.0.0+# v1.0.0 Initial release:
README.md view
@@ -1,8 +1,8 @@ # graphql-client ---[](https://codecov.io/gh/LeapYear/graphql-client)+[](https://github.com/LeapYear/graphql-client/actions?query=branch%3Amain)+[](https://codecov.io/gh/LeapYear/graphql-client)+[](https://hackage.haskell.org/package/graphql-client) A client for Haskell applications to query [GraphQL](https://graphql.org) APIs. This package provides two resources:
exe/Codegen.hs view
@@ -15,56 +15,68 @@ import Options.Applicative import Path import Path.IO (doesFileExist, ensureDir, resolveFile', withSystemTempDir)-import System.Exit (ExitCode(..))+import System.Exit (ExitCode (..)) import System.Process.Typed (proc, readProcess, runProcess_) data CliOptions = CliOptions- { cliNode :: Maybe FilePath+ { cliNode :: Maybe FilePath , cliConfig :: FilePath } parseCliOptions :: Parser CliOptions-parseCliOptions = CliOptions- <$> parseCliNode- <*> parseCliConfig+parseCliOptions =+ CliOptions+ <$> parseCliNode+ <*> parseCliConfig where- parseCliNode = optional $ strOption $ mconcat- [ long "node"- , metavar "NODE"- , help "Path to Node.JS executable (default to finding on PATH)"- ]- parseCliConfig = strOption $ mconcat- [ long "config"- , short 'c'- , metavar "CONFIG"- , help "Path to codegen.yml file (defaults to ./codegen.yml)"- , value "./codegen.yml"- ]+ parseCliNode =+ optional $+ strOption $+ mconcat+ [ long "node"+ , metavar "NODE"+ , help "Path to Node.JS executable (default to finding on PATH)"+ ]+ parseCliConfig =+ strOption $+ mconcat+ [ long "config"+ , short 'c'+ , metavar "CONFIG"+ , help "Path to codegen.yml file (defaults to ./codegen.yml)"+ , value "./codegen.yml"+ ] graphqlCodegenScript :: ByteString-graphqlCodegenScript = $(do- let script = [relfile|js/graphql-codegen-haskell.js|]- fallbackScript = [relfile|js/graphql-codegen-haskell-fallback.js|]+graphqlCodegenScript =+ $( do+ let script = [relfile|js/graphql-codegen-haskell.js|]+ fallbackScript = [relfile|js/graphql-codegen-haskell-fallback.js|] - scriptExists <- runIO $ doesFileExist script- embedFile $ toFilePath $ if scriptExists then script else fallbackScript- )+ scriptExists <- runIO $ doesFileExist script+ embedFile $ toFilePath $ if scriptExists then script else fallbackScript+ ) mockedLibraries :: [(Path Rel File, ByteString)]-mockedLibraries = $(do- let dir = "js/mocks/"- files <- runIO $ getDir dir- listE $ flip map files $ \(fp, content) -> do- addDependentFile $ dir ++ fp- tupE [mkRelFile fp, bsToExp content]- )+mockedLibraries =+ $( do+ let dir = "js/mocks/"+ files <- runIO $ getDir dir+ listE $+ flip map files $ \(fp, content) -> do+ addDependentFile $ dir ++ fp+ tupE [mkRelFile fp, bsToExp content]+ ) main :: IO () main = do- CliOptions{..} <- execParser $ info (parseCliOptions <**> helper) $ mconcat- [ fullDesc- , progDesc "Generate Haskell definitions from .graphql files"- ]+ CliOptions{..} <-+ execParser $+ info (parseCliOptions <**> helper) $+ mconcat+ [ fullDesc+ , progDesc "Generate Haskell definitions from .graphql files"+ ] withSystemTempDir "graphql-codegen" $ \tmpDir -> do let graphqlCodegen = tmpDir </> [relfile|graphql-codegen-haskell.js|]@@ -80,7 +92,8 @@ configFileExists <- doesFileExist configFile unless configFileExists $- errorWithoutStackTrace $ "Config file doesn't exist: " ++ toFilePath configFile+ errorWithoutStackTrace $+ "Config file doesn't exist: " ++ toFilePath configFile try @SomeException (readProcess $ proc nodeExe ["-e", "console.log('TEST')"]) >>= \case Right (ExitSuccess, "TEST\n", _) -> return ()
graphql-client.cabal view
@@ -1,13 +1,11 @@ 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.35.0. -- -- see: https://github.com/sol/hpack------ hash: c25accf7e3c21d0afe69d04182d67865d8683906b4a860ad622fb99dcfcaa63d name: graphql-client-version: 1.1.1+version: 1.2.0 synopsis: A client for Haskell programs to query a GraphQL API description: A client for Haskell programs to query a GraphQL API. category: Graphql@@ -46,22 +44,22 @@ src ghc-options: -Wall build-depends:- aeson >=1.2.4.0 && <1.6- , aeson-schemas >=1.3 && <1.4- , base >=4.9 && <5+ aeson >=1.2.4.0 && <3+ , aeson-schemas >=1.3 && <1.5+ , base >=4.14 && <5 , http-client >=0.5.13.1 && <0.8 , http-client-tls >=0.3.5.3 && <0.4 , http-types >=0.12.1 && <0.13 , mtl >=2.2.2 && <2.3- , template-haskell >=2.12.0.0 && <3+ , template-haskell >=2.16 && <3 , text >=1.2.3.0 && <1.3 , transformers >=0.5.2.0 && <0.6 , unliftio-core >=0.1.1.0 && <0.3+ default-language: Haskell2010 if impl(ghc >= 8.0) ghc-options: -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wnoncanonical-monad-instances if impl(ghc < 8.8) ghc-options: -Wnoncanonical-monadfail-instances- default-language: Haskell2010 executable graphql-codegen main-is: exe/Codegen.hs@@ -69,29 +67,29 @@ Paths_graphql_client ghc-options: -Wall build-depends:- aeson >=1.2.4.0 && <1.6- , aeson-schemas >=1.3 && <1.4- , base >=4.9 && <5- , bytestring >=0.10.8.2 && <0.11- , file-embed >=0.0.10.1 && <0.0.14+ aeson >=1.2.4.0 && <3+ , aeson-schemas >=1.3 && <1.5+ , base >=4.14 && <5+ , bytestring >=0.10.8.2 && <0.12+ , file-embed >=0.0.10.1 && <0.1 , graphql-client , http-client >=0.5.13.1 && <0.8 , http-client-tls >=0.3.5.3 && <0.4 , http-types >=0.12.1 && <0.13 , mtl >=2.2.2 && <2.3- , optparse-applicative >=0.14.2.0 && <0.16.2- , path >=0.6.1 && <0.8.0- , path-io >=1.3.3 && <1.7.0- , template-haskell >=2.12.0.0 && <3+ , optparse-applicative >=0.14.2.0 && <0.18+ , path >=0.6.1 && <0.10+ , path-io >=1.3.3 && <1.8+ , template-haskell >=2.16 && <3 , text >=1.2.3.0 && <1.3 , transformers >=0.5.2.0 && <0.6- , typed-process >=0.2.3.0 && <0.2.7+ , typed-process >=0.2.3.0 && <0.3 , unliftio-core >=0.1.1.0 && <0.3+ default-language: Haskell2010 if impl(ghc >= 8.0) ghc-options: -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wnoncanonical-monad-instances if impl(ghc < 8.8) ghc-options: -Wnoncanonical-monadfail-instances- default-language: Haskell2010 test-suite graphql-client-test type: exitcode-stdio-1.0@@ -105,9 +103,9 @@ test ghc-options: -Wall build-depends:- aeson >=1.2.4.0 && <1.6- , aeson-schemas >=1.3 && <1.4- , base >=4.9 && <5+ aeson >=1.2.4.0 && <3+ , aeson-schemas >=1.3 && <1.5+ , base >=4.14 && <5 , graphql-client , http-client >=0.5.13.1 && <0.8 , http-client-tls >=0.3.5.3 && <0.4@@ -115,12 +113,12 @@ , mtl >=2.2.2 && <2.3 , tasty , tasty-hunit- , template-haskell >=2.12.0.0 && <3+ , template-haskell >=2.16 && <3 , text >=1.2.3.0 && <1.3 , transformers >=0.5.2.0 && <0.6 , unliftio-core >=0.1.1.0 && <0.3+ default-language: Haskell2010 if impl(ghc >= 8.0) ghc-options: -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wnoncanonical-monad-instances if impl(ghc < 8.8) ghc-options: -Wnoncanonical-monadfail-instances- default-language: Haskell2010
js/graphql-codegen-haskell-fallback.js view
@@ -21,12 +21,12 @@ the GRAPHQL_CODEGEN environment variable to the location of that bundle. You can get the bundle in one of the following ways: - 1. Download from CircleCI (recommended)+ 1. Download from CI (recommended) a. Go to ${utils.GITHUB_URL} b. Navigate to the appropriate commit- c. Click on the commit status icon and click on the 'test_latest' CI job- d. Go to the Artifacts tab- e. Download 'graphql-codegen-haskell.js'+ c. Click on the commit status icon and click on the any of the CI links+ d. Click on the "Summary" tab+ e. Under "Artifacts", download 'graphql-codegen-haskell.js' 2. Build manually a. Clone ${utils.GITHUB_URL} b. Follow the instructions in DEVELOPER.md to build the bundle
js/graphql-codegen-haskell.js view
@@ -74,21 +74,7 @@ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ -/* global Reflect, Promise */ -var extendStatics = function(d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; - return extendStatics(d, b); -}; - -function __extends(d, b) { - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); -} - var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { @@ -138,6 +124,7 @@ } } +/** @deprecated */ function __spreadArrays() { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) @@ -15020,6 +15007,35 @@ findDeprecatedUsages: findDeprecatedUsages }); +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +/* global Reflect, Promise */ + +var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); +}; + +function __extends(d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}+ var cleanInternalStack = function (stack) { return stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); }; /** @@ -53360,7 +53376,7 @@ }))); }); -var template = "{- This file was automatically generated and should not be edited. -}\n\n{-# LANGUAGE DataKinds #-}\n{-# LANGUAGE DuplicateRecordFields #-}\n{-# LANGUAGE OverloadedStrings #-}\n{-# LANGUAGE QuasiQuotes #-}\n{-# LANGUAGE TypeFamilies #-}\n{-# OPTIONS_GHC -w #-}\n\nmodule {{apiModule}} where\n\nimport Data.GraphQL\nimport Data.GraphQL.Bootstrap\n\nimport {{scalarsModule}}\n{{#enumModules}}\nimport {{.}}\n{{/enumModules}}\n\n{{#operations}}\n{-----------------------------------------------------------------------------\n* {{name}}\n\n-- result :: Object {{schemaType}}; throws a GraphQL exception on errors\nresult <- runQuery {{queryName}}\n {{#overArgs}}\n { _{{arg}} = ...\n {{/overArgs}}\n }\n\n-- result :: GraphQLResult (Object {{schemaType}})\nresult <- runQuerySafe {{queryName}}\n {{#overArgs}}\n { _{{arg}} = ...\n {{/overArgs}}\n }\n-----------------------------------------------------------------------------}\n\ndata {{queryName}} = {{queryName}}\n {{#overArgs}}\n { _{{arg}} :: {{type}}\n {{/overArgs}}\n }\n deriving (Show)\n\ntype {{schemaType}} = [schema|\n {{schema}}\n|]\n\ninstance GraphQLQuery {{queryName}} where\n type ResultSchema {{queryName}} = {{schemaType}}\n\n getQueryName _ = \"{{name}}\"\n\n getQueryText _ = [query|\n {{queryText}}\n |]\n\n getArgs query = object\n {{#overArgs}}\n [ \"{{arg}}\" .= _{{arg}} (query :: {{queryName}})\n {{/overArgs}}\n ]\n\n{{/operations}}\n";+var template = "{- This file was automatically generated and should not be edited. -}\n\n{-# LANGUAGE DataKinds #-}\n{-# LANGUAGE DuplicateRecordFields #-}\n{-# LANGUAGE OverloadedStrings #-}\n{-# LANGUAGE QuasiQuotes #-}\n{-# LANGUAGE TypeFamilies #-}\n{-# OPTIONS_GHC -w #-}\n\nmodule {{apiModule}} where\n\nimport Data.GraphQL\nimport Data.GraphQL.Bootstrap\n\nimport {{scalarsModule}}\n{{#enumModules}}\nimport {{.}}\n{{/enumModules}}\n\n{{#operations}}\n{-----------------------------------------------------------------------------\n-- {{name}}\n\n-- result :: Object {{schemaType}}; throws a GraphQL exception on errors\nresult <- runQuery {{queryName}}\n {{#overArgs}}\n { _{{arg}} = ...\n {{/overArgs}}\n }\n\n-- result :: GraphQLResult (Object {{schemaType}})\nresult <- runQuerySafe {{queryName}}\n {{#overArgs}}\n { _{{arg}} = ...\n {{/overArgs}}\n }\n-----------------------------------------------------------------------------}\n\ndata {{queryName}} = {{queryName}}\n {{#overArgs}}\n { _{{arg}} :: {{type}}\n {{/overArgs}}\n }\n deriving (Show)\n\ntype {{schemaType}} = [schema|\n {{schema}}\n|]\n\ninstance GraphQLQuery {{queryName}} where\n type ResultSchema {{queryName}} = {{schemaType}}\n\n getQueryName _ = \"{{name}}\"\n\n getQueryText _ = [query|\n {{queryText}}\n |]\n\n getArgs query = object\n {{#overArgs}}\n [ \"{{arg}}\" .= _{{arg}} (query :: {{queryName}})\n {{/overArgs}}\n ]\n\n{{/operations}}\n"; var renderAPIModule = function (config, enumModules, operations) { return mustache.render(template, __assign(__assign({}, config), { enumModules: enumModules, operations: operations.map(function (operation) { return (__assign(__assign({}, operation), { queryText: indent$3(indent$3(operation.queryText)), schema: indent$3(renderAesonSchema(operation.schema)), args: operation.args.map(function (_a) {
src/Data/GraphQL.hs view
@@ -1,4 +1,4 @@-{-|+{- | Module : Data.GraphQL Maintainer : Brandon Chinn <brandon@leapyear.io> Stability : experimental@@ -6,7 +6,6 @@ Core functionality for querying GraphQL APIs. -}- module Data.GraphQL (module X) where import Data.Aeson.Schema as X
src/Data/GraphQL/Bootstrap.hs view
@@ -1,4 +1,4 @@-{-|+{- | Module : Data.GraphQL.Bootstrap Maintainer : Brandon Chinn <brandon@leapyear.io> Stability : experimental@@ -6,10 +6,9 @@ Imports needed for the generated API. -}--module Data.GraphQL.Bootstrap- ( module X- ) where+module Data.GraphQL.Bootstrap (+ module X,+) where import Control.Monad.IO.Class as X (MonadIO) import Data.Aeson as X (object, (.=))
src/Data/GraphQL/Error.hs view
@@ -1,4 +1,8 @@-{-|+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++{- | Module : Data.GraphQL.Error Maintainer : Brandon Chinn <brandon@leapyear.io> Stability : experimental@@ -6,33 +10,31 @@ Definitions for GraphQL errors and exceptions. -}-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.GraphQL.Error- ( GraphQLError(..)- , GraphQLErrorLoc(..)- , GraphQLException(..)- ) where+module Data.GraphQL.Error (+ GraphQLError (..),+ GraphQLErrorLoc (..),+ GraphQLException (..),+) where import Control.Exception (Exception)-import Data.Aeson (FromJSON(..), ToJSON, Value, withObject, (.:))+import Data.Aeson (FromJSON (..), ToJSON, Value, withObject, (.:)) import Data.Text (Text) import GHC.Generics (Generic) -- | An error in a GraphQL query. data GraphQLError = GraphQLError- { message :: Text+ { message :: Text , locations :: Maybe [GraphQLErrorLoc]- , path :: Maybe [Value]- } deriving (Show,Eq,Generic,ToJSON,FromJSON)+ , path :: Maybe [Value]+ }+ deriving (Show, Eq, Generic, ToJSON, FromJSON) -- | A location in an error in a GraphQL query. data GraphQLErrorLoc = GraphQLErrorLoc { errorLine :: Int- , errorCol :: Int- } deriving (Show,Eq,Generic,ToJSON)+ , errorCol :: Int+ }+ deriving (Show, Eq, Generic, ToJSON) instance FromJSON GraphQLErrorLoc where parseJSON = withObject "GraphQLErrorLoc" $ \o ->@@ -42,4 +44,4 @@ -- | An exception thrown as a result of an error in a GraphQL query. newtype GraphQLException = GraphQLException [GraphQLError]- deriving (Show,Exception,Eq)+ deriving (Show, Exception, Eq)
src/Data/GraphQL/Monad.hs view
@@ -1,77 +1,154 @@-{-|+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++{- | Module : Data.GraphQL.Monad Maintainer : Brandon Chinn <brandon@leapyear.io> Stability : experimental Portability : portable -Defines the 'GraphQLQueryT' monad transformer, which implements-'MonadGraphQLQuery' to allow querying GraphQL APIs.+Defines the 'MonadGraphQLQuery' type class to query GraphQL APIs.+Also provides the 'GraphQLQueryT' monad transformer that can be+added to a transformer stack to implement the type class, and the+'runQuerySafeIO' function to manually implement it yourself. -}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE RecordWildCards #-}+module Data.GraphQL.Monad (+ -- * MonadGraphQLQuery API+ MonadGraphQLQuery (..),+ runQuery,+ runQuerySafeIO, -module Data.GraphQL.Monad- ( module Data.GraphQL.Monad.Class- , GraphQLQueryT- , runGraphQLQueryT- , GraphQLSettings(..)- , defaultGraphQLSettings- ) where+ -- * GraphQLSettings+ GraphQLSettings (..),+ defaultGraphQLSettings, -import Control.Monad.IO.Class (MonadIO(..))-import Control.Monad.IO.Unlift (MonadUnliftIO(..))-import Control.Monad.Reader (MonadReader, ReaderT, ask, runReaderT)+ -- * GraphQLManager+ GraphQLManager,+ initGraphQLManager,++ -- * GraphQLQueryT+ GraphQLQueryT,+ runGraphQLQueryT,+) where++import Control.Monad.IO.Class (MonadIO (..))+import Control.Monad.IO.Unlift (MonadUnliftIO (..))+import Control.Monad.Reader (ReaderT, ask, runReaderT) import Control.Monad.Trans.Class (MonadTrans) import Data.Aeson ((.=)) import qualified Data.Aeson as Aeson-import Network.HTTP.Client- ( Manager- , ManagerSettings- , Request(..)- , RequestBody(..)- , httpLbs- , newManager- , parseUrlThrow- , responseBody- )+import Data.Aeson.Schema (Object)+import Network.HTTP.Client (+ Manager,+ ManagerSettings,+ Request (..),+ RequestBody (..),+ httpLbs,+ newManager,+ parseUrlThrow,+ responseBody,+ ) import Network.HTTP.Client.TLS (tlsManagerSettings) import Network.HTTP.Types (hContentType) import Data.GraphQL.Monad.Class-import Data.GraphQL.Query (GraphQLQuery(..))+import Data.GraphQL.Query (GraphQLQuery (..))+import Data.GraphQL.Result (GraphQLResult) --- | The state for running GraphQLQueryT.-data QueryState = QueryState+{- GraphQLSettings -}++-- | The settings for initializing a 'GraphQLManager'.+data GraphQLSettings = GraphQLSettings+ { managerSettings :: ManagerSettings+ -- ^ Uses TLS by default+ , url :: String+ , modifyReq :: Request -> Request+ }++{- | Default settings for 'GraphQLSettings'. Requires 'url' field to be overridden.++ Example usage:++ >>> defaultGraphQLSettings+ ... { url = "https://api.github.com/graphql"+ ... , modifyReq = \\req -> req+ ... { requestHeaders =+ ... (hAuthorization, "bearer my_github_token") : requestHeaders req+ ... }+ ... }+-}+defaultGraphQLSettings :: GraphQLSettings+defaultGraphQLSettings =+ GraphQLSettings+ { managerSettings = tlsManagerSettings+ , url = error "No URL is provided"+ , modifyReq = id+ }++{- The base runQuerySafeIO implementation -}++-- | The manager for running GraphQL queries.+data GraphQLManager = GraphQLManager { manager :: Manager , baseReq :: Request } --- | The monad transformer type that should be used to run GraphQL queries.------ @--- newtype MyMonad a = MyMonad { unMyMonad :: GraphQLQueryT IO a }------ runMyMonad :: MyMonad a -> IO a--- runMyMonad = runGraphQLQueryT graphQLSettings . unMyMonad--- where--- graphQLSettings = defaultGraphQLSettings--- { url = "https://api.github.com/graphql"--- , modifyReq = \\req -> req--- { requestHeaders =--- (hAuthorization, "bearer my_github_token") : requestHeaders req--- }--- }--- @-newtype GraphQLQueryT m a = GraphQLQueryT { unGraphQLQueryT :: ReaderT QueryState m a }+initGraphQLManager :: GraphQLSettings -> IO GraphQLManager+initGraphQLManager GraphQLSettings{..} = do+ manager <- newManager managerSettings+ baseReq <- modifyReq . modifyReq' <$> parseUrlThrow url+ return GraphQLManager{..}+ where+ modifyReq' req =+ req+ { method = "POST"+ , requestHeaders = (hContentType, "application/json") : requestHeaders req+ }++-- | Execute a GraphQL query with the given 'GraphQLManager'.+runQuerySafeIO ::+ (GraphQLQuery query, schema ~ ResultSchema query) =>+ GraphQLManager ->+ query ->+ IO (GraphQLResult (Object schema))+runQuerySafeIO GraphQLManager{..} query = httpLbs request manager >>= decodeBody+ where+ request =+ baseReq+ { requestBody =+ RequestBodyLBS $+ Aeson.encode $+ Aeson.object+ [ "query" .= getQueryText query+ , "variables" .= getArgs query+ ]+ }++ decodeBody = either fail return . Aeson.eitherDecode . responseBody++{- GraphQLQueryT monad transformer -}++{- | The monad transformer type that can be used to run GraphQL queries.++ @+ newtype MyMonad a = MyMonad { unMyMonad :: GraphQLQueryT IO a }++ runMyMonad :: MyMonad a -> IO a+ runMyMonad = runGraphQLQueryT graphQLSettings . unMyMonad+ where+ graphQLSettings = defaultGraphQLSettings{url = "https://api.github.com/graphql"}+ @+-}+newtype GraphQLQueryT m a = GraphQLQueryT {unGraphQLQueryT :: ReaderT GraphQLManager m a} deriving ( Functor , Applicative , Monad , MonadIO- , MonadReader QueryState , MonadTrans ) @@ -80,46 +157,11 @@ instance MonadIO m => MonadGraphQLQuery (GraphQLQueryT m) where runQuerySafe query = do- QueryState{..} <- ask-- let request = baseReq- { requestBody = RequestBodyLBS $ Aeson.encode $ Aeson.object- [ "query" .= getQueryText query- , "variables" .= getArgs query- ]- }-- liftIO $ either fail return . Aeson.eitherDecode . responseBody =<< httpLbs request manager+ manager <- GraphQLQueryT ask+ liftIO $ runQuerySafeIO manager query --- | Run a GraphQLQueryT stack.+-- | Run the GraphQLQueryT monad transformer. runGraphQLQueryT :: MonadIO m => GraphQLSettings -> GraphQLQueryT m a -> m a-runGraphQLQueryT GraphQLSettings{..} m = do- state <- liftIO $ do- manager <- newManager managerSettings- baseReq <- modifyReq . modifyReq' <$> parseUrlThrow url- return QueryState{..}-- (`runReaderT` state)- . unGraphQLQueryT- $ m- where- modifyReq' req = req- { method = "POST"- , requestHeaders = (hContentType, "application/json") : requestHeaders req- }---- | The settings for running GraphQLQueryT.-data GraphQLSettings = GraphQLSettings- { managerSettings :: ManagerSettings- -- ^ Uses TLS by default- , url :: String- , modifyReq :: Request -> Request- }---- | Default query settings.-defaultGraphQLSettings :: GraphQLSettings-defaultGraphQLSettings = GraphQLSettings- { managerSettings = tlsManagerSettings- , url = error "No URL is provided"- , modifyReq = id- }+runGraphQLQueryT settings m = do+ manager <- liftIO $ initGraphQLManager settings+ (`runReaderT` manager) . unGraphQLQueryT $ m
src/Data/GraphQL/Monad/Class.hs view
@@ -1,4 +1,10 @@-{-|+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeFamilies #-}++{- | Module : Data.GraphQL.Monad.Class Maintainer : Brandon Chinn <brandon@leapyear.io> Stability : experimental@@ -6,26 +12,20 @@ Defines the 'MonadGraphQLQuery' type class, which defines how GraphQL queries should be run. -}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE TypeFamilies #-}--module Data.GraphQL.Monad.Class- ( MonadGraphQLQuery(..)- , runQuery- ) where+module Data.GraphQL.Monad.Class (+ MonadGraphQLQuery (..),+ runQuery,+) where import Control.Exception (throwIO)-import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.IO.Class (MonadIO (..)) 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@@ -33,20 +33,23 @@ import Data.Aeson.Schema (Object) import Data.Maybe (fromJust) -import Data.GraphQL.Error (GraphQLException(..))-import Data.GraphQL.Query (GraphQLQuery(..))+import Data.GraphQL.Error (GraphQLException (..))+import Data.GraphQL.Query (GraphQLQuery (..)) import Data.GraphQL.Result (GraphQLResult, getErrors, getResult) -- | A type class for monads that can run GraphQL queries. class Monad m => MonadGraphQLQuery m where- runQuerySafe- :: (GraphQLQuery query, schema ~ ResultSchema query)- => query -> m (GraphQLResult (Object schema))+ -- | Run the given query and return the 'GraphQLResult'.+ runQuerySafe ::+ (GraphQLQuery query, schema ~ ResultSchema query) =>+ query ->+ m (GraphQLResult (Object schema)) --- | Runs the given query and returns the result, erroring if the query returned errors.-runQuery- :: (MonadIO m, MonadGraphQLQuery m, GraphQLQuery query, schema ~ ResultSchema query)- => query -> m (Object schema)+-- | Run the given query and returns the result, erroring if the query returned errors.+runQuery ::+ (MonadIO m, MonadGraphQLQuery m, GraphQLQuery query, schema ~ ResultSchema query) =>+ query ->+ m (Object schema) runQuery query = do result <- runQuerySafe query case getErrors result of
src/Data/GraphQL/Query.hs view
@@ -1,4 +1,9 @@-{-|+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeInType #-}++{- | Module : Data.GraphQL.Query Maintainer : Brandon Chinn <brandon@leapyear.io> Stability : experimental@@ -6,43 +11,41 @@ Definitions needed by GraphQL queries. -}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeInType #-}--module Data.GraphQL.Query- ( GraphQLQuery(..)- , query- ) where+module Data.GraphQL.Query (+ GraphQLQuery (..),+ query,+) where import Data.Aeson (Value) import Data.Aeson.Schema (IsSchema, Schema) import Data.Text (Text) import qualified Data.Text as Text-import Language.Haskell.TH.Quote (QuasiQuoter(..))+import Language.Haskell.TH.Quote (QuasiQuoter (..)) import Language.Haskell.TH.Syntax (lift) --- | A type class for defining GraphQL queries.------ Should be generated via the `graphql-codegen` command. Any manual instances needs--- to be certain that `getArgs query` satisfies the arguments defined in--- `getQueryText query`, and that the result adheres to `ResultSchema query`.+{- | A type class for defining GraphQL queries.++ Should be generated via the `graphql-codegen` command. Any manual instances needs+ to be certain that `getArgs query` satisfies the arguments defined in+ `getQueryText query`, and that the result adheres to `ResultSchema query`.+-} class IsSchema (ResultSchema query) => GraphQLQuery query where type ResultSchema query :: Schema getQueryName :: query -> Text getQueryText :: query -> Text getArgs :: query -> Value --- | A quasiquoter that interpolates the given string as raw text.------ Trying to avoid a dependency on raw-strings-qq+{- | A quasiquoter that interpolates the given string as raw text.++ Trying to avoid a dependency on raw-strings-qq+-} query :: QuasiQuoter-query = QuasiQuoter- { quoteExp = liftText . Text.strip . Text.pack- , quotePat = error "Cannot use the 'query' QuasiQuoter for patterns"- , quoteType = error "Cannot use the 'query' QuasiQuoter for types"- , quoteDec = error "Cannot use the 'query' QuasiQuoter for declarations"- }+query =+ QuasiQuoter+ { quoteExp = liftText . Text.strip . Text.pack+ , quotePat = error "Cannot use the 'query' QuasiQuoter for patterns"+ , quoteType = error "Cannot use the 'query' QuasiQuoter for types"+ , quoteDec = error "Cannot use the 'query' QuasiQuoter for declarations"+ } where- liftText s = [| Text.pack $(lift $ Text.unpack s) |]+ liftText s = [|Text.pack $(lift $ Text.unpack s)|]
src/Data/GraphQL/Result.hs view
@@ -1,4 +1,8 @@-{-|+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++{- | Module : Data.GraphQL.Result Maintainer : Brandon Chinn <brandon@leapyear.io> Stability : experimental@@ -6,17 +10,13 @@ Definitions parsing responses from a GraphQL API. -}-{-# LANGUAGE DeriveTraversable #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.GraphQL.Result- ( GraphQLResult(..)- , getErrors- , getResult- ) where+module Data.GraphQL.Result (+ GraphQLResult (..),+ getErrors,+ getResult,+) where -import Data.Aeson (FromJSON(..), withObject, (.!=), (.:?))+import Data.Aeson (FromJSON (..), withObject, (.!=), (.:?)) import Data.GraphQL.Error (GraphQLError) @@ -24,7 +24,8 @@ data GraphQLResult r = GraphQLResult { resultErrors :: [GraphQLError] , resultResult :: Maybe r- } deriving (Show,Functor,Foldable,Traversable)+ }+ deriving (Show, Functor, Foldable, Traversable) instance FromJSON r => FromJSON (GraphQLResult r) where parseJSON = withObject "GraphQLResult" $ \o ->
src/Data/GraphQL/TestUtils.hs view
@@ -1,4 +1,10 @@-{-|+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}++{- | Module : Data.GraphQL.TestUtils Maintainer : Brandon Chinn <brandon@leapyear.io> Stability : experimental@@ -6,19 +12,13 @@ Defines test utilities for testing GraphQL queries. -}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE StandaloneDeriving #-}--module Data.GraphQL.TestUtils- ( ResultMock(..)- , mocked- , MockQueryT- , runMockQueryT- , AnyResultMock- ) where+module Data.GraphQL.TestUtils (+ ResultMock (..),+ mocked,+ MockQueryT,+ runMockQueryT,+ AnyResultMock,+) where import Control.Monad.IO.Class (MonadIO) import Control.Monad.State.Strict (MonadState, StateT, evalStateT, state)@@ -28,13 +28,14 @@ import qualified Data.Text as Text import Data.GraphQL.Error (GraphQLError)-import Data.GraphQL.Monad (MonadGraphQLQuery(..))-import Data.GraphQL.Query (GraphQLQuery(..))+import Data.GraphQL.Monad (MonadGraphQLQuery (..))+import Data.GraphQL.Query (GraphQLQuery (..)) data ResultMock query = ResultMock- { query :: query+ { query :: query , result :: Value- } deriving (Show)+ }+ deriving (Show) mocked :: (Show query, GraphQLQuery query) => ResultMock query -> AnyResultMock mocked = AnyResultMock@@ -53,7 +54,7 @@ {- MockQueryT -} -newtype MockQueryT m a = MockQueryT { unMockQueryT :: StateT [AnyResultMock] m a }+newtype MockQueryT m a = MockQueryT {unMockQueryT :: StateT [AnyResultMock] m a} deriving (Functor, Applicative, Monad, MonadIO, MonadState [AnyResultMock], MonadTrans) instance Monad m => MonadGraphQLQuery (MockQueryT m) where@@ -61,7 +62,7 @@ where takeWhere :: (a -> Bool) -> [a] -> Maybe (a, [a]) takeWhere f xs = case break f xs of- (before, match:after) -> Just (match, before ++ after)+ (before, match : after) -> Just (match, before ++ after) (_, []) -> Nothing -- Find the first matching mock and remove it from the state@@ -72,10 +73,12 @@ Nothing -> error $ "No more mocked responses for query: " ++ Text.unpack (getQueryName testQuery) toGraphQLResult :: FromJSON a => Value -> a- toGraphQLResult mockData = either error id . Aeson.parseEither Aeson.parseJSON $ object- [ "errors" .= ([] :: [GraphQLError])- , "data" .= Just mockData- ]+ toGraphQLResult mockData =+ either error id . Aeson.parseEither Aeson.parseJSON $+ object+ [ "errors" .= ([] :: [GraphQLError])+ , "data" .= Just mockData+ ] runMockQueryT :: Monad m => MockQueryT m a -> [AnyResultMock] -> m a runMockQueryT mockQueryT mocks = (`evalStateT` mocks) . unMockQueryT $ mockQueryT
test/Data/GraphQL/Test/Monad/Class.hs view
@@ -15,12 +15,12 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, (@?=)) -import Data.GraphQL.Error (GraphQLError(..), GraphQLException(..))-import Data.GraphQL.Monad (MonadGraphQLQuery(..), runQuery)-import Data.GraphQL.Result (GraphQLResult(..))-import Data.GraphQL.Test.TestQuery (TestQuery(..))+import Data.GraphQL.Error (GraphQLError (..), GraphQLException (..))+import Data.GraphQL.Monad (MonadGraphQLQuery (..), runQuery)+import Data.GraphQL.Result (GraphQLResult (..))+import Data.GraphQL.Test.TestQuery (TestQuery (..)) -newtype MockQueryM a = MockQueryM { unMock :: ReaderT (Either GraphQLError Value) IO a }+newtype MockQueryM a = MockQueryM {unMock :: ReaderT (Either GraphQLError Value) IO a} deriving (Functor, Applicative, Monad, MonadIO, MonadReader (Either GraphQLError Value)) runMockQueryM :: Either GraphQLError Value -> MockQueryM a -> IO a@@ -34,15 +34,15 @@ Right v -> GraphQLResult [] $ Aeson.parseMaybe Aeson.parseJSON v testRunQuery :: TestTree-testRunQuery = testGroup "runQuery <-> runQuerySafe"- [ testCase "runQuery throws if runQuerySafe returns an error" $ do- let err = GraphQLError "Something went wrong" Nothing Nothing- result <- try $ runMockQueryM (Left err) (runQuery TestQuery)- show <$> result @?= Left (GraphQLException [err])-- , testCase "runQuery returns the result of runQuerySafe" $ do- let v = object [ "getUser" .= object [ "id" .= (1 :: Int) ] ]- result <- runMockQueryM (Right v) (runQuery TestQuery)- [get| result.getUser.id |] @?= 1- ]-+testRunQuery =+ testGroup+ "runQuery <-> runQuerySafe"+ [ testCase "runQuery throws if runQuerySafe returns an error" $ do+ let err = GraphQLError "Something went wrong" Nothing Nothing+ result <- try $ runMockQueryM (Left err) (runQuery TestQuery)+ show <$> result @?= Left (GraphQLException [err])+ , testCase "runQuery returns the result of runQuerySafe" $ do+ let v = object ["getUser" .= object ["id" .= (1 :: Int)]]+ result <- runMockQueryM (Right v) (runQuery TestQuery)+ [get| result.getUser.id |] @?= 1+ ]
test/Data/GraphQL/Test/TestQuery.hs view
@@ -11,7 +11,8 @@ data TestQuery = TestQuery deriving (Show) -type TestSchema = [schema|+type TestSchema =+ [schema| { getUser: { id: Int@@ -22,7 +23,8 @@ instance GraphQLQuery TestQuery where type ResultSchema TestQuery = TestSchema getQueryName _ = "test"- getQueryText _ = [query|+ getQueryText _ =+ [query| query test($name: String!) { getUser(name: $name) { id
test/Data/GraphQL/Test/TestUtils.hs view
@@ -13,32 +13,41 @@ import Test.Tasty.HUnit (assertFailure, testCase, (@?), (@?=)) import Data.GraphQL.Monad (runQuery)-import Data.GraphQL.Test.TestQuery (TestQuery(..))-import Data.GraphQL.TestUtils (ResultMock(..), mocked, runMockQueryT)+import Data.GraphQL.Test.TestQuery (TestQuery (..))+import Data.GraphQL.TestUtils (ResultMock (..), mocked, runMockQueryT) testTestUtils :: TestTree-testTestUtils = testGroup "TestUtils"- [ testCase "MockQueryT returns a mock" $ do- obj <- runMockQueryT runTestQuery- [ mocked ResultMock- { query = TestQuery- , result = object [ "getUser" .= object [ "id" .= (20 :: Int) ] ]- }- ]- [get| obj.getUser.id |] @?= 20- , testCase "MockQueryT errors with no matching mock" $ do- obj <- try @SomeException $ runMockQueryT runTestQuery []- case obj of- Right _ -> assertFailure "MockQueryT did not error"- Left e -> (head . lines . show) e @?= "No more mocked responses for query: test"- , testCase "MockQueryT removes mock after use" $ do- obj <- try @SomeException $ runMockQueryT (runTestQuery >> runTestQuery)- [ mocked ResultMock- { query = TestQuery- , result = object [ "getUser" .= object [ "id" .= (20 :: Int) ] ]- }- ]- isLeft obj @? "MockQueryT did not error"- ]+testTestUtils =+ testGroup+ "TestUtils"+ [ testCase "MockQueryT returns a mock" $ do+ obj <-+ runMockQueryT+ runTestQuery+ [ mocked+ ResultMock+ { query = TestQuery+ , result = object ["getUser" .= object ["id" .= (20 :: Int)]]+ }+ ]+ [get| obj.getUser.id |] @?= 20+ , testCase "MockQueryT errors with no matching mock" $ do+ obj <- try @SomeException $ runMockQueryT runTestQuery []+ case obj of+ Right _ -> assertFailure "MockQueryT did not error"+ Left e -> (head . lines . show) e @?= "No more mocked responses for query: test"+ , testCase "MockQueryT removes mock after use" $ do+ obj <-+ try @SomeException $+ runMockQueryT+ (runTestQuery >> runTestQuery)+ [ mocked+ ResultMock+ { query = TestQuery+ , result = object ["getUser" .= object ["id" .= (20 :: Int)]]+ }+ ]+ isLeft obj @? "MockQueryT did not error"+ ] where runTestQuery = runQuery TestQuery
test/Main.hs view
@@ -4,7 +4,10 @@ import Data.GraphQL.Test.TestUtils main :: IO ()-main = defaultMain $ testGroup "graphql-client"- [ testRunQuery- , testTestUtils- ]+main =+ defaultMain $+ testGroup+ "graphql-client"+ [ testRunQuery+ , testTestUtils+ ]