use crate::types::DynamicSObject;
use crate::types::describe::{FieldDescribe, FieldType, SObjectDescribe};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ValidationError {
#[error("Required field is missing or null: {0}")]
MissingRequiredField(String),
#[error("String length for '{0}' exceeds maximum length of {1}")]
LengthExceeded(String, i32),
#[error("Type mismatch for '{0}': expected {1}, found {2}")]
TypeMismatch(String, String, String),
}
#[derive(Debug, Clone)]
pub struct DataValidator<'a> {
describe: &'a SObjectDescribe,
}
impl<'a> DataValidator<'a> {
#[must_use]
pub fn new(describe: &'a SObjectDescribe) -> Self {
Self { describe }
}
pub fn validate(&self, record: &DynamicSObject) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
for field in &self.describe.fields {
if field.createable && !field.nillable && !field.defaulted_on_create {
let value_opt = record.fields.get(&field.name);
if value_opt.is_none_or(serde_json::Value::is_null) {
errors.push(ValidationError::MissingRequiredField(field.name.clone()));
}
}
}
for (key, val) in &record.fields {
if val.is_null() {
continue;
}
if let Some(field) = self.find_field(key) {
if Self::is_string_type(&field.type_) {
if let Value::String(s) = val {
if field.length > 0
&& s.chars().count() > field.length.unsigned_abs() as usize
{
errors.push(ValidationError::LengthExceeded(
field.name.clone(),
field.length,
));
}
} else {
errors.push(ValidationError::TypeMismatch(
field.name.clone(),
"string".to_string(),
Self::value_type_name(val).to_string(),
));
}
} else if field.type_ == FieldType::Boolean {
if !val.is_boolean() {
errors.push(ValidationError::TypeMismatch(
field.name.clone(),
"boolean".to_string(),
Self::value_type_name(val).to_string(),
));
}
} else if Self::is_number_type(&field.type_) {
if !val.is_number() {
errors.push(ValidationError::TypeMismatch(
field.name.clone(),
"number".to_string(),
Self::value_type_name(val).to_string(),
));
}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn find_field(&self, name: &str) -> Option<&FieldDescribe> {
self.describe
.fields
.iter()
.find(|f| f.name.eq_ignore_ascii_case(name))
}
fn is_string_type(field_type: &FieldType) -> bool {
matches!(
field_type,
FieldType::String
| FieldType::Textarea
| FieldType::Email
| FieldType::Phone
| FieldType::Url
| FieldType::Id
| FieldType::Reference
| FieldType::Picklist
| FieldType::Combobox
)
}
fn is_number_type(field_type: &FieldType) -> bool {
matches!(
field_type,
FieldType::Int | FieldType::Double | FieldType::Currency | FieldType::Percent
)
}
fn value_type_name(val: &Value) -> &'static str {
match val {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::must::Must;
use crate::types::{Attributes, SalesforceId};
use serde_json::json;
fn create_mock_describe(fields_json: &serde_json::Value) -> SObjectDescribe {
let describe_json = json!({
"name": "Contact",
"label": "Contact",
"custom": false,
"queryable": true,
"activateable": false, "createable": true, "customSetting": false, "deletable": true,
"deprecatedAndHidden": false, "feedEnabled": true, "hasSubtypes": false,
"isSubtype": false, "keyPrefix": "003", "labelPlural": "Contacts", "layoutable": true,
"mergeable": true, "mruEnabled": true, "replicateable": true, "retrieveable": true,
"searchable": true, "triggerable": true, "undeletable": true, "updateable": true,
"urls": {}, "childRelationships": [], "recordTypeInfos": [],
"fields": fields_json.clone()
});
serde_json::from_value(describe_json).must()
}
fn mock_field(name: &str, field_type: &str, nillable: bool, length: i32) -> serde_json::Value {
json!({
"name": name,
"type": field_type,
"label": format!("{} Label", name),
"referenceTo": [],
"encrypted": false,
"createable": true, "autoNumber": false, "calculated": false,
"aggregatable": true, "byteLength": length * 3, "cascadeDelete": false,
"caseSensitive": false, "custom": false, "defaultedOnCreate": false,
"dependentPicklist": false, "deprecatedAndHidden": false, "digits": 0,
"displayLocationInDecimal": false, "externalId": false, "filterable": true,
"groupable": true, "highScaleNumber": false, "htmlFormatted": false,
"idLookup": false, "length": length, "nameField": false, "namePointing": false,
"nillable": nillable, "permissionable": false, "polymorphicForeignKey": false,
"precision": 0, "queryByDistance": false, "restrictedDelete": false,
"restrictedPicklist": false, "scale": 0, "soapType": "xsd:string",
"sortable": true, "unique": false, "updateable": true,
"writeRequiresMasterRead": false
})
}
fn create_mock_record(fields: serde_json::Map<String, Value>) -> DynamicSObject {
let id = SalesforceId::new("003000000000001AAA").must();
let attrs = Attributes::new("Contact", &id, "v67.0");
let mut record = DynamicSObject::new(attrs);
record.fields = fields;
record
}
#[test]
fn test_validation_error_display() {
assert_eq!(
ValidationError::MissingRequiredField("LastName".to_string()).to_string(),
"Required field is missing or null: LastName"
);
assert_eq!(
ValidationError::LengthExceeded("FirstName".to_string(), 10).to_string(),
"String length for 'FirstName' exceeds maximum length of 10"
);
assert_eq!(
ValidationError::TypeMismatch(
"Age".to_string(),
"number".to_string(),
"string".to_string()
)
.to_string(),
"Type mismatch for 'Age': expected number, found string"
);
}
#[test]
fn test_valid_record() {
let describe = create_mock_describe(&json!([
mock_field("LastName", "string", false, 80),
mock_field("FirstName", "string", true, 40),
mock_field("Age", "int", true, 0)
]));
let validator = DataValidator::new(&describe);
let mut record_fields = serde_json::Map::new();
record_fields.insert("LastName".to_string(), json!("Doe"));
record_fields.insert("FirstName".to_string(), json!("John"));
record_fields.insert("Age".to_string(), json!(30));
let record = create_mock_record(record_fields);
assert_eq!(validator.validate(&record), Ok(()));
}
#[test]
fn test_missing_required_field() {
let describe = create_mock_describe(&json!([
mock_field("LastName", "string", false, 80),
mock_field("FirstName", "string", true, 40)
]));
let validator = DataValidator::new(&describe);
let mut record_fields = serde_json::Map::new();
record_fields.insert("FirstName".to_string(), json!("John"));
let record = create_mock_record(record_fields);
let errors = match validator.validate(&record) {
Ok(()) => panic!("Expected validation error"),
Err(e) => e,
};
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0],
ValidationError::MissingRequiredField("LastName".to_string())
);
}
#[test]
fn test_null_required_field() {
let describe = create_mock_describe(&json!([
mock_field("LastName", "string", false, 80),
mock_field("FirstName", "string", true, 40)
]));
let validator = DataValidator::new(&describe);
let mut record_fields = serde_json::Map::new();
record_fields.insert("LastName".to_string(), json!(null)); record_fields.insert("FirstName".to_string(), json!("John"));
let record = create_mock_record(record_fields);
let errors = match validator.validate(&record) {
Ok(()) => panic!("Expected validation error"),
Err(e) => e,
};
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0],
ValidationError::MissingRequiredField("LastName".to_string())
);
}
#[test]
fn test_length_exceeded() {
let describe = create_mock_describe(&json!([
mock_field("FirstName", "string", true, 5) ]));
let validator = DataValidator::new(&describe);
let mut record_fields = serde_json::Map::new();
record_fields.insert("FirstName".to_string(), json!("Jonathan"));
let record = create_mock_record(record_fields);
let errors = match validator.validate(&record) {
Ok(()) => panic!("Expected validation error"),
Err(e) => e,
};
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0],
ValidationError::LengthExceeded("FirstName".to_string(), 5)
);
}
#[test]
fn test_type_mismatch() {
let describe = create_mock_describe(&json!([
mock_field("Age", "int", true, 0),
mock_field("IsActive", "boolean", true, 0),
mock_field("Name", "string", true, 80)
]));
let validator = DataValidator::new(&describe);
let mut record_fields = serde_json::Map::new();
record_fields.insert("Age".to_string(), json!("Thirty")); record_fields.insert("IsActive".to_string(), json!("True")); record_fields.insert("Name".to_string(), json!(123));
let record = create_mock_record(record_fields);
let mut errors = match validator.validate(&record) {
Ok(()) => panic!("Expected validation error"),
Err(e) => e,
};
errors.sort_by_key(|e| e.to_string());
assert_eq!(errors.len(), 3);
assert_eq!(
errors[0],
ValidationError::TypeMismatch(
"Age".to_string(),
"number".to_string(),
"string".to_string()
)
);
assert_eq!(
errors[1],
ValidationError::TypeMismatch(
"IsActive".to_string(),
"boolean".to_string(),
"string".to_string()
)
);
assert_eq!(
errors[2],
ValidationError::TypeMismatch(
"Name".to_string(),
"string".to_string(),
"number".to_string()
)
);
}
}