use crate::ids::WorkspaceId;
use anyhow::{Result, anyhow, ensure};
use serde_json::{Value, json};
use crate::choices::{TaskPriority, TaskStatus};
use crate::types::Project;
use crate::types::Task;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskField {
Title,
Description,
Project,
Status,
Priority,
AvailableAt,
DueOn,
Deleted,
IsEpic,
}
impl TaskField {
pub const VERSIONED: [TaskField; 9] = [
TaskField::Title,
TaskField::Description,
TaskField::Project,
TaskField::Status,
TaskField::Priority,
TaskField::AvailableAt,
TaskField::Deleted,
TaskField::IsEpic,
TaskField::DueOn,
];
pub fn parse(field: &str) -> Option<Self> {
match field {
"title" => Some(Self::Title),
"description" => Some(Self::Description),
"project" => Some(Self::Project),
"status" => Some(Self::Status),
"priority" => Some(Self::Priority),
"available_at" => Some(Self::AvailableAt),
"due_on" => Some(Self::DueOn),
"deleted" => Some(Self::Deleted),
"is_epic" => Some(Self::IsEpic),
_ => None,
}
}
pub fn is_project(self) -> bool {
matches!(self, Self::Project)
}
pub fn is_scalar(self) -> bool {
!self.is_project()
}
pub fn parse_or_unknown(field: &str) -> Result<Self> {
Self::parse(field).ok_or_else(|| anyhow!("error unknown-field field={field}"))
}
pub fn parse_for_sync(field: &str) -> Result<Self> {
Self::parse(field).ok_or_else(|| anyhow!("error invalid-sync-change field={field}"))
}
pub fn as_str(self) -> &'static str {
match self {
Self::Title => "title",
Self::Description => "description",
Self::Project => "project",
Self::Status => "status",
Self::Priority => "priority",
Self::AvailableAt => "available_at",
Self::DueOn => "due_on",
Self::Deleted => "deleted",
Self::IsEpic => "is_epic",
}
}
pub fn validate_value(self, value: &str) -> Result<()> {
match self {
Self::Status => TaskStatus::parse(value).map(|_| ()).map_err(Into::into),
Self::Priority => TaskPriority::parse(value).map(|_| ()).map_err(Into::into),
Self::AvailableAt => crate::time_validation::validate_available_at_value(value),
Self::DueOn => crate::time_validation::validate_due_on_value(value),
Self::Deleted if matches!(value, "0" | "1") => Ok(()),
Self::Deleted => anyhow::bail!("error invalid-deleted value={value}"),
Self::IsEpic if matches!(value, "0" | "1") => Ok(()),
Self::IsEpic => anyhow::bail!("error invalid-is-epic value={value}"),
_ => Ok(()),
}
}
pub fn updates_queue_activity(self) -> bool {
matches!(self, Self::Status | Self::Priority)
}
pub fn scalar_payload(
self,
workspace_id: &WorkspaceId,
workspace_key: &str,
value: &str,
) -> Result<Value> {
ensure!(self.is_scalar(), "error project-update-requires-project-id");
Ok(json!({
"workspace_id": workspace_id,
"workspace_key": workspace_key,
"value": value,
}))
}
pub fn project_payload(
workspace_id: &WorkspaceId,
workspace_key: &str,
project: &Project,
) -> Value {
json!({
"workspace_id": workspace_id,
"workspace_key": workspace_key,
"value": &project.id,
"project_id": &project.id,
"project_key": &project.key,
"project_name": &project.name,
"project_prefix": &project.prefix,
})
}
pub fn current_value(self, task: &Task) -> String {
match self {
Self::Title => task.title.clone(),
Self::Description => task.description.clone(),
Self::Project => task.project_id.to_string(),
Self::Status => task.status.as_str().to_string(),
Self::Priority => task.priority.as_str().to_string(),
Self::AvailableAt => task.available_at.clone().unwrap_or_default(),
Self::DueOn => task.due_on.clone().unwrap_or_default(),
Self::Deleted => {
if task.deleted {
"1".to_string()
} else {
"0".to_string()
}
}
Self::IsEpic => {
if task.is_epic {
"1".to_string()
} else {
"0".to_string()
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::TaskField;
use crate::ids::WorkspaceId;
use crate::types::Project;
#[test]
fn versioned_fields_keep_protocol_order() {
let fields: Vec<_> = TaskField::VERSIONED
.into_iter()
.map(TaskField::as_str)
.collect();
assert_eq!(
fields,
[
"title",
"description",
"project",
"status",
"priority",
"available_at",
"deleted",
"is_epic",
"due_on"
]
);
}
#[test]
fn parse_unknown_field_errors_are_canonicalized() {
assert_eq!(
TaskField::parse_or_unknown("missing")
.unwrap_err()
.to_string(),
"error unknown-field field=missing"
);
assert_eq!(
TaskField::parse_for_sync("missing")
.unwrap_err()
.to_string(),
"error invalid-sync-change field=missing"
);
}
#[test]
fn deleted_validation() {
assert!(TaskField::Deleted.validate_value("0").is_ok());
assert!(TaskField::Deleted.validate_value("1").is_ok());
assert_eq!(
TaskField::Deleted
.validate_value("x")
.unwrap_err()
.to_string(),
"error invalid-deleted value=x"
);
}
#[test]
fn scalar_payload_shape() {
let workspace_id: WorkspaceId = "0000000000000001".parse().unwrap();
let payload = TaskField::Title
.scalar_payload(&workspace_id, "key", "v")
.expect("valid scalar field");
assert_eq!(
payload,
serde_json::json!({
"workspace_id": "0000000000000001",
"workspace_key": "key",
"value": "v",
})
);
assert!(
TaskField::Project
.scalar_payload(&workspace_id, "key", "v")
.is_err()
);
}
#[test]
fn project_payload_shape() {
let workspace_id: WorkspaceId = "0000000000000001".parse().unwrap();
let project = Project {
id: "0000000000000001".parse().unwrap(),
workspace_id: workspace_id.clone(),
key: "proj".to_string(),
name: "Project".to_string(),
prefix: "pp".to_string(),
};
assert_eq!(
TaskField::project_payload(&workspace_id, "key", &project),
serde_json::json!({
"workspace_id": "0000000000000001",
"workspace_key": "key",
"value": "0000000000000001",
"project_id": "0000000000000001",
"project_key": "proj",
"project_name": "Project",
"project_prefix": "pp",
})
);
}
}