Mercurial > crates > nonstick
comparison 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 | 80c07e5ab22f |
comparison
equal
deleted
inserted
replaced
| 95:51c9d7e8261a | 96:f3e260f9ddcb |
|---|---|
| 234 /// | 234 /// |
| 235 /// The returned Vec of messages always contains exactly as many entries | 235 /// The returned Vec of messages always contains exactly as many entries |
| 236 /// as there were messages in the request; one corresponding to each. | 236 /// as there were messages in the request; one corresponding to each. |
| 237 /// | 237 /// |
| 238 /// TODO: write detailed documentation about how to use this. | 238 /// TODO: write detailed documentation about how to use this. |
| 239 fn communicate(&mut self, messages: &[Message]); | 239 fn communicate(&self, messages: &[Message]); |
| 240 } | 240 } |
| 241 | 241 |
| 242 /// Turns a simple function into a [`Conversation`]. | 242 /// Turns a simple function into a [`Conversation`]. |
| 243 /// | 243 /// |
| 244 /// This can be used to wrap a free-floating function for use as a | 244 /// This can be used to wrap a free-floating function for use as a |
| 260 /// | 260 /// |
| 261 /// fn main() { | 261 /// fn main() { |
| 262 /// some_library::get_auth_data(&mut conversation_func(my_terminal_prompt)); | 262 /// some_library::get_auth_data(&mut conversation_func(my_terminal_prompt)); |
| 263 /// } | 263 /// } |
| 264 /// ``` | 264 /// ``` |
| 265 pub fn conversation_func(func: impl FnMut(&[Message])) -> impl Conversation { | 265 pub fn conversation_func(func: impl Fn(&[Message])) -> impl Conversation { |
| 266 Convo(func) | 266 FunctionConvo(func) |
| 267 } | 267 } |
| 268 | 268 |
| 269 struct Convo<C: FnMut(&[Message])>(C); | 269 struct FunctionConvo<C: Fn(&[Message])>(C); |
| 270 | 270 |
| 271 impl<C: FnMut(&[Message])> Conversation for Convo<C> { | 271 impl<C: Fn(&[Message])> Conversation for FunctionConvo<C> { |
| 272 fn communicate(&mut self, messages: &[Message]) { | 272 fn communicate(&self, messages: &[Message]) { |
| 273 self.0(messages) | 273 self.0(messages) |
| 274 } | 274 } |
| 275 } | |
| 276 | |
| 277 /// A Conversation | |
| 278 struct UsernamePasswordConvo { | |
| 279 username: String, | |
| 280 password: String, | |
| 275 } | 281 } |
| 276 | 282 |
| 277 /// A conversation trait for asking or answering one question at a time. | 283 /// A conversation trait for asking or answering one question at a time. |
| 278 /// | 284 /// |
| 279 /// An implementation of this is provided for any [`Conversation`], | 285 /// An implementation of this is provided for any [`Conversation`], |
| 280 /// or a PAM application can implement this trait and handle messages | 286 /// or a PAM application can implement this trait and handle messages |
| 281 /// one at a time. | 287 /// one at a time. |
| 282 /// | 288 /// |
| 283 /// For example, to use a `Conversation` as a `SimpleConversation`: | 289 /// For example, to use a `Conversation` as a `ConversationAdapter`: |
| 284 /// | 290 /// |
| 285 /// ``` | 291 /// ``` |
| 286 /// # use nonstick::{Conversation, Result}; | 292 /// # use nonstick::{Conversation, Result}; |
| 287 /// // Bring this trait into scope to get `masked_prompt`, among others. | 293 /// // Bring this trait into scope to get `masked_prompt`, among others. |
| 288 /// use nonstick::SimpleConversation; | 294 /// use nonstick::ConversationAdapter; |
| 289 /// | 295 /// |
| 290 /// fn ask_for_token(convo: &mut impl Conversation) -> Result<String> { | 296 /// fn ask_for_token(convo: &impl Conversation) -> Result<String> { |
| 291 /// convo.masked_prompt("enter your one-time token") | 297 /// convo.masked_prompt("enter your one-time token") |
| 292 /// } | 298 /// } |
| 293 /// ``` | 299 /// ``` |
| 294 /// | 300 /// |
| 295 /// or to use a `SimpleConversation` as a `Conversation`: | 301 /// or to use a `ConversationAdapter` as a `Conversation`: |
| 296 /// | 302 /// |
| 297 /// ``` | 303 /// ``` |
| 298 /// use nonstick::{Conversation, SimpleConversation}; | 304 /// use nonstick::{Conversation, ConversationAdapter}; |
| 299 /// # use nonstick::{BinaryData, Result}; | 305 /// # use nonstick::{BinaryData, Result}; |
| 300 /// mod some_library { | 306 /// mod some_library { |
| 301 /// # use nonstick::Conversation; | 307 /// # use nonstick::Conversation; |
| 302 /// pub fn get_auth_data(conv: &mut impl Conversation) { /* ... */ | 308 /// pub fn get_auth_data(conv: &impl Conversation) { /* ... */ |
| 303 /// } | 309 /// } |
| 304 /// } | 310 /// } |
| 305 /// | 311 /// |
| 306 /// struct MySimpleConvo {/* ... */} | 312 /// struct MySimpleConvo {/* ... */} |
| 307 /// # impl MySimpleConvo { fn new() -> Self { Self{} } } | 313 /// # impl MySimpleConvo { fn new() -> Self { Self{} } } |
| 308 /// | 314 /// |
| 309 /// impl SimpleConversation for MySimpleConvo { | 315 /// impl ConversationAdapter for MySimpleConvo { |
| 310 /// // ... | 316 /// // ... |
| 311 /// # fn prompt(&mut self, request: &str) -> Result<String> { | 317 /// # fn prompt(&self, request: &str) -> Result<String> { |
| 312 /// # unimplemented!() | 318 /// # unimplemented!() |
| 313 /// # } | 319 /// # } |
| 314 /// # | 320 /// # |
| 315 /// # fn masked_prompt(&mut self, request: &str) -> Result<String> { | 321 /// # fn masked_prompt(&self, request: &str) -> Result<String> { |
| 316 /// # unimplemented!() | 322 /// # unimplemented!() |
| 317 /// # } | 323 /// # } |
| 318 /// # | 324 /// # |
| 319 /// # fn error_msg(&mut self, message: &str) { | 325 /// # fn error_msg(&self, message: &str) { |
| 320 /// # unimplemented!() | 326 /// # unimplemented!() |
| 321 /// # } | 327 /// # } |
| 322 /// # | 328 /// # |
| 323 /// # fn info_msg(&mut self, message: &str) { | 329 /// # fn info_msg(&self, message: &str) { |
| 324 /// # unimplemented!() | 330 /// # unimplemented!() |
| 325 /// # } | 331 /// # } |
| 326 /// # | 332 /// # |
| 327 /// # fn radio_prompt(&mut self, request: &str) -> Result<String> { | 333 /// # fn radio_prompt(&self, request: &str) -> Result<String> { |
| 328 /// # unimplemented!() | 334 /// # unimplemented!() |
| 329 /// # } | 335 /// # } |
| 330 /// # | 336 /// # |
| 331 /// # fn binary_prompt(&mut self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> { | 337 /// # fn binary_prompt(&self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> { |
| 332 /// # unimplemented!() | 338 /// # unimplemented!() |
| 333 /// # } | 339 /// # } |
| 334 /// } | 340 /// } |
| 335 /// | 341 /// |
| 336 /// fn main() { | 342 /// fn main() { |
| 337 /// let mut simple = MySimpleConvo::new(); | 343 /// let mut simple = MySimpleConvo::new(); |
| 338 /// some_library::get_auth_data(&mut simple.as_conversation()) | 344 /// some_library::get_auth_data(&mut simple.into_conversation()) |
| 339 /// } | 345 /// } |
| 340 /// ``` | 346 /// ``` |
| 341 pub trait SimpleConversation { | 347 pub trait ConversationAdapter { |
| 342 /// Lets you use this simple conversation as a full [Conversation]. | 348 /// Lets you use this simple conversation as a full [Conversation]. |
| 343 /// | 349 /// |
| 344 /// The wrapper takes each message received in [`Conversation::communicate`] | 350 /// The wrapper takes each message received in [`Conversation::communicate`] |
| 345 /// and passes them one-by-one to the appropriate method, | 351 /// and passes them one-by-one to the appropriate method, |
| 346 /// then collects responses to return. | 352 /// then collects responses to return. |
| 347 fn as_conversation(&mut self) -> Demux<'_, Self> | 353 fn into_conversation(self) -> Demux<Self> |
| 348 where | 354 where |
| 349 Self: Sized, | 355 Self: Sized, |
| 350 { | 356 { |
| 351 Demux(self) | 357 Demux(self) |
| 352 } | 358 } |
| 353 /// Prompts the user for something. | 359 /// Prompts the user for something. |
| 354 fn prompt(&mut self, request: &str) -> Result<String>; | 360 fn prompt(&self, request: &str) -> Result<String>; |
| 355 /// Prompts the user for something, but hides what the user types. | 361 /// Prompts the user for something, but hides what the user types. |
| 356 fn masked_prompt(&mut self, request: &str) -> Result<String>; | 362 fn masked_prompt(&self, request: &str) -> Result<String>; |
| 357 /// Alerts the user to an error. | 363 /// Alerts the user to an error. |
| 358 fn error_msg(&mut self, message: &str); | 364 fn error_msg(&self, message: &str); |
| 359 /// Sends an informational message to the user. | 365 /// Sends an informational message to the user. |
| 360 fn info_msg(&mut self, message: &str); | 366 fn info_msg(&self, message: &str); |
| 361 /// \[Linux extension] Prompts the user for a yes/no/maybe conditional. | 367 /// \[Linux extension] Prompts the user for a yes/no/maybe conditional. |
| 362 /// | 368 /// |
| 363 /// PAM documentation doesn't define the format of the response. | 369 /// PAM documentation doesn't define the format of the response. |
| 364 /// | 370 /// |
| 365 /// When called on an implementation that doesn't support radio prompts, | 371 /// When called on an implementation that doesn't support radio prompts, |
| 366 /// this will return [`ErrorCode::ConversationError`]. | 372 /// this will return [`ErrorCode::ConversationError`]. |
| 367 /// If implemented on an implementation that doesn't support radio prompts, | 373 /// If implemented on an implementation that doesn't support radio prompts, |
| 368 /// this will never be called. | 374 /// this will never be called. |
| 369 fn radio_prompt(&mut self, request: &str) -> Result<String> { | 375 fn radio_prompt(&self, request: &str) -> Result<String> { |
| 370 let _ = request; | 376 let _ = request; |
| 371 Err(ErrorCode::ConversationError) | 377 Err(ErrorCode::ConversationError) |
| 372 } | 378 } |
| 373 /// \[Linux extension] Requests binary data from the user. | 379 /// \[Linux extension] Requests binary data from the user. |
| 374 /// | 380 /// |
| 375 /// When called on an implementation that doesn't support radio prompts, | 381 /// When called on an implementation that doesn't support radio prompts, |
| 376 /// this will return [`ErrorCode::ConversationError`]. | 382 /// this will return [`ErrorCode::ConversationError`]. |
| 377 /// If implemented on an implementation that doesn't support radio prompts, | 383 /// If implemented on an implementation that doesn't support radio prompts, |
| 378 /// this will never be called. | 384 /// this will never be called. |
| 379 fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { | 385 fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { |
| 380 let _ = data_and_type; | 386 let _ = data_and_type; |
| 381 Err(ErrorCode::ConversationError) | 387 Err(ErrorCode::ConversationError) |
| 388 } | |
| 389 } | |
| 390 | |
| 391 impl<CA: ConversationAdapter> From<CA> for Demux<CA> { | |
| 392 fn from(value: CA) -> Self { | |
| 393 Demux(value) | |
| 382 } | 394 } |
| 383 } | 395 } |
| 384 | 396 |
| 385 macro_rules! conv_fn { | 397 macro_rules! conv_fn { |
| 386 ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { | 398 ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { |
| 387 $(#[$m])* | 399 $(#[$m])* |
| 388 fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> { | 400 fn $fn_name(&self, $($param: $pt),*) -> Result<$resp_type> { |
| 389 let prompt = <$msg>::new($($param),*); | 401 let prompt = <$msg>::new($($param),*); |
| 390 self.communicate(&[prompt.message()]); | 402 self.communicate(&[prompt.message()]); |
| 391 prompt.answer() | 403 prompt.answer() |
| 392 } | 404 } |
| 393 }; | 405 }; |
| 394 ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => { | 406 ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => { |
| 395 $(#[$m])* | 407 $(#[$m])* |
| 396 fn $fn_name(&mut self, $($param: $pt),*) { | 408 fn $fn_name(&self, $($param: $pt),*) { |
| 397 self.communicate(&[<$msg>::new($($param),*).message()]); | 409 self.communicate(&[<$msg>::new($($param),*).message()]); |
| 398 } | 410 } |
| 399 }; | 411 }; |
| 400 } | 412 } |
| 401 | 413 |
| 402 impl<C: Conversation> SimpleConversation for C { | 414 impl<C: Conversation> ConversationAdapter for C { |
| 403 conv_fn!(prompt(message: &str) -> String { QAndA }); | 415 conv_fn!(prompt(message: &str) -> String { QAndA }); |
| 404 conv_fn!(masked_prompt(message: &str) -> String { MaskedQAndA } ); | 416 conv_fn!(masked_prompt(message: &str) -> String { MaskedQAndA } ); |
| 405 conv_fn!(error_msg(message: &str) { ErrorMsg }); | 417 conv_fn!(error_msg(message: &str) { ErrorMsg }); |
| 406 conv_fn!(info_msg(message: &str) { InfoMsg }); | 418 conv_fn!(info_msg(message: &str) { InfoMsg }); |
| 407 conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA }); | 419 conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA }); |
| 408 conv_fn!(binary_prompt((data, data_type): (&[u8], u8)) -> BinaryData { BinaryQAndA }); | 420 conv_fn!(binary_prompt((data, data_type): (&[u8], u8)) -> BinaryData { BinaryQAndA }); |
| 409 } | 421 } |
| 410 | 422 |
| 411 /// A [`Conversation`] which asks the questions one at a time. | 423 /// A [`Conversation`] which asks the questions one at a time. |
| 412 /// | 424 /// |
| 413 /// This is automatically created by [`SimpleConversation::as_conversation`]. | 425 /// This is automatically created by [`ConversationAdapter::into_conversation`]. |
| 414 pub struct Demux<'a, SC: SimpleConversation>(&'a mut SC); | 426 pub struct Demux<CA: ConversationAdapter>(CA); |
| 415 | 427 |
| 416 impl<SC: SimpleConversation> Conversation for Demux<'_, SC> { | 428 impl<CA: ConversationAdapter> Demux<CA> { |
| 417 fn communicate(&mut self, messages: &[Message]) { | 429 /// Gets the original Conversation out of this wrapper. |
| 430 fn into_inner(self) -> CA { | |
| 431 self.0 | |
| 432 } | |
| 433 } | |
| 434 | |
| 435 impl<CA: ConversationAdapter> Conversation for Demux<CA> { | |
| 436 fn communicate(&self, messages: &[Message]) { | |
| 418 for msg in messages { | 437 for msg in messages { |
| 419 match msg { | 438 match msg { |
| 420 Message::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())), | 439 Message::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())), |
| 421 Message::MaskedPrompt(prompt) => { | 440 Message::MaskedPrompt(prompt) => { |
| 422 prompt.set_answer(self.0.masked_prompt(prompt.question())) | 441 prompt.set_answer(self.0.masked_prompt(prompt.question())) |
| 448 | 467 |
| 449 #[test] | 468 #[test] |
| 450 fn test_demux() { | 469 fn test_demux() { |
| 451 #[derive(Default)] | 470 #[derive(Default)] |
| 452 struct DemuxTester { | 471 struct DemuxTester { |
| 453 error_ran: bool, | 472 error_ran: Cell<bool>, |
| 454 info_ran: bool, | 473 info_ran: Cell<bool>, |
| 455 } | 474 } |
| 456 | 475 |
| 457 impl SimpleConversation for DemuxTester { | 476 impl ConversationAdapter for DemuxTester { |
| 458 fn prompt(&mut self, request: &str) -> Result<String> { | 477 fn prompt(&self, request: &str) -> Result<String> { |
| 459 match request { | 478 match request { |
| 460 "what" => Ok("whatwhat".to_owned()), | 479 "what" => Ok("whatwhat".to_owned()), |
| 461 "give_err" => Err(ErrorCode::PermissionDenied), | 480 "give_err" => Err(ErrorCode::PermissionDenied), |
| 462 _ => panic!("unexpected prompt!"), | 481 _ => panic!("unexpected prompt!"), |
| 463 } | 482 } |
| 464 } | 483 } |
| 465 fn masked_prompt(&mut self, request: &str) -> Result<String> { | 484 fn masked_prompt(&self, request: &str) -> Result<String> { |
| 466 assert_eq!("reveal", request); | 485 assert_eq!("reveal", request); |
| 467 Ok("my secrets".to_owned()) | 486 Ok("my secrets".to_owned()) |
| 468 } | 487 } |
| 469 fn error_msg(&mut self, message: &str) { | 488 fn error_msg(&self, message: &str) { |
| 470 self.error_ran = true; | 489 self.error_ran.set(true); |
| 471 assert_eq!("whoopsie", message); | 490 assert_eq!("whoopsie", message); |
| 472 } | 491 } |
| 473 fn info_msg(&mut self, message: &str) { | 492 fn info_msg(&self, message: &str) { |
| 474 self.info_ran = true; | 493 self.info_ran.set(true); |
| 475 assert_eq!("did you know", message); | 494 assert_eq!("did you know", message); |
| 476 } | 495 } |
| 477 fn radio_prompt(&mut self, request: &str) -> Result<String> { | 496 fn radio_prompt(&self, request: &str) -> Result<String> { |
| 478 assert_eq!("channel?", request); | 497 assert_eq!("channel?", request); |
| 479 Ok("zero".to_owned()) | 498 Ok("zero".to_owned()) |
| 480 } | 499 } |
| 481 fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { | 500 fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { |
| 482 assert_eq!((&[10, 9, 8][..], 66), data_and_type); | 501 assert_eq!((&[10, 9, 8][..], 66), data_and_type); |
| 483 Ok(BinaryData::new(vec![5, 5, 5], 5)) | 502 Ok(BinaryData::new(vec![5, 5, 5], 5)) |
| 484 } | 503 } |
| 485 } | 504 } |
| 486 | 505 |
| 487 let mut tester = DemuxTester::default(); | 506 let tester = DemuxTester::default(); |
| 488 | 507 |
| 489 let what = QAndA::new("what"); | 508 let what = QAndA::new("what"); |
| 490 let pass = MaskedQAndA::new("reveal"); | 509 let pass = MaskedQAndA::new("reveal"); |
| 491 let err = ErrorMsg::new("whoopsie"); | 510 let err = ErrorMsg::new("whoopsie"); |
| 492 let info = InfoMsg::new("did you know"); | 511 let info = InfoMsg::new("did you know"); |
| 493 let has_err = QAndA::new("give_err"); | 512 let has_err = QAndA::new("give_err"); |
| 494 | 513 |
| 495 let mut conv = tester.as_conversation(); | 514 let conv = tester.into_conversation(); |
| 496 | 515 |
| 497 // Basic tests. | 516 // Basic tests. |
| 498 | 517 |
| 499 conv.communicate(&[ | 518 conv.communicate(&[ |
| 500 what.message(), | 519 what.message(), |
| 507 assert_eq!("whatwhat", what.answer().unwrap()); | 526 assert_eq!("whatwhat", what.answer().unwrap()); |
| 508 assert_eq!("my secrets", pass.answer().unwrap()); | 527 assert_eq!("my secrets", pass.answer().unwrap()); |
| 509 assert_eq!(Ok(()), err.answer()); | 528 assert_eq!(Ok(()), err.answer()); |
| 510 assert_eq!(Ok(()), info.answer()); | 529 assert_eq!(Ok(()), info.answer()); |
| 511 assert_eq!(ErrorCode::PermissionDenied, has_err.answer().unwrap_err()); | 530 assert_eq!(ErrorCode::PermissionDenied, has_err.answer().unwrap_err()); |
| 512 assert!(tester.error_ran); | 531 let tester = conv.into_inner(); |
| 513 assert!(tester.info_ran); | 532 assert!(tester.error_ran.get()); |
| 533 assert!(tester.info_ran.get()); | |
| 514 | 534 |
| 515 // Test the Linux extensions separately. | 535 // Test the Linux extensions separately. |
| 516 { | 536 { |
| 517 let mut conv = tester.as_conversation(); | 537 let conv = tester.into_conversation(); |
| 518 | 538 |
| 519 let radio = RadioQAndA::new("channel?"); | 539 let radio = RadioQAndA::new("channel?"); |
| 520 let bin = BinaryQAndA::new((&[10, 9, 8], 66)); | 540 let bin = BinaryQAndA::new((&[10, 9, 8], 66)); |
| 521 conv.communicate(&[radio.message(), bin.message()]); | 541 conv.communicate(&[radio.message(), bin.message()]); |
| 522 | 542 |
| 527 | 547 |
| 528 fn test_mux() { | 548 fn test_mux() { |
| 529 struct MuxTester; | 549 struct MuxTester; |
| 530 | 550 |
| 531 impl Conversation for MuxTester { | 551 impl Conversation for MuxTester { |
| 532 fn communicate(&mut self, messages: &[Message]) { | 552 fn communicate(&self, messages: &[Message]) { |
| 533 if let [msg] = messages { | 553 if let [msg] = messages { |
| 534 match *msg { | 554 match *msg { |
| 535 Message::Info(info) => { | 555 Message::Info(info) => { |
| 536 assert_eq!("let me tell you", info.question()); | 556 assert_eq!("let me tell you", info.question()); |
| 537 info.set_answer(Ok(())) | 557 info.set_answer(Ok(())) |
| 565 ) | 585 ) |
| 566 } | 586 } |
| 567 } | 587 } |
| 568 } | 588 } |
| 569 | 589 |
| 570 let mut tester = MuxTester; | 590 let tester = MuxTester; |
| 571 | 591 |
| 572 assert_eq!("answer", tester.prompt("question").unwrap()); | 592 assert_eq!("answer", tester.prompt("question").unwrap()); |
| 573 assert_eq!("open sesame", tester.masked_prompt("password!").unwrap()); | 593 assert_eq!("open sesame", tester.masked_prompt("password!").unwrap()); |
| 574 tester.error_msg("oh no"); | 594 tester.error_msg("oh no"); |
| 575 tester.info_msg("let me tell you"); | 595 tester.info_msg("let me tell you"); |
