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