use crate::document::DocumentViewId;
use crate::entry::traits::{AsEncodedEntry, AsEntry};
use crate::entry::validate::{validate_log_integrity, validate_payload};
use crate::hash::Hash;
use crate::operation::error::ValidateOperationError;
use crate::operation::plain::{PlainFields, PlainOperation};
use crate::operation::traits::{Actionable, Schematic};
use crate::operation::{
EncodedOperation, Operation, OperationAction, OperationId, OperationVersion,
};
use crate::schema::validate::{validate_all_fields, validate_only_given_fields};
use crate::schema::Schema;
use crate::Human;
#[allow(clippy::too_many_arguments)]
pub fn validate_operation_with_entry(
entry: &impl AsEntry,
entry_encoded: &impl AsEncodedEntry,
skiplink: Option<(&impl AsEntry, &Hash)>,
backlink: Option<(&impl AsEntry, &Hash)>,
plain_operation: &PlainOperation,
operation_encoded: &EncodedOperation,
schema: &Schema,
) -> Result<(Operation, OperationId), ValidateOperationError> {
validate_payload(entry, operation_encoded)?;
validate_log_integrity(entry, skiplink, backlink)?;
let operation_id = entry_encoded.hash().into();
let operation = validate_operation(plain_operation, schema)?;
Ok((operation, operation_id))
}
pub fn validate_operation_format(
operation: &(impl Actionable + Schematic),
) -> Result<(), ValidateOperationError> {
match operation.action() {
OperationAction::Create => {
let _ = validate_create_operation_format(operation.previous(), operation.fields())?;
Ok(())
}
OperationAction::Update => {
let _ = validate_update_operation_format(operation.previous(), operation.fields())?;
Ok(())
}
OperationAction::Delete => {
validate_delete_operation_format(operation.previous(), operation.fields())
}
}
}
pub fn validate_operation(
operation: &(impl Actionable + Schematic),
schema: &Schema,
) -> Result<Operation, ValidateOperationError> {
let previous = operation.previous();
let fields = operation.fields();
if operation.schema_id() != schema.id() {
return Err(ValidateOperationError::SchemaNotMatching(
operation.schema_id().display(),
schema.id().display(),
));
}
match operation.action() {
OperationAction::Create => validate_create_operation(previous, fields, schema),
OperationAction::Update => validate_update_operation(previous, fields, schema),
OperationAction::Delete => validate_delete_operation(previous, fields, schema),
}
}
fn validate_create_operation_format(
plain_previous_operations: Option<&DocumentViewId>,
plain_fields: Option<PlainFields>,
) -> Result<PlainFields, ValidateOperationError> {
match (plain_fields, plain_previous_operations) {
(None, _) => Err(ValidateOperationError::ExpectedFields),
(Some(_), Some(_)) => Err(ValidateOperationError::UnexpectedPreviousOperations),
(Some(fields), None) => Ok(fields),
}
}
fn validate_update_operation_format(
plain_previous_operations: Option<&DocumentViewId>,
plain_fields: Option<PlainFields>,
) -> Result<PlainFields, ValidateOperationError> {
match (plain_fields, plain_previous_operations) {
(None, _) => Err(ValidateOperationError::ExpectedFields),
(Some(_), None) => Err(ValidateOperationError::ExpectedPreviousOperations),
(Some(fields), Some(_)) => Ok(fields),
}
}
fn validate_delete_operation_format(
plain_previous_operations: Option<&DocumentViewId>,
plain_fields: Option<PlainFields>,
) -> Result<(), ValidateOperationError> {
match (plain_fields, plain_previous_operations) {
(Some(_), _) => Err(ValidateOperationError::UnexpectedFields),
(None, None) => Err(ValidateOperationError::ExpectedPreviousOperations),
(None, Some(_)) => Ok(()),
}
}
fn validate_create_operation(
plain_previous_operations: Option<&DocumentViewId>,
plain_fields: Option<PlainFields>,
schema: &Schema,
) -> Result<Operation, ValidateOperationError> {
let fields = validate_create_operation_format(plain_previous_operations, plain_fields)?;
let validated_fields = validate_all_fields(&fields, schema)?;
Ok(Operation {
version: OperationVersion::V1,
action: OperationAction::Create,
schema_id: schema.id().to_owned(),
previous: None,
fields: Some(validated_fields),
})
}
fn validate_update_operation(
plain_previous_operations: Option<&DocumentViewId>,
plain_fields: Option<PlainFields>,
schema: &Schema,
) -> Result<Operation, ValidateOperationError> {
let fields = validate_update_operation_format(plain_previous_operations, plain_fields)?;
let validated_fields = validate_only_given_fields(&fields, schema)?;
Ok(Operation {
version: OperationVersion::V1,
action: OperationAction::Update,
schema_id: schema.id().to_owned(),
previous: plain_previous_operations.cloned(),
fields: Some(validated_fields),
})
}
fn validate_delete_operation(
plain_previous_operations: Option<&DocumentViewId>,
plain_fields: Option<PlainFields>,
schema: &Schema,
) -> Result<Operation, ValidateOperationError> {
validate_delete_operation_format(plain_previous_operations, plain_fields)?;
Ok(Operation {
version: OperationVersion::V1,
action: OperationAction::Delete,
schema_id: schema.id().to_owned(),
previous: plain_previous_operations.cloned(),
fields: None,
})
}
#[cfg(test)]
mod tests {
use ciborium::cbor;
use ciborium::value::{Error, Value};
use rstest::rstest;
use rstest_reuse::apply;
use crate::document::{DocumentId, DocumentViewId};
use crate::operation::decode::decode_operation;
use crate::operation::plain::PlainOperation;
use crate::operation::{EncodedOperation, OperationAction, OperationBuilder};
use crate::schema::{FieldType, Schema, SchemaId};
use crate::test_utils::constants::{HASH, SCHEMA_ID};
use crate::test_utils::fixtures::{document_id, document_view_id, schema, schema_id, Fixture};
use crate::test_utils::templates::version_fixtures;
use super::validate_operation;
fn cbor_to_plain(value: Value) -> PlainOperation {
let mut cbor_bytes = Vec::new();
ciborium::ser::into_writer(&value, &mut cbor_bytes).unwrap();
let encoded_operation = EncodedOperation::new(&cbor_bytes);
decode_operation(&encoded_operation).unwrap()
}
#[rstest]
#[case(
vec![
("country", FieldType::Relation(schema_id.clone())),
("national_dish", FieldType::String),
("vegan_friendly", FieldType::Boolean),
("yummyness", FieldType::Integer),
("yumsimumsiness", FieldType::Float),
],
cbor!([
1, 0, SCHEMA_ID,
{
"country" => HASH,
"national_dish" => "Pumpkin",
"vegan_friendly" => true,
"yummyness" => 8,
"yumsimumsiness" => 7.2,
},
]),
)]
fn valid_operations(
#[from(schema_id)] schema_id: SchemaId,
#[case] schema_fields: Vec<(&str, FieldType)>,
#[case] cbor: Result<Value, Error>,
) {
let schema = Schema::new(&schema_id, "Some schema description", &schema_fields)
.expect("Could not create schema");
let plain_operation = cbor_to_plain(cbor.expect("Invalid CBOR value"));
assert!(validate_operation(&plain_operation, &schema).is_ok());
}
#[rstest]
#[case::incomplete_hash(
vec![
("country", FieldType::Relation(schema_id.clone())),
],
cbor!([
1, 0, SCHEMA_ID,
{
"country" => "0020",
},
]),
"field 'country' does not match schema: invalid hash length 2 bytes, expected 34 bytes"
)]
#[case::invalid_hex_encoding(
vec![
("country", FieldType::Relation(schema_id.clone())),
],
cbor!([
1, 0, SCHEMA_ID,
{
"country" => "xyz",
},
]),
"field 'country' does not match schema: invalid hex encoding in hash string"
)]
#[case::missing_field(
vec![
("national_dish", FieldType::String),
],
cbor!([
1, 0, SCHEMA_ID,
{
"vegan_friendly" => true,
},
]),
"field 'vegan_friendly' does not match schema: expected field name 'national_dish'"
)]
fn wrong_operation_fields(
#[from(schema_id)] schema_id: SchemaId,
#[case] schema_fields: Vec<(&str, FieldType)>,
#[case] raw_operation: Result<Value, Error>,
#[case] expected: &str,
) {
let schema = Schema::new(&schema_id, "Some schema description", &schema_fields)
.expect("Could not create schema");
let plain_operation = cbor_to_plain(raw_operation.expect("Invalid CBOR value"));
assert_eq!(
validate_operation(&plain_operation, &schema)
.expect_err("Expect error")
.to_string(),
expected
);
}
#[apply(version_fixtures)]
fn validate_fixture_operation(#[case] fixture: Fixture) {
let plain_operation = decode_operation(&fixture.operation_encoded).unwrap();
assert!(validate_operation(&plain_operation, &fixture.schema).is_ok());
}
#[rstest]
fn operation_schema_validation(
#[with(vec![
("firstname".into(), FieldType::String),
("year".into(), FieldType::Integer),
("is_cute".into(), FieldType::Boolean),
("address".into(), FieldType::Relation(schema_id(SCHEMA_ID))),
])]
schema: Schema,
document_id: DocumentId,
document_view_id: DocumentViewId,
) {
let operation = OperationBuilder::new(schema.id())
.fields(&[
("firstname", "Peter".into()),
("year", 2020.into()),
("is_cute", false.into()),
("address", document_id.clone().into()),
])
.build()
.unwrap();
assert!(validate_operation(&operation, &schema).is_ok());
let operation = OperationBuilder::new(schema.id())
.fields(&[
("address", document_id.clone().into()),
("is_cute", false.into()),
("year", 2020.into()),
("firstname", "Peter".into()),
])
.build()
.unwrap();
assert!(validate_operation(&operation, &schema).is_ok());
let operation = OperationBuilder::new(schema.id())
.fields(&[
("firstname", "Peter".into()),
("is_cute", false.into()),
("address", document_id.clone().into()),
])
.build()
.unwrap();
assert!(validate_operation(&operation, &schema).is_err());
let operation = OperationBuilder::new(schema.id())
.fields(&[
("firstname", "Peter".into()),
("year", "2020".into()),
("is_cute", false.into()),
("address", document_id.clone().into()),
])
.build()
.unwrap();
assert!(validate_operation(&operation, &schema).is_err());
let operation = OperationBuilder::new(schema.id())
.action(OperationAction::Update)
.previous(&document_view_id)
.fields(&[("address", document_id.into())])
.build()
.unwrap();
assert!(validate_operation(&operation, &schema).is_ok());
}
}