packages feed

project-m36-1.1.0: examples/Plantfarm.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingVia #-}
-- The calls to 'S.raise' from the @scotty@ package are /deprecated/
-- TODO:
-- Replace 'raise' with 'throw'
--
-- Until then, supproess the deprecation warning
{-# OPTIONS_GHC -fno-warn-deprecations #-}
module Main
  ( main
    -- * Other example exports
  , closeConn
  , getAllPlantsWith
  , update
  , updatePlant
  ) where

import Codec.Winery (Serialise, WineryVariant(WineryVariant))
import Control.DeepSeq (NFData)
import Data.Aeson (FromJSON, ToJSON(toEncoding, toJSON))
import Data.Data (Proxy(Proxy))
import Data.Either (lefts, rights)
import Data.Functor (($>))
import Data.Text as T (Text)
import qualified Data.Text.Lazy as TL (pack)
import GHC.Generics (Generic)
import qualified ProjectM36.Base as Base
import ProjectM36.DatabaseContext
import ProjectM36.Client
       ( AtomExprBase(NakedAtomExpr)
       , Atomable(toAddTypeExpr, toAtom)
       , AttributeName
       , Connection
       , ConnectionInfo(InProcessConnectionInfo)
       , DatabaseContextExpr
       , DatabaseContextExprBase(Delete)
       , PersistenceStrategy(NoPersistence)
       , RelationalError
       , RelationalExprBase(RelationVariable, Restrict)
       , RestrictionPredicateExprBase(AttributeEqualityPredicate)
       , SessionId
       , close
       , commit
       , connectProjectM36
       , createSessionAtHead
       , databaseContextExprForUniqueKey
       , defaultHeadName
       , emptyNotificationCallback
       , executeDatabaseContextExpr
       , executeRelationalExpr
       , withTransaction
       )
import qualified ProjectM36.Relation as Relation
import ProjectM36.Tupleable
       ( Tupleable(fromTuple)
       , toDefineExpr
       , toDeleteExpr
       , toInsertExpr
       , toUpdateExpr
       )
import qualified System.Random as R


import Control.Monad.IO.Class (MonadIO(liftIO))
import qualified Web.Scotty as S

-- |    This is an example showcasing the updating of data
--      with the help of plants. \\
--      This is a very simple example and of course not how
--      plants grow in nature. \\
--      The main goal of this example is to showcase the updating
--      of data and the different options of persistence.
--      You can just pipe the output from curl into jq:
--
--      curl -X POST -H 'Accept: application/json' http://localhost:8001/plant/water/angu | jq -r '.stage'


data Stage = Seed | Sprout | Seedling | Adult | Dead
    deriving (Show, Eq, Ord, Generic, NFData, Atomable)
    deriving Serialise via WineryVariant Stage


instance ToJSON Stage where --  The where can be removed and the implementation commented out
                            --  if the ascii values are not wanted
  toJSON s = toJSON $ show s <> "\nascii only provided for visualisation:\n" <> stagetoAscii s

  toEncoding s = toEncoding $ show s <> "\nascii only provided for visualisation:\n" <> stagetoAscii s

instance FromJSON Stage

instance S.Parsable Stage where
  parseParam t
    | t == "seed" || t == "Seed" = Right Seed
    | t == "sprout" || t == "Sprout" = Right Sprout
    | t == "seedling" || t == "Seedling" = Right Seedling
    | t == "adult" || t == "Adult" = Right Adult
    | t == "dead" || t == "Dead" = Right Dead
    | otherwise = Left t


-- Just for ToJSON for illustration of stage
type ASCIIStage = String

seedStr :: ASCIIStage
seedStr = "<>\n\
          \'''\n"
sproutStr :: ASCIIStage
sproutStr = " ..\n\
            \<|>\n\
            \''''\n"
seedlingStr :: ASCIIStage
seedlingStr = " `\\⁄o \n\
              \ v|/\n\
              \'´;'''\n"
adultStr :: ASCIIStage
adultStr = "*~(#)~\n\
           \ \\,Y,e\n\
           \  \\|/\n\
           \''+'\\''\n"
deadStr :: ASCIIStage
deadStr = "  ,__:<\n\
          \'/'''''\n"

-- Just used for ToJSON illustration of stages
stagetoAscii :: Stage -> ASCIIStage
stagetoAscii Seed = seedStr
stagetoAscii Sprout = sproutStr
stagetoAscii Seedling = seedlingStr
stagetoAscii Adult = adultStr
stagetoAscii Dead = deadStr

-- used for update stage
next :: Stage -> Stage
next Seed     = Sprout
next Sprout   = Seedling
next Seedling = Adult
next s        = s

data Plant = Plant { name :: Text, species :: Text, stage :: Stage, waterings:: Integer } deriving (Show, Generic)
instance Tupleable Plant
instance ToJSON Plant
instance FromJSON Plant


main :: IO ()
main = do
  putStrLn "Connecting to plant farm"
  c <- dbConnection
  _ <- createSchema c
  putStrLn "Planting some plants"
  _ <- insertSampleData c
  let port = 8001
  putStrLn $ "Started Plant farm at " <> show port
  S.scotty port $ do

    --  retrieve a plant by name
    S.get "/plant/:name" $ do
        n <- S.pathParam "name"
        e <- liftIO $ getPlant c n
        p <- handleWebError e
        S.json p


    --  save a plant providing it as json data
    S.post "/plant" $ do
        pl <- S.jsonData :: S.ActionM Plant
        e <- liftIO $ savePlant c [pl]
        p <- handleWebError e
        S.json p

    --  updating a plant providing it as json data
    S.put "/plant" $ do
        pl <- S.jsonData :: S.ActionM Plant
        e <- liftIO $ updatePlantFst c pl
        p <- handleWebError e
        S.json p

    --  watering the plant having the provided name.
    --  This will water the plant and might let it progress to the next stage. It might also die.
    S.post "/plant/water/:name" $ do
      n <- S.pathParam "name"
      e <- liftIO $ waterPlant c n
      p <- handleWebError e
      S.json p

    --  retrieving all the plants as json data
    S.get "/plants" $ do
      e <- liftIO $ getAllPlants c
      ps <- handleWebErrors e
      S.json ps

    --  saving many plants at the same time
    S.post "/plants" $ do
        pl <- S.jsonData :: S.ActionM [Plant]
        e <- liftIO $ savePlant c pl
        p <- handleWebError e
        S.json p

    --  deleting all plants at a specific stage
    S.delete "/plants?stage=:stage" $ do
      s <- S.pathParam "stage"
      e <- liftIO $ deletePlantsByStage c s
      p <- handleWebError e
      S.json p

    --  deleting all plants at a specific stage
    S.delete "/plants" $ do
      s <- S.pathParam "name"
      e <- liftIO $ deletePlantByName c s
      p <- handleWebError e
      S.json p

    --  deleting all dead plants
    S.delete "/plants/clear" $ do
      e <- liftIO $ clearDeadPlants c
      ps <- handleWebErrors e
      S.json ps

handleWebError :: Either Err b -> S.ActionM b
handleWebError (Left e) = S.raise . TL.pack $ "An error occurred:\n" <> show e
handleWebError (Right v) = pure v

handleWebErrors :: [Either Err b] -> S.ActionM [b]
handleWebErrors e = do
  case lefts e of
    [] -> pure (rights e)
    l -> S.raise . TL.pack $ "Errors occurred:\n" <> concatMap ((<> "\n") . show) l


-- |    watering a plant and thereby possibly updating its stage
waterPlant :: DBConnection -> Text -> IO (Either Err Plant)
waterPlant db n = do
  p <- getPlant db n
  case p of
        Left e  -> pure $ Left e
        Right v -> updateStage db $ v { waterings = waterings v + 1 }

-- |    used by 'waterPlant' updates the stage depending on random numbers
updateStage :: DBConnection -> Plant ->  IO (Either Err Plant)
updateStage db p@(Plant _ _ Dead _ ) = do
  tmp <- updatePlantFst db p
  case tmp of
    Left e  -> pure $ Left e
    Right _ -> pure $ Right p
updateStage db p = do
  res <- calculateStage p
  tmp <- updatePlantFst db res
  case tmp of
    Left e  -> pure $ Left e
    Right _ -> pure $ Right res
  where calculateStage p' = do
          r1 <- R.randomRIO (1, 10) :: IO Integer
          let np = if r1 < waterings p' then p' { stage = next $ stage p', waterings = 0 } else p'
          r2 <- R.randomRIO (1, 20)
          pure $ if r2 < waterings np then np { stage = Dead, waterings = 0 } else np

-- |    deletes all plants with Stage = Dead
clearDeadPlants :: DBConnection -> IO [Either Err Plant]
clearDeadPlants db = do
  _ <- deletePlantsByStage db Dead
  getAllPlants db


-- ****************************
-- |    *Database functions* :
-- ****************************


-- |    Error type for passing. It is not very specific.
--      Just minimal and non optimal for this example.
data Err = NotSpecified | NotFound deriving (Show ,Generic)

instance ToJSON Err

-- |    Just for convenience for passing around the SessionId
--      and the Connection
data DBConnection = DB SessionId Connection


-- |    Getting plants by their name.\
--      Because plant has name as primary key \
--      we can assume will get at most one result. \
--      Alternatively we could return a list.
getPlant :: DBConnection -> Text -> IO (Either Err Plant)
getPlant db n =  defaultHead (Left NotFound)  <$> (
    get db
    ( Restrict -- Restrict can be thought of as similar to "WITH" in SQL
      (AttributeEqualityPredicate "name" (NakedAtomExpr (toAtom n))) -- This is the Predicate we are "restricting" our RelationalExprBase with
      (RelationVariable "plants" ())
    )
    :: IO [Either Err Plant] -- this is needed because our 'get' function is
  )

defaultHead :: a -> [a] -> a
defaultHead d []    = d
defaultHead _ (x:_) = x

-- |    Getting all the plants
getAllPlants :: DBConnection -> IO [Either Err Plant]
getAllPlants db = get db (RelationVariable "plants" ())

-- |    Getting all the plants that satisfy the restrictions
getAllPlantsWith :: DBConnection -> Base.RestrictionPredicateExpr -> IO [Either Err Plant]
getAllPlantsWith db ex = get db (Restrict ex (RelationVariable "plants" ()))


-- |    Saving a plant
savePlant :: (Traversable t) => DBConnection -> t Plant -> IO (Either Err ())
savePlant db sps = insert db sps "plants"

-- |    An alternative to update alone because due to updates
--      not being cached yet \
--      they have to be calculated every update. \
--      This seems like an alternative until caching is implemented.
updatePlantFst :: DBConnection -> Plant -> IO (Either Err ())
updatePlantFst db pln = do
  _ <- delete db pln ["name"] "plants"
  savePlant db [pln]

-- |    Updating a plant. Due to the way project 36 works, \
--      updates have to be executed every time.\
--      This is slow but won't be an issue soon because they will
--      be cached.\
--      But because of this project 36 can provide O(1) commits.
updatePlant :: DBConnection -> Plant -> IO (Either Err ())
updatePlant db sps = update
  db          -- db connection
  sps         -- plant value
  ["name"]   -- the attributes to be updated by (in case of unique key)
  "plants"    -- the relation we want to update on


-- |    deleting all plants that have given name.
deletePlantByName :: DBConnection -> Text -> IO (Either Err ())
deletePlantByName db s = executeWithTransaction db $ Right $ Delete "plants" $ AttributeEqualityPredicate "name" (NakedAtomExpr (toAtom s))

-- |    deleting all plants that have given stage.
deletePlantsByStage :: DBConnection -> Stage -> IO (Either Err ())
deletePlantsByStage db s = executeWithTransaction db $ Right $ Delete "plants" $ AttributeEqualityPredicate "stage" (NakedAtomExpr (toAtom s))


-- |    Inserting the schema into the DB
createSchema :: DBConnection -> IO ()
createSchema (DB sessionId conn) = do
  _ <- handleIOErrorsAndQuit $ mapM (executeDatabaseContextExpr sessionId conn) [
        toAddTypeExpr (Proxy :: Proxy Stage) -- Adds the Type Stage as data to the DB
    ,   toDefineExpr (Proxy :: Proxy Plant) "plants" -- Creates the plants relation
    ,   databaseContextExprForUniqueKey "plants" ["name"] -- Makes name of the plants relation a unique Key,
                                                          -- Foreign Key restrictions are available too
    ]
  pure ()


-- |    inserting some sample data
insertSampleData :: DBConnection -> IO (Either Err ())
insertSampleData (DB sid conn) = do
    insert (DB sid conn) [
        Plant "angu" "Caladenia angustata" Seed 0
      , Plant "thely" "Thelymitra alcockiae" Seed 0
      , Plant "monstera" "Araceae" Seed 0
      ] "plants"

dbConnection :: IO DBConnection
dbConnection = do
  --  connect to the database
  let connInfo = InProcessConnectionInfo NoPersistence emptyNotificationCallback [] basicDatabaseContext
  --  The code below persists the data in a DB with the name "base". \\
--  let connInfo = InProcessConnectionInfo (CrashSafePersistence "base") emptyNotificationCallback [] \\
  --  In addition minimal persistence is available. \\
--  let connInfo = InProcessConnectionInfo (MinimalPersistence "base") emptyNotificationCallback []
  conn <- handleIOErrorAndQuit $ connectProjectM36 connInfo
  --create a database session at the default branch of the database
  sessionId <- handleIOErrorAndQuit $ createSessionAtHead conn defaultHeadName
  pure (DB sessionId conn)


-- |    A polymorphic function to insert data (a traversable of data) into the DB
insert :: (Tupleable a, Traversable t) => DBConnection -> t a -> Base.RelVarName -> IO (Either Err ())
insert db rlv rlvName = executeWithTransaction db $ toInsertExpr rlv rlvName

-- |    A polymorphic function to update data in the DB.
--      An update in one function would take:
--
--      - SessionId
--      - Connection
--      - Tuplable a        -- the Tuplable you want to update
--      - [AttributeName]   -- the Attributes of the Tuplable you want to update those are of StringType
--      - RelVarName        -- the name of the relation
--
--      With that you can via 'toUpdateExpr' create a 'DatabaseContextExpr' to be executed. \\
--      For inserting its very similarly done with 'toInsertExpr'.
update :: (Tupleable a) => DBConnection -> a -> [AttributeName]-> Base.RelVarName -> IO (Either Err ())
update db rlv attr rlvName = executeWithTransaction db $ toUpdateExpr rlvName attr rlv

-- |    A polymorphic function to delete data in the DB
delete :: (Tupleable a) => DBConnection -> a -> [AttributeName]-> Base.RelVarName -> IO (Either Err ())
delete db rlv attr rlvName = executeWithTransaction db $ toDeleteExpr rlvName attr rlv

-- |    A convenience function to make executing DBContextExpr with committing simpler. \\
--      In particular for expr that just insert, delete and update.
--      Therefore ultimately return Either _ ()
executeWithTransaction :: DBConnection -> Either RelationalError DatabaseContextExpr -> IO (Either Err ())
executeWithTransaction (DB sid conn) expr = do
    iEx <- handleError expr
    case iEx of
      Left e -> pure $ Left e
      Right v -> handleIOError $ withTransaction sid conn (executeDatabaseContextExpr sid conn v) (commit sid conn)

-- |    A polymorphic function to get data (a list of data and possibly errors) from the DB
get :: Tupleable b => DBConnection -> Base.RelationalExpr -> IO [Either Err b]
get (DB sessionId conn) q = do
  eRel <-  executeRelationalExpr sessionId conn q
  e <- handleError eRel
  case e of
    Left err -> pure [Left err]
    Right pRel -> do
      ws <- Relation.toList pRel
      mapM (handleError . fromTuple) ws

-- |    for closing the DB connection.
--      Not really needed when using in-memory DB
closeConn :: DBConnection -> IO ()
closeConn (DB _ conn) = close conn


-- |    Error handling is heavily inspired by 'blog.hs' the blog example \\
--      by (agentm)[https://github.com/agentm] (https://github.com/agentm/project-m36, commit: f8432522adaafeae7c32bc2b8b6cb09c00396fc6). \\
--      As stated there, your application should have proper error handling.
handleIOErrorAndQuit :: Show e => IO (Either e a) -> IO a
handleIOErrorAndQuit m = do
  v <- m
  handleErrorAndQuit v

handleError :: Show e => Either e a -> IO (Either Err a)
handleError eErr = case eErr of
  Left err -> print err $> Left NotSpecified
  Right v  -> pure $ Right v

handleIOError :: Show e => IO (Either e a) -> IO (Either Err a)
handleIOError m = do
  v <- m
  handleError v

handleErrorAndQuit :: Show e => Either e a -> IO a
handleErrorAndQuit eErr = case eErr of
  Left err -> print err >> error "Quit."
  Right v  -> pure v

handleIOErrorsAndQuit :: Show e => IO [Either e a] -> IO [a]
handleIOErrorsAndQuit m = do
  eErrs <- m
  case lefts eErrs of
    []    -> pure (rights eErrs)
    err:_ -> handleErrorAndQuit (Left err)