From 3766807de84d488727d90f02985c34c959090a91 Mon Sep 17 00:00:00 2001 From: David Stern Date: Fri, 22 May 2026 14:52:36 -0400 Subject: [PATCH 1/2] Move `GenericServerObject` to `warp_server_client`. --- .../ai/execution_profiles/profiles_tests.rs | 12 +- app/src/cloud_object/mod.rs | 307 ++++++------------ app/src/cloud_object/model/model_tests.rs | 102 +++--- app/src/notebooks/editor/model_tests.rs | 12 +- app/src/notebooks/notebook_tests.rs | 12 +- .../notebooks/data_source_tests.rs | 22 +- .../rules/data_source_tests.rs | 26 +- .../command_palette/data_sources_tests.rs | 24 +- .../cloud_objects/fake_object_client.rs | 8 +- .../cloud_objects/update_manager_tests.rs | 118 +++---- app/src/server/graphql/schema/mod.rs | 269 +++------------ app/src/server/server_api/object.rs | 119 ++----- .../cloud_preferences_syncer_tests.rs | 12 +- app/src/settings/onboarding_tests.rs | 12 +- app/src/workspaces/gql_convert.rs | 276 +++------------- app/src/workspaces/update_manager_tests.rs | 12 +- .../src/cloud_object/mod.rs | 2 + .../src/cloud_object/server_object.rs | 158 +++++++++ 18 files changed, 555 insertions(+), 948 deletions(-) create mode 100644 crates/warp_server_client/src/cloud_object/server_object.rs diff --git a/app/src/ai/execution_profiles/profiles_tests.rs b/app/src/ai/execution_profiles/profiles_tests.rs index 61c9fe80af..55f8fb0161 100644 --- a/app/src/ai/execution_profiles/profiles_tests.rs +++ b/app/src/ai/execution_profiles/profiles_tests.rs @@ -137,12 +137,12 @@ fn reconciles_unsynced_default_profile_with_cloud_after_initial_load() { apply_code_diffs: ActionPermission::AlwaysAllow, ..Default::default() }; - let server_object = ServerAIExecutionProfile { - id: cloud_sync_id, - model: CloudAIExecutionProfileModel::new(cloud_profile), - metadata: mock_server_metadata(cloud_uid), - permissions: ServerPermissions::mock_personal(), - }; + let server_object = ServerAIExecutionProfile::new( + cloud_sync_id, + CloudAIExecutionProfileModel::new(cloud_profile), + mock_server_metadata(cloud_uid), + ServerPermissions::mock_personal(), + ); // Insert the object into CloudModel via the initial-load path // (`emit_events=false`) and then emit `InitialLoadCompleted` so the diff --git a/app/src/cloud_object/mod.rs b/app/src/cloud_object/mod.rs index ced23e52e6..df6afc38bf 100644 --- a/app/src/cloud_object/mod.rs +++ b/app/src/cloud_object/mod.rs @@ -911,6 +911,35 @@ where } } +impl ServerObjectModel for GenericStringModel +where + T: StringModel< + CloudObjectType = GenericCloudObject>, + >, + S: Serializer, +{ + fn object_type(&self) -> ObjectType { + ::object_type(self) + } +} + +impl ServerObjectModel for CloudFolderModel { + fn object_type(&self) -> ObjectType { + ::object_type(self) + } +} + +impl ServerObjectModel for CloudNotebookModel { + fn object_type(&self) -> ObjectType { + ::object_type(self) + } +} + +impl ServerObjectModel for CloudWorkflowModel { + fn object_type(&self) -> ObjectType { + ::object_type(self) + } +} /// Extracts the server id and object type from a (caller validated) Drive link. /// Intended use is deriving metadata from links such that Warp objects /// can be opened natively in Warp with no web interaction. @@ -988,23 +1017,6 @@ impl Clone for Box { } } -#[derive(Clone, Debug, Default)] -pub enum ConflictStatus { - #[default] - NoConflicts, - ConflictingChanges { - object: Arc, - }, -} - -impl ConflictStatus { - /// Utility function that allows for a more ergonomic way of figuring out whether there is a - /// conflict (for cases where we don't care about the conflict details). - pub fn has_conflicts(&self) -> bool { - matches!(self, ConflictStatus::ConflictingChanges { .. }) - } -} - impl From<&dyn CloudObject> for ObjectType { fn from(value: &dyn CloudObject) -> Self { value.object_type() @@ -1214,45 +1226,43 @@ where M: CloudModelType + 'static, { fn from(value: &GenericServerObject) -> Self { - if let Some(server_notebook) = value.as_any().downcast_ref::() { + let value = value as &dyn Any; + if let Some(server_notebook) = value.downcast_ref::() { ServerCloudObject::Notebook(server_notebook.clone()) - } else if let Some(server_workflow) = value.as_any().downcast_ref::() { + } else if let Some(server_workflow) = value.downcast_ref::() { ServerCloudObject::Workflow(Box::new(server_workflow.clone())) - } else if let Some(server_folder) = value.as_any().downcast_ref::() { + } else if let Some(server_folder) = value.downcast_ref::() { ServerCloudObject::Folder(server_folder.clone()) - } else if let Some(server_preferences) = value.as_any().downcast_ref::() { + } else if let Some(server_preferences) = value.downcast_ref::() { ServerCloudObject::Preference(server_preferences.clone()) } else if let Some(server_env_var_collection) = - value.as_any().downcast_ref::() + value.downcast_ref::() { ServerCloudObject::EnvVarCollection(server_env_var_collection.clone()) - } else if let Some(server_workflow_enum) = - value.as_any().downcast_ref::() - { + } else if let Some(server_workflow_enum) = value.downcast_ref::() { ServerCloudObject::WorkflowEnum(server_workflow_enum.clone()) - } else if let Some(server_aifact) = value.as_any().downcast_ref::() { + } else if let Some(server_aifact) = value.downcast_ref::() { ServerCloudObject::AIFact(server_aifact.clone()) - } else if let Some(server_mcp_server) = value.as_any().downcast_ref::() { + } else if let Some(server_mcp_server) = value.downcast_ref::() { ServerCloudObject::MCPServer(server_mcp_server.clone()) } else if let Some(server_ai_execution_profile) = - value.as_any().downcast_ref::() + value.downcast_ref::() { ServerCloudObject::AIExecutionProfile(server_ai_execution_profile.clone()) } else if let Some(server_templatable_mcp_server) = - value.as_any().downcast_ref::() + value.downcast_ref::() { ServerCloudObject::TemplatableMCPServer(server_templatable_mcp_server.clone()) - } else if let Some(server_ambient_agent_environment) = value - .as_any() - .downcast_ref::( - ) { + } else if let Some(server_ambient_agent_environment) = + value.downcast_ref::() + { ServerCloudObject::AmbientAgentEnvironment(server_ambient_agent_environment.clone()) } else if let Some(server_scheduled_ambient_agent) = - value.as_any().downcast_ref::() + value.downcast_ref::() { ServerCloudObject::ScheduledAmbientAgent(server_scheduled_ambient_agent.clone()) } else if let Some(server_cloud_agent_config) = - value.as_any().downcast_ref::() + value.downcast_ref::() { ServerCloudObject::CloudAgentConfig(server_cloud_agent_config.clone()) } else { @@ -1261,89 +1271,6 @@ where } } -/// Common trait for server objects that allows us to use them as trait objects -/// and downcast to concrete types when needed. -pub trait ServerObject: Debug + Send + Sync { - /// Returns the object type of this server object - fn object_type(&self) -> ObjectType; - - /// Returns this object as a ref to the Any type. Needed for typecasts. - fn as_any(&self) -> &dyn Any; - - /// Returns the trait object as a concrete type reference by downcasting it. - /// Returns None if the downcast fails. - fn as_concrete_type( - server_object: &dyn ServerObject, - ) -> Option<&GenericServerObject> - where - Self: Sized, - K: HashableId + ToServerId + Debug + Into + Clone + 'static, - M: CloudModelType + 'static, - { - server_object - .as_any() - .downcast_ref::>() - } - - /// Returns a cloned boxed version of this server object. - /// Note that we can't force the ServerObject trait to derive from Cloned - /// directly because that would make the trait not object safe. This - /// is a workaround. - fn clone_box(&self) -> Box; -} - -/// An object that maps directly to the data returned from the server -/// for a given model and id type. -#[derive(Debug, Clone)] -pub struct GenericServerObject -where - K: HashableId + ToServerId + Debug + Into + Clone + 'static, - M: CloudModelType + 'static, -{ - pub id: SyncId, - pub model: M, - pub metadata: ServerMetadata, - pub permissions: ServerPermissions, -} - -impl<'a, K, M> From<&'a dyn ServerObject> for Option<&'a GenericServerObject> -where - K: HashableId + ToServerId + Debug + Into + Clone + 'static, - M: CloudModelType + 'static, -{ - fn from(value: &'a dyn ServerObject) -> Self { - as ServerObject>::as_concrete_type(value) - } -} - -impl<'a, K, M> From<&'a Box> for Option<&'a GenericServerObject> -where - K: HashableId + ToServerId + Debug + Into + Clone + 'static, - M: CloudModelType + 'static, -{ - fn from(value: &'a Box) -> Self { - as ServerObject>::as_concrete_type(value.as_ref()) - } -} - -impl ServerObject for GenericServerObject -where - K: HashableId + ToServerId + Debug + Into + Clone + 'static, - M: CloudModelType + 'static, -{ - fn object_type(&self) -> ObjectType { - self.model.object_type() - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } -} - pub type ServerPreference = GenericServerObject; pub type ServerFolder = GenericServerObject; pub type ServerWorkflow = GenericServerObject; @@ -1363,107 +1290,83 @@ pub type ServerScheduledAmbientAgent = GenericServerObject; pub type ServerCloudAgentConfig = GenericServerObject; -impl GenericServerObject> +/// Tries to convert a GraphQL object payload into a local server object. +pub trait TryFromGql: Sized { + type GqlType; + + fn try_from_gql(value: Self::GqlType) -> Result; +} + +impl TryFromGql for GenericServerObject> where T: StringModel< CloudObjectType = GenericCloudObject>, >, S: Serializer, { - /// Helper function to create a `ServerObject` that has a GenericStringObjectId from common graphql fields. - pub fn try_from_graphql_fields( - uid: ServerId, - serialized_model: Option, - metadata: ServerMetadata, - permissions: ServerPermissions, - ) -> Result { - if let Some(serialized_model) = serialized_model { - let model = GenericStringModel::::deserialize_owned(&serialized_model)?; - let id = SyncId::ServerId(uid); - Ok(Self { - id, - model, - metadata, - permissions, - }) - } else { - Err(anyhow::anyhow!( - "Missing serialized model in the generic string object value" - )) - } + type GqlType = warp_graphql::generic_string_object::GenericStringObject; + + fn try_from_gql(value: Self::GqlType) -> Result { + let uid = ServerId::from_string_lossy(value.metadata.uid.inner()); + let model = GenericStringModel::::deserialize_owned(&value.serialized_model)?; + Ok(Self::new( + SyncId::ServerId(uid), + model, + value.metadata.try_into()?, + value.permissions.try_into()?, + )) } } -impl ServerFolder { - /// Helper function to create a `ServerFolder` from common graphql fields. - pub fn try_from_graphql_fields( - uid: ServerId, - name: Option, - metadata: ServerMetadata, - permissions: ServerPermissions, - is_warp_pack: bool, - ) -> Result { - match name { - Some(name) => Ok(Self { - id: SyncId::ServerId(uid), - model: CloudFolderModel::new(&name, is_warp_pack), - metadata, - permissions, - }), - _ => Err(anyhow::anyhow!("Missing fields in the folder value")), - } +impl TryFromGql for ServerFolder { + type GqlType = warp_graphql::folder::Folder; + + fn try_from_gql(value: Self::GqlType) -> Result { + let uid = ServerId::from_string_lossy(value.metadata.uid.inner()); + Ok(Self::new( + SyncId::ServerId(uid), + CloudFolderModel::new(&value.name, value.is_warp_pack), + value.metadata.try_into()?, + value.permissions.try_into()?, + )) } } -impl ServerNotebook { - /// Helper function to create a `ServerNotebook` from common graphql fields. - pub fn try_from_graphql_fields( - uid: ServerId, - title: Option, - data: Option, - ai_document_id: Option, - metadata: ServerMetadata, - permissions: ServerPermissions, - ) -> Result { - let ai_document_id: Option = ai_document_id +impl TryFromGql for ServerNotebook { + type GqlType = warp_graphql::notebook::Notebook; + + fn try_from_gql(value: Self::GqlType) -> Result { + let uid = ServerId::from_string_lossy(value.metadata.uid.inner()); + let ai_document_id: Option = value + .ai_document_id .map(|id| AIDocumentId::try_from(&id[..])) .transpose()?; - match (title, data) { - (Some(title), Some(data)) => Ok(Self { - id: SyncId::ServerId(uid), - model: CloudNotebookModel { - title, - data, - ai_document_id, - conversation_id: None, - }, - metadata, - permissions, - }), - (title, data) => Err(anyhow::anyhow!( - "Missing fields in the team notebook value - title: {}, data: {}", - title.is_some(), - data.is_some() - )), - } + Ok(Self::new( + SyncId::ServerId(uid), + CloudNotebookModel { + title: value.title, + data: value.data, + ai_document_id, + conversation_id: None, + }, + value.metadata.try_into()?, + value.permissions.try_into()?, + )) } } -impl ServerWorkflow { - /// Helper function to create a `ServerWorkflow` from common graphql fields. - pub fn try_from_graphql_fields( - uid: ServerId, - data: String, - metadata: ServerMetadata, - permissions: ServerPermissions, - ) -> Result { - let data = serde_json::from_str(data.as_str()); - data.map_err(Into::into).map(|workflow| Self { - id: SyncId::ServerId(uid), - model: CloudWorkflowModel { data: workflow }, - metadata, - permissions, - }) +impl TryFromGql for ServerWorkflow { + type GqlType = warp_graphql::workflow::Workflow; + + fn try_from_gql(value: Self::GqlType) -> Result { + let uid = ServerId::from_string_lossy(value.metadata.uid.inner()); + let workflow = serde_json::from_str(value.data.as_str())?; + Ok(Self::new( + SyncId::ServerId(uid), + CloudWorkflowModel { data: workflow }, + value.metadata.try_into()?, + value.permissions.try_into()?, + )) } } diff --git a/app/src/cloud_object/model/model_tests.rs b/app/src/cloud_object/model/model_tests.rs index 5e88a0b160..ee05abc867 100644 --- a/app/src/cloud_object/model/model_tests.rs +++ b/app/src/cloud_object/model/model_tests.rs @@ -158,14 +158,16 @@ fn mock_server_workflows( number_of_workflows: i64, ) -> Vec { (0..number_of_workflows) - .map(|idx| ServerWorkflow { - id: SyncId::ServerId((start_id + idx).into()), - metadata: mock_server_metadata(), - permissions: mock_server_permissions(owner), - model: CloudWorkflowModel::new(Workflow::new( - format!("w{}", start_id + idx), - format!("c{}", start_id + idx), - )), + .map(|idx| { + ServerWorkflow::new( + SyncId::ServerId((start_id + idx).into()), + CloudWorkflowModel::new(Workflow::new( + format!("w{}", start_id + idx), + format!("c{}", start_id + idx), + )), + mock_server_metadata(), + mock_server_permissions(owner), + ) }) .collect() } @@ -179,11 +181,13 @@ fn mock_random_folders(start_id: i64, owner: Owner) -> Vec { fn mock_server_folders(start_id: i64, owner: Owner, number_of_folders: i64) -> Vec { (0..number_of_folders) - .map(|idx| ServerFolder { - id: SyncId::ServerId((start_id + idx).into()), - model: CloudFolderModel::new(&format!("f{}", start_id + idx), false), - metadata: mock_server_metadata(), - permissions: mock_server_permissions(owner), + .map(|idx| { + ServerFolder::new( + SyncId::ServerId((start_id + idx).into()), + CloudFolderModel::new(&format!("f{}", start_id + idx), false), + mock_server_metadata(), + mock_server_permissions(owner), + ) }) .collect() } @@ -191,50 +195,50 @@ fn mock_server_folders(start_id: i64, owner: Owner, number_of_folders: i64) -> V fn mock_server_notebooks() -> Vec { let owner = Owner::mock_current_user(); vec![ - ServerNotebook { - id: SyncId::ServerId(1.into()), - model: CloudNotebookModel { + ServerNotebook::new( + SyncId::ServerId(1.into()), + CloudNotebookModel { title: "t1".to_string(), data: "d1".to_string(), ai_document_id: None, conversation_id: None, }, - metadata: mock_server_metadata(), - permissions: mock_server_permissions(owner), - }, - ServerNotebook { - id: SyncId::ServerId(2.into()), - model: CloudNotebookModel { + mock_server_metadata(), + mock_server_permissions(owner), + ), + ServerNotebook::new( + SyncId::ServerId(2.into()), + CloudNotebookModel { title: "t2".to_string(), data: "d2".to_string(), ai_document_id: None, conversation_id: None, }, - metadata: mock_server_metadata(), - permissions: mock_server_permissions(owner), - }, - ServerNotebook { - id: SyncId::ServerId(3.into()), - model: CloudNotebookModel { + mock_server_metadata(), + mock_server_permissions(owner), + ), + ServerNotebook::new( + SyncId::ServerId(3.into()), + CloudNotebookModel { title: "t3".to_string(), data: "d3".to_string(), ai_document_id: None, conversation_id: None, }, - metadata: mock_server_metadata(), - permissions: mock_server_permissions(owner), - }, - ServerNotebook { - id: SyncId::ServerId(4.into()), - model: CloudNotebookModel { + mock_server_metadata(), + mock_server_permissions(owner), + ), + ServerNotebook::new( + SyncId::ServerId(4.into()), + CloudNotebookModel { title: "t4".to_string(), data: "d4".to_string(), ai_document_id: None, conversation_id: None, }, - metadata: mock_server_metadata(), - permissions: mock_server_permissions(owner), - }, + mock_server_metadata(), + mock_server_permissions(owner), + ), ] } @@ -1320,15 +1324,15 @@ fn test_update_folder_timestamp_from_child_update() { let new_ts = initial_ts + chrono::Duration::seconds(5); receive_rtc_update( ObjectUpdateMessage::ObjectContentChanged { - server_object: Box::new(ServerCloudObject::Notebook(ServerNotebook { - id: SyncId::ServerId(notebook_id), - model: CloudNotebookModel { + server_object: Box::new(ServerCloudObject::Notebook(ServerNotebook::new( + SyncId::ServerId(notebook_id), + CloudNotebookModel { title: "Test Notebook".to_string(), data: "test2".into(), ai_document_id: None, conversation_id: None, }, - metadata: ServerMetadata { + ServerMetadata { uid: notebook_id, revision: new_ts.into(), metadata_last_updated_ts: new_ts.into(), @@ -1339,8 +1343,8 @@ fn test_update_folder_timestamp_from_child_update() { last_editor_uid: None, current_editor_uid: None, }, - permissions: mock_server_permissions(Owner::mock_current_user()), - })), + mock_server_permissions(Owner::mock_current_user()), + ))), last_editor: None, }, &mut app, @@ -1451,15 +1455,15 @@ fn test_update_folder_timestamp_from_new_child() { // Create a notebook inside the folder. receive_rtc_update( ObjectUpdateMessage::ObjectContentChanged { - server_object: Box::new(ServerCloudObject::Notebook(ServerNotebook { - id: SyncId::ServerId(notebook_id), - model: CloudNotebookModel { + server_object: Box::new(ServerCloudObject::Notebook(ServerNotebook::new( + SyncId::ServerId(notebook_id), + CloudNotebookModel { title: "Test Notebook".to_string(), data: "test".to_string(), ai_document_id: None, conversation_id: None, }, - metadata: ServerMetadata { + ServerMetadata { uid: notebook_id, revision: t2.into(), metadata_last_updated_ts: t2.into(), @@ -1470,8 +1474,8 @@ fn test_update_folder_timestamp_from_new_child() { last_editor_uid: None, current_editor_uid: None, }, - permissions: mock_server_permissions(Owner::mock_current_user()), - })), + mock_server_permissions(Owner::mock_current_user()), + ))), last_editor: None, }, &mut app, diff --git a/app/src/notebooks/editor/model_tests.rs b/app/src/notebooks/editor/model_tests.rs index 045bd54fa2..3f9530392f 100644 --- a/app/src/notebooks/editor/model_tests.rs +++ b/app/src/notebooks/editor/model_tests.rs @@ -1850,17 +1850,17 @@ fn mock_server_workflow(id: i64, app: &mut App) { current_editor_uid: None, }; - let workflow = ServerWorkflow { - id: SyncId::ServerId(workflow_id.into()), - metadata: server_metadata, - permissions: ServerPermissions { + let workflow = ServerWorkflow::new( + SyncId::ServerId(workflow_id.into()), + CloudWorkflowModel::new(Workflow::new(format!("w{id}"), format!("c{id}"))), + server_metadata, + ServerPermissions { space: Owner::mock_current_user(), guests: Vec::new(), permissions_last_updated_ts: ts.into(), anyone_link_sharing: None, }, - model: CloudWorkflowModel::new(Workflow::new(format!("w{id}"), format!("c{id}"))), - }; + ); CloudModel::handle(app).update(app, |cloud_model, _| { cloud_model.add_object(sync_id, CloudWorkflow::new_from_server(workflow)); diff --git a/app/src/notebooks/notebook_tests.rs b/app/src/notebooks/notebook_tests.rs index 6388d36c33..8f8698a200 100644 --- a/app/src/notebooks/notebook_tests.rs +++ b/app/src/notebooks/notebook_tests.rs @@ -154,15 +154,15 @@ fn cloud_notebook(title: impl Into, data: impl Into) -> CloudNot /// Mock a server notebook fn mock_server_notebook(title: impl Into, data: impl Into) -> ServerNotebook { let metadata_ts = Utc::now().into(); - ServerNotebook { - id: ServerId(123.into()), - model: CloudNotebookModel { + ServerNotebook::new( + ServerId(123.into()), + CloudNotebookModel { title: title.into(), data: data.into(), ai_document_id: None, conversation_id: None, }, - metadata: ServerMetadata { + ServerMetadata { uid: 123.into(), revision: Revision::now(), metadata_last_updated_ts: metadata_ts, @@ -173,13 +173,13 @@ fn mock_server_notebook(title: impl Into, data: impl Into) -> Se last_editor_uid: None, current_editor_uid: None, }, - permissions: ServerPermissions { + ServerPermissions { space: Owner::mock_current_user(), guests: Vec::new(), anyone_link_sharing: None, permissions_last_updated_ts: metadata_ts, }, - } + ) } /// Send changed objects to [`UpdateManager`] so that tests requiring "up-to-date" metadata can run. diff --git a/app/src/search/ai_context_menu/notebooks/data_source_tests.rs b/app/src/search/ai_context_menu/notebooks/data_source_tests.rs index 913ac6c257..920a1d4492 100644 --- a/app/src/search/ai_context_menu/notebooks/data_source_tests.rs +++ b/app/src/search/ai_context_menu/notebooks/data_source_tests.rs @@ -28,9 +28,15 @@ use crate::workspaces::user_workspaces::UserWorkspaces; use crate::NetworkStatus; fn mock_server_notebook_with_revision(id: i64, title: &str, revision: Revision) -> ServerNotebook { - ServerNotebook { - id: SyncId::ServerId(id.into()), - metadata: ServerMetadata { + ServerNotebook::new( + SyncId::ServerId(id.into()), + CloudNotebookModel { + title: title.to_string(), + data: format!("{title} content"), + ai_document_id: None, + conversation_id: None, + }, + ServerMetadata { uid: ServerId::default(), revision, metadata_last_updated_ts: Utc::now().into(), @@ -41,19 +47,13 @@ fn mock_server_notebook_with_revision(id: i64, title: &str, revision: Revision) last_editor_uid: None, current_editor_uid: None, }, - permissions: ServerPermissions { + ServerPermissions { space: Owner::mock_current_user(), guests: Vec::new(), anyone_link_sharing: None, permissions_last_updated_ts: Utc::now().into(), }, - model: CloudNotebookModel { - title: title.to_string(), - data: format!("{title} content"), - ai_document_id: None, - conversation_id: None, - }, - } + ) } fn initialize_app(app: &mut App) { diff --git a/app/src/search/ai_context_menu/rules/data_source_tests.rs b/app/src/search/ai_context_menu/rules/data_source_tests.rs index 852a71dfde..4416048594 100644 --- a/app/src/search/ai_context_menu/rules/data_source_tests.rs +++ b/app/src/search/ai_context_menu/rules/data_source_tests.rs @@ -36,9 +36,17 @@ type ServerAIFact = GenericServerObject< >; fn mock_server_ai_fact(id: i64, name: &str, content: &str, revision: Revision) -> ServerAIFact { - GenericServerObject { - id: SyncId::ServerId(id.into()), - metadata: ServerMetadata { + GenericServerObject::new( + SyncId::ServerId(id.into()), + GenericStringModel { + string_model: AIFact::Memory(AIMemory { + name: Some(name.to_string()), + content: content.to_string(), + is_autogenerated: false, + suggested_logging_id: None, + }), + }, + ServerMetadata { uid: ServerId::default(), revision, metadata_last_updated_ts: Utc::now().into(), @@ -49,21 +57,13 @@ fn mock_server_ai_fact(id: i64, name: &str, content: &str, revision: Revision) - last_editor_uid: None, current_editor_uid: None, }, - permissions: ServerPermissions { + ServerPermissions { space: Owner::mock_current_user(), guests: Vec::new(), anyone_link_sharing: None, permissions_last_updated_ts: Utc::now().into(), }, - model: GenericStringModel { - string_model: AIFact::Memory(AIMemory { - name: Some(name.to_string()), - content: content.to_string(), - is_autogenerated: false, - suggested_logging_id: None, - }), - }, - } + ) } fn initialize_app(app: &mut App) { diff --git a/app/src/search/command_palette/data_sources_tests.rs b/app/src/search/command_palette/data_sources_tests.rs index c26da5568c..7107482bc4 100644 --- a/app/src/search/command_palette/data_sources_tests.rs +++ b/app/src/search/command_palette/data_sources_tests.rs @@ -58,26 +58,26 @@ fn mock_server_permissions(owner: Owner) -> ServerPermissions { } fn mock_server_workflow(id: WorkflowId, owner: Owner) -> ServerWorkflow { - ServerWorkflow { - id: SyncId::ServerId(id.into()), - metadata: mock_server_metadata(), - permissions: mock_server_permissions(owner), - model: CloudWorkflowModel::new(Workflow::new(format!("foo{id}"), format!("bar{id}"))), - } + ServerWorkflow::new( + SyncId::ServerId(id.into()), + CloudWorkflowModel::new(Workflow::new(format!("foo{id}"), format!("bar{id}"))), + mock_server_metadata(), + mock_server_permissions(owner), + ) } fn mock_server_notebook(id: NotebookId, owner: Owner) -> ServerNotebook { - ServerNotebook { - id: SyncId::ServerId(id.into()), - metadata: mock_server_metadata(), - permissions: mock_server_permissions(owner), - model: CloudNotebookModel { + ServerNotebook::new( + SyncId::ServerId(id.into()), + CloudNotebookModel { title: format!("foo{id}"), data: format!("bar{id}"), ai_document_id: None, conversation_id: None, }, - } + mock_server_metadata(), + mock_server_permissions(owner), + ) } fn initialize_app(app: &mut App) { diff --git a/app/src/server/cloud_objects/fake_object_client.rs b/app/src/server/cloud_objects/fake_object_client.rs index 583c72f60e..40fcb7cc83 100644 --- a/app/src/server/cloud_objects/fake_object_client.rs +++ b/app/src/server/cloud_objects/fake_object_client.rs @@ -144,12 +144,12 @@ impl FakeObjectClient { anyone_link_sharing: None, permissions_last_updated_ts: stored.metadata_ts.into(), }; - let server_pref = ServerPreference { - id: SyncId::ServerId(ServerId::from(*id)), - model: stored.model.clone(), + let server_pref = ServerPreference::new( + SyncId::ServerId(ServerId::from(*id)), + stored.model.clone(), metadata, permissions, - }; + ); Box::new(server_pref) as Box }) .collect(); diff --git a/app/src/server/cloud_objects/update_manager_tests.rs b/app/src/server/cloud_objects/update_manager_tests.rs index 7d9b2ffa61..f17706f2f6 100644 --- a/app/src/server/cloud_objects/update_manager_tests.rs +++ b/app/src/server/cloud_objects/update_manager_tests.rs @@ -255,12 +255,12 @@ fn create_workflow_enum_internal( } fn mock_server_workflow(id: WorkflowId, owner: Owner, metadata: ServerMetadata) -> ServerWorkflow { - ServerWorkflow { - id: SyncId::ServerId(id.into()), + ServerWorkflow::new( + SyncId::ServerId(id.into()), + CloudWorkflowModel::new(Workflow::new(format!("w{id}"), format!("c{id}"))), metadata, - permissions: mock_server_permissions(owner), - model: CloudWorkflowModel::new(Workflow::new(format!("w{id}"), format!("c{id}"))), - } + mock_server_permissions(owner), + ) } fn mock_server_workflow_with_enum( @@ -269,11 +269,9 @@ fn mock_server_workflow_with_enum( owner: Owner, metadata: ServerMetadata, ) -> (ServerWorkflow, ServerWorkflowEnum) { - let workflow = ServerWorkflow { - id: SyncId::ServerId(id.into()), - metadata: metadata.clone(), - permissions: mock_server_permissions(owner), - model: CloudWorkflowModel::new( + let workflow = ServerWorkflow::new( + SyncId::ServerId(id.into()), + CloudWorkflowModel::new( Workflow::new(format!("w{id}"), format!("c{id}")).with_arguments(vec![Argument { name: format!("e{enum_id}"), default_value: None, @@ -283,47 +281,49 @@ fn mock_server_workflow_with_enum( }, }]), ), - }; + metadata.clone(), + mock_server_permissions(owner), + ); - let workflow_enum = ServerWorkflowEnum { - id: SyncId::ServerId(enum_id.into()), - metadata, - permissions: mock_server_permissions(owner), - model: CloudWorkflowEnumModel::new(WorkflowEnum { + let workflow_enum = ServerWorkflowEnum::new( + SyncId::ServerId(enum_id.into()), + CloudWorkflowEnumModel::new(WorkflowEnum { name: format!("e{id}"), is_shared: false, variants: EnumVariants::Static(vec!["v1".to_string(), "v2".to_string()]), }), - }; + metadata, + mock_server_permissions(owner), + ); (workflow, workflow_enum) } fn mock_server_notebook(id: NotebookId, owner: Owner, metadata: ServerMetadata) -> ServerNotebook { - ServerNotebook { - id: SyncId::ServerId(id.into()), - metadata, - permissions: mock_server_permissions(owner), - model: CloudNotebookModel { + ServerNotebook::new( + SyncId::ServerId(id.into()), + CloudNotebookModel { title: format!("n{id}"), data: format!("n{id}"), ai_document_id: None, conversation_id: None, }, - } + metadata, + mock_server_permissions(owner), + ) } fn mock_server_folder(id: FolderId, owner: Owner, metadata: ServerMetadata) -> ServerFolder { - ServerFolder { - id: SyncId::ServerId(id.into()), - metadata, - permissions: mock_server_permissions(owner), - model: CloudFolderModel { + ServerFolder::new( + SyncId::ServerId(id.into()), + CloudFolderModel { name: format!("f{id}"), is_open: false, is_warp_pack: false, }, - } + metadata, + mock_server_permissions(owner), + ) } fn update_notebook( @@ -597,10 +597,10 @@ fn mock_fetch_single_cloud_object( .times(1) .return_once(move |_| { Ok(GetCloudObjectResponse { - object: ServerCloudObject::Workflow(Box::new(ServerWorkflow { - id: SyncId::ServerId(workflow_id.into()), - model: CloudWorkflowModel::new(Workflow::new("server workflow", "echo server")), - metadata: ServerMetadata { + object: ServerCloudObject::Workflow(Box::new(ServerWorkflow::new( + SyncId::ServerId(workflow_id.into()), + CloudWorkflowModel::new(Workflow::new("server workflow", "echo server")), + ServerMetadata { uid: server_id, revision: Revision::now(), metadata_last_updated_ts: Utc::now().into(), @@ -611,13 +611,13 @@ fn mock_fetch_single_cloud_object( last_editor_uid: None, current_editor_uid: None, }, - permissions: ServerPermissions { + ServerPermissions { space: Owner::mock_current_user(), guests: Vec::new(), anyone_link_sharing: None, permissions_last_updated_ts: Utc::now().into(), }, - })), + ))), descendants: vec![], action_histories: vec![ObjectActionHistory { uid: server_id.uid(), @@ -2667,9 +2667,9 @@ fn test_pending_metadata_update_with_polling() { > = HashMap::new(); updated_generic_string_objects.insert( GenericStringObjectFormat::Json(JsonObjectType::Preference), - vec![Box::new(ServerPreference { - id: SyncId::ServerId(generic_object_server_id), - model: CloudPreferenceModel::new( + vec![Box::new(ServerPreference::new( + SyncId::ServerId(generic_object_server_id), + CloudPreferenceModel::new( Preference::new( "test_storage_key".to_string(), "{\"test_key\": \"test_value\"}", @@ -2677,23 +2677,23 @@ fn test_pending_metadata_update_with_polling() { ) .expect("error creating preference"), ), - metadata: mocked_metadata.clone(), - permissions: mock_server_permissions(Owner::mock_current_user()), - })], + mocked_metadata.clone(), + mock_server_permissions(Owner::mock_current_user()), + ))], ); let mocked_response = InitialLoadResponse { - updated_notebooks: vec![ServerNotebook { - id: SyncId::ServerId(notebood_server_id), - model: CloudNotebookModel { + updated_notebooks: vec![ServerNotebook::new( + SyncId::ServerId(notebood_server_id), + CloudNotebookModel { title: "".into(), data: "".into(), ai_document_id: None, conversation_id: None, }, - metadata: mocked_metadata.clone(), - permissions: mock_server_permissions(Owner::mock_current_user()), - }], + mocked_metadata.clone(), + mock_server_permissions(Owner::mock_current_user()), + )], deleted_notebooks: vec![], updated_workflows: vec![], deleted_workflows: vec![], @@ -2800,17 +2800,17 @@ fn test_metadata_update_with_polling_no_pending() { current_editor_uid: Some("ian@warp.dev".to_string()), }; let mocked_response = InitialLoadResponse { - updated_notebooks: vec![ServerNotebook { - id: SyncId::ServerId(server_id), - model: CloudNotebookModel { + updated_notebooks: vec![ServerNotebook::new( + SyncId::ServerId(server_id), + CloudNotebookModel { title: "".into(), data: "".into(), ai_document_id: None, conversation_id: None, }, - metadata: mocked_metadata.clone(), - permissions: mock_server_permissions(Owner::mock_current_user()), - }], + mocked_metadata.clone(), + mock_server_permissions(Owner::mock_current_user()), + )], deleted_notebooks: vec![], updated_workflows: vec![], deleted_workflows: vec![], @@ -4101,17 +4101,17 @@ fn test_accepts_new_metadata_with_force_refresh() { current_editor_uid: None, }; let mocked_response = InitialLoadResponse { - updated_notebooks: vec![ServerNotebook { - id: SyncId::ServerId(server_id), - model: CloudNotebookModel { + updated_notebooks: vec![ServerNotebook::new( + SyncId::ServerId(server_id), + CloudNotebookModel { title: "".into(), data: "".into(), ai_document_id: None, conversation_id: None, }, - metadata: mocked_metadata.clone(), - permissions: mock_server_permissions(Owner::mock_current_user()), - }], + mocked_metadata.clone(), + mock_server_permissions(Owner::mock_current_user()), + )], deleted_notebooks: vec![], updated_workflows: vec![], deleted_workflows: vec![], diff --git a/app/src/server/graphql/schema/mod.rs b/app/src/server/graphql/schema/mod.rs index 2d0c322be6..8cb795b146 100644 --- a/app/src/server/graphql/schema/mod.rs +++ b/app/src/server/graphql/schema/mod.rs @@ -7,21 +7,24 @@ use warp_graphql::mutations::update_generic_string_object::{ }; use warp_graphql::object::ObjectUpdateSuccess; -use crate::ai::ambient_agents::scheduled::CloudScheduledAmbientAgentModel; -use crate::ai::cloud_environments::CloudAmbientAgentEnvironmentModel; -use crate::ai::execution_profiles::CloudAIExecutionProfileModel; -use crate::ai::facts::CloudAIFactModel; -use crate::ai::mcp::templatable::CloudTemplatableMCPServerModel; -use crate::ai::mcp::CloudMCPServerModel; -use crate::cloud_object::model::generic_string_model::GenericStringObjectId; use crate::cloud_object::{ - GenericServerObject, RevisionAndLastEditor, ServerFolder, ServerObject, UpdateCloudObjectResult, + RevisionAndLastEditor, ServerAIExecutionProfile, ServerAIFact, ServerAmbientAgentEnvironment, + ServerEnvVarCollection, ServerFolder, ServerMCPServer, ServerObject, ServerPreference, + ServerScheduledAmbientAgent, ServerTemplatableMCPServer, ServerWorkflowEnum, TryFromGql, + UpdateCloudObjectResult, }; -use crate::env_vars::CloudEnvVarCollectionModel; use crate::server::graphql::get_user_facing_error_message; -use crate::server::ids::ServerId; -use crate::settings::cloud_preferences::CloudPreferenceModel; -use crate::workflows::workflow_enum::CloudWorkflowEnumModel; + +fn boxed_rejected_generic_string_object( + object: warp_graphql::generic_string_object::GenericStringObject, +) -> Result> +where + T: TryFromGql + + ServerObject + + 'static, +{ + Ok(Box::new(T::try_from_gql(object)?)) +} pub fn update_generic_string_object_result_to_update_result( value: UpdateGenericStringObjectResult, @@ -38,236 +41,52 @@ pub fn update_generic_string_object_result_to_update_result( }) } GenericStringObjectUpdate::GenericStringObjectUpdateRejected(rejected) => { - let boxed: Box = match rejected - .conflicting_generic_string_object - .format - { + let format = rejected.conflicting_generic_string_object.format.clone(); + let boxed: Box = match format { GenericStringObjectFormat::JsonEnvVarCollection => { - let gso = GenericServerObject::< - GenericStringObjectId, - CloudEnvVarCollectionModel, - >::try_from_graphql_fields( - ServerId::from_string_lossy( - rejected - .conflicting_generic_string_object - .metadata - .uid - .inner(), - ), - Some(rejected.conflicting_generic_string_object.serialized_model), - rejected - .conflicting_generic_string_object - .metadata - .try_into()?, - rejected - .conflicting_generic_string_object - .permissions - .try_into()?, - )?; - let boxed: Box = Box::new(gso); - boxed + boxed_rejected_generic_string_object::( + rejected.conflicting_generic_string_object, + )? } GenericStringObjectFormat::JsonPreference => { - let gso = GenericServerObject::< - GenericStringObjectId, - CloudPreferenceModel, - >::try_from_graphql_fields( - ServerId::from_string_lossy( - rejected - .conflicting_generic_string_object - .metadata - .uid - .inner(), - ), - Some(rejected.conflicting_generic_string_object.serialized_model), - rejected - .conflicting_generic_string_object - .metadata - .try_into()?, - rejected - .conflicting_generic_string_object - .permissions - .try_into()?, - )?; - let boxed: Box = Box::new(gso); - boxed + boxed_rejected_generic_string_object::( + rejected.conflicting_generic_string_object, + )? } GenericStringObjectFormat::JsonWorkflowEnum => { - let gso = GenericServerObject::< - GenericStringObjectId, - CloudWorkflowEnumModel, - >::try_from_graphql_fields( - ServerId::from_string_lossy( - rejected - .conflicting_generic_string_object - .metadata - .uid - .inner(), - ), - Some(rejected.conflicting_generic_string_object.serialized_model), - rejected - .conflicting_generic_string_object - .metadata - .try_into()?, - rejected - .conflicting_generic_string_object - .permissions - .try_into()?, - )?; - let boxed: Box = Box::new(gso); - boxed + boxed_rejected_generic_string_object::( + rejected.conflicting_generic_string_object, + )? } GenericStringObjectFormat::JsonAIFact => { - let gso = GenericServerObject::< - GenericStringObjectId, - CloudAIFactModel, - >::try_from_graphql_fields( - ServerId::from_string_lossy( - rejected - .conflicting_generic_string_object - .metadata - .uid - .inner(), - ), - Some( - rejected.conflicting_generic_string_object.serialized_model, - ), - rejected - .conflicting_generic_string_object - .metadata - .try_into()?, - rejected - .conflicting_generic_string_object - .permissions - .try_into()?, - )?; - let boxed: Box = Box::new(gso); - boxed + boxed_rejected_generic_string_object::( + rejected.conflicting_generic_string_object, + )? } GenericStringObjectFormat::JsonAIExecutionProfile => { - let gso = GenericServerObject::< - GenericStringObjectId, - CloudAIExecutionProfileModel, - >::try_from_graphql_fields( - ServerId::from_string_lossy( - rejected - .conflicting_generic_string_object - .metadata - .uid - .inner(), - ), - Some(rejected.conflicting_generic_string_object.serialized_model), - rejected - .conflicting_generic_string_object - .metadata - .try_into()?, - rejected - .conflicting_generic_string_object - .permissions - .try_into()?, - )?; - let boxed: Box = Box::new(gso); - boxed + boxed_rejected_generic_string_object::( + rejected.conflicting_generic_string_object, + )? } GenericStringObjectFormat::JsonMCPServer => { - let gso = GenericServerObject::< - GenericStringObjectId, - CloudMCPServerModel, - >::try_from_graphql_fields( - ServerId::from_string_lossy( - rejected - .conflicting_generic_string_object - .metadata - .uid - .inner(), - ), - Some(rejected.conflicting_generic_string_object.serialized_model), - rejected - .conflicting_generic_string_object - .metadata - .try_into()?, - rejected - .conflicting_generic_string_object - .permissions - .try_into()?, - )?; - let boxed: Box = Box::new(gso); - boxed + boxed_rejected_generic_string_object::( + rejected.conflicting_generic_string_object, + )? } GenericStringObjectFormat::JsonTemplatableMCPServer => { - let gso = GenericServerObject::< - GenericStringObjectId, - CloudTemplatableMCPServerModel, - >::try_from_graphql_fields( - ServerId::from_string_lossy( - rejected - .conflicting_generic_string_object - .metadata - .uid - .inner(), - ), - Some(rejected.conflicting_generic_string_object.serialized_model), - rejected - .conflicting_generic_string_object - .metadata - .try_into()?, - rejected - .conflicting_generic_string_object - .permissions - .try_into()?, - )?; - let boxed: Box = Box::new(gso); - boxed + boxed_rejected_generic_string_object::( + rejected.conflicting_generic_string_object, + )? } GenericStringObjectFormat::JsonCloudEnvironment => { - let gso = GenericServerObject::< - GenericStringObjectId, - CloudAmbientAgentEnvironmentModel, - >::try_from_graphql_fields( - ServerId::from_string_lossy( - rejected - .conflicting_generic_string_object - .metadata - .uid - .inner(), - ), - Some(rejected.conflicting_generic_string_object.serialized_model), - rejected - .conflicting_generic_string_object - .metadata - .try_into()?, - rejected - .conflicting_generic_string_object - .permissions - .try_into()?, - )?; - let boxed: Box = Box::new(gso); - boxed + boxed_rejected_generic_string_object::( + rejected.conflicting_generic_string_object, + )? } GenericStringObjectFormat::JsonScheduledAmbientAgent => { - let gso = GenericServerObject::< - GenericStringObjectId, - CloudScheduledAmbientAgentModel, - >::try_from_graphql_fields( - ServerId::from_string_lossy( - rejected - .conflicting_generic_string_object - .metadata - .uid - .inner(), - ), - Some(rejected.conflicting_generic_string_object.serialized_model), - rejected - .conflicting_generic_string_object - .metadata - .try_into()?, - rejected - .conflicting_generic_string_object - .permissions - .try_into()?, - )?; - let boxed: Box = Box::new(gso); - boxed + boxed_rejected_generic_string_object::( + rejected.conflicting_generic_string_object, + )? } }; Ok(UpdateCloudObjectResult::Rejected { object: boxed }) diff --git a/app/src/server/server_api/object.rs b/app/src/server/server_api/object.rs index 7fe10b934f..f6a7e0c98d 100644 --- a/app/src/server/server_api/object.rs +++ b/app/src/server/server_api/object.rs @@ -134,7 +134,7 @@ use crate::cloud_object::{ ObjectIdType, ObjectMetadataUpdateResult, ObjectPermissionUpdateResult, ObjectPermissionsUpdateData, ObjectType, ObjectsToUpdate, Owner, Revision, RevisionAndLastEditor, ServerCloudObject, ServerFolder, ServerMetadata, ServerNotebook, - ServerObject, ServerPermissions, ServerWorkflow, UpdateCloudObjectResult, + ServerObject, ServerPermissions, ServerWorkflow, TryFromGql as _, UpdateCloudObjectResult, }; use crate::drive::folders::FolderId; use crate::drive::sharing::SharingAccessLevel; @@ -414,7 +414,7 @@ impl ObjectClient for ServerApi { } WorkflowUpdate::WorkflowUpdateRejected(rejected) => { Ok(UpdateCloudObjectResult::Rejected { - object: rejected.conflicting_workflow.try_into()?, + object: ServerWorkflow::try_from_gql(rejected.conflicting_workflow)?, }) } WorkflowUpdate::Unknown => Err(anyhow!("WorkflowUpdate has unknown variant")), @@ -662,7 +662,7 @@ impl ObjectClient for ServerApi { } NotebookUpdate::NotebookUpdateRejected(rejected) => { Ok(UpdateCloudObjectResult::Rejected { - object: rejected.conflicting_notebook.try_into()?, + object: ServerNotebook::try_from_gql(rejected.conflicting_notebook)?, }) } NotebookUpdate::Unknown => Err(anyhow!("NotebookUpdate has unknown variant")), @@ -907,17 +907,7 @@ impl ObjectClient for ServerApi { .map(|notebooks| { notebooks .into_iter() - .filter_map(|notebook| { - ServerNotebook::try_from_graphql_fields( - ServerId::from_string_lossy(notebook.metadata.uid.inner()), - Some(notebook.title), - Some(notebook.data), - notebook.ai_document_id, - notebook.metadata.try_into().ok()?, - notebook.permissions.try_into().ok()?, - ) - .ok() - }) + .filter_map(|notebook| ServerNotebook::try_from_gql(notebook).ok()) .collect() }) .unwrap_or_default(); @@ -927,15 +917,7 @@ impl ObjectClient for ServerApi { .map(|workflows| { workflows .into_iter() - .filter_map(|workflow| { - ServerWorkflow::try_from_graphql_fields( - ServerId::from_string_lossy(workflow.metadata.uid.inner()), - workflow.data, - workflow.metadata.try_into().ok()?, - workflow.permissions.try_into().ok()?, - ) - .ok() - }) + .filter_map(|workflow| ServerWorkflow::try_from_gql(workflow).ok()) .collect() }) .unwrap_or_default(); @@ -945,16 +927,7 @@ impl ObjectClient for ServerApi { .map(|folders| { folders .into_iter() - .filter_map(|folder| { - ServerFolder::try_from_graphql_fields( - ServerId::from_string_lossy(folder.metadata.uid.inner()), - Some(folder.name), - folder.metadata.try_into().ok()?, - folder.permissions.try_into().ok()?, - folder.is_warp_pack, - ) - .ok() - }) + .filter_map(|folder| ServerFolder::try_from_gql(folder).ok()) .collect() }) .unwrap_or_default(); @@ -962,120 +935,68 @@ impl ObjectClient for ServerApi { let mut updated_generic_string_objects = HashMap::new(); if let Some(objects) = output.generic_string_objects { for gso in objects { - let uid = gso.metadata.uid.inner().to_string(); - let server_id = ServerId::from_string_lossy(&uid); - - let metadata = match ServerMetadata::try_from(gso.metadata) { - Ok(metadata) => metadata, - Err(err) => { - report_error!(err.context(format!( - "Failed to convert metadata for GSO {:?} {uid}", - gso.format - ))); - continue; - } - }; - - let permissions = match ServerPermissions::try_from(gso.permissions) { - Ok(permissions) => permissions, - Err(err) => { - report_error!(err.context(format!( - "Failed to convert permissions for GSO {:?} {uid}", - gso.format - ))); - continue; - } - }; - - match gso.format { + match gso.format.clone() { warp_graphql::generic_string_object::GenericStringObjectFormat::JsonEnvVarCollection => { parse_server_gso::( &mut updated_generic_string_objects, GenericStringObjectFormat::Json(JsonObjectType::EnvVarCollection), - server_id, - metadata, - permissions, - gso.serialized_model, + gso, ); } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonPreference => { parse_server_gso::( &mut updated_generic_string_objects, GenericStringObjectFormat::Json(JsonObjectType::Preference), - server_id, - metadata, - permissions, - gso.serialized_model, + gso, ); } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonWorkflowEnum => { parse_server_gso::( &mut updated_generic_string_objects, GenericStringObjectFormat::Json(JsonObjectType::WorkflowEnum), - server_id, - metadata, - permissions, - gso.serialized_model, + gso, ); } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonAIFact => { parse_server_gso::( &mut updated_generic_string_objects, GenericStringObjectFormat::Json(JsonObjectType::AIFact), - server_id, - metadata, - permissions, - gso.serialized_model, + gso, ); } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonMCPServer => { parse_server_gso::( &mut updated_generic_string_objects, GenericStringObjectFormat::Json(JsonObjectType::MCPServer), - server_id, - metadata, - permissions, - gso.serialized_model, + gso, ); } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonAIExecutionProfile => { parse_server_gso::( &mut updated_generic_string_objects, GenericStringObjectFormat::Json(JsonObjectType::AIExecutionProfile), - server_id, - metadata, - permissions, - gso.serialized_model, + gso, ); } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonTemplatableMCPServer => { parse_server_gso::( &mut updated_generic_string_objects, GenericStringObjectFormat::Json(JsonObjectType::TemplatableMCPServer), - server_id, - metadata, - permissions, - gso.serialized_model, + gso, ); } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonCloudEnvironment => { parse_server_gso::( &mut updated_generic_string_objects, GenericStringObjectFormat::Json(JsonObjectType::CloudEnvironment), - server_id, - metadata, - permissions, - gso.serialized_model, + gso, ); } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonScheduledAmbientAgent => { parse_server_gso::( &mut updated_generic_string_objects, GenericStringObjectFormat::Json(JsonObjectType::ScheduledAmbientAgent), - server_id, - metadata, - permissions, - gso.serialized_model, + gso, ); } } @@ -1637,17 +1558,15 @@ impl ObjectClient for ServerApi { fn parse_server_gso( map: &mut HashMap>>, format: GenericStringObjectFormat, - uid: ServerId, - metadata: ServerMetadata, - permissions: ServerPermissions, - serialized_model: String, + gso: warp_graphql::generic_string_object::GenericStringObject, ) where T: StringModel< CloudObjectType = GenericCloudObject>, >, S: Serializer, { - match GenericServerObject::>::try_from_graphql_fields(uid, Some(serialized_model), metadata, permissions) + let uid = ServerId::from_string_lossy(gso.metadata.uid.inner()); + match GenericServerObject::>::try_from_gql(gso) { Ok(object) => { map.entry(format).or_default().push(Box::new(object)); diff --git a/app/src/settings/cloud_preferences_syncer_tests.rs b/app/src/settings/cloud_preferences_syncer_tests.rs index e5b7c950a4..af8ca15f6f 100644 --- a/app/src/settings/cloud_preferences_syncer_tests.rs +++ b/app/src/settings/cloud_preferences_syncer_tests.rs @@ -159,18 +159,18 @@ fn initial_load_response_with_cloud_settings( current_editor_uid: None, }; - let cloud_setting = ServerPreference { - id: SyncId::ServerId(id.into()), + let cloud_setting = ServerPreference::new( + SyncId::ServerId(id.into()), + CloudPreferenceModel::deserialize_owned(&setting.serialized_preference) + .expect("error creating preference"), metadata, - permissions: ServerPermissions { + ServerPermissions { space: Owner::mock_current_user(), guests: Vec::new(), anyone_link_sharing: None, permissions_last_updated_ts: Utc::now().into(), }, - model: CloudPreferenceModel::deserialize_owned(&setting.serialized_preference) - .expect("error creating preference"), - }; + ); Box::new(cloud_setting) as Box }) .collect::>>(); diff --git a/app/src/settings/onboarding_tests.rs b/app/src/settings/onboarding_tests.rs index 694f0f4e63..94fab59e24 100644 --- a/app/src/settings/onboarding_tests.rs +++ b/app/src/settings/onboarding_tests.rs @@ -86,12 +86,12 @@ fn apply_onboarding_settings_preserves_existing_cloud_profile_on_existing_user_l mcp_permissions: ActionPermission::AlwaysAllow, ..Default::default() }; - let server_object = ServerAIExecutionProfile { - id: cloud_sync_id, - model: CloudAIExecutionProfileModel::new(cloud_profile), - metadata: mock_server_metadata(cloud_uid), - permissions: ServerPermissions::mock_personal(), - }; + let server_object = ServerAIExecutionProfile::new( + cloud_sync_id, + CloudAIExecutionProfileModel::new(cloud_profile), + mock_server_metadata(cloud_uid), + ServerPermissions::mock_personal(), + ); // Insert the existing user's cloud profile via the initial-load // path (no per-object events) and emit `InitialLoadCompleted` so diff --git a/app/src/workspaces/gql_convert.rs b/app/src/workspaces/gql_convert.rs index 36ae03a5f1..441871c6fb 100644 --- a/app/src/workspaces/gql_convert.rs +++ b/app/src/workspaces/gql_convert.rs @@ -65,10 +65,10 @@ use crate::ai::execution_profiles::{ use crate::ai::{BonusGrant, BonusGrantScope}; use crate::auth::UserUid; use crate::cloud_object::{ - ServerAIExecutionProfile, ServerAIFact, ServerAmbientAgentEnvironment, ServerCloudAgentConfig, - ServerCloudObject, ServerEnvVarCollection, ServerFolder, ServerMCPServer, ServerNotebook, - ServerPreference, ServerScheduledAmbientAgent, ServerTemplatableMCPServer, ServerWorkflow, - ServerWorkflowEnum, + ServerAIExecutionProfile, ServerAIFact, ServerAmbientAgentEnvironment, ServerCloudObject, + ServerEnvVarCollection, ServerFolder, ServerMCPServer, ServerNotebook, ServerPreference, + ServerScheduledAmbientAgent, ServerTemplatableMCPServer, ServerWorkflow, ServerWorkflowEnum, + TryFromGql as _, }; use crate::server::cloud_objects::listener::ObjectUpdateMessage; use crate::server::experiments::ServerExperiment; @@ -1124,252 +1124,54 @@ impl TryFrom for ObjectUpdateMessage { } } -impl TryFrom for ServerFolder { - type Error = anyhow::Error; - - fn try_from(folder: warp_graphql::folder::Folder) -> Result { - ServerFolder::try_from_graphql_fields( - ServerId::from_string_lossy(folder.metadata.uid.inner()), - Some(folder.name), - folder.metadata.try_into()?, - folder.permissions.try_into()?, - folder.is_warp_pack, - ) - } -} - -impl TryFrom for ServerNotebook { - type Error = anyhow::Error; - - fn try_from(notebook: warp_graphql::notebook::Notebook) -> Result { - ServerNotebook::try_from_graphql_fields( - ServerId::from_string_lossy(notebook.metadata.uid.inner()), - Some(notebook.title), - Some(notebook.data), - notebook.ai_document_id, - notebook.metadata.try_into()?, - notebook.permissions.try_into()?, - ) - } -} - -impl TryFrom for ServerWorkflow { - type Error = anyhow::Error; - - fn try_from(workflow: warp_graphql::workflow::Workflow) -> Result { - ServerWorkflow::try_from_graphql_fields( - ServerId::from_string_lossy(workflow.metadata.uid.inner()), - workflow.data, - workflow.metadata.try_into()?, - workflow.permissions.try_into()?, - ) - } -} - -impl TryFrom for ServerEnvVarCollection { - type Error = anyhow::Error; - - fn try_from( - gso: warp_graphql::generic_string_object::GenericStringObject, - ) -> Result { - ServerEnvVarCollection::try_from_graphql_fields( - ServerId::from_string_lossy(gso.metadata.uid.inner()), - Some(gso.serialized_model), - gso.metadata.try_into()?, - gso.permissions.try_into()?, - ) - } -} - -impl TryFrom for ServerWorkflowEnum { - type Error = anyhow::Error; - - fn try_from( - gso: warp_graphql::generic_string_object::GenericStringObject, - ) -> Result { - ServerWorkflowEnum::try_from_graphql_fields( - ServerId::from_string_lossy(gso.metadata.uid.inner()), - Some(gso.serialized_model), - gso.metadata.try_into()?, - gso.permissions.try_into()?, - ) - } -} - -impl TryFrom for ServerAIFact { - type Error = anyhow::Error; - - fn try_from( - gso: warp_graphql::generic_string_object::GenericStringObject, - ) -> Result { - ServerAIFact::try_from_graphql_fields( - ServerId::from_string_lossy(gso.metadata.uid.inner()), - Some(gso.serialized_model), - gso.metadata.try_into()?, - gso.permissions.try_into()?, - ) - } -} - -impl TryFrom - for ServerAIExecutionProfile -{ - type Error = anyhow::Error; - - fn try_from( - gso: warp_graphql::generic_string_object::GenericStringObject, - ) -> Result { - ServerAIExecutionProfile::try_from_graphql_fields( - ServerId::from_string_lossy(gso.metadata.uid.inner()), - Some(gso.serialized_model), - gso.metadata.try_into()?, - gso.permissions.try_into()?, - ) - } -} -impl TryFrom for ServerMCPServer { - type Error = anyhow::Error; - - fn try_from( - gso: warp_graphql::generic_string_object::GenericStringObject, - ) -> Result { - ServerMCPServer::try_from_graphql_fields( - ServerId::from_string_lossy(gso.metadata.uid.inner()), - Some(gso.serialized_model), - gso.metadata.try_into()?, - gso.permissions.try_into()?, - ) - } -} - -impl TryFrom - for ServerTemplatableMCPServer -{ - type Error = anyhow::Error; - fn try_from( - gso: warp_graphql::generic_string_object::GenericStringObject, - ) -> Result { - ServerTemplatableMCPServer::try_from_graphql_fields( - ServerId::from_string_lossy(gso.metadata.uid.inner()), - Some(gso.serialized_model), - gso.metadata.try_into()?, - gso.permissions.try_into()?, - ) - } -} - -impl TryFrom for ServerPreference { - type Error = anyhow::Error; - - fn try_from( - gso: warp_graphql::generic_string_object::GenericStringObject, - ) -> Result { - ServerPreference::try_from_graphql_fields( - ServerId::from_string_lossy(gso.metadata.uid.inner()), - Some(gso.serialized_model), - gso.metadata.try_into()?, - gso.permissions.try_into()?, - ) - } -} - -impl TryFrom - for ServerAmbientAgentEnvironment -{ - type Error = anyhow::Error; - - fn try_from( - gso: warp_graphql::generic_string_object::GenericStringObject, - ) -> Result { - ServerAmbientAgentEnvironment::try_from_graphql_fields( - ServerId::from_string_lossy(gso.metadata.uid.inner()), - Some(gso.serialized_model), - gso.metadata.try_into()?, - gso.permissions.try_into()?, - ) - } -} - -impl TryFrom - for ServerScheduledAmbientAgent -{ - type Error = anyhow::Error; - - fn try_from( - gso: warp_graphql::generic_string_object::GenericStringObject, - ) -> Result { - ServerScheduledAmbientAgent::try_from_graphql_fields( - ServerId::from_string_lossy(gso.metadata.uid.inner()), - Some(gso.serialized_model), - gso.metadata.try_into()?, - gso.permissions.try_into()?, - ) - } -} - -impl TryFrom for ServerCloudAgentConfig { - type Error = anyhow::Error; - - fn try_from( - gso: warp_graphql::generic_string_object::GenericStringObject, - ) -> Result { - ServerCloudAgentConfig::try_from_graphql_fields( - ServerId::from_string_lossy(gso.metadata.uid.inner()), - Some(gso.serialized_model), - gso.metadata.try_into()?, - gso.permissions.try_into()?, - ) - } -} - impl TryFrom for ServerCloudObject { type Error = anyhow::Error; fn try_from(value: warp_graphql::object::CloudObject) -> Result { match value { - warp_graphql::object::CloudObject::AIConversation(_) => { - Err(anyhow::anyhow!("AIConversation is not a supported object type for this operation")) - } - warp_graphql::object::CloudObject::Folder(folder) => { - Ok(ServerCloudObject::Folder(folder.try_into()?)) - } + warp_graphql::object::CloudObject::AIConversation(_) => Err(anyhow::anyhow!( + "AIConversation is not a supported object type for this operation" + )), + warp_graphql::object::CloudObject::Folder(folder) => Ok(ServerCloudObject::Folder( + ServerFolder::try_from_gql(folder)?, + )), warp_graphql::object::CloudObject::GenericStringObject(gso) => { - match gso.format { + match gso.format.clone() { warp_graphql::generic_string_object::GenericStringObjectFormat::JsonEnvVarCollection => { - Ok(ServerCloudObject::EnvVarCollection(gso.try_into()?)) + Ok(ServerCloudObject::EnvVarCollection(ServerEnvVarCollection::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonPreference => { - Ok(ServerCloudObject::Preference(gso.try_into()?)) + Ok(ServerCloudObject::Preference(ServerPreference::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonWorkflowEnum => { - Ok(ServerCloudObject::WorkflowEnum(gso.try_into()?)) + Ok(ServerCloudObject::WorkflowEnum(ServerWorkflowEnum::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonAIFact => { - Ok(ServerCloudObject::AIFact(gso.try_into()?)) + Ok(ServerCloudObject::AIFact(ServerAIFact::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonMCPServer => { - Ok(ServerCloudObject::MCPServer(gso.try_into()?)) + Ok(ServerCloudObject::MCPServer(ServerMCPServer::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonAIExecutionProfile => { - Ok(ServerCloudObject::AIExecutionProfile(gso.try_into()?)) + Ok(ServerCloudObject::AIExecutionProfile(ServerAIExecutionProfile::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonTemplatableMCPServer => { - Ok(ServerCloudObject::TemplatableMCPServer(gso.try_into()?)) + Ok(ServerCloudObject::TemplatableMCPServer(ServerTemplatableMCPServer::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonCloudEnvironment => { - Ok(ServerCloudObject::AmbientAgentEnvironment(gso.try_into()?)) + Ok(ServerCloudObject::AmbientAgentEnvironment(ServerAmbientAgentEnvironment::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonScheduledAmbientAgent => { - Ok(ServerCloudObject::ScheduledAmbientAgent(gso.try_into()?)) + Ok(ServerCloudObject::ScheduledAmbientAgent(ServerScheduledAmbientAgent::try_from_gql(gso)?)) } } } - warp_graphql::object::CloudObject::Notebook(notebook) => { - Ok(ServerCloudObject::Notebook(notebook.try_into()?)) - } - warp_graphql::object::CloudObject::Workflow(workflow) => { - Ok(ServerCloudObject::Workflow(Box::new(workflow.try_into()?))) - } + warp_graphql::object::CloudObject::Notebook(notebook) => Ok( + ServerCloudObject::Notebook(ServerNotebook::try_from_gql(notebook)?), + ), + warp_graphql::object::CloudObject::Workflow(workflow) => Ok( + ServerCloudObject::Workflow(Box::new(ServerWorkflow::try_from_gql(workflow)?)), + ), warp_graphql::object::CloudObject::Unknown => { Err(anyhow::anyhow!("Unable to convert cloud object type")) } @@ -1386,39 +1188,39 @@ impl TryFrom for ServerCloudObject { Err(anyhow::anyhow!("AIConversation is not a supported object type for this operation")) } CloudObjectWithDescendants::FolderWithDescendants(fwd) => { - Ok(ServerCloudObject::Folder(fwd.folder.try_into()?)) + Ok(ServerCloudObject::Folder(ServerFolder::try_from_gql(fwd.folder)?)) } - CloudObjectWithDescendants::GenericStringObject(gso) => match gso.format { + CloudObjectWithDescendants::GenericStringObject(gso) => match gso.format.clone() { warp_graphql::generic_string_object::GenericStringObjectFormat::JsonEnvVarCollection => { - Ok(ServerCloudObject::EnvVarCollection(gso.try_into()?)) + Ok(ServerCloudObject::EnvVarCollection(ServerEnvVarCollection::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonPreference => { - Ok(ServerCloudObject::Preference(gso.try_into()?)) + Ok(ServerCloudObject::Preference(ServerPreference::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonWorkflowEnum => { - Ok(ServerCloudObject::WorkflowEnum(gso.try_into()?)) + Ok(ServerCloudObject::WorkflowEnum(ServerWorkflowEnum::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonAIFact => { - Ok(ServerCloudObject::AIFact(gso.try_into()?)) + Ok(ServerCloudObject::AIFact(ServerAIFact::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonMCPServer => { - Ok(ServerCloudObject::MCPServer(gso.try_into()?)) + Ok(ServerCloudObject::MCPServer(ServerMCPServer::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonAIExecutionProfile => { - Ok(ServerCloudObject::AIExecutionProfile(gso.try_into()?)) + Ok(ServerCloudObject::AIExecutionProfile(ServerAIExecutionProfile::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonTemplatableMCPServer => { - Ok(ServerCloudObject::TemplatableMCPServer(gso.try_into()?)) + Ok(ServerCloudObject::TemplatableMCPServer(ServerTemplatableMCPServer::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonCloudEnvironment => { - Ok(ServerCloudObject::AmbientAgentEnvironment(gso.try_into()?)) + Ok(ServerCloudObject::AmbientAgentEnvironment(ServerAmbientAgentEnvironment::try_from_gql(gso)?)) } warp_graphql::generic_string_object::GenericStringObjectFormat::JsonScheduledAmbientAgent => { - Ok(ServerCloudObject::ScheduledAmbientAgent(gso.try_into()?)) + Ok(ServerCloudObject::ScheduledAmbientAgent(ServerScheduledAmbientAgent::try_from_gql(gso)?)) } } - CloudObjectWithDescendants::Notebook(notebook) => Ok(ServerCloudObject::Notebook(notebook.try_into()?)), - CloudObjectWithDescendants::Workflow(workflow) => Ok(ServerCloudObject::Workflow(Box::new(workflow.try_into()?))), + CloudObjectWithDescendants::Notebook(notebook) => Ok(ServerCloudObject::Notebook(ServerNotebook::try_from_gql(notebook)?)), + CloudObjectWithDescendants::Workflow(workflow) => Ok(ServerCloudObject::Workflow(Box::new(ServerWorkflow::try_from_gql(workflow)?))), CloudObjectWithDescendants::Unknown => Err(anyhow::anyhow!("Unable to convert cloud object with descendants type")), } } diff --git a/app/src/workspaces/update_manager_tests.rs b/app/src/workspaces/update_manager_tests.rs index 9cb923cdf9..b53883350c 100644 --- a/app/src/workspaces/update_manager_tests.rs +++ b/app/src/workspaces/update_manager_tests.rs @@ -55,10 +55,10 @@ fn mock_workflow(id: WorkflowId, owner: Owner) -> CloudWorkflow { } fn mock_server_workflow(id: WorkflowId, owner: Owner) -> ServerWorkflow { - ServerWorkflow { - id: SyncId::ServerId(id.into()), - model: CloudWorkflowModel::new(Workflow::new("Test Workflow", "echo hello")), - metadata: ServerMetadata { + ServerWorkflow::new( + SyncId::ServerId(id.into()), + CloudWorkflowModel::new(Workflow::new("Test Workflow", "echo hello")), + ServerMetadata { uid: id.into(), revision: Revision::now(), metadata_last_updated_ts: Utc::now().into(), @@ -69,13 +69,13 @@ fn mock_server_workflow(id: WorkflowId, owner: Owner) -> ServerWorkflow { last_editor_uid: None, current_editor_uid: None, }, - permissions: ServerPermissions { + ServerPermissions { space: owner, permissions_last_updated_ts: Utc::now().into(), anyone_link_sharing: None, guests: vec![], }, - } + ) } #[test] diff --git a/crates/warp_server_client/src/cloud_object/mod.rs b/crates/warp_server_client/src/cloud_object/mod.rs index 1837c1c154..397611bbd3 100644 --- a/crates/warp_server_client/src/cloud_object/mod.rs +++ b/crates/warp_server_client/src/cloud_object/mod.rs @@ -25,9 +25,11 @@ use crate::drive::sharing::{SharingAccessLevel, Subject, TeamKind, UserKind}; use crate::ids::{FolderId, ServerId, SyncId}; mod creation; +mod server_object; mod update; pub use creation::*; +pub use server_object::*; pub use update::*; /// The type of object id each ObjectType corresponds to. #[derive(Copy, Clone, Debug, Eq, PartialEq)] diff --git a/crates/warp_server_client/src/cloud_object/server_object.rs b/crates/warp_server_client/src/cloud_object/server_object.rs new file mode 100644 index 0000000000..3fa988d0ef --- /dev/null +++ b/crates/warp_server_client/src/cloud_object/server_object.rs @@ -0,0 +1,158 @@ +use std::{ + any::Any, + fmt::{self, Debug}, + marker::PhantomData, + sync::Arc, +}; + +use crate::ids::SyncId; + +use super::{ObjectType, ServerMetadata, ServerPermissions}; + +#[derive(Clone, Debug, Default)] +pub enum ConflictStatus { + #[default] + NoConflicts, + ConflictingChanges { + object: Arc, + }, +} + +impl ConflictStatus { + /// Utility function that allows for a more ergonomic way of figuring out whether there is a + /// conflict (for cases where we don't care about the conflict details). + pub fn has_conflicts(&self) -> bool { + matches!(self, ConflictStatus::ConflictingChanges { .. }) + } +} + +/// Common behavior that server-backed models expose to generic server objects. +pub trait ServerObjectModel: Debug + Clone + Send + Sync + 'static { + /// Returns the object type for this model. + fn object_type(&self) -> ObjectType; +} + +/// Common trait for server objects that allows us to use them as trait objects +/// and downcast to concrete types when needed. +pub trait ServerObject: Debug + Send + Sync { + /// Returns the object type of this server object + fn object_type(&self) -> ObjectType; + + /// Returns this object as a ref to the Any type. Needed for typecasts. + fn as_any(&self) -> &dyn Any; + + /// Returns the trait object as a concrete type reference by downcasting it. + /// Returns None if the downcast fails. + fn as_concrete_type( + server_object: &dyn ServerObject, + ) -> Option<&GenericServerObject> + where + Self: Sized, + K: 'static, + M: 'static, + { + server_object + .as_any() + .downcast_ref::>() + } + + /// Returns a cloned boxed version of this server object. + /// Note that we can't force the ServerObject trait to derive from Cloned + /// directly because that would make the trait not object safe. This + /// is a workaround. + fn clone_box(&self) -> Box; +} + +/// An object that maps directly to the data returned from the server +/// for a given model and id type. +pub struct GenericServerObject { + pub id: SyncId, + pub model: M, + pub metadata: ServerMetadata, + pub permissions: ServerPermissions, + _marker: PhantomData K>, +} + +impl Clone for GenericServerObject +where + M: Clone, +{ + fn clone(&self) -> Self { + Self::new( + self.id, + self.model.clone(), + self.metadata.clone(), + self.permissions.clone(), + ) + } +} + +impl Debug for GenericServerObject +where + M: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GenericServerObject") + .field("id", &self.id) + .field("model", &self.model) + .field("metadata", &self.metadata) + .field("permissions", &self.permissions) + .finish() + } +} + +impl GenericServerObject { + /// Constructs a server object from its server-provided parts. + pub fn new( + id: SyncId, + model: M, + metadata: ServerMetadata, + permissions: ServerPermissions, + ) -> Self { + Self { + id, + model, + metadata, + permissions, + _marker: PhantomData, + } + } +} + +impl<'a, K, M> From<&'a dyn ServerObject> for Option<&'a GenericServerObject> +where + K: 'static, + M: 'static, +{ + fn from(value: &'a dyn ServerObject) -> Self { + value.as_any().downcast_ref::>() + } +} + +impl<'a, K, M> From<&'a Box> for Option<&'a GenericServerObject> +where + K: 'static, + M: 'static, +{ + fn from(value: &'a Box) -> Self { + value.as_ref().into() + } +} + +impl ServerObject for GenericServerObject +where + K: 'static, + M: ServerObjectModel, +{ + fn object_type(&self) -> ObjectType { + self.model.object_type() + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} From 61b767d88425bee07001b11fa9717fc1ec22e7ae Mon Sep 17 00:00:00 2001 From: David Stern Date: Fri, 22 May 2026 15:37:58 -0400 Subject: [PATCH 2/2] clippy --- app/src/server/graphql/schema/mod.rs | 2 +- app/src/server/server_api/object.rs | 2 +- app/src/workspaces/gql_convert.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/server/graphql/schema/mod.rs b/app/src/server/graphql/schema/mod.rs index 8cb795b146..23d8df4a79 100644 --- a/app/src/server/graphql/schema/mod.rs +++ b/app/src/server/graphql/schema/mod.rs @@ -41,7 +41,7 @@ pub fn update_generic_string_object_result_to_update_result( }) } GenericStringObjectUpdate::GenericStringObjectUpdateRejected(rejected) => { - let format = rejected.conflicting_generic_string_object.format.clone(); + let format = rejected.conflicting_generic_string_object.format; let boxed: Box = match format { GenericStringObjectFormat::JsonEnvVarCollection => { boxed_rejected_generic_string_object::( diff --git a/app/src/server/server_api/object.rs b/app/src/server/server_api/object.rs index f6a7e0c98d..77196c6887 100644 --- a/app/src/server/server_api/object.rs +++ b/app/src/server/server_api/object.rs @@ -935,7 +935,7 @@ impl ObjectClient for ServerApi { let mut updated_generic_string_objects = HashMap::new(); if let Some(objects) = output.generic_string_objects { for gso in objects { - match gso.format.clone() { + match gso.format { warp_graphql::generic_string_object::GenericStringObjectFormat::JsonEnvVarCollection => { parse_server_gso::( &mut updated_generic_string_objects, diff --git a/app/src/workspaces/gql_convert.rs b/app/src/workspaces/gql_convert.rs index 441871c6fb..51a610d269 100644 --- a/app/src/workspaces/gql_convert.rs +++ b/app/src/workspaces/gql_convert.rs @@ -1136,7 +1136,7 @@ impl TryFrom for ServerCloudObject { ServerFolder::try_from_gql(folder)?, )), warp_graphql::object::CloudObject::GenericStringObject(gso) => { - match gso.format.clone() { + match gso.format { warp_graphql::generic_string_object::GenericStringObjectFormat::JsonEnvVarCollection => { Ok(ServerCloudObject::EnvVarCollection(ServerEnvVarCollection::try_from_gql(gso)?)) } @@ -1190,7 +1190,7 @@ impl TryFrom for ServerCloudObject { CloudObjectWithDescendants::FolderWithDescendants(fwd) => { Ok(ServerCloudObject::Folder(ServerFolder::try_from_gql(fwd.folder)?)) } - CloudObjectWithDescendants::GenericStringObject(gso) => match gso.format.clone() { + CloudObjectWithDescendants::GenericStringObject(gso) => match gso.format { warp_graphql::generic_string_object::GenericStringObjectFormat::JsonEnvVarCollection => { Ok(ServerCloudObject::EnvVarCollection(ServerEnvVarCollection::try_from_gql(gso)?)) }