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
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");