use heck::ToSnakeCase;
use super::dto::{create_columns, update_columns};
use super::feature::{Bodies, Endpoint, Feature};
use super::layout::{Uses, push_call};
use crate::generator::keys::referenced_table;
use crate::generator::{insert_field_type, param_type};
use crate::schema::{Column, Table};
const FIELD_FAILURES: &str = "crate::shared::validation::FieldFailures";
#[derive(Debug, Clone)]
pub struct Reference<'a> {
pub column: &'a Column,
pub target: &'a Table,
pub owned: bool,
pub bodies: Bodies,
}
impl Reference<'_> {
pub fn singular(&self) -> String {
self.target.struct_name.to_snake_case().replace('_', " ")
}
}
pub fn bodies_holding(feature: &Feature<'_>, columns: &[&Column]) -> Bodies {
let holds = |body: &[&Column]| {
columns
.iter()
.all(|column| body.iter().any(|c| c.sql_name == column.sql_name))
};
Bodies {
create: feature.serves(Endpoint::Create) && holds(&create_columns(feature)),
update: feature.serves(Endpoint::Update)
&& !feature.table.key_columns().is_empty()
&& holds(&update_columns(feature)),
}
}
pub fn references<'a>(
feature: &Feature<'a>,
features: &[Feature<'a>],
tables: &'a [Table],
) -> Vec<Reference<'a>> {
let mut tenant: Option<&Column> = feature.tenant;
let mut references = Vec::new();
for foreign_key in &feature.table.foreign_keys {
let Some(column) = feature
.table
.columns
.iter()
.find(|column| column.sql_name == foreign_key.column)
else {
continue;
};
let Some(target) = referenced_table(tables, foreign_key) else {
continue;
};
let keys = target.key_columns();
let [key] = keys.as_slice() else {
continue;
};
let typed = key.sql_name == foreign_key.referenced_column
&& key.value_type() == column.value_type()
&& (column.is_copy() || param_type(column).starts_with('&'));
let bodies = bodies_holding(feature, &[column]);
if !typed || !bodies.any() {
continue;
}
if let Some(theirs) = target.tenant() {
match tenant {
Some(ours) if ours.value_type() != theirs.value_type() => continue,
Some(_) => {}
None if theirs.is_copy() => tenant = Some(theirs),
None => continue,
}
}
let owned = feature.knows_caller()
&& features
.iter()
.any(|other| std::ptr::eq(other.table, target) && other.reads_owned_rows());
references.push(Reference {
column,
target,
owned,
bodies,
});
}
references
}
fn checked<'f, 'a>(feature: &'f Feature<'a>, endpoint: Endpoint) -> Vec<&'f Reference<'a>> {
feature
.references
.iter()
.filter(|reference| reference.bodies.holds(endpoint))
.collect()
}
fn runs_any(feature: &Feature<'_>, endpoint: Endpoint) -> bool {
!checked(feature, endpoint).is_empty()
|| feature
.unique_checks
.iter()
.any(|check| check.bodies.holds(endpoint))
}
pub fn import(uses: &mut Uses, feature: &Feature<'_>, endpoint: Endpoint) {
if runs_any(feature, endpoint) {
uses.add(FIELD_FAILURES);
}
for reference in checked(feature, endpoint) {
uses.add(&format!(
"{}::{}",
feature.db_path, reference.target.module_name
));
}
}
pub fn push_checks(output: &mut String, feature: &Feature<'_>, endpoint: Endpoint) {
if !runs_any(feature, endpoint) {
return;
}
output.push_str(" let mut failures = FieldFailures::default();\n");
for reference in checked(feature, endpoint) {
push_reference(output, endpoint, reference);
}
super::unique::push_checks(output, feature, endpoint);
output.push_str(" failures.into_result()?;\n");
}
pub struct BodyField {
pub expr: String,
pub options: usize,
}
impl BodyField {
pub fn of(column: &Column, endpoint: Endpoint) -> Self {
let options = if endpoint == Endpoint::Create {
insert_field_type(column).matches("Option<").count()
} else {
1 + usize::from(column.nullable)
};
let borrow = if param_type(column).starts_with('&') {
"&"
} else {
""
};
BodyField {
expr: format!("{borrow}input.{}", column.rust_name),
options,
}
}
pub fn binding(&self, binding: &str) -> String {
format!(
"let {}{binding}{} = {}",
"Some(".repeat(self.options),
")".repeat(self.options),
self.expr
)
}
}
fn push_reference(output: &mut String, endpoint: Endpoint, reference: &Reference<'_>) {
let column = reference.column;
let field = BodyField::of(column, endpoint);
let module = &reference.target.module_name;
let mut args = vec!["&mut *conn".to_owned()];
if reference.target.tenant().is_some() {
args.push("tenant".to_owned());
}
let indent = if field.options == 0 {
args.push(field.expr.clone());
8
} else {
output.push_str(&format!(" if {} {{\n", field.binding("value")));
args.push("value".to_owned());
12
};
let callee = if reference.owned {
args.push("user.id".to_owned());
format!("{module}::listing::exists_owned")
} else {
format!("{module}::read::exists_by_id")
};
let pad = " ".repeat(indent);
push_call(output, indent, "let exists = ", &callee, &args, ".await?;");
output.push_str(&format!("{pad}if !exists {{\n"));
let missing = [
format!("\"{}\"", column.sql_name),
format!("\"{}\"", reference.singular()),
];
push_call(output, indent + 4, "", "failures.missing", &missing, ";");
output.push_str(&format!("{pad}}}\n"));
if field.options > 0 {
output.push_str(" }\n");
}
}