module Composite.Aeson.Formats.DateTime
( DateTimeFormat(..), regularDateTimeFormat
, dateTimeJsonFormat
, iso8601DateJsonFormat, iso8601DateTimeJsonFormat, iso8601TimeJsonFormat
) where
import Composite.Aeson.Base (JsonFormat(JsonFormat), JsonProfunctor(JsonProfunctor))
import Composite.Aeson.DateTimeFormatUtils (fixupTzIn, fixupTzOut, fixupMs)
import Composite.Aeson.Formats.Provided (stringJsonFormat)
import Control.Monad.Error.Class (throwError)
import qualified Data.Aeson.BetterErrors as ABE
import Data.Either (partitionEithers)
import Data.Monoid ((<>))
import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty((:|)))
import qualified Data.List.NonEmpty as NEL
import Data.Time.Calendar (Day)
import Data.Time.Clock (UTCTime)
import Data.Time.Format (FormatTime, ParseTime, TimeLocale, defaultTimeLocale, formatTime, parseTimeM)
import Data.Time.LocalTime (TimeOfDay)
data DateTimeFormat = DateTimeFormat
{ dateTimeFormat :: String
, dateTimeFormatExample :: String
, dateTimeFormatPreParse :: String -> String
, dateTimeFormatPostFormat :: String -> String
}
regularDateTimeFormat :: String -> String -> DateTimeFormat
regularDateTimeFormat format example = DateTimeFormat format example id id
dateTimeJsonFormat :: (ParseTime t, FormatTime t) => TimeLocale -> NonEmpty DateTimeFormat -> JsonFormat e t
dateTimeJsonFormat locale formats@(outFormat :| otherInFormats) = JsonFormat (JsonProfunctor dayOut dayIn)
where
formatsList = NEL.toList formats
JsonFormat (JsonProfunctor stringOut stringIn) = stringJsonFormat
dayOut = stringOut . dateTimeFormatPostFormat outFormat . formatTime locale (dateTimeFormat outFormat)
dayIn = do
s <- stringIn
let attempt format = successOrFail Left Right $ parseTimeM True locale (dateTimeFormat format) (dateTimeFormatPreParse format s)
attempts = map attempt formatsList
case partitionEithers attempts of
(_, a : _) ->
pure a
(es, _) | null otherInFormats ->
toss $ "expected date/time string formatted as " <> dateTimeFormatExample outFormat <> ", but: " <> intercalate ", " es
(es, _) ->
toss $ "expected date/time string formatted as one of "
<> intercalate ", " (map dateTimeFormatExample formatsList)
<> ", but: " <> intercalate ", " es
toss = throwError . ABE.BadSchema [] . ABE.FromAeson
iso8601DateJsonFormat :: JsonFormat e Day
iso8601DateJsonFormat =
dateTimeJsonFormat defaultTimeLocale (fmt :| [])
where
fmt = regularDateTimeFormat "%F" "yyyy-mm-dd"
iso8601DateTimeJsonFormat :: JsonFormat e UTCTime
iso8601DateTimeJsonFormat =
dateTimeJsonFormat defaultTimeLocale (withMs :| [withoutMs])
where
withMs = DateTimeFormat "%FT%T.%Q%z" "yyyy-mm-ddThh:mm:ss.sssZ" fixupTzIn (fixupTzOut . fixupMs)
withoutMs = DateTimeFormat "%FT%T%z" "yyyy-mm-ddThh:mm:ssZ" fixupTzIn fixupTzOut
iso8601TimeJsonFormat :: JsonFormat e TimeOfDay
iso8601TimeJsonFormat =
dateTimeJsonFormat defaultTimeLocale (withMs :| [withoutMs])
where
withMs = DateTimeFormat "%T.%Q%z" "hh:mm:ss.sss" id fixupMs
withoutMs = DateTimeFormat "%T.%Q" "hh:mm:ss" id id
data SuccessOrFail a = Fail String | Success a
instance Functor SuccessOrFail where
fmap f (Success a) = Success (f a)
fmap _ (Fail f) = Fail f
instance Applicative SuccessOrFail where
pure = Success
Success f <*> Success a = Success (f a)
Success _ <*> Fail f = Fail f
Fail f <*> _ = Fail f
instance Monad SuccessOrFail where
return = Success
fail = Fail
Success a >>= k = k a
Fail f >>= _ = Fail f
successOrFail :: (String -> b) -> (a -> b) -> (forall m. Monad m => m a) -> b
successOrFail _ f (Success a) = f a
successOrFail f _ (Fail s) = f s