use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use thiserror::Error;
use super::project_config::{
CacheConfig, IgnoreConfig, IncludeConfig, IndexingConfig, LanguageConfig,
};
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Error)]
pub enum SchemaValidationError {
#[error("Incompatible schema version: expected {expected}, found {found}")]
IncompatibleVersion {
expected: u32,
found: u32,
},
#[error("Missing required field: {0}")]
MissingField(String),
#[error("Invalid type for field '{field}': expected {expected}, got {got}")]
InvalidType {
field: String,
expected: String,
got: String,
},
#[error("Invalid value for field '{field}': {reason}")]
InvalidValue {
field: String,
reason: String,
},
}
pub type ValidationResult<T> = Result<T, SchemaValidationError>;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GraphConfigFile {
pub schema_version: u32,
pub metadata: GraphConfigMetadata,
pub integrity: GraphConfigIntegrity,
pub config: GraphConfig,
#[serde(default)]
pub extensions: GraphConfigExtensions,
}
impl Default for GraphConfigFile {
fn default() -> Self {
Self {
schema_version: SCHEMA_VERSION,
metadata: GraphConfigMetadata::default(),
integrity: GraphConfigIntegrity::default(),
config: GraphConfig::default(),
extensions: GraphConfigExtensions::default(),
}
}
}
impl GraphConfigFile {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn validate_version(&self) -> ValidationResult<()> {
if self.schema_version != SCHEMA_VERSION {
return Err(SchemaValidationError::IncompatibleVersion {
expected: SCHEMA_VERSION,
found: self.schema_version,
});
}
Ok(())
}
pub fn validate(&self) -> ValidationResult<()> {
self.validate_version()?;
self.config.validate()?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GraphConfigMetadata {
pub created_at: String,
pub updated_at: String,
pub written_by: WrittenByInfo,
}
impl Default for GraphConfigMetadata {
fn default() -> Self {
let now = chrono::Utc::now().to_rfc3339();
Self {
created_at: now.clone(),
updated_at: now,
written_by: WrittenByInfo::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WrittenByInfo {
pub sqry_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub rustc_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_revision: Option<String>,
}
impl Default for WrittenByInfo {
fn default() -> Self {
Self {
sqry_version: env!("CARGO_PKG_VERSION").to_string(),
rustc_version: None,
git_revision: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GraphConfigIntegrity {
pub normalized_hash_alg: String,
pub normalized_hash_of: String,
pub normalized_hash: String,
pub last_verified_at: String,
}
impl Default for GraphConfigIntegrity {
fn default() -> Self {
Self {
normalized_hash_alg: "blake3".to_string(),
normalized_hash_of: "config".to_string(),
normalized_hash: String::new(), last_verified_at: chrono::Utc::now().to_rfc3339(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
#[derive(Default)]
pub struct GraphConfig {
pub aliases: BTreeMap<String, AliasEntry>,
pub cli: CliPreferences,
pub validation: ValidationConfig,
pub locking: LockingConfig,
pub durability: DurabilityConfig,
pub limits: LimitsConfig,
pub output: OutputConfig,
pub parallelism: ParallelismConfig,
pub timeouts: TimeoutsConfig,
pub persistence: PersistenceConfig,
#[serde(default)]
pub indexing: IndexingConfig,
#[serde(default)]
pub cache: CacheConfig,
#[serde(default)]
pub languages: LanguageConfig,
#[serde(default)]
pub include: IncludeConfig,
#[serde(default)]
pub ignore: IgnoreConfig,
#[serde(default)]
pub buffers: BuffersConfig,
}
impl GraphConfig {
pub fn validate(&self) -> ValidationResult<()> {
self.limits.validate()?;
self.locking.validate()?;
self.output.validate()?;
self.timeouts.validate()?;
self.persistence.validate()?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AliasEntry {
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub created_at: String,
pub updated_at: String,
}
impl AliasEntry {
pub fn new(query: impl Into<String>, description: Option<String>) -> Self {
let now = chrono::Utc::now().to_rfc3339();
Self {
query: query.into(),
description,
created_at: now.clone(),
updated_at: now,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct CliPreferences {
pub default_output_format: String,
pub default_json: bool,
}
impl Default for CliPreferences {
fn default() -> Self {
Self {
default_output_format: "pretty".to_string(),
default_json: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct ValidationConfig {
pub mode: String,
pub enforce_integrity: bool,
}
impl Default for ValidationConfig {
fn default() -> Self {
Self {
mode: "warn".to_string(),
enforce_integrity: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct LockingConfig {
pub write_lock_timeout_ms: u64,
pub stale_lock_timeout_ms: u64,
pub stale_takeover_policy: String,
}
impl Default for LockingConfig {
fn default() -> Self {
Self {
write_lock_timeout_ms: 5000,
stale_lock_timeout_ms: 30000,
stale_takeover_policy: "allow".to_string(),
}
}
}
impl LockingConfig {
pub fn validate(&self) -> ValidationResult<()> {
let valid_policies = ["deny", "warn", "allow"];
if !valid_policies.contains(&self.stale_takeover_policy.as_str()) {
return Err(SchemaValidationError::InvalidValue {
field: "locking.stale_takeover_policy".to_string(),
reason: format!(
"must be one of {:?}, got '{}'",
valid_policies, self.stale_takeover_policy
),
});
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
#[derive(Default)]
pub struct DurabilityConfig {
pub allow_network_filesystems: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct LimitsConfig {
pub max_results: u64,
pub max_depth: u64,
pub max_bytes_per_file: u64,
pub max_files: u64,
pub max_repositories: u64,
pub max_query_length: u64,
pub max_sample_locations: u64,
pub max_relations_per_language_pair: u64,
pub max_regex_length: u64,
pub max_repetition_count: u64,
pub max_predicates: u64,
pub max_query_memory_bytes: u64,
pub max_query_cost: u64,
pub max_git_output_bytes: u64,
pub max_index_uncompressed_bytes: u64,
pub max_compression_ratio: u64,
pub max_index_bytes: u64,
pub max_prewarm_header_bytes: u64,
pub max_prewarm_payload_bytes: u64,
pub analysis_label_budget_per_kind: u64,
pub analysis_density_gate_threshold: u64,
pub analysis_budget_exceeded_policy: String,
}
impl Default for LimitsConfig {
fn default() -> Self {
Self {
max_results: 5000,
max_depth: 6,
max_bytes_per_file: 10 * 1024 * 1024, max_files: 0, max_repositories: 0, max_query_length: 0,
max_sample_locations: 3,
max_relations_per_language_pair: 10_000,
max_regex_length: 1000,
max_repetition_count: 1000,
max_predicates: 100,
max_query_memory_bytes: 512 * 1024 * 1024, max_query_cost: 1_000_000,
max_git_output_bytes: 10 * 1024 * 1024,
max_index_uncompressed_bytes: 500 * 1024 * 1024, max_compression_ratio: 100,
max_index_bytes: 1_000_000_000,
max_prewarm_header_bytes: 4 * 1024, max_prewarm_payload_bytes: 1024 * 1024 * 1024,
analysis_label_budget_per_kind: 15_000_000,
analysis_density_gate_threshold: 64,
analysis_budget_exceeded_policy: "degrade".to_string(),
}
}
}
impl LimitsConfig {
pub fn validate(&self) -> ValidationResult<()> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct OutputConfig {
pub default_pagination: bool,
pub page_size: u64,
pub max_preview_bytes: u64,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
default_pagination: true,
page_size: 50,
max_preview_bytes: 64 * 1024, }
}
}
impl OutputConfig {
pub fn validate(&self) -> ValidationResult<()> {
if self.page_size == 0 {
return Err(SchemaValidationError::InvalidValue {
field: "output.page_size".to_string(),
reason: "must be greater than 0".to_string(),
});
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct ParallelismConfig {
pub max_threads: u64,
pub lexer_pool_max: u64,
pub compaction_chunk_size: u64,
}
impl Default for ParallelismConfig {
fn default() -> Self {
Self {
max_threads: 0, lexer_pool_max: 4,
compaction_chunk_size: 10_000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct TimeoutsConfig {
pub query_timeout_ms: u64,
pub build_timeout_ms: u64,
pub parse_timeout_us: u64,
pub session_timeout_ms: u64,
pub watch_debounce_ms: u64,
}
impl Default for TimeoutsConfig {
fn default() -> Self {
Self {
query_timeout_ms: 0, build_timeout_ms: 0, parse_timeout_us: 2_000_000, session_timeout_ms: 120_000, watch_debounce_ms: 50, }
}
}
impl TimeoutsConfig {
pub fn validate(&self) -> ValidationResult<()> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct PersistenceConfig {
pub max_manifest_bytes: u64,
pub max_snapshot_bytes: u64,
}
impl Default for PersistenceConfig {
fn default() -> Self {
Self {
max_manifest_bytes: 1024 * 1024, max_snapshot_bytes: 0, }
}
}
impl PersistenceConfig {
pub fn validate(&self) -> ValidationResult<()> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct BuffersConfig {
pub parse_buffer_bytes: u64,
pub symbol_buffer_bytes: u64,
pub ast_cache_capacity: u64,
pub query_result_capacity: u64,
pub watch_event_queue_size: u64,
pub channel_capacity: u64,
pub read_buffer_bytes: u64,
pub write_buffer_bytes: u64,
pub index_buffer_bytes: u64,
}
impl Default for BuffersConfig {
fn default() -> Self {
Self {
parse_buffer_bytes: 1024 * 1024, symbol_buffer_bytes: 512 * 1024, ast_cache_capacity: 100, query_result_capacity: 1000, watch_event_queue_size: 10_000, channel_capacity: 1000, read_buffer_bytes: 8192, write_buffer_bytes: 8192, index_buffer_bytes: 1024 * 1024, }
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct GraphConfigExtensions {
#[serde(flatten)]
pub custom: BTreeMap<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_defaults_roundtrip() {
let config = GraphConfigFile::default();
let json = serde_json::to_string_pretty(&config).unwrap();
let parsed: GraphConfigFile = serde_json::from_str(&json).unwrap();
assert_eq!(config, parsed);
}
#[test]
fn test_schema_version_check() {
let config = GraphConfigFile::default();
assert!(config.validate_version().is_ok());
#[allow(clippy::field_reassign_with_default)]
let old_config = {
let mut c = GraphConfigFile::default();
c.schema_version = 0;
c
};
assert!(old_config.validate_version().is_err());
}
#[test]
fn test_validation_rejects_invalid_policy() {
let mut config = GraphConfigFile::default();
config.config.locking.stale_takeover_policy = "invalid".to_string();
assert!(config.validate().is_err());
}
#[test]
fn test_validation_accepts_valid_policies() {
for policy in ["deny", "warn", "allow"] {
let mut config = GraphConfigFile::default();
config.config.locking.stale_takeover_policy = policy.to_string();
assert!(config.validate().is_ok());
}
}
#[test]
fn test_validation_rejects_zero_page_size() {
let mut config = GraphConfigFile::default();
config.config.output.page_size = 0;
assert!(config.validate().is_err());
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_limits_zero_means_unlimited() {
let mut config = LimitsConfig::default();
config.max_results = 0;
config.max_depth = 0;
config.max_bytes_per_file = 0;
config.max_files = 0;
config.max_repositories = 0;
config.max_query_length = 0;
assert!(config.validate().is_ok());
}
#[test]
fn test_alias_entry_creation() {
let alias = AliasEntry::new("kind:function", Some("Find functions".to_string()));
assert_eq!(alias.query, "kind:function");
assert_eq!(alias.description, Some("Find functions".to_string()));
assert!(!alias.created_at.is_empty());
assert!(!alias.updated_at.is_empty());
}
#[test]
fn test_metadata_timestamps() {
let metadata = GraphConfigMetadata::default();
assert!(!metadata.created_at.is_empty());
assert!(!metadata.updated_at.is_empty());
}
#[test]
fn test_written_by_has_version() {
let written_by = WrittenByInfo::default();
assert!(!written_by.sqry_version.is_empty());
}
#[test]
fn test_extensions_empty_by_default() {
let ext = GraphConfigExtensions::default();
assert!(ext.custom.is_empty());
}
#[test]
fn test_full_validation() {
let config = GraphConfigFile::default();
assert!(config.validate().is_ok());
}
}