use std::env;
use std::fs;
use std::path::Path;
const EXPECTED_DEFAULT_PARAMS_FIELDS: usize = 85;
const EXPECTED_MODEL_SETTINGS_FIELDS: usize = 75;
const EXPECTED_MODEL_OVERRIDE_FIELDS: usize = 70;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let src_dir = Path::new(&crate_dir).join("src");
let default_params_count = count_struct_fields(&src_dir, "config.rs", "DefaultParams");
let model_settings_count = count_struct_fields(&src_dir, "models.rs", "ModelSettings");
let model_override_count = count_struct_fields(&src_dir, "config.rs", "ModelOverride");
let mut errors = Vec::new();
if default_params_count != EXPECTED_DEFAULT_PARAMS_FIELDS {
errors.push((
"DefaultParams",
default_params_count,
EXPECTED_DEFAULT_PARAMS_FIELDS,
vec![
" 1. src/config.rs — DefaultParams struct field definition",
" 2. src/config.rs — DefaultParams Default impl (default values)",
" 3. src/models.rs — ModelSettings struct field (if shared)",
" 4. src/models.rs — From<DefaultParams> for ModelSettings",
" 5. src/config.rs — ModelOverride struct field (if shared)",
" 6. src/config.rs — ModelOverride::from_settings()",
" 7. src/config.rs — ModelOverride::apply() (macro call)",
" 8. src/tui/settings.rs — all_fields() SettingField entry",
" 9. src/tui/settings.rs — profile_settings_parts() diff macro",
"10. src/tui/app/profiles.rs — settings_fingerprint()",
],
));
}
if model_settings_count != EXPECTED_MODEL_SETTINGS_FIELDS {
errors.push((
"ModelSettings",
model_settings_count,
EXPECTED_MODEL_SETTINGS_FIELDS,
vec![
" 1. src/models.rs — ModelSettings struct field",
" 2. src/models.rs — From<DefaultParams> for ModelSettings",
" 3. src/config.rs — DefaultParams struct (if shared)",
" 4. src/config.rs — DefaultParams Default impl (if shared)",
" 5. src/config.rs — ModelOverride struct (if shared)",
" 6. src/config.rs — ModelOverride::from_settings()",
" 7. src/config.rs — ModelOverride::apply() (macro call)",
" 8. src/tui/settings.rs — all_fields() SettingField entry",
" 9. src/tui/settings.rs — profile_settings_parts() diff macro",
"10. src/tui/app/profiles.rs — settings_fingerprint()",
"11. src/tui/event/helpers.rs — sync_global_settings() (if global)",
],
));
}
if model_override_count != EXPECTED_MODEL_OVERRIDE_FIELDS {
errors.push((
"ModelOverride",
model_override_count,
EXPECTED_MODEL_OVERRIDE_FIELDS,
vec![
" 1. src/config.rs — ModelOverride struct field (Option<T>)",
" 2. src/config.rs — ModelOverride::from_settings()",
" 3. src/config.rs — ModelOverride::apply() (macro call)",
" 4. src/tui/settings.rs — profile_settings_parts() diff macro",
],
));
}
if !errors.is_empty() {
eprintln!("\nERROR: Parameter struct field count mismatch!\n");
for (name, actual, expected, locations) in &errors {
eprintln!(" {} has {} fields (expected {})", name, actual, expected);
eprintln!(" When adding a field, update:");
for loc in locations {
eprintln!("{}", loc);
}
eprintln!();
}
eprintln!(
"The derived PartialEq on ModelSettings and DefaultParams provides\n\
compile-time guarantees for is_dirty() correctness.\n\
This build script provides an additional runtime check on field counts."
);
std::process::exit(1);
}
}
fn count_struct_fields(src_dir: &Path, file: &str, struct_name: &str) -> usize {
let file_path = src_dir.join(file);
let content = fs::read_to_string(&file_path).unwrap_or_default();
let _struct_line_num = content
.lines()
.position(|l| {
l.trim().starts_with(&format!("pub struct {}", struct_name))
|| l.trim().starts_with(&format!("pub struct {}", struct_name))
})
.unwrap_or_else(|| {
eprintln!("WARNING: Could not find struct {} in {}", struct_name, file);
panic!("Could not find struct {}", struct_name);
});
let after_struct = &content[content
.find(&format!("pub struct {}", struct_name))
.unwrap_or(0)..];
let brace_offset = after_struct.find('{').unwrap_or_else(|| {
eprintln!(
"WARNING: Could not find opening brace for struct {} in {}",
struct_name, file
);
panic!("Could not find opening brace for struct {}", struct_name);
});
let abs_start = content
.find(&format!("pub struct {}", struct_name))
.unwrap_or(0)
+ brace_offset;
let end_idx = find_matching_brace(&content, abs_start);
let body = &content[abs_start + 1..end_idx];
body.lines()
.filter(|line| {
let trimmed = line.trim();
trimmed.starts_with("pub ")
&& trimmed.contains(':')
&& !trimmed.starts_with("//")
&& !trimmed.starts_with("#")
})
.count()
}
fn find_matching_brace(content: &str, start: usize) -> usize {
let mut depth = 0;
let mut i = start;
if content.as_bytes().get(start) == Some(&b'{') {
depth = 1;
i += 1;
}
while i < content.len() && depth > 0 {
let byte = content.as_bytes()[i];
match byte {
b'{' => depth += 1,
b'}' => depth -= 1,
b'"' => {
i += 1;
while i < content.len() {
if content.as_bytes()[i] == b'"' {
break;
}
if content.as_bytes()[i] == b'\\' {
i += 1; }
i += 1;
}
}
b'/' if content.as_bytes().get(i + 1) == Some(&b'/') => {
while i < content.len() && content.as_bytes()[i] != b'\n' {
i += 1;
}
}
b'/' if content.as_bytes().get(i + 1) == Some(&b'*') => {
i += 2;
while i + 1 < content.len() {
if content.as_bytes()[i] == b'*' && content.as_bytes()[i + 1] == b'/' {
i += 2;
break;
}
i += 1;
}
}
_ => {}
}
i += 1;
}
i
}