asana 1.0.1.0 → 1.0.1.1
raw patch · 12 files changed
+406/−368 lines, 12 filesdep ~aesondep ~aeson-casingdep ~bytestringPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: aeson, aeson-casing, bytestring, hashable, http-conduit, iso8601-time, microlens, microlens-mtl, monad-logger, mtl, scientific, text, time, unliftio, unliftio-core, unordered-containers
API changes (from Hackage documentation)
- Asana.Api.Prelude: (<$!>) :: Monad m => (a -> b) -> m a -> m b
- Asana.Api.Prelude: (<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
- Asana.Api.Prelude: (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
- Asana.Api.Prelude: ap :: Monad m => m (a -> b) -> m a -> m b
- Asana.Api.Prelude: asks :: MonadReader r m => (r -> a) -> m a
- Asana.Api.Prelude: class Monad m => MonadFix (m :: Type -> Type)
- Asana.Api.Prelude: class (Alternative m, Monad m) => MonadPlus (m :: Type -> Type)
- Asana.Api.Prelude: filterM :: Applicative m => (a -> m Bool) -> [a] -> m [a]
- Asana.Api.Prelude: fix :: (a -> a) -> a
- Asana.Api.Prelude: foldM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
- Asana.Api.Prelude: foldM_ :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m ()
- Asana.Api.Prelude: forM :: (Traversable t, Monad m) => t a -> (a -> m b) -> m (t b)
- Asana.Api.Prelude: forM_ :: (Foldable t, Monad m) => t a -> (a -> m b) -> m ()
- Asana.Api.Prelude: forever :: Applicative f => f a -> f b
- Asana.Api.Prelude: join :: Monad m => m (m a) -> m a
- Asana.Api.Prelude: liftM :: Monad m => (a1 -> r) -> m a1 -> m r
- Asana.Api.Prelude: liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
- Asana.Api.Prelude: liftM3 :: Monad m => (a1 -> a2 -> a3 -> r) -> m a1 -> m a2 -> m a3 -> m r
- Asana.Api.Prelude: liftM4 :: Monad m => (a1 -> a2 -> a3 -> a4 -> r) -> m a1 -> m a2 -> m a3 -> m a4 -> m r
- Asana.Api.Prelude: liftM5 :: Monad m => (a1 -> a2 -> a3 -> a4 -> a5 -> r) -> m a1 -> m a2 -> m a3 -> m a4 -> m a5 -> m r
- Asana.Api.Prelude: mapAndUnzipM :: Applicative m => (a -> m (b, c)) -> [a] -> m ([b], [c])
- Asana.Api.Prelude: mapReader :: (a -> b) -> Reader r a -> Reader r b
- Asana.Api.Prelude: mapReaderT :: (m a -> n b) -> ReaderT r m a -> ReaderT r n b
- Asana.Api.Prelude: mfilter :: MonadPlus m => (a -> Bool) -> m a -> m a
- Asana.Api.Prelude: mfix :: MonadFix m => (a -> m a) -> m a
- Asana.Api.Prelude: mplus :: MonadPlus m => m a -> m a -> m a
- Asana.Api.Prelude: msum :: (Foldable t, MonadPlus m) => t (m a) -> m a
- Asana.Api.Prelude: mzero :: MonadPlus m => m a
- Asana.Api.Prelude: replicateM :: Applicative m => Int -> m a -> m [a]
- Asana.Api.Prelude: replicateM_ :: Applicative m => Int -> m a -> m ()
- Asana.Api.Prelude: runReader :: Reader r a -> r -> a
- Asana.Api.Prelude: unless :: Applicative f => Bool -> f () -> f ()
- Asana.Api.Prelude: withReader :: (r' -> r) -> Reader r a -> Reader r' a
- Asana.Api.Prelude: withReaderT :: forall r' r (m :: Type -> Type) a. (r' -> r) -> ReaderT r m a -> ReaderT r' m a
- Asana.Api.Prelude: zipWithM :: Applicative m => (a -> b -> m c) -> [a] -> [b] -> m [c]
- Asana.Api.Prelude: zipWithM_ :: Applicative m => (a -> b -> m c) -> [a] -> [b] -> m ()
+ Asana.Api.Prelude: class a ~# b => (a :: k) ~ (b :: k)
+ Asana.Api.Prelude: liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
- Asana.Api.Prelude: (!!) :: [a] -> Int -> a
+ Asana.Api.Prelude: (!!) :: HasCallStack => [a] -> Int -> a
- Asana.Api.Prelude: class Bounded a
+ Asana.Api.Prelude: class () => Bounded a
- Asana.Api.Prelude: class Enum a
+ Asana.Api.Prelude: class () => Enum a
- Asana.Api.Prelude: class Eq a
+ Asana.Api.Prelude: class () => Eq a
- Asana.Api.Prelude: class Foldable (t :: TYPE LiftedRep -> Type)
+ Asana.Api.Prelude: class () => Foldable (t :: Type -> Type)
- Asana.Api.Prelude: class Functor (f :: Type -> Type)
+ Asana.Api.Prelude: class () => Functor (f :: Type -> Type)
- Asana.Api.Prelude: class Generic a
+ Asana.Api.Prelude: class () => Generic a
- Asana.Api.Prelude: class MonadTrans (t :: Type -> Type -> Type -> Type)
+ Asana.Api.Prelude: class forall (m :: Type -> Type). Monad m => Monad t m => MonadTrans (t :: Type -> Type -> Type -> Type)
- Asana.Api.Prelude: class Num a
+ Asana.Api.Prelude: class () => Num a
- Asana.Api.Prelude: class Read a
+ Asana.Api.Prelude: class () => Read a
- Asana.Api.Prelude: class Semigroup a
+ Asana.Api.Prelude: class () => Semigroup a
- Asana.Api.Prelude: class Show a
+ Asana.Api.Prelude: class () => Show a
- Asana.Api.Prelude: class ToLogStr msg
+ Asana.Api.Prelude: class () => ToLogStr msg
- Asana.Api.Prelude: cycle :: [a] -> [a]
+ Asana.Api.Prelude: cycle :: HasCallStack => [a] -> [a]
- Asana.Api.Prelude: data Bool
+ Asana.Api.Prelude: data () => Bool
- Asana.Api.Prelude: data ByteString
+ Asana.Api.Prelude: data () => ByteString
- Asana.Api.Prelude: data Char
+ Asana.Api.Prelude: data () => Char
- Asana.Api.Prelude: data Double
+ Asana.Api.Prelude: data () => Double
- Asana.Api.Prelude: data Either a b
+ Asana.Api.Prelude: data () => Either a b
- Asana.Api.Prelude: data Float
+ Asana.Api.Prelude: data () => Float
- Asana.Api.Prelude: data IO a
+ Asana.Api.Prelude: data () => IO a
- Asana.Api.Prelude: data Int
+ Asana.Api.Prelude: data () => Int
- Asana.Api.Prelude: data Integer
+ Asana.Api.Prelude: data () => Integer
- Asana.Api.Prelude: data Loc
+ Asana.Api.Prelude: data () => Loc
- Asana.Api.Prelude: data LogLevel
+ Asana.Api.Prelude: data () => LogLevel
- Asana.Api.Prelude: data LogStr
+ Asana.Api.Prelude: data () => LogStr
- Asana.Api.Prelude: data Maybe a
+ Asana.Api.Prelude: data () => Maybe a
- Asana.Api.Prelude: data Ordering
+ Asana.Api.Prelude: data () => Ordering
- Asana.Api.Prelude: data Text
+ Asana.Api.Prelude: data () => Text
- Asana.Api.Prelude: data Word
+ Asana.Api.Prelude: data () => Word
- Asana.Api.Prelude: head :: [a] -> a
+ Asana.Api.Prelude: head :: HasCallStack => [a] -> a
- Asana.Api.Prelude: infixl 4 <$!>
+ Asana.Api.Prelude: infixl 4 <$>
- Asana.Api.Prelude: infixl 6 -
+ Asana.Api.Prelude: infixl 6 +
- Asana.Api.Prelude: infixr 8 ^
+ Asana.Api.Prelude: infixr 8 ^^
- Asana.Api.Prelude: init :: [a] -> [a]
+ Asana.Api.Prelude: init :: HasCallStack => [a] -> [a]
- Asana.Api.Prelude: last :: [a] -> a
+ Asana.Api.Prelude: last :: HasCallStack => [a] -> a
- Asana.Api.Prelude: newtype LoggingT (m :: Type -> Type) a
+ Asana.Api.Prelude: newtype () => LoggingT (m :: Type -> Type) a
- Asana.Api.Prelude: newtype NoLoggingT (m :: Type -> Type) a
+ Asana.Api.Prelude: newtype () => NoLoggingT (m :: Type -> Type) a
- Asana.Api.Prelude: newtype ReaderT r (m :: Type -> Type) a
+ Asana.Api.Prelude: newtype () => ReaderT r (m :: Type -> Type) a
- Asana.Api.Prelude: newtype WriterLoggingT (m :: Type -> Type) a
+ Asana.Api.Prelude: newtype () => WriterLoggingT (m :: Type -> Type) a
- Asana.Api.Prelude: tail :: [a] -> [a]
+ Asana.Api.Prelude: tail :: HasCallStack => [a] -> [a]
Files
- CHANGELOG.md +5/−1
- asana.cabal +19/−19
- library/Asana/Api/CustomField.hs +37/−35
- library/Asana/Api/Gid.hs +14/−9
- library/Asana/Api/Named.hs +6/−6
- library/Asana/Api/Prelude.hs +20/−6
- library/Asana/Api/Project.hs +7/−7
- library/Asana/Api/Request.hs +173/−165
- library/Asana/Api/Tag.hs +6/−6
- library/Asana/Api/Task.hs +83/−79
- library/Asana/Api/Task/Search.hs +34/−34
- package.yaml +2/−1
CHANGELOG.md view
@@ -1,4 +1,8 @@-## [_Unreleased_](https://github.com/freckle/asana-hs/compare/v1.0.1.0...main)+## [_Unreleased_](https://github.com/freckle/asana-hs/compare/v1.0.1.1...main)++## [v1.0.1.1](https://github.com/freckle/asana-hs/compare/v1.0.1.0...v1.0.1.1)++- Add support for GHC 9.6 ## [v1.0.1.0](https://github.com/freckle/asana-hs/compare/v1.0.0.0...v1.0.1.0)
asana.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.18 name: asana-version: 1.0.1.0+version: 1.0.1.1 license: MIT license-file: LICENSE maintainer: Freckle Education@@ -40,8 +40,8 @@ FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings RankNTypes- RecordWildCards ScopedTypeVariables StandaloneDeriving- TypeApplications TypeFamilies+ RecordWildCards RoleAnnotations ScopedTypeVariables+ StandaloneDeriving TypeApplications TypeFamilies ghc-options: -Weverything -Wno-missing-exported-signatures@@ -49,23 +49,23 @@ -Wno-monomorphism-restriction -Wno-unsafe -Wno-safe build-depends:- aeson,- aeson-casing,+ aeson >=1.3.1.1,+ aeson-casing >=0.1.0.5, base >=4.11.1.0 && <5,- bytestring,- hashable,- http-conduit,- iso8601-time,- microlens,- microlens-mtl,- monad-logger,- mtl,- scientific,- text,- time,- unliftio,- unliftio-core,- unordered-containers+ bytestring >=0.10.8.2,+ hashable >=1.2.7.0,+ http-conduit >=2.3.2,+ iso8601-time >=0.1.5,+ microlens >=0.4.9.1,+ microlens-mtl >=0.1.11.1,+ monad-logger >=0.3.30,+ mtl >=2.2.2,+ scientific >=0.3.6.2,+ text >=1.2.3.1,+ time >=1.8.0.2,+ unliftio >=0.2.9.0,+ unliftio-core >=0.1.2.0,+ unordered-containers >=0.2.9.0 if impl(ghc >=9.2) ghc-options: -Wno-missing-kind-signatures
library/Asana/Api/CustomField.hs view
@@ -1,15 +1,15 @@ module Asana.Api.CustomField- ( CustomField(..)- , CustomFields(..)- , customEnumId- , EnumOption(..)- , putCustomField- , putCustomFields- ) where--import Asana.Api.Prelude+ ( CustomField (..),+ CustomFields (..),+ customEnumId,+ EnumOption (..),+ putCustomField,+ putCustomFields,+ )+where import Asana.Api.Gid (Gid, gidToText)+import Asana.Api.Prelude import Asana.Api.Request import Data.Aeson import Data.Aeson.Casing (aesonPrefix, snakeCase)@@ -22,27 +22,28 @@ = CustomNumber Gid Text (Maybe Scientific) | CustomEnum Gid Text [EnumOption] (Maybe Text) | CustomText Gid Text (Maybe Text)- | Other -- ^ Unexpected types dumped here+ | -- | Unexpected types dumped here+ Other deriving stock (Eq, Generic, Show) -newtype CustomFields = CustomFields { getCustomFields :: [CustomField] }+newtype CustomFields = CustomFields {getCustomFields :: [CustomField]} deriving stock (Show, Eq) deriving newtype (FromJSON) instance ToJSON CustomFields where toJSON (CustomFields fields) = object $ concatMap toPair fields- where- toPair = \case- CustomNumber gid _ n -> [gidToKey gid .= n]- e@(CustomEnum gid _ _ _) -> [gidToKey gid .= customEnumId e]- _ -> []+ where+ toPair = \case+ CustomNumber gid _ n -> [gidToKey gid .= n]+ e@(CustomEnum gid _ _ _) -> [gidToKey gid .= customEnumId e]+ _ -> [] - -- fromString will give us Text for aeson-1.x and Key for aeson-2.x- gidToKey = fromString . T.unpack . gidToText+ -- fromString will give us Text for aeson-1.x and Key for aeson-2.x+ gidToKey = fromString . T.unpack . gidToText data EnumOption = EnumOption- { eoGid :: Gid- , eoName :: Text+ { eoGid :: Gid,+ eoName :: Text } deriving stock (Eq, Generic, Show) @@ -54,7 +55,6 @@ -- - Must be a @'CustomEnum'@ -- - Must have a value -- - Must have an option with the same name as that value--- customEnumId :: CustomField -> Maybe Gid customEnumId (CustomEnum _ _ opts mValue) = do value <- mValue@@ -77,22 +77,24 @@ <*> (o .: "name") <*> (o .: "enum_options") <*> case value of- Object vo -> vo .:? "name"- _ -> pure Nothing+ Object vo -> vo .:? "name"+ _ -> pure Nothing _ -> pure Other -putCustomField- :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)- => Gid- -> CustomField- -> m ()+putCustomField ::+ (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env) =>+ Gid ->+ CustomField ->+ m () putCustomField taskId = putCustomFields taskId . CustomFields . pure -putCustomFields- :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)- => Gid- -> CustomFields- -> m ()+putCustomFields ::+ (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env) =>+ Gid ->+ CustomFields ->+ m () putCustomFields taskId fields =- void $ put ("/tasks/" <> T.unpack (gidToText taskId)) $ ApiData- (object ["custom_fields" .= fields])+ void $+ put ("/tasks/" <> T.unpack (gidToText taskId)) $+ ApiData+ (object ["custom_fields" .= fields])
library/Asana/Api/Gid.hs view
@@ -1,24 +1,29 @@ -- | A globally unique identifier module Asana.Api.Gid- ( Gid- , AsanaReference(..)- , gidToText- , textToGid- ) where+ ( Gid,+ AsanaReference (..),+ gidToText,+ textToGid,+ )+where import Asana.Api.Prelude- import Data.Aeson- (FromJSON(..), FromJSONKey, ToJSON, ToJSONKey, genericParseJSON)+ ( FromJSON (..),+ FromJSONKey,+ ToJSON,+ ToJSONKey,+ genericParseJSON,+ ) import Data.Aeson.Casing (aesonPrefix, snakeCase) import Data.Hashable (Hashable) -newtype Gid = Gid { gidToText :: Text }+newtype Gid = Gid {gidToText :: Text} deriving stock (Eq, Generic, Show) deriving newtype (FromJSON, ToJSON, ToJSONKey, FromJSONKey, Hashable) -- | An object @{ gid: <Gid> }@-newtype AsanaReference = AsanaReference { arGid :: Gid }+newtype AsanaReference = AsanaReference {arGid :: Gid} deriving stock (Eq, Generic, Show) instance FromJSON AsanaReference where
library/Asana/Api/Named.hs view
@@ -1,17 +1,17 @@ -- | Anything with a compact @{ id, name }@ representation module Asana.Api.Named- ( Named(..)- ) where--import Asana.Api.Prelude+ ( Named (..),+ )+where import Asana.Api.Gid (Gid)+import Asana.Api.Prelude import Data.Aeson (FromJSON, genericParseJSON, parseJSON) import Data.Aeson.Casing (aesonPrefix, snakeCase) data Named = Named- { nGid :: Gid- , nName :: Text+ { nGid :: Gid,+ nName :: Text } deriving stock (Eq, Generic, Show)
library/Asana/Api/Prelude.hs view
@@ -1,22 +1,36 @@ module Asana.Api.Prelude- ( module X- ) where--import Prelude as X+ ( module X,+ )+where import Control.Arrow as X ((&&&), (***))+import Control.Monad as X (guard, when) import Control.Monad.IO.Unlift as X (MonadUnliftIO) import Control.Monad.Logger.CallStack as X import Control.Monad.Reader as X+ ( MonadIO (..),+ MonadReader (..),+ MonadTrans (..),+ Reader,+ ReaderT (..),+ ) import Data.Bifunctor as X (first, second) import Data.ByteString as X (ByteString) import Data.Foldable as X (for_)+import Data.Functor as X (void) import Data.Maybe as X- (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe)+ ( catMaybes,+ fromMaybe,+ isJust,+ isNothing,+ listToMaybe,+ mapMaybe,+ ) import Data.Text as X (Text, pack, unpack) import Data.Traversable as X (for) import GHC.Generics as X (Generic) import Lens.Micro as X (Lens', lens) import Lens.Micro.Mtl as X (view) import Text.Read as X (readMaybe)-import UnliftIO.Exception as X (Exception(..), catch, throwIO)+import UnliftIO.Exception as X (Exception (..), catch, throwIO)+import Prelude as X
library/Asana/Api/Project.hs view
@@ -1,18 +1,18 @@ module Asana.Api.Project- ( Project(..)- ) where--import Asana.Api.Prelude+ ( Project (..),+ )+where import Asana.Api.Gid (Gid)+import Asana.Api.Prelude import Data.Aeson (FromJSON, genericParseJSON, parseJSON) import Data.Aeson.Casing (aesonPrefix, snakeCase) import Data.Time (UTCTime) data Project = Project- { pGid :: Gid- , pName :: Text- , pCreatedAt :: UTCTime+ { pGid :: Gid,+ pName :: Text,+ pCreatedAt :: UTCTime } deriving stock (Generic, Show)
library/Asana/Api/Request.hs view
@@ -1,20 +1,20 @@ module Asana.Api.Request- ( AsanaAccessKey(..)- , HasAsanaAccessKey(..)- , Single(..)- , Page(..)- , NextPage(..)- , ApiData(..)- , getAll- , getAllParams- , getSingle- , put- , post- , maxRequests- ) where+ ( AsanaAccessKey (..),+ HasAsanaAccessKey (..),+ Single (..),+ Page (..),+ NextPage (..),+ ApiData (..),+ getAll,+ getAllParams,+ getSingle,+ put,+ post,+ maxRequests,+ )+where import Asana.Api.Prelude- import Data.Aeson import Data.Aeson.Casing (aesonPrefix, snakeCase) import qualified Data.ByteString.Lazy as BSL@@ -22,23 +22,23 @@ import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding.Error as T import Network.HTTP.Simple- ( JSONException(JSONConversionException, JSONParseException)- , Request- , Response- , addRequestHeader- , getResponseBody- , getResponseHeader- , getResponseStatusCode- , httpJSON- , parseRequest_- , setRequestBodyJSON- , setRequestMethod+ ( JSONException (JSONConversionException, JSONParseException),+ Request,+ Response,+ addRequestHeader,+ getResponseBody,+ getResponseHeader,+ getResponseStatusCode,+ httpJSON,+ parseRequest_,+ setRequestBodyJSON,+ setRequestMethod, ) import UnliftIO.Concurrent (threadDelay) newtype AsanaAccessKey = AsanaAccessKey- { unAsanaAccessKey :: Text- }+ { unAsanaAccessKey :: Text+ } class HasAsanaAccessKey env where asanaAccessKeyL :: Lens' env AsanaAccessKey@@ -50,30 +50,32 @@ maxRequests = 50 -- | Type for a single-resource response, containing @{ data: { ... } }@+type role Single representational newtype Single a = Single { sData :: a } deriving newtype (Eq, Show)- deriving stock Generic+ deriving stock (Generic) -instance FromJSON a => FromJSON (Single a) where+instance (FromJSON a) => FromJSON (Single a) where parseJSON = genericParseJSON $ aesonPrefix snakeCase -- | Type for a list-resource response, containing @{ data: [{ ... }] }@+type role Page representational data Page a = Page- { pData :: [a]- , pNextPage :: Maybe NextPage+ { pData :: [a],+ pNextPage :: Maybe NextPage } deriving stock (Eq, Generic, Show) -instance FromJSON a => FromJSON (Page a) where+instance (FromJSON a) => FromJSON (Page a) where parseJSON = genericParseJSON $ aesonPrefix snakeCase -- | The @next_page@ element of a paginated response data NextPage = NextPage- { npOffset :: Text- , npPath :: Text- , npUri :: Text+ { npOffset :: Text,+ npPath :: Text,+ npUri :: Text } deriving stock (Eq, Generic, Show) @@ -81,141 +83,147 @@ parseJSON = genericParseJSON $ aesonPrefix snakeCase -- | Generic type for un/wrapping an item as @{ data: <item> }@+type role ApiData representational newtype ApiData a = ApiData { adData :: a } deriving newtype (Show, Eq)- deriving stock Generic+ deriving stock (Generic) -instance FromJSON a => FromJSON (ApiData a) where+instance (FromJSON a) => FromJSON (ApiData a) where parseJSON = genericParseJSON $ aesonPrefix snakeCase -instance ToJSON a => ToJSON (ApiData a) where+instance (ToJSON a) => ToJSON (ApiData a) where toJSON = genericToJSON $ aesonPrefix snakeCase toEncoding = genericToEncoding $ aesonPrefix snakeCase -- | Naively GET all pages of a paginated resource-getAll- :: ( MonadUnliftIO m- , MonadLogger m- , MonadReader env m- , HasAsanaAccessKey env- , FromJSON a- )- => String- -> m [a]+getAll ::+ ( MonadUnliftIO m,+ MonadLogger m,+ MonadReader env m,+ HasAsanaAccessKey env,+ FromJSON a+ ) =>+ String ->+ m [a] getAll path = getAllParams path [] -getAllParams- :: ( MonadUnliftIO m- , MonadLogger m- , MonadReader env m- , HasAsanaAccessKey env- , FromJSON a- )- => String- -> [(String, String)]- -> m [a]+getAllParams ::+ ( MonadUnliftIO m,+ MonadLogger m,+ MonadReader env m,+ HasAsanaAccessKey env,+ FromJSON a+ ) =>+ String ->+ [(String, String)] ->+ m [a] getAllParams path params = go Nothing- where- go mOffset = do- Page d mNextPage <- get path params 50 mOffset+ where+ go mOffset = do+ Page d mNextPage <- get path params 50 mOffset - maybe (pure d) (fmap (d ++) . go . Just . T.unpack . npOffset) mNextPage+ maybe (pure d) (fmap (d ++) . go . Just . T.unpack . npOffset) mNextPage -- | Get a single resource-getSingle- :: ( MonadUnliftIO m- , MonadLogger m- , MonadReader env m- , HasAsanaAccessKey env- , FromJSON a- )- => String- -> m a+getSingle ::+ ( MonadUnliftIO m,+ MonadLogger m,+ MonadReader env m,+ HasAsanaAccessKey env,+ FromJSON a+ ) =>+ String ->+ m a getSingle path = sData <$> get path [] 1 Nothing -get- :: ( MonadUnliftIO m- , MonadLogger m- , MonadReader env m- , HasAsanaAccessKey env- , FromJSON a- )- => String- -> [(String, String)]- -> Int- -> Maybe String- -> m a+get ::+ ( MonadUnliftIO m,+ MonadLogger m,+ MonadReader env m,+ HasAsanaAccessKey env,+ FromJSON a+ ) =>+ String ->+ [(String, String)] ->+ Int ->+ Maybe String ->+ m a get path params limit mOffset = do AsanaAccessKey key <- view asanaAccessKeyL- let- request =- parseRequest_- $ "https://app.asana.com/api/1.0"- <> path- <> "?limit="- <> show limit -- Ignored on not paging responses- <> maybe "" ("&offset=" <>) mOffset- <> concatMap (\(k, v) -> "&" <> k <> "=" <> v) params+ let request =+ parseRequest_ $+ "https://app.asana.com/api/1.0"+ <> path+ <> "?limit="+ <> show limit -- Ignored on not paging responses+ <> maybe "" ("&offset=" <>) mOffset+ <> concatMap (\(k, v) -> "&" <> k <> "=" <> v) params response <- retry 50 $ httpJSON (addAuthorization key request)- when (300 <= getResponseStatusCode response)- $ logWarnNS "Asana"- $ "GET failed, status: "- <> pack (show $ getResponseStatusCode response)+ when (300 <= getResponseStatusCode response) $+ logWarnNS "Asana" $+ "GET failed, status: "+ <> pack (show $ getResponseStatusCode response) pure $ getResponseBody response -put- :: ( MonadUnliftIO m- , MonadLogger m- , MonadReader env m- , HasAsanaAccessKey env- , ToJSON a- )- => String- -> a- -> m Value+put ::+ ( MonadUnliftIO m,+ MonadLogger m,+ MonadReader env m,+ HasAsanaAccessKey env,+ ToJSON a+ ) =>+ String ->+ a ->+ m Value put = httpAction "PUT" -post- :: ( MonadUnliftIO m- , MonadLogger m- , MonadReader env m- , HasAsanaAccessKey env- , ToJSON a- )- => String- -> a- -> m Value+post ::+ ( MonadUnliftIO m,+ MonadLogger m,+ MonadReader env m,+ HasAsanaAccessKey env,+ ToJSON a+ ) =>+ String ->+ a ->+ m Value post = httpAction "POST" -httpAction- :: ( MonadUnliftIO m- , MonadLogger m- , MonadReader env m- , HasAsanaAccessKey env- , ToJSON a- )- => ByteString- -> String- -> a- -> m Value+httpAction ::+ ( MonadUnliftIO m,+ MonadLogger m,+ MonadReader env m,+ HasAsanaAccessKey env,+ ToJSON a+ ) =>+ ByteString ->+ String ->+ a ->+ m Value httpAction verb path payload = do AsanaAccessKey key <- view asanaAccessKeyL let request = parseRequest_ $ "https://app.asana.com/api/1.0" <> path - response <- retry 10 $ httpJSON- (setRequestMethod verb . setRequestBodyJSON payload $ addAuthorization- key- request- )- when (300 <= getResponseStatusCode response) $ logWarnNS "Asana" $ mconcat- [ "Request failed"- , "\n method: " <> T.decodeUtf8 verb- , "\n status: " <> pack (show $ getResponseStatusCode response)- , "\n body : " <> T.decodeUtf8- (BSL.toStrict $ encode $ toJSON $ getResponseBody @Value response)- ]+ response <-+ retry 10 $+ httpJSON+ ( setRequestMethod verb . setRequestBodyJSON payload $+ addAuthorization+ key+ request+ )+ when (300 <= getResponseStatusCode response) $+ logWarnNS "Asana" $+ mconcat+ [ "Request failed",+ "\n method: " <> T.decodeUtf8 verb,+ "\n status: " <> pack (show $ getResponseStatusCode response),+ "\n body : "+ <> T.decodeUtf8+ (BSL.toStrict $ encode $ toJSON $ getResponseBody @Value response)+ ] pure $ getResponseBody response @@ -223,38 +231,38 @@ addAuthorization key = addRequestHeader "Authorization" $ "Bearer " <> T.encodeUtf8 key -retry- :: forall a m- . (MonadUnliftIO m, MonadLogger m)- => Int- -> m (Response a)- -> m (Response a)+retry ::+ forall a m.+ (MonadUnliftIO m, MonadLogger m) =>+ Int ->+ m (Response a) ->+ m (Response a) retry attempt go | attempt <= 0 = go | otherwise = handler =<< go `catch` handleParseError- where- handleParseError :: JSONException -> m (Response a)- handleParseError e = case e of- JSONParseException _ rsp _ -> orThrow e rsp- JSONConversionException _ rsp _ -> orThrow e rsp+ where+ handleParseError :: JSONException -> m (Response a)+ handleParseError e = case e of+ JSONParseException _ rsp _ -> orThrow e rsp+ JSONConversionException _ rsp _ -> orThrow e rsp - orThrow :: Exception e => e -> Response b -> m (Response a)- orThrow e response- | getResponseStatusCode response == 429 = do- let seconds = getResponseDelay response- logWarnNS "Asana" $ "Retrying after " <> pack (show seconds) <> " seconds"- threadDelay $ seconds * 1000000- retry (pred attempt) go- | otherwise = liftIO $ throwIO e+ orThrow :: (Exception e) => e -> Response b -> m (Response a)+ orThrow e response+ | getResponseStatusCode response == 429 = do+ let seconds = getResponseDelay response+ logWarnNS "Asana" $ "Retrying after " <> pack (show seconds) <> " seconds"+ threadDelay $ seconds * 1000000+ retry (pred attempt) go+ | otherwise = liftIO $ throwIO e - handler :: Response a -> m (Response a)- handler response- | getResponseStatusCode response == 429 = do- let seconds = getResponseDelay response- logWarnNS "Asana" $ "Retrying after " <> pack (show seconds) <> " seconds"- threadDelay $ seconds * 100000- retry (pred attempt) go- | otherwise = pure response+ handler :: Response a -> m (Response a)+ handler response+ | getResponseStatusCode response == 429 = do+ let seconds = getResponseDelay response+ logWarnNS "Asana" $ "Retrying after " <> pack (show seconds) <> " seconds"+ threadDelay $ seconds * 100000+ retry (pred attempt) go+ | otherwise = pure response getResponseDelay :: Response a -> Int getResponseDelay =
library/Asana/Api/Tag.hs view
@@ -1,16 +1,16 @@ module Asana.Api.Tag- ( Tag(..)- ) where--import Asana.Api.Prelude+ ( Tag (..),+ )+where import Asana.Api.Gid+import Asana.Api.Prelude import Data.Aeson import Data.Aeson.Casing (aesonPrefix, snakeCase) data Tag = Tag- { tGid :: Gid- , tName :: Text+ { tGid :: Gid,+ tName :: Text } deriving stock (Eq, Generic, Show)
library/Asana/Api/Task.hs view
@@ -1,25 +1,25 @@ module Asana.Api.Task- ( Task(..)- , Membership(..)- , TaskStatusFilter(..)- , ResourceSubtype(..)- , PostTask(..)- , getTask- , getProjectTasks- , getProjectTasksCompletedSince- , postTask- , addTag- , putCompleted- , taskUrl- , extractNumberField- , extractEnumField- ) where--import Asana.Api.Prelude+ ( Task (..),+ Membership (..),+ TaskStatusFilter (..),+ ResourceSubtype (..),+ PostTask (..),+ getTask,+ getProjectTasks,+ getProjectTasksCompletedSince,+ postTask,+ addTag,+ putCompleted,+ taskUrl,+ extractNumberField,+ extractEnumField,+ )+where import Asana.Api.CustomField import Asana.Api.Gid import Asana.Api.Named+import Asana.Api.Prelude import Asana.Api.Request import Asana.Api.Tag import Data.Aeson@@ -30,8 +30,8 @@ import Data.Time.ISO8601 (formatISO8601) data Membership = Membership- { mProject :: Named- , mSection :: Maybe Named+ { mProject :: Named,+ mSection :: Maybe Named } deriving stock (Eq, Generic, Show) @@ -43,21 +43,21 @@ instance FromJSON ResourceSubtype where parseJSON =- genericParseJSON $ defaultOptions { constructorTagModifier = snakeCase }+ genericParseJSON $ defaultOptions {constructorTagModifier = snakeCase} data Task = Task- { tAssignee :: Maybe Named- , tName :: Text- , tCompleted :: Bool- , tCompletedAt :: Maybe UTCTime- , tCreatedAt :: UTCTime- , tCustomFields :: CustomFields- , tMemberships :: [Membership]- , tGid :: Gid- , tResourceSubtype :: ResourceSubtype- , tNotes :: Text- , tProjects :: [AsanaReference]- , tTags :: [Tag]+ { tAssignee :: Maybe Named,+ tName :: Text,+ tCompleted :: Bool,+ tCompletedAt :: Maybe UTCTime,+ tCreatedAt :: UTCTime,+ tCustomFields :: CustomFields,+ tMemberships :: [Membership],+ tGid :: Gid,+ tResourceSubtype :: ResourceSubtype,+ tNotes :: Text,+ tProjects :: [AsanaReference],+ tTags :: [Tag] } deriving stock (Eq, Generic, Show) @@ -65,20 +65,20 @@ parseJSON = genericParseJSON $ aesonPrefix snakeCase -- | Return all details for a task by id-getTask- :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)- => Gid- -> m Task+getTask ::+ (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env) =>+ Gid ->+ m Task getTask taskId = getSingle $ "/tasks/" <> T.unpack (gidToText taskId) data PostTask = PostTask- { ptProjects :: [Gid]- , ptCustomFields :: HashMap Gid Text- , ptName :: Text- , ptNotes :: Text- , ptParent :: Maybe Gid+ { ptProjects :: [Gid],+ ptCustomFields :: HashMap Gid Text,+ ptName :: Text,+ ptNotes :: Text,+ ptParent :: Maybe Gid }- deriving stock Generic+ deriving stock (Generic) instance FromJSON PostTask where parseJSON = genericParseJSON $ aesonPrefix snakeCase@@ -88,10 +88,10 @@ toEncoding = genericToEncoding $ aesonPrefix snakeCase -- | Create a new 'Task'-postTask- :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)- => PostTask- -> m (Result Task)+postTask ::+ (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env) =>+ PostTask ->+ m (Result Task) postTask body = fmap adData . fromJSON <$> post "/tasks" (ApiData body) -- | Return compact task details for a project@@ -99,51 +99,55 @@ -- Iterating ourselves and returning @['Task']@ is a better interface but -- precludes us logging things each time we request an element. So we return -- @'Named'@ for now and let the caller use @'getTask'@ themselves.----getProjectTasks- :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)- => Gid- -> TaskStatusFilter- -> m [Named]+getProjectTasks ::+ (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env) =>+ Gid ->+ TaskStatusFilter ->+ m [Named] getProjectTasks projectId taskStatusFilter = do now <- liftIO getCurrentTime getAllParams (T.unpack $ "/projects/" <> gidToText projectId <> "/tasks") (completedSince now)-- where- completedSince now = case taskStatusFilter of- AllTasks -> []- IncompletedTasks -> [("completed_since", formatISO8601 now)]+ where+ completedSince now = case taskStatusFilter of+ AllTasks -> []+ IncompletedTasks -> [("completed_since", formatISO8601 now)] data TaskStatusFilter = IncompletedTasks | AllTasks -getProjectTasksCompletedSince- :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)- => Gid- -> UTCTime- -> m [Named]-getProjectTasksCompletedSince projectId since = getAllParams- (T.unpack $ "/projects/" <> gidToText projectId <> "/tasks")- [("completed_since", formatISO8601 since)]+getProjectTasksCompletedSince ::+ (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env) =>+ Gid ->+ UTCTime ->+ m [Named]+getProjectTasksCompletedSince projectId since =+ getAllParams+ (T.unpack $ "/projects/" <> gidToText projectId <> "/tasks")+ [("completed_since", formatISO8601 since)] -addTag- :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)- => Gid- -> Gid -- ^ Tag- -> m ()+addTag ::+ (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env) =>+ Gid ->+ -- | Tag+ Gid ->+ m () addTag task tag =- void $ post ("/tasks/" <> T.unpack (gidToText task) <> "/addTag") $ ApiData- (object ["tag" .= tag])+ void $+ post ("/tasks/" <> T.unpack (gidToText task) <> "/addTag") $+ ApiData+ (object ["tag" .= tag]) -putCompleted- :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)- => Gid- -> Bool- -> m ()+putCompleted ::+ (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env) =>+ Gid ->+ Bool ->+ m () putCompleted taskId completed =- void $ put ("/tasks/" <> T.unpack (gidToText taskId)) $ ApiData- (object ["completed" .= completed])+ void $+ put ("/tasks/" <> T.unpack (gidToText taskId)) $+ ApiData+ (object ["completed" .= completed]) taskUrl :: Task -> Text taskUrl Task {..} = "https://app.asana.com/0/0/" <> gidToText tGid <> "/f"
library/Asana/Api/Task/Search.hs view
@@ -1,13 +1,13 @@ module Asana.Api.Task.Search- ( SearchWorkspace(..)- , TaskTypeFilter(..)- , searchWorkspace- ) where--import Asana.Api.Prelude+ ( SearchWorkspace (..),+ TaskTypeFilter (..),+ searchWorkspace,+ )+where import Asana.Api.Gid import Asana.Api.Named+import Asana.Api.Prelude import Asana.Api.Request import Asana.Api.Task import Data.HashMap.Strict (HashMap)@@ -18,40 +18,40 @@ data TaskTypeFilter = TasksOnly | SubtasksOnly | AllTaskTypes data SearchWorkspace = SearchWorkspace- { swWorkspaceId :: Gid- , swProjectIds :: [Gid]- , swTaskStatusFilter :: TaskStatusFilter- , swCustomFields :: HashMap Gid Text- , swTaskTypeFilter :: TaskTypeFilter+ { swWorkspaceId :: Gid,+ swProjectIds :: [Gid],+ swTaskStatusFilter :: TaskStatusFilter,+ swCustomFields :: HashMap Gid Text,+ swTaskTypeFilter :: TaskTypeFilter } -- | Search for tasks within a workspace-searchWorkspace- :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)- => SearchWorkspace- -> m [Named]+searchWorkspace ::+ (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env) =>+ SearchWorkspace ->+ m [Named] searchWorkspace SearchWorkspace {..} = getAllParams- (T.unpack $ "/workspaces/" <> gidToText swWorkspaceId <> "/tasks/search")- $ ( "projects.all"- , intercalate "," $ map (T.unpack . gidToText) swProjectIds+ (T.unpack $ "/workspaces/" <> gidToText swWorkspaceId <> "/tasks/search")+ $ ( "projects.all",+ intercalate "," $ map (T.unpack . gidToText) swProjectIds )- : customFieldParams- <> completed- <> isSubtask- where- customFieldParams =- map- (\(a, b) ->- ("custom_fields." <> T.unpack (gidToText a) <> ".value", T.unpack b)+ : customFieldParams+ <> completed+ <> isSubtask+ where+ customFieldParams =+ map+ ( \(a, b) ->+ ("custom_fields." <> T.unpack (gidToText a) <> ".value", T.unpack b) )- $ HashMap.toList swCustomFields+ $ HashMap.toList swCustomFields - completed = case swTaskStatusFilter of- AllTasks -> []- IncompletedTasks -> [("completed", "false")]+ completed = case swTaskStatusFilter of+ AllTasks -> []+ IncompletedTasks -> [("completed", "false")] - isSubtask = case swTaskTypeFilter of- AllTaskTypes -> []- TasksOnly -> [("is_subtask", "false")]- SubtasksOnly -> [("is_subtask", "true")]+ isSubtask = case swTaskTypeFilter of+ AllTaskTypes -> []+ TasksOnly -> [("is_subtask", "false")]+ SubtasksOnly -> [("is_subtask", "true")]
package.yaml view
@@ -1,5 +1,5 @@ name: asana-version: 1.0.1.0+version: 1.0.1.1 maintainer: Freckle Education category: Utils github: freckle/asana-hs@@ -55,6 +55,7 @@ - OverloadedStrings - RankNTypes - RecordWildCards+ - RoleAnnotations - ScopedTypeVariables - StandaloneDeriving - TypeApplications