use logisheets_base::{BlockCellId, SheetId};
use super::enum_manager::EnumSetManager;
use super::schema_manager::SchemaManager;
use super::schema_manager::field_type::FieldType;
use super::schema_manager::manager::BlockFieldView;
use super::schema_manager::schema::SchemaTrait;
fn lit(v: &str) -> String {
v.replace('"', "\"\"")
}
const IS_EMPTY: &str = "ISBLANK(#PLACEHOLDER)";
fn required_rule() -> String {
format!("NOT({IS_EMPTY})")
}
fn unique_rule(sheet_id: SheetId, block_id: usize, field: &str) -> String {
format!(
"OR({IS_EMPTY},COUNTIF(BLOCKREFSB({sheet_id},{block_id},\"*\",\"{}\"),#PLACEHOLDER)=1)",
lit(field)
)
}
fn unique_together_rule(
sheet_id: SheetId,
block_id: usize,
this_field: &str,
group: &[String],
) -> Option<String> {
if group.len() < 2 || !group.iter().any(|f| f == this_field) {
return None;
}
let col = |f: &str| format!("BLOCKREFSB({sheet_id},{block_id},\"*\",\"{}\")", lit(f));
let value_of = |f: &str| {
if f == this_field {
"#PLACEHOLDER".to_string()
} else {
format!("#FIELD(\"{}\")", lit(f))
}
};
let pairs = group
.iter()
.map(|f| format!("{},{}", col(f), value_of(f)))
.collect::<Vec<_>>()
.join(",");
let blanks = group
.iter()
.map(|f| format!("ISBLANK({})", value_of(f)))
.collect::<Vec<_>>()
.join(",");
Some(format!("OR({blanks},COUNTIFS({pairs})=1)"))
}
fn membership_rule(variant_ids: &[&str]) -> Option<String> {
if variant_ids.is_empty() {
return None;
}
let clauses: Vec<String> = variant_ids
.iter()
.map(|v| format!("EXACT(#PLACEHOLDER,\"{}\")", lit(v)))
.collect();
Some(format!("OR({IS_EMPTY},{})", clauses.join(",")))
}
fn reference_rule(sheet_id: SheetId, block_id: usize, field: &str) -> String {
format!(
"OR({IS_EMPTY},COUNTIF(BLOCKREFSB({sheet_id},{block_id},\"*\",\"{}\"),#PLACEHOLDER)>=1)",
lit(field)
)
}
pub fn derived_rules(
enums: &EnumSetManager,
sheet_id: SheetId,
block_id: usize,
field: &BlockFieldView<'_>,
unique_together: &[Vec<String>],
) -> Vec<String> {
let mut out = Vec::new();
if field.required {
out.push(required_rule());
}
if field.unique {
out.push(unique_rule(sheet_id, block_id, field.name));
}
for group in unique_together {
if let Some(rule) = unique_together_rule(sheet_id, block_id, field.name, group) {
out.push(rule);
}
}
match field.field_type {
FieldType::Enum { set_id } | FieldType::MultiSelect { set_id } => {
if matches!(field.field_type, FieldType::Enum { .. }) {
if let Some(set) = enums.get(set_id) {
if let Some(rule) = membership_rule(&set.variant_ids()) {
out.push(rule);
}
}
}
}
FieldType::FieldRef {
sheet_id: target_sheet,
block_id: target_block,
field_name,
} => {
out.push(reference_rule(*target_sheet, *target_block, field_name));
}
FieldType::MultiSelectRef { .. } => {}
FieldType::Unspecified
| FieldType::Text
| FieldType::Number
| FieldType::Boolean
| FieldType::Datetime
| FieldType::Image => {}
}
out
}
pub fn effective_validation(
schema: &SchemaManager,
enums: &EnumSetManager,
sheet_id: SheetId,
cell: &BlockCellId,
analysis: Option<crate::block_manager::analysis::AnalysisTarget<'_>>,
) -> Option<String> {
if schema.is_header_cell(sheet_id, cell) {
return None;
}
if let Some(target) = analysis {
if let Some(pivot) = target.pivot {
if schema.is_key_cell(sheet_id, cell) {
return Some(crate::block_manager::analysis::pivot_key_validation(
sheet_id,
target.source,
pivot,
));
}
}
}
let field = schema.field_view_for_block_cell(sheet_id, cell)?;
let groups = schema
.schemas
.get(&(sheet_id, cell.block_id))
.map(|s| s.unique_together().to_vec())
.unwrap_or_default();
let mut parts = derived_rules(enums, sheet_id, cell.block_id, &field, &groups);
if let Some(own) = field.validation_formula {
let own = own.trim();
if !own.is_empty() {
parts.push(own.to_string());
}
}
match parts.len() {
0 => None,
1 => Some(parts.pop().unwrap()),
_ => Some(format!("AND({})", parts.join(","))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block_manager::enum_manager::{EnumSet, EnumVariant};
fn enums_with(id: &str, ids: &[&str]) -> EnumSetManager {
let mut m = EnumSetManager::new();
m.upsert(EnumSet {
id: id.to_string(),
name: String::new(),
variants: ids
.iter()
.map(|v| EnumVariant {
id: v.to_string(),
label: v.to_string(),
})
.collect(),
});
m
}
fn field<'a>(
name: &'a str,
ty: &'a FieldType,
required: bool,
unique: bool,
) -> BlockFieldView<'a> {
BlockFieldView {
pivot_column: None,
name,
field_type: ty,
required,
unique,
validation_formula: None,
aggregate: None,
}
}
#[test]
fn a_field_that_declares_nothing_derives_nothing() {
let ty = FieldType::Text;
let rules = derived_rules(
&EnumSetManager::new(),
0,
1,
&field("a", &ty, false, false),
&[],
);
assert!(rules.is_empty());
}
#[test]
fn required_is_the_only_rule_that_is_about_emptiness() {
let ty = FieldType::Text;
let rules = derived_rules(
&EnumSetManager::new(),
0,
1,
&field("a", &ty, true, false),
&[],
);
assert_eq!(rules, vec!["NOT(ISBLANK(#PLACEHOLDER))"]);
}
#[test]
fn unique_counts_by_id_and_exempts_the_empty_cell() {
let ty = FieldType::Text;
let rules = derived_rules(
&EnumSetManager::new(),
7,
3,
&field("email", &ty, false, true),
&[],
);
assert_eq!(
rules,
vec![
"OR(ISBLANK(#PLACEHOLDER),COUNTIF(BLOCKREFSB(7,3,\"*\",\"email\"),#PLACEHOLDER)=1)"
]
);
}
#[test]
fn a_quote_in_a_name_is_escaped_rather_than_closing_the_literal() {
let ty = FieldType::Text;
let rules = derived_rules(
&EnumSetManager::new(),
0,
1,
&field("say \"hi\"", &ty, false, true),
&[],
);
assert!(
rules[0].contains("\"say \"\"hi\"\"\""),
"expected a doubled quote, got {}",
rules[0]
);
}
#[test]
fn an_enum_whitelists_its_options_case_sensitively() {
let ty = FieldType::Enum {
set_id: "status".into(),
};
let rules = derived_rules(
&enums_with("status", &["open", "done"]),
0,
1,
&field("s", &ty, false, false),
&[],
);
assert_eq!(
rules,
vec![
"OR(ISBLANK(#PLACEHOLDER),EXACT(#PLACEHOLDER,\"open\"),EXACT(#PLACEHOLDER,\"done\"))"
]
);
}
#[test]
fn an_enum_whose_set_is_gone_derives_nothing_rather_than_rejecting_everything() {
let ty = FieldType::Enum {
set_id: "status".into(),
};
let rules = derived_rules(
&EnumSetManager::new(),
0,
1,
&field("s", &ty, false, false),
&[],
);
assert!(rules.is_empty());
}
#[test]
fn a_reference_checks_existence_in_the_target_field() {
let ty = FieldType::FieldRef {
sheet_id: 2,
block_id: 9,
field_name: "code".into(),
};
let rules = derived_rules(
&EnumSetManager::new(),
0,
1,
&field("c", &ty, false, false),
&[],
);
assert_eq!(
rules,
vec![
"OR(ISBLANK(#PLACEHOLDER),COUNTIF(BLOCKREFSB(2,9,\"*\",\"code\"),#PLACEHOLDER)>=1)"
]
);
}
#[test]
fn the_multi_valued_forms_derive_nothing_yet() {
for ty in [
FieldType::MultiSelect {
set_id: "status".into(),
},
FieldType::MultiSelectRef {
sheet_id: 0,
block_id: 1,
field_name: "code".into(),
},
] {
let rules = derived_rules(
&enums_with("status", &["open"]),
0,
1,
&field("s", &ty, false, false),
&[],
);
assert!(rules.is_empty(), "{:?} should derive nothing yet", ty);
}
}
#[test]
fn several_constraints_are_anded_with_the_authors_own_rule_last() {
let ty = FieldType::Enum {
set_id: "status".into(),
};
let mut f = field("s", &ty, true, true);
f.validation_formula = Some("LEN(#PLACEHOLDER)<10");
let mut parts = derived_rules(&enums_with("status", &["open"]), 0, 1, &f, &[]);
parts.push("LEN(#PLACEHOLDER)<10".into());
let composed = format!("AND({})", parts.join(","));
assert!(composed.starts_with("AND(NOT(ISBLANK(#PLACEHOLDER)),OR("));
assert!(composed.ends_with("LEN(#PLACEHOLDER)<10)"));
}
}