typex 0.1.0

Runtime reflection: inspection, traversal, equality and mutation
Documentation
use super::support::*;
use super::*;
use core::any::Any;

#[derive(Debug, PartialEq)]
struct RejectMiddleInsert {
  values: Vec<u8>,
}

impl Meta for RejectMiddleInsert {
  fn kind(&self) -> ValueKind {
    ValueKind::Sequence
  }

  fn access_kind(&self) -> Option<AccessKind> {
    Some(AccessKind::Item)
  }

  fn item(&self, index: usize) -> Option<ObjectRef<'_>> {
    self
      .values
      .get(index)
      .map(|value| ObjectRef::new(value as &dyn Meta))
  }

  fn len(&self) -> Option<usize> {
    Some(self.values.len())
  }

  fn into_any(self: Box<Self>) -> Box<dyn Any> {
    self
  }

  fn as_any(&self) -> &dyn Any {
    self
  }
}

impl MetaMut for RejectMiddleInsert {
  set_body!();

  fn move_item(&mut self, from: usize, to: usize) -> Result<(), MoveItemError> {
    move_item_by_remove_insert(self, from, to)
  }

  fn item_mut(&mut self, index: usize) -> Option<ObjectRefMut<'_>> {
    self
      .values
      .get_mut(index)
      .map(|value| ObjectRefMut::new(value as &mut dyn MetaMut))
  }

  fn insert_item(&mut self, index: usize, value: Object) -> Result<ObjectRefMut<'_>, Object> {
    if index == 1 || index > self.values.len() || !value.is::<u8>() {
      return Err(value);
    }

    self.values.insert(index, value.to::<u8>().unwrap());
    Ok(ObjectRefMut::new(&mut self.values[index]))
  }

  fn remove_item(&mut self, index: usize) -> Option<Object> {
    (index < self.values.len()).then(|| Object::new(self.values.remove(index)))
  }

  fn as_any_mut(&mut self) -> &mut dyn Any {
    self
  }
}

#[test]
fn immediate_apply_keeps_successful_operations_when_a_later_operation_fails() {
  let mut pair = Pair {
    count: 1,
    label: Text("before"),
  };
  let patch = [
    PatchOperation::set([PathSegment::Field("count")], 7_u16),
    PatchOperation::set([PathSegment::Field("label")], 8_u16),
  ];

  let error = ObjectRefMut::new(&mut pair).apply(patch).unwrap_err();

  assert!(matches!(error, ApplyError::TypeMismatch { .. }));
  assert_eq!(pair.count, 7);
  assert_eq!(pair.label, Text("before"));
}

#[test]
fn apply_updates_only_fields_present_in_a_patch() {
  let mut pair = Pair {
    count: 1,
    label: Text("before"),
  };
  let patch = vec![PatchOperation::set(
    vec![PathSegment::Field("count")],
    7_u16,
  )];

  let target: &mut dyn MetaMut = &mut pair;
  ObjectRefMut::new(target).apply(patch).unwrap();

  assert_eq!(pair.count, 7);
  assert_eq!(pair.label, Text("before"));
}

#[test]
fn apply_upserts_map_entries() {
  let mut map = BTreeMap::from([(String::from("primary"), 1_u16)]);
  let patch = vec![
    PatchOperation::insert_key(Vec::new(), "primary", 2_u16),
    PatchOperation::insert_key(Vec::new(), "secondary", 3_u16),
  ];

  ObjectRefMut::new(&mut map).apply(patch).unwrap();

  assert_eq!(map.get("primary"), Some(&2));
  assert_eq!(map.get("secondary"), Some(&3));
}

#[test]
fn apply_supports_sequence_edits_and_map_removal() {
  let mut values = vec![1_u16, 2, 3];
  let patch = vec![
    PatchOperation::set(vec![PathSegment::Item(1)], 7_u16),
    PatchOperation::insert_item(Vec::new(), 2, 8_u16),
    PatchOperation::remove_item(Vec::new(), 0),
  ];

  ObjectRefMut::new(&mut values).apply(patch).unwrap();

  assert_eq!(values, vec![7, 8, 3]);

  let mut map = BTreeMap::from([
    (String::from("keep"), 1_u16),
    (String::from("remove"), 2_u16),
  ]);
  ObjectRefMut::new(&mut map)
    .apply([PatchOperation::remove_key([], "remove")])
    .unwrap();
  assert_eq!(map, BTreeMap::from([(String::from("keep"), 1_u16)]));
}

#[test]
fn apply_reports_missing_paths() {
  let mut pair = Pair {
    count: 1,
    label: Text("before"),
  };
  let patch = vec![PatchOperation::set(
    vec![PathSegment::Field("missing")],
    7_u16,
  )];

  let error = ObjectRefMut::new(&mut pair).apply(patch).unwrap_err();

  assert!(matches!(
    error,
    ApplyError::PathNotFound {
      operation: PatchOperationKind::Set,
      path,
      ..
    } if path == vec![PathSegment::Field("missing")]
  ));
  assert_eq!(pair.count, 1);
  assert_eq!(pair.label, Text("before"));
}

#[test]
fn patch_operation_kind_display_names_are_consistent() {
  assert_eq!(PatchOperationKind::Set.to_string(), "set");
  assert_eq!(PatchOperationKind::InsertKey.to_string(), "insert key");
  assert_eq!(PatchOperationKind::RemoveKey.to_string(), "remove key");
  assert_eq!(PatchOperationKind::InsertItem.to_string(), "insert item");
  assert_eq!(PatchOperationKind::PushItem.to_string(), "push item");
  assert_eq!(PatchOperationKind::RemoveItem.to_string(), "remove item");
  assert_eq!(PatchOperationKind::MoveItem.to_string(), "move item");
}

#[test]
fn move_item_out_of_bounds_leaves_sequence_unchanged() {
  let mut items = vec![1_u8, 2_u8, 3_u8];

  let result =
    ObjectRefMut::new(&mut items as &mut dyn MetaMut).apply([PatchOperation::move_item([], 0, 99)]);

  assert_eq!(
    result,
    Err(ApplyError::IndexOutOfBounds {
      path: Vec::new(),
      operation: PatchOperationKind::MoveItem,
      index: 99,
      len: 3,
    })
  );
  assert_eq!(items, vec![1, 2, 3]);
}

#[test]
fn move_item_reports_out_of_bounds_source_index() {
  let mut items = vec![1_u8, 2_u8, 3_u8];

  let result = items.apply([PatchOperation::move_item([], 3, 0)]);

  assert_eq!(
    result,
    Err(ApplyError::IndexOutOfBounds {
      path: Vec::new(),
      operation: PatchOperationKind::MoveItem,
      index: 3,
      len: 3,
    })
  );
  assert_eq!(
    result.unwrap_err().to_string(),
    "Reflective patch operation move item received index 3 for a sequence of length 3"
  );
}

#[test]
fn failed_move_restores_the_removed_item() {
  let mut sequence = RejectMiddleInsert {
    values: vec![1, 2, 3],
  };

  let result = sequence.apply([PatchOperation::move_item([], 0, 2)]);

  assert!(matches!(
    result,
    Err(ApplyError::Unsupported {
      operation: PatchOperationKind::MoveItem,
      ..
    })
  ));
  assert_eq!(sequence.values, vec![1, 2, 3]);
}

#[test]
fn failed_move_reports_an_item_that_it_cannot_restore() {
  let mut sequence = RejectInsert {
    values: vec![1, 2, 3],
  };

  let result = sequence.apply([PatchOperation::move_item([], 0, 2)]);

  assert_eq!(
    result,
    Err(ApplyError::ItemLost {
      path: Vec::new(),
      index: 0,
    })
  );
  assert_eq!(sequence.values, vec![2, 3]);
}

#[test]
fn ordered_patch_updates_nested_values_and_preserves_operation_order() {
  let mut config = PatchConfig {
    enabled: true,
    labels: BTreeMap::from([(String::from("primary"), 1)]),
    profile: Pair {
      count: 1,
      label: Text("before"),
    },
    items: vec![1, 2, 3],
  };
  let patch = vec![
    PatchOperation::set(
      vec![PathSegment::Field("profile"), PathSegment::Field("count")],
      7_u16,
    ),
    PatchOperation::insert_item(vec![PathSegment::Field("items")], 1, 8_u8),
    PatchOperation::remove_item(vec![PathSegment::Field("items")], 0),
  ];

  ObjectRefMut::new(&mut config).apply(patch).unwrap();

  assert_eq!(config.profile.count, 7);
  assert_eq!(config.items, vec![8, 2, 3]);
}

#[test]
fn patches_reject_non_string_map_keys() {
  let mut map = BTreeMap::from([(1_u8, 2_u8)]);
  let patch = vec![PatchOperation::remove_key(Vec::new(), "1")];

  assert!(ObjectRefMut::new(&mut map).apply(patch).is_err());
  assert_eq!(map.get(&1), Some(&2));
}

#[derive(Debug)]
struct NonClonePatchValue(u8);

impl Meta for NonClonePatchValue {
  fn kind(&self) -> ValueKind {
    ValueKind::Scalar
  }

  fn eq_dyn(&self, other: &dyn Meta) -> bool {
    other.as_any().downcast_ref::<Self>().map(|other| other.0) == Some(self.0)
  }

  fn into_any(self: Box<Self>) -> Box<dyn Any> {
    self
  }

  fn as_any(&self) -> &dyn Any {
    self
  }
}

impl MetaMut for NonClonePatchValue {
  set_body!();

  fn as_any_mut(&mut self) -> &mut dyn Any {
    self
  }
}

#[test]
fn patch_values_do_not_require_clone() {
  let mut value = NonClonePatchValue(1);
  let patch = vec![PatchOperation::set(Vec::new(), NonClonePatchValue(2))];

  ObjectRefMut::new(&mut value).apply(patch).unwrap();

  assert_eq!(value.0, 2);
}

#[test]
fn built_in_containers_do_not_require_clone_for_meta_access() {
  let values = Object::new(vec![NonClonePatchValue(1)]);
  assert_eq!(
    values
      .item(0)
      .unwrap()
      .to_ref::<NonClonePatchValue>()
      .unwrap()
      .0,
    1
  );

  let map = Object::new(BTreeMap::from([(
    String::from("value"),
    NonClonePatchValue(2),
  )]));
  assert_eq!(
    map
      .key("value")
      .unwrap()
      .to_ref::<NonClonePatchValue>()
      .unwrap()
      .0,
    2
  );
}