Skip to main content

fse_cli/
config.rs

1//! `fse.toml` — all optional, all defaulted to the starter layout, so a
2//! fresh app needs no config file at all. Nothing app-specific lives in the
3//! CLI itself: even the framework's required-columns contract arrives here.
4
5use std::collections::BTreeMap;
6use std::path::Path;
7
8use fse_schema::Error;
9use serde::Deserialize;
10
11#[derive(Debug, Deserialize)]
12#[serde(default)]
13pub struct OrmConfig {
14    /// Folder holding one `#[derive(Table)]` struct per file.
15    pub tables_dir: String,
16    /// Plain sqlx migrations folder; generated and hand-written migrations
17    /// interleave by timestamp.
18    pub migrations_dir: String,
19    /// Committed snapshot of the schema the generated migrations produce.
20    pub snapshot_path: String,
21    /// Env var holding the database URL.
22    pub database_url_env: String,
23    /// Columns that must exist, per table — e.g. the framework's auth
24    /// contract on `users`. Shipped in the starter template, not hardcoded.
25    pub required_columns: BTreeMap<String, Vec<String>>,
26    /// Module crate names (cargo package names) whose tables and frontend
27    /// sources this app consumes. Their shipped `.fse/schema.json` snapshots
28    /// merge into `fse migrate`; `fse sync` extracts their `frontend/`
29    /// sources into `.fse/modules/` for the Astro build.
30    pub modules: Vec<String>,
31}
32
33impl Default for OrmConfig {
34    fn default() -> Self {
35        Self {
36            tables_dir: "src/tables".into(),
37            migrations_dir: "migrations".into(),
38            snapshot_path: ".fse/schema.json".into(),
39            database_url_env: "DATABASE_URL".into(),
40            required_columns: BTreeMap::new(),
41            modules: Vec::new(),
42        }
43    }
44}
45
46#[derive(Debug, Default, Deserialize)]
47struct FseToml {
48    #[serde(default)]
49    orm: OrmConfig,
50}
51
52pub fn load(root: &Path) -> Result<OrmConfig, Error> {
53    let path = root.join("fse.toml");
54    if !path.exists() {
55        return Ok(OrmConfig::default());
56    }
57    let raw = std::fs::read_to_string(&path)
58        .map_err(|e| Error::new(format!("cannot read {}: {e}", path.display())))?;
59    let parsed: FseToml = toml::from_str(&raw).map_err(|e| Error::new(format!("fse.toml: {e}")))?;
60    Ok(parsed.orm)
61}
62
63/// Reads `cfg.database_url_env` from `.env`/the environment, in that order.
64/// `override_url` (from `--database-url`/test hooks) wins over both.
65pub fn resolve_database_url(
66    root: &Path,
67    cfg: &OrmConfig,
68    override_url: Option<&str>,
69) -> Result<String, Error> {
70    if let Some(url) = override_url {
71        return Ok(url.to_string());
72    }
73    dotenvy::from_path(root.join(".env")).ok();
74    std::env::var(&cfg.database_url_env)
75        .map_err(|_| Error::new(format!("{} is not set (env or .env)", cfg.database_url_env)))
76}