Mercurial > crates > nonstick
diff src/conv.rs @ 96:f3e260f9ddcb
Make conversation trait use immutable references.
Since sending a conversation a message doesn't really "mutate" it,
it shouldn't really be considered "mutable" for that purpose.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Mon, 23 Jun 2025 14:26:34 -0400 |
parents | db167f96ba46 |
children |
line wrap: on
line diff
--- a/src/conv.rs Mon Jun 23 14:03:44 2025 -0400 +++ b/src/conv.rs Mon Jun 23 14:26:34 2025 -0400 @@ -236,7 +236,7 @@ /// as there were messages in the request; one corresponding to each. /// /// TODO: write detailed documentation about how to use this. - fn communicate(&mut self, messages: &[Message]); + fn communicate(&self, messages: &[Message]); } /// Turns a simple function into a [`Conversation`]. @@ -262,102 +262,108 @@ /// some_library::get_auth_data(&mut conversation_func(my_terminal_prompt)); /// } /// ``` -pub fn conversation_func(func: impl FnMut(&[Message])) -> impl Conversation { - Convo(func) +pub fn conversation_func(func: impl Fn(&[Message])) -> impl Conversation { + FunctionConvo(func) } -struct Convo<C: FnMut(&[Message])>(C); +struct FunctionConvo<C: Fn(&[Message])>(C); -impl<C: FnMut(&[Message])> Conversation for Convo<C> { - fn communicate(&mut self, messages: &[Message]) { +impl<C: Fn(&[Message])> Conversation for FunctionConvo<C> { + fn communicate(&self, messages: &[Message]) { self.0(messages) } } +/// A Conversation +struct UsernamePasswordConvo { + username: String, + password: String, +} + /// A conversation trait for asking or answering one question at a time. /// /// An implementation of this is provided for any [`Conversation`], /// or a PAM application can implement this trait and handle messages /// one at a time. /// -/// For example, to use a `Conversation` as a `SimpleConversation`: +/// For example, to use a `Conversation` as a `ConversationAdapter`: /// /// ``` /// # use nonstick::{Conversation, Result}; /// // Bring this trait into scope to get `masked_prompt`, among others. -/// use nonstick::SimpleConversation; +/// use nonstick::ConversationAdapter; /// -/// fn ask_for_token(convo: &mut impl Conversation) -> Result<String> { +/// fn ask_for_token(convo: &impl Conversation) -> Result<String> { /// convo.masked_prompt("enter your one-time token") /// } /// ``` /// -/// or to use a `SimpleConversation` as a `Conversation`: +/// or to use a `ConversationAdapter` as a `Conversation`: /// /// ``` -/// use nonstick::{Conversation, SimpleConversation}; +/// use nonstick::{Conversation, ConversationAdapter}; /// # use nonstick::{BinaryData, Result}; /// mod some_library { /// # use nonstick::Conversation; -/// pub fn get_auth_data(conv: &mut impl Conversation) { /* ... */ +/// pub fn get_auth_data(conv: &impl Conversation) { /* ... */ /// } /// } /// /// struct MySimpleConvo {/* ... */} /// # impl MySimpleConvo { fn new() -> Self { Self{} } } /// -/// impl SimpleConversation for MySimpleConvo { +/// impl ConversationAdapter for MySimpleConvo { /// // ... -/// # fn prompt(&mut self, request: &str) -> Result<String> { +/// # fn prompt(&self, request: &str) -> Result<String> { /// # unimplemented!() /// # } /// # -/// # fn masked_prompt(&mut self, request: &str) -> Result<String> { +/// # fn masked_prompt(&self, request: &str) -> Result<String> { /// # unimplemented!() /// # } /// # -/// # fn error_msg(&mut self, message: &str) { +/// # fn error_msg(&self, message: &str) { /// # unimplemented!() /// # } /// # -/// # fn info_msg(&mut self, message: &str) { +/// # fn info_msg(&self, message: &str) { /// # unimplemented!() /// # } /// # -/// # fn radio_prompt(&mut self, request: &str) -> Result<String> { +/// # fn radio_prompt(&self, request: &str) -> Result<String> { /// # unimplemented!() /// # } /// # -/// # fn binary_prompt(&mut self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> { +/// # fn binary_prompt(&self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> { /// # unimplemented!() /// # } /// } /// /// fn main() { /// let mut simple = MySimpleConvo::new(); -/// some_library::get_auth_data(&mut simple.as_conversation()) +/// some_library::get_auth_data(&mut simple.into_conversation()) /// } /// ``` -pub trait SimpleConversation { +pub trait ConversationAdapter { /// Lets you use this simple conversation as a full [Conversation]. /// /// The wrapper takes each message received in [`Conversation::communicate`] /// and passes them one-by-one to the appropriate method, /// then collects responses to return. - fn as_conversation(&mut self) -> Demux<'_, Self> + fn into_conversation(self) -> Demux<Self> where Self: Sized, { Demux(self) } /// Prompts the user for something. - fn prompt(&mut self, request: &str) -> Result<String>; + fn prompt(&self, request: &str) -> Result<String>; /// Prompts the user for something, but hides what the user types. - fn masked_prompt(&mut self, request: &str) -> Result<String>; + fn masked_prompt(&self, request: &str) -> Result<String>; /// Alerts the user to an error. - fn error_msg(&mut self, message: &str); + fn error_msg(&self, message: &str); /// Sends an informational message to the user. - fn info_msg(&mut self, message: &str); + fn info_msg(&self, message: &str); /// \[Linux extension] Prompts the user for a yes/no/maybe conditional. /// /// PAM documentation doesn't define the format of the response. @@ -366,7 +372,7 @@ /// this will return [`ErrorCode::ConversationError`]. /// If implemented on an implementation that doesn't support radio prompts, /// this will never be called. - fn radio_prompt(&mut self, request: &str) -> Result<String> { + fn radio_prompt(&self, request: &str) -> Result<String> { let _ = request; Err(ErrorCode::ConversationError) } @@ -376,16 +382,22 @@ /// this will return [`ErrorCode::ConversationError`]. /// If implemented on an implementation that doesn't support radio prompts, /// this will never be called. - fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { + fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { let _ = data_and_type; Err(ErrorCode::ConversationError) } } +impl<CA: ConversationAdapter> From<CA> for Demux<CA> { + fn from(value: CA) -> Self { + Demux(value) + } +} + macro_rules! conv_fn { ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { $(#[$m])* - fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> { + fn $fn_name(&self, $($param: $pt),*) -> Result<$resp_type> { let prompt = <$msg>::new($($param),*); self.communicate(&[prompt.message()]); prompt.answer() @@ -393,13 +405,13 @@ }; ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => { $(#[$m])* - fn $fn_name(&mut self, $($param: $pt),*) { + fn $fn_name(&self, $($param: $pt),*) { self.communicate(&[<$msg>::new($($param),*).message()]); } }; } -impl<C: Conversation> SimpleConversation for C { +impl<C: Conversation> ConversationAdapter for C { conv_fn!(prompt(message: &str) -> String { QAndA }); conv_fn!(masked_prompt(message: &str) -> String { MaskedQAndA } ); conv_fn!(error_msg(message: &str) { ErrorMsg }); @@ -410,11 +422,18 @@ /// A [`Conversation`] which asks the questions one at a time. /// -/// This is automatically created by [`SimpleConversation::as_conversation`]. -pub struct Demux<'a, SC: SimpleConversation>(&'a mut SC); +/// This is automatically created by [`ConversationAdapter::into_conversation`]. +pub struct Demux<CA: ConversationAdapter>(CA); -impl<SC: SimpleConversation> Conversation for Demux<'_, SC> { - fn communicate(&mut self, messages: &[Message]) { +impl<CA: ConversationAdapter> Demux<CA> { + /// Gets the original Conversation out of this wrapper. + fn into_inner(self) -> CA { + self.0 + } +} + +impl<CA: ConversationAdapter> Conversation for Demux<CA> { + fn communicate(&self, messages: &[Message]) { for msg in messages { match msg { Message::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())), @@ -450,41 +469,41 @@ fn test_demux() { #[derive(Default)] struct DemuxTester { - error_ran: bool, - info_ran: bool, + error_ran: Cell<bool>, + info_ran: Cell<bool>, } - impl SimpleConversation for DemuxTester { - fn prompt(&mut self, request: &str) -> Result<String> { + impl ConversationAdapter for DemuxTester { + fn prompt(&self, request: &str) -> Result<String> { match request { "what" => Ok("whatwhat".to_owned()), "give_err" => Err(ErrorCode::PermissionDenied), _ => panic!("unexpected prompt!"), } } - fn masked_prompt(&mut self, request: &str) -> Result<String> { + fn masked_prompt(&self, request: &str) -> Result<String> { assert_eq!("reveal", request); Ok("my secrets".to_owned()) } - fn error_msg(&mut self, message: &str) { - self.error_ran = true; + fn error_msg(&self, message: &str) { + self.error_ran.set(true); assert_eq!("whoopsie", message); } - fn info_msg(&mut self, message: &str) { - self.info_ran = true; + fn info_msg(&self, message: &str) { + self.info_ran.set(true); assert_eq!("did you know", message); } - fn radio_prompt(&mut self, request: &str) -> Result<String> { + fn radio_prompt(&self, request: &str) -> Result<String> { assert_eq!("channel?", request); Ok("zero".to_owned()) } - fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { + fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { assert_eq!((&[10, 9, 8][..], 66), data_and_type); Ok(BinaryData::new(vec![5, 5, 5], 5)) } } - let mut tester = DemuxTester::default(); + let tester = DemuxTester::default(); let what = QAndA::new("what"); let pass = MaskedQAndA::new("reveal"); @@ -492,7 +511,7 @@ let info = InfoMsg::new("did you know"); let has_err = QAndA::new("give_err"); - let mut conv = tester.as_conversation(); + let conv = tester.into_conversation(); // Basic tests. @@ -509,12 +528,13 @@ assert_eq!(Ok(()), err.answer()); assert_eq!(Ok(()), info.answer()); assert_eq!(ErrorCode::PermissionDenied, has_err.answer().unwrap_err()); - assert!(tester.error_ran); - assert!(tester.info_ran); + let tester = conv.into_inner(); + assert!(tester.error_ran.get()); + assert!(tester.info_ran.get()); // Test the Linux extensions separately. { - let mut conv = tester.as_conversation(); + let conv = tester.into_conversation(); let radio = RadioQAndA::new("channel?"); let bin = BinaryQAndA::new((&[10, 9, 8], 66)); @@ -529,7 +549,7 @@ struct MuxTester; impl Conversation for MuxTester { - fn communicate(&mut self, messages: &[Message]) { + fn communicate(&self, messages: &[Message]) { if let [msg] = messages { match *msg { Message::Info(info) => { @@ -567,7 +587,7 @@ } } - let mut tester = MuxTester; + let tester = MuxTester; assert_eq!("answer", tester.prompt("question").unwrap()); assert_eq!("open sesame", tester.masked_prompt("password!").unwrap());