mealstrom-0.0.1.0: test/Upgrade.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-|
Module : Upgrade
Description : Test that upgrading the data model actually works
Copyright : (c) Max Amanshauser, 2016
License : MIT
Maintainer : max@lambdalifting.org
All this looks a bit goofy because you cannot have duplicate
data constructors and I did not want to make separate modules.
In practice it's much less confusing.
-}
module Upgrade (runUpgradeTests) where
import Test.Tasty
import Test.Tasty.HUnit
import Control.Concurrent
import Control.Monad (guard)
import Data.Aeson
import Data.Aeson.Types
import Data.Hashable
import Data.Maybe (isNothing)
import qualified Data.Text as Text
import Data.Text (Text)
import Data.Typeable (Typeable)
import Data.UUID
import Data.UUID.V4
import GHC.Generics
import Mealstrom
import Mealstrom.FSMStore
import Mealstrom.WALStore
import Mealstrom.PostgresJSONStore as PGJSON
-- |First FSM Instance
type FirstKey = UUID
data FirstState = OldState1 deriving (Eq,Show,Typeable)
instance ToJSON FirstState where
toJSON s = "OldState1"
instance FromJSON FirstState where
parseJSON = withText "expected OldState1" (\t -> case t of "OldState1" -> return OldState1)
data FirstEvent = OldEvent1 deriving (Eq,Show,Typeable)
instance ToJSON FirstEvent where
toJSON e = "OldEvent1"
instance FromJSON FirstEvent where
parseJSON = withText "expected OldEvent1" (\t -> case t of "OldEvent1" -> return OldEvent1)
data FirstAction = OldAction1 {
_a :: String,
_b :: Int
} deriving (Eq,Show,Typeable)
instance ToJSON FirstAction where
toJSON a = object [
"a" .= _a a,
"b" .= _b a
]
instance FromJSON FirstAction where
parseJSON = withObject "expected OldAction1" $ \o -> do
_a <- o .: "a"
_b <- o .: "b"
return $ OldAction1 _a _b
instance MealyInstance FirstKey FirstState FirstEvent FirstAction
firstTransition :: (FirstState,FirstEvent) -> (FirstState,[FirstAction])
firstTransition (OldState1,OldEvent1) =
(OldState1,[OldAction1 "lol" 42])
firstEffects :: Msg FirstAction -> IO Bool
firstEffects _a = do
putStrLn "Action completed"
return True
-- |Second FSM Instance
-- here we want to parse both values from the first Instance
-- as well as values from the second Instance
type SecondKey = UUID
-- The first upgrade is adding another data constructor to the State
data SecondState = NewState1 | NewState2 Int deriving (Eq,Show)
instance ToJSON SecondState where
toJSON NewState1 = "NewState1"
toJSON (NewState2 i) = object [
"name" .= String "NewState2",
"value" .= i
]
instance FromJSON SecondState where
parseJSON (Object o) = do
name <- o .: "name"
guard (name == String "NewState2")
val <- o .: "value"
return $ NewState2 val
-- |Conversion from old version
parseJSON (String t) =
case t of
"NewState1" -> return NewState1
"OldState1" -> return NewState1
-- The second upgrade is adding a parameter to the Event
data SecondEvent = NewEvent1 Int deriving (Eq,Show)
instance ToJSON SecondEvent where
toJSON (NewEvent1 i) = object [
"name" .= String "NewEvent1",
"value" .= i
]
instance FromJSON SecondEvent where
parseJSON (Object o) = do
name <- o .: "name"
guard(name == String "NewEvent1")
val <- o .: "value"
return $ NewEvent1 val
-- Conversion from old version
parseJSON (String t) = case t of
"OldEvent1" -> return $ NewEvent1 0
-- |Here we just add another field to the existing data type
data SecondAction = NewAction1 {
__a :: String,
__b :: Int,
__c :: Double
} deriving (Eq,Show,Typeable)
instance ToJSON SecondAction where
toJSON a = object [
"a" .= __a a,
"b" .= __b a,
"c" .= __c a
]
instance FromJSON SecondAction where
parseJSON = withObject "expected FirstAction" $ \o -> do
__a <- o .: "a"
__b <- o .: "b"
__c <- o .:? "c" .!= 0.0
return $ NewAction1 __a __b __c
instance MealyInstance SecondKey SecondState SecondEvent SecondAction
secondTransition :: (SecondState,SecondEvent) -> (SecondState,[SecondAction])
secondTransition (NewState1,NewEvent1 i) =
(NewState2 i,[NewAction1 "double lol" 666 1.0])
secondEffects :: Msg SecondAction -> IO Bool
secondEffects _a = do
putStrLn "Action completed"
return True
runUpgradeTests :: String -> TestTree
runUpgradeTests c = testGroup "UpgradeFSM" [
testCase "UpgradePG" (runTest (PGJSON.mkStore c))
]
runTest :: (Text -> IO PostgresJSONStore) -> IO ()
runTest c = do
-- Start by creating the first FSMHandle and adding some data
st <- c "UpgradeTest" :: IO PostgresJSONStore
let t1 = FSMTable firstTransition firstEffects
let myFSM1 = FSMHandle st st t1 90 3
firstId <- nextRandom
res1 <- post myFSM1 firstId OldState1
mkMsgs [OldEvent1] >>= patch myFSM1 firstId
sndId <- nextRandom
res2 <- post myFSM1 sndId OldState1
mkMsgs [OldEvent1] >>= patch myFSM1 sndId
-- Then, try to access the same FSM using another MealyInstance that uses upgraded data types
let t2 = FSMTable secondTransition secondEffects
let myFSM2 = FSMHandle st st t2 90 3
-- Conversion when accessing
res3 <- get myFSM2 firstId
res3 @?= Just NewState1
-- Conversion when saving
mkMsgs [NewEvent1 15] >>= patch myFSM2 firstId
res4 <- get myFSM2 firstId
res4 @?= Just (NewState2 15)
-- Batch conversion. Also examines already converted instances, but writes them back untouched.
PGJSON._batchConversion st "UpgradeTest" (Proxy :: Proxy SecondKey SecondState SecondEvent SecondAction)
res5 <- get myFSM2 sndId
res5 @?= Just NewState1