use pollster::block_on;
use raskell::hdo;
#[derive(Debug, PartialEq)]
enum StoreError {
Unavailable,
}
#[derive(Debug, PartialEq)]
enum ApiError {
Store,
Missing,
TooSmall,
}
impl From<StoreError> for ApiError {
fn from(_: StoreError) -> Self {
ApiError::Store
}
}
async fn fetch(value: i32) -> Result<i32, ApiError> {
value_of(value).await
}
async fn value_of(value: i32) -> Result<i32, ApiError> {
value
.checked_mul(1)
.map(Ok)
.unwrap_or(Err(ApiError::Missing))
}
async fn from_store() -> Result<i32, StoreError> {
Err(StoreError::Unavailable)
}
async fn maybe(value: Option<i32>) -> Option<i32> {
value
}
#[test]
fn awaits_inside_every_position() {
let result: Result<i32, ApiError> = block_on(hdo!(async {
x <- fetch(10).await;
y <- fetch(20).await;
let sum = x + y;
guard fetch(sum).await? > 5 throw ApiError::TooSmall;
pure sum
}));
assert_eq!(result, Ok(30));
}
#[test]
fn short_circuits_on_err() {
let result: Result<i32, ApiError> = block_on(hdo!(async {
x <- fetch(1).await;
y: i32 <- Err(ApiError::Missing);
pure x + y
}));
assert_eq!(result, Err(ApiError::Missing));
}
#[test]
fn try_bind_converts_error() {
let result: Result<i32, ApiError> = block_on(hdo!(async {
x <-? from_store().await;
pure x
}));
assert_eq!(result, Err(ApiError::Store));
}
#[test]
fn option_block() {
let result: Option<i32> = block_on(hdo!(async {
x <- maybe(Some(10)).await;
y <- maybe(Some(20)).await;
guard x + y > 5;
pure x + y
}));
assert_eq!(result, Some(30));
}
#[test]
fn option_block_short_circuits() {
let result: Option<i32> = block_on(hdo!(async {
x <- maybe(Some(10)).await;
y <- maybe(None).await;
pure x + y
}));
assert_eq!(result, None);
}
#[test]
fn guard_throws() {
let result: Result<i32, ApiError> = block_on(hdo!(async {
x <- fetch(1).await;
guard x > 5 throw ApiError::TooSmall;
pure x
}));
assert_eq!(result, Err(ApiError::TooSmall));
}
#[test]
fn refutable_pattern_with_throw() {
async fn nickname() -> Result<Option<String>, ApiError> {
Ok(None)
}
let result: Result<String, ApiError> = block_on(hdo!(async {
Some(name) <- nickname().await throw ApiError::Missing;
pure name
}));
assert_eq!(result, Err(ApiError::Missing));
}
#[test]
fn refutable_pattern_in_option_block() {
let result: Option<i32> = block_on(hdo!(async {
Some(x) <- maybe(Some(10)).await.map(Some);
pure x
}));
assert_eq!(result, Some(10));
}
#[test]
fn action_short_circuits() {
async fn check() -> Result<(), ApiError> {
Err(ApiError::Missing)
}
let result: Result<i32, ApiError> = block_on(hdo!(async {
check().await;
pure 1
}));
assert_eq!(result, Err(ApiError::Missing));
}
#[test]
fn unit_statements_do_not_short_circuit() {
let result: Result<i32, ApiError> = block_on(hdo!(async {
x <- fetch(10).await;
println!("bound {x}");
pure x
}));
assert_eq!(result, Ok(10));
}
#[test]
fn ascription_pins_the_bound_type() {
let result: Result<i32, ApiError> = block_on(hdo!(async {
Some(x): Option<i32> <- Err(ApiError::Missing) throw ApiError::TooSmall;
pure x
}));
assert_eq!(result, Err(ApiError::Missing));
}
#[test]
fn the_block_is_a_plain_future() {
#[allow(clippy::manual_async_fn)]
fn spawnable() -> impl Future<Output = Result<i32, ApiError>> {
hdo!(async {
x <- fetch(10).await;
pure x
})
}
assert_eq!(block_on(spawnable()), Ok(10));
}
#[test]
fn the_future_is_send_and_static() {
fn assert_spawnable<F>(future: F) -> F
where
F: Future<Output = Result<i32, ApiError>> + Send + 'static,
{
future
}
let future = assert_spawnable(hdo!(async {
x <- fetch(10).await;
y <-? from_store().await;
guard x > 5 throw ApiError::TooSmall;
pure x + y
}));
assert_eq!(block_on(future), Err(ApiError::Store));
}
#[test]
fn user_bindings_named_like_the_expansion_are_untouched() {
let __raskell_value = 100;
let __raskell_bound = 200;
let __raskell_short = 300;
let result: Result<i32, ApiError> = block_on(hdo!(async {
Some(x) <- fetch(1).await.map(Some) throw ApiError::Missing;
pure x + __raskell_value + __raskell_bound + __raskell_short
}));
assert_eq!(result, Ok(601));
}
#[test]
fn nothing_after_a_failure_is_awaited() {
let calls = std::cell::Cell::new(0);
let counter = &calls;
let result: Result<i32, ApiError> = block_on(hdo!(async {
x <- fetch(1).await;
y: i32 <- Err(ApiError::Missing);
counter.set(counter.get() + 1);
pure x + y
}));
assert_eq!(result, Err(ApiError::Missing));
assert_eq!(calls.get(), 0);
}
#[test]
fn option_actions_short_circuit() {
let result: Option<i32> = block_on(hdo!(async {
x <- maybe(Some(10)).await;
maybe(None).await;
pure x
}));
assert_eq!(result, None);
}
#[test]
fn try_bind_with_a_refutable_pattern() {
async fn stored() -> Result<Option<i32>, StoreError> {
Ok(None)
}
let result: Result<i32, ApiError> = block_on(hdo!(async {
Some(x) <-? stored().await throw ApiError::Missing;
pure x
}));
assert_eq!(result, Err(ApiError::Missing));
}
#[test]
fn a_sync_block_nests_inside_an_async_one() {
let result: Result<i32, ApiError> = block_on(hdo!(async {
x <- fetch(10).await;
y <- hdo! {
z <- Ok(2);
pure z * 10
};
pure x + y
}));
assert_eq!(result, Ok(30));
}
#[test]
fn items_and_typed_lets_survive_expansion() {
let result: Result<i64, ApiError> = block_on(hdo!(async {
fn triple(value: i64) -> i64 {
value * 3
}
x <- fetch(10).await;
let y: i64 = triple(i64::from(x));
pure y
}));
assert_eq!(result, Ok(30));
}
#[test]
fn an_async_block_without_binds_still_wraps_pure() {
let result: Result<i32, ApiError> = block_on(hdo!(async {
let x = 20;
pure x
}));
assert_eq!(result, Ok(20));
}