packages feed

Shpadoinkle-examples-0.0.0.1: servant-crud/Types.hs

{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveAnyClass             #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE DuplicateRecordFields      #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE FunctionalDependencies     #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE StandaloneDeriving         #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeApplications           #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE TypeOperators              #-}
{-# LANGUAGE UndecidableInstances       #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}


module Types (module Types, module Types.Prim) where


import           Control.Lens                      as Lens hiding (Context)
import           Control.Lens.TH                   ()
import           Control.Monad.Except
import           Data.Aeson
import           Data.Function
import           Data.Maybe
import           Data.Proxy
import           Data.Text
import           Database.Beam

import           Servant.API                       hiding (Description)
import           Shpadoinkle
import qualified Shpadoinkle.Html                  as H
import           Shpadoinkle.Router
import           Shpadoinkle.Widgets.Form.Dropdown as Dropdown
import           Shpadoinkle.Widgets.Table         as Table
import           Shpadoinkle.Widgets.Types
import           Shpadoinkle.Widgets.Validation

import           Types.Prim


data SpaceCraftT f = SpaceCraft
  { _identity    :: Columnar f SpaceCraftId
  , _sku         :: Columnar f SKU
  , _description :: Columnar (Nullable f) Description
  , _serial      :: Columnar f SerialNumber
  , _squadron    :: Columnar f Squadron
  , _operable    :: Columnar f Operable
  } deriving (Generic, Beamable)


instance Table SpaceCraftT where
  newtype PrimaryKey SpaceCraftT f = SpaceCraftKey (Columnar f SpaceCraftId) deriving (Generic) deriving anyclass (Beamable)
  primaryKey = SpaceCraftKey . _identity


type SpaceCraft = SpaceCraftT Identity
deriving instance Eq SpaceCraft
deriving instance Ord SpaceCraft
deriving instance Show SpaceCraft
deriving instance ToJSON SpaceCraft
deriving instance FromJSON SpaceCraft


makeFieldsNoPrefix ''SpaceCraftT


newtype DB f = DB { _roster :: f (TableEntity SpaceCraftT) } deriving (Generic) deriving anyclass (Database be)


db :: DatabaseSettings be DB
db = defaultDbSettings


data SpaceCraftUpdate s = SpaceCraftUpdate
  { _sku         :: Field s Text Input SKU
  , _description :: Field s Text Input (Maybe Description)
  , _serial      :: Field s Text Input SerialNumber
  , _squadron    :: Field s Text (Dropdown 'One) Squadron
  , _operable    :: Field s Text (Dropdown 'AtleastOne) Operable
  } deriving Generic


deriving instance Eq       (SpaceCraftUpdate 'Valid)
deriving instance Ord      (SpaceCraftUpdate 'Valid)
deriving instance Show     (SpaceCraftUpdate 'Valid)
deriving instance ToJSON   (SpaceCraftUpdate 'Valid)
deriving instance FromJSON (SpaceCraftUpdate 'Valid)

deriving instance Eq       (SpaceCraftUpdate 'Edit)
deriving instance Ord      (SpaceCraftUpdate 'Edit)
deriving instance Show     (SpaceCraftUpdate 'Edit)
deriving instance ToJSON   (SpaceCraftUpdate 'Edit)
deriving instance FromJSON (SpaceCraftUpdate 'Edit)

deriving instance Show     (SpaceCraftUpdate 'Errors)


instance Validate SpaceCraftUpdate where
  rules = SpaceCraftUpdate
    { _sku         = positive <> nonZero
    , _description = nonMEmpty
    , _serial      = between (30, maxBound)
    , _squadron    = maybe (throwError "Cannot be empty") pure
    , _operable    = pure
    }


makeFieldsNoPrefix ''SpaceCraftUpdate


data Roster = Roster
  { _sort   :: SortCol [SpaceCraft]
  , _search :: Input Search
  , _table  :: [SpaceCraft]
  }


deriving instance Eq      Roster
deriving instance Ord     Roster
deriving instance Show    Roster
deriving instance Generic Roster
instance (ToJSON   (Table.Column [SpaceCraft])) => ToJSON   Roster
instance (FromJSON (Table.Column [SpaceCraft])) => FromJSON Roster


makeFieldsNoPrefix ''Roster


emptyEditForm :: SpaceCraftUpdate 'Edit
emptyEditForm = SpaceCraftUpdate
  { _sku         = Input Clean 0
  , _description = Input Clean Nothing
  , _serial      = Input Clean 0
  , _squadron    = fullOptions
  , _operable    = fullOptionsMin
  }


data Frontend
  = MEcho (Maybe Text)
  | MList Roster
  | MDetail (Maybe SpaceCraftId) (SpaceCraftUpdate 'Edit)
  | M404
  deriving (Eq, Ord, Show, Generic)


instance (ToJSON   (Column [SpaceCraft])) => ToJSON   Frontend
instance (FromJSON (Column [SpaceCraft])) => FromJSON Frontend


makeLenses ''Frontend


data Route
  = REcho (Maybe Text)
  | RList (Input Search)
  | RNew
  | RExisting SpaceCraftId
  deriving (Eq, Ord, Show, Generic)


makeLenses ''Route


type API = "api" :> "space-craft" :> Get '[JSON] [SpaceCraft]
      :<|> "api" :> "space-craft" :> Capture "id" SpaceCraftId :> Get '[JSON] (Maybe SpaceCraft)
      :<|> "api" :> "space-craft" :> Capture "id" SpaceCraftId :> ReqBody '[JSON] (SpaceCraftUpdate 'Valid) :> Post '[JSON] ()
      :<|> "api" :> "space-craft" :> ReqBody '[JSON] (SpaceCraftUpdate 'Valid) :> Put '[JSON] SpaceCraftId
      :<|> "api" :> "space-craft" :> ReqBody '[JSON] SpaceCraftId :> Delete '[JSON] ()


type SPA = "app" :> "echo" :> QueryParam "echo" Text :> Raw
      :<|> "app" :> "new"  :> Raw
      :<|> "app" :> "edit" :> Capture "id" SpaceCraftId :> Raw
      :<|> "app" :> QueryParam "search" Search :> Raw
      :<|> Raw


routes :: SPA :>> Route
routes = REcho
    :<|> RNew
    :<|> RExisting
    :<|> RList . Input Clean . fromMaybe ""
    :<|> RList (Input Clean "")


deriving newtype instance ToHttpApiData   Search
deriving newtype instance FromHttpApiData Search


instance Routed SPA Route where
  redirect = \case
    REcho t     -> Redirect (Proxy @("app" :> "echo" :> QueryParam "echo" Text :> Raw)) ($ t)
    RNew        -> Redirect (Proxy @("app" :> "new" :> Raw)) id
    RExisting i -> Redirect (Proxy @("app" :> "edit" :> Capture "id" SpaceCraftId :> Raw)) ($ i)
    RList s     -> Redirect (Proxy @("app" :> QueryParam "search" Search :> Raw)) ($ Just (_value s))


class CRUDSpaceCraft m where
  listSpaceCraft   :: m [SpaceCraft]
  getSpaceCraft    :: SpaceCraftId -> m (Maybe SpaceCraft)
  updateSpaceCraft :: SpaceCraftId -> SpaceCraftUpdate 'Valid -> m ()
  createSpaceCraft :: SpaceCraftUpdate 'Valid -> m SpaceCraftId
  deleteSpaceCraft :: SpaceCraftId -> m ()


instance (MonadTrans t, Monad m, CRUDSpaceCraft m) => CRUDSpaceCraft (t m) where
  listSpaceCraft     = lift listSpaceCraft
  getSpaceCraft      = lift . getSpaceCraft
  updateSpaceCraft x = lift . updateSpaceCraft x
  createSpaceCraft   = lift . createSpaceCraft
  deleteSpaceCraft   = lift . deleteSpaceCraft


instance Humanize (Column [SpaceCraft]) where
  humanize = \case
    SKUT          -> "SKU"
    DescriptionT  -> "Desc"
    SerialNumberT -> "Serial #"
    SquadronT     -> "Squadron"
    OperableT     -> "Status"
    ToolsT        -> ""


data instance Column [SpaceCraft] =
    SKUT | DescriptionT | SerialNumberT | SquadronT | OperableT | ToolsT
    deriving (Eq, Ord, Show, Enum, Bounded, Generic, ToJSON, FromJSON)


newtype instance Row [SpaceCraft] = SpaceCraftRow { unRow :: SpaceCraft }
    deriving (Eq, Ord, Show)


instance Tabular [SpaceCraft] where

  type Effect [SpaceCraft] m = (MonadJSM m, CRUDSpaceCraft m)

  toRows = fmap SpaceCraftRow

  toCell xs (SpaceCraftRow SpaceCraft {..}) = \case
    SKUT          -> present _sku
    DescriptionT  -> present _description
    SerialNumberT -> present _serial
    SquadronT     -> present _squadron
    OperableT     -> present _operable
    ToolsT        ->
      [ H.div "btn-group"
        [ H.button [ H.class' "btn btn-sm btn-secondary", H.onClick' (xs <$ navigate @ SPA (RExisting _identity)) ] [ "Edit" ]
        , H.button [ H.class' "btn btn-sm btn-secondary", H.onClick' (Prelude.filter (\x -> x ^. identity /= _identity) xs
                          <$ deleteSpaceCraft _identity) ] [ "Delete" ]
        ]
      ]

  sortTable (SortCol c d) = f $ case c of
    SKUT          -> g sku
    DescriptionT  -> g description
    SerialNumberT -> g serial
    SquadronT     -> g squadron
    OperableT     -> g operable
    ToolsT        -> \_ _ -> EQ
    where f = case d of ASC -> id; DESC -> flip
          g l = compare `on` Lens.view l . unRow