project-m36-0.9.4: src/lib/ProjectM36/Tupleable.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE CPP #-}
module ProjectM36.Tupleable
( toInsertExpr
, toDefineExpr
, tupleAssocsEqualityPredicate
, partitionByAttributes
, toUpdateExpr
, toDeleteExpr
, validateAttributes
, Tupleable(..)
-- * Generics
, genericToTuple
, genericFromTuple
, genericToAttributes
, TupleableG(..)
-- ** Options
, defaultTupleableOptions
, TupleableOptions()
, fieldModifier
) where
import Data.Foldable
import Data.List (partition)
import qualified Data.Map as Map
#if __GLASGOW_HASKELL__ < 804
import Data.Monoid
#endif
import Data.Proxy
import qualified Data.Text as T
import qualified Data.Vector as V
import GHC.Generics
import ProjectM36.Atomable
import ProjectM36.Attribute as A hiding (null, toList)
import ProjectM36.Base
import ProjectM36.DataTypes.Primitive
import ProjectM36.Error
import ProjectM36.Tuple
import ProjectM36.TupleSet
import qualified Data.Set as S
{-import Data.Binary
import Control.DeepSeq
data Test1T = Test1C {
attrA :: Int
}
deriving (Generic, Show)
data Test2T a b = Test2C {
attrB :: a,
attrC :: b
}
deriving (Generic, Show)
instance (Atomable a, Atomable b, Show a, Show b) => Tupleable (Test2T a b)
instance Tupleable Test1T
data TestUnnamed1 = TestUnnamed1 Int Double T.Text
deriving (Show,Eq, Generic)
instance Tupleable TestUnnamed1
data Test7A = Test7AC Integer
deriving (Generic, Show, Eq, Atomable, NFData, Binary)
data Test7T = Test7C Test7A
deriving (Generic, Show, Eq)
instance Tupleable Test7T
-}
-- | Convert a 'Traverseable' of 'Tupleable's to an 'Insert' 'DatabaseContextExpr'. This is useful for converting, for example, a list of data values to a set of Insert expressions which can be used to add the values to the database.
toInsertExpr :: forall a t. (Tupleable a, Traversable t) => t a -> RelVarName -> Either RelationalError DatabaseContextExpr
toInsertExpr vals rvName = do
let attrs = toAttributes (Proxy :: Proxy a)
tuples <- mkTupleSet attrs $ toList (fmap toTuple vals)
let rel = MakeStaticRelation attrs tuples
pure (Insert rvName rel)
-- | Convert a 'Tupleable' to a create a 'Define' expression which can be used to create an empty relation variable. Use 'toInsertExpr' to insert the actual tuple data. This function is typically used with 'Data.Proxy'.
toDefineExpr :: forall a proxy. Tupleable a => proxy a -> RelVarName -> DatabaseContextExpr
toDefineExpr _ rvName = Define rvName (map NakedAttributeExpr (V.toList (attributesVec attrs)))
where
attrs = toAttributes (Proxy :: Proxy a)
tupleAssocsEqualityPredicate :: [(AttributeName, Atom)] -> RestrictionPredicateExpr
tupleAssocsEqualityPredicate [] = TruePredicate
tupleAssocsEqualityPredicate pairs =
foldr1 AndPredicate $
map
(\(name, atom) -> AttributeEqualityPredicate name (NakedAtomExpr atom))
pairs
partitionByAttributes ::
Tupleable a
=> [AttributeName]
-> a
-> ([(AttributeName, Atom)], [(AttributeName, Atom)])
partitionByAttributes attrs =
partition ((`elem` attrs) . fst) . tupleAssocs . toTuple
-- | Convert a list of key attributes and a 'Tupleable' value to an 'Update'
-- expression. This expression flushes the non-key attributes of the value to
-- a tuple with the matching key attributes.
toUpdateExpr ::
forall a. Tupleable a => RelVarName -> [AttributeName] -> a -> Either RelationalError DatabaseContextExpr
toUpdateExpr rvName keyAttrs a = validateAttributes (S.fromList keyAttrs) expectedAttrSet (Update rvName updateMap keyRestriction)
where
(keyPairs, updatePairs) = partitionByAttributes keyAttrs a
updateMap = Map.fromList $ fmap NakedAtomExpr <$> updatePairs
keyRestriction = tupleAssocsEqualityPredicate keyPairs
expectedAttrSet = attributeNameSet (toAttributes (Proxy :: Proxy a))
-- | Convert a list of key attributes and a 'Tupleable' value to a 'Delete'
-- expression. This expression deletes tuples matching the key attributes from
-- the value.
toDeleteExpr ::
forall a. Tupleable a => RelVarName -> [AttributeName] -> a -> Either RelationalError DatabaseContextExpr
toDeleteExpr rvName keyAttrs val = validateAttributes (S.fromList keyAttrs) expectedAttrSet (Delete rvName keyRestriction)
where
keyPairs = fst $ partitionByAttributes keyAttrs val
keyRestriction = tupleAssocsEqualityPredicate keyPairs
expectedAttrSet = attributeNameSet (toAttributes (Proxy :: Proxy a))
validateAttributes :: S.Set AttributeName -> S.Set AttributeName -> a -> Either RelationalError a
validateAttributes actualAttrs expectedAttrs val
| S.null actualAttrs = Left EmptyAttributesError
| not (S.null nonMatchingAttrs) = Left (NoSuchAttributeNamesError nonMatchingAttrs)
| otherwise = Right val
where
nonMatchingAttrs = attributeNamesNotContained actualAttrs expectedAttrs
-- | Types that can be converted to and from 'RelationTuple'.
--
-- deriving without customization:
--
-- > data Example = Example
-- > { foo :: Integer
-- > , bar :: Text
-- > }
-- > deriving (Generic)
-- >
-- > instance Tupleable Example
--
-- deriving with customization using "ProjectM36.Tupleable.Deriving":
--
-- > data Example = Example
-- > { exampleFoo :: Integer
-- > , exampleBar :: Text
-- > }
-- > deriving stock (Generic)
-- > deriving (Tupleable)
-- > via Codec (Field (DropPrefix "example" >>> CamelCase)) Example
class Tupleable a where
toTuple :: a -> RelationTuple
fromTuple :: RelationTuple -> Either RelationalError a
toAttributes :: Proxy a -> Attributes
default toTuple :: (Generic a, TupleableG (Rep a)) => a -> RelationTuple
toTuple = genericToTuple defaultTupleableOptions
default fromTuple :: (Generic a, TupleableG (Rep a)) => RelationTuple -> Either RelationalError a
fromTuple = genericFromTuple defaultTupleableOptions
default toAttributes :: (Generic a, TupleableG (Rep a)) => Proxy a -> Attributes
toAttributes = genericToAttributes defaultTupleableOptions
-- | Options that influence deriving behavior.
newtype TupleableOptions = TupleableOptions {
-- | A function that translates record field names into attribute names.
fieldModifier :: T.Text -> T.Text
}
-- | The default options for deriving Tupleable instances.
--
-- These options can be customized by using record update syntax. For example,
--
-- > defaultTupleableOptions
-- > { fieldModifier = \fieldName ->
-- > case Data.Text.stripPrefix "example" fieldName of
-- > Nothing -> fieldName
-- > Just attributeName -> attributeName
-- > }
--
-- will result in record field names being translated into attribute names by
-- removing the prefix "example" from the field names.
defaultTupleableOptions :: TupleableOptions
defaultTupleableOptions = TupleableOptions {
fieldModifier = id
}
genericToTuple :: (Generic a, TupleableG (Rep a)) => TupleableOptions -> a -> RelationTuple
genericToTuple opts v = toTupleG opts (from v)
genericFromTuple :: (Generic a, TupleableG (Rep a)) => TupleableOptions -> RelationTuple -> Either RelationalError a
genericFromTuple opts tup = to <$> fromTupleG opts tup
genericToAttributes :: forall a. (Generic a, TupleableG (Rep a)) => TupleableOptions -> Proxy a -> Attributes
genericToAttributes opts _ = toAttributesG opts (from (undefined :: a))
class TupleableG g where
toTupleG :: TupleableOptions -> g a -> RelationTuple
toAttributesG :: TupleableOptions -> g a -> Attributes
fromTupleG :: TupleableOptions -> RelationTuple -> Either RelationalError (g a)
isRecordTypeG :: g a -> Bool
--data type metadata
instance (Datatype c, TupleableG a) => TupleableG (M1 D c a) where
toTupleG opts (M1 v) = toTupleG opts v
toAttributesG opts (M1 v) = toAttributesG opts v
fromTupleG opts v = M1 <$> fromTupleG opts v
isRecordTypeG (M1 v) = isRecordTypeG v
--constructor metadata
instance (Constructor c, TupleableG a, AtomableG a) => TupleableG (M1 C c a) where
toTupleG opts (M1 v) = RelationTuple attrs atoms
where
attrsToCheck = toAttributesG opts v
counter = V.generate (arity attrsToCheck) id
attrs = attributesFromList (V.toList attrsV)
attrsV = V.zipWith (\num attr@(Attribute name typ) -> if T.null name then
Attribute ("attr" <> T.pack (show (num + 1))) typ
else
attr) counter (attributesVec attrsToCheck)
atoms = V.fromList (toAtomsG v)
toAttributesG opts (M1 v) = toAttributesG opts v
fromTupleG opts tup = M1 <$> fromTupleG opts tup
isRecordTypeG (M1 v) = isRecordTypeG v
-- product types
instance (TupleableG a, TupleableG b) => TupleableG (a :*: b) where
toTupleG = error "toTupleG"
toAttributesG opts ~(x :*: y) = toAttributesG opts x <> toAttributesG opts y --a bit of extra laziness prevents whnf so that we can use toAttributes (undefined :: Test2T Int Int) without throwing an exception
fromTupleG opts tup = (:*:) <$> fromTupleG opts tup <*> fromTupleG opts processedTuple
where
processedTuple = if isRecordTypeG (undefined :: a x) then
tup
else
trimTuple 1 tup
isRecordTypeG ~(x :*: y) = isRecordTypeG x || isRecordTypeG y
--selector/record
instance (Selector c, AtomableG a) => TupleableG (M1 S c a) where
toTupleG = error "toTupleG"
toAttributesG opts m@(M1 v) = A.singleton (Attribute modifiedName aType)
where
name = T.pack (selName m)
modifiedName = if T.null name then
name
else
fieldModifier opts name
aType = toAtomTypeG v
fromTupleG opts tup = if null name then -- non-record type, just pull off the first tuple item
M1 <$> atomv (V.head (tupleAtoms tup))
else do
atom <- atomForAttributeName (fieldModifier opts (T.pack name)) tup
val <- atomv atom
pure (M1 val)
where
expectedAtomType = atomType (V.head (attributesVec (toAttributesG opts (undefined :: M1 S c a x))))
atomv atom = maybe (Left (AtomTypeMismatchError
expectedAtomType
(atomTypeForAtom atom)
)) Right (fromAtomG atom [atom])
name = selName (undefined :: M1 S c a x)
isRecordTypeG _ = not (null (selName (undefined :: M1 S c a x)))
--constructors with no arguments
--basically useless but orthoganal to relationTrue
instance TupleableG U1 where
toTupleG _ _ = emptyTuple
toAttributesG _ _ = emptyAttributes
fromTupleG _ _ = pure U1
isRecordTypeG _ = False