use std::collections::BTreeMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
pub use tokmd_types::{ChildIncludeMode, ChildrenMode, ConfigMode, ExportFormat, RedactMode};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UserConfig {
pub profiles: BTreeMap<String, Profile>,
pub repos: BTreeMap<String, String>, }
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Profile {
pub format: Option<String>, pub top: Option<usize>,
pub files: Option<bool>,
pub module_roots: Option<Vec<String>>,
pub module_depth: Option<usize>,
pub min_code: Option<usize>,
pub max_rows: Option<usize>,
pub redact: Option<RedactMode>,
pub meta: Option<bool>,
pub children: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScanOptions {
#[serde(default)]
pub excluded: Vec<String>,
#[serde(default)]
pub config: ConfigMode,
#[serde(default)]
pub hidden: bool,
#[serde(default)]
pub no_ignore: bool,
#[serde(default)]
pub no_ignore_parent: bool,
#[serde(default)]
pub no_ignore_dot: bool,
#[serde(default)]
pub no_ignore_vcs: bool,
#[serde(default)]
pub treat_doc_strings_as_comments: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScanSettings {
#[serde(default)]
pub paths: Vec<String>,
#[serde(flatten)]
pub options: ScanOptions,
}
impl ScanSettings {
pub fn current_dir() -> Self {
Self {
paths: vec![".".to_string()],
..Default::default()
}
}
pub fn for_paths(paths: Vec<String>) -> Self {
Self {
paths,
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LangSettings {
#[serde(default)]
pub top: usize,
#[serde(default)]
pub files: bool,
#[serde(default = "default_children_mode")]
pub children: ChildrenMode,
#[serde(default)]
pub redact: Option<RedactMode>,
}
impl Default for LangSettings {
fn default() -> Self {
Self {
top: 0,
files: false,
children: ChildrenMode::Collapse,
redact: None,
}
}
}
fn default_children_mode() -> ChildrenMode {
ChildrenMode::Collapse
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleSettings {
#[serde(default)]
pub top: usize,
#[serde(default = "default_module_roots")]
pub module_roots: Vec<String>,
#[serde(default = "default_module_depth")]
pub module_depth: usize,
#[serde(default = "default_child_include_mode")]
pub children: ChildIncludeMode,
#[serde(default)]
pub redact: Option<RedactMode>,
}
fn default_module_roots() -> Vec<String> {
vec!["crates".to_string(), "packages".to_string()]
}
fn default_module_depth() -> usize {
2
}
fn default_child_include_mode() -> ChildIncludeMode {
ChildIncludeMode::Separate
}
impl Default for ModuleSettings {
fn default() -> Self {
Self {
top: 0,
module_roots: default_module_roots(),
module_depth: default_module_depth(),
children: default_child_include_mode(),
redact: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportSettings {
#[serde(default = "default_export_format")]
pub format: ExportFormat,
#[serde(default = "default_module_roots")]
pub module_roots: Vec<String>,
#[serde(default = "default_module_depth")]
pub module_depth: usize,
#[serde(default = "default_child_include_mode")]
pub children: ChildIncludeMode,
#[serde(default)]
pub min_code: usize,
#[serde(default)]
pub max_rows: usize,
#[serde(default = "default_redact_mode")]
pub redact: RedactMode,
#[serde(default = "default_meta")]
pub meta: bool,
#[serde(default)]
pub strip_prefix: Option<String>,
}
fn default_redact_mode() -> RedactMode {
RedactMode::None
}
fn default_export_format() -> ExportFormat {
ExportFormat::Jsonl
}
fn default_meta() -> bool {
true
}
impl Default for ExportSettings {
fn default() -> Self {
Self {
format: default_export_format(),
module_roots: default_module_roots(),
module_depth: default_module_depth(),
children: default_child_include_mode(),
min_code: 0,
max_rows: 0,
redact: RedactMode::None,
meta: true,
strip_prefix: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyzeSettings {
#[serde(default = "default_preset")]
pub preset: String,
#[serde(default)]
pub window: Option<usize>,
#[serde(default)]
pub git: Option<bool>,
#[serde(default)]
pub max_files: Option<usize>,
#[serde(default)]
pub max_bytes: Option<u64>,
#[serde(default)]
pub max_file_bytes: Option<u64>,
#[serde(default)]
pub max_commits: Option<usize>,
#[serde(default)]
pub max_commit_files: Option<usize>,
#[serde(default = "default_granularity")]
pub granularity: String,
#[serde(default)]
pub effort_model: Option<String>,
#[serde(default)]
pub effort_layer: Option<String>,
#[serde(default)]
pub effort_base_ref: Option<String>,
#[serde(default)]
pub effort_head_ref: Option<String>,
#[serde(default)]
pub effort_monte_carlo: Option<bool>,
#[serde(default)]
pub effort_mc_iterations: Option<usize>,
#[serde(default)]
pub effort_mc_seed: Option<u64>,
}
fn default_preset() -> String {
"receipt".to_string()
}
fn default_granularity() -> String {
"module".to_string()
}
impl Default for AnalyzeSettings {
fn default() -> Self {
Self {
preset: default_preset(),
window: None,
git: None,
max_files: None,
max_bytes: None,
max_file_bytes: None,
max_commits: None,
max_commit_files: None,
granularity: default_granularity(),
effort_model: None,
effort_layer: None,
effort_base_ref: None,
effort_head_ref: None,
effort_monte_carlo: None,
effort_mc_iterations: None,
effort_mc_seed: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CockpitSettings {
#[serde(default = "default_cockpit_base")]
pub base: String,
#[serde(default = "default_cockpit_head")]
pub head: String,
#[serde(default = "default_cockpit_range_mode")]
pub range_mode: String,
#[serde(default)]
pub baseline: Option<String>,
}
fn default_cockpit_base() -> String {
"main".to_string()
}
fn default_cockpit_head() -> String {
"HEAD".to_string()
}
fn default_cockpit_range_mode() -> String {
"two-dot".to_string()
}
impl Default for CockpitSettings {
fn default() -> Self {
Self {
base: default_cockpit_base(),
head: default_cockpit_head(),
range_mode: default_cockpit_range_mode(),
baseline: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DiffSettings {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct TomlConfig {
pub scan: ScanConfig,
pub module: ModuleConfig,
pub export: ExportConfig,
pub analyze: AnalyzeConfig,
pub context: ContextConfig,
pub badge: BadgeConfig,
pub gate: GateConfig,
#[serde(default)]
pub view: BTreeMap<String, ViewProfile>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ScanConfig {
pub paths: Option<Vec<String>>,
pub exclude: Option<Vec<String>>,
pub hidden: Option<bool>,
pub config: Option<String>,
pub no_ignore: Option<bool>,
pub no_ignore_parent: Option<bool>,
pub no_ignore_dot: Option<bool>,
pub no_ignore_vcs: Option<bool>,
pub doc_comments: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ModuleConfig {
pub roots: Option<Vec<String>>,
pub depth: Option<usize>,
pub children: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ExportConfig {
pub min_code: Option<usize>,
pub max_rows: Option<usize>,
pub redact: Option<String>,
pub format: Option<String>,
pub children: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct AnalyzeConfig {
pub preset: Option<String>,
pub window: Option<usize>,
pub format: Option<String>,
pub git: Option<bool>,
pub max_files: Option<usize>,
pub max_bytes: Option<u64>,
pub max_file_bytes: Option<u64>,
pub max_commits: Option<usize>,
pub max_commit_files: Option<usize>,
pub granularity: Option<String>,
pub effort_model: Option<String>,
pub effort_layer: Option<String>,
pub effort_base_ref: Option<String>,
pub effort_head_ref: Option<String>,
pub effort_monte_carlo: Option<bool>,
pub effort_mc_iterations: Option<usize>,
pub effort_mc_seed: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ContextConfig {
pub budget: Option<String>,
pub strategy: Option<String>,
pub rank_by: Option<String>,
pub output: Option<String>,
pub compress: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct BadgeConfig {
pub metric: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct GateConfig {
pub policy: Option<String>,
pub baseline: Option<String>,
pub preset: Option<String>,
pub fail_fast: Option<bool>,
pub rules: Option<Vec<GateRule>>,
pub ratchet: Option<Vec<RatchetRuleConfig>>,
pub allow_missing_baseline: Option<bool>,
pub allow_missing_current: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatchetRuleConfig {
pub pointer: String,
#[serde(default)]
pub max_increase_pct: Option<f64>,
#[serde(default)]
pub max_value: Option<f64>,
#[serde(default)]
pub level: Option<String>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateRule {
pub name: String,
pub pointer: String,
pub op: String,
#[serde(default)]
pub value: Option<serde_json::Value>,
#[serde(default)]
pub values: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub negate: bool,
#[serde(default)]
pub level: Option<String>,
#[serde(default)]
pub message: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ViewProfile {
pub format: Option<String>,
pub top: Option<usize>,
pub files: Option<bool>,
pub module_roots: Option<Vec<String>>,
pub module_depth: Option<usize>,
pub min_code: Option<usize>,
pub max_rows: Option<usize>,
pub redact: Option<String>,
pub meta: Option<bool>,
pub children: Option<String>,
pub preset: Option<String>,
pub window: Option<usize>,
pub budget: Option<String>,
pub strategy: Option<String>,
pub rank_by: Option<String>,
pub output: Option<String>,
pub compress: Option<bool>,
pub metric: Option<String>,
}
impl TomlConfig {
pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
toml::from_str(s)
}
pub fn from_file(path: &Path) -> std::io::Result<Self> {
let content = std::fs::read_to_string(path)?;
toml::from_str(&content)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
}
pub type TomlResult<T> = Result<T, toml::de::Error>;
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn scan_options_default() {
let opts = ScanOptions::default();
assert!(opts.excluded.is_empty());
assert!(!opts.hidden);
assert!(!opts.no_ignore);
}
#[test]
fn scan_settings_current_dir() {
let s = ScanSettings::current_dir();
assert_eq!(s.paths, vec!["."]);
}
#[test]
fn scan_settings_for_paths() {
let s = ScanSettings::for_paths(vec!["src".into(), "lib".into()]);
assert_eq!(s.paths.len(), 2);
}
#[test]
fn scan_settings_flatten() {
let s = ScanSettings {
paths: vec![".".into()],
options: ScanOptions {
hidden: true,
..Default::default()
},
};
assert!(s.options.hidden);
}
#[test]
fn serde_roundtrip_scan_options() {
let opts = ScanOptions {
excluded: vec!["target".into()],
config: ConfigMode::None,
hidden: true,
no_ignore: false,
no_ignore_parent: true,
no_ignore_dot: false,
no_ignore_vcs: true,
treat_doc_strings_as_comments: true,
};
let json = serde_json::to_string(&opts).unwrap();
let back: ScanOptions = serde_json::from_str(&json).unwrap();
assert_eq!(back.excluded, opts.excluded);
assert!(back.hidden);
assert!(back.no_ignore_parent);
assert!(back.no_ignore_vcs);
assert!(back.treat_doc_strings_as_comments);
}
#[test]
fn serde_roundtrip_scan_settings() {
let s = ScanSettings {
paths: vec!["src".into()],
options: ScanOptions {
excluded: vec!["*.bak".into()],
..Default::default()
},
};
let json = serde_json::to_string(&s).unwrap();
let back: ScanSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back.paths, s.paths);
assert_eq!(back.options.excluded, s.options.excluded);
}
#[test]
fn serde_roundtrip_lang_settings() {
let s = LangSettings {
top: 10,
files: true,
children: ChildrenMode::Separate,
redact: Some(RedactMode::Paths),
};
let json = serde_json::to_string(&s).unwrap();
let back: LangSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back.top, 10);
assert!(back.files);
}
#[test]
fn serde_roundtrip_export_settings() {
let s = ExportSettings::default();
let json = serde_json::to_string(&s).unwrap();
let back: ExportSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back.min_code, 0);
assert!(back.meta);
}
#[test]
fn serde_roundtrip_analyze_settings() {
let s = AnalyzeSettings::default();
let json = serde_json::to_string(&s).unwrap();
let back: AnalyzeSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back.preset, "receipt");
assert_eq!(back.granularity, "module");
}
#[test]
fn serde_roundtrip_cockpit_settings() {
let s = CockpitSettings::default();
let json = serde_json::to_string(&s).unwrap();
let back: CockpitSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back.base, "main");
assert_eq!(back.head, "HEAD");
assert_eq!(back.range_mode, "two-dot");
assert!(back.baseline.is_none());
}
#[test]
fn serde_roundtrip_cockpit_settings_with_baseline() {
let s = CockpitSettings {
base: "v1.0".into(),
head: "feature".into(),
range_mode: "three-dot".into(),
baseline: Some("baseline.json".into()),
};
let json = serde_json::to_string(&s).unwrap();
let back: CockpitSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back.base, "v1.0");
assert_eq!(back.baseline, Some("baseline.json".to_string()));
}
#[test]
fn serde_roundtrip_diff_settings() {
let s = DiffSettings {
from: "v1.0".into(),
to: "v2.0".into(),
};
let json = serde_json::to_string(&s).unwrap();
let back: DiffSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back.from, "v1.0");
assert_eq!(back.to, "v2.0");
}
#[test]
fn toml_parse_and_view_profiles() {
let toml_str = r#"
[scan]
hidden = true
[view.llm]
format = "json"
top = 10
"#;
let config = TomlConfig::parse(toml_str).expect("parse config");
assert_eq!(config.scan.hidden, Some(true));
let llm = config.view.get("llm").expect("llm profile");
assert_eq!(llm.format.as_deref(), Some("json"));
assert_eq!(llm.top, Some(10));
}
#[test]
fn toml_from_file_roundtrip() {
let toml_content = r#"
[module]
depth = 3
roots = ["src", "tests"]
"#;
let mut temp_file = NamedTempFile::new().expect("temp file");
temp_file
.write_all(toml_content.as_bytes())
.expect("write config");
let config = TomlConfig::from_file(temp_file.path()).expect("load config");
assert_eq!(config.module.depth, Some(3));
assert_eq!(
config.module.roots,
Some(vec!["src".to_string(), "tests".to_string()])
);
}
}