use crate::schemasync::{PreservationMode, mockmake::coordinate::CoordinationGroup};
use bon::Builder;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use tracing::{debug, trace};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PluginConfig {
pub path: String,
#[serde(default)]
pub params: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Builder)]
pub struct SchemasyncConfig {
pub database: DatabaseConfig,
pub should_generate_mocks: bool,
#[serde(default)]
pub mock_gen_config: SchemasyncMockGenConfig,
#[serde(default)]
pub performance: PerformanceConfig,
#[serde(default)]
#[builder(default)]
pub plugins: BTreeMap<String, PluginConfig>,
#[serde(default)]
#[builder(default)]
pub lint: LintConfig,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct LintConfig {
#[serde(default)]
pub silence_unverifiable_annotations: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
#[serde(rename_all = "lowercase")]
pub enum DatabaseProvider {
#[default]
Surrealdb,
Postgres,
Mysql,
Sqlite,
}
impl std::fmt::Display for DatabaseProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DatabaseProvider::Surrealdb => write!(f, "surrealdb"),
DatabaseProvider::Postgres => write!(f, "postgres"),
DatabaseProvider::Mysql => write!(f, "mysql"),
DatabaseProvider::Sqlite => write!(f, "sqlite"),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DatabaseConfig {
#[serde(default)]
pub provider: DatabaseProvider,
pub url: String,
#[serde(default)]
pub namespace: String,
#[serde(default)]
pub database: String,
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(default)]
pub accesses: AccessesSource,
#[serde(default)]
pub functions: Option<FunctionsSource>,
#[serde(default)]
pub analyzers: Option<AnalyzersSource>,
#[serde(skip)]
pub resolved: ResolvedDatabaseItems,
#[serde(default)]
pub max_connections: Option<u32>,
#[serde(default)]
pub min_connections: Option<u32>,
#[serde(default)]
pub schema: Option<String>,
}
fn default_timeout() -> u64 {
60
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AccessConfig {
pub name: String,
pub access_type: AccessType,
pub table_name: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub enum AccessType {
System,
Record,
Bearer,
Jwt,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum AccessesSource {
Inline(Vec<AccessConfig>),
Path { path: String },
}
impl Default for AccessesSource {
fn default() -> Self {
AccessesSource::Inline(vec![])
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FunctionsSource {
pub path: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AnalyzersSource {
pub path: String,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedDatabaseItems {
pub access_surql: Option<String>,
pub functions_surql: Option<String>,
pub analyzers_surql: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Builder)]
#[serde(default)]
pub struct SchemasyncMockGenConfig {
pub default_record_count: usize,
pub default_preservation_mode: PreservationMode,
pub default_batch_size: usize,
#[builder(default)]
pub coordination_groups: Vec<CoordinationGroup>,
pub full_refresh_mode: bool,
#[builder(default = true)]
pub scripting_asserts: bool,
}
impl Default for SchemasyncMockGenConfig {
fn default() -> Self {
Self {
default_record_count: 10,
default_preservation_mode: PreservationMode::default(),
default_batch_size: 1000,
coordination_groups: Vec::new(),
full_refresh_mode: false,
scripting_asserts: true,
}
}
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
provider: DatabaseProvider::default(),
url: String::new(),
namespace: String::new(),
database: String::new(),
timeout: default_timeout(),
accesses: AccessesSource::default(),
functions: None,
analyzers: None,
resolved: ResolvedDatabaseItems::default(),
max_connections: None,
min_connections: None,
schema: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MockOverrides {
pub skip_mocks: bool,
pub full_refresh: bool,
}
impl SchemasyncConfig {
pub fn apply_mock_overrides(&mut self, overrides: &MockOverrides) {
if overrides.skip_mocks {
self.should_generate_mocks = false;
}
if overrides.full_refresh {
self.mock_gen_config.full_refresh_mode = true;
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ConnectionOverrides {
pub url: Option<String>,
pub namespace: Option<String>,
pub database: Option<String>,
}
impl DatabaseConfig {
pub fn apply_connection_overrides(&mut self, overrides: &ConnectionOverrides) {
if let Some(url) = &overrides.url {
self.url = url.clone();
}
if let Some(namespace) = &overrides.namespace {
self.namespace = namespace.clone();
}
if let Some(database) = &overrides.database {
self.database = database.clone();
}
}
pub fn unresolved_connection_var(&self) -> Option<String> {
[&self.url, &self.namespace, &self.database]
.into_iter()
.find_map(|value| {
let start = value.find("${")? + 2;
let name: String = value[start..]
.chars()
.take_while(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || *c == '_')
.collect();
(!name.is_empty()).then_some(name)
})
}
pub fn for_testing() -> Self {
debug!("Creating database configuration for testing environment");
let config = Self {
provider: DatabaseProvider::Surrealdb,
url: "http://localhost:8000".to_string(),
namespace: "test".to_string(),
database: "test".to_string(),
accesses: AccessesSource::Inline(vec![AccessConfig {
name: "user".to_owned(),
access_type: AccessType::Record,
table_name: "user".to_owned(),
}]),
functions: None,
analyzers: None,
resolved: ResolvedDatabaseItems::default(),
timeout: 60,
max_connections: None,
min_connections: None,
schema: None,
};
trace!(
"Test database config - URL: {}, namespace: {}, database: {}, timeout: {}s",
config.url, config.namespace, config.database, config.timeout
);
if let AccessesSource::Inline(ref accesses) = config.accesses {
trace!("Test access configs: {} entries", accesses.len());
}
config
}
pub fn for_postgres_testing(url: &str) -> Self {
debug!("Creating PostgreSQL database configuration for testing");
Self {
provider: DatabaseProvider::Postgres,
url: url.to_string(),
namespace: String::new(),
database: String::new(),
timeout: 60,
accesses: AccessesSource::default(),
functions: None,
analyzers: None,
resolved: ResolvedDatabaseItems::default(),
max_connections: Some(5),
min_connections: Some(1),
schema: Some("public".to_string()),
}
}
pub fn for_sqlite_testing(path: &str) -> Self {
debug!("Creating SQLite database configuration for testing");
Self {
provider: DatabaseProvider::Sqlite,
url: format!("sqlite:{}", path),
namespace: String::new(),
database: String::new(),
timeout: 60,
accesses: AccessesSource::default(),
functions: None,
analyzers: None,
resolved: ResolvedDatabaseItems::default(),
max_connections: Some(1),
min_connections: Some(1),
schema: None,
}
}
#[cfg(feature = "schemasync")]
pub fn to_provider_config(&self) -> crate::schemasync::database::DatabaseConfig {
crate::schemasync::database::DatabaseConfig {
provider: match self.provider {
DatabaseProvider::Surrealdb => crate::schemasync::database::ProviderType::SurrealDb,
DatabaseProvider::Postgres => crate::schemasync::database::ProviderType::Postgres,
DatabaseProvider::Mysql => crate::schemasync::database::ProviderType::MySql,
DatabaseProvider::Sqlite => crate::schemasync::database::ProviderType::Sqlite,
},
url: self.url.clone(),
namespace: if self.namespace.is_empty() {
None
} else {
Some(self.namespace.clone())
},
database: if self.database.is_empty() {
None
} else {
Some(self.database.clone())
},
username: None, password: None, max_connections: self.max_connections,
min_connections: self.min_connections,
schema: self.schema.clone(),
timeout_secs: self.timeout,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PerformanceConfig {
pub embedded_db_memory_limit: String,
pub cache_duration_seconds: u64,
pub use_progressive_loading: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MockMode {
Smart,
RegenerateAll,
PreserveAll,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegenerateFieldsConfig {
pub always: Vec<String>,
}
impl Default for RegenerateFieldsConfig {
fn default() -> Self {
debug!("Creating default regenerate fields configuration");
let config = Self {
always: vec!["updated_at".to_string(), "created_at".to_string()],
};
trace!("Default regenerate fields: {:?}", config.always);
config
}
}
impl Default for PerformanceConfig {
fn default() -> Self {
debug!("Creating default performance configuration");
let config = Self {
embedded_db_memory_limit: "1GB".to_string(),
cache_duration_seconds: 300,
use_progressive_loading: true,
};
trace!(
"Default performance config - memory: {}, cache: {}s, progressive: {}",
config.embedded_db_memory_limit,
config.cache_duration_seconds,
config.use_progressive_loading
);
config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mock_overrides_only_switch_settings_on() {
let mut config: SchemasyncConfig =
toml::from_str("should_generate_mocks = true\n[database]\nurl = \"x\"\n").unwrap();
config.apply_mock_overrides(&MockOverrides::default());
assert!(config.should_generate_mocks);
assert!(!config.mock_gen_config.full_refresh_mode);
config.apply_mock_overrides(&MockOverrides {
skip_mocks: true,
full_refresh: true,
});
assert!(!config.should_generate_mocks);
assert!(config.mock_gen_config.full_refresh_mode);
}
#[test]
fn connection_overrides_replace_unresolved_settings() {
let mut database = DatabaseConfig::for_testing();
database.url = "${SURREALDB_URL}".to_string();
database.namespace = "prefix_${SURREALDB_NS}".to_string();
assert_eq!(
database.unresolved_connection_var().as_deref(),
Some("SURREALDB_URL")
);
database.apply_connection_overrides(&ConnectionOverrides {
url: Some("http://localhost:8000".to_string()),
namespace: None,
database: None,
});
assert_eq!(database.url, "http://localhost:8000");
assert_eq!(
database.unresolved_connection_var().as_deref(),
Some("SURREALDB_NS")
);
database.apply_connection_overrides(&ConnectionOverrides {
url: None,
namespace: Some("app".to_string()),
database: Some("main".to_string()),
});
assert_eq!(database.unresolved_connection_var(), None);
assert_eq!(database.database, "main");
}
}