data-object-0.2.0: Data/Object/Base.hs
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE TemplateHaskell #-}
---------------------------------------------------------
--
-- Module : Data.Object.Base
-- Copyright : Michael Snoyman
-- License : BSD3
--
-- Maintainer : Michael Snoyman <michael@snoyman.com>
-- Stability : Stable
-- Portability : portable
--
-- These objects show up in different places, eg JSON, Yaml.
-- By providing a representation in a separate repository,
-- other libraries can share a single representation of
-- these structures.
--
---------------------------------------------------------
-- | The core of this package is the 'Object' data type, which is used for
-- handling scalars, sequences and mappings in a nested manner. This
-- is the same structure used in JSON or Yaml data.
--
-- The 'Object' data type is polymorphic in its keys and values. Submodules
-- within this package provide more concrete datatypes, such as a 'String'
-- 'Object' and a specialized scalar type.
--
-- Besides the 'Object' data type, there are utility functions and type classes
-- for converting objects around. Care has been taken to avoid any overloaded
-- instances for these type classes.
module Data.Object.Base
( -- * Object data type
Object (..)
-- * Basic mapping of keys and values
, mapKeys
, mapValues
, mapKeysValues
, mapKeysValuesA
, mapKeysValuesM
-- * Convert entires objects
, convertObject
, convertObjectM
-- * Extracting underlying values
, ObjectExtractError (..)
, fromScalar
, fromSequence
, fromMapping
-- * Common object conversions
, sTO
, sFO
, lTO
, lFO
, mTO
, mFO
, olTO
, olFO
, omTO
, omFO
-- * Automatic deriving of instances
, deriveSuccessConvs
-- * Helper functions
, lookupObject
-- * Re-export
, module Data.Convertible.Text
) where
import Control.Arrow
import Control.Applicative
import Control.Monad (ap, (<=<))
import Prelude hiding (mapM, sequence)
import Data.Foldable hiding (concatMap, concat)
import Data.Traversable
import Data.Monoid
import Data.Generics
import qualified Safe.Failure as A
import Control.Exception (Exception)
import Data.Attempt
import Data.Convertible.Text
import Language.Haskell.TH
-- | Can represent nested values as scalars, sequences and mappings. A
-- sequence is synonymous with a list, while a mapping is synonymous with a
-- list of pairs.
--
-- Note that instances of standard library type classes for this data type
-- leave the key untouched while altering the value. For example, the 'Functor'
-- instance defines 'fmap' to be synonymous with 'mapValues'.
data Object key val =
Mapping [(key, Object key val)]
| Sequence [Object key val]
| Scalar val
deriving (Show, Eq, Data, Typeable)
instance Functor (Object key) where
fmap = mapValues
instance Foldable (Object key) where
foldMap f (Scalar v) = f v
foldMap f (Sequence vs) = mconcat $ map (foldMap f) vs
foldMap f (Mapping pairs) = mconcat $ map (foldMap f . snd) pairs
instance Traversable (Object key) where
traverse f (Scalar v) = Scalar <$> f v
traverse f (Sequence vs) = Sequence <$> traverse (traverse f) vs
traverse f (Mapping pairs) =
Mapping <$> traverse (traverse' (traverse f)) pairs
-- It would be nice if there were an "instance Traversable ((,) a)", but I
-- won't make an orphan instance simply for convenience. Instead:
traverse' :: Applicative f => (a -> f b) -> (x, a) -> f (x, b)
traverse' f (x, a) = (,) x <$> f a
joinObj :: Object key (Object key scalar) -> Object key scalar
joinObj (Scalar x) = x
joinObj (Sequence xs) = Sequence (map joinObj xs)
joinObj (Mapping xs) = Mapping (map (second joinObj) xs)
instance Monad (Object key) where
return = Scalar
x >>= f = joinObj . fmap f $ x
instance Applicative (Object key) where
pure = Scalar
(<*>) = ap
-- | Apply some conversion to the keys of an 'Object', leaving the values
-- unchanged.
mapKeys :: (keyIn -> keyOut) -> Object keyIn val -> Object keyOut val
mapKeys = flip mapKeysValues id
-- | Apply some conversion to the values of an 'Object', leaving the keys
-- unchanged. This is equivalent to 'fmap'.
mapValues :: (valIn -> valOut) -> Object key valIn -> Object key valOut
mapValues = mapKeysValues id
-- | Apply a conversion to both the keys and values of an 'Object'.
mapKeysValues :: (keyIn -> keyOut)
-> (valIn -> valOut)
-> Object keyIn valIn
-> Object keyOut valOut
mapKeysValues _ fv (Scalar v) = Scalar $ fv v
mapKeysValues fk fv (Sequence os)= Sequence $ map (mapKeysValues fk fv) os
mapKeysValues fk fv (Mapping pairs) =
Mapping $ map (fk *** mapKeysValues fk fv) pairs
-- | Apply an 'Applicative' conversion to both the keys and values of an
-- 'Object'.
mapKeysValuesA :: Applicative f
=> (keyIn -> f keyOut)
-> (valIn -> f valOut)
-> Object keyIn valIn
-> f (Object keyOut valOut)
mapKeysValuesA _ fv (Scalar v) = Scalar <$> fv v
mapKeysValuesA fk fv (Sequence os) =
Sequence <$> traverse (mapKeysValuesA fk fv) os
mapKeysValuesA fk fv (Mapping pairs) = Mapping <$>
traverse (uncurry (liftA2 (,)) . (fk *** mapKeysValuesA fk fv)) pairs
-- | The same as 'mapKeysValuesA', but using a 'Monad' since some people are
-- more comfortable with 'Monad's and not all 'Monad's are 'Applicative'.
mapKeysValuesM :: Monad m
=> (keyIn -> m keyOut)
-> (valIn -> m valOut)
-> Object keyIn valIn
-> m (Object keyOut valOut)
mapKeysValuesM fk fv =
let fk' = WrapMonad . fk
fv' = WrapMonad . fv
in unwrapMonad . mapKeysValuesA fk' fv'
convertObject :: (ConvertSuccess k k', ConvertSuccess v v')
=> Object k v
-> Object k' v'
convertObject = mapKeysValues cs cs
convertObjectM :: (ConvertAttempt k k', ConvertAttempt v v')
=> Object k v
-> Attempt (Object k' v')
convertObjectM = mapKeysValuesM ca ca
-- | An error value returned when an unexpected node is encountered, eg you
-- were expecting a 'Scalar' and found a 'Mapping'.
data ObjectExtractError =
ExpectedScalar
| ExpectedSequence
| ExpectedMapping
deriving (Typeable, Show)
instance Exception ObjectExtractError
-- | Extra a scalar from the input, failing if the input is a sequence or
-- mapping.
fromScalar :: MonadFailure ObjectExtractError m => Object k v -> m v
fromScalar (Scalar s) = return s
fromScalar _ = failure ExpectedScalar
-- | Extra a sequence from the input, failing if the input is a scalar or
-- mapping.
fromSequence :: MonadFailure ObjectExtractError m
=> Object k v
-> m [Object k v]
fromSequence (Sequence s) = return s
fromSequence _ = failure ExpectedSequence
-- | Extra a mapping from the input, failing if the input is a scalar or
-- sequence.
fromMapping :: MonadFailure ObjectExtractError m
=> Object k v
-> m [(k, Object k v)]
fromMapping (Mapping m) = return m
fromMapping _ = failure ExpectedMapping
sTO :: ConvertSuccess v v' => v -> Object k v'
sTO = Scalar . cs
sFO :: ConvertAttempt v' v => Object k v' -> Attempt v
sFO = ca <=< fromScalar
lTO :: ConvertSuccess v v' => [v] -> Object k v'
lTO = Sequence . map (Scalar . cs)
lFO :: ConvertAttempt v' v => Object k v' -> Attempt [v]
lFO = mapM (ca <=< fromScalar) <=< fromSequence
mTO :: (ConvertSuccess k k', ConvertSuccess v v')
=> [(k, v)]
-> Object k' v'
mTO = Mapping . map (cs *** Scalar . cs)
mFO :: (ConvertAttempt k' k, ConvertAttempt v' v)
=> Object k' v'
-> Attempt [(k, v)]
mFO =
mapM (runKleisli (Kleisli ca *** Kleisli sFO))
<=< fromMapping
olTO :: ConvertSuccess x (Object k v) => [x] -> Object k v
olTO = Sequence . map cs
olFO :: ConvertAttempt (Object k v) x => Object k v -> Attempt [x]
olFO = mapM ca <=< fromSequence
omTO :: (ConvertSuccess k' k, ConvertSuccess x (Object k v))
=> [(k', x)]
-> Object k v
omTO = Mapping . map (cs *** cs)
omFO :: (ConvertAttempt k k', ConvertAttempt (Object k v) x)
=> Object k v
-> Attempt [(k', x)]
omFO = mapM (runKleisli (Kleisli ca *** Kleisli ca)) <=< fromMapping
deriveSuccessConvs :: Name -- ^ dest key
-> Name -- ^ dest value
-> [Name] -- ^ source keys
-> [Name] -- ^ source values
-> Q [Dec]
deriveSuccessConvs dk dv sks svs = do
sto <- [|sTO|]
sfo <- [|sFO|]
lto <- [|lTO|]
lfo <- [|lFO|]
mto <- [|mTO|]
mfo <- [|mFO|]
olto <- [|olTO|]
olfo <- [|olFO|]
omto <- [|omTO|]
omfo <- [|omFO|]
co <- [|convertObject|]
coa <- [|convertObjectM|]
let sks' = map ConT sks
svs' = map ConT svs
pairs = do
sk <- sks'
sv <- svs'
return (sk, sv)
let valOnly = concatMap (helper1 sto sfo lto lfo) svs'
both = concatMap (helper2 mto mfo olto olfo co coa omto omfo) pairs
keyOnly = concatMap (helper3 omto omfo) sks'
return $ valOnly ++ both ++ keyOnly
where
dk' = ConT dk
dv' = ConT dv
objectt k v = ConT (mkName "Object") `AppT` k `AppT` v
to' src = ConT (mkName "ConvertSuccess") `AppT` src `AppT`
objectt dk' dv'
fo' dst = ConT (mkName "ConvertAttempt") `AppT`
objectt dk' dv' `AppT` dst
cs' = mkName "convertSuccess"
ca' = mkName "convertAttempt"
to src f =
InstanceD [] (to' src) [FunD cs' [Clause [] (NormalB f) []]]
fo dst f =
InstanceD [] (fo' dst) [FunD ca' [Clause [] (NormalB f) []]]
tofo ty x y = [to ty x, fo ty y]
listt = AppT ListT
pairt k v = TupleT 2 `AppT` k `AppT` v
helper1 sto sfo lto lfo sv = concat
[ tofo sv sto sfo
, tofo (listt sv) lto lfo
]
helper2 mto mfo olto olfo co coa omto omfo (sk, sv) = concat
[ tofo (listt $ pairt sk sv) mto mfo
, tofo (listt $ objectt sk sv) olto olfo
, if sk == dk' && sv == dv' -- avoid overlapping with identity
then []
else tofo (objectt sk sv) co coa
, if sk == dk' && sv == dv' -- avoid overlapping with helper3
then []
else tofo (listt $ pairt sk $ objectt sk sv) omto omfo
]
helper3 omto omfo sk = concat
[ tofo (listt $ pairt sk $ objectt dk' dv') omto omfo
]
-- | An equivalent of 'lookup' to deal specifically with maps of 'Object's. In
-- particular, it will:
--
-- 1. Automatically convert the lookup key as necesary. For example- assuming
-- you have the appropriate 'ConvertSuccess' instances, you could lookup an 'Int' in
-- a map that has 'String' keys.
--
-- 2. Return the result in an 'Attempt', not 'Maybe'. This is especially useful
-- when creating 'FromObject' instances.
--
-- 3. Show a more useful error message. Since this function requires the key to
-- be 'Show'able, the fail message states what key was not found.
--
-- 4. Calls 'fromObject' automatically, so you get out the value type that you
-- want, not just an 'Object'.
lookupObject :: ( ConvertSuccess k' k
, ConvertAttempt (Object k v) o
, Typeable k
, Typeable v
, Show k
, Eq k
)
=> k'
-> [(k, Object k v)]
-> Attempt o
lookupObject key = ca <=< A.lookup (convertSuccess key)