rxrust 1.0.0-rc.4

A Rust implementation of Reactive Extensions.
Documentation
//! FromFn operator implementation
//!
//! This module contains the FromFn operator, which creates an observable that
//! emits a single value generated by a function at subscription time.

use std::convert::Infallible;

use crate::{
  context::Context,
  observable::{CoreObservable, ObservableType},
  observer::Observer,
};

/// FromFn operator: Creates an observable that emits a single value generated
/// by a function
///
/// This operator is useful when you need to generate a value at subscription
/// time, such as when the value depends on the current state of the system.
///
/// # Examples
///
/// ```
/// use rxrust::prelude::*;
///
/// let observable = Local::from_fn(|| 42);
/// let mut result = None;
/// observable.subscribe(|v| {
///   result = Some(v);
/// });
/// assert_eq!(result, Some(42));
/// ```
#[derive(Clone)]
pub struct FromFn<F>(pub F);

impl<F, T> ObservableType for FromFn<F>
where
  F: FnOnce() -> T,
{
  type Item<'a>
    = T
  where
    Self: 'a;
  type Err = Infallible;
}

impl<C, F, T> CoreObservable<C> for FromFn<F>
where
  C: Context,
  C::Inner: Observer<T, Infallible>,
  F: FnOnce() -> T,
{
  type Unsub = ();

  fn subscribe(self, context: C) -> Self::Unsub {
    // Execute the function to get the value
    let value = (self.0)();

    // Get the observer and emit the value, then complete
    let mut observer = context.into_inner();
    observer.next(value);
    observer.complete();
  }
}

#[cfg(test)]
mod tests {
  use std::{cell::RefCell, rc::Rc};

  use crate::prelude::*;

  #[rxrust_macro::test(local)]
  async fn test_from_fn_emits_value() {
    let result = Rc::new(RefCell::new(None));
    let result_clone = result.clone();

    Local::from_fn(|| 42).subscribe(move |v| {
      *result_clone.borrow_mut() = Some(v);
    });
    assert_eq!(*result.borrow(), Some(42));
  }

  #[rxrust_macro::test(local)]
  async fn test_from_fn_with_different_types() {
    let string_result = Rc::new(RefCell::new(None));
    let string_result_clone = string_result.clone();

    Local::from_fn(|| "hello".to_string()).subscribe(move |v| {
      *string_result_clone.borrow_mut() = Some(v);
    });
    assert_eq!(*string_result.borrow(), Some("hello".to_string()));

    let bool_result = Rc::new(RefCell::new(None));
    let bool_result_clone = bool_result.clone();

    Local::from_fn(|| true).subscribe(move |v| {
      *bool_result_clone.borrow_mut() = Some(v);
    });
    assert_eq!(*bool_result.borrow(), Some(true));
  }

  #[rxrust_macro::test(local)]
  async fn test_from_fn_executes_at_subscription_time() {
    let counter = Rc::new(RefCell::new(0));
    let counter_clone = counter.clone();

    // The function should execute when subscribe is called, not when from_fn is
    // called
    let observable = Local::from_fn(move || {
      *counter_clone.borrow_mut() += 1;
      *counter_clone.borrow()
    });

    // Counter should still be 0 before subscription
    assert_eq!(*counter.borrow(), 0);

    let result = Rc::new(RefCell::new(None));
    let result_clone = result.clone();

    observable.clone().subscribe(move |v| {
      *result_clone.borrow_mut() = Some(v);
    });

    // Counter should be 1 after subscription (function executed once)
    assert_eq!(*counter.borrow(), 1);
    assert_eq!(*result.borrow(), Some(1));

    // Subscribe again to verify function executes again
    let result2 = Rc::new(RefCell::new(None));
    let result2_clone = result2.clone();

    observable.clone().subscribe(move |v| {
      *result2_clone.borrow_mut() = Some(v);
    });

    // Counter should be 2 after second subscription
    assert_eq!(*counter.borrow(), 2);
    assert_eq!(*result2.borrow(), Some(2));
  }

  #[rxrust_macro::test]
  async fn test_from_fn_with_shared_context() {
    let result = std::sync::Arc::new(std::sync::Mutex::new(None));
    let result_clone = result.clone();

    Shared::from_fn(|| 123).subscribe(move |v| {
      *result_clone.lock().unwrap() = Some(v);
    });
    assert_eq!(*result.lock().unwrap(), Some(123));
  }
}