Skip to main content

combat_service/
lib.rs

1//! Combat service: authoritative, event-sourced combat encounters with live
2//! updates over WebSocket. Any number of instances can run against the same
3//! Postgres database.
4
5/// Errors, as responses.
6/// What whoever runs the service asks of it over HTTP.
7pub mod admin;
8pub mod encounters;
9pub mod error;
10mod export;
11pub mod hub;
12/// The `/combats` routes and the WebSocket.
13pub mod routes;
14pub mod services;
15pub mod store;
16pub mod sync;
17
18use std::sync::Arc;
19
20use axum::{Extension, Router};
21use edda_auth::server::TokenVerifier;
22use edda_server::{Limits, openapi::Conventions};
23use sqlx::{PgPool, postgres::PgListener};
24
25/// The service's `OpenAPI` description.
26#[derive(utoipa::OpenApi)]
27#[openapi(
28    paths(
29        edda_server::health,
30        edda_server::ready,
31        routes::list,
32        routes::create,
33        routes::snapshot,
34        routes::log,
35        routes::command,
36        routes::websocket,
37        encounters::list,
38        encounters::create,
39        encounters::fetch,
40        encounters::replace,
41        encounters::remove,
42    ),
43    components(schemas(
44        edda_types::combat::ClientMessage,
45        edda_types::combat::ServerMessage,
46    )),
47    modifiers(&Conventions),
48    tags(
49        (name = "combats", description = "Live, event-sourced combats."),
50        (name = "encounters", description = "Planned encounters: the DM's only."),
51    )
52)]
53pub struct ApiDoc;
54
55/// Where the other services are, the limits, and where to listen for
56/// other instances' events.
57#[derive(Clone, Default)]
58pub struct Config {
59    /// The other services to use, as this service itself. `None` uses
60    /// none: sheets are left alone, and monsters can't be added by id.
61    pub services: Option<services::ServicesConfig>,
62    /// How much a campaign may keep, and how many may watch a combat.
63    pub limits: Limits,
64    /// The database to `LISTEN` on, by a connection of its own (see
65    /// [`edda_server::direct_database_url`]); `None` takes one from the
66    /// pool, which works unless the pool goes through `PgBouncer`.
67    pub listen_url: Option<String>,
68}
69
70/// Leaves the URL out: it holds the database's password.
71impl std::fmt::Debug for Config {
72    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
73        f.debug_struct("Config")
74            .field("services", &self.services)
75            .field("limits", &self.limits)
76            .field("listens_directly", &self.listen_url.is_some())
77            .finish()
78    }
79}
80
81/// Connects to the database, creating it if needed, and applies migrations.
82pub async fn connect(database_url: &str) -> Result<PgPool, sqlx::Error> {
83    edda_server::open_database(database_url, &sqlx::migrate!()).await
84}
85
86/// The service, with its background tasks started on the current runtime.
87/// Accepts tokens that `verifier` vouches for.
88pub async fn app(
89    pool: PgPool,
90    verifier: TokenVerifier,
91    config: Config,
92) -> Result<Router, sqlx::Error> {
93    let ready = edda_server::readiness(pool.clone());
94    let store = store::Store::new(pool);
95    let (mut sheet_sync, mut monsters) = (None, None);
96    if let Some(config) = config.services {
97        let login = Arc::new(services::ServiceLogin::new(&config));
98        sheet_sync = config
99            .character_service
100            .map(|url| sync::SheetSync::new(store.clone(), login.clone(), url));
101        monsters = config
102            .monster_service
103            .map(|url| services::Monsters::new(login, url));
104    }
105    let listener = match &config.listen_url {
106        Some(url) => PgListener::connect(url).await?,
107        None => PgListener::connect_with(store.pool()).await?,
108    };
109    let hub = hub::Hub::start(store, listener, sheet_sync, monsters, config.limits).await?;
110    Ok(Router::new()
111        .merge(routes::router())
112        .merge(encounters::router())
113        .with_state(hub)
114        .layer(Extension(Arc::new(verifier)))
115        .merge(ready))
116}