tasklite-core-0.5.0.0: source/ImportTask.hs
{-|
Datatype `ImportTask` plus instances and functions
-}
module ImportTask where
import Protolude (
Alternative ((<|>)),
Applicative (pure),
Char,
Eq ((==)),
Functor (fmap),
Generic,
Hashable (hash),
Integral (toInteger),
Maybe (..),
Num (abs),
Semigroup ((<>)),
Show,
Text,
asum,
fromMaybe,
hush,
isJust,
optional,
show,
($),
(&),
(.),
(/=),
(<&>),
(=<<),
(>>=),
(||),
)
import Protolude qualified as P
import Control.Arrow ((>>>))
import Data.Aeson (Value)
import Data.Aeson as Aeson (
FromJSON (parseJSON),
ToJSON,
Value (Object, String),
withObject,
(.!=),
(.:),
(.:?),
)
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Types (Parser, parseMaybe)
import Data.Hourglass (
DateTime,
Time (timeFromElapsedP),
TimeFormatString,
timePrint,
toFormat,
)
import Data.Text qualified as T
import Data.Time.ISO8601.Duration qualified as Iso
import Data.ULID (ULID, ulidFromInteger)
import FullTask qualified
import Note (Note (..))
import System.Hourglass (dateCurrent)
import Task (
Task (
Task,
awake_utc,
body,
closed_utc,
due_utc,
group_ulid,
metadata,
modified_utc,
priority_adjustment,
ready_utc,
recurrence_duration,
repetition_duration,
review_utc,
state,
ulid,
user,
waiting_utc
),
emptyTask,
textToTaskState,
)
import Utils (
emptyUlid,
parseUlidText,
parseUtc,
parseUtcNum,
setDateTime,
toUlidTime,
zeroTime,
zeroUlidTxt,
)
data Annotation = Annotation
{ entry :: Text
, description :: Text
}
deriving (Generic, Eq)
instance Hashable Annotation
instance ToJSON Annotation
instance FromJSON Annotation where
parseJSON = withObject "annotation" $ \o -> do
entry <- o .: "entry"
description <- o .: "description"
pure Annotation{..}
annotationToNote :: Annotation -> Note
annotationToNote annot@Annotation{entry, description} = do
let
utc = entry & parseUtc & fromMaybe (timeFromElapsedP 0 :: DateTime)
ulidGeneratedRes = annot & (hash >>> toInteger >>> abs >>> ulidFromInteger)
ulidCombined = (ulidGeneratedRes & P.fromRight emptyUlid) `setDateTime` utc
Note
{ ulid = (T.toLower . show) ulidCombined
, body = description
}
textToNote :: DateTime -> Text -> Note
textToNote utc body =
let
ulidGeneratedRes = body & (hash >>> toInteger >>> abs >>> ulidFromInteger)
ulidCombined = (ulidGeneratedRes & P.fromRight emptyUlid) `setDateTime` utc
in
Note
{ ulid = (T.toLower . show) ulidCombined
, body = body
}
importUtcFormat :: TimeFormatString
importUtcFormat =
toFormat ("YYYY-MM-DD H:MI:S.ms" :: [Char])
data ImportTask = ImportTask
{ task :: Task
, notes :: [Note]
, tags :: [Text]
}
deriving (Show)
emptyImportTask :: ImportTask
emptyImportTask =
ImportTask
{ task = emptyTask
, notes = []
, tags = []
}
-- | Values a suffixed with a prime (') to avoid name collisions
instance FromJSON ImportTask where
parseJSON = withObject "task" $ \o -> do
utc <- o .:? "utc"
entry <- o .:? "entry"
creation <- o .:? "creation"
creation_utc <- o .:? "creation_utc"
creationUtc <- o .:? "creationUtc"
created <- o .:? "created"
created_at <- o .:? "created_at"
createdAt <- o .:? "createdAt"
created_utc <- o .:? "created_utc"
createdUtc_ <- o .:? "createdUtc"
let
parsedCreatedUtc =
parseUtc
=<< ( utc
<|> entry
<|> creation
<|> creation_utc
<|> creationUtc
<|> created
<|> created_at
<|> createdAt
<|> created_utc
<|> createdUtc_
)
createdUtc = fromMaybe zeroTime parsedCreatedUtc
o_title <- o .:? "title"
o_body <- o .:? "body"
description <- o .:? "description"
let body = fromMaybe "" (o_title <|> o_body <|> description)
o_priority_adjustment <- o .:? "priority_adjustment"
urgency <- o .:? "urgency"
priority <- optional (o .: "priority")
let priority_adjustment = o_priority_adjustment <|> urgency <|> priority
modified <- o .:? "modified"
modified_at <- o .:? "modified_at"
o_modified_utc <- o .:? "modified_utc"
modification_date <- o .:? "modification_date"
updated_at <- o .:? "updated_at"
let
-- TODO: Parse the fields first and then combine them with `<|>`
maybeModified =
modified
<|> modified_at
<|> o_modified_utc
<|> modification_date
<|> updated_at
modified_utc =
maybeModified
>>= parseUtc
& fromMaybe createdUtc
& timePrint importUtcFormat
& T.pack
o_state <- o .:? "state"
status <- o .:? "status"
let
state = textToTaskState =<< (o_state <|> status)
implicitCloseUtcMaybe =
if isJust state
then
maybeModified
<|> Just (T.pack $ timePrint importUtcFormat createdUtc)
else Nothing
o_tags <- o .:? "tags"
(o_labelsMb :: Maybe [Value]) <- o .:? "labels"
let labels =
o_labelsMb
<&> ( ( \o_labels ->
o_labels <&> \case
String txt -> Just txt
Object obj ->
P.flip parseMaybe obj (\o_ -> o_ .:? "name" .!= "")
_ -> Nothing
)
>>> P.catMaybes
)
project <- o .:? "project"
let
projects = project <&> (: [])
tags = fromMaybe [] (o_tags <> labels <> projects)
due <- o .:? "due"
o_due_utc <- o .:? "due_utc"
due_on <- o .:? "due_on"
let
maybeDue = due <|> o_due_utc <|> due_on
due_utc =
fmap
(T.pack . timePrint importUtcFormat)
(parseUtc =<< maybeDue)
awake' <- o .:? "awake"
awake_at' <- o .:? "awake_at"
sleep' <- o .:? "sleep"
sleep_utc' <- o .:? "sleep_utc"
sleep_until' <- o .:? "sleep_until"
wait' <- o .:? "wait"
wait_until' <- o .:? "wait_until"
let
maybeAwake =
awake'
<|> awake_at'
<|> sleep'
<|> sleep_utc'
<|> sleep_until'
<|> wait'
<|> wait_until'
awake_utc =
fmap
(T.pack . timePrint importUtcFormat)
(parseUtc =<< maybeAwake)
ready' <- o .:? "ready"
ready_since' <- o .:? "ready_since"
ready_utc' <- o .:? "ready_utc"
let
maybeReady = ready' <|> ready_since' <|> ready_utc'
ready_utc =
fmap
(T.pack . timePrint importUtcFormat)
(parseUtc =<< maybeReady)
review' <- o .:? "review"
review_at' <- o .:? "review_at"
review_since' <- o .:? "review_since"
review_utc' <- o .:? "review_utc"
let
maybeReview = review' <|> review_at' <|> review_since' <|> review_utc'
review_utc =
fmap
(T.pack . timePrint importUtcFormat)
(parseUtc =<< maybeReview)
waiting' <- o .:? "waiting"
waiting_since' <- o .:? "waiting_since"
waiting_utc' <- o .:? "waiting_utc"
let
maybewaiting = waiting' <|> waiting_since' <|> waiting_utc'
waiting_utc =
fmap
(T.pack . timePrint importUtcFormat)
(parseUtc =<< maybewaiting)
closed <- o .:? "closed"
o_closed_utc <- o .:? "closed_utc"
closed_at <- o .:? "closed_at"
closed_on <- o .:? "closed_on"
end <- o .:? "end"
o_end_utc <- o .:? "end_utc"
end_on <- o .:? "end_on"
let
maybeClosed =
closed
<|> o_closed_utc
<|> closed_at
<|> closed_on
<|> end
<|> o_end_utc
<|> end_on
<|> implicitCloseUtcMaybe
closed_utc =
fmap
(T.pack . timePrint importUtcFormat)
(parseUtc =<< maybeClosed)
group_ulid <- o .:? "group_ulid"
let parseIsoDurationMb durTextMb =
hush $
fmap (P.decodeUtf8 . Iso.formatDuration) $
Iso.parseDuration $
P.encodeUtf8 $
fromMaybe "" durTextMb
repetition_duration' <- o .:? "repetition_duration"
repeat_duration' <- o .:? "repeat_duration"
let
maybeRepetition = repetition_duration' <|> repeat_duration'
repetition_duration = parseIsoDurationMb maybeRepetition
recurrence_duration' <- o .:? "recurrence_duration"
recur_duration' <- o .:? "recur_duration"
let
maybeRecurrence = recurrence_duration' <|> recur_duration'
recurrence_duration = parseIsoDurationMb maybeRecurrence
o_notes <-
asum
[ o .:? "notes" :: Parser (Maybe [Note])
, do
notesMb <- o .:? "notes" :: Parser (Maybe [Text])
pure $ case notesMb of
Just textNotes -> Just $ textNotes <&> textToNote createdUtc
Nothing -> Just []
]
annotations <- o .:? "annotations" :: Parser (Maybe [Annotation])
let
notes = case (o_notes, annotations) of
(Nothing, Nothing) -> []
(Nothing, Just values) -> values <&> annotationToNote
(Just theNotes, _) -> case parsedCreatedUtc of
Just crUtc ->
theNotes
<&> ( \theNote ->
theNote
{ Note.ulid =
theNote.ulid
& parseUlidText
<&> P.flip setDateTime crUtc
<&> show @ULID
& fromMaybe theNote.ulid
& T.toLower
}
)
Nothing -> theNotes
(o_userMb :: Maybe Value) <- o .:? "user"
o_author <- o .:? "author"
let
o_userNormMb =
o_userMb >>= \case
String txt -> Just txt
Object obj ->
P.flip parseMaybe obj (\o_ -> o_ .:? "login" .!= "")
_ -> Nothing
userMaybe = o_userNormMb <|> o_author
user = fromMaybe "" userMaybe
o_metadata <- o .:? "metadata"
let
-- Only delete fields with highest priority,
-- as they would definitely have been used if available
-- TODO: Check which fields were actually used and delete only them
-- (Crudly done for title and body)
metadata =
o_metadata
<|> ( ( o
& ( case (o_title, o_body) of
(Nothing, Just _) -> KeyMap.delete "body"
_ -> KeyMap.delete "title"
)
& KeyMap.delete "utc"
& KeyMap.delete "priority_adjustment"
& KeyMap.delete "tags"
& (if notes /= [] then KeyMap.delete "notes" else P.identity)
)
& ( \val ->
if val == KeyMap.empty
then
Nothing
else Just (Object val)
)
)
tempTask = Task{ulid = "", ..}
o_ulid <- o .:? "ulid"
let
ulidGeneratedRes =
tempTask & (hash >>> toInteger >>> abs >>> ulidFromInteger)
ulidCombined =
(ulidGeneratedRes & P.fromRight emptyUlid) `setDateTime` createdUtc
ulid =
T.toLower $
fromMaybe
""
(o_ulid <|> Just (show ulidCombined))
-- let showInt = show :: Int -> Text
-- uuid <- o .:? "uuid"
-- -- Map `show` over `Parser` & `Maybe` to convert possible `Int` to `Text`
-- id <- (o .:? "id" <|> ((showInt <$>) <$> (o .:? "id")))
-- let id = (uuid <|> id)
let finalTask = tempTask{Task.ulid = ulid}
pure $ ImportTask finalTask notes tags
setMissingFields :: ImportTask -> P.IO ImportTask
setMissingFields importTaskRec = do
now <- dateCurrent
let nowUlidTxt = now & toUlidTime & show & T.toLower
pure $
importTaskRec
{ task =
importTaskRec.task
{ Task.ulid =
if zeroUlidTxt `T.isPrefixOf` importTaskRec.task.ulid
then
importTaskRec.task.ulid
& T.replace zeroUlidTxt nowUlidTxt
else importTaskRec.task.ulid
, Task.modified_utc =
if importTaskRec.task.modified_utc == ""
|| importTaskRec.task.modified_utc == "1970-01-01 00:00:00"
|| importTaskRec.task.modified_utc == "1970-01-01 00:00:00.000"
|| parseUtc importTaskRec.task.modified_utc == parseUtcNum 0
then
now
& timePrint (toFormat importUtcFormat)
& T.pack
else show importTaskRec.task.modified_utc
}
}
importTaskToFullTask :: ImportTask -> FullTask.FullTask
importTaskToFullTask ImportTask{task, notes, tags} =
FullTask.FullTask
{ FullTask.ulid = task.ulid
, FullTask.body = task.body
, FullTask.modified_utc = task.modified_utc
, FullTask.awake_utc = task.awake_utc
, FullTask.ready_utc = task.ready_utc
, FullTask.waiting_utc = task.waiting_utc
, FullTask.review_utc = task.review_utc
, FullTask.due_utc = task.due_utc
, FullTask.closed_utc = task.closed_utc
, FullTask.state = task.state
, FullTask.group_ulid = task.group_ulid
, FullTask.repetition_duration = task.repetition_duration
, FullTask.recurrence_duration = task.recurrence_duration
, FullTask.tags = Just tags
, FullTask.notes = Just notes
, FullTask.priority = task.priority_adjustment
, FullTask.user = task.user
, FullTask.metadata = task.metadata
}