use std::fmt;
use std::str::FromStr;
use serde::de::{SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use crate::hash::Hash;
use crate::operation::OperationId;
use crate::Validate;
use super::error::DocumentViewIdError;
#[derive(Clone, Debug, Eq)]
pub struct DocumentViewId(Vec<OperationId>);
impl DocumentViewId {
pub fn new(graph_tips: &[OperationId]) -> Result<Self, DocumentViewIdError> {
let document_view_id = Self(graph_tips.to_vec());
document_view_id.validate()?;
Ok(document_view_id)
}
pub fn graph_tips(&self) -> &[OperationId] {
self.0.as_slice()
}
pub fn sorted(&self) -> Vec<OperationId> {
let mut graph_tips = self.0.clone();
graph_tips.sort();
graph_tips
}
pub fn as_str(&self) -> String {
let mut id_str = "".to_string();
for (i, operation_id) in self.sorted().iter().enumerate() {
let separator = if i == 0 { "" } else { "_" };
id_str += format!("{}{}", separator, operation_id.as_hash().as_str()).as_str();
}
id_str
}
}
impl fmt::Display for DocumentViewId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, operation_id) in self.0.clone().into_iter().enumerate() {
let separator = if i == 0 { "" } else { "_" };
write!(f, "{}{}", separator, operation_id.as_hash().short_str())?;
}
Ok(())
}
}
impl std::hash::Hash for DocumentViewId {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.sorted().hash(state);
}
}
impl PartialEq for DocumentViewId {
fn eq(&self, other: &Self) -> bool {
self.sorted() == other.sorted()
}
}
impl Validate for DocumentViewId {
type Error = DocumentViewIdError;
fn validate(&self) -> Result<(), Self::Error> {
if self.0.is_empty() {
return Err(DocumentViewIdError::ZeroOperationIds);
};
for hash in &self.0 {
hash.validate()?;
}
Ok(())
}
}
impl Serialize for DocumentViewId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.sorted().serialize(serializer)
}
}
struct DocumentViewIdVisitor;
impl<'de> Visitor<'de> for DocumentViewIdVisitor {
type Value = DocumentViewId;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("sequence of operation id strings")
}
fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
where
S: SeqAccess<'de>,
{
let mut op_ids: Vec<OperationId> = Vec::new();
let mut prev_id = None;
while let Some(seq_value) = seq.next_element::<String>()? {
let operation_id = match seq_value.parse::<OperationId>() {
Ok(operation_id) => operation_id,
Err(hash_err) => {
return Err(serde::de::Error::custom(format!(
"Error parsing document view id at position {}: {}",
op_ids.len(),
hash_err
)))
}
};
if prev_id.is_some() && prev_id.unwrap() > operation_id {
return Err(serde::de::Error::custom(format!(
"Encountered unsorted value in document view id at position {}",
op_ids.len()
)));
}
op_ids.push(operation_id.clone());
prev_id = Some(operation_id);
}
let document_view_id = DocumentViewId::new(&op_ids);
match document_view_id {
Ok(id) => Ok(id),
Err(err) => Err(serde::de::Error::custom(err.to_string())),
}
}
}
impl<'de> Deserialize<'de> for DocumentViewId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_seq(DocumentViewIdVisitor)
}
}
impl IntoIterator for DocumentViewId {
type Item = OperationId;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl From<OperationId> for DocumentViewId {
fn from(operation_id: OperationId) -> Self {
Self::new(&[operation_id]).unwrap()
}
}
impl From<Hash> for DocumentViewId {
fn from(hash: Hash) -> Self {
Self::new(&[hash.into()]).unwrap()
}
}
impl FromStr for DocumentViewId {
type Err = DocumentViewIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut operations: Vec<OperationId> = Vec::new();
s.rsplit('_')
.try_for_each::<_, Result<(), Self::Err>>(|hash_str| {
let hash = Hash::new(hash_str)?;
operations.push(hash.into());
Ok(())
})?;
Ok(Self::new(&operations).unwrap())
}
}
#[cfg(test)]
mod tests {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash as StdHash, Hasher};
use rstest::rstest;
use crate::hash::Hash;
use crate::operation::OperationId;
use crate::test_utils::constants::DEFAULT_HASH;
use crate::test_utils::fixtures::{document_view_id, random_hash, random_operation_id};
use crate::Validate;
use super::DocumentViewId;
#[rstest]
fn conversion(#[from(random_hash)] hash: Hash) {
let hash_str = "0020d3235c8fe6f58608200851b83cd8482808eb81e4c6b4b17805bba57da9f16e79";
let document_id: DocumentViewId = hash_str.parse().unwrap();
assert_eq!(
document_id,
DocumentViewId::new(&[hash_str.parse::<OperationId>().unwrap()]).unwrap()
);
let document_id: DocumentViewId = hash.clone().into();
assert_eq!(
document_id,
DocumentViewId::new(&[hash.clone().into()]).unwrap()
);
let document_id: DocumentViewId = OperationId::new(hash.clone()).into();
assert_eq!(document_id, DocumentViewId::new(&[hash.into()]).unwrap());
assert!("This is not a hash".parse::<DocumentViewId>().is_err());
}
#[rstest]
fn iterates(document_view_id: DocumentViewId) {
for hash in document_view_id {
assert!(hash.validate().is_ok());
}
}
#[test]
fn debug_representation() {
let document_view_id = DEFAULT_HASH.parse::<DocumentViewId>().unwrap();
assert_eq!(format!("{}", document_view_id), "496543");
let operation_1 = "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
.parse::<OperationId>()
.unwrap();
let operation_2 = "0020d3235c8fe6f58608200851b83cd8482808eb81e4c6b4b17805bba57da9f16e79"
.parse::<OperationId>()
.unwrap();
let view_id_unmerged = DocumentViewId::new(&[operation_1, operation_2]).unwrap();
assert_eq!(format!("{}", view_id_unmerged), "496543_f16e79");
}
#[test]
fn string_representation() {
let document_view_id = DEFAULT_HASH.parse::<DocumentViewId>().unwrap();
assert_eq!(
document_view_id.as_str(),
"0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
);
let operation_1 = "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
.parse::<OperationId>()
.unwrap();
let operation_2 = "0020d3235c8fe6f58608200851b83cd8482808eb81e4c6b4b17805bba57da9f16e79"
.parse::<OperationId>()
.unwrap();
let document_view_id = DocumentViewId::new(&[operation_1, operation_2]).unwrap();
assert_eq!(document_view_id.as_str(), "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543_0020d3235c8fe6f58608200851b83cd8482808eb81e4c6b4b17805bba57da9f16e79");
assert_eq!("0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543_0020d3235c8fe6f58608200851b83cd8482808eb81e4c6b4b17805bba57da9f16e79".parse::<DocumentViewId>().unwrap(), document_view_id);
}
#[rstest]
fn equality(
#[from(random_operation_id)] operation_id_1: OperationId,
#[from(random_operation_id)] operation_id_2: OperationId,
) {
let view_id_1 =
DocumentViewId::new(&[operation_id_1.clone(), operation_id_2.clone()]).unwrap();
let view_id_2 = DocumentViewId::new(&[operation_id_2, operation_id_1]).unwrap();
assert_eq!(view_id_1, view_id_2);
}
#[rstest]
fn hash_equality(
#[from(random_operation_id)] operation_id_1: OperationId,
#[from(random_operation_id)] operation_id_2: OperationId,
) {
let mut hasher_1 = DefaultHasher::default();
let mut hasher_2 = DefaultHasher::default();
let view_id_1 =
DocumentViewId::new(&[operation_id_1.clone(), operation_id_2.clone()]).unwrap();
let view_id_2 = DocumentViewId::new(&[operation_id_2, operation_id_1]).unwrap();
view_id_1.hash(&mut hasher_1);
view_id_2.hash(&mut hasher_2);
assert_eq!(hasher_1.finish(), hasher_2.finish());
}
#[rstest]
fn deserialize_unsorted_view_id(
#[from(random_operation_id)] operation_id_1: OperationId,
#[from(random_operation_id)] operation_id_2: OperationId,
) {
let unsorted_hashes = [
"0020c13cdc58dfc6f4ebd32992ff089db79980363144bdb2743693a019636fa72ec8",
"00202dce4b32cd35d61cf54634b93a526df333c5ed3d93230c2f026f8d1ecabc0cd7",
];
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&unsorted_hashes, &mut cbor_bytes).unwrap();
let unsorted_operation_ids = hex::encode(cbor_bytes);
let result: Result<DocumentViewId, ciborium::de::Error<std::io::Error>> =
ciborium::de::from_reader(&hex::decode(unsorted_operation_ids).unwrap()[..]);
let expected_result = ciborium::de::Error::<std::io::Error>::Semantic(
None,
"Encountered unsorted value in document view id at position 1".to_string(),
);
assert_eq!(result.unwrap_err().to_string(), expected_result.to_string());
let mut reversed_ids = vec![operation_id_1, operation_id_2];
reversed_ids.sort();
reversed_ids.reverse();
let view_id_unsorted = DocumentViewId::new(&reversed_ids).unwrap();
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&view_id_unsorted, &mut cbor_bytes).unwrap();
let result: Result<DocumentViewId, ciborium::de::Error<std::io::Error>> =
ciborium::de::from_reader(&cbor_bytes[..]);
assert!(result.is_ok());
assert_eq!(result.unwrap(), view_id_unsorted);
}
#[test]
fn deserialize_invalid_view_id() {
let invalid_hashes = [
"0020c13cdc58dfc6f4ebd32992ff089db79980363144bdb2743693a019636fa72ec8",
"2dce4b32cd35d61cf54634b93a526df333c5ed3d93230c2f026f8d1ecabc0cd7",
];
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&invalid_hashes, &mut cbor_bytes).unwrap();
let invalid_id_encoded = hex::encode(cbor_bytes);
let result: Result<DocumentViewId, ciborium::de::Error<std::io::Error>> =
ciborium::de::from_reader(&hex::decode(invalid_id_encoded).unwrap()[..]);
let expected_result = ciborium::de::Error::<std::io::Error>::Semantic(
None,
"Error parsing document view id at position 1: invalid hash length 32 bytes, expected 34 bytes".to_string()
);
assert_eq!(result.unwrap_err().to_string(), expected_result.to_string());
}
}