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    /// Show who may do what in this queue.
55    #[command(long_about = crate::cli::help::md(crate::cli::help::QUEUE_ACCESS))]
56    Access {
57        /// Queue key, e.g. PROJ.
58        key: String,
59    },
60    /// List the fields this queue defines itself.
61    #[command(name = "local-fields", long_about = crate::cli::help::md(crate::cli::help::QUEUE_LOCAL_FIELDS))]
62    LocalFields {
63        /// Queue key, e.g. PROJ.
64        key: String,
65    },
66    /// Show a queue's fields, including custom ones and their keys.
67    #[command(long_about = crate::cli::help::md(crate::cli::help::QUEUE_FIELDS))]
68    Fields {
69        /// Queue key, e.g. PROJ.
70        key: String,
71    },
72}
73
74pub async fn run(command: &QueueCommand, session: &Session) -> ExitCode {
75    let client = match session.client() {
76        Ok(client) => client,
77        Err(code) => return code,
78    };
79
80    match command {
81        QueueCommand::List => match client.queues().await {
82            Ok(queues) => render(&queues, session, |queues| {
83                queue::queues(queues, &session.render)
84            }),
85            Err(error) => {
86                let code = error.exit_code();
87                report(&error, code)
88            }
89        },
90        QueueCommand::Get { key } => match client.queue(key).await {
91            Ok(settings) => render_one(&settings, session, |settings| {
92                queue::settings(settings, &session.render)
93            }),
94            Err(error) => {
95                let code = error.exit_code();
96                report(&error, code)
97            }
98        },
99        QueueCommand::Create {
100            key,
101            name,
102            like,
103            lead,
104        } => create(&client, key, name, like, lead.as_deref(), session).await,
105        QueueCommand::Versions { key } => match client.queue_versions(key).await {
106            Ok(versions) => render(&versions, session, |versions| {
107                queue::versions(key, versions, &session.render)
108            }),
109            Err(error) => {
110                let code = error.exit_code();
111                report(&error, code)
112            }
113        },
114        QueueCommand::Tags { key } => match client.queue_tags(key).await {
115            Ok(tags) => render(&tags, session, |tags| {
116                queue::tags(key, tags, &session.render)
117            }),
118            Err(error) => {
119                let code = error.exit_code();
120                report(&error, code)
121            }
122        },
123        QueueCommand::Access { key } => match client.queue_access(key).await {
124            Ok(access) => render_one(&access, session, |access| {
125                queue::access(key, access, &session.render)
126            }),
127            Err(error) => {
128                let code = error.exit_code();
129                report(&error, code)
130            }
131        },
132        QueueCommand::Automation { key } => match client.queue_automation(key).await {
133            Ok(automation) => render_one(&automation, session, |automation| {
134                queue::automation(key, automation, &session.render)
135            }),
136            Err(error) => {
137                let code = error.exit_code();
138                report(&error, code)
139            }
140        },
141        QueueCommand::LocalFields { key } => match client.queue_local_fields(key).await {
142            Ok(fields) => render(&fields, session, |fields| {
143                queue::local_fields(key, fields, &session.render)
144            }),
145            Err(error) => {
146                let code = error.exit_code();
147                report(&error, code)
148            }
149        },
150        QueueCommand::Fields { key } => match client.queue_fields(key).await {
151            Ok(fields) => render(&fields, session, |fields| {
152                queue::fields(fields, &session.render)
153            }),
154            Err(error) => {
155                let code = error.exit_code();
156                report(&error, code)
157            }
158        },
159    }
160}
161
162/// Render either as text, through the entity's own renderer, or in whichever
163/// machine format was asked for.
164fn render<T: serde::Serialize>(
165    value: &[T],
166    session: &Session,
167    as_text: impl Fn(&[T]) -> String,
168) -> ExitCode {
169    let rendered = match session.render.format {
170        Format::Text => Ok(as_text(value)),
171        // There is no upstream payload worth preserving separately here: these
172        // listings are already flat, so raw and normalised would be the same
173        // shape with uglier names.
174        Format::JsonRaw => machine(&value, Format::Json),
175        other => machine(&value, other),
176    };
177
178    match rendered {
179        Ok(text) => {
180            emit(&text);
181            ExitCode::Success
182        }
183        Err(error) => report(&error, ExitCode::Failure),
184    }
185}
186
187/// The same, for a command whose answer is one thing rather than a list.
188fn render_one<T: serde::Serialize>(
189    value: &T,
190    session: &Session,
191    as_text: impl Fn(&T) -> String,
192) -> ExitCode {
193    let rendered = match session.render.format {
194        Format::Text => Ok(as_text(value)),
195        Format::JsonRaw => machine(value, Format::Json),
196        other => machine(value, other),
197    };
198
199    match rendered {
200        Ok(text) => {
201            emit(&text);
202            ExitCode::Success
203        }
204        Err(error) => report(&error, ExitCode::Failure),
205    }
206}
207
208/// Create a queue, modelled on an existing one.
209///
210/// A queue needs an issue type paired with a workflow and a set of resolutions,
211/// and workflow ids are organisation-specific strings nobody has memorised. So
212/// the shape is copied from a queue that already works rather than asked for:
213/// `--like PROJ` is the difference between a command someone can run and a
214/// command someone can run after reading the API reference.
215async fn create(
216    client: &crate::api::Client,
217    key: &str,
218    name: &str,
219    like: &str,
220    lead: Option<&str>,
221    session: &Session,
222) -> ExitCode {
223    let blueprint = match client.queue_blueprint(like).await {
224        Ok(blueprint) => blueprint,
225        Err(error) => {
226            let code = error.exit_code();
227            return report(&error, code);
228        }
229    };
230
231    let lead = match lead {
232        Some(lead) => lead.to_owned(),
233        None => match client.myself().await {
234            Ok(user) => user.login.unwrap_or(user.id),
235            Err(error) => {
236                let code = error.exit_code();
237                return report(&error, code);
238            }
239        },
240    };
241
242    let body = serde_json::json!({
243        "key": key,
244        "name": name,
245        "lead": lead,
246        "defaultType": blueprint.default_type,
247        "defaultPriority": blueprint.default_priority,
248        "issueTypesConfig": blueprint.issue_types,
249    });
250
251    let action = format!("create queue {key} modelled on {like}");
252    let targets = [key.to_owned()];
253    let intent = crate::cli::write::Intent {
254        action: &action,
255        targets: &targets,
256        body: &body,
257        // A queue key is claimed once and cannot be given back: Tracker deletes
258        // a queue by hiding it, and the key stays spent. Irreversible in kind,
259        // not at scale, which is the case `--yes` exists for either way.
260        always_confirm: true,
261    };
262    if let crate::cli::write::Gate::Stop(code) = crate::cli::write::check(&intent, session) {
263        return code;
264    }
265
266    match client.create_queue(&body).await {
267        Ok(created) => {
268            let rendered = match session.render.format {
269                Format::Text => Ok(queue::settings(&created, &session.render)),
270                Format::JsonRaw => machine(&created, Format::Json),
271                other => machine(&created, other),
272            };
273            match rendered {
274                Ok(text) => {
275                    emit(&text);
276                    ExitCode::Success
277                }
278                Err(error) => report(&error, ExitCode::Failure),
279            }
280        }
281        Err(error) => {
282            let code = error.exit_code();
283            report(&error, code)
284        }
285    }
286}