#[cfg(not(target_family = "wasm"))] pub mod persistence; use cloud_objects::cloud_object::{ GenericCloudObject, GenericServerObject, ObjectType, ServerObjectModel, }; use cloud_objects::ids::{GenericStringObjectId, ServerId, SyncId}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; /// Workflow model used by Warp and warp-internal. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] #[allow(clippy::large_enum_variant)] pub enum Workflow { AgentMode { name: String, /// The query to be inserted in the terminal input. query: String, #[serde(skip_serializing_if = "Option::is_none")] description: Option, #[serde(default)] arguments: Vec, }, #[serde(untagged)] Command { name: String, command: String, #[serde(default)] tags: Vec, description: Option, #[serde(default)] arguments: Vec, source_url: Option, author: Option, author_url: Option, #[serde(default)] shells: Vec, #[serde(default)] environment_variables: Option, }, } impl Workflow { pub fn name(&self) -> &str { match self { Self::AgentMode { name, .. } => name.as_str(), Self::Command { name, .. } => name.as_str(), } } /// The core "content" of the workflow. /// /// For Command workflows, this is the shell command. For Agent Mode workflows, this is the /// query. pub fn content(&self) -> &str { match self { Self::AgentMode { query, .. } => query, Self::Command { command, .. } => command, } } pub fn prompt(&self) -> Option<&str> { if let Self::AgentMode { query, .. } = self { Some(query.as_str()) } else { None } } pub fn command(&self) -> Option<&str> { if let Self::Command { command, .. } = self { Some(command.as_str()) } else { None } } pub fn description(&self) -> Option<&String> { match self { Self::AgentMode { description, .. } => description.as_ref(), Self::Command { description, .. } => description.as_ref(), } } pub fn arguments(&self) -> &Vec { match self { Self::AgentMode { arguments, .. } => arguments, Self::Command { arguments, .. } => arguments, } } pub fn tags(&self) -> Option<&Vec> { match self { Self::Command { tags, .. } => Some(tags), Self::AgentMode { .. } => None, } } pub fn source_url(&self) -> Option<&String> { match self { Self::Command { source_url, .. } => source_url.as_ref(), Self::AgentMode { .. } => None, } } pub fn author_name(&self) -> Option<&String> { match self { Self::Command { author, .. } => author.as_ref(), Self::AgentMode { .. } => None, } } pub fn shells(&self) -> Option<&Vec> { match self { Self::Command { shells, .. } => Some(shells), Self::AgentMode { .. } => None, } } pub fn is_command_workflow(&self) -> bool { matches!(self, Self::Command { .. }) } pub fn is_agent_mode_workflow(&self) -> bool { matches!(self, Self::AgentMode { .. }) } /// Returns `true` if the workflow name starts with the given character (case-insensitive). /// /// Used by prompt search datasources to prefix-match on single-character queries, where /// fuzzy matching would be unreliable. pub fn name_starts_with_char_ignore_case(&self, c: char) -> bool { self.name() .chars() .next() .is_some_and(|first| first.eq_ignore_ascii_case(&c)) } /// Return a list of every enum ID referenced by this workflow. pub fn get_enum_ids(&self) -> Vec { self.arguments() .iter() .filter_map(|arg| match arg.arg_type { ArgumentType::Enum { enum_id } => Some(enum_id), ArgumentType::Text => None, }) .collect() } /// Return a list of every enum ID that has been synced to the server, used for telemetry. pub fn get_server_enum_ids(&self) -> Vec { self.arguments() .iter() .filter_map(|arg| match arg.arg_type { ArgumentType::Enum { enum_id } => enum_id.into_server(), ArgumentType::Text => None, }) .map(Into::into) .collect() } pub fn default_env_vars(&self) -> Option { match self { Workflow::Command { environment_variables, .. } => *environment_variables, Workflow::AgentMode { .. } => None, } } /// Given two IDs, replace any instance of the old ID referenced by this workflow with the new ID. /// Returns `true` if any instances of the old_id were present. pub fn replace_object_id(&mut self, old_id: SyncId, new_id: SyncId) -> bool { let mut changed = false; let arguments = match self { Self::Command { arguments, .. } => arguments, Self::AgentMode { arguments, .. } => arguments, }; for arg in arguments.iter_mut() { match &mut arg.arg_type { ArgumentType::Enum { enum_id } if *enum_id == old_id => { *enum_id = new_id; changed = true; } ArgumentType::Enum { .. } | ArgumentType::Text => {} } } if let Self::Command { environment_variables, .. } = self && *environment_variables == Some(old_id) { *environment_variables = Some(new_id); changed = true; } changed } pub fn new(name: impl Into, command: impl Into) -> Self { Workflow::Command { name: name.into(), command: command.into(), tags: Vec::new(), arguments: Vec::new(), description: None, source_url: None, author: None, author_url: None, shells: Vec::new(), environment_variables: None, } } pub fn with_arguments(mut self, new_arguments: Vec) -> Self { match self { Workflow::AgentMode { ref mut arguments, .. } | Workflow::Command { ref mut arguments, .. } => { *arguments = new_arguments; } } self } pub fn with_description(mut self, new_description: String) -> Self { match self { Workflow::AgentMode { ref mut description, .. } | Workflow::Command { ref mut description, .. } => { *description = Some(new_description); } } self } pub fn set_name(&mut self, new_name: &str) { match self { Workflow::AgentMode { name, .. } | Workflow::Command { name, .. } => { new_name.clone_into(name) } } } } /// Create a warp-internal Workflow model from a public-facing workflow /// https://github.com/warpdotdev/workflows/blob/main/workflow-types/src/lib.rs impl From for Workflow { fn from(workflow: warp_workflows::Workflow) -> Self { Workflow::Command { name: workflow.name, command: workflow.command, description: workflow.description, arguments: workflow.arguments.into_iter().map(Argument::from).collect(), tags: workflow.tags, source_url: workflow.source_url, author: workflow.author, author_url: workflow.author_url, shells: workflow.shells, environment_variables: None, } } } /// Argument model to be used in `warp-internal` #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Hash, Default)] pub struct Argument { pub name: String, /// The type of the argument to the workflow #[serde(flatten, deserialize_with = "deserialize_arg_type")] pub arg_type: ArgumentType, pub description: Option, pub default_value: Option, } impl From for Argument { fn from(arg: warp_workflows::Argument) -> Self { Argument { name: arg.name, arg_type: ArgumentType::Text, description: arg.description, default_value: arg.default_value, } } } impl Argument { pub fn new(name: impl Into, arg_type: ArgumentType) -> Self { Argument { arg_type, name: name.into(), description: None, default_value: None, } } pub fn with_description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); self } pub fn with_default(mut self, default: impl Into) -> Self { self.default_value = Some(default.into()); self } pub fn name(&self) -> &str { &self.name } pub fn description(&self) -> &Option { &self.description } pub fn arg_type(&self) -> &ArgumentType { &self.arg_type } pub fn default_value(&self) -> &Option { &self.default_value } } /// The type of the workflow argument #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Hash)] #[serde(tag = "arg_type")] #[derive(Default)] pub enum ArgumentType { #[default] Text, Enum { /// The ID of the associated WorkflowEnum Generic String Object enum_id: SyncId, }, } /// Custom deserialization for argument types, used to both `flatten` the argument type /// and allow for the specification of `default` behavior. /// /// Necessary because serde currently does not support the use of `flatten` with a `default`, /// related GitHub issue here: https://github.com/serde-rs/serde/issues/1626 fn deserialize_arg_type<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let value: Value = Deserialize::deserialize(deserializer)?; let arg_type = match value.get("arg_type").and_then(|value| value.as_str()) { Some("Text") => ArgumentType::Text, Some("Enum") => { let enum_id = value .get("enum_id") .ok_or(serde::de::Error::missing_field("enum_id"))?; let deserialized_id = SyncId::deserialize(enum_id) .map_err(|_| serde::de::Error::custom("Unable to parse enum_id"))?; ArgumentType::Enum { enum_id: deserialized_id, } } _ => ArgumentType::default(), }; Ok(arg_type) } /// The model for a `CloudWorkflow`. #[derive(Clone, Debug, PartialEq)] pub struct CloudWorkflowModel { pub data: Workflow, } impl CloudWorkflowModel { pub fn new(workflow: Workflow) -> Self { Self { data: workflow } } } impl ServerObjectModel for CloudWorkflowModel { fn object_type(&self) -> ObjectType { ObjectType::Workflow } } #[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] pub struct WorkflowId(ServerId); cloud_objects::server_id_traits! { WorkflowId, "Workflow" } /// `CloudWorkflow` is a workflow retrieved from the server. pub type CloudWorkflow = GenericCloudObject; pub type ServerWorkflow = GenericServerObject; impl From for Workflow { fn from(cloud_workflow: CloudWorkflow) -> Self { cloud_workflow.model().data.clone() } } impl From<&CloudWorkflow> for Workflow { fn from(cloud_workflow: &CloudWorkflow) -> Self { cloud_workflow.model().data.to_owned() } } #[cfg(test)] #[path = "workflow_tests.rs"] mod tests;