Skip to main content

ytcli/cli/
queue.rs

1//! Queue commands. Reads, and one write: creating a queue.
2
3use clap::Subcommand;
4
5use crate::cli::{Session, emit, report};
6use crate::exit::ExitCode;
7use crate::render::{Format, machine, queue};
8
9#[derive(Debug, Subcommand)]
10pub enum QueueCommand {
11    /// List queues visible to this profile.
12    #[command(long_about = crate::cli::help::md(crate::cli::help::QUEUE_LIST))]
13    List,
14    /// Show a queue and the defaults issues in it start with.
15    #[command(long_about = crate::cli::help::md(crate::cli::help::QUEUE_GET))]
16    Get {
17        /// Queue key, e.g. PROJ.
18        key: String,
19    },
20    /// Create a queue, modelled on one that already exists.
21    #[command(long_about = crate::cli::help::md(crate::cli::help::QUEUE_CREATE))]
22    Create {
23        /// Key for the new queue, e.g. PROJ. Uppercase, and permanent.
24        #[arg(long, short = 'k')]
25        key: String,
26        /// Human-readable name.
27        #[arg(long, short = 'n')]
28        name: String,
29        /// Existing queue to copy the issue types, workflows and defaults from.
30        #[arg(long, short = 'l', value_name = "QUEUE")]
31        like: String,
32        /// Queue lead. Defaults to whoever the token belongs to.
33        #[arg(long)]
34        lead: Option<String>,
35    },
36    /// List the versions a queue defines.
37    #[command(long_about = crate::cli::help::md(crate::cli::help::QUEUE_VERSIONS))]
38    Versions {
39        /// Queue key, e.g. PROJ.
40        key: String,
41    },
42    /// List the tags in use in a queue.
43    #[command(long_about = crate::cli::help::md(crate::cli::help::QUEUE_TAGS))]
44    Tags {
45        /// Queue key, e.g. PROJ.
46        key: String,
47    },
48    /// Show what changes issues in this queue on its own.
49    #[command(long_about = crate::cli::help::md(crate::cli::help::QUEUE_AUTOMATION))]
50    Automation {
51        /// Queue key, e.g. PROJ.
52        key: String,
53    },
54    /// List the fields this queue defines itself.
55    #[command(name = "local-fields", long_about = crate::cli::help::md(crate::cli::help::QUEUE_LOCAL_FIELDS))]
56    LocalFields {
57        /// Queue key, e.g. PROJ.
58        key: String,
59    },
60    /// Show a queue's fields, including custom ones and their keys.
61    #[command(long_about = crate::cli::help::md(crate::cli::help::QUEUE_FIELDS))]
62    Fields {
63        /// Queue key, e.g. PROJ.
64        key: String,
65    },
66}
67
68pub async fn run(command: &QueueCommand, session: &Session) -> ExitCode {
69    let client = match session.client() {
70        Ok(client) => client,
71        Err(code) => return code,
72    };
73
74    match command {
75        QueueCommand::List => match client.queues().await {
76            Ok(queues) => render(&queues, session, |queues| {
77                queue::queues(queues, &session.render)
78            }),
79            Err(error) => {
80                let code = error.exit_code();
81                report(&error, code)
82            }
83        },
84        QueueCommand::Get { key } => match client.queue(key).await {
85            Ok(settings) => {
86                let rendered = match session.render.format {
87                    Format::Text => Ok(queue::settings(&settings, &session.render)),
88                    Format::JsonRaw => machine(&settings, Format::Json),
89                    other => machine(&settings, other),
90                };
91                match rendered {
92                    Ok(text) => {
93                        emit(&text);
94                        ExitCode::Success
95                    }
96                    Err(error) => report(&error, ExitCode::Failure),
97                }
98            }
99            Err(error) => {
100                let code = error.exit_code();
101                report(&error, code)
102            }
103        },
104        QueueCommand::Create {
105            key,
106            name,
107            like,
108            lead,
109        } => create(&client, key, name, like, lead.as_deref(), session).await,
110        QueueCommand::Versions { key } => match client.queue_versions(key).await {
111            Ok(versions) => render(&versions, session, |versions| {
112                queue::versions(key, versions, &session.render)
113            }),
114            Err(error) => {
115                let code = error.exit_code();
116                report(&error, code)
117            }
118        },
119        QueueCommand::Tags { key } => match client.queue_tags(key).await {
120            Ok(tags) => render(&tags, session, |tags| {
121                queue::tags(key, tags, &session.render)
122            }),
123            Err(error) => {
124                let code = error.exit_code();
125                report(&error, code)
126            }
127        },
128        QueueCommand::Automation { key } => match client.queue_automation(key).await {
129            Ok(automation) => {
130                let rendered = match session.render.format {
131                    Format::Text => Ok(queue::automation(key, &automation, &session.render)),
132                    Format::JsonRaw => machine(&automation, Format::Json),
133                    other => machine(&automation, other),
134                };
135                match rendered {
136                    Ok(text) => {
137                        emit(&text);
138                        ExitCode::Success
139                    }
140                    Err(error) => report(&error, ExitCode::Failure),
141                }
142            }
143            Err(error) => {
144                let code = error.exit_code();
145                report(&error, code)
146            }
147        },
148        QueueCommand::LocalFields { key } => match client.queue_local_fields(key).await {
149            Ok(fields) => render(&fields, session, |fields| {
150                queue::local_fields(key, fields, &session.render)
151            }),
152            Err(error) => {
153                let code = error.exit_code();
154                report(&error, code)
155            }
156        },
157        QueueCommand::Fields { key } => match client.queue_fields(key).await {
158            Ok(fields) => render(&fields, session, |fields| {
159                queue::fields(fields, &session.render)
160            }),
161            Err(error) => {
162                let code = error.exit_code();
163                report(&error, code)
164            }
165        },
166    }
167}
168
169/// Render either as text, through the entity's own renderer, or in whichever
170/// machine format was asked for.
171fn render<T: serde::Serialize>(
172    value: &[T],
173    session: &Session,
174    as_text: impl Fn(&[T]) -> String,
175) -> ExitCode {
176    let rendered = match session.render.format {
177        Format::Text => Ok(as_text(value)),
178        // There is no upstream payload worth preserving separately here: these
179        // listings are already flat, so raw and normalised would be the same
180        // shape with uglier names.
181        Format::JsonRaw => machine(&value, Format::Json),
182        other => machine(&value, other),
183    };
184
185    match rendered {
186        Ok(text) => {
187            emit(&text);
188            ExitCode::Success
189        }
190        Err(error) => report(&error, ExitCode::Failure),
191    }
192}
193
194/// Create a queue, modelled on an existing one.
195///
196/// A queue needs an issue type paired with a workflow and a set of resolutions,
197/// and workflow ids are organisation-specific strings nobody has memorised. So
198/// the shape is copied from a queue that already works rather than asked for:
199/// `--like PROJ` is the difference between a command someone can run and a
200/// command someone can run after reading the API reference.
201async fn create(
202    client: &crate::api::Client,
203    key: &str,
204    name: &str,
205    like: &str,
206    lead: Option<&str>,
207    session: &Session,
208) -> ExitCode {
209    let blueprint = match client.queue_blueprint(like).await {
210        Ok(blueprint) => blueprint,
211        Err(error) => {
212            let code = error.exit_code();
213            return report(&error, code);
214        }
215    };
216
217    let lead = match lead {
218        Some(lead) => lead.to_owned(),
219        None => match client.myself().await {
220            Ok(user) => user.login.unwrap_or(user.id),
221            Err(error) => {
222                let code = error.exit_code();
223                return report(&error, code);
224            }
225        },
226    };
227
228    let body = serde_json::json!({
229        "key": key,
230        "name": name,
231        "lead": lead,
232        "defaultType": blueprint.default_type,
233        "defaultPriority": blueprint.default_priority,
234        "issueTypesConfig": blueprint.issue_types,
235    });
236
237    let action = format!("create queue {key} modelled on {like}");
238    let targets = [key.to_owned()];
239    let intent = crate::cli::write::Intent {
240        action: &action,
241        targets: &targets,
242        body: &body,
243        // A queue key is claimed once and cannot be given back: Tracker deletes
244        // a queue by hiding it, and the key stays spent. Irreversible in kind,
245        // not at scale, which is the case `--yes` exists for either way.
246        always_confirm: true,
247    };
248    if let crate::cli::write::Gate::Stop(code) = crate::cli::write::check(&intent, session) {
249        return code;
250    }
251
252    match client.create_queue(&body).await {
253        Ok(created) => {
254            let rendered = match session.render.format {
255                Format::Text => Ok(queue::settings(&created, &session.render)),
256                Format::JsonRaw => machine(&created, Format::Json),
257                other => machine(&created, other),
258            };
259            match rendered {
260                Ok(text) => {
261                    emit(&text);
262                    ExitCode::Success
263                }
264                Err(error) => report(&error, ExitCode::Failure),
265            }
266        }
267        Err(error) => {
268            let code = error.exit_code();
269            report(&error, code)
270        }
271    }
272}