Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions src/collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ pub trait MapExt<K, Q: ?Sized = K> {
fn replace_key(&mut self, k1: &Q, k2: K) -> Result<(), ReplaceKeyErr>
where
K: Borrow<Q>;

/// Try to replace an existing key with a new one, returning `true` if successful.
///
/// This is a convenience method that returns `true` if the replacement was successful,
/// and `false` otherwise. It's useful when you don't need to distinguish between
/// different error types.
fn try_replace_key(&mut self, k1: &Q, k2: K) -> bool
where
K: Borrow<Q>,
{
self.replace_key(k1, k2).is_ok()
}
}

impl<K, Q, V, S> MapExt<K, Q> for HashMap<K, V, S>
Expand All @@ -44,19 +56,23 @@ where
S: BuildHasher,
{
fn replace_key(&mut self, k1: &Q, k2: K) -> Result<(), ReplaceKeyErr> {
// Check if old key exists first
if !self.contains_key(k1) {
return Err(ReplaceKeyErr::OldKeyNotExist);
}

// Early return if keys are equal (after confirming old key exists)
if k1 == k2.borrow() {
return Ok(());
}

// Check if new key already exists
if self.contains_key(k2.borrow()) {
return Err(ReplaceKeyErr::NewKeyOccupied);
}

let v = self.remove(k1).expect("this should be unreachable");
// Remove old key and get value in one operation
let v = self.remove(k1).expect("key should exist");
self.insert(k2, v);
Ok(())
}
Expand All @@ -68,19 +84,23 @@ where
Q: Ord + ?Sized,
{
fn replace_key(&mut self, k1: &Q, k2: K) -> Result<(), ReplaceKeyErr> {
// Check if old key exists first
if !self.contains_key(k1) {
return Err(ReplaceKeyErr::OldKeyNotExist);
}

// Early return if keys are equal (after confirming old key exists)
if k1 == k2.borrow() {
return Ok(());
}

// Check if new key already exists
if self.contains_key(k2.borrow()) {
return Err(ReplaceKeyErr::NewKeyOccupied);
}

let v = self.remove(k1).expect("this should be unreachable");
// Remove old key and get value in one operation
let v = self.remove(k1).expect("key should exist");
self.insert(k2, v);
Ok(())
}
Expand Down Expand Up @@ -159,6 +179,24 @@ mod tests {
assert_eq!(map["k2"], 456);
}

#[test]
fn try_replace_key_hashmap() {
let mut map = HashMap::new();
map.insert("k1".to_string(), 123);
map.insert("k2".to_string(), 456);

// Test successful replacement
assert!(map.try_replace_key("k1", "k3".to_string()));
assert!(!map.contains_key("k1"));
assert_eq!(map["k3"], 123);

// Test failed replacement (old key doesn't exist)
assert!(!map.try_replace_key("k4", "k5".to_string()));

// Test failed replacement (new key already exists)
assert!(!map.try_replace_key("k3", "k2".to_string()));
}

#[test]
fn replace_key_btreemap() {
let mut map = BTreeMap::new();
Expand Down
87 changes: 87 additions & 0 deletions src/future.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ pub struct WithCancelSignal<F: Future, C: Future> {
cancel: Pin<Box<C>>,
}

/// A more efficient version of [`WithCancelSignal`] for [`Unpin`] futures that avoids boxing.
///
/// Use [`FutureExt::with_cancel_signal_unpin`] to construct.
#[derive(Debug)]
pub struct WithCancelSignalUnpin<F: Future + Unpin, C: Future + Unpin> {
future: Option<F>,
cancel: Option<C>,
}

impl<F, C> Future for WithCancelSignal<F, C>
where
F: Future,
Expand All @@ -42,6 +51,32 @@ where
}
}

impl<F, C> Future for WithCancelSignalUnpin<F, C>
where
F: Future + Unpin,
C: Future + Unpin,
{
type Output = Result<F::Output, C::Output>;

fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// Poll the main future first
if let Some(ref mut future) = self.future {
if let Poll::Ready(o) = Pin::new(future).poll(cx) {
return Poll::Ready(Ok(o));
}
}

// Poll the cancel signal
if let Some(ref mut cancel) = self.cancel {
if let Poll::Ready(o) = Pin::new(cancel).poll(cx) {
return Poll::Ready(Err(o));
}
}

Poll::Pending
}
}

/// [`Future`] extension trait.
///
/// This trait has been implemented for all [`Sized`] `Future`s.
Expand Down Expand Up @@ -73,6 +108,42 @@ pub trait FutureExt: Future + Sized {
cancel: Box::pin(cancel),
}
}

/// Construct a [`WithCancelSignalUnpin`] Future that avoids boxing for [`Unpin`] futures.
///
/// This is a more efficient version of [`with_cancel_signal`] for futures that implement
/// [`Unpin`], as it avoids the heap allocation of boxing.
///
/// [`with_cancel_signal`]: FutureExt::with_cancel_signal
///
/// # Example
///
/// ```
/// use est::future::FutureExt;
/// use std::future::ready;
///
/// #[tokio::main]
/// async fn main() {
/// let future = ready(42);
/// let cancel = ready(());
/// // Since both are ready immediately, the future should complete first
/// assert!(future.with_cancel_signal_unpin(cancel).await.is_ok());
///
/// // Test with boxed futures (which are Unpin)
/// let future = Box::pin(async { 42 });
/// let cancel = Box::pin(async { () });
/// assert!(future.with_cancel_signal_unpin(cancel).await.is_ok());
/// }
/// ```
fn with_cancel_signal_unpin<C: Future + Unpin>(self, cancel: C) -> WithCancelSignalUnpin<Self, C>
where
Self: Unpin,
{
WithCancelSignalUnpin {
future: Some(self),
cancel: Some(cancel),
}
}
}

impl<T: Future + Sized> FutureExt for T {}
Expand Down Expand Up @@ -122,6 +193,22 @@ mod tests {
assert!(future.with_cancel_signal(cancel).await.is_ok());
}

#[tokio::test]
async fn with_cancel_signal_unpin() {
use std::future::ready;

// Test cancellation with ready futures (which are Unpin)
let cancel = ready(());
let future = ready(42);
// Since both are ready immediately, the future should complete first
assert!(future.with_cancel_signal_unpin(cancel).await.is_ok());

// Test with async blocks that are pinned
let cancel = Box::pin(async { () });
let future = Box::pin(async { 42 });
assert!(future.with_cancel_signal_unpin(cancel).await.is_ok());
}

#[tokio::test]
async fn into_future_with_args() {
async fn into_signal(num: i32) -> i32 {
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ pub mod task;
pub mod thread;

#[cfg(feature = "result")]
pub use result::AnyRes;
pub use result::{AnyRes, ResultExt};
57 changes: 37 additions & 20 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,33 +170,50 @@ impl From<Command> for TokioCommand {

impl Clone for Command {
fn clone(&self) -> Self {
let kill_on_drop = self.as_tokio().map(TokioCommand::get_kill_on_drop);
let cmd = self.as_std();
let mut cloned = StdCommand::new(cmd.get_program());

cloned.args(cmd.get_args());
match self {
// Fast path for std commands - no need to check kill_on_drop
Self::Std(cmd) => {
let mut cloned = StdCommand::new(cmd.get_program());
cloned.args(cmd.get_args());

for (k, v) in cmd.get_envs() {
match v {
Some(v) => cloned.env(k, v),
None => cloned.env_remove(k),
};
}

for (k, v) in cmd.get_envs() {
match v {
Some(v) => cloned.env(k, v),
None => cloned.env_remove(k),
};
}
if let Some(current_dir) = cmd.get_current_dir() {
cloned.current_dir(current_dir);
}

if let Some(current_dir) = cmd.get_current_dir() {
cloned.current_dir(current_dir);
}
Self::Std(cloned)
}
// Handle tokio commands with kill_on_drop preservation
Self::Tokio(tokio_cmd) => {
let kill_on_drop = tokio_cmd.get_kill_on_drop();
let cmd = tokio_cmd.as_std();
let mut cloned = StdCommand::new(cmd.get_program());

cloned.args(cmd.get_args());

for (k, v) in cmd.get_envs() {
match v {
Some(v) => cloned.env(k, v),
None => cloned.env_remove(k),
};
}

match kill_on_drop {
None => cloned.into(),
Some(kill_on_drop) => {
let mut cmd: TokioCommand = cloned.into();
if let Some(current_dir) = cmd.get_current_dir() {
cloned.current_dir(current_dir);
}

let mut tokio_cloned: TokioCommand = cloned.into();
if kill_on_drop {
cmd.kill_on_drop(true);
tokio_cloned.kill_on_drop(true);
}

cmd.into()
Self::Tokio(tokio_cloned)
}
}
}
Expand Down
61 changes: 61 additions & 0 deletions src/result.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,63 @@
/// `Result` with default types.
pub type AnyRes<T = (), E = anyhow::Error> = Result<T, E>;

/// Extension trait for [`Result`] types.
pub trait ResultExt<T, E> {
/// Convert the error to a string and ignore the specific error type.
///
/// This is useful when you want to log an error but don't need to handle
/// the specific error type.
fn ignore_err(self) -> Result<T, ()>;

/// Convert both success and error values to the same type using provided closures.
///
/// This is a more ergonomic version of `map().map_err()` when you want to
/// convert both sides to the same type.
fn map_both<U, F, G>(self, ok_fn: F, err_fn: G) -> U
where
F: FnOnce(T) -> U,
G: FnOnce(E) -> U;
}

impl<T, E> ResultExt<T, E> for Result<T, E> {
fn ignore_err(self) -> Result<T, ()> {
self.map_err(|_| ())
}

fn map_both<U, F, G>(self, ok_fn: F, err_fn: G) -> U
where
F: FnOnce(T) -> U,
G: FnOnce(E) -> U,
{
match self {
Ok(t) => ok_fn(t),
Err(e) => err_fn(e),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn ignore_err() {
let ok_result: Result<i32, String> = Ok(42);
let err_result: Result<i32, String> = Err("error".to_string());

assert_eq!(ok_result.ignore_err(), Ok(42));
assert_eq!(err_result.ignore_err(), Err(()));
}

#[test]
fn map_both() {
let ok_result: Result<i32, String> = Ok(42);
let err_result: Result<i32, String> = Err("error".to_string());

let ok_mapped = ok_result.map_both(|x| x * 2, |_| 0);
let err_mapped = err_result.map_both(|x| x * 2, |_| 0);

assert_eq!(ok_mapped, 84);
assert_eq!(err_mapped, 0);
}
}
30 changes: 30 additions & 0 deletions src/sync/once.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,36 @@ pub fn once_event() -> (OnceTrigger, OnceWaiter) {
(OnceTrigger(send), OnceWaiter { recv, triggered })
}

/// Creates multiple independent one-time events.
///
/// This is a convenience function for creating multiple independent trigger-waiter pairs.
/// Each trigger can only notify its corresponding waiter.
///
/// # Examples
///
/// ```
/// use est::sync::once::once_events;
///
/// #[tokio::main]
/// async fn main() {
/// let events = once_events(3);
///
/// for (i, (trigger, waiter)) in events.into_iter().enumerate() {
/// tokio::spawn(async move {
/// if waiter.await {
/// println!("waiter {} received event", i);
/// }
/// });
///
/// // Trigger each event independently
/// trigger.trigger();
/// }
/// }
/// ```
pub fn once_events(count: usize) -> Vec<(OnceTrigger, OnceWaiter)> {
(0..count).map(|_| once_event()).collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down