slave-framework 0.8.1

Runtime for slave apps (a Laravel-inspired Rust framework for MCP-first apps): server lifecycle, config, logging, a Toasty database (Turso), and OpenID Connect sign-in
//! The application's [Toasty](https://github.com/tokio-rs/toasty) database.
//!
//! Models are Rust structs with `#[derive(toasty::Model)]`; this module opens
//! the connection pool they run on. The default backend is Turso's embedded
//! engine: a local database file, pure Rust, no server to run. Configure it
//! with `DATABASE_URL` (default [`DEFAULT_URL`]).
//!
//! Schema changes are Toasty migrations: generated from the models by the
//! app's `migrate` binary (built on `toasty-cli`) and applied by it.

use toasty::{Db, ModelSet};

use crate::Result;

/// The lock mode of a transaction (`BEGIN IMMEDIATE` and friends), for
/// [`toasty::TransactionBuilder::mode`].
pub use toasty_core::driver::operation::TransactionMode;

/// Environment variable holding the database URL.
pub const URL_ENV: &str = "DATABASE_URL";

/// The database used when `DATABASE_URL` is not set: a Turso file in
/// `database/`, relative to the working directory (the project root under
/// `slave serve` and `slave migrate`).
pub const DEFAULT_URL: &str = "turso:database/database.db";

/// Where to connect.
#[derive(Debug, Clone)]
pub struct DatabaseConfig {
    /// A Toasty connection URL, e.g. `turso:database/database.db`.
    pub url: String,
}

impl DatabaseConfig {
    /// Reads `DATABASE_URL`, falling back to [`DEFAULT_URL`].
    pub fn from_env() -> Self {
        Self {
            url: crate::env::var_or(URL_ENV, DEFAULT_URL),
        }
    }
}

/// Opens a pooled handle to the database for `models`. Cheap to clone: clones
/// share the pool. Creates the parent directory of a file database if needed.
pub async fn connect(models: ModelSet, config: &DatabaseConfig) -> Result<Db> {
    if let Some(dir) = file_path(&config.url).and_then(|path| path.parent()) {
        if !dir.as_os_str().is_empty() {
            std::fs::create_dir_all(dir).map_err(crate::Error::DatabaseDir)?;
        }
    }
    Ok(Db::builder().models(models).connect(&config.url).await?)
}

/// The file behind a `turso:` or `sqlite:` URL, if it names one.
fn file_path(url: &str) -> Option<&std::path::Path> {
    let path = url
        .strip_prefix("turso:")
        .or_else(|| url.strip_prefix("sqlite:"))?;
    (!path.starts_with("//") && !path.starts_with(':')).then(|| std::path::Path::new(path))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn file_paths() {
        let path = |url| file_path(url).map(|p| p.to_str().unwrap().to_owned());
        assert_eq!(path("turso:database/app.db").as_deref(), Some("database/app.db"));
        assert_eq!(path("sqlite:/tmp/x.db").as_deref(), Some("/tmp/x.db"));
        assert_eq!(path("turso::memory:"), None);
        assert_eq!(path("turso://db.example.com"), None);
        assert_eq!(path("postgresql://localhost/app"), None);
    }
}