init
This commit is contained in:
commit
6e2fb3b9bd
53
HELPS.cabal
Normal file
53
HELPS.cabal
Normal file
@ -0,0 +1,53 @@
|
||||
cabal-version: 1.12
|
||||
|
||||
name: HELPS
|
||||
version: 0.0.1
|
||||
description: Haskell, Effectful, Lucid, PostgreSQL, Servant
|
||||
author: James Eversole
|
||||
maintainer: james@eversole.co
|
||||
copyright: James Eversole
|
||||
license: ISC
|
||||
license-file: LICENSE
|
||||
build-type: Simple
|
||||
extra-source-files:
|
||||
README
|
||||
|
||||
executable Main
|
||||
main-is: Main.hs
|
||||
hs-source-dirs:
|
||||
src
|
||||
default-extensions:
|
||||
BlockArguments
|
||||
ConstraintKinds
|
||||
DataKinds
|
||||
DeriveAnyClass
|
||||
DeriveGeneric
|
||||
DerivingStrategies
|
||||
FlexibleContexts
|
||||
FlexibleInstances
|
||||
GeneralizedNewtypeDeriving
|
||||
LambdaCase
|
||||
OverloadedRecordDot
|
||||
OverloadedStrings
|
||||
ScopedTypeVariables
|
||||
StrictData
|
||||
TypeFamilies
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N -optl-pthread -fPIC
|
||||
build-depends:
|
||||
base
|
||||
, aeson
|
||||
, bytestring
|
||||
, effectful
|
||||
, exceptions
|
||||
, lucid
|
||||
, mtl
|
||||
, postgresql-simple
|
||||
, servant-server
|
||||
, text
|
||||
, utf8-string
|
||||
, warp
|
||||
other-modules:
|
||||
Core
|
||||
Database
|
||||
Handlers
|
||||
default-language: GHC2021
|
9
README.md
Normal file
9
README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# servant-effectful-template
|
||||
|
||||
A nix starting template for web projects utilizing
|
||||
|
||||
- [Haskell](https://wiki.haskell.org/Haskell)
|
||||
- [Effectful](https://github.com/haskell-effectful/effectful)
|
||||
- [Lucid](https://github.com/chrisdone/lucid)
|
||||
- [PostgreSQL](https://www.postgresql.org/)
|
||||
- [Servant](https://github.com/haskell-servant/servant)
|
60
flake.lock
generated
Normal file
60
flake.lock
generated
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1728764407,
|
||||
"narHash": "sha256-J4kaIxwjrGVQkLA6njCFH09xj2oCf/VWFircIy7b65k=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4ada35702a7146e1df24f8d6987a1d7c1a5a4707",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
62
flake.nix
Normal file
62
flake.nix
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
description = "HELPS";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
packageName = "HELPS";
|
||||
containerPackageName = "${packageName}-container";
|
||||
|
||||
haskellPackages = pkgs.haskellPackages;
|
||||
|
||||
enableSharedExecutables = false;
|
||||
enableSharedLibraries = false;
|
||||
|
||||
HELPS = pkgs.haskell.lib.justStaticExecutables self.packages.${system}.default;
|
||||
in {
|
||||
|
||||
packages.${packageName} =
|
||||
haskellPackages.callCabal2nix packageName self rec {};
|
||||
|
||||
packages.default = self.packages.${system}.${packageName};
|
||||
defaultPackage = self.packages.${system}.default;
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
ghcid
|
||||
cabal-install
|
||||
ghc
|
||||
];
|
||||
inputsFrom = builtins.attrValues self.packages.${system};
|
||||
};
|
||||
devShell = self.devShells.${system}.default;
|
||||
|
||||
packages.${containerPackageName} = pkgs.dockerTools.buildImage {
|
||||
name = "HELPS";
|
||||
|
||||
copyToRoot = pkgs.buildEnv {
|
||||
name = "image-root";
|
||||
paths = [ HELPS ];
|
||||
pathsToLink = [ "/bin" ];
|
||||
};
|
||||
tag = "latest";
|
||||
config = {
|
||||
Cmd = [
|
||||
"/bin/Purr"
|
||||
];
|
||||
WorkingDir = "/app";
|
||||
ExposedPorts = {
|
||||
"3000/tcp" = {};
|
||||
};
|
||||
extraCommands = ''
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
54
src/Core.hs
Normal file
54
src/Core.hs
Normal file
@ -0,0 +1,54 @@
|
||||
module Core where
|
||||
|
||||
import Control.Exception (IOException)
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Text as T
|
||||
import Database.PostgreSQL.Simple (Query)
|
||||
import Database.PostgreSQL.Simple.FromRow (FromRow, field, fromRow)
|
||||
import Database.PostgreSQL.Simple.ToRow (ToRow, toRow)
|
||||
import Database.PostgreSQL.Simple.ToField (ToField)
|
||||
import Effectful
|
||||
import Effectful.Dispatch.Dynamic
|
||||
import Effectful.Error.Static (Error)
|
||||
import Effectful.FileSystem
|
||||
import GHC.Generics (Generic)
|
||||
import Servant hiding ((:>))
|
||||
import qualified Servant as S
|
||||
|
||||
-- Core data
|
||||
data User = User { userId :: Int, userName :: String}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance FromRow User where
|
||||
fromRow = User <$> field <*> field
|
||||
instance ToRow User where
|
||||
toRow (User uid name) = toRow (uid, name)
|
||||
|
||||
instance ToJSON User
|
||||
instance FromJSON User
|
||||
|
||||
-- Effects
|
||||
type AppEff = Eff '[Database, FileSystem, Error ServerError, IOE]
|
||||
|
||||
data Database :: Effect where
|
||||
DatabaseRead :: (Query, Int) -> Database (Eff es) (Maybe User)
|
||||
DatabaseWrite :: (Query, String) -> Database (Eff es) ()
|
||||
|
||||
type instance DispatchOf Database = 'Dynamic
|
||||
|
||||
databaseRead :: (Database :> es, Error ServerError :> es) => (Query, Int) -> Eff es (Maybe User)
|
||||
databaseRead = send . DatabaseRead
|
||||
|
||||
databaseWrite :: (Database :> es, Error ServerError :> es) => (Query, String) -> Eff es ()
|
||||
databaseWrite = send . DatabaseWrite
|
||||
|
||||
-- Routes
|
||||
type Root = Get '[PlainText] T.Text
|
||||
type UserList = "user" S.:> Get '[JSON] [User]
|
||||
type UserGet = "user" S.:> Capture "userId" Int S.:> Get '[JSON] User
|
||||
type UserPost = "user" S.:> ReqBody '[PlainText] String S.:> PostCreated '[PlainText] NoContent
|
||||
|
||||
type API = Root
|
||||
:<|> UserList
|
||||
:<|> UserGet
|
||||
:<|> UserPost
|
58
src/Database.hs
Normal file
58
src/Database.hs
Normal file
@ -0,0 +1,58 @@
|
||||
module Database where
|
||||
|
||||
import Core
|
||||
|
||||
import Control.Exception (IOException)
|
||||
import Control.Monad.Catch (catch)
|
||||
import Data.Aeson (ToJSON)
|
||||
import Data.ByteString.Lazy.UTF8 (fromString)
|
||||
import Data.Maybe (listToMaybe)
|
||||
import Database.PostgreSQL.Simple
|
||||
import Effectful
|
||||
import Effectful.Dispatch.Dynamic
|
||||
import Effectful.Error.Static (Error, HasCallStack, catchError, runErrorNoCallStack, throwError)
|
||||
import Servant hiding ((:>), throwError)
|
||||
|
||||
runDatabaseDebug :: (IOE :> es, Error ServerError :> es) => Eff (Database : es) a -> Eff es a
|
||||
runDatabaseDebug = interpret $ \_ -> \case
|
||||
DatabaseRead (statement, values) -> adapt $ read statement values
|
||||
DatabaseWrite (statement, values) -> adapt $ write statement values
|
||||
where
|
||||
read _ values =
|
||||
putStrLn "We just mocked a READ database operation" >>= \_ -> pure $
|
||||
Just (User values "Mock User")
|
||||
write _ values =
|
||||
putStrLn $ "We just mocked a WRITE database operation with a user named "
|
||||
++ values
|
||||
|
||||
runDatabaseIO :: (IOE :> es, Error ServerError :> es) => Eff (Database : es) a -> Eff es a
|
||||
runDatabaseIO = interpret $ \_ -> \case
|
||||
DatabaseRead (statement, values) -> adapt $ read statement values
|
||||
DatabaseWrite (statement, values) -> adapt $ write statement values
|
||||
where
|
||||
read :: Query -> Int -> IO (Maybe User)
|
||||
read statement values = do
|
||||
conn <- openConn
|
||||
user <- query conn statement (Only values)
|
||||
pure $ listToMaybe user
|
||||
write :: Query -> String -> IO ()
|
||||
write statement values = do
|
||||
conn <- openConn
|
||||
execute conn statement (Only values)
|
||||
putStrLn $ "Wrote user to database using statement:\n" ++ show statement
|
||||
openConn = connectPostgreSQL "host=localhost port=5432 dbname=demo connect_timeout=10"
|
||||
|
||||
adapt :: (IOE :> es, Error ServerError :> es) => IO a -> Eff es a
|
||||
adapt m = liftIO m `catch` \(e::IOException) ->
|
||||
throwError $ ServerError
|
||||
{ errHTTPCode = 500
|
||||
, errReasonPhrase = "Internal Database Error"
|
||||
, errBody = fromString $ show e
|
||||
, errHeaders = []
|
||||
}
|
||||
|
||||
queryUser :: Int -> (Query, Int)
|
||||
queryUser userId = ("SELECT id, name FROM users WHERE id = ?;", userId)
|
||||
|
||||
writeUser :: String -> (Query, String)
|
||||
writeUser name = ("INSERT INTO users (name) VALUES (?);", name)
|
29
src/Handlers.hs
Normal file
29
src/Handlers.hs
Normal file
@ -0,0 +1,29 @@
|
||||
module Handlers where
|
||||
|
||||
import Core
|
||||
import Database
|
||||
|
||||
import qualified Data.ByteString.Char8 as C
|
||||
import Data.List
|
||||
import qualified Data.Text as T
|
||||
import Effectful
|
||||
import Effectful.Error.Static (Error, throwError)
|
||||
import Effectful.FileSystem
|
||||
import Effectful.FileSystem.IO.ByteString as EBS
|
||||
import Servant hiding ((:>), throwError)
|
||||
import qualified Servant as S
|
||||
|
||||
rootHandler :: (Error ServerError :> es) => Eff es T.Text
|
||||
rootHandler = return "Hello, World!"
|
||||
|
||||
userListHandler :: (Database :> es, Error ServerError :> es, FileSystem :> es) => Eff es [User]
|
||||
userListHandler = mapM userGetHandler [1, 2, 3]
|
||||
|
||||
userGetHandler :: (Database :> es, Error ServerError :> es, FileSystem :> es) => Int -> Eff es User
|
||||
userGetHandler userId = databaseRead (queryUser userId) >>= \mUser ->
|
||||
case mUser of
|
||||
Just a -> pure a
|
||||
Nothing -> pure (User 0 "No user found")
|
||||
|
||||
userPostHandler :: (Database :> es, Error ServerError :> es, FileSystem :> es) => String -> Eff es NoContent
|
||||
userPostHandler name = databaseWrite (writeUser name) >>= \_ -> return NoContent
|
33
src/Main.hs
Normal file
33
src/Main.hs
Normal file
@ -0,0 +1,33 @@
|
||||
module Main (main) where
|
||||
|
||||
import Core
|
||||
import Database
|
||||
import Handlers
|
||||
|
||||
import Control.Monad.Except (ExceptT (ExceptT))
|
||||
import Data.List
|
||||
import Effectful
|
||||
import Effectful.Error.Static (Error, runError, runErrorNoCallStack, throwError)
|
||||
import Effectful.FileSystem
|
||||
import Network.Wai.Handler.Warp (run)
|
||||
import Servant hiding ((:>), throwError)
|
||||
import qualified Servant as S
|
||||
|
||||
main :: IO ()
|
||||
main = run port $ serve proxy app
|
||||
|
||||
app :: Server API
|
||||
app = α $ rootHandler
|
||||
:<|> userListHandler
|
||||
:<|> userGetHandler
|
||||
:<|> userPostHandler
|
||||
|
||||
α :: ServerT API AppEff -> ServerT API Handler
|
||||
α = hoistServer proxy $ Handler . ExceptT .
|
||||
runEff . runErrorNoCallStack . runFileSystem . runDatabaseIO
|
||||
|
||||
port :: Int
|
||||
port = 8080
|
||||
|
||||
proxy :: Proxy API
|
||||
proxy = Proxy
|
Loading…
x
Reference in New Issue
Block a user