Skip to main content

rskit_cli/prompt/
choice.rs

1//! Selectable options: [`Choice`] and its stable [`ChoiceId`].
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// The stable identifier a caller uses to recognise a chosen [`Choice`].
8///
9/// A `ChoiceId` is opaque data (not a closure): the caller maps it back to
10/// domain meaning after the prompt returns.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
12pub struct ChoiceId(String);
13
14impl ChoiceId {
15    /// Create a choice identifier from any string-like value.
16    #[must_use]
17    pub fn new(id: impl Into<String>) -> Self {
18        Self(id.into())
19    }
20
21    /// Borrow the identifier as a string slice.
22    #[must_use]
23    pub fn as_str(&self) -> &str {
24        &self.0
25    }
26
27    /// Consume the identifier, returning the owned string.
28    #[must_use]
29    pub fn into_string(self) -> String {
30        self.0
31    }
32}
33
34impl From<&str> for ChoiceId {
35    fn from(value: &str) -> Self {
36        Self(value.to_string())
37    }
38}
39
40impl From<String> for ChoiceId {
41    fn from(value: String) -> Self {
42        Self(value)
43    }
44}
45
46impl fmt::Display for ChoiceId {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(&self.0)
49    }
50}
51
52/// A single selectable option: pure data carrying an id, a human label, an
53/// optional annotation line, and whether it is the recommended default.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct Choice {
56    id: ChoiceId,
57    label: String,
58    annotation: Option<String>,
59    recommended: bool,
60}
61
62impl Choice {
63    /// Create a choice from an id and human-readable label.
64    #[must_use]
65    pub fn new(id: impl Into<ChoiceId>, label: impl Into<String>) -> Self {
66        Self {
67            id: id.into(),
68            label: label.into(),
69            annotation: None,
70            recommended: false,
71        }
72    }
73
74    /// Attach a secondary annotation line (for example `detected in dev-deps`).
75    #[must_use]
76    pub fn with_annotation(mut self, annotation: impl Into<String>) -> Self {
77        self.annotation = Some(annotation.into());
78        self
79    }
80
81    /// Mark this choice as the recommended default.
82    ///
83    /// In [`PromptMode::NonInteractive`](crate::prompt::PromptMode::NonInteractive)
84    /// a `select` resolves to the recommended choice, and an interactive prompt
85    /// offers it when the answer is left blank.
86    #[must_use]
87    pub const fn recommended(mut self) -> Self {
88        self.recommended = true;
89        self
90    }
91
92    /// The stable identifier of this choice.
93    #[must_use]
94    pub const fn id(&self) -> &ChoiceId {
95        &self.id
96    }
97
98    /// The human-readable label.
99    #[must_use]
100    pub fn label(&self) -> &str {
101        &self.label
102    }
103
104    /// The optional annotation line, if any.
105    #[must_use]
106    pub fn annotation(&self) -> Option<&str> {
107        self.annotation.as_deref()
108    }
109
110    /// Whether this choice is the recommended default.
111    #[must_use]
112    pub const fn is_recommended(&self) -> bool {
113        self.recommended
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::{Choice, ChoiceId};
120
121    #[test]
122    fn choice_id_borrows_and_consumes_its_string() {
123        let id = ChoiceId::new("rust");
124        assert_eq!(id.as_str(), "rust");
125        assert_eq!(id.into_string(), "rust");
126    }
127
128    #[test]
129    fn choice_id_converts_from_owned_and_borrowed_strings() {
130        assert_eq!(ChoiceId::from("go").as_str(), "go");
131        assert_eq!(ChoiceId::from("node".to_string()).as_str(), "node");
132    }
133
134    #[test]
135    fn choice_id_displays_its_inner_value() {
136        assert_eq!(ChoiceId::new("rust").to_string(), "rust");
137    }
138
139    #[test]
140    fn choice_accessors_expose_metadata() {
141        let choice = Choice::new("rust", "Rust")
142            .with_annotation("detected in dev-deps")
143            .recommended();
144        assert_eq!(choice.id(), &ChoiceId::new("rust"));
145        assert_eq!(choice.label(), "Rust");
146        assert_eq!(choice.annotation(), Some("detected in dev-deps"));
147        assert!(choice.is_recommended());
148
149        let plain = Choice::new("go", "Go");
150        assert_eq!(plain.annotation(), None);
151        assert!(!plain.is_recommended());
152    }
153}