use crate::types::describe::SObjectDescribe;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LintSeverity {
Warning,
Info,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LintResult {
pub rule_name: String,
pub severity: LintSeverity,
pub message: String,
}
pub trait LintRule {
fn evaluate(&self, describe: &SObjectDescribe) -> Vec<LintResult>;
}
pub struct TooManyFieldsRule {
pub max_fields: usize,
}
impl Default for TooManyFieldsRule {
fn default() -> Self {
Self { max_fields: 100 }
}
}
impl LintRule for TooManyFieldsRule {
fn evaluate(&self, describe: &SObjectDescribe) -> Vec<LintResult> {
let count = describe.fields.len();
if count > self.max_fields {
vec![LintResult {
rule_name: "TooManyFields".to_string(),
severity: LintSeverity::Warning,
message: format!(
"SObject '{}' has {} fields, which exceeds the recommended maximum of {}.",
describe.name, count, self.max_fields
),
}]
} else {
vec![]
}
}
}
pub struct MissingCustomSuffixRule;
impl LintRule for MissingCustomSuffixRule {
fn evaluate(&self, describe: &SObjectDescribe) -> Vec<LintResult> {
let mut results = Vec::new();
for field in &describe.fields {
if field.custom && !field.name.ends_with("__c") {
results.push(LintResult {
rule_name: "MissingCustomSuffix".to_string(),
severity: LintSeverity::Warning,
message: format!(
"Custom field '{}' in SObject '{}' does not end with '__c'.",
field.name, describe.name
),
});
}
}
results
}
}
pub struct SchemaLinter {
rules: Vec<Box<dyn LintRule>>,
}
impl SchemaLinter {
#[must_use]
pub fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn add_rule<R: LintRule + 'static>(&mut self, rule: R) {
self.rules.push(Box::new(rule));
}
#[must_use]
pub fn lint(&self, describe: &SObjectDescribe) -> Vec<LintResult> {
let mut results = Vec::with_capacity(self.rules.len());
for rule in &self.rules {
results.extend(rule.evaluate(describe));
}
results
}
}
impl Default for SchemaLinter {
fn default() -> Self {
let mut linter = Self::new();
linter.add_rule(TooManyFieldsRule::default());
linter.add_rule(MissingCustomSuffixRule);
linter
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::Must;
use serde_json::json;
fn create_mock_describe(
name: &str,
fields_count: usize,
custom_count: usize,
missing_suffix: bool,
) -> SObjectDescribe {
let mut fields = Vec::new();
for i in 0..fields_count {
let is_custom = i < custom_count;
let suffix = if is_custom && !(missing_suffix && i == 0) {
"__c"
} else {
""
};
fields.push(json!({
"name": format!("Field{}{}", i, suffix),
"type": "string",
"label": format!("Field {}", i),
"referenceTo": [],
"custom": is_custom,
"calculated": false,
"nillable": true,
"defaultedOnCreate": false,
"createable": true, "autoNumber": false, "aggregatable": true, "byteLength": 18,
"cascadeDelete": false, "caseSensitive": false,
"dependentPicklist": false, "deprecatedAndHidden": false,
"digits": 0, "displayLocationInDecimal": false, "encrypted": false, "externalId": false,
"filterable": true, "groupable": true, "highScaleNumber": false, "htmlFormatted": false,
"idLookup": false, "length": 18, "nameField": false, "namePointing": false,
"permissionable": false, "polymorphicForeignKey": false, "precision": 0, "queryByDistance": false,
"restrictedDelete": false, "restrictedPicklist": false, "scale": 0, "soapType": "tns:ID",
"sortable": true, "unique": false, "updateable": false, "writeRequiresMasterRead": false
}));
}
let describe_json = json!({
"name": name,
"label": name,
"custom": true,
"queryable": true,
"activateable": false, "createable": true, "customSetting": false, "deletable": true,
"deprecatedAndHidden": false, "feedEnabled": true, "hasSubtypes": false,
"isSubtype": false, "keyPrefix": "001", "labelPlural": format!("{}s", name), "layoutable": true,
"mergeable": true, "mruEnabled": true, "replicateable": true, "retrieveable": true,
"searchable": true, "triggerable": true, "undeletable": true, "updateable": true,
"urls": {}, "childRelationships": [], "recordTypeInfos": [],
"fields": fields
});
serde_json::from_value(describe_json).must()
}
#[test]
fn test_too_many_fields_rule() {
let describe = create_mock_describe("FatObject__c", 105, 5, false);
let rule = TooManyFieldsRule::default();
let results = rule.evaluate(&describe);
assert_eq!(results.len(), 1);
assert_eq!(results[0].rule_name, "TooManyFields");
assert_eq!(results[0].severity, LintSeverity::Warning);
assert!(results[0].message.contains("has 105 fields"));
}
#[test]
fn test_missing_custom_suffix_rule() {
let describe = create_mock_describe("BadObject__c", 5, 2, true);
let rule = MissingCustomSuffixRule;
let results = rule.evaluate(&describe);
assert_eq!(results.len(), 1);
assert_eq!(results[0].rule_name, "MissingCustomSuffix");
assert_eq!(results[0].severity, LintSeverity::Warning);
assert!(results[0].message.contains("does not end with '__c'"));
}
#[test]
fn test_schema_linter_default() {
let describe_clean = create_mock_describe("CleanObject__c", 10, 5, false);
let describe_dirty = create_mock_describe("GodObject__c", 150, 100, true);
let linter = SchemaLinter::default();
let clean_results = linter.lint(&describe_clean);
assert!(
clean_results.is_empty(),
"Clean object should have no findings"
);
let dirty_results = linter.lint(&describe_dirty);
assert_eq!(
dirty_results.len(),
2,
"God object should trigger multiple rules"
);
let rule_names: Vec<String> = dirty_results.into_iter().map(|r| r.rule_name).collect();
assert!(rule_names.contains(&"TooManyFields".to_string()));
assert!(rule_names.contains(&"MissingCustomSuffix".to_string()));
}
}