use std::collections::BTreeMap;
use cratestack_core::route_naming::to_snake_case;
use cratestack_core::{Field, Model, SourceSpan};
use crate::diagnostics::{SchemaError, span_error};
fn find_snake_case_collision<'a>(
entries: impl IntoIterator<Item = (&'a str, SourceSpan)>,
) -> Option<(&'a str, &'a str, SourceSpan, String)> {
let mut seen: BTreeMap<String, &str> = BTreeMap::new();
for (name, span) in entries {
let normalized = to_snake_case(name);
match seen.get(normalized.as_str()) {
Some(&existing) if existing != name => {
return Some((existing, name, span, normalized));
}
Some(_) => continue,
None => {
seen.insert(normalized, name);
}
}
}
None
}
pub(super) fn validate_field_column_collisions(
fields: &[Field],
owner_kind: &str,
owner_name: &str,
) -> Result<(), SchemaError> {
let entries = fields.iter().map(|field| (field.name.as_str(), field.span));
if let Some((existing, colliding, span, normalized)) = find_snake_case_collision(entries) {
return Err(span_error(
format!(
"field `{colliding}` on {owner_kind} `{owner_name}` collides with field \
`{existing}` — both normalize to the SQL/codegen column name `{normalized}` \
(see `cratestack_core::route_naming::to_snake_case`); rename one of them",
),
span,
));
}
Ok(())
}
pub(super) fn validate_model_name_collisions(models: &[Model]) -> Result<(), SchemaError> {
let entries = models
.iter()
.map(|model| (model.name.as_str(), model.name_span));
if let Some((existing, colliding, span, normalized)) = find_snake_case_collision(entries) {
return Err(span_error(
format!(
"model `{colliding}` collides with model `{existing}` — both normalize to \
`{normalized}` for the generated SQL table name, Rust accessor constant, and \
REST route path (see `cratestack_core::route_naming::to_snake_case`); rename \
one of them",
),
span,
));
}
Ok(())
}