module Mellon.Web.Server.API
(
MellonAPI
, State(..)
, Time(..)
, app
, mellonAPI
, server
) where
import Control.Lens ((&), (.~), (?~), mapped)
import Control.Monad.Trans.Reader (ReaderT, runReaderT, ask)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Aeson.Types as Aeson (Value(String))
import Data.Aeson.Types
((.=), (.:), (.:?), FromJSON(..), Pair, Series, ToJSON(..),
Value(Object), defaultOptions, genericToJSON, genericParseJSON,
object, pairs, typeMismatch)
import Data.Data
import Data.Maybe (maybe)
import Data.Monoid ((<>))
import Data.Swagger
(NamedSchema(..), Referenced(Inline), SwaggerType(..),
ToSchema(..), declareSchemaRef, defaultSchemaOptions, description,
enum_, example, genericDeclareNamedSchema, properties, required,
schema, type_)
import Data.Text (Text)
import Data.Time.Calendar (fromGregorian)
import Data.Time.Clock (UTCTime(..), getCurrentTime)
import GHC.Generics
import Lucid
(ToHtml(..), HtmlT, doctypehtml_, head_, title_, body_)
import Mellon.Controller
(Controller, lockController, unlockController, queryController)
import qualified Mellon.Controller as Controller (State(..))
import Network.Wai (Application)
import Servant
((:<|>)(..), (:>), (:~>)(..), Get, Handler, JSON, Proxy(..), Put,
ReqBody, Server, ServerT, enter, serve)
import Servant.Docs (ToSample(..))
import Servant.HTML.Lucid (HTML)
wrapBody :: Monad m => HtmlT m () -> HtmlT m a -> HtmlT m a
wrapBody title body =
doctypehtml_ $
do head_ $
title_ title
body_ body
data State
= Locked
| Unlocked !UTCTime
deriving (Eq, Data, Read, Show, Generic, Typeable)
stateToState :: Controller.State -> State
stateToState Controller.StateLocked = Locked
stateToState (Controller.StateUnlocked date) = Unlocked date
lockedName :: Text
lockedName = "Locked"
unlockedName :: Text
unlockedName = "Unlocked"
untilName :: Text
untilName = "until"
stateName :: Text
stateName = "state"
lockedPair :: Pair
lockedPair = stateName .= lockedName
unlockedPair :: Pair
unlockedPair = stateName .= unlockedName
untilPair :: UTCTime -> Pair
untilPair t = untilName .= t
lockedSeries :: Series
lockedSeries = stateName .= lockedName
unlockedSeries :: Series
unlockedSeries = stateName .= unlockedName
untilSeries :: UTCTime -> Series
untilSeries t = untilName .= t
instance ToJSON State where
toJSON Locked = object [lockedPair]
toJSON (Unlocked time) = object [unlockedPair, untilPair time]
toEncoding Locked = pairs lockedSeries
toEncoding (Unlocked time) = pairs $ unlockedSeries <> untilSeries time
instance FromJSON State where
parseJSON (Object v) = do
state :: Text <- v .: stateName
until_ :: Maybe Time <- v .:? untilName
case state of
"Locked" ->
maybe
(pure Locked)
(const $ fail "'Locked' state takes no argument")
until_
"Unlocked" ->
maybe
(fail "'Unlocked' state requires an expiration date")
(\(Time t) -> pure $ Unlocked t)
until_
_ -> fail "Invalid 'state' value"
parseJSON invalid = typeMismatch "State" invalid
instance ToSchema State where
declareNamedSchema _ = do
utcTimeSchema <- declareSchemaRef (Proxy :: Proxy UTCTime)
let stateSchema =
mempty & enum_ ?~ [Aeson.String lockedName, Aeson.String unlockedName]
& type_ .~ SwaggerString
return $
NamedSchema (Just "State") $
mempty & type_ .~ SwaggerObject
& properties .~ [(stateName, Inline stateSchema), (untilName, utcTimeSchema)]
& required .~ [stateName]
& description ?~ "The controller state; a variant type."
& example ?~ toJSON (Unlocked sampleDate)
sampleDate :: UTCTime
sampleDate = UTCTime { utctDay = fromGregorian 2015 10 06, utctDayTime = 0 }
instance ToSample State where
toSamples _ =
[ ("Locked", Locked)
, ("Unlocked until a given date", Unlocked sampleDate)
]
stateDocument :: Monad m => HtmlT m a -> HtmlT m a
stateDocument = wrapBody "Mellon state"
instance ToHtml State where
toHtml Locked = stateDocument "Locked"
toHtml (Unlocked time) = stateDocument $ "Unlocked until " >> toHtml (show time)
toHtmlRaw = toHtml
newtype Time =
Time UTCTime
deriving (Eq, Data, Ord, Read, Show, Generic, Typeable)
instance ToJSON Time where
toJSON = genericToJSON defaultOptions
instance FromJSON Time where
parseJSON = genericParseJSON defaultOptions
instance ToSchema Time where
declareNamedSchema proxy =
genericDeclareNamedSchema defaultSchemaOptions proxy
& mapped.schema.description ?~ "A UTC date"
& mapped.schema.example ?~ toJSON sampleDate
instance ToSample Time where
toSamples _ = [("2015-10-06",Time sampleDate)]
timeDocument :: Monad m => HtmlT m a -> HtmlT m a
timeDocument = wrapBody "Server time"
instance ToHtml Time where
toHtml (Time time) = timeDocument $ toHtml $ "Server time is " ++ show time
toHtmlRaw = toHtml
type MellonAPI =
"time" :> Get '[JSON, HTML] Time :<|>
"state" :> Get '[JSON, HTML] State :<|>
"state" :> ReqBody '[JSON] State :> Put '[JSON, HTML] State
type AppM d = ReaderT (Controller d) Handler
serverT :: ServerT MellonAPI (AppM d)
serverT =
getTime :<|>
getState :<|>
putState
where
getTime :: AppM d Time
getTime =
do now <- liftIO getCurrentTime
return $ Time now
getState :: AppM d State
getState =
do cc <- ask
fmap stateToState (queryController cc)
putState :: State -> AppM d State
putState Locked =
do cc <- ask
fmap stateToState (lockController cc)
putState (Unlocked date) =
do cc <- ask
fmap stateToState (unlockController date cc)
mellonAPI :: Proxy MellonAPI
mellonAPI = Proxy
appToHandler :: Controller d -> AppM d :~> Handler
appToHandler cc = NT $ \m -> runReaderT m cc
server :: Controller d -> Server MellonAPI
server cc = enter (appToHandler cc) serverT
app :: Controller d -> Application
app = serve mellonAPI . server