sudo stack --allow-different-user exec website
SCRIPT
+$runconf = <<-SCRIPT
+sudo killall website
+cd /vagrant
+sudo stack --allow-different-user exec website config.json
+SCRIPT
+
Vagrant.configure("2") do |config|
config.vm.box = "debian/buster64"
config.vm.provision "build", type: "shell", run: "never", inline: $build
config.vm.provision "test", type: "shell", run: "never", inline: $test
config.vm.provision "run", type: "shell", run: "never", inline: $run
+ config.vm.provision "runconf", type: "shell", run: "never", inline: $runconf
end
import Server
import ServerMonad (ConfigMonad(..), ServerMonad(..))
import System.Environment (getArgs)
-import Text.Read (readMaybe)
+import System.IO (hPutStrLn, stderr)
main :: IO ()
-main = runConfigMonad getConfiguration >>= flip run app . configPort
+main = do
+ config <- runConfigMonad getConfiguration
+ printError $ show config
+ flip run (app config) $ configPort config
getConfiguration :: ConfigMonad ServerConfiguration
getConfiguration = do
- configFilePath <- liftIO getArgs >>= pure . (readMaybe <=< listToMaybe)
+ configFilePath <- liftIO getArgs >>= pure . listToMaybe
maybe (pure $ Right defaultConfiguration) readConfiguration configFilePath >>= pure . either error id
+
+printError :: String -> IO ()
+printError = hPutStrLn stderr
--- /dev/null
+{
+ "configPort": 5000,
+ "configShowExceptions": true
+}
data ServerConfiguration = ServerConfiguration { configPort :: !Port
, configShowExceptions :: !Bool
- } deriving (Generic)
+ } deriving (Show, Generic)
instance FromJSON ServerConfiguration
module Html where
import ApiTypes
+import Configuration (ServerConfiguration(..))
import Control.Exception.Safe (SomeException)
import Control.Monad (void)
import Control.Monad.Error.Class (MonadError(..))
import Control.Monad.IO.Class (MonadIO(..), liftIO)
+import Control.Monad.Reader (MonadReader(..))
import Data.Bifunctor (first)
import Data.ByteString.Lazy (ByteString(..))
import Data.List (sortOn)
imageNotFound :: (MonadError ServerError m) => SomeException -> m a
imageNotFound _ = throwError $ err404 { errBody = "No image found." }
-blogNotFound :: (MonadIO m, MonadError ServerError m) => Maybe Theme -> BlogId -> SomeException -> m a
-blogNotFound theme blogId _ = do
+blogNotFound :: (MonadIO m, MonadError ServerError m, MonadReader ServerConfiguration m) => Maybe Theme -> BlogId -> SomeException -> m a
+blogNotFound theme blogId exceptionReason = do
+ showExceptions <- ask >>= pure . configShowExceptions
body <- htmlContainer theme Nothing $ do
div_ [class_ "not-found"] $ do
h1_ $ toHtml @T.Text "Blog not found"
toHtml @T.Text "Blog post "
em_ $ toHtml $ T.pack blogId
toHtml @T.Text " could not found."
+ if showExceptions then p_ $ toHtml $ T.pack $ show exceptionReason else pure ()
throwError $ err404 { errBody = renderBS body }
siteTitle :: T.Text
module Server where
import ApiTypes
+import Configuration (ServerConfiguration(..))
import Control.Exception.Safe (handleAny)
import Control.Monad.IO.Class (liftIO)
+import Control.Monad.Reader (runReaderT)
import Data.Maybe (fromMaybe)
import Html
import Lucid
import RenderBlog (renderBlog)
import Servant
+import ServerMonad (ServerMonad(..))
import StyleSheet
import System.FilePath.Posix ((</>))
import qualified Clay as C
import qualified Data.Text as T
import qualified Data.Text.IO.Utf8 as T
-app :: Application
-app = serve apiProxy api
+app :: ServerConfiguration -> Application
+app config = serve apiProxy $ hoistServer apiProxy (serverMonadToHandler config) api
-api :: Server Api
+serverMonadToHandler :: ServerConfiguration -> ServerMonad a -> Handler a
+serverMonadToHandler config = Handler . flip runReaderT config . runServerMonad
+
+api :: ServerT Api ServerMonad
api = styling :<|> page
-page :: Server Page
+page :: ServerT Page ServerMonad
page = changeTheme :<|> imageLink :<|> mainPage :<|> blogPost
-mainPage :: Maybe Theme -> Handler (Html ())
+mainPage :: Maybe Theme -> ServerMonad (Html ())
mainPage = flip blogPost defaultBlogId
-blogPost :: Maybe Theme -> BlogId -> Handler (Html ())
+blogPost :: Maybe Theme -> BlogId -> ServerMonad (Html ())
blogPost theme blogId = handleAny (blogNotFound theme blogId) $ findBlogPost blogId >>= htmlContainer theme (Just blogId) . renderBlog
-findBlogPost :: BlogId -> Handler T.Text
+findBlogPost :: BlogId -> ServerMonad T.Text
findBlogPost = liftIO . T.readFile . (</>) staticPath . flip (<>) (T.unpack markdownExtension)
-changeTheme :: Theme -> BlogId -> Handler (Html ())
+changeTheme :: Theme -> BlogId -> ServerMonad (Html ())
changeTheme theme = blogPost (Just theme)
-imageLink :: ImageId -> Handler B.ByteString
+imageLink :: ImageId -> ServerMonad B.ByteString
imageLink imageId = handleAny imageNotFound $ liftIO $ B.readFile $ imagePath </> imageId
-styling :: Maybe Theme -> Handler C.Css
+styling :: Maybe Theme -> ServerMonad C.Css
styling (fromMaybe defaultTheme -> theme) = pure $ getStyleFromTheme (themeType theme) $ C.rgba (themeRed theme) (themeGreen theme) (themeBlue theme) 1
getStyleFromTheme :: LightDark -> C.Color -> C.Css
module ServerMonad where
import Configuration (ServerConfiguration(..), MonadReadConfig(..))
+import Control.Exception.Safe (MonadCatch(..), MonadThrow(..))
+import Control.Monad.Error.Class (MonadError(..))
+import Control.Monad.Except (ExceptT(..))
import Control.Monad.IO.Class (MonadIO(..), liftIO)
import Control.Monad.Reader (MonadReader(..), ReaderT(..))
+import Servant (ServerError)
import qualified Data.ByteString as B
newtype ConfigMonad a = ConfigMonad { runConfigMonad :: IO a
} deriving (Functor, Applicative, Monad, MonadIO)
-newtype ServerMonad a = ServerMonad { runServerMonad :: ReaderT ServerConfiguration IO a
- } deriving (Functor, Applicative, Monad, MonadIO, MonadReader ServerConfiguration)
+newtype ServerMonad a = ServerMonad { runServerMonad :: ReaderT ServerConfiguration (ExceptT ServerError IO) a
+ } deriving (Functor, Applicative, Monad, MonadIO, MonadReader ServerConfiguration, MonadError ServerError, MonadThrow, MonadCatch)
instance MonadReadConfig ConfigMonad where
readConfigFile = liftIO . B.readFile