use std::collections::HashMap;
use std::path::PathBuf;
use std::time::SystemTime;
use anyhow::{Context, Result};
#[cfg(feature = "templating")]
use flowscope_core::TemplateConfig;
use flowscope_core::{Dialect, FileSource, SchemaMetadata};
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub dialect: Dialect,
pub watch_dirs: Vec<PathBuf>,
pub static_files: Option<Vec<FileSource>>,
pub metadata_url: Option<String>,
pub metadata_schema: Option<String>,
pub port: u16,
pub open_browser: bool,
pub schema_path: Option<PathBuf>,
#[cfg(feature = "templating")]
pub template_config: Option<TemplateConfig>,
}
pub struct AppState {
pub config: ServerConfig,
pub files: RwLock<Vec<FileSource>>,
pub schema: RwLock<Option<SchemaMetadata>>,
pub mtimes: RwLock<HashMap<PathBuf, SystemTime>>,
}
impl AppState {
pub async fn new(config: ServerConfig) -> Result<Self> {
let (files, mtimes) = if let Some(ref static_files) = config.static_files {
(static_files.clone(), HashMap::new())
} else {
let watch_dirs = config.watch_dirs.clone();
let scan_result =
tokio::task::spawn_blocking(move || super::scan_sql_files(&watch_dirs))
.await
.context("File scan task was cancelled")?;
scan_result.context("Failed to scan SQL files")?
};
let file_count = files.len();
let schema = Self::load_schema(&config).await?;
if file_count > 0 {
println!("flowscope: loaded {} SQL file(s)", file_count);
}
Ok(Self {
config,
files: RwLock::new(files),
schema: RwLock::new(schema),
mtimes: RwLock::new(mtimes),
})
}
#[cfg(feature = "metadata-provider")]
async fn load_schema(config: &ServerConfig) -> Result<Option<SchemaMetadata>> {
if let Some(ref url) = config.metadata_url {
let url = url.clone();
let schema_filter = config.metadata_schema.clone();
let fetch_result = tokio::task::spawn_blocking(move || {
crate::metadata::fetch_metadata_from_database(&url, schema_filter)
})
.await
.context("Metadata fetch task was cancelled")?;
let schema = fetch_result.context("Failed to fetch database metadata")?;
println!("flowscope: loaded schema from database");
return Ok(Some(schema));
}
Self::load_schema_from_file(config)
}
#[cfg(not(feature = "metadata-provider"))]
async fn load_schema(config: &ServerConfig) -> Result<Option<SchemaMetadata>> {
Self::load_schema_from_file(config)
}
fn load_schema_from_file(config: &ServerConfig) -> Result<Option<SchemaMetadata>> {
if let Some(ref path) = config.schema_path {
let schema = crate::schema::load_schema_from_ddl(path, config.dialect)?;
println!("flowscope: loaded schema from DDL file: {}", path.display());
return Ok(Some(schema));
}
Ok(None)
}
pub async fn reload_files(&self) -> Result<()> {
if self.config.static_files.is_some() {
return Ok(());
}
let watch_dirs = self.config.watch_dirs.clone();
let scan_result = tokio::task::spawn_blocking(move || super::scan_sql_files(&watch_dirs))
.await
.context("File scan task was cancelled")?;
let (files, mtimes) = scan_result.context("Failed to scan SQL files")?;
let count = files.len();
*self.files.write().await = files;
*self.mtimes.write().await = mtimes;
println!("flowscope: reloaded {} SQL file(s)", count);
Ok(())
}
}