packages feed

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 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