328 lines · 11.2 KB · Rust
RawDownload
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<dyn CloudStringObject>;} /// 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<Box<dyn WarpDriveItem>> {        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<Revision>,        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<GenericStringObjectUniqueKey>;} impl<M, S> CloudStringObject for GenericCloudObject<GenericStringObjectId, GenericStringModel<M, S>>where    M: StringModel<        CloudObjectType = GenericCloudObject<GenericStringObjectId, GenericStringModel<M, S>>,    >,    S: Serializer<M>,{    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<dyn CloudStringObject> {        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<M, S> CloudModelType for GenericStringModel<M, S>where    M: StringModel<        CloudObjectType = GenericCloudObject<GenericStringObjectId, GenericStringModel<M, S>>,    >,    S: Serializer<M>,{    type CloudObjectType = GenericCloudObject<GenericStringObjectId, Self>;    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<Self>) -> ModelEvent {        let object = GenericCloudObject::<GenericStringObjectId, Self>::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<CloudObjectUpsertParams<Self>>) -> ModelEvent {        ModelEvent::UpsertGenericStringObjects(            objects                .into_iter()                .map(|params| {                    Box::new(GenericCloudObject::<GenericStringObjectId, Self>::from(                        params,                    )) as Box<dyn CloudStringObject>                })                .collect(),        )    }     fn create_object_queue_item(        &self,        object: &GenericCloudObject<GenericStringObjectId, Self>,        entrypoint: CloudObjectEventEntrypoint,        initiated_by: InitiatedBy,    ) -> Option<QueueItem> {        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<Revision>,        object: &GenericCloudObject<GenericStringObjectId, Self>,    ) -> 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<dyn ObjectClient>,        request: CreateObjectRequest,    ) -> Result<CreateCloudObjectResult> {        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<dyn ObjectClient>,        server_id: ServerId,        revision: Option<Revision>,    ) -> Result<UpdateCloudObjectResult<GenericServerObject<GenericStringObjectId, Self>>> {        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<GenericStringObjectId, Self>> =                    (&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<GenericStringObjectId, Self>,    ) -> Option<Box<dyn WarpDriveItem>> {        self.string_model.to_warp_drive_item(id, appearance, object)    }}