toml-reader-0.1.0.0: src/TOML/Decode.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeSynonymInstances #-}
module TOML.Decode (
-- * Decoding functions
decode,
decodeWith,
decodeWithOpts,
decodeFile,
-- * Decoder interface
DecodeTOML (..),
Decoder (..),
-- ** Decoder getters
getField,
getFields,
getFieldOpt,
getFieldsOpt,
getFieldWith,
getFieldsWith,
getFieldOptWith,
getFieldsOptWith,
getArrayOf,
-- ** Build custom Decoder
DecodeM (..),
makeDecoder,
runDecoder,
addContextItem,
invalidValue,
typeMismatch,
decodeFail,
decodeError,
) where
import Control.Applicative (Alternative (..), Const (..))
import Control.Monad (zipWithM)
#if MIN_VERSION_base(4,9,0) && !MIN_VERSION_base(4,13,0)
import qualified Control.Monad.Fail as MonadFail
#endif
import Data.Bifunctor (first)
import Data.Fixed (Fixed, HasResolution)
import Data.Functor.Identity (Identity (..))
import Data.Int (Int16, Int32, Int64, Int8)
import Data.IntMap (IntMap)
import qualified Data.IntMap as IntMap
import Data.IntSet (IntSet)
import qualified Data.IntSet as IntSet
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Map (Map)
import qualified Data.Map.Strict as Map
import qualified Data.Monoid as Monoid
import Data.Proxy (Proxy (..))
import Data.Ratio (Ratio)
import qualified Data.Semigroup as Semigroup
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Data.Set (Set)
import qualified Data.Set as Set
import Data.String (IsString, fromString)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import qualified Data.Text.Lazy as Lazy (Text)
import qualified Data.Text.Lazy as Text.Lazy
import qualified Data.Time as Time
import qualified Data.Time.Clock.System as Time
import Data.Version (Version, parseVersion)
import Data.Void (Void)
import Data.Word (Word16, Word32, Word64, Word8)
import Numeric.Natural (Natural)
import Text.ParserCombinators.ReadP (readP_to_S)
import TOML.Error (
ContextItem (..),
DecodeContext,
DecodeError (..),
TOMLError (..),
)
import TOML.Parser (parseTOML)
import TOML.Value (Value (..))
{--- Decoder ---}
{- |
A @Decoder a@ represents a function for decoding a TOML value to a value of type @a@.
Generally, you'd only need to chain the @getField*@ functions together, like
@
decoder =
MyConfig
\<$> getField "a"
\<*> getField "b"
\<*> getField "c"
@
or use interfaces like 'Monad' and 'Alternative':
@
decoder = do
cfgType <- getField "type"
case cfgType of
"int" -> MyIntValue \<$> (getField "int" \<|> getField "integer")
"bool" -> MyBoolValue \<$> getField "bool"
_ -> fail $ "Invalid type: " <> cfgType
@
but you can also manually implement a 'Decoder' with 'makeDecoder'.
-}
newtype Decoder a = Decoder {unDecoder :: Value -> DecodeM a}
instance Functor Decoder where
fmap f = Decoder . (fmap . fmap) f . unDecoder
instance Applicative Decoder where
pure v = Decoder $ \_ -> pure v
Decoder decodeF <*> Decoder decodeV = Decoder $ \v -> decodeF v <*> decodeV v
instance Monad Decoder where
Decoder decodeA >>= f = Decoder $ \v -> do
a <- decodeA v
let Decoder decodeB = f a
decodeB v
#if !MIN_VERSION_base(4,13,0)
fail msg = Decoder $ \_ -> decodeFail $ Text.pack msg
#endif
instance Alternative Decoder where
empty = fail "Decoder.Alternative: empty"
Decoder decode1 <|> Decoder decode2 = Decoder $ \v -> decode1 v <|> decode2 v
#if MIN_VERSION_base(4,13,0)
instance MonadFail Decoder where
fail msg = Decoder $ \_ -> decodeFail $ Text.pack msg
#elif MIN_VERSION_base(4,9,0)
instance MonadFail.MonadFail Decoder where
fail msg = Decoder $ \_ -> decodeFail $ Text.pack msg
#endif
-- | Manually implement a 'Decoder' with the given function.
makeDecoder :: (Value -> DecodeM a) -> Decoder a
makeDecoder = Decoder
decoderToEither :: Decoder a -> Value -> DecodeContext -> Either (DecodeContext, DecodeError) a
decoderToEither decoder v ctx = unDecodeM (unDecoder decoder v) ctx
-- | The underlying decoding monad that either returns a value of type @a@ or returns an error.
newtype DecodeM a = DecodeM {unDecodeM :: DecodeContext -> Either (DecodeContext, DecodeError) a}
instance Functor DecodeM where
fmap f = DecodeM . (fmap . fmap) f . unDecodeM
instance Applicative DecodeM where
pure v = DecodeM $ \_ -> pure v
DecodeM decodeF <*> DecodeM decodeV = DecodeM $ \ctx -> decodeF ctx <*> decodeV ctx
instance Monad DecodeM where
DecodeM decodeA >>= f = DecodeM $ \ctx -> do
a <- decodeA ctx
let DecodeM decodeB = f a
decodeB ctx
#if !MIN_VERSION_base(4,13,0)
fail = decodeFail . Text.pack
#endif
instance Alternative DecodeM where
empty = decodeFail "DecodeM.Alternative: empty"
DecodeM decode1 <|> DecodeM decode2 = DecodeM $ \ctx ->
case decode1 ctx of
Left _ -> decode2 ctx
Right x -> Right x
#if MIN_VERSION_base(4,13,0)
instance MonadFail DecodeM where
fail = decodeFail . Text.pack
#elif MIN_VERSION_base(4,9,0)
instance MonadFail.MonadFail DecodeM where
fail = decodeFail . Text.pack
#endif
{- |
Run a 'Decoder' with the given 'Value'.
@
makeDecoder $ \\v -> do
a <- runDecoder decoder1 v
b <- runDecoder decoder2 v
return (a, b)
@
Satisfies
@
makeDecoder . runDecoder === id
runDecoder . makeDecoder === id
@
-}
runDecoder :: Decoder a -> Value -> DecodeM a
runDecoder decoder v = DecodeM (decoderToEither decoder v)
{- |
Throw an error indicating that the given 'Value' is invalid.
@
makeDecoder $ \\v ->
case v of
Integer 42 -> invalidValue "We don't like this number" v
_ -> runDecoder tomlDecoder v
-- or alternatively,
tomlDecoder >>= \case
42 -> makeDecoder $ invalidValue "We don't like this number"
v -> pure v
@
-}
invalidValue :: Text -> Value -> DecodeM a
invalidValue msg v = decodeError $ InvalidValue msg v
{- |
Throw an error indicating that the given 'Value' isn't the correct type of value.
@
makeDecoder $ \\v ->
case v of
String s -> ...
_ -> typeMismatch v
@
-}
typeMismatch :: Value -> DecodeM a
typeMismatch v = decodeError $ TypeMismatch v
-- | Throw a generic failure message.
decodeFail :: Text -> DecodeM a
decodeFail msg = decodeError $ OtherDecodeError msg
-- | Throw the given 'DecodeError'.
decodeError :: DecodeError -> DecodeM a
decodeError e = DecodeM $ \ctx -> Left (ctx, e)
addContextItem :: ContextItem -> DecodeM a -> DecodeM a
addContextItem p m = DecodeM $ \ctx -> unDecodeM m (ctx <> [p])
{--- Decoding ---}
-- | Decode the given TOML input.
decode :: DecodeTOML a => Text -> Either TOMLError a
decode = decodeWith tomlDecoder
-- | Decode the given TOML input using the given 'Decoder'.
decodeWith :: Decoder a -> Text -> Either TOMLError a
decodeWith decoder = decodeWithOpts decoder ""
decodeWithOpts :: Decoder a -> String -> Text -> Either TOMLError a
decodeWithOpts decoder filename input = do
v <- parseTOML filename input
first (uncurry DecodeError) $ decoderToEither decoder v []
-- | Decode a TOML file at the given file path.
decodeFile :: DecodeTOML a => FilePath -> IO (Either TOMLError a)
decodeFile fp = decodeWithOpts tomlDecoder fp <$> Text.readFile fp
{--- Decoder helpers ---}
{- |
Decode a field in a TOML Value.
Equivalent to 'getFields' with a single-element list.
@
a = 1
b = 'asdf'
@
@
-- MyConfig 1 "asdf"
MyConfig \<$> getField "a" \<*> getField "b"
@
-}
getField :: DecodeTOML a => Text -> Decoder a
getField = getFieldWith tomlDecoder
-- | Same as 'getField', except with the given 'Decoder'.
getFieldWith :: Decoder a -> Text -> Decoder a
getFieldWith decoder key = getFieldsWith decoder [key]
{- |
Decode a field in a TOML Value, or Nothing if the field doesn't exist.
Equivalent to 'getFieldsOpt' with a single-element list.
@
a = 1
@
@
-- MyConfig (Just 1) Nothing
MyConfig \<$> getFieldOpt "a" \<*> getFieldOpt "b"
@
-}
getFieldOpt :: DecodeTOML a => Text -> Decoder (Maybe a)
getFieldOpt = getFieldOptWith tomlDecoder
-- | Same as 'getFieldOpt', except with the given 'Decoder'.
getFieldOptWith :: Decoder a -> Text -> Decoder (Maybe a)
getFieldOptWith decoder key = getFieldsOptWith decoder [key]
{- |
Decode a nested field in a TOML Value.
@
a.b = 1
@
@
-- MyConfig 1
MyConfig \<$> getFields ["a", "b"]
@
-}
getFields :: DecodeTOML a => [Text] -> Decoder a
getFields = getFieldsWith tomlDecoder
-- | Same as 'getFields', except with the given 'Decoder'.
getFieldsWith :: Decoder a -> [Text] -> Decoder a
getFieldsWith decoder = makeDecoder . go
where
go [] v = runDecoder decoder v
go (k : ks) v =
case v of
Table o ->
addContextItem (Key k) $
case Map.lookup k o of
Just v' -> go ks v'
Nothing -> decodeError MissingField
_ -> typeMismatch v
{- |
Decode a nested field in a TOML Value, or 'Nothing' if any of the fields don't exist.
@
a.b = 1
@
@
-- MyConfig (Just 1) Nothing Nothing
MyConfig
\<$> getFieldsOpt ["a", "b"]
\<*> getFieldsOpt ["a", "c"]
\<*> getFieldsOpt ["b", "c"]
@
-}
getFieldsOpt :: DecodeTOML a => [Text] -> Decoder (Maybe a)
getFieldsOpt = getFieldsOptWith tomlDecoder
-- | Same as 'getFieldsOpt', except with the given 'Decoder'.
getFieldsOptWith :: Decoder a -> [Text] -> Decoder (Maybe a)
getFieldsOptWith decoder keys =
makeDecoder $ \v ->
DecodeM $ \ctx ->
case (`unDecodeM` ctx) . (`runDecoder` v) $ getFieldsWith decoder keys of
Left (_, MissingField) -> Right Nothing
Left (ctx', e) -> Left (ctx', e)
Right x -> Right $ Just x
{- |
Decode a list of values using the given 'Decoder'.
@
[[a]]
b = 1
[[a]]
b = 2
@
@
-- MyConfig [1, 2]
MyConfig
\<$> getFieldWith (getArrayOf (getField "b")) "a"
@
-}
getArrayOf :: Decoder a -> Decoder [a]
getArrayOf decoder =
makeDecoder $ \case
Array vs -> zipWithM (\i -> addContextItem (Index i) . runDecoder decoder) [0 ..] vs
v -> typeMismatch v
{--- DecodeTOML ---}
{- |
A type class containing the default 'Decoder' for the given type.
See the docs for 'Decoder' for examples.
-}
class DecodeTOML a where
tomlDecoder :: Decoder a
instance DecodeTOML Value where
tomlDecoder = Decoder pure
instance DecodeTOML Void where
tomlDecoder = makeDecoder typeMismatch
instance DecodeTOML Bool where
tomlDecoder =
makeDecoder $ \case
Boolean x -> pure x
v -> typeMismatch v
instance DecodeTOML Integer where
tomlDecoder =
makeDecoder $ \case
Integer x -> pure x
v -> typeMismatch v
tomlDecoderInt :: forall a. Num a => Decoder a
tomlDecoderInt = fromInteger <$> tomlDecoder
tomlDecoderBoundedInt :: forall a. (Integral a, Bounded a) => Decoder a
tomlDecoderBoundedInt =
tomlDecoder >>= \case
x
| x < toInteger (minBound @a) -> makeDecoder $ invalidValue "Underflow"
| x > toInteger (maxBound @a) -> makeDecoder $ invalidValue "Overflow"
| otherwise -> pure $ fromInteger x
instance DecodeTOML Int where
tomlDecoder = tomlDecoderBoundedInt
instance DecodeTOML Int8 where
tomlDecoder = tomlDecoderBoundedInt
instance DecodeTOML Int16 where
tomlDecoder = tomlDecoderBoundedInt
instance DecodeTOML Int32 where
tomlDecoder = tomlDecoderBoundedInt
instance DecodeTOML Int64 where
tomlDecoder = tomlDecoderBoundedInt
instance DecodeTOML Word where
tomlDecoder = tomlDecoderBoundedInt
instance DecodeTOML Word8 where
tomlDecoder = tomlDecoderBoundedInt
instance DecodeTOML Word16 where
tomlDecoder = tomlDecoderBoundedInt
instance DecodeTOML Word32 where
tomlDecoder = tomlDecoderBoundedInt
instance DecodeTOML Word64 where
tomlDecoder = tomlDecoderBoundedInt
instance DecodeTOML Natural where
tomlDecoder =
tomlDecoder >>= \case
x
| x >= 0 -> pure $ fromInteger x
| otherwise -> makeDecoder $ invalidValue "Got negative number"
instance DecodeTOML Double where
tomlDecoder =
makeDecoder $ \case
Float x -> pure x
v -> typeMismatch v
tomlDecoderFrac :: Fractional a => Decoder a
tomlDecoderFrac = realToFrac <$> tomlDecoder @Double
instance DecodeTOML Float where
tomlDecoder = tomlDecoderFrac
instance Integral a => DecodeTOML (Ratio a) where
tomlDecoder = tomlDecoderFrac
instance HasResolution a => DecodeTOML (Fixed a) where
tomlDecoder = tomlDecoderFrac
instance DecodeTOML Char where
tomlDecoder =
tomlDecoder >>= \case
s
| Text.length s == 1 -> pure $ Text.head s
| otherwise -> makeDecoder $ invalidValue "Expected single character string"
instance {-# OVERLAPPING #-} DecodeTOML String where
tomlDecoder = Text.unpack <$> tomlDecoder
instance DecodeTOML Text where
tomlDecoder =
makeDecoder $ \case
String s -> pure s
v -> typeMismatch v
instance DecodeTOML Lazy.Text where
tomlDecoder = Text.Lazy.fromStrict <$> tomlDecoder
instance DecodeTOML Time.ZonedTime where
tomlDecoder =
makeDecoder $ \case
OffsetDateTime (lt, tz) -> pure $ Time.ZonedTime lt tz
v -> typeMismatch v
instance DecodeTOML Time.UTCTime where
tomlDecoder = Time.zonedTimeToUTC <$> tomlDecoder
instance DecodeTOML Time.SystemTime where
tomlDecoder = Time.utcToSystemTime . Time.zonedTimeToUTC <$> tomlDecoder
instance DecodeTOML Time.LocalTime where
tomlDecoder =
makeDecoder $ \case
LocalDateTime dt -> pure dt
v -> typeMismatch v
instance DecodeTOML Time.Day where
tomlDecoder =
makeDecoder $ \case
LocalDate d -> pure d
v -> typeMismatch v
instance DecodeTOML Time.TimeOfDay where
tomlDecoder =
makeDecoder $ \case
LocalTime t -> pure t
v -> typeMismatch v
#if MIN_VERSION_time(1,9,0)
instance DecodeTOML Time.DayOfWeek where
tomlDecoder = toDayOfWeek . Text.toLower =<< tomlDecoder
where
toDayOfWeek = \case
"monday" -> pure Time.Monday
"tuesday" -> pure Time.Tuesday
"wednesday" -> pure Time.Wednesday
"thursday" -> pure Time.Thursday
"friday" -> pure Time.Friday
"saturday" -> pure Time.Saturday
"sunday" -> pure Time.Sunday
_ -> makeDecoder $ invalidValue "Invalid day of week"
#endif
instance DecodeTOML Time.DiffTime where
tomlDecoder = tomlDecoderInt <|> tomlDecoderFrac
instance DecodeTOML Time.NominalDiffTime where
tomlDecoder = tomlDecoderInt <|> tomlDecoderFrac
#if MIN_VERSION_time(1,9,0)
instance DecodeTOML Time.CalendarDiffTime where
tomlDecoder =
Time.CalendarDiffTime
<$> getField "months"
<*> getField "time"
instance DecodeTOML Time.CalendarDiffDays where
tomlDecoder =
Time.CalendarDiffDays
<$> getField "months"
<*> getField "days"
#endif
instance DecodeTOML Version where
tomlDecoder = go . readP_to_S parseVersion =<< tomlDecoder
where
go ((v, []) : _) = pure v
go (_ : vs) = go vs
go [] = makeDecoder $ invalidValue "Invalid Version"
instance DecodeTOML Ordering where
tomlDecoder =
tomlDecoder @Text >>= \case
"LT" -> pure LT
"EQ" -> pure EQ
"GT" -> pure GT
_ -> makeDecoder $ invalidValue "Invalid Ordering"
instance DecodeTOML a => DecodeTOML (Identity a) where
tomlDecoder = Identity <$> tomlDecoder
instance DecodeTOML (Proxy a) where
tomlDecoder = pure Proxy
instance DecodeTOML a => DecodeTOML (Const a b) where
tomlDecoder = Const <$> tomlDecoder
{- |
Since TOML doesn't support literal NULLs, this will only ever return 'Just'.
To get the absence of a field, use 'getFieldOpt' or one of its variants.
-}
instance DecodeTOML a => DecodeTOML (Maybe a) where
tomlDecoder = Just <$> tomlDecoder
instance (DecodeTOML a, DecodeTOML b) => DecodeTOML (Either a b) where
tomlDecoder = (Right <$> tomlDecoder) <|> (Left <$> tomlDecoder)
instance DecodeTOML a => DecodeTOML (Monoid.First a) where
tomlDecoder = Monoid.First <$> tomlDecoder
instance DecodeTOML a => DecodeTOML (Monoid.Last a) where
tomlDecoder = Monoid.Last <$> tomlDecoder
instance DecodeTOML a => DecodeTOML (Semigroup.First a) where
tomlDecoder = Semigroup.First <$> tomlDecoder
instance DecodeTOML a => DecodeTOML (Semigroup.Last a) where
tomlDecoder = Semigroup.Last <$> tomlDecoder
instance DecodeTOML a => DecodeTOML (Semigroup.Max a) where
tomlDecoder = Semigroup.Max <$> tomlDecoder
instance DecodeTOML a => DecodeTOML (Semigroup.Min a) where
tomlDecoder = Semigroup.Min <$> tomlDecoder
instance DecodeTOML a => DecodeTOML (Monoid.Dual a) where
tomlDecoder = Monoid.Dual <$> tomlDecoder
instance DecodeTOML a => DecodeTOML [a] where
tomlDecoder = getArrayOf tomlDecoder
instance (IsString k, Ord k, DecodeTOML v) => DecodeTOML (Map k v) where
tomlDecoder =
makeDecoder $ \case
Table o -> Map.mapKeys (fromString . Text.unpack) <$> mapM (runDecoder tomlDecoder) o
v -> typeMismatch v
instance DecodeTOML a => DecodeTOML (NonEmpty a) where
tomlDecoder = maybe raiseEmpty pure . NonEmpty.nonEmpty =<< tomlDecoder
where
raiseEmpty = makeDecoder $ invalidValue "Got empty list"
instance DecodeTOML IntSet where
tomlDecoder = IntSet.fromList <$> tomlDecoder
instance (DecodeTOML a, Ord a) => DecodeTOML (Set a) where
tomlDecoder = Set.fromList <$> tomlDecoder
instance DecodeTOML a => DecodeTOML (IntMap a) where
tomlDecoder = IntMap.fromList <$> tomlDecoder
instance DecodeTOML a => DecodeTOML (Seq a) where
tomlDecoder = Seq.fromList <$> tomlDecoder
tomlDecoderTuple :: ([Value] -> Maybe (DecodeM a)) -> Decoder a
tomlDecoderTuple f =
makeDecoder $ \case
Array vs | Just decodeM <- f vs -> decodeM
v -> typeMismatch v
decodeElem :: DecodeTOML a => Int -> Value -> DecodeM a
decodeElem i v = addContextItem (Index i) (runDecoder tomlDecoder v)
instance DecodeTOML () where
tomlDecoder = tomlDecoderTuple $ \case
[] -> Just $ pure ()
_ -> Nothing
instance (DecodeTOML a, DecodeTOML b) => DecodeTOML (a, b) where
tomlDecoder = tomlDecoderTuple $ \case
[a, b] ->
Just $
(,)
<$> decodeElem 0 a
<*> decodeElem 1 b
_ -> Nothing
instance (DecodeTOML a, DecodeTOML b, DecodeTOML c) => DecodeTOML (a, b, c) where
tomlDecoder = tomlDecoderTuple $ \case
[a, b, c] ->
Just $
(,,)
<$> decodeElem 0 a
<*> decodeElem 1 b
<*> decodeElem 2 c
_ -> Nothing
instance (DecodeTOML a, DecodeTOML b, DecodeTOML c, DecodeTOML d) => DecodeTOML (a, b, c, d) where
tomlDecoder = tomlDecoderTuple $ \case
[a, b, c, d] ->
Just $
(,,,)
<$> decodeElem 0 a
<*> decodeElem 1 b
<*> decodeElem 2 c
<*> decodeElem 3 d
_ -> Nothing