Skip to main content

wekan_cli/commands/
dependencies.rs

1mod create;
2mod delete;
3mod list;
4#[cfg(test)]
5mod tests;
6mod update;
7
8use crate::{
9    client::{WekanClientFactory, dependencies::DependencyMetadata},
10    command_result::{CommandSuccess, DependencySubmittedField},
11    credentials::CredentialStore,
12    error::AppError,
13    input::ConfirmationProvider,
14};
15use clap::{Args, Subcommand};
16
17#[derive(Debug, Args)]
18pub struct DependencyArgs {
19    #[command(subcommand)]
20    pub command: DependencyCommand,
21}
22
23#[derive(Debug, Subcommand)]
24pub enum DependencyCommand {
25    /// List a board's dependency lines or one card's outgoing dependencies.
26    List(list::ListArgs),
27    /// Create or update a dependency to another card on the same board.
28    Create(create::CreateArgs),
29    /// Update dependency metadata; omitted fields stay unchanged.
30    Update(update::UpdateArgs),
31    /// Remove a dependency line, with confirmation or --yes.
32    Delete(delete::DeleteArgs),
33}
34
35pub(crate) async fn dispatch(
36    command: DependencyCommand,
37    factory: &WekanClientFactory,
38    store: &dyn CredentialStore,
39    confirmation: &dyn ConfirmationProvider,
40) -> Result<CommandSuccess, AppError> {
41    match command {
42        DependencyCommand::List(args) => list::execute(args, factory, store).await,
43        DependencyCommand::Create(args) => create::execute(args, factory, store).await,
44        DependencyCommand::Update(args) => update::execute(args, factory, store).await,
45        DependencyCommand::Delete(args) => {
46            delete::execute(args, factory, store, confirmation).await
47        }
48    }
49}
50
51#[derive(Debug, Args)]
52struct MutationTarget {
53    /// Wekan target card ID; the dependency is stored on --card.
54    #[arg(value_parser = identifier, help_heading = "Target")]
55    target_card_id: String,
56    /// Wekan board ID.
57    #[arg(long = "board", value_parser = identifier, help_heading = "Target")]
58    board_id: String,
59    /// Wekan source card ID.
60    #[arg(long = "card", value_parser = identifier, help_heading = "Target")]
61    card_id: String,
62}
63
64#[derive(Debug, Args)]
65struct MetadataArgs {
66    /// Relation: related-to, blocks, is-blocked-by, fixes, or is-fixed-by; also accepts empty.
67    #[arg(long = "type", value_name = "TYPE", value_parser = dependency_type, conflicts_with = "null_type", help_heading = "Metadata")]
68    dependency_type: Option<String>,
69    /// Send JSON null for the relation type.
70    #[arg(long, help_heading = "Metadata")]
71    null_type: bool,
72    /// CSS color, sent unchanged; may be empty.
73    #[arg(long, conflicts_with = "null_color", help_heading = "Metadata")]
74    color: Option<String>,
75    /// Send JSON null for the color.
76    #[arg(long, help_heading = "Metadata")]
77    null_color: bool,
78    /// FontAwesome icon name without fa-, sent unchanged; may be empty.
79    #[arg(long, conflicts_with = "null_icon", help_heading = "Metadata")]
80    icon: Option<String>,
81    /// Send JSON null for the icon.
82    #[arg(long, help_heading = "Metadata")]
83    null_icon: bool,
84}
85
86impl MetadataArgs {
87    fn into_request(self) -> DependencyMetadata {
88        DependencyMetadata {
89            dependency_type: nullable(self.dependency_type, self.null_type),
90            color: nullable(self.color, self.null_color),
91            icon: nullable(self.icon, self.null_icon),
92        }
93    }
94}
95
96fn submitted_fields(request: &DependencyMetadata) -> Vec<DependencySubmittedField> {
97    let mut fields = Vec::new();
98    if request.dependency_type.is_some() {
99        fields.push(DependencySubmittedField::Type);
100    }
101    if request.color.is_some() {
102        fields.push(DependencySubmittedField::Color);
103    }
104    if request.icon.is_some() {
105        fields.push(DependencySubmittedField::Icon);
106    }
107    fields
108}
109
110fn nullable(value: Option<String>, null: bool) -> Option<Option<String>> {
111    if null { Some(None) } else { value.map(Some) }
112}
113
114fn identifier(value: &str) -> Result<String, String> {
115    let value = value.trim();
116    if value.is_empty() || matches!(value, "." | "..") {
117        Err("ID must not be empty, whitespace, or dot-only".to_owned())
118    } else {
119        Ok(value.to_owned())
120    }
121}
122
123fn dependency_type(value: &str) -> Result<String, String> {
124    if matches!(
125        value,
126        "" | "related-to" | "blocks" | "is-blocked-by" | "fixes" | "is-fixed-by"
127    ) {
128        Ok(value.to_owned())
129    } else {
130        Err("type must be related-to, blocks, is-blocked-by, fixes, is-fixed-by, or an empty string".to_owned())
131    }
132}