use std::convert::TryFrom;
use std::fmt::{Debug, Display};
use crate::document::error::{DocumentBuilderError, DocumentReducerError};
use crate::document::traits::AsDocument;
use crate::document::{DocumentId, DocumentViewFields, DocumentViewId};
use crate::graph::{Graph, Reducer};
use crate::hash::HashId;
use crate::identity::PublicKey;
use crate::operation::traits::{AsOperation, WithPublicKey};
use crate::operation::{Operation, OperationId};
use crate::schema::SchemaId;
use crate::{Human, WithId};
use super::error::DocumentError;
#[derive(Debug, Clone)]
pub struct Document {
id: DocumentId,
fields: Option<DocumentViewFields>,
schema_id: SchemaId,
view_id: DocumentViewId,
author: PublicKey,
}
impl AsDocument for Document {
fn id(&self) -> &DocumentId {
&self.id
}
fn view_id(&self) -> &DocumentViewId {
&self.view_id
}
fn author(&self) -> &PublicKey {
&self.author
}
fn schema_id(&self) -> &SchemaId {
&self.schema_id
}
fn fields(&self) -> Option<&DocumentViewFields> {
self.fields.as_ref()
}
fn update_view(&mut self, id: &DocumentViewId, view: Option<&DocumentViewFields>) {
id.clone_into(&mut self.view_id);
self.fields = view.cloned();
}
}
impl Display for Document {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.id)
}
}
impl Human for Document {
fn display(&self) -> String {
let offset = yasmf_hash::MAX_YAMF_HASH_SIZE * 2 - 6;
format!("<Document {}>", &self.id.as_str()[offset..])
}
}
impl<T> TryFrom<Vec<&T>> for Document
where
T: AsOperation + WithId<OperationId> + WithPublicKey,
{
type Error = DocumentBuilderError;
fn try_from(operations: Vec<&T>) -> Result<Self, Self::Error> {
let document_builder: DocumentBuilder = operations.into();
let (document, _) = document_builder.build()?;
Ok(document)
}
}
impl<T> TryFrom<&Vec<T>> for Document
where
T: AsOperation + WithId<OperationId> + WithPublicKey,
{
type Error = DocumentBuilderError;
fn try_from(operations: &Vec<T>) -> Result<Self, Self::Error> {
let document_builder: DocumentBuilder = operations.into();
let (document, _) = document_builder.build()?;
Ok(document)
}
}
#[derive(Debug, Default)]
struct DocumentReducer {
document: Option<Document>,
}
impl Reducer<(OperationId, Operation, PublicKey)> for DocumentReducer {
type Error = DocumentReducerError;
fn combine(&mut self, value: &(OperationId, Operation, PublicKey)) -> Result<(), Self::Error> {
let (operation_id, operation, public_key) = value;
let document = self.document.clone();
match document {
Some(mut document) => {
match document.commit(operation_id, operation) {
Ok(_) => Ok(()),
Err(err) => match err {
DocumentError::PreviousDoesNotMatch(_) => {
document.commit_unchecked(operation_id, operation);
Ok(())
}
err => Err(err),
},
}?;
self.document = Some(document);
Ok(())
}
None => {
if !operation.is_create() {
return Err(DocumentReducerError::FirstOperationNotCreate);
}
let document_fields = DocumentViewFields::new_from_operation_fields(
operation_id,
&operation.fields().unwrap(),
);
let document = Document {
id: operation_id.as_hash().clone().into(),
fields: Some(document_fields),
schema_id: operation.schema_id(),
view_id: DocumentViewId::new(&[operation_id.to_owned()]),
author: public_key.to_owned(),
};
self.document = Some(document);
Ok(())
}
}
}
}
type PublishedOperation = (OperationId, Operation, PublicKey);
type OperationGraph = Graph<OperationId, PublishedOperation>;
#[derive(Debug, Clone)]
pub struct DocumentBuilder(Vec<(OperationId, Operation, PublicKey)>);
impl DocumentBuilder {
pub fn new(operations: Vec<(OperationId, Operation, PublicKey)>) -> Self {
Self(operations)
}
pub fn operations(&self) -> &Vec<PublishedOperation> {
&self.0
}
pub fn build(&self) -> Result<(Document, Vec<PublishedOperation>), DocumentBuilderError> {
let mut graph = self.construct_graph()?;
self.reduce_document(&mut graph)
}
pub fn build_to_view_id(
&self,
document_view_id: DocumentViewId,
) -> Result<(Document, Vec<PublishedOperation>), DocumentBuilderError> {
let mut graph = self.construct_graph()?;
graph = graph.trim(document_view_id.graph_tips())?;
self.reduce_document(&mut graph)
}
fn construct_graph(&self) -> Result<OperationGraph, DocumentBuilderError> {
let mut graph = Graph::new();
let mut create_seen = false;
for (id, operation, public_key) in &self.0 {
if operation.is_create() && create_seen {
return Err(DocumentBuilderError::MultipleCreateOperations);
};
if operation.is_create() {
create_seen = true;
}
graph.add_node(id, (id.to_owned(), operation.to_owned(), *public_key));
}
for (id, operation, _public_key) in &self.0 {
if let Some(previous) = operation.previous() {
for previous in previous.iter() {
let success = graph.add_link(previous, id);
if !success {
return Err(DocumentBuilderError::InvalidOperationLink(id.to_owned()));
}
}
}
}
Ok(graph)
}
fn reduce_document(
&self,
graph: &mut OperationGraph,
) -> Result<(Document, Vec<PublishedOperation>), DocumentBuilderError> {
let mut document_reducer = DocumentReducer::default();
let graph_data = graph.reduce(&mut document_reducer)?;
let graph_tips: Vec<OperationId> = graph_data
.current_graph_tips()
.iter()
.map(|(id, _, _)| id.to_owned())
.collect();
let mut document = document_reducer.document.unwrap();
document.view_id = DocumentViewId::new(&graph_tips);
Ok((document, graph_data.sorted()))
}
}
impl<T> From<Vec<&T>> for DocumentBuilder
where
T: AsOperation + WithId<OperationId> + WithPublicKey,
{
fn from(operations: Vec<&T>) -> Self {
let operations = operations
.into_iter()
.map(|operation| {
(
operation.id().to_owned(),
operation.into(),
operation.public_key().to_owned(),
)
})
.collect();
Self(operations)
}
}
impl<T> From<&Vec<T>> for DocumentBuilder
where
T: AsOperation + WithId<OperationId> + WithPublicKey,
{
fn from(operations: &Vec<T>) -> Self {
let operations = operations
.iter()
.map(|operation| {
(
operation.id().to_owned(),
operation.into(),
operation.public_key().to_owned(),
)
})
.collect();
Self(operations)
}
}
#[cfg(test)]
mod tests {
use std::convert::{TryFrom, TryInto};
use rstest::rstest;
use crate::document::traits::AsDocument;
use crate::document::{
Document, DocumentId, DocumentViewFields, DocumentViewId, DocumentViewValue,
};
use crate::entry::traits::AsEncodedEntry;
use crate::identity::KeyPair;
use crate::operation::{OperationAction, OperationBuilder, OperationId, OperationValue};
use crate::schema::{FieldType, Schema, SchemaId, SchemaName};
use crate::test_utils::constants::{self, PRIVATE_KEY};
use crate::test_utils::fixtures::{
operation, operation_fields, published_operation, random_document_view_id,
random_operation_id, schema,
};
use crate::test_utils::memory_store::helpers::send_to_store;
use crate::test_utils::memory_store::{MemoryStore, PublishedOperation};
use crate::{Human, WithId};
use super::DocumentBuilder;
#[rstest]
fn string_representation(#[from(published_operation)] operation: PublishedOperation) {
let document: Document = vec![&operation].try_into().unwrap();
assert_eq!(
document.to_string(),
"00207f8ffabff270f21098a457b900b4989b7272a6cb637f3c938b06be0a77b708ed"
);
assert_eq!(document.display(), "<Document b708ed>");
assert_eq!(
document.id().as_str(),
"00207f8ffabff270f21098a457b900b4989b7272a6cb637f3c938b06be0a77b708ed"
);
}
#[rstest]
#[tokio::test]
async fn resolve_documents(
#[with(vec![("name".to_string(), FieldType::String)])] schema: Schema,
) {
let panda = KeyPair::from_private_key_str(
"ddcafe34db2625af34c8ba3cf35d46e23283d908c9848c8b43d1f5d0fde779ea",
)
.unwrap();
let penguin = KeyPair::from_private_key_str(
"1c86b2524b48f0ba86103cddc6bdfd87774ab77ab4c0ea989ed0eeab3d28827a",
)
.unwrap();
let store = MemoryStore::default();
let panda_operation_1 = OperationBuilder::new(schema.id())
.action(OperationAction::Create)
.fields(&[("name", OperationValue::String("Panda Cafe".to_string()))])
.build()
.unwrap();
let (panda_entry_1, _) = send_to_store(&store, &panda_operation_1, &schema, &panda)
.await
.unwrap();
let panda_operation_2 = OperationBuilder::new(schema.id())
.action(OperationAction::Update)
.fields(&[("name", OperationValue::String("Panda Cafe!".to_string()))])
.previous(&panda_entry_1.hash().into())
.build()
.unwrap();
let (panda_entry_2, _) = send_to_store(&store, &panda_operation_2, &schema, &panda)
.await
.unwrap();
let penguin_operation_1 = OperationBuilder::new(schema.id())
.action(OperationAction::Update)
.fields(&[(
"name",
OperationValue::String("Penguin Cafe!!!".to_string()),
)])
.previous(&panda_entry_1.hash().into())
.build()
.unwrap();
let (penguin_entry_1, _) = send_to_store(&store, &penguin_operation_1, &schema, &penguin)
.await
.unwrap();
let penguin_operation_2 = OperationBuilder::new(schema.id())
.action(OperationAction::Update)
.fields(&[(
"name",
OperationValue::String("Polar Bear Cafe".to_string()),
)])
.previous(&DocumentViewId::new(&[
penguin_entry_1.hash().into(),
panda_entry_2.hash().into(),
]))
.build()
.unwrap();
let (penguin_entry_2, _) = send_to_store(&store, &penguin_operation_2, &schema, &penguin)
.await
.unwrap();
let penguin_operation_3 = OperationBuilder::new(schema.id())
.action(OperationAction::Update)
.fields(&[(
"name",
OperationValue::String("Polar Bear Cafe!!!!!!!!!!".to_string()),
)])
.previous(&penguin_entry_2.hash().into())
.build()
.unwrap();
let (penguin_entry_3, _) = send_to_store(&store, &penguin_operation_3, &schema, &penguin)
.await
.unwrap();
let operations = store.operations.lock().unwrap();
let operations = operations.values().collect::<Vec<&PublishedOperation>>();
let document = Document::try_from(operations.clone());
assert!(document.is_ok(), "{:#?}", document);
let document = document.unwrap();
let mut exp_result = DocumentViewFields::new();
exp_result.insert(
"name",
DocumentViewValue::new(
&penguin_entry_3.hash().into(),
&OperationValue::String("Polar Bear Cafe!!!!!!!!!!".to_string()),
),
);
let document_id = DocumentId::new(&panda_entry_1.hash().into());
let expected_graph_tips: Vec<OperationId> = vec![penguin_entry_3.hash().into()];
assert_eq!(
document.fields().unwrap().get("name"),
exp_result.get("name")
);
assert!(document.is_edited());
assert!(!document.is_deleted());
assert_eq!(document.author(), &panda.public_key());
assert_eq!(document.schema_id(), schema.id());
assert_eq!(document.view_id().graph_tips(), expected_graph_tips);
assert_eq!(document.id(), &document_id);
let replica_1: Document = vec![
operations[4],
operations[3],
operations[2],
operations[1],
operations[0],
]
.try_into()
.unwrap();
let replica_2: Document = vec![
operations[2],
operations[1],
operations[0],
operations[4],
operations[3],
]
.try_into()
.unwrap();
assert_eq!(
replica_1.fields().unwrap().get("name"),
exp_result.get("name")
);
assert!(replica_1.is_edited());
assert!(!replica_1.is_deleted());
assert_eq!(replica_1.author(), &panda.public_key());
assert_eq!(replica_1.schema_id(), schema.id());
assert_eq!(replica_1.view_id().graph_tips(), expected_graph_tips);
assert_eq!(replica_1.id(), &document_id);
assert_eq!(
replica_1.fields().unwrap().get("name"),
replica_2.fields().unwrap().get("name")
);
assert_eq!(replica_1.id(), replica_2.id());
assert_eq!(
replica_1.view_id().graph_tips(),
replica_2.view_id().graph_tips(),
);
}
#[rstest]
fn must_have_create_operation(
#[from(published_operation)]
#[with(
Some(operation_fields(constants::test_fields())),
constants::schema(),
Some(random_document_view_id())
)]
update_operation: PublishedOperation,
) {
let document: Result<Document, _> = vec![&update_operation].try_into();
assert_eq!(
document.unwrap_err().to_string(),
format!(
"operation {} cannot be connected to the document graph",
WithId::<OperationId>::id(&update_operation)
)
);
}
#[rstest]
#[tokio::test]
async fn incorrect_previous_operations(
#[from(published_operation)]
#[with(Some(operation_fields(constants::test_fields())), constants::schema())]
create_operation: PublishedOperation,
#[from(published_operation)]
#[with(
Some(operation_fields(constants::test_fields())),
constants::schema(),
Some(random_document_view_id())
)]
update_operation: PublishedOperation,
) {
let document: Result<Document, _> = vec![&create_operation, &update_operation].try_into();
assert_eq!(
document.unwrap_err().to_string(),
format!(
"operation {} cannot be connected to the document graph",
WithId::<OperationId>::id(&update_operation).clone()
)
);
}
#[rstest]
#[tokio::test]
async fn operation_schemas_not_matching() {
let create_operation = published_operation(
Some(operation_fields(constants::test_fields())),
constants::schema(),
None,
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
);
let update_operation = published_operation(
Some(operation_fields(vec![
("name", "is_cute".into()),
("type", "bool".into()),
])),
Schema::get_system(SchemaId::SchemaFieldDefinition(1))
.unwrap()
.to_owned(),
Some(WithId::<OperationId>::id(&create_operation).clone().into()),
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
);
let document: Result<Document, _> = vec![&create_operation, &update_operation].try_into();
assert_eq!(
document.unwrap_err().to_string(),
"Could not perform reducer function: Operation 0020b7674a56756183f7d2c6afa20e06041a9a9a30b0aec728e35acf281ecff2b544 does not match the documents schema".to_string()
);
}
#[rstest]
#[tokio::test]
async fn is_deleted(
#[from(published_operation)]
#[with(Some(operation_fields(constants::test_fields())), constants::schema())]
create_operation: PublishedOperation,
) {
let delete_operation = published_operation(
None,
constants::schema(),
Some(DocumentViewId::new(&[WithId::<OperationId>::id(
&create_operation,
)
.clone()])),
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
);
let document: Document = vec![&create_operation, &delete_operation]
.try_into()
.unwrap();
assert!(document.is_deleted());
assert!(document.fields().is_none());
}
#[rstest]
#[tokio::test]
async fn more_than_one_create(
#[from(published_operation)] create_operation: PublishedOperation,
) {
let document: Result<Document, _> = vec![&create_operation, &create_operation].try_into();
assert_eq!(
document.unwrap_err().to_string(),
"multiple CREATE operations found when building operation graph".to_string()
);
}
#[rstest]
#[tokio::test]
async fn fields(#[with(vec![("name".to_string(), FieldType::String)])] schema: Schema) {
let mut operations = Vec::new();
let panda = KeyPair::new().public_key().to_owned();
let penguin = KeyPair::new().public_key().to_owned();
let operation_1_id = random_operation_id();
let operation = OperationBuilder::new(schema.id())
.action(OperationAction::Create)
.fields(&[("name", OperationValue::String("Panda Cafe".to_string()))])
.build()
.unwrap();
operations.push((operation_1_id.clone(), operation, panda));
let operation_2_id = random_operation_id();
let operation = OperationBuilder::new(schema.id())
.action(OperationAction::Update)
.fields(&[("name", OperationValue::String("Panda Cafe!".to_string()))])
.previous(&DocumentViewId::new(&[operation_1_id.clone()]))
.build()
.unwrap();
operations.push((operation_2_id.clone(), operation, panda));
let operation_3_id = random_operation_id();
let operation = OperationBuilder::new(schema.id())
.action(OperationAction::Update)
.fields(&[(
"name",
OperationValue::String("Penguin Cafe!!!".to_string()),
)])
.previous(&DocumentViewId::new(&[operation_2_id.clone()]))
.build()
.unwrap();
operations.push((operation_3_id.clone(), operation, penguin));
let document_builder = DocumentBuilder::new(operations);
let (document, _) = document_builder
.build_to_view_id(DocumentViewId::new(&[operation_1_id]))
.unwrap();
assert_eq!(
document.fields().unwrap().get("name").unwrap().value(),
&OperationValue::String("Panda Cafe".to_string())
);
let (document, _) = document_builder
.build_to_view_id(DocumentViewId::new(&[operation_2_id.clone()]))
.unwrap();
assert_eq!(
document.fields().unwrap().get("name").unwrap().value(),
&OperationValue::String("Panda Cafe!".to_string())
);
let (document, _) = document_builder
.build_to_view_id(DocumentViewId::new(&[operation_3_id.clone()]))
.unwrap();
assert_eq!(
document.fields().unwrap().get("name").unwrap().value(),
&OperationValue::String("Penguin Cafe!!!".to_string())
);
let (document, _) = document_builder
.build_to_view_id(DocumentViewId::new(&[operation_2_id, operation_3_id]))
.unwrap();
assert_eq!(
document.fields().unwrap().get("name").unwrap().value(),
&OperationValue::String("Penguin Cafe!!!".to_string())
);
}
#[rstest]
#[tokio::test]
async fn apply_commit(
#[from(published_operation)]
#[with(Some(operation_fields(constants::test_fields())), constants::schema())]
create_operation: PublishedOperation,
) {
let create_view_id =
DocumentViewId::new(&[WithId::<OperationId>::id(&create_operation).clone()]);
let update_operation = operation(
Some(operation_fields(vec![("age", OperationValue::Integer(21))])),
Some(create_view_id.clone()),
constants::schema().id().to_owned(),
);
let update_operation_id = random_operation_id();
let update_view_id = DocumentViewId::new(&[update_operation_id.clone()]);
let delete_operation = operation(
None,
Some(update_view_id.clone()),
constants::schema().id().to_owned(),
);
let delete_operation_id = random_operation_id();
let delete_view_id = DocumentViewId::new(&[delete_operation_id.clone()]);
let mut document: Document = vec![&create_operation].try_into().unwrap();
assert!(!document.is_edited());
assert_eq!(document.view_id(), &create_view_id);
assert_eq!(document.get("age").unwrap(), &OperationValue::Integer(28));
document
.commit(&update_operation_id, &update_operation)
.unwrap();
assert!(document.is_edited());
assert_eq!(document.view_id(), &update_view_id);
assert_eq!(document.get("age").unwrap(), &OperationValue::Integer(21));
document
.commit(&delete_operation_id, &delete_operation)
.unwrap();
assert!(document.is_deleted());
assert_eq!(document.view_id(), &delete_view_id);
assert_eq!(document.fields(), None);
}
#[rstest]
#[tokio::test]
async fn validate_commit_operation(
#[from(published_operation)]
#[with(Some(operation_fields(constants::test_fields())), constants::schema())]
create_operation: PublishedOperation,
) {
let mut document: Document = vec![&create_operation].try_into().unwrap();
assert!(document
.commit(create_operation.id(), &create_operation)
.is_err());
let create_view_id =
DocumentViewId::new(&[WithId::<OperationId>::id(&create_operation).clone()]);
let schema_name = SchemaName::new("my_wrong_schema").expect("Valid schema name");
let update_with_incorrect_schema_id = published_operation(
Some(operation_fields(vec![("age", OperationValue::Integer(21))])),
schema(
vec![("age".into(), FieldType::Integer)],
SchemaId::new_application(&schema_name, &random_document_view_id()),
"Schema with a wrong id",
),
Some(create_view_id.clone()),
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
);
assert!(document
.commit(
update_with_incorrect_schema_id.id(),
&update_with_incorrect_schema_id
)
.is_err());
let update_not_referring_to_current_view = published_operation(
Some(operation_fields(vec![("age", OperationValue::Integer(21))])),
constants::schema(),
Some(random_document_view_id()),
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
);
assert!(document
.commit(
update_not_referring_to_current_view.id(),
&update_not_referring_to_current_view
)
.is_err());
let delete_operation = published_operation(
None,
constants::schema(),
Some(create_view_id.clone()),
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
);
assert!(document
.commit(delete_operation.id(), &delete_operation)
.is_ok());
let delete_view_id =
DocumentViewId::new(&[WithId::<OperationId>::id(&delete_operation).clone()]);
let update_on_a_deleted_document = published_operation(
Some(operation_fields(vec![("age", OperationValue::Integer(21))])),
constants::schema(),
Some(delete_view_id.to_owned()),
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
);
assert!(document
.commit(
update_on_a_deleted_document.id(),
&update_on_a_deleted_document
)
.is_err());
}
}