use crate::types::describe::{FieldDescribe, FieldType, SObjectDescribe};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldChange<'a> {
pub name: &'a str,
pub old_type: &'a FieldType,
pub new_type: &'a FieldType,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct SchemaDiffResult<'a> {
pub added: Vec<&'a FieldDescribe>,
pub removed: Vec<&'a FieldDescribe>,
pub changed: Vec<FieldChange<'a>>,
}
impl SchemaDiffResult<'_> {
#[must_use]
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
}
}
#[must_use]
pub fn compare_schemas<'a>(
old_schema: &'a SObjectDescribe,
new_schema: &'a SObjectDescribe,
) -> SchemaDiffResult<'a> {
let mut result = SchemaDiffResult::default();
let mut old_fields: Vec<&'a FieldDescribe> = old_schema.fields.iter().collect();
let mut new_fields: Vec<&'a FieldDescribe> = new_schema.fields.iter().collect();
old_fields.sort_by(|a, b| crate::schema::cmp_field_names(&a.name, &b.name));
new_fields.sort_by(|a, b| crate::schema::cmp_field_names(&a.name, &b.name));
let mut old_iter = old_fields.into_iter();
let mut new_iter = new_fields.into_iter();
let mut current_old = old_iter.next();
let mut current_new = new_iter.next();
while let (Some(old), Some(new)) = (current_old, current_new) {
match crate::schema::cmp_field_names(&old.name, &new.name) {
std::cmp::Ordering::Less => {
result.removed.push(old);
current_old = old_iter.next();
}
std::cmp::Ordering::Greater => {
result.added.push(new);
current_new = new_iter.next();
}
std::cmp::Ordering::Equal => {
if old.type_ != new.type_ {
result.changed.push(FieldChange {
name: new.name.as_str(),
old_type: &old.type_,
new_type: &new.type_,
});
}
current_old = old_iter.next();
current_new = new_iter.next();
}
}
}
if let Some(old) = current_old {
result.removed.push(old);
result.removed.extend(old_iter);
}
if let Some(new) = current_new {
result.added.push(new);
result.added.extend(new_iter);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::must::Must;
use serde_json::json;
fn create_mock_describe(fields_json: &serde_json::Value) -> SObjectDescribe {
let describe_json = json!({
"name": "Account",
"label": "Account",
"custom": false,
"queryable": true,
"activateable": false, "createable": true, "customSetting": false, "deletable": true,
"deprecatedAndHidden": false, "feedEnabled": true, "hasSubtypes": false,
"isSubtype": false, "keyPrefix": "001", "labelPlural": "Accounts", "layoutable": true,
"mergeable": true, "mruEnabled": true, "replicateable": true, "retrieveable": true,
"searchable": true, "triggerable": true, "undeletable": true, "updateable": true,
"urls": {}, "childRelationships": [], "recordTypeInfos": [],
"fields": fields_json
});
serde_json::from_value(describe_json).must()
}
fn mock_field(name: &str, field_type: &str) -> serde_json::Value {
json!({
"name": name,
"type": field_type,
"label": format!("{} Label", name),
"referenceTo": [],
"aggregatable": true, "autoNumber": false, "byteLength": 18, "calculated": false,
"cascadeDelete": false, "caseSensitive": false, "createable": false, "custom": false,
"defaultedOnCreate": true, "dependentPicklist": false, "deprecatedAndHidden": false,
"digits": 0, "displayLocationInDecimal": false, "encrypted": false, "externalId": false,
"filterable": true, "groupable": true, "highScaleNumber": false, "htmlFormatted": false,
"idLookup": true, "length": 18, "nameField": false, "namePointing": false, "nillable": 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
})
}
#[test]
fn test_schema_diff_no_changes() {
let old_schema = create_mock_describe(&json!([
mock_field("Id", "id"),
mock_field("Name", "string")
]));
let new_schema = create_mock_describe(&json!([
mock_field("Id", "id"),
mock_field("Name", "string")
]));
let diff = compare_schemas(&old_schema, &new_schema);
assert!(diff.is_empty());
assert_eq!(diff.added.len(), 0);
assert_eq!(diff.removed.len(), 0);
assert_eq!(diff.changed.len(), 0);
}
#[test]
fn test_schema_diff_added_fields() {
let old_schema = create_mock_describe(&json!([mock_field("Id", "id")]));
let new_schema = create_mock_describe(&json!([
mock_field("Id", "id"),
mock_field("Name", "string"),
mock_field("Website", "url")
]));
let diff = compare_schemas(&old_schema, &new_schema);
assert!(!diff.is_empty());
assert_eq!(diff.added.len(), 2);
assert_eq!(diff.removed.len(), 0);
assert_eq!(diff.changed.len(), 0);
assert_eq!(diff.added[0].name, "Name");
assert_eq!(diff.added[1].name, "Website");
}
#[test]
fn test_schema_diff_removed_fields() {
let old_schema = create_mock_describe(&json!([
mock_field("Id", "id"),
mock_field("Name", "string"),
mock_field("Website", "url")
]));
let new_schema = create_mock_describe(&json!([mock_field("Id", "id")]));
let diff = compare_schemas(&old_schema, &new_schema);
assert!(!diff.is_empty());
assert_eq!(diff.added.len(), 0);
assert_eq!(diff.removed.len(), 2);
assert_eq!(diff.changed.len(), 0);
assert_eq!(diff.removed[0].name, "Name");
assert_eq!(diff.removed[1].name, "Website");
}
#[test]
fn test_schema_diff_changed_fields() {
let old_schema =
create_mock_describe(&json!([mock_field("Id", "id"), mock_field("Age", "int")]));
let new_schema = create_mock_describe(&json!([
mock_field("Id", "id"),
mock_field("Age", "double")
]));
let diff = compare_schemas(&old_schema, &new_schema);
assert!(!diff.is_empty());
assert_eq!(diff.added.len(), 0);
assert_eq!(diff.removed.len(), 0);
assert_eq!(diff.changed.len(), 1);
assert_eq!(diff.changed[0].name, "Age");
assert_eq!(diff.changed[0].old_type, &FieldType::Int);
assert_eq!(diff.changed[0].new_type, &FieldType::Double);
}
}