use std::fmt::Debug; use std::sync::Arc; use anyhow::Result; use async_trait::async_trait; use cloud_objects::cloud_object::CloudObjectUpsertParams; // Re-exported from cloud_objects. pub use cloud_objects::cloud_object::{GenericStringModel, Serializer}; pub use warp_server_client::ids::GenericStringObjectId; use crate::appearance::Appearance; use crate::cloud_object::{ CloudModelType, CloudObject, CloudObjectEventEntrypoint, CreateCloudObjectResult, CreateObjectRequest, GenericCloudObject, GenericServerObject, GenericStringObjectFormat, GenericStringObjectUniqueKey, ObjectType, Revision, UpdateCloudObjectResult, }; use crate::drive::CloudObjectTypeAndId; use crate::drive::items::WarpDriveItem; use crate::persistence::ModelEvent; use crate::server::cloud_objects::update_manager::InitiatedBy; use crate::server::ids::{ServerId, SyncId}; use crate::server::server_api::object::ObjectClient; use crate::server::sync_queue::{QueueItem, SerializedModel}; /// A trait that generic string-based objects should implement. pub trait CloudStringObject: CloudObject + Send + Sync { /// Returns the object format for this object. fn generic_string_object_format(&self) -> GenericStringObjectFormat; /// Returns the id for this specific object. fn id(&self) -> SyncId; /// Returns a serialized model from this string object. fn serialized(&self) -> SerializedModel; /// Returns a cloned boxed version of this cloud object. /// Note that we can't force this trait to derive from Cloned /// directly because that would make the trait not object safe. This /// is a workaround. fn clone_box(&self) -> Box; } /// A `StringModel` is a model that can be serialized and deserialized as a simple string. /// /// Any model that has a simple string representation (e.g. JSON, markdown, yaml) that can be atomically updated /// can implement this trait and get most cloud object functionality for free. /// /// Objects that implement this type all share common storage and server apis. pub trait StringModel: Clone + Debug + PartialEq + Send + Sync + 'static { type CloudObjectType: CloudObject + 'static; /// Returns the name of this model type (e.g. Workflow, Folder, Notebook) fn model_type_name(&self) -> &'static str; /// Whether we should enforce revisions for this model type. /// If revisions are not enforced, updates will have last-write-wins semantics. /// If revisions are enforced, the object will need to add logic to /// the update manager for how conflicts are resolved. fn should_enforce_revisions() -> bool; /// Returns the serialization format for this model. fn model_format() -> GenericStringObjectFormat; /// Whether to show update toasts for this type of model. fn should_show_activity_toasts() -> bool; /// Whether to show a warning if this type of model is unsaved at quit time /// (which typically blocks the user from quitting) fn warn_if_unsaved_at_quit() -> bool; /// Returns the display name for this model. fn display_name(&self) -> String; /// Returns whether to render this model as a WarpDriveItem. fn renders_in_warp_drive(&self) -> bool { false } /// Returns whether this model can be exported to a file fn can_export(&self) -> bool { false } /// Returns whether this model can be shared via a link fn supports_linking(&self) -> bool { false } /// Sets the display name for this model fn set_display_name(&mut self, _name: &str) {} /// Creates a new warp drive item for this model type. Returns None /// if this object does not render in Warp Drive. fn to_warp_drive_item( &self, _id: SyncId, _appearance: &Appearance, _object: &Self::CloudObjectType, ) -> Option> { None } /// Returns a sync queue item of this object that would allow it to be updated /// properly on the server. Takes an optional revision_ts to set as the revision /// in the sync queue item. fn update_object_queue_item( &self, revision_ts: Option, object: &Self::CloudObjectType, ) -> QueueItem; /// Returns whether this model type should clear on a unique key conflict. fn should_clear_on_unique_key_conflict(&self) -> bool { false } /// Returns a unique key for this object, if one exists. Unique keys are used /// to enforce that only one object with a given key can exist in the generic string /// object server database. fn uniqueness_key(&self) -> Option; } impl CloudStringObject for GenericCloudObject> where M: StringModel< CloudObjectType = GenericCloudObject>, >, S: Serializer, { fn generic_string_object_format(&self) -> GenericStringObjectFormat { M::model_format() } fn id(&self) -> SyncId { self.id } fn serialized(&self) -> SerializedModel { self.model().serialized() } fn clone_box(&self) -> Box { Box::new(self.clone()) } } /// Implements the CloudModelType trait for all generic string models. /// /// This has common logic for storing string models to SQLite, sending them to the server /// updating from the server -- basically for anything not specific to the contents /// of the string model. #[cfg_attr(not(target_family = "wasm"), async_trait)] #[cfg_attr(target_family = "wasm", async_trait(?Send))] impl CloudModelType for GenericStringModel where M: StringModel< CloudObjectType = GenericCloudObject>, >, S: Serializer, { type CloudObjectType = GenericCloudObject; type IdType = GenericStringObjectId; fn model_type_name(&self) -> &'static str { self.string_model.model_type_name() } fn object_type(&self) -> ObjectType { ObjectType::GenericStringObject(M::model_format()) } fn cloud_object_type_and_id(&self, id: SyncId) -> CloudObjectTypeAndId { CloudObjectTypeAndId::GenericStringObject { object_type: M::model_format(), id, } } fn display_name(&self) -> String { self.string_model.display_name() } fn set_display_name(&mut self, name: &str) { self.string_model.set_display_name(name); } fn upsert_event(params: CloudObjectUpsertParams) -> ModelEvent { let object = GenericCloudObject::::from(params); let object = &object as &dyn CloudStringObject; ModelEvent::UpsertGenericStringObject { object: CloudStringObject::clone_box(object), } } fn supports_linking(&self) -> bool { self.string_model.supports_linking() } fn should_show_activity_toasts(&self) -> bool { M::should_show_activity_toasts() } fn warn_if_unsaved_at_quit(&self) -> bool { M::warn_if_unsaved_at_quit() } fn can_export(&self) -> bool { self.string_model.can_export() } fn bulk_upsert_event(objects: Vec>) -> ModelEvent { ModelEvent::UpsertGenericStringObjects( objects .into_iter() .map(|params| { Box::new(GenericCloudObject::::from( params, )) as Box }) .collect(), ) } fn create_object_queue_item( &self, object: &GenericCloudObject, entrypoint: CloudObjectEventEntrypoint, initiated_by: InitiatedBy, ) -> Option { if let SyncId::ClientId(client_id) = object.id { return Some(QueueItem::CreateObject { object_type: self.object_type(), owner: object.permissions.owner, id: client_id, title: None, serialized_model: Some(object.model().serialized().into()), initial_folder_id: object.metadata.folder_id, entrypoint, initiated_by, }); } None } fn update_object_queue_item( &self, revision_ts: Option, object: &GenericCloudObject, ) -> QueueItem { self.string_model .update_object_queue_item(revision_ts, object) } fn should_clear_on_unique_key_conflict(&self) -> bool { self.string_model.should_clear_on_unique_key_conflict() } fn should_update_after_server_conflict(&self) -> bool { true } fn serialized(&self) -> SerializedModel { S::serialize(&self.string_model) } async fn send_create_request( object_client: Arc, request: CreateObjectRequest, ) -> Result { let model_as_str = request .serialized_model .as_ref() .ok_or_else(|| anyhow::anyhow!("Missing serialized model"))? .model_as_str(); let model = S::deserialize_owned(model_as_str)?; object_client .create_generic_string_object(M::model_format(), model.uniqueness_key(), request) .await } async fn send_update_request( &self, object_client: Arc, server_id: ServerId, revision: Option, ) -> Result>> { let revision = if M::should_enforce_revisions() { Some(revision.ok_or_else(|| { anyhow::anyhow!("Missing revision on update of generic object") })?) } else { None }; let res = object_client .update_generic_string_object(server_id.into(), self.serialized(), revision) .await; res.and_then(|update_result| match update_result { UpdateCloudObjectResult::Success { revision_and_editor, } => Ok(UpdateCloudObjectResult::Success { revision_and_editor, }), UpdateCloudObjectResult::Rejected { object } => { // Downcast to the concrete type to handle an update rejection (should be rare) let concrete_object: Option<&GenericServerObject> = (&object).into(); let object = concrete_object .cloned() .ok_or_else(|| anyhow::anyhow!("Failed to convert object to concrete type"))?; Ok(UpdateCloudObjectResult::Rejected { object }) } }) } fn renders_in_warp_drive(&self) -> bool { self.string_model.renders_in_warp_drive() } fn to_warp_drive_item( &self, id: SyncId, appearance: &Appearance, object: &GenericCloudObject, ) -> Option> { self.string_model.to_warp_drive_item(id, appearance, object) } }