use std::hash::{Hash as StdHash, Hasher};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::document::DocumentViewId;
use crate::operation::{AsOperation, OperationEncoded, OperationError, OperationFields};
use crate::schema::SchemaId;
use crate::Validate;
#[derive(Clone, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[serde(untagged)]
#[repr(u8)]
pub enum OperationVersion {
Default = 1,
}
impl Copy for OperationVersion {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OperationAction {
Create,
Update,
Delete,
}
impl OperationAction {
#[allow(unused)]
pub fn as_str(&self) -> &str {
match self {
OperationAction::Create => "create",
OperationAction::Update => "update",
OperationAction::Delete => "delete",
}
}
}
impl Serialize for OperationAction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(match *self {
OperationAction::Create => "create",
OperationAction::Update => "update",
OperationAction::Delete => "delete",
})
}
}
impl<'de> Deserialize<'de> for OperationAction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"create" => Ok(OperationAction::Create),
"update" => Ok(OperationAction::Update),
"delete" => Ok(OperationAction::Delete),
_ => Err(serde::de::Error::custom("unknown operation action")),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Operation {
action: OperationAction,
schema: SchemaId,
version: OperationVersion,
#[serde(skip_serializing_if = "Option::is_none")]
previous_operations: Option<DocumentViewId>,
#[serde(skip_serializing_if = "Option::is_none")]
fields: Option<OperationFields>,
}
impl Operation {
pub fn new_create(schema: SchemaId, fields: OperationFields) -> Result<Self, OperationError> {
let operation = Self {
action: OperationAction::Create,
version: OperationVersion::Default,
schema,
previous_operations: None,
fields: Some(fields),
};
operation.validate()?;
Ok(operation)
}
pub fn new_update(
schema: SchemaId,
previous_operations: DocumentViewId,
fields: OperationFields,
) -> Result<Self, OperationError> {
let operation = Self {
action: OperationAction::Update,
version: OperationVersion::Default,
schema,
previous_operations: Some(previous_operations),
fields: Some(fields),
};
operation.validate()?;
Ok(operation)
}
pub fn new_delete(
schema: SchemaId,
previous_operations: DocumentViewId,
) -> Result<Self, OperationError> {
let operation = Self {
action: OperationAction::Delete,
version: OperationVersion::Default,
schema,
previous_operations: Some(previous_operations),
fields: None,
};
operation.validate()?;
Ok(operation)
}
pub fn to_cbor(&self) -> Vec<u8> {
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&self, &mut cbor_bytes).unwrap();
cbor_bytes
}
}
impl AsOperation for Operation {
fn action(&self) -> OperationAction {
self.action.to_owned()
}
fn version(&self) -> OperationVersion {
self.version.to_owned()
}
fn schema(&self) -> SchemaId {
self.schema.to_owned()
}
fn fields(&self) -> Option<OperationFields> {
self.fields.clone()
}
fn previous_operations(&self) -> Option<DocumentViewId> {
self.previous_operations.clone()
}
}
impl From<&OperationEncoded> for Operation {
fn from(operation_encoded: &OperationEncoded) -> Self {
ciborium::de::from_reader(&operation_encoded.to_bytes()[..]).unwrap()
}
}
impl PartialEq for Operation {
fn eq(&self, other: &Self) -> bool {
self.to_cbor() == other.to_cbor()
}
}
impl Eq for Operation {}
impl StdHash for Operation {
fn hash<H: Hasher>(&self, state: &mut H) {
self.to_cbor().hash(state);
}
}
impl Validate for Operation {
type Error = OperationError;
fn validate(&self) -> Result<(), Self::Error> {
if !self.is_delete() && (!self.has_fields() || self.fields().unwrap().is_empty()) {
return Err(OperationError::EmptyFields);
}
if self.is_delete() && self.has_fields() {
return Err(OperationError::DeleteWithFields);
}
if !self.is_create() && (!self.has_previous_operations()) {
return Err(OperationError::EmptyPreviousOperations);
}
if self.is_create() && (self.has_previous_operations()) {
return Err(OperationError::ExistingPreviousOperations);
}
if self.has_fields() {
self.fields().unwrap().validate()?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::convert::TryFrom;
use rstest::rstest;
use rstest_reuse::apply;
use crate::document::{DocumentId, DocumentViewId};
use crate::operation::{AsOperation, OperationEncoded, OperationValue, Relation};
use crate::schema::SchemaId;
use crate::test_utils::fixtures::{
operation_fields, random_document_id, random_document_view_id, schema,
};
use crate::test_utils::templates::many_valid_operations;
use crate::Validate;
use super::{Operation, OperationAction, OperationFields, OperationVersion};
#[test]
fn stringify_action() {
assert_eq!(OperationAction::Create.as_str(), "create");
assert_eq!(OperationAction::Update.as_str(), "update");
assert_eq!(OperationAction::Delete.as_str(), "delete");
}
#[rstest]
fn operation_validation(
operation_fields: OperationFields,
schema: SchemaId,
#[from(random_document_view_id)] prev_op_id: DocumentViewId,
) {
let invalid_create_operation_1 = Operation {
action: OperationAction::Create,
version: OperationVersion::Default,
schema: schema.clone(),
previous_operations: None,
fields: None, };
assert!(invalid_create_operation_1.validate().is_err());
let invalid_create_operation_2 = Operation {
action: OperationAction::Create,
version: OperationVersion::Default,
schema: schema.clone(),
previous_operations: Some(prev_op_id.clone()), fields: Some(operation_fields.clone()),
};
assert!(invalid_create_operation_2.validate().is_err());
let invalid_update_operation_1 = Operation {
action: OperationAction::Update,
version: OperationVersion::Default,
schema: schema.clone(),
previous_operations: None, fields: Some(operation_fields.clone()),
};
assert!(invalid_update_operation_1.validate().is_err());
let invalid_update_operation_2 = Operation {
action: OperationAction::Update,
version: OperationVersion::Default,
schema: schema.clone(),
previous_operations: Some(prev_op_id.clone()),
fields: None, };
assert!(invalid_update_operation_2.validate().is_err());
let invalid_delete_operation_1 = Operation {
action: OperationAction::Delete,
version: OperationVersion::Default,
schema: schema.clone(),
previous_operations: None, fields: None,
};
assert!(invalid_delete_operation_1.validate().is_err());
let invalid_delete_operation_2 = Operation {
action: OperationAction::Delete,
version: OperationVersion::Default,
schema,
previous_operations: Some(prev_op_id),
fields: Some(operation_fields), };
assert!(invalid_delete_operation_2.validate().is_err());
}
#[rstest]
fn encode_and_decode(
schema: SchemaId,
#[from(random_document_view_id)] prev_op_id: DocumentViewId,
#[from(random_document_id)] document_id: DocumentId,
) {
let mut fields = OperationFields::new();
fields
.add("username", OperationValue::Text("bubu".to_owned()))
.unwrap();
fields.add("height", OperationValue::Float(3.5)).unwrap();
fields.add("age", OperationValue::Integer(28)).unwrap();
fields
.add("is_admin", OperationValue::Boolean(false))
.unwrap();
fields
.add(
"profile_picture",
OperationValue::Relation(Relation::new(document_id)),
)
.unwrap();
let operation = Operation::new_update(schema, prev_op_id, fields).unwrap();
assert!(operation.is_update());
let encoded = OperationEncoded::try_from(&operation).unwrap();
let operation_restored = Operation::try_from(&encoded).unwrap();
assert_eq!(operation, operation_restored);
}
#[rstest]
fn field_ordering(schema: SchemaId) {
let mut fields = OperationFields::new();
fields
.add("a", OperationValue::Text("sloth".to_owned()))
.unwrap();
fields
.add("b", OperationValue::Text("penguin".to_owned()))
.unwrap();
let first_operation = Operation::new_create(schema.clone(), fields).unwrap();
let mut second_fields = OperationFields::new();
second_fields
.add("b", OperationValue::Text("penguin".to_owned()))
.unwrap();
second_fields
.add("a", OperationValue::Text("sloth".to_owned()))
.unwrap();
let second_operation = Operation::new_create(schema, second_fields).unwrap();
assert_eq!(first_operation.to_cbor(), second_operation.to_cbor());
}
#[test]
fn field_iteration() {
let mut fields = OperationFields::new();
fields
.add("a", OperationValue::Text("sloth".to_owned()))
.unwrap();
fields
.add("b", OperationValue::Text("penguin".to_owned()))
.unwrap();
let mut field_iterator = fields.iter();
assert_eq!(
field_iterator.next().unwrap().1,
&OperationValue::Text("sloth".to_owned())
);
assert_eq!(
field_iterator.next().unwrap().1,
&OperationValue::Text("penguin".to_owned())
);
}
#[apply(many_valid_operations)]
fn many_valid_operations_should_encode(#[case] operation: Operation) {
assert!(OperationEncoded::try_from(&operation).is_ok())
}
#[apply(many_valid_operations)]
fn it_hashes(#[case] operation: Operation) {
let mut hash_map = HashMap::new();
let key_value = "Value identified by a hash".to_string();
hash_map.insert(&operation, key_value.clone());
let key_value_retrieved = hash_map.get(&operation).unwrap().to_owned();
assert_eq!(key_value, key_value_retrieved)
}
}