Skip to main content

ytcli/cli/
dict.rs

1//! The values a write is allowed to take.
2//!
3//! Everything else in this tool answers what *is*; this answers what *may be*.
4//! `issue create --type` and `issue update --set priority=…` are otherwise
5//! written on faith and judged by Tracker, which refuses with a message about a
6//! field the caller was never shown a list for.
7//!
8//! Read-only, and organisation-wide: a queue narrows these — `queue get` says
9//! which type and priority its issues start with — but the dictionary itself is
10//! defined once for the whole organisation.
11
12use clap::{Subcommand, ValueEnum};
13
14use crate::api::Dictionary;
15use crate::api::models::DictEntry;
16use crate::cli::{Session, emit, report};
17use crate::exit::ExitCode;
18use crate::render::{Format, dict as render, machine};
19
20#[derive(Debug, Subcommand)]
21pub enum DictCommand {
22    /// List the values issues can take.
23    #[command(long_about = crate::cli::help::md(crate::cli::help::DICT_LIST))]
24    List {
25        /// One dictionary instead of all four.
26        #[arg(long, value_enum)]
27        kind: Option<Kind>,
28    },
29}
30
31#[derive(Debug, Clone, Copy, ValueEnum)]
32pub enum Kind {
33    Types,
34    Priorities,
35    Statuses,
36    Resolutions,
37}
38
39impl From<Kind> for Dictionary {
40    fn from(kind: Kind) -> Self {
41        match kind {
42            Kind::Types => Self::Types,
43            Kind::Priorities => Self::Priorities,
44            Kind::Statuses => Self::Statuses,
45            Kind::Resolutions => Self::Resolutions,
46        }
47    }
48}
49
50pub async fn run(command: &DictCommand, session: &Session) -> ExitCode {
51    let client = match session.client() {
52        Ok(client) => client,
53        Err(code) => return code,
54    };
55
56    let DictCommand::List { kind } = command;
57
58    // All four by default, and sequentially. The whole point of the command is
59    // to be the one call an agent makes before writing anything, and four small
60    // responses in one answer beat four round trips to discover the same thing.
61    let wanted: Vec<Dictionary> = match kind {
62        Some(kind) => vec![(*kind).into()],
63        None => Dictionary::ALL.to_vec(),
64    };
65
66    let mut sections: Vec<(Dictionary, Vec<DictEntry>)> = Vec::with_capacity(wanted.len());
67    for kind in wanted {
68        match client.dictionary(kind).await {
69            Ok(entries) => sections.push((kind, entries)),
70            Err(error) => {
71                let code = error.exit_code();
72                return report(&error, code);
73            }
74        }
75    }
76
77    let rendered = match session.render.format {
78        Format::Text => Ok(render::many(&sections, &session.render)),
79        other => {
80            // Machine output is keyed by dictionary rather than concatenated:
81            // `bug` the issue type and `bug` the anything-else are only telling
82            // apart by which list they came from.
83            let keyed: serde_json::Map<String, serde_json::Value> = sections
84                .iter()
85                .map(|(kind, entries)| {
86                    (
87                        kind.label().to_owned(),
88                        serde_json::to_value(entries).unwrap_or(serde_json::Value::Null),
89                    )
90                })
91                .collect();
92            machine(
93                &keyed,
94                if other == Format::JsonRaw {
95                    Format::Json
96                } else {
97                    other
98                },
99            )
100        }
101    };
102
103    match rendered {
104        Ok(text) => {
105            emit(&text);
106            ExitCode::Success
107        }
108        Err(error) => report(&error, ExitCode::Failure),
109    }
110}