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):
10/// the caller maps it back to 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 optional annotation line,
53/// 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) a `select` resolves to the recommended choice,
84    /// and an interactive prompt offers it when the answer is left blank.
85    #[must_use]
86    pub const fn recommended(mut self) -> Self {
87        self.recommended = true;
88        self
89    }
90
91    /// The stable identifier of this choice.
92    #[must_use]
93    pub const fn id(&self) -> &ChoiceId {
94        &self.id
95    }
96
97    /// The human-readable label.
98    #[must_use]
99    pub fn label(&self) -> &str {
100        &self.label
101    }
102
103    /// The optional annotation line, if any.
104    #[must_use]
105    pub fn annotation(&self) -> Option<&str> {
106        self.annotation.as_deref()
107    }
108
109    /// Whether this choice is the recommended default.
110    #[must_use]
111    pub const fn is_recommended(&self) -> bool {
112        self.recommended
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::{Choice, ChoiceId};
119
120    #[test]
121    fn choice_id_borrows_and_consumes_its_string() {
122        let id = ChoiceId::new("rust");
123        assert_eq!(id.as_str(), "rust");
124        assert_eq!(id.into_string(), "rust");
125    }
126
127    #[test]
128    fn choice_id_converts_from_owned_and_borrowed_strings() {
129        assert_eq!(ChoiceId::from("go").as_str(), "go");
130        assert_eq!(ChoiceId::from("node".to_string()).as_str(), "node");
131    }
132
133    #[test]
134    fn choice_id_displays_its_inner_value() {
135        assert_eq!(ChoiceId::new("rust").to_string(), "rust");
136    }
137
138    #[test]
139    fn choice_accessors_expose_metadata() {
140        let choice = Choice::new("rust", "Rust")
141            .with_annotation("detected in dev-deps")
142            .recommended();
143        assert_eq!(choice.id(), &ChoiceId::new("rust"));
144        assert_eq!(choice.label(), "Rust");
145        assert_eq!(choice.annotation(), Some("detected in dev-deps"));
146        assert!(choice.is_recommended());
147
148        let plain = Choice::new("go", "Go");
149        assert_eq!(plain.annotation(), None);
150        assert!(!plain.is_recommended());
151    }
152}