Using RIO with Servant in Haskell, with nested environments.
Port of servant-nested-apis Gist.
Using RIO with Servant in Haskell, with nested environments.
Port of servant-nested-apis Gist.
| {-# LANGUAGE DataKinds #-} | |
| {-# LANGUAGE TypeOperators #-} | |
| {-# OPTIONS_GHC -Wno-missing-export-lists #-} | |
| module Api where | |
| import RIO | |
| import Servant.API ( | |
| Capture, | |
| GenericMode ((:-)), | |
| Get, | |
| GetNoContent, | |
| Header, | |
| NamedRoutes, | |
| PlainText, | |
| Post, | |
| ReqBody, | |
| (:>), | |
| ) | |
| type AuthorizationHeader = Text | |
| type TraceParentHeader = Text | |
| type OrganizationId = Text | |
| type ProjectId = Text | |
| type TicketId = Text | |
| type ListOrganizationsResponse = Text | |
| type LayoutResponse = Text | |
| type CreateProjectRequest = Text | |
| type CreateProjectResponse = Text | |
| type GetProjectResponse = Text | |
| type CreateTicketRequest = Text | |
| type CreateTicketResponse = Text | |
| type GetTicketResponse = Text | |
| type Api = | |
| "v1" | |
| :> Header "traceparent" TraceParentHeader | |
| :> NamedRoutes RootApi | |
| data RootApi mode = RootApi | |
| { health | |
| :: mode | |
| :- "health" | |
| :> GetNoContent | |
| , layout | |
| :: mode | |
| :- "layout" | |
| :> Get '[PlainText] LayoutResponse | |
| , authenticatedApi | |
| :: mode | |
| :- Header "Authorization" AuthorizationHeader | |
| :> NamedRoutes AuthenticatedApi | |
| } | |
| deriving stock (Generic) | |
| data AuthenticatedApi mode = AuthenticatedApi | |
| { listOrganizations | |
| :: mode | |
| :- "organizations" | |
| :> Get '[PlainText] ListOrganizationsResponse | |
| , projectApi | |
| :: mode | |
| :- "organizations" | |
| :> Capture "organizationId" OrganizationId | |
| :> "projects" | |
| :> NamedRoutes ProjectApi | |
| } | |
| deriving stock (Generic) | |
| data ProjectApi mode = ProjectApi | |
| { createProject | |
| :: mode | |
| :- ReqBody '[PlainText] CreateProjectRequest | |
| :> Post '[PlainText] CreateProjectResponse | |
| , getProject | |
| :: mode | |
| :- Capture "projectId" ProjectId | |
| :> Get '[PlainText] GetProjectResponse | |
| , ticketApi | |
| :: mode | |
| :- Capture "projectId" ProjectId | |
| :> "tickets" | |
| :> NamedRoutes TicketApi | |
| } | |
| deriving stock (Generic) | |
| data TicketApi mode = TicketApi | |
| { createTicket | |
| :: mode | |
| :- ReqBody '[PlainText] CreateTicketRequest | |
| :> Post '[PlainText] CreateTicketResponse | |
| , getTicket | |
| :: mode | |
| :- Capture "ticketId" TicketId | |
| :> Get '[PlainText] GetTicketResponse | |
| } | |
| deriving stock (Generic) |
| module App ( | |
| AppDeps (..), | |
| AppEnv (..), | |
| HasAppEnv (..), | |
| runApp, | |
| healthHandler, | |
| layoutHandler, | |
| ) where | |
| import RIO | |
| import Api (Api, TraceParentHeader) | |
| import Control.Monad.Except (ExceptT (..)) | |
| import Control.Monad.Logger (MonadLogger (..)) | |
| import Database (Connection, Database (..), HasDatabase (..), Pool) | |
| import Logging (HasLogging (..), Logging, monadLoggerLogImpl) | |
| import Servant (Handler (..), NoContent (..), layout) | |
| import Tracing (HasTracing (..), Tracer, Tracing (..), createNewSpan) | |
| data AppDeps = AppDeps | |
| { dbPool :: Pool Connection | |
| , depsLogger :: Logging | |
| , tracer :: Tracer | |
| } | |
| data AppEnv = AppEnv | |
| { appLogger :: Logging | |
| , databaseEnv :: Database | |
| , appTracing :: Tracing | |
| } | |
| instance MonadLogger (RIO AppEnv) where | |
| monadLoggerLog = monadLoggerLogImpl | |
| class | |
| (HasLogging env, HasDatabase env, HasTracing env) => | |
| HasAppEnv env | |
| where | |
| getAppEnv :: env -> AppEnv | |
| instance HasAppEnv AppEnv where | |
| getAppEnv = id | |
| instance HasLogging AppEnv where | |
| getLogging = appLogger | |
| instance HasDatabase AppEnv where | |
| getDatabase = databaseEnv | |
| instance HasTracing AppEnv where | |
| getTracing = appTracing | |
| runAppServant :: AppEnv -> RIO AppEnv a -> Servant.Handler a | |
| runAppServant appEnv action = | |
| Servant.Handler . ExceptT . try $ runRIO appEnv action | |
| runApp | |
| :: AppDeps | |
| -> Maybe TraceParentHeader | |
| -> RIO AppEnv a | |
| -> Servant.Handler a | |
| runApp | |
| AppDeps {dbPool, depsLogger, tracer} | |
| maybeTraceParentHeader | |
| action = do | |
| activeSpan <- createNewSpan maybeTraceParentHeader >>= newIORef | |
| let tracing = | |
| Tracing | |
| { tracer = tracer | |
| , activeSpan = activeSpan | |
| } | |
| databaseEnv = | |
| Database | |
| { dbLogger = depsLogger | |
| , connectionPool = dbPool | |
| } | |
| appEnv = | |
| AppEnv | |
| { appLogger = depsLogger | |
| , databaseEnv = databaseEnv | |
| , appTracing = tracing | |
| } | |
| runAppServant appEnv action | |
| healthHandler :: RIO AppEnv NoContent | |
| healthHandler = pure NoContent | |
| layoutHandler :: RIO AppEnv Text | |
| layoutHandler = pure $ layout (Proxy @Api) |
| module AppAuthenticated ( | |
| AppAuthenticatedDeps (..), | |
| AppAuthenticatedEnv (..), | |
| HasAppAuthenticatedEnv (..), | |
| runAppAuthenticated, | |
| listOrganizationsHandler, | |
| ) where | |
| import RIO hiding (logInfo) | |
| import Api ( | |
| AuthorizationHeader, | |
| ListOrganizationsResponse, | |
| ) | |
| import App (AppEnv (..), HasAppEnv (..)) | |
| import Authentication ( | |
| Auth (..), | |
| AuthKey, | |
| HasAuth (..), | |
| authenticateUser, | |
| getUserId, | |
| ) | |
| import Control.Monad.Logger (MonadLogger (..)) | |
| import Control.Monad.Logger.Aeson (Message ((:#)), logInfo, (.=)) | |
| import Database (HasDatabase (..)) | |
| import Logging (HasLogging (..), monadLoggerLogImpl) | |
| import Organization ( | |
| HasOrganizationService (..), | |
| Organization (organizationId), | |
| OrganizationService, | |
| fetchUserOrganizations, | |
| ) | |
| import Servant (ServerError (..), err500) | |
| import Tracing (HasTracing (..), traced) | |
| data AppAuthenticatedDeps = AppAuthenticatedDeps | |
| { authKey :: AuthKey | |
| , organizationService :: OrganizationService | |
| } | |
| data AppAuthenticatedEnv = AppAuthenticatedEnv | |
| { appEnv :: AppEnv | |
| , appAuth :: Auth | |
| , appOrganizationService :: OrganizationService | |
| } | |
| instance MonadLogger (RIO AppAuthenticatedEnv) where | |
| monadLoggerLog = monadLoggerLogImpl | |
| class | |
| (HasAppEnv env, HasAuth env, HasOrganizationService env) => | |
| HasAppAuthenticatedEnv env | |
| where | |
| getAppAuthenticatedEnv :: env -> AppAuthenticatedEnv | |
| instance HasAppAuthenticatedEnv AppAuthenticatedEnv where | |
| getAppAuthenticatedEnv = id | |
| instance HasAuth AppAuthenticatedEnv where | |
| getAuth = appAuth | |
| instance HasOrganizationService AppAuthenticatedEnv where | |
| getOrganizationService = appOrganizationService | |
| instance HasAppEnv AppAuthenticatedEnv where | |
| getAppEnv = appEnv | |
| instance HasLogging AppAuthenticatedEnv where | |
| getLogging = getLogging . getAppEnv | |
| instance HasDatabase AppAuthenticatedEnv where | |
| getDatabase = getDatabase . getAppEnv | |
| instance HasTracing AppAuthenticatedEnv where | |
| getTracing = getTracing . getAppEnv | |
| runAppAuthenticated | |
| :: AppAuthenticatedDeps | |
| -> Maybe AuthorizationHeader | |
| -> RIO AppAuthenticatedEnv a | |
| -> RIO AppEnv a | |
| runAppAuthenticated | |
| AppAuthenticatedDeps {authKey, organizationService} | |
| maybeAuthHeader | |
| action = do | |
| userId <- authenticateUser authKey maybeAuthHeader | |
| let auth = | |
| Auth | |
| { userId = userId | |
| } | |
| mapEnv appEnv' = | |
| AppAuthenticatedEnv | |
| { appEnv = appEnv' | |
| , appAuth = auth | |
| , appOrganizationService = organizationService | |
| } | |
| mapRIO mapEnv action | |
| listOrganizationsHandler :: RIO AppAuthenticatedEnv ListOrganizationsResponse | |
| listOrganizationsHandler = traced "list_organizations" $ do | |
| userId <- getUserId | |
| organizations <- fetchUserOrganizations userId | |
| logInfo $ | |
| "fetched organizations" | |
| :# [ "user_id" .= userId | |
| , "organizations" .= map organizationId organizations | |
| ] | |
| throwIO $ err500 {errBody = "Not implemented"} |
| module AppProject ( | |
| Project (..), | |
| AppProjectEnv (..), | |
| HasAppProjectEnv (..), | |
| runAppProject, | |
| createProjectHandler, | |
| getProjectHandler, | |
| getProjectOrganization, | |
| findProjectById, | |
| ) where | |
| import RIO hiding (logInfo) | |
| import Api ( | |
| CreateProjectRequest, | |
| CreateProjectResponse, | |
| GetProjectResponse, | |
| OrganizationId, | |
| ProjectId, | |
| ) | |
| import App (HasAppEnv (..)) | |
| import AppAuthenticated ( | |
| AppAuthenticatedEnv (..), | |
| HasAppAuthenticatedEnv (..), | |
| ) | |
| import Authentication (HasAuth (..), getUserId) | |
| import Control.Monad.Logger (MonadLogger (..)) | |
| import Control.Monad.Logger.Aeson (Message ((:#)), logInfo, (.=)) | |
| import Database (HasDatabase (..), query) | |
| import Logging (HasLogging (..), monadLoggerLogImpl) | |
| import Organization ( | |
| HasOrganizationService (..), | |
| Organization (organizationId), | |
| fetchOrganization, | |
| ) | |
| import Servant (ServerError (..), err500) | |
| import Tracing (HasTracing (..), traced) | |
| data Project = Project | |
| { projectId :: ProjectId | |
| , name :: Text | |
| } | |
| data AppProjectEnv = AppProjectEnv | |
| { appAuthenticatedEnv :: AppAuthenticatedEnv | |
| , projectOrganization :: Organization | |
| } | |
| instance MonadLogger (RIO AppProjectEnv) where | |
| monadLoggerLog = monadLoggerLogImpl | |
| class (HasAppAuthenticatedEnv env) => HasAppProjectEnv env where | |
| getAppProjectEnv :: env -> AppProjectEnv | |
| instance HasAppProjectEnv AppProjectEnv where | |
| getAppProjectEnv = id | |
| instance HasAppAuthenticatedEnv AppProjectEnv where | |
| getAppAuthenticatedEnv = appAuthenticatedEnv | |
| instance HasAuth AppProjectEnv where | |
| getAuth = getAuth . getAppAuthenticatedEnv | |
| instance HasOrganizationService AppProjectEnv where | |
| getOrganizationService = getOrganizationService . getAppAuthenticatedEnv | |
| instance HasAppEnv AppProjectEnv where | |
| getAppEnv = getAppEnv . getAppAuthenticatedEnv | |
| instance HasLogging AppProjectEnv where | |
| getLogging = getLogging . getAppEnv | |
| instance HasDatabase AppProjectEnv where | |
| getDatabase = getDatabase . getAppEnv | |
| instance HasTracing AppProjectEnv where | |
| getTracing = getTracing . getAppEnv | |
| runAppProject | |
| :: OrganizationId | |
| -> RIO AppProjectEnv a | |
| -> RIO AppAuthenticatedEnv a | |
| runAppProject organizationId action = do | |
| projectOrganization <- fetchOrganization organizationId | |
| let mapEnv appAuthenticatedEnv' = | |
| AppProjectEnv | |
| { appAuthenticatedEnv = appAuthenticatedEnv' | |
| , projectOrganization = projectOrganization | |
| } | |
| mapRIO mapEnv action | |
| createProjectHandler | |
| :: CreateProjectRequest -> RIO AppProjectEnv CreateProjectResponse | |
| createProjectHandler projectName = traced "create_project" $ do | |
| userId <- getUserId | |
| organizationId <- organizationId <$> getProjectOrganization | |
| _ <- | |
| query | |
| "insert into projects (name, organization_id) values (?, ?) returning id" | |
| (projectName, organizationId) | |
| logInfo $ | |
| "created project" | |
| :# [ "user_id" .= userId | |
| , "organization_id" .= organizationId | |
| ] | |
| throwIO $ err500 {errBody = "Not implemented"} | |
| getProjectHandler :: ProjectId -> RIO AppProjectEnv GetProjectResponse | |
| getProjectHandler projectId = traced "get_project" $ do | |
| userId <- getUserId | |
| organizationId <- organizationId <$> getProjectOrganization | |
| _ <- findProjectById projectId | |
| logInfo $ | |
| "fetched project" | |
| :# [ "user_id" .= userId | |
| , "organization_id" .= organizationId | |
| ] | |
| throwIO $ err500 {errBody = "Not implemented"} | |
| getProjectOrganization | |
| :: (MonadReader env m, HasAppProjectEnv env) => m Organization | |
| getProjectOrganization = | |
| asks (projectOrganization . getAppProjectEnv) | |
| findProjectById :: (HasDatabase env) => ProjectId -> RIO env (Maybe Project) | |
| findProjectById projectId = do | |
| _ <- | |
| query | |
| "select id, name from projects where id = ?" | |
| projectId | |
| pure . Just $ | |
| Project | |
| { projectId = projectId | |
| , name = "My project" | |
| } |
| module AppTicket ( | |
| AppTicketEnv (..), | |
| HasAppTicketEnv (..), | |
| runAppTicket, | |
| createTicketHandler, | |
| getTicketHandler, | |
| getTicketProject, | |
| ) where | |
| import RIO hiding (logInfo) | |
| import Api ( | |
| CreateTicketRequest, | |
| CreateTicketResponse, | |
| GetTicketResponse, | |
| ProjectId, | |
| TicketId, | |
| ) | |
| import App (HasAppEnv (..)) | |
| import AppAuthenticated (HasAppAuthenticatedEnv (..)) | |
| import AppProject ( | |
| AppProjectEnv, | |
| HasAppProjectEnv (..), | |
| Project (..), | |
| findProjectById, | |
| getProjectOrganization, | |
| ) | |
| import Authentication (HasAuth (..), getUserId) | |
| import Control.Monad.Logger (MonadLogger (..)) | |
| import Control.Monad.Logger.Aeson (Message ((:#)), logInfo, (.=)) | |
| import Database (HasDatabase (..), query) | |
| import Logging (HasLogging (..), monadLoggerLogImpl) | |
| import Organization (HasOrganizationService (..), Organization (organizationId)) | |
| import Servant (ServerError (..), err404, err500) | |
| import Tracing (HasTracing (..), traced) | |
| data AppTicketEnv = AppTicketEnv | |
| { appProjectEnv :: AppProjectEnv | |
| , ticketProject :: Project | |
| } | |
| instance MonadLogger (RIO AppTicketEnv) where | |
| monadLoggerLog = monadLoggerLogImpl | |
| class (HasAppProjectEnv env) => HasAppTicketEnv env where | |
| getAppTicketEnv :: env -> AppTicketEnv | |
| instance HasAppTicketEnv AppTicketEnv where | |
| getAppTicketEnv = id | |
| instance HasAppProjectEnv AppTicketEnv where | |
| getAppProjectEnv = appProjectEnv | |
| instance HasAppAuthenticatedEnv AppTicketEnv where | |
| getAppAuthenticatedEnv = getAppAuthenticatedEnv . getAppProjectEnv | |
| instance HasAuth AppTicketEnv where | |
| getAuth = getAuth . getAppAuthenticatedEnv | |
| instance HasOrganizationService AppTicketEnv where | |
| getOrganizationService = getOrganizationService . getAppAuthenticatedEnv | |
| instance HasAppEnv AppTicketEnv where | |
| getAppEnv = getAppEnv . getAppAuthenticatedEnv . getAppProjectEnv | |
| instance HasLogging AppTicketEnv where | |
| getLogging = getLogging . getAppEnv | |
| instance HasDatabase AppTicketEnv where | |
| getDatabase = getDatabase . getAppEnv | |
| instance HasTracing AppTicketEnv where | |
| getTracing = getTracing . getAppEnv | |
| runAppTicket | |
| :: ProjectId | |
| -> RIO AppTicketEnv a | |
| -> RIO AppProjectEnv a | |
| runAppTicket projectId action = do | |
| let projectNotFound :: RIO AppProjectEnv Project | |
| projectNotFound = | |
| throwIO $ err404 {errBody = "Project not found"} | |
| maybeProject <- findProjectById projectId | |
| project <- maybe projectNotFound pure maybeProject | |
| let mapEnv appProjectEnv' = | |
| AppTicketEnv | |
| { appProjectEnv = appProjectEnv' | |
| , ticketProject = project | |
| } | |
| mapRIO mapEnv action | |
| createTicketHandler | |
| :: CreateTicketRequest -> RIO AppTicketEnv CreateTicketResponse | |
| createTicketHandler ticketName = traced "create_ticket" $ do | |
| userId <- getUserId | |
| organizationId <- organizationId <$> getProjectOrganization | |
| projectId <- projectId <$> getTicketProject | |
| _ <- | |
| query | |
| "insert into tickets (name, project_id) values (?, ?) returning id" | |
| (ticketName, projectId) | |
| logInfo $ | |
| "created ticket" | |
| :# [ "user_id" .= userId | |
| , "organization_id" .= organizationId | |
| , "project_id" .= projectId | |
| ] | |
| throwIO $ err500 {errBody = "Not implemented"} | |
| getTicketHandler :: TicketId -> RIO AppTicketEnv GetTicketResponse | |
| getTicketHandler ticketId = traced "get_ticket" $ do | |
| userId <- getUserId | |
| organizationId <- organizationId <$> getProjectOrganization | |
| projectId <- projectId <$> getTicketProject | |
| _ <- | |
| query | |
| "select id, name from tickets where id = ?" | |
| ticketId | |
| logInfo $ | |
| "fetched ticket" | |
| :# [ "user_id" .= userId | |
| , "organization_id" .= organizationId | |
| , "project_id" .= projectId | |
| ] | |
| throwIO $ err500 {errBody = "Not implemented"} | |
| getTicketProject | |
| :: (MonadReader env m, HasAppTicketEnv env) => m Project | |
| getTicketProject = | |
| asks (ticketProject . getAppTicketEnv) |
| -- | Fake authentication | |
| module Authentication ( | |
| AuthKey, | |
| UserId, | |
| parseAuthHeader, | |
| authenticateUser, | |
| Auth (..), | |
| HasAuth (..), | |
| getUserId, | |
| ) where | |
| import RIO | |
| import Api (AuthorizationHeader) | |
| import Servant (err401, errBody) | |
| type AuthKey = Text | |
| type UserId = Text | |
| parseAuthHeader :: Maybe AuthorizationHeader -> Either Text UserId | |
| parseAuthHeader Nothing = Left "Missing 'Authorization' header" | |
| parseAuthHeader _ = Right "d42ed530-adba-41f0-99af-60bd6c476617" | |
| authenticateUser | |
| :: (MonadIO m) | |
| => AuthKey | |
| -> Maybe AuthorizationHeader | |
| -> m UserId | |
| authenticateUser _authKey maybeAuthHeader = | |
| case parseAuthHeader maybeAuthHeader of | |
| Left _ -> | |
| throwIO $ | |
| err401 | |
| { errBody = "Missing or invalid 'Authorization' header" | |
| } | |
| Right userId -> pure userId | |
| data Auth = Auth | |
| { userId :: UserId | |
| } | |
| class HasAuth env where | |
| getAuth :: env -> Auth | |
| getUserId :: (HasAuth env) => RIO env Text | |
| getUserId = userId <$> asks getAuth |
| -- | Fake database | |
| module Database ( | |
| Pool, | |
| Connection, | |
| createDbPool, | |
| Database (..), | |
| HasDatabase (..), | |
| query, | |
| ) where | |
| import RIO hiding (logDebug) | |
| import Control.Monad.Logger.Aeson (Message ((:#)), logDebug, runLoggingT, (.=)) | |
| import Data.Pool (Pool, defaultPoolConfig, newPool, withResource) | |
| import Logging (Logging) | |
| import RIO.Text qualified as T | |
| data Connection = Connection | |
| createDbPool :: Text -> Int -> IO (Pool Connection) | |
| createDbPool _databaseUrl poolSize = do | |
| newPool $ | |
| defaultPoolConfig | |
| create | |
| destroy | |
| poolTtl | |
| poolSize | |
| where | |
| create = pure Connection | |
| destroy = const $ pure () | |
| poolTtl = 10 | |
| data Database = Database | |
| { dbLogger :: Logging | |
| , connectionPool :: Pool Connection | |
| } | |
| class HasDatabase env where | |
| getDatabase :: env -> Database | |
| query :: (HasDatabase env, Show p) => Text -> p -> RIO env [r] | |
| query q parameters = do | |
| logger <- asks (dbLogger . getDatabase) | |
| void . flip runLoggingT logger . logDebug $ | |
| "Database.query" | |
| :# [ "query" .= q | |
| , "parameters" .= (T.pack . show $ parameters) | |
| ] | |
| withConnection $ const (pure []) | |
| withConnection :: (HasDatabase env) => (Connection -> IO a) -> RIO env a | |
| withConnection action = do | |
| pool <- asks (connectionPool . getDatabase) | |
| liftIO $ withResource pool action |
| module Logging ( | |
| Logging, | |
| HasLogging (..), | |
| monadLoggerLogImpl, | |
| ) where | |
| import RIO hiding (LogLevel, LogSource) | |
| import Control.Monad.Logger ( | |
| Loc, | |
| LogLevel, | |
| LogSource, | |
| LogStr, | |
| ToLogStr (toLogStr), | |
| ) | |
| type Logging = | |
| Loc -> LogSource -> LogLevel -> LogStr -> IO () | |
| class HasLogging env where | |
| getLogging :: env -> Logging | |
| monadLoggerLogImpl | |
| :: (HasLogging env, ToLogStr msg) | |
| => Loc | |
| -> LogSource | |
| -> LogLevel | |
| -> msg | |
| -> RIO env () | |
| monadLoggerLogImpl loc logSource logLevel msg = do | |
| logger <- asks getLogging | |
| liftIO $ logger loc logSource logLevel (toLogStr msg) |
| {-# LANGUAGE DataKinds #-} | |
| {-# LANGUAGE UndecidableInstances #-} | |
| module Main (main) where | |
| import RIO | |
| import Api (Api) | |
| import App (AppDeps (..)) | |
| import AppAuthenticated (AppAuthenticatedDeps (..)) | |
| import Control.Monad.Logger.Aeson qualified as Logger (defaultOutput) | |
| import Database (createDbPool) | |
| import Network.HTTP.Client ( | |
| defaultManagerSettings, | |
| managerConnCount, | |
| newManager, | |
| ) | |
| import Network.Wai.Handler.Warp qualified as Warp | |
| import Organization (createOrganizationServiceClient) | |
| import RIO.Text qualified as T | |
| import Servant (serve) | |
| import Server (server) | |
| import System.Environment (lookupEnv) | |
| import Tracing (createTracer) | |
| main :: IO () | |
| main = do | |
| authKey <- T.pack . fromMaybe "abc123" <$> lookupEnv "AUTH_KEY" | |
| projectServiceUrl <- | |
| T.pack . fromMaybe "http://localhost:3001" | |
| <$> lookupEnv "PROJECT_SERVICE_URL" | |
| dbPool <- createDbPool "app:app@localhost:5432/app" 10 | |
| tracer <- createTracer "app" | |
| httpManager <- | |
| newManager $ | |
| defaultManagerSettings {managerConnCount = 20} | |
| let port = 3000 | |
| appDeps = | |
| AppDeps | |
| { dbPool = dbPool | |
| , depsLogger = Logger.defaultOutput stdout | |
| , tracer = tracer | |
| } | |
| appAuthenticatedDeps = | |
| AppAuthenticatedDeps | |
| { authKey = authKey | |
| , organizationService = | |
| createOrganizationServiceClient | |
| httpManager | |
| projectServiceUrl | |
| } | |
| waiApp = serve (Proxy @Api) (server appDeps appAuthenticatedDeps) | |
| Warp.run port waiApp |
| -- | Fake organization service client | |
| module Organization ( | |
| Organization (..), | |
| OrganizationService (..), | |
| createOrganizationServiceClient, | |
| HasOrganizationService (..), | |
| fetchUserOrganizations, | |
| fetchOrganization, | |
| ) where | |
| import RIO | |
| import Api (OrganizationId) | |
| import Authentication (UserId) | |
| import Network.HTTP.Client (Manager) | |
| data Organization = Organization | |
| { organizationId :: OrganizationId | |
| , name :: Text | |
| } | |
| data OrganizationService = OrganizationService | |
| { fetchUserOrganizationsImpl :: UserId -> IO [Organization] | |
| , fetchOrganizationImpl :: OrganizationId -> IO Organization | |
| } | |
| class HasOrganizationService env where | |
| getOrganizationService :: env -> OrganizationService | |
| createOrganizationServiceClient :: Manager -> Text -> OrganizationService | |
| createOrganizationServiceClient _httpManager _serviceBaseUrl = | |
| OrganizationService | |
| { fetchUserOrganizationsImpl = | |
| \_userId -> | |
| pure | |
| [ Organization | |
| { organizationId = "90ee1361-ee8b-4b22-be38-14bf46a28cfd" | |
| , name = "Org 1" | |
| } | |
| , Organization | |
| { organizationId = "6e0549c0-15da-4262-9046-4357413c2791" | |
| , name = "Org 2" | |
| } | |
| ] | |
| , fetchOrganizationImpl = \organizationId -> | |
| pure | |
| Organization | |
| { organizationId = organizationId | |
| , name = "Org 1" | |
| } | |
| } | |
| fetchUserOrganizations | |
| :: (HasOrganizationService env) | |
| => UserId | |
| -> RIO env [Organization] | |
| fetchUserOrganizations userId = do | |
| service <- asks getOrganizationService | |
| liftIO $ fetchUserOrganizationsImpl service userId | |
| fetchOrganization | |
| :: (HasOrganizationService env) | |
| => OrganizationId | |
| -> RIO env Organization | |
| fetchOrganization organizationId = do | |
| service <- asks getOrganizationService | |
| liftIO $ fetchOrganizationImpl service organizationId |
| cabal-version: 3.0 | |
| name: rio-servant-nested | |
| version: 1.0.0 | |
| common options | |
| build-depends: | |
| , base | |
| , http-client | |
| , monad-logger | |
| , monad-logger-aeson | |
| , mtl | |
| , resource-pool | |
| , rio | |
| , servant | |
| , servant-server | |
| , warp | |
| ghc-options: | |
| -Wall | |
| -Wcompat | |
| -Widentities | |
| -Wincomplete-uni-patterns | |
| -Wincomplete-record-updates | |
| -Wredundant-constraints | |
| -Wmissing-export-lists | |
| -Wpartial-fields | |
| -Wunused-packages | |
| default-language: GHC2021 | |
| default-extensions: | |
| DeriveAnyClass | |
| DerivingStrategies | |
| DerivingVia | |
| DuplicateRecordFields | |
| NoImplicitPrelude | |
| OverloadedRecordDot | |
| OverloadedStrings | |
| StrictData | |
| executable rio-servant-nested | |
| import: options | |
| main-is: Main.hs | |
| other-modules: | |
| Api | |
| App | |
| AppAuthenticated | |
| AppProject | |
| AppTicket | |
| Authentication | |
| Database | |
| Logging | |
| Organization | |
| Server | |
| Tracing | |
| hs-source-dirs: . |
| module Server (server) where | |
| import RIO | |
| import Api ( | |
| AuthenticatedApi (..), | |
| AuthorizationHeader, | |
| OrganizationId, | |
| ProjectApi (..), | |
| ProjectId, | |
| RootApi (..), | |
| TicketApi (..), | |
| TraceParentHeader, | |
| ) | |
| import App (AppDeps, AppEnv, healthHandler, layoutHandler, runApp) | |
| import AppAuthenticated ( | |
| AppAuthenticatedDeps (..), | |
| AppAuthenticatedEnv, | |
| listOrganizationsHandler, | |
| runAppAuthenticated, | |
| ) | |
| import AppProject ( | |
| AppProjectEnv, | |
| createProjectHandler, | |
| getProjectHandler, | |
| runAppProject, | |
| ) | |
| import AppTicket ( | |
| AppTicketEnv, | |
| createTicketHandler, | |
| getTicketHandler, | |
| runAppTicket, | |
| ) | |
| import Servant (Handler, NamedRoutes, hoistServer) | |
| import Servant.Server.Generic (AsServerT) | |
| server | |
| :: AppDeps | |
| -> AppAuthenticatedDeps | |
| -> Maybe TraceParentHeader | |
| -> RootApi (AsServerT Servant.Handler) | |
| server appDeps appAuthenticatedDeps maybeTraceParentHeader = | |
| hoistServer | |
| (Proxy @(NamedRoutes RootApi)) | |
| (runApp appDeps maybeTraceParentHeader) | |
| (rootServer appAuthenticatedDeps) | |
| rootServer :: AppAuthenticatedDeps -> RootApi (AsServerT (RIO AppEnv)) | |
| rootServer appAuthenticatedDeps = | |
| RootApi | |
| { health = healthHandler | |
| , layout = layoutHandler | |
| , authenticatedApi = authenticatedServer' | |
| } | |
| where | |
| authenticatedServer' maybeAuthHeader = | |
| hoistServer | |
| (Proxy @(NamedRoutes AuthenticatedApi)) | |
| (runAppAuthenticated appAuthenticatedDeps maybeAuthHeader) | |
| (authenticatedServer maybeAuthHeader) | |
| authenticatedServer | |
| :: Maybe AuthorizationHeader | |
| -> AuthenticatedApi (AsServerT (RIO AppAuthenticatedEnv)) | |
| authenticatedServer _maybeAuthHeader = | |
| AuthenticatedApi | |
| { listOrganizations = listOrganizationsHandler | |
| , projectApi = projectServer' | |
| } | |
| where | |
| projectServer' organizationId = | |
| hoistServer | |
| (Proxy @(NamedRoutes ProjectApi)) | |
| (runAppProject organizationId) | |
| (projectServer organizationId) | |
| projectServer :: OrganizationId -> ProjectApi (AsServerT (RIO AppProjectEnv)) | |
| projectServer _organizationId = | |
| ProjectApi | |
| { createProject = createProjectHandler | |
| , getProject = getProjectHandler | |
| , ticketApi = ticketServer' | |
| } | |
| where | |
| ticketServer' projectId = | |
| hoistServer | |
| (Proxy @(NamedRoutes TicketApi)) | |
| (runAppTicket projectId) | |
| (ticketServer projectId) | |
| ticketServer :: ProjectId -> TicketApi (AsServerT (RIO AppTicketEnv)) | |
| ticketServer _projectId = | |
| TicketApi | |
| { createTicket = createTicketHandler | |
| , getTicket = getTicketHandler | |
| } |
| -- | Fake tracing | |
| module Tracing ( | |
| Tracer, | |
| Span, | |
| Tracing (..), | |
| HasTracing (..), | |
| createTracer, | |
| createNewSpan, | |
| traced, | |
| ) where | |
| import RIO | |
| import Api (TraceParentHeader) | |
| data Tracer = Tracer | |
| data Span = Span | |
| data Tracing = Tracing | |
| { tracer :: Tracer | |
| , activeSpan :: IORef Span | |
| } | |
| class HasTracing env where | |
| getTracing :: env -> Tracing | |
| createTracer :: (MonadIO m) => Text -> m Tracer | |
| createTracer _ = pure Tracer | |
| createNewSpan :: (MonadIO m) => Maybe TraceParentHeader -> m Span | |
| createNewSpan _ = pure Span | |
| childSpan :: (MonadIO m) => IORef Span -> Text -> m () | |
| childSpan activeSpan _childSpanName = | |
| atomicModifyIORef activeSpan ((,())) | |
| traced :: (HasTracing env) => Text -> RIO env a -> RIO env a | |
| traced spanName action = do | |
| activeSpan <- activeSpan <$> asks getTracing | |
| childSpan activeSpan spanName | |
| action |