packages feed

tasklite-core-0.3.0.0: source/FullTask.hs

{-# LANGUAGE QuasiQuotes #-}

{-|
Data type to represent tasks from the `tasks_view`
-}
module FullTask where

import Protolude (
  Applicative ((<*>)),
  Eq ((==)),
  Float,
  Generic,
  Hashable,
  Maybe (Just, Nothing),
  Show,
  decodeUtf8,
  encodeUtf8,
  show,
  toStrict,
  ($),
  (.),
  (<$>),
  (<&>),
  (<>),
 )

import Data.Aeson as Aeson (ToJSON, Value (Object))
import Data.Aeson.Text as Aeson (encodeToLazyText)
import Data.Csv as Csv (
  DefaultOrdered,
  ToField (..),
  ToNamedRecord,
  ToRecord,
 )
import Data.Text as T (Text, dropEnd, intercalate, split, splitOn)
import Data.Yaml as Yaml (encode)
import Database.SQLite.Simple (
  FromRow (..),
  Query,
  ResultError (ConversionFailed),
  SQLData (SQLNull, SQLText),
  field,
 )
import Database.SQLite.Simple.FromField (FromField (..), fieldData, returnError)
import Database.SQLite.Simple.FromRow (fieldWith)
import Database.SQLite.Simple.Internal (Field (Field))
import Database.SQLite.Simple.Ok (Ok (Errors, Ok))
import Database.SQLite.Simple.QQ (sql)
import GHC.Exception (errorCallException)
import Prettyprinter (Pretty (pretty))

import Note (Note (..))
import Task (
  Task (
    awake_utc,
    closed_utc,
    due_utc,
    modified_utc,
    ready_utc,
    review_utc,
    state,
    ulid,
    waiting_utc
  ),
  TaskState,
  zeroTask,
 )


-- | Final user-facing format of tasks
data FullTask = FullTask
  { ulid :: Text -- TODO: Use Ulid type
  , body :: Text
  , modified_utc :: Text
  , awake_utc :: Maybe Text
  , ready_utc :: Maybe Text
  , waiting_utc :: Maybe Text
  , review_utc :: Maybe Text
  , due_utc :: Maybe Text
  , closed_utc :: Maybe Text
  , state :: Maybe TaskState
  , group_ulid :: Maybe Text
  , repetition_duration :: Maybe Text
  , recurrence_duration :: Maybe Text
  , tags :: Maybe [Text]
  , notes :: Maybe [Note]
  , priority :: Maybe Float
  , user :: Text
  , metadata :: Maybe Aeson.Value
  }
  deriving (Generic, Show, Eq)


-- For conversion from SQLite with SQLite.Simple
instance FromRow FullTask where
  fromRow =
    FullTask
      <$> field
      <*> field
      <*> field
      <*> field
      <*> field
      <*> field
      <*> field
      <*> field
      <*> field
      <*> field
      <*> field
      <*> field
      <*> field
      -- Tags
      <*> fieldWith
        ( \value -> case fieldData value of
            SQLText txt ->
              -- Parse tags as a comma-separated list
              -- TODO: Parse tags as a JSON array
              Ok (Just (T.splitOn "," txt))
            SQLNull -> Ok Nothing
            val ->
              Errors
                [ errorCallException $
                    "Expected a string, but got: " <> show val
                ]
        )
      -- Notes
      <*> fieldWith
        ( \value -> case fieldData value of
            SQLText _txt ->
              -- TODO: Parse notes as a JSON array
              -- Must return a `Just` to signal that notes exist
              Ok (Just [])
            SQLNull -> Ok Nothing
            val ->
              Errors
                [ errorCallException $
                    "Expected a string, but got: " <> show val
                ]
        )
      <*> field
      <*> field
      <*> ( field <&> \case
              Just (Object obj) -> Just $ Object obj
              _ -> Nothing
          )


instance FromField [Text] where
  fromField (Field (SQLText txt) _) = Ok $ split (== ',') txt
  fromField f = returnError ConversionFailed f "expecting SQLText column type"


instance FromField [Note] where
  fromField (Field (SQLText txt) _) =
    let notes = split (== ',') txt
    in  Ok $ notes <&> Note ""
  fromField f = returnError ConversionFailed f "expecting SQLText column type"


instance Csv.ToField [Text] where
  toField = encodeUtf8 . T.intercalate ","


instance Csv.ToField Value where
  toField = encodeUtf8 . toStrict . Aeson.encodeToLazyText


-- For conversion to CSV
instance ToRecord FullTask
instance ToNamedRecord FullTask
instance DefaultOrdered FullTask


instance ToJSON FullTask


instance Hashable FullTask


instance Pretty FullTask where
  pretty =
    pretty
      . T.dropEnd 1 -- Drop trailing newline to maybe add it later
      . decodeUtf8
      . Yaml.encode


emptyFullTask :: FullTask
emptyFullTask =
  FullTask
    { ulid = ""
    , body = ""
    , modified_utc = ""
    , awake_utc = Nothing
    , ready_utc = Nothing
    , waiting_utc = Nothing
    , review_utc = Nothing
    , due_utc = Nothing
    , closed_utc = Nothing
    , state = Nothing
    , group_ulid = Nothing
    , repetition_duration = Nothing
    , recurrence_duration = Nothing
    , tags = Nothing
    , notes = Nothing
    , priority = Nothing
    , user = ""
    , metadata = Nothing
    }


selectQuery :: Query
selectQuery =
  [sql|
    SELECT
      tasks_view.ulid AS ulid,
      body,
      modified_utc,
      awake_utc,
      ready_utc,
      waiting_utc,
      review_utc,
      due_utc,
      closed_utc,
      state,
      group_ulid,
      repetition_duration,
      recurrence_duration,
      tags,
      notes,
      priority,
      user,
      metadata
  |]


cpTimesAndState :: FullTask -> Task
cpTimesAndState (FullTask{..}) =
  zeroTask
    { Task.ulid
    , Task.modified_utc
    , Task.awake_utc
    , Task.ready_utc
    , Task.waiting_utc
    , Task.review_utc
    , Task.due_utc
    , Task.closed_utc
    , Task.state
    }