use std::cmp::Ordering;
use crate::{
mapping::{
endpoint::{Endpoint, Level},
InterfaceMapping, MappingError,
},
schema::{Mapping, MappingType},
MappingPath,
};
pub const MIN_OBJECT_ENDPOINT_LEN: usize = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatastreamObjectMapping {
pub(crate) endpoint: Endpoint<String>,
pub(crate) mapping_type: MappingType,
pub(crate) required: bool,
#[cfg(feature = "doc-fields")]
pub(crate) description: Option<String>,
#[cfg(feature = "doc-fields")]
pub(crate) doc: Option<String>,
}
impl DatastreamObjectMapping {
pub fn required(&self) -> bool {
self.required
}
pub fn cmp_object_field(&self, path: &str) -> Ordering {
let last = self.endpoint.last();
debug_assert!(
last.is_some(),
"an endpoint should always have at least an endpoint"
);
match last {
Some(Level::Simple(simple)) => simple.as_str().cmp(path),
Some(Level::Parameter(_)) => Ordering::Equal,
None => Ordering::Less,
}
}
pub fn eq_object_field(&self, path: &str) -> bool {
let last = self.endpoint.last();
debug_assert!(
last.is_some(),
"an endpoint should always have at least an endpoint"
);
last.is_some_and(|endpoint_level| *endpoint_level == path)
}
pub(crate) fn is_object_path<'a>(&self, path: &MappingPath<'a>) -> bool {
if self.endpoint.len().saturating_sub(1) != path.len() {
return false;
}
self.endpoint
.iter()
.zip(path.levels.iter())
.all(|(endpoint_level, path_level)| endpoint_level == path_level)
}
pub(crate) fn is_same_object(&self, other: &DatastreamObjectMapping) -> bool {
if self.endpoint.len() != other.endpoint.len() {
return false;
}
self.endpoint
.iter()
.zip(other.endpoint.iter())
.rev()
.skip(1)
.all(|(level, other_level)| level == other_level)
}
}
impl InterfaceMapping for DatastreamObjectMapping {
fn endpoint(&self) -> &Endpoint<String> {
&self.endpoint
}
fn mapping_type(&self) -> MappingType {
self.mapping_type
}
#[cfg(feature = "doc-fields")]
#[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
fn description(&self) -> Option<&str> {
self.description.as_deref()
}
#[cfg(feature = "doc-fields")]
#[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
fn doc(&self) -> Option<&str> {
self.doc.as_deref()
}
}
impl<T> TryFrom<Mapping<T>> for DatastreamObjectMapping
where
T: AsRef<str> + Into<String>,
{
type Error = MappingError;
fn try_from(value: Mapping<T>) -> Result<Self, Self::Error> {
let endpoint = Endpoint::try_from(value.endpoint.as_ref())?;
if endpoint.len() < MIN_OBJECT_ENDPOINT_LEN {
return Err(MappingError::TooShortForObject(endpoint.to_string()));
}
Ok(Self {
endpoint,
mapping_type: value.mapping_type,
required: value.required.unwrap_or(false),
#[cfg(feature = "doc-fields")]
description: value.description.map(T::into),
#[cfg(feature = "doc-fields")]
doc: value.doc.map(T::into),
})
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
fn mock_object_mapping(endpoint: &str) -> DatastreamObjectMapping {
DatastreamObjectMapping {
endpoint: endpoint.parse().unwrap(),
mapping_type: MappingType::Boolean,
required: false,
#[cfg(feature = "doc-fields")]
description: None,
#[cfg(feature = "doc-fields")]
doc: None,
}
}
#[test]
fn getters_success() {
let description = Some("Object mapping description");
let doc = Some("Object mapping doc");
let mapping_type = MappingType::Boolean;
let mapping = Mapping {
endpoint: "/object/path",
mapping_type,
reliability: None,
explicit_timestamp: None,
retention: None,
expiry: None,
database_retention_policy: None,
database_retention_ttl: None,
allow_unset: None,
required: None,
description,
doc,
};
let obj_mapping = DatastreamObjectMapping::try_from(mapping).unwrap();
let exp = Endpoint::try_from("/object/path").unwrap();
assert_eq!(*obj_mapping.endpoint(), exp);
assert_eq!(obj_mapping.mapping_type(), mapping_type);
#[cfg(feature = "doc-fields")]
{
assert_eq!(description, obj_mapping.description());
assert_eq!(doc, obj_mapping.doc());
}
}
#[test]
fn mapping_error_to_short() {
let mapping = Mapping {
endpoint: "/tooShort",
mapping_type: MappingType::Boolean,
reliability: None,
explicit_timestamp: None,
retention: None,
expiry: None,
database_retention_policy: None,
database_retention_ttl: None,
allow_unset: None,
required: None,
description: None,
doc: None,
};
let err = DatastreamObjectMapping::try_from(mapping).unwrap_err();
assert!(matches!(err, MappingError::TooShortForObject(_)));
}
#[test]
fn check_object_path() {
let endpoint = mock_object_mapping("/%{sensor_id}/boolean_endpoint");
let path = MappingPath::try_from("/1/boolean_endpoint").unwrap();
assert!(!endpoint.is_object_path(&path));
let path = MappingPath::try_from("/1").unwrap();
assert!(endpoint.is_object_path(&path));
}
#[test]
fn object_field() {
let endpoint = mock_object_mapping("/%{sensor_id}/boolean_endpoint");
assert!(endpoint.eq_object_field("boolean_endpoint"));
assert!(!endpoint.eq_object_field("foo"));
}
#[test]
fn same_object() {
let endpoint = mock_object_mapping("/base/foo");
let same = mock_object_mapping("/base/bar");
let different = mock_object_mapping("/other/bar");
assert!(endpoint.is_same_object(&same));
assert!(!endpoint.is_same_object(&different));
}
}