packages feed

taskwarrior 0.1.2.4 → 0.2.0.0

raw patch · 12 files changed

+232/−92 lines, 12 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Taskwarrior.Status: RecurringChild :: Text -> Integer -> UUID -> Status
- Taskwarrior.Status: RecurringParent :: Text -> Mask -> Status
- Taskwarrior.Status: [imask] :: Status -> Integer
- Taskwarrior.Status: [parent] :: Status -> UUID
+ Taskwarrior.Priority: High :: Priority
+ Taskwarrior.Priority: Low :: Priority
+ Taskwarrior.Priority: Medium :: Priority
+ Taskwarrior.Priority: instance Data.Aeson.Types.FromJSON.FromJSON Taskwarrior.Priority.Priority
+ Taskwarrior.RecurringChild: RecurringChild :: Text -> Integer -> UUID -> RecurringChild
+ Taskwarrior.RecurringChild: [imask] :: RecurringChild -> Integer
+ Taskwarrior.RecurringChild: [parent] :: RecurringChild -> UUID
+ Taskwarrior.RecurringChild: [recur] :: RecurringChild -> Text
+ Taskwarrior.RecurringChild: data RecurringChild
+ Taskwarrior.RecurringChild: instance Data.Aeson.Types.FromJSON.FromJSON Taskwarrior.RecurringChild.RecurringChild
+ Taskwarrior.RecurringChild: instance Data.Aeson.Types.ToJSON.ToJSON Taskwarrior.RecurringChild.RecurringChild
+ Taskwarrior.RecurringChild: instance GHC.Classes.Eq Taskwarrior.RecurringChild.RecurringChild
+ Taskwarrior.RecurringChild: instance GHC.Classes.Ord Taskwarrior.RecurringChild.RecurringChild
+ Taskwarrior.RecurringChild: instance GHC.Read.Read Taskwarrior.RecurringChild.RecurringChild
+ Taskwarrior.RecurringChild: instance GHC.Show.Show Taskwarrior.RecurringChild.RecurringChild
+ Taskwarrior.RecurringChild: parseFromObjectMay :: Object -> Parser (Maybe RecurringChild)
+ Taskwarrior.RecurringChild: toPairs :: RecurringChild -> [Pair]
+ Taskwarrior.Status: Recurring :: Text -> Mask -> Status
+ Taskwarrior.Status: instance Data.Aeson.Types.FromJSON.FromJSON Taskwarrior.Status.Status
+ Taskwarrior.Status: instance Data.Aeson.Types.ToJSON.ToJSON Taskwarrior.Status.Status
+ Taskwarrior.Task: [id] :: Task -> Maybe Integer
+ Taskwarrior.Task: [recurringChild] :: Task -> Maybe RecurringChild
+ Taskwarrior.Task: [urgency] :: Task -> Double
- Taskwarrior.Task: Task :: Status -> UUID -> UTCTime -> Text -> Maybe UTCTime -> Maybe UTCTime -> Maybe UTCTime -> Maybe UTCTime -> [Annotation] -> Maybe UTCTime -> Maybe Text -> Maybe Priority -> [UUID] -> [Tag] -> UDA -> Task
+ Taskwarrior.Task: Task :: Status -> Maybe RecurringChild -> UUID -> Maybe Integer -> UTCTime -> Text -> Maybe UTCTime -> Maybe UTCTime -> Maybe UTCTime -> Maybe UTCTime -> [Annotation] -> Maybe UTCTime -> Maybe Text -> Maybe Priority -> [UUID] -> [Tag] -> Double -> UDA -> Task

Files

CHANGELOG.md view
@@ -1,5 +1,33 @@ # Revision history for taskwarrior +## 0.2.0.0++### Breaking changes++* data Status: Removed RecurringChild, renamed RecurringParent to Recurring.++### Other API changes++* RecurringChild is now a new Type and a Maybe field of Task.+* Added the fields 'id' and 'urgency' to Task. They were regarded as UDAs before.+* Now exporting the data consructors of Priority+* Added To- and FromJSON for Status+* Add FromJSON for Priority and improved Parser errors for it++### Documentation++* Added documentation about adherence to the specification+* Improved linking in the docs+* Expanded README regarding help and contributions++### Development++* Github actions will now check formatting and linking in haddocks++### Thanks++Thanks to @ryantrinkle and @tom-audm for their contributions, which triggered this release.+ ## 0.1.2.4  * Fix a bug where recurring masks where parsed wrong
README.md view
@@ -31,4 +31,14 @@  ## Contributions -Any form of issue reports, general feedback or suggestions and of course code contributions are highly welcome.+Any form of issue reports, general feedback, feature requests or suggestions and of course code contributions are highly welcome.++Also I'd be curious to know what you use this library for.++This project uses brittany in default configuration as code formatter.+The tests on github will check for hlints, missing docs and unapplied formatting.++## Help & Contact++You can always open an issue on GitHub. You can also ask in `#haskell-taskwarrior` on freenode irc. If you don‘t have an irc client you can log in via [the webchat](https://webchat.freenode.net/#haskell-taskwarrior).+Shooting @maralorn a mail is also an option. But of course that won’t be public and therefore not help anyone else.
src/Taskwarrior/Annotation.hs view
@@ -1,5 +1,4 @@-{-# LANGUAGE RecordWildCards, ScopedTypeVariables #-}--- |+-- | Provides the 'Annotation' type with 'Data.Aeson.ToJSON' and 'Data.Aeson.FromJSON' instances. module Taskwarrior.Annotation   ( Annotation(..)   )@@ -13,7 +12,7 @@                                                 ) import qualified Data.Aeson                    as Aeson --- |+-- | A taskwarrior 'Taskwarrior.Task.Task' can have multiple annotations. They contain a timestamp 'entry' and a 'description'. data Annotation = Annotation { entry :: UTCTime, description :: Text } deriving (Eq, Show, Read, Ord)  instance Aeson.FromJSON Annotation where
src/Taskwarrior/IO.hs view
@@ -1,6 +1,6 @@ -- | This modules contains IO actions to interact with the taskwarrior application. -- The taskwarrior documentation very explicitly disallows accessing the files by itself.--- So all functions here work via calling the `task` binary which needs to be in the PATH.+-- So all functions here work via calling the @task@ binary which needs to be in the PATH. module Taskwarrior.IO   ( getTasks   , saveTasks@@ -85,7 +85,7 @@ -- -- @ -- newTask <- 'createTask' "Buy Milk"--- 'saveTasks' [newTask { 'tags' = ["groceries"] }]+-- 'saveTasks' [newTask { 'Taskwarrior.Task.tags' = ["groceries"] }] -- @ createTask :: Text -> IO Task createTask description = do
src/Taskwarrior/Mask.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE LambdaCase #-} -- | The Mask module models the state a recurring parent saves about its child tasks. module Taskwarrior.Mask   ( Mask(..)@@ -10,10 +9,10 @@ import qualified Data.Aeson                    as Aeson import qualified Data.Aeson.Types              as Aeson.Types --- | Represents the state of a child in a recurring task.+-- | Represents the state of a child in a 'Status.Recurring' 'Task.Task'. data MaskState = Pending | Completed | Deleted | Waiting deriving (Eq, Show, Enum, Read, Ord, Bounded) --- | The mask is a newtype to provide Aeson instances from and to a JSON string.+-- | The mask is a newtype to provide 'Data.Aeson' instances from and to a JSON string. newtype Mask = Mask {mask :: [MaskState]} deriving (Eq, Read, Ord, Show)  toChar :: MaskState -> Char@@ -33,7 +32,7 @@   '+'  -> pure Completed   'X'  -> pure Deleted   'W'  -> pure Waiting-  char -> fail $ "Not a Mask Char: '"++[char]++"'"+  char -> fail $ "Not a Mask Char: '" ++ [char] ++ "'"  instance Aeson.ToJSON Mask where   toJSON = Aeson.String . Text.pack . fmap toChar . mask
src/Taskwarrior/Priority.hs view
@@ -1,25 +1,40 @@--- | This module provides the type for the priority of a task.-module Taskwarrior.Priority (parseMay, Priority) where+-- | This module provides the type for the 'Priority' of a task.+module Taskwarrior.Priority+  ( parseMay+  , Priority(..)+  )+where  import qualified Data.Aeson                    as Aeson-import           Data.Aeson.Types               ( Parser, typeMismatch )+import           Data.Aeson.Types               ( Parser )  --- | A task can have the priorities high, medium, low or none, which is modeled via a Maybe Priority.+-- | A 'Taskwarrior.Task.Task' can have the priorities 'High', 'Medium', 'Low' or none, which is modeled via a 'Maybe' 'Priority'. data Priority = High | Medium | Low         deriving (Eq, Show, Read, Enum, Ord, Bounded)  instance Aeson.ToJSON Priority where-   toJSON = \case-      High -> "H"-      Medium -> "M"-      Low -> "L"+  toJSON = \case+    High   -> "H"+    Medium -> "M"+    Low    -> "L" --- | Parses a JSON string to a Maybe Priority, fails on anything else.+-- | Parses a JSON string to a 'Maybe' 'Priority', fails on anything else. parseMay :: Aeson.Value -> Parser (Maybe Priority)-parseMay val = Aeson.withText "Priority" (\case+parseMay = Aeson.withText "Priority" $ \case   "H" -> pure $ Just High   "M" -> pure $ Just Medium   "L" -> pure $ Just Low-  "" -> pure Nothing-  _ -> typeMismatch "Priority" val) val+  ""  -> pure Nothing+  s ->+    fail+      $  "parsing Priority failed, unexpected "+      ++ show s+      ++ " (expected \"H\", \"M\", \"L\", or \"\")"++instance Aeson.FromJSON Priority where+  parseJSON val = parseMay val >>= \case+    Nothing ->+      fail+        "parsing Priority failed, unexpected null (expected \"H\", \"M\", or \"L\")"+    Just p -> pure p
+ src/Taskwarrior/RecurringChild.hs view
@@ -0,0 +1,54 @@+-- | This Module provides the RecurringChild type with+-- FromJSON and ToJSON instances.+module Taskwarrior.RecurringChild+  ( RecurringChild(..)+  , parseFromObjectMay+  , toPairs+  )+where++import           Control.Applicative            ( optional )+import           Data.Aeson                     ( Object+                                                , (.:)+                                                , (.=)+                                                , ToJSON+                                                , FromJSON+                                                , pairs+                                                , object+                                                , withObject+                                                )+import qualified Data.Aeson                    as Aeson+import           Data.Aeson.Types               ( Parser+                                                , Pair+                                                )+import           Data.Text                      ( Text )+import           Data.UUID                      ( UUID )++-- | The 'RecurringChild' type saves information about how a 'Taskwarrior.Task.Task'+-- is child of another 'Taskwarrior.Task.Task' wich is recurring.+data RecurringChild =+  RecurringChild {+  recur :: Text,+  imask :: Integer,+  parent :: UUID }+  deriving (Eq, Show, Read, Ord)++-- | Gathers all fields for a 'RecurringChild' status.+parseFromObjectMay :: Object -> Parser (Maybe RecurringChild)+parseFromObjectMay = optional . parseFromObject++parseFromObject :: Object -> Parser RecurringChild+parseFromObject o =+  RecurringChild <$> o .: "recur" <*> o .: "imask" <*> o .: "parent"++-- | Can be used to serialize 'RecurringChild' to JSON.+toPairs :: RecurringChild -> [Pair]+toPairs RecurringChild {..} =+  ["recur" .= recur, "imask" .= imask, "parent" .= parent]++instance FromJSON RecurringChild where+  parseJSON = withObject "RecurringChild" parseFromObject++instance ToJSON RecurringChild where+  toJSON     = object . toPairs+  toEncoding = pairs . mconcat . map (uncurry (.=)) . toPairs
src/Taskwarrior/Status.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE LambdaCase #-} -- | This module deals with information of a task which is dependent on the status. module Taskwarrior.Status   ( Status(..)@@ -12,66 +11,57 @@ import           Data.Aeson                     ( Object                                                 , (.:)                                                 , (.=)+                                                , ToJSON+                                                , FromJSON+                                                , pairs+                                                , object+                                                , withObject                                                 ) import qualified Data.Aeson                    as Aeson-import           Control.Applicative            ( (<|>) ) import           Data.Text                      ( Text ) import           Data.Time                      ( UTCTime )-import           Data.UUID                      ( UUID ) import           Data.Aeson.Types               ( Parser                                                 , typeMismatch                                                 , Pair                                                 )+ -- | A task can be pending, deleted, completed, waiting or recurring.--- If I task is a recurring child or a recurring parent depends on the existence of the corresponding fields and can not be told from the status field alone. -- It is recommended to access the fields only by pattern matching since the getters are partial. data Status =   Pending |   Deleted {  end :: UTCTime } |   Completed {  end :: UTCTime } |   Waiting { wait :: UTCTime } |-  RecurringParent {-    recur :: Text,-    mask :: Mask} |-  RecurringChild {+  Recurring {     recur :: Text,-    imask :: Integer,-    parent :: UUID }+    mask :: Mask}   deriving (Eq, Show, Read, Ord) -parseFromObject, parseParentFromObject, parseChildFromObject-  :: Object -> Parser Status -- | Takes all information that is dependent on the status from a JSON object.+parseFromObject :: Object -> Parser Status parseFromObject o = (o .: "status") >>= \case   "pending"   -> pure Pending   "deleted"   -> Deleted <$> (o .: "end" >>= Time.parse)   "completed" -> Completed <$> (o .: "end" >>= Time.parse)   "waiting"   -> Waiting <$> (o .: "wait" >>= Time.parse)-  "recurring" -> parseParentFromObject o <|> parseChildFromObject o+  "recurring" -> Recurring <$> o .: "recur" <*> o .: "mask"   str         -> typeMismatch "status" (Aeson.String str) --- | Gathers all fields for a RecurringChild status.-parseChildFromObject o =-  RecurringChild <$> o .: "recur" <*> o .: "imask" <*> o .: "parent"---- | Gathers all fields for a RecurringParent status.-parseParentFromObject o = RecurringParent <$> o .: "recur" <*> o .: "mask"---- | A list of Pairs can be used to construct a JSON object later. The result of Status.toPairs is supposed to be combined with the rest of the fields of a task.+-- | A list of Pairs can be used to construct a JSON object later. The result of 'toPairs' is supposed to be combined with the rest of the fields of a task. toPairs :: Status -> [Pair] toPairs = \case   Pending        -> [statusLabel "pending"]   Deleted {..}   -> [statusLabel "deleted", "end" .= Time.toValue end]   Completed {..} -> [statusLabel "completed", "end" .= Time.toValue end]   Waiting {..}   -> [statusLabel "waiting", "wait" .= Time.toValue wait]-  RecurringParent {..} ->-    [statusLabel "recurring", "recur" .= recur, "mask" .= mask]-  RecurringChild {..} ->-    [ statusLabel "recurring"-    , "recur" .= recur-    , "imask" .= imask-    , "parent" .= parent-    ]+  Recurring {..} -> [statusLabel "recurring", "recur" .= recur, "mask" .= mask]  where   statusLabel :: Text -> Pair   statusLabel = ("status" .=)++instance FromJSON Status where+  parseJSON = withObject "Status" parseFromObject++instance ToJSON Status where+  toJSON     = object . toPairs+  toEncoding = pairs . mconcat . map (uncurry (.=)) . toPairs
src/Taskwarrior/Task.hs view
@@ -1,13 +1,24 @@-{-# LANGUAGE NamedFieldPuns #-} -- | This Module exports the main datatype of this library: Task. -- It is provided with FromJSON and ToJSON instances.+-- module Taskwarrior.Task   ( Task(..)   , Tag   , makeTask+  -- | == Adherence to specification+  -- This library uses the [taskwarrior specification for the JSON serialisation format](https://taskwarrior.org/docs/design/task.html).+  -- But it deviates in a small number of ways to be more pragmatic.+  --+  -- * 'Task' has the fields 'id' and 'urgency' although they are technically UDAs.+  -- * There are two invalid states which are not prevented via the Haskell type system by the chosen modeling:+  --+  --   1. A 'Task' with a 'Just' value for 'recurringChild' should not have the 'Status' 'Taskwarrior.Status.Recurring'.+  --   2. The 'due' field needs to be a 'Just' value on a 'Task' with 'Status' 'Taskwarrior.Status.Recurring'.   ) where +import           Prelude                 hiding ( id )+ import qualified Data.Text                     as Text import           Data.Text                      ( Text ) import           Data.Time                      ( UTCTime )@@ -26,11 +37,14 @@                                                 , Value                                                 ) import qualified Data.Semigroup                as Semigroup+import           Data.Maybe                     ( fromMaybe ) import qualified Data.Maybe                    as Maybe import           Control.Monad                  ( join ) import qualified Data.Foldable                 as Foldable import           Taskwarrior.Status             ( Status ) import qualified Taskwarrior.Status            as Status+import           Taskwarrior.RecurringChild     ( RecurringChild )+import qualified Taskwarrior.RecurringChild    as RecurringChild import           Taskwarrior.Priority           ( Priority ) import qualified Taskwarrior.Priority          as Priority import           Taskwarrior.UDA                ( UDA )@@ -39,33 +53,36 @@ import qualified Data.HashMap.Strict           as HashMap import           Foreign.Marshal.Utils          ( fromBool ) --- | A Task represents a task from taskwarrior. See <https://taskwarrior.org/docs/design/task.html> for the specification of the fields.+-- | A 'Task' represents a task from taskwarrior. -- The specification demands, that the existence of some fields is dependent on the status of the task.--- Those fields are therefore bundled in status as a sum-type.+-- Those fields are therefore bundled in 'Status' as a sum-type. ----- All fields in an imported task which are not part of the specification will be put in the UDA (user defined attributes) HashMap.+-- All fields in an imported task which are not part of the specification will be put in the 'UDA' (user defined attributes) 'Data.HashMap.Strict.HashMap'. -- -- Since the json can have multiple semantically equivalent representations of a task first serializing and then deserializing is not identity. -- But deserializing and then serializing should be. (Thus making serializing and deserializing idempotent.) data Task = Task {-        status      :: Status,-        uuid        :: UUID,-        entry       :: UTCTime,-        description :: Text,-        start       :: Maybe UTCTime,-        modified    :: Maybe UTCTime,-        due         :: Maybe UTCTime,-        until       :: Maybe UTCTime,-        annotations :: [Annotation],-        scheduled   :: Maybe UTCTime,-        project     :: Maybe Text,-        priority    :: Maybe Priority,-        depends     :: [UUID],-        tags        :: [Tag],-        uda         :: UDA+        status         :: Status,+        recurringChild :: Maybe RecurringChild,+        uuid           :: UUID,+        id             :: Maybe Integer,+        entry          :: UTCTime,+        description    :: Text,+        start          :: Maybe UTCTime,+        modified       :: Maybe UTCTime,+        due            :: Maybe UTCTime,+        until          :: Maybe UTCTime,+        annotations    :: [Annotation],+        scheduled      :: Maybe UTCTime,+        project        :: Maybe Text,+        priority       :: Maybe Priority,+        depends        :: [UUID],+        tags           :: [Tag],+        urgency        :: Double,+        uda            :: UDA } deriving (Eq, Show, Read) --- | A Tag can be basically any string. But beware: Special symbols work but might clash with `task` cli syntax. As an example you can use a space in a @'Tag'@. But then you cannot use @task +my tag@ on the command line.+-- | A Tag can be basically any string. But beware: Special symbols work but might clash with @task@ cli syntax. As an example you can use a space in a @'Tag'@. But then you cannot use @task +my tag@ on the command line. type Tag = Text  @@ -73,6 +90,7 @@ reservedKeys =   [ "status"   , "uuid"+  , "id"   , "description"   , "entry"   , "modified"@@ -91,14 +109,18 @@   , "imask"   , "parent"   , "recur"+  , "urgency"   ]  instance FromJSON Task where   parseJSON = withObject "Task" $ \object -> do     let parseTimeFromFieldMay = parseFromFieldWithMay Time.parse object         uda = HashMap.filterWithKey (\k _ -> k `notElem` reservedKeys) object-    status      <- Status.parseFromObject object-    uuid        <- object .: "uuid"+    status         <- Status.parseFromObject object+    recurringChild <- RecurringChild.parseFromObjectMay object+    uuid           <- object .: "uuid"+    idRaw          <- object .: "id"+    let id = if idRaw == 0 then Nothing else Just idRaw     entry       <- object .: "entry" >>= Time.parse     description <- object .: "description"     start       <- parseTimeFromFieldMay "start"@@ -112,6 +134,7 @@       <$> parseFromFieldWithMay Priority.parseMay object "priority"     depends <- maybe (pure []) parseUuidList (HashMap.lookup "depends" object)     tags    <- Foldable.fold <$> object .:? "tags"+    urgency <- object .: "urgency"     pure Task { until = until_, .. }  parseFromFieldWithMay@@ -131,9 +154,12 @@     Aeson.object       $  Status.toPairs status       <> [ "uuid" .= uuid+         , "id" .= fromMaybe 0 id          , "entry" .= Time.toValue entry          , "description" .= description+         , "urgency" .= urgency          ]+      <> maybe [] RecurringChild.toPairs recurringChild       <> ifNotNullList annotations ("annotations" .=)       <> Maybe.mapMaybe            (\(name, value) -> (name .=) . Time.toValue <$> value)@@ -161,16 +187,19 @@ makeTask uuid entry description = Task { uuid                                        , description                                        , entry-                                       , modified    = Just entry-                                       , status      = Status.Pending-                                       , due         = Nothing-                                       , priority    = Nothing-                                       , project     = Nothing-                                       , start       = Nothing-                                       , scheduled   = Nothing-                                       , until       = Nothing-                                       , annotations = []-                                       , depends     = []-                                       , tags        = []-                                       , uda         = HashMap.empty+                                       , id             = Nothing+                                       , modified       = Just entry+                                       , status         = Status.Pending+                                       , recurringChild = Nothing+                                       , due            = Nothing+                                       , priority       = Nothing+                                       , project        = Nothing+                                       , start          = Nothing+                                       , scheduled      = Nothing+                                       , until          = Nothing+                                       , annotations    = []+                                       , depends        = []+                                       , tags           = []+                                       , urgency        = 0+                                       , uda            = HashMap.empty                                        }
src/Taskwarrior/UDA.hs view
@@ -1,4 +1,4 @@--- | User defined attributes are stored in a HashMap from Text to json Values because we have no type information about them.+-- | User defined attributes are stored in a 'HashMap' from 'Text' to json 'Value's because we have no type information about them. module Taskwarrior.UDA   ( UDA   )
taskwarrior.cabal view
@@ -4,7 +4,7 @@ -- PVP summary:     +-+------- breaking API changes --                  | | +----- non-breaking API additions --                  | | | +--- code changes with no API change-version: 0.1.2.4+version: 0.2.0.0 synopsis:           Types and aeson instances for taskwarrior tasks description:   Types and aeson instances for the https://taskwarrior.org task import/export feature@@ -20,6 +20,10 @@   CHANGELOG.md   README.md +source-repository head+  type:     git+  location: git://github.com/maralorn/haskell-taskwarrior.git+ library   ghc-options:        -Wall   exposed-modules:@@ -27,6 +31,7 @@     Taskwarrior.IO     Taskwarrior.Mask     Taskwarrior.Priority+    Taskwarrior.RecurringChild     Taskwarrior.Status     Taskwarrior.Task     Taskwarrior.Time@@ -34,6 +39,7 @@    default-extensions:     LambdaCase+    NamedFieldPuns     OverloadedStrings     RecordWildCards     StrictData
test/TaskSpec.hs view
@@ -4,12 +4,16 @@   ) where +import           Prelude                 hiding ( id )+import           Control.Arrow                  ( second )+ import           Test.Hspec import           Test.QuickCheck import           Data.Aeson import           Taskwarrior.Task import           Taskwarrior.Mask import           Taskwarrior.Status+import           Taskwarrior.RecurringChild import           Taskwarrior.Annotation import           Taskwarrior.Priority import           Data.Time@@ -19,7 +23,6 @@ import           Test.QuickCheck.Instances.UnorderedContainers                                                 ( ) - prop_taskDeEncode :: Task -> Property prop_taskDeEncode task = Just task === decode (encode task) @@ -44,10 +47,12 @@     , Deleted <$> arbitrary     , Completed <$> arbitrary     , Waiting <$> arbitrary-    , RecurringParent <$> arbitrary <*> arbitrary-    , RecurringChild <$> arbitrary <*> arbitrary <*> arbitrary+    , Recurring <$> arbitrary <*> arbitrary     ] +instance Arbitrary RecurringChild where+  arbitrary = RecurringChild <$> arbitrary <*> arbitrary <*> arbitrary+ instance Arbitrary UTCTime where   arbitrary = do     day     <- ModifiedJulianDay <$> arbitrary@@ -65,8 +70,12 @@  instance Arbitrary Task where   arbitrary = do-    status      <- arbitrary+    status         <- arbitrary+    recurringChild <- case status of+      Recurring{} -> pure Nothing -- A task cannot be both a parent and child recurrence+      _           -> arbitrary     uuid        <- arbitrary+    id          <- arbitrary `suchThat` maybe True (>= 1) -- IDs can't be negative, and 0 is used as "not present"     entry       <- arbitrary     description <- arbitrary     start       <- arbitrary@@ -79,5 +88,6 @@     priority    <- arbitrary     depends     <- arbitrary     tags        <- arbitrary-    uda <- HashMap.fromList . fmap (\(x, y) -> (x, String y)) <$> arbitrary+    urgency     <- arbitrary+    uda         <- HashMap.fromList . fmap (second String) <$> arbitrary     pure Task { until = until_, .. }