use std::fmt::{Debug, Formatter};
use std::hash::Hash;
use std::sync::Arc;
use indexmap::IndexSet;
use thiserror::Error;
use crate::util::{ListView, ObjectUtils, SetView, Validate, ValidateError};
type ComponentPredicate<T> = dyn Fn(&T) -> bool + Send + Sync;
#[derive(Clone)]
pub struct ObjectArrayValue<T> {
elements: Vec<Option<T>>,
component_class_name: String,
component_predicate: Arc<ComponentPredicate<T>>,
}
impl<T> ObjectArrayValue<T> {
pub(crate) fn from_parts(
component_class_name: String,
elements: Vec<Option<T>>,
component_predicate: Arc<ComponentPredicate<T>>,
) -> Self {
Self {
elements,
component_class_name,
component_predicate,
}
}
pub(crate) fn component_predicate(&self) -> Arc<ComponentPredicate<T>> {
Arc::clone(&self.component_predicate)
}
pub fn typed(
component_class_name: impl Into<String>,
elements: Vec<Option<T>>,
component_predicate: impl Fn(&T) -> bool + Send + Sync + 'static,
) -> Result<Self, ObjectsError> {
let component_class_name = component_class_name.into();
let component_predicate: Arc<ComponentPredicate<T>> = Arc::new(component_predicate);
if elements
.iter()
.flatten()
.any(|element| !component_predicate(element))
{
return Err(ObjectsError::ArrayStore {
component_class_name,
});
}
Ok(Self {
elements,
component_class_name,
component_predicate,
})
}
#[must_use]
pub fn object(elements: Vec<Option<T>>) -> Self {
Self {
elements,
component_class_name: "java.lang.Object".to_owned(),
component_predicate: Arc::new(|_| true),
}
}
#[must_use]
pub fn component_class_name(&self) -> &str {
&self.component_class_name
}
#[must_use]
pub fn len(&self) -> usize {
self.elements.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
#[must_use]
pub fn as_slice(&self) -> &[Option<T>] {
&self.elements
}
pub fn set(&mut self, index: usize, value: Option<T>) -> Result<(), ObjectsError> {
if index >= self.elements.len() {
return Err(ObjectsError::ArrayIndexOutOfBounds {
index,
length: self.elements.len(),
});
}
if value
.as_ref()
.is_some_and(|value| !(self.component_predicate)(value))
{
return Err(ObjectsError::ArrayStore {
component_class_name: self.component_class_name.clone(),
});
}
self.elements[index] = value;
Ok(())
}
}
impl<T: Debug> Debug for ObjectArrayValue<T> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ObjectArrayValue")
.field("elements", &self.elements)
.field("component_class_name", &self.component_class_name)
.finish_non_exhaustive()
}
}
#[derive(Debug, Error, Clone, Eq, PartialEq)]
pub enum ObjectsError {
#[error(transparent)]
Validation(#[from] ValidateError),
#[error("value cannot be stored in array with component class \"{component_class_name}\"")]
ArrayStore {
component_class_name: String,
},
#[error("index {index} out of bounds for array length {length}")]
ArrayIndexOutOfBounds {
index: usize,
length: usize,
},
}
impl ObjectsError {
#[must_use]
pub const fn class_name(&self) -> &'static str {
match self {
Self::Validation(error) => error.class_name(),
Self::ArrayStore { .. } => "java.lang.ArrayStoreException",
Self::ArrayIndexOutOfBounds { .. } => "java.lang.ArrayIndexOutOfBoundsException",
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Objects;
impl Objects {
#[must_use]
pub const fn new() -> Self {
Self
}
#[must_use]
pub fn null_safe<T>(&self, target: Option<T>, default_value: Option<T>) -> Option<T> {
ObjectUtils::null_safe(target, default_value)
}
pub fn array_null_safe<T>(
&self,
target: Option<&ObjectArrayValue<T>>,
default_value: Option<&T>,
) -> Result<ObjectArrayValue<T>, ObjectsError>
where
T: Clone,
{
Validate::not_null(target, Some("Target cannot be null"))?;
let target = target.expect("validated target");
let mut result = target.clone();
for index in 0..result.elements.len() {
if result.elements[index].is_none() {
result.set(index, default_value.cloned())?;
}
}
Ok(result)
}
pub fn list_null_safe<T>(
&self,
target: Option<&dyn ListView<Option<T>>>,
default_value: Option<&T>,
) -> Result<Vec<Option<T>>, ObjectsError>
where
T: Clone,
{
Validate::not_null(target, Some("Target cannot be null"))?;
let target = target.expect("validated target");
Ok(target
.iter()
.map(|element| ObjectUtils::null_safe(element.clone(), default_value.cloned()))
.collect())
}
pub fn set_null_safe<T>(
&self,
target: Option<&dyn SetView<Option<T>>>,
default_value: Option<&T>,
) -> Result<IndexSet<Option<T>>, ObjectsError>
where
T: Clone + Eq + Hash,
{
Validate::not_null(target, Some("Target cannot be null"))?;
let target = target.expect("validated target");
Ok(target
.iter()
.map(|element| ObjectUtils::null_safe(element.clone(), default_value.cloned()))
.collect())
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use indexmap::IndexSet;
use super::{ObjectArrayValue, Objects, ObjectsError};
use crate::util::{ListView, SetView, ValidateError};
#[test]
fn scalar_selection_preserves_selected_identity() {
let objects = Objects::new();
let target = Rc::new("target".to_owned());
let default_value = Rc::new("default".to_owned());
let selected = objects
.null_safe(Some(Rc::clone(&target)), Some(Rc::clone(&default_value)))
.expect("selected");
assert!(Rc::ptr_eq(&selected, &target));
}
#[test]
fn array_result_is_independent_and_preserves_runtime_component_type() {
let objects = Objects;
let source = ObjectArrayValue::typed(
"java.lang.Number",
vec![Some(1_i32), None, Some(3_i32)],
|_| true,
)
.expect("source");
assert_eq!(
format!("{source:?}"),
"ObjectArrayValue { elements: [Some(1), None, Some(3)], component_class_name: \
\"java.lang.Number\", .. }"
);
let mut result = objects
.array_null_safe(Some(&source), Some(&2))
.expect("result");
assert_eq!(result.component_class_name(), "java.lang.Number");
assert_eq!(result.as_slice(), &[Some(1), Some(2), Some(3)]);
assert_eq!(source.as_slice(), &[Some(1), None, Some(3)]);
result.set(0, Some(9)).expect("mutable result");
assert_eq!(source.as_slice()[0], Some(1));
}
#[test]
fn array_store_check_runs_only_when_a_null_slot_is_replaced() {
#[derive(Clone, Debug, Eq, PartialEq)]
enum Value {
Text(&'static str),
Number(i32),
}
let accepts_text = |value: &Value| matches!(value, Value::Text(_));
let with_null = ObjectArrayValue::typed(
"java.lang.String",
vec![Some(Value::Text("one")), None],
accepts_text,
)
.expect("source");
let error = Objects
.array_null_safe(Some(&with_null), Some(&Value::Number(2)))
.expect_err("array store");
assert_eq!(
error,
ObjectsError::ArrayStore {
component_class_name: "java.lang.String".to_owned()
}
);
assert_eq!(
error.to_string(),
"value cannot be stored in array with component class \"java.lang.String\""
);
assert_eq!(error.class_name(), "java.lang.ArrayStoreException");
let without_null = ObjectArrayValue::typed(
"java.lang.String",
vec![Some(Value::Text("one"))],
accepts_text,
)
.expect("source");
assert!(
Objects
.array_null_safe(Some(&without_null), Some(&Value::Number(2)))
.is_ok()
);
}
#[test]
fn array_adapter_enforces_store_and_index_contracts() {
let invalid =
ObjectArrayValue::typed("positive.Integer", vec![Some(-1)], |value| *value > 0)
.expect_err("invalid source");
assert_eq!(invalid.class_name(), "java.lang.ArrayStoreException");
let mut target = ObjectArrayValue::object(vec![Some("one")]);
let error = target.set(1, Some("two")).expect_err("bounds");
assert_eq!(
error,
ObjectsError::ArrayIndexOutOfBounds {
index: 1,
length: 1
}
);
assert_eq!(
error.to_string(),
"index 1 out of bounds for array length 1"
);
assert_eq!(
error.class_name(),
"java.lang.ArrayIndexOutOfBoundsException"
);
assert_eq!(target.len(), 1);
assert!(!target.is_empty());
}
#[test]
fn list_result_is_mutable_ordered_and_independent() {
let source = vec![Some("one".to_owned()), None, Some("one".to_owned())];
let view: &dyn ListView<Option<String>> = &source;
let mut result = Objects
.list_null_safe(Some(view), Some(&"default".to_owned()))
.expect("result");
result.push(Some("tail".to_owned()));
assert_eq!(
result,
vec![
Some("one".to_owned()),
Some("default".to_owned()),
Some("one".to_owned()),
Some("tail".to_owned())
]
);
assert_eq!(source[1], None);
}
#[test]
fn set_result_preserves_first_order_and_deduplicates_replacement() {
let source = IndexSet::from([Some("default".to_owned()), None, Some("other".to_owned())]);
let view: &dyn SetView<Option<String>> = &source;
let mut result = Objects
.set_null_safe(Some(view), Some(&"default".to_owned()))
.expect("result");
result.insert(Some("tail".to_owned()));
assert_eq!(
result.into_iter().collect::<Vec<_>>(),
vec![
Some("default".to_owned()),
Some("other".to_owned()),
Some("tail".to_owned())
]
);
assert!(source.contains(&None));
}
#[test]
fn collection_and_array_null_targets_use_exact_validation_message() {
let array_error = Objects
.array_null_safe::<String>(None, None)
.expect_err("array null");
assert_eq!(
array_error,
ObjectsError::Validation(ValidateError::IllegalArgument {
message: Some("Target cannot be null".to_owned())
})
);
assert_eq!(
array_error.class_name(),
"java.lang.IllegalArgumentException"
);
assert_eq!(array_error.to_string(), "Target cannot be null");
assert!(Objects.list_null_safe::<String>(None, None).is_err());
assert!(Objects.set_null_safe::<String>(None, None).is_err());
}
}