Z-Data-1.0.0.0: Z/Data/JSON/Converter.hs
{-|
Module : Z.Data.JSON.Converter
Description : IR Converter
Copyright : (c) Dong Han, 2019
License : BSD
Maintainer : winterland1989@gmail.com
Stability : experimental
Portability : non-portable
This module provides tools for converting protocol IR (e.g. 'Value') to Haskell ADTs:
-}
module Z.Data.JSON.Converter where
import Control.Applicative
import Control.Monad
import qualified Control.Monad.Fail as Fail
import Control.DeepSeq
import GHC.Generics
import qualified Z.Data.JSON.Builder as JB
import qualified Z.Data.Text as T
import qualified Z.Data.Text.Print as T
-- | Run a 'Converter' with input value.
convert :: (a -> Converter r) -> a -> Either ConvertError r
{-# INLINE convert #-}
convert m v = runConverter (m v) (\ paths msg -> (Left (ConvertError paths msg))) Right
-- | Elements of a (JSON) Value path used to describe the location of an error.
data PathElement
= Key {-# UNPACK #-} !T.Text
-- ^ Path element of a key into an object,
-- \"object.key\".
| Index {-# UNPACK #-} !Int
-- ^ Path element of an index into an
-- array, \"array[index]\".
| Embedded
-- ^ path of a embedded (JSON) String
deriving (Eq, Show, Ord, Generic, NFData)
-- | Error info with (JSON) Path info.
data ConvertError = ConvertError { errPath :: [PathElement], errMsg :: T.Text }
deriving (Eq, Ord, Generic, NFData)
instance Show ConvertError where
show = T.toString
instance T.Print ConvertError where
{-# INLINABLE toUTF8BuilderP #-}
toUTF8BuilderP _ (ConvertError [] msg) = T.toUTF8Builder msg
toUTF8BuilderP _ (ConvertError paths msg) = do
mapM_ renderPath (reverse paths)
T.char7 ':'
T.toUTF8Builder msg
where
renderPath (Index ix) = T.char7 '[' >> T.int ix >> T.char7 ']'
renderPath (Key k) = T.char7 '.' >> (JB.string k)
renderPath Embedded = "<Embedded>"
-- | 'Converter' provides a monadic interface to convert protocol IR (e.g.'Value') to Haskell ADT.
--
newtype Converter a = Converter { runConverter :: forall r. ([PathElement] -> T.Text -> r) -> (a -> r) -> r }
instance Functor Converter where
fmap f m = Converter (\ kf k -> runConverter m kf (k . f))
{-# INLINE fmap #-}
instance Applicative Converter where
pure a = Converter (\ _ k -> k a)
{-# INLINE pure #-}
(Converter f) <*> (Converter g) = Converter (\ kf k ->
f kf (\ f' -> g kf (k . f')))
{-# INLINE (<*>) #-}
instance Alternative Converter where
{-# INLINE (<|>) #-}
(Converter f) <|> (Converter g) = Converter (\ kf k -> f (\ _ _ -> g kf k) k)
{-# INLINE empty #-}
empty = fail' "Z.Data.JSON.Converter(Alternative).empty"
instance MonadPlus Converter where
mzero = empty
{-# INLINE mzero #-}
mplus = (<|>)
{-# INLINE mplus #-}
instance Monad Converter where
(Converter f) >>= g = Converter (\ kf k ->
f kf (\ a -> runConverter (g a) kf k))
{-# INLINE (>>=) #-}
return = pure
{-# INLINE return #-}
instance Fail.MonadFail Converter where
{-# INLINE fail #-}
fail = fail' . T.pack
-- | 'T.Text' version of 'fail'.
fail' :: T.Text -> Converter a
{-# INLINE fail' #-}
fail' msg = Converter (\ kf _ -> kf [] msg)
-- | Add (JSON) Path context to a converter
--
-- When converting a complex structure, it helps to annotate (sub)converters
-- with context, so that if an error occurs, you can find its location.
--
-- > withFlatMapR "Person" $ \o ->
-- > Person
-- > <$> o .: "name" <?> Key "name"
-- > <*> o .: "age" <?> Key "age"
--
-- (Standard methods like '(.:)' already do this.)
--
-- With such annotations, if an error occurs, you will get a (JSON) Path location of that error.
(<?>) :: Converter a -> PathElement -> Converter a
{-# INLINE (<?>) #-}
(Converter p) <?> path = Converter (\ kf k -> p (kf . (path:)) k)
infixl 9 <?>
-- | Add context to a failure message, indicating the name of the structure
-- being converted.
--
-- > prependContext "MyType" (fail "[error message]")
-- > -- Error: "converting MyType failed, [error message]"
prependContext :: T.Text -> Converter a -> Converter a
{-# INLINE prependContext #-}
prependContext name (Converter p) = Converter (\ kf k ->
p (\ paths msg -> kf paths (T.concat ["converting ", name, " failed, ", msg])) k)