Skip to content

Commit

Permalink
Merge pull request #13 from c-git/develop
Browse files Browse the repository at this point in the history
0.6.0
  • Loading branch information
c-git authored Jan 17, 2025
2 parents 1a9e2ac + c4a66c1 commit f3fd497
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 121 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "reqwest-cross"
version = "0.5.1"
version = "0.6.0"
authors = ["One <[email protected]>"]
categories = ["web-programming::http-client", "wasm"]
documentation = "https://docs.rs/reqwest-cross"
Expand Down
50 changes: 26 additions & 24 deletions examples/loop_yield_data_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// DataState type.

use anyhow::Context;
use reqwest_cross::{fetch_plus, reqwest, Awaiting, DataState};
use reqwest_cross::{fetch_plus, oneshot, reqwest, DataState};

#[cfg(all(not(target_arch = "wasm32"), feature = "native-tokio"))]
#[tokio::main]
Expand All @@ -22,10 +22,6 @@ fn main() {
}

async fn common_code() -> Result<(), Box<dyn std::error::Error>> {
// Allows for one iteration where we see no progress but next loop should go
// into first branch
let mut seen_no_progress = false;

let client = reqwest::Client::new();
let mut state = DataState::None;

Expand All @@ -34,34 +30,40 @@ async fn common_code() -> Result<(), Box<dyn std::error::Error>> {
// This loop would normally be a game loop, or the executor of an immediate mode
// GUI.
loop {
if let DataState::Present(status_code) = state.as_ref() {
if state.is_none() {
let client = client.clone();
let can_make_progress =
state.start_request(|| make_request(client, "https://httpbin.org/get"));
assert!(can_make_progress.is_able_to_make_progress());
}
if let Some(status_code) = state.poll().present() {
println!("Response received");
assert_eq!(status_code, &200);
break;
} else {
let outcome = state.get(|| {
let req = client.get("https://httpbin.org/get");
let response_handler = |resp: reqwest::Result<reqwest::Response>| async {
resp.map(|resp| resp.status())
.context("Request failed, got an error back")
};
let ui_notify = || {
println!("Request Completed, this is where you would wake up your UI thread");
};
Awaiting(fetch_plus(req, response_handler, ui_notify))
});
assert!(!seen_no_progress);
if outcome.is_unable_to_make_progress() {
// We should never get into this branch again
seen_no_progress = true;
}
reqwest_cross::yield_now().await;
}
reqwest_cross::yield_now().await;
}
println!("Exited loop");
Ok(())
}

fn make_request(
client: reqwest::Client,
url: impl reqwest::IntoUrl,
) -> oneshot::Receiver<anyhow::Result<reqwest::StatusCode>> {
let req = client.get(url);
let response_handler = |resp: reqwest::Result<reqwest::Response>| async {
resp.map(|resp| resp.status())
.context("Request failed, got an error back")
};
let ui_notify = || {
println!("Request Completed, this is where you would wake up your UI thread.
If using egui version of the functions the associated methods add spinners which will keep the loop going so no wake up is needed.
Passing an empty closure would suffice.");
};
fetch_plus(req, response_handler, ui_notify)
}

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {

Expand Down
153 changes: 96 additions & 57 deletions src/data_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ pub enum CanMakeProgress {
/// Used to represent data that is pending being available
#[derive(Debug)]
pub struct Awaiting<T, E: ErrorBounds>(pub oneshot::Receiver<Result<T, E>>);
impl<T, E: ErrorBounds> From<oneshot::Receiver<Result<T, E>>> for Awaiting<T, E> {
fn from(value: oneshot::Receiver<Result<T, E>>) -> Self {
Self(value)
}
}

/// Used to store a type that is not always available and we need to keep
/// polling it to get it ready
Expand All @@ -55,80 +60,89 @@ pub enum DataState<T, E: ErrorBounds = anyhow::Error> {

impl<T, E: ErrorBounds> DataState<T, E> {
#[cfg(feature = "egui")]
/// Attempts to load the data and displays appropriate UI if applicable.
/// Some branches lead to no UI being displayed, in particular when the data
/// or an error is received (On the expectation it will show next frame).
/// When in an error state the error messages will show as applicable.
/// If called an already has data present this function does nothing and
/// returns [CanMakeProgress::UnableToMakeProgress]
/// Calls [Self::start_request] and adds a spinner if progress can be made
pub fn egui_start_request<F, R>(&mut self, ui: &mut egui::Ui, fetch_fn: F) -> CanMakeProgress
where
F: FnOnce() -> R,
R: Into<Awaiting<T, E>>,
{
let result = self.start_request(fetch_fn);
if result.is_able_to_make_progress() {
ui.spinner();
}
result
}

/// Starts a new request. Only intended to be on [Self::None] and if state
/// is any other value it returns [CanMakeProgress::UnableToMakeProgress]
#[must_use]
pub fn start_request<F, R>(&mut self, fetch_fn: F) -> CanMakeProgress
where
F: FnOnce() -> R,
R: Into<Awaiting<T, E>>,
{
if self.is_none() {
*self = DataState::AwaitingResponse(fetch_fn().into());
CanMakeProgress::AbleToMakeProgress
} else {
debug_assert!(
false,
"No known good reason this path should be hit other than logic error"
);
CanMakeProgress::UnableToMakeProgress
}
}

/// Convenience method that will try to make progress if in
/// [Self::AwaitingResponse] and does nothing otherwise. Returns a reference
/// to self for chaining
pub fn poll(&mut self) -> &mut Self {
if let DataState::AwaitingResponse(rx) = self {
if let Some(new_state) = Self::await_data(rx) {
*self = new_state;
}
}
self
}

#[cfg(feature = "egui")]
/// Meant to be a simple method to just provide the data if it's ready or
/// help with UI and polling to get it ready if it's not.
///
/// If a `retry_msg` is provided then it overrides the default
/// WARNING: Does nothing if `self` is [Self::None]
///
/// Note see [`Self::get`] for more info.
#[must_use]
pub fn egui_get<F>(
/// If a `error_btn_text` is provided then it overrides the default
pub fn egui_poll_mut(
&mut self,
ui: &mut egui::Ui,
retry_msg: Option<&str>,
fetch_fn: F,
) -> CanMakeProgress
where
F: FnOnce() -> Awaiting<T, E>,
{
error_btn_text: Option<&str>,
) -> Option<&mut T> {
match self {
DataState::None => {
ui.spinner();
self.get(fetch_fn)
}
DataState::None => {}
DataState::AwaitingResponse(_) => {
ui.spinner();
self.get(fetch_fn)
self.poll();
}
DataState::Present(_data) => {
// Does nothing as data is already present
CanMakeProgress::UnableToMakeProgress
DataState::Present(data) => {
return Some(data);
}
DataState::Failed(e) => {
ui.colored_label(ui.visuals().error_fg_color, e.to_string());
if ui.button(retry_msg.unwrap_or("Retry Request")).clicked() {
if ui
.button(error_btn_text.unwrap_or("Clear Error Status"))
.clicked()
{
*self = DataState::default();
}
CanMakeProgress::AbleToMakeProgress
}
}
None
}

/// Attempts to load the data and returns if it is able to make progress.
///
/// Note: F needs to return `AwaitingType<T>` and not T because it needs to
/// be able to be pending if T is not ready.
#[must_use]
pub fn get<F>(&mut self, fetch_fn: F) -> CanMakeProgress
where
F: FnOnce() -> Awaiting<T, E>,
{
match self {
DataState::None => {
let rx = fetch_fn();
*self = DataState::AwaitingResponse(rx);
CanMakeProgress::AbleToMakeProgress
}
DataState::AwaitingResponse(rx) => {
if let Some(new_state) = Self::await_data(rx) {
*self = new_state;
}
CanMakeProgress::AbleToMakeProgress
}
DataState::Present(_data) => {
// Does nothing data is already present
CanMakeProgress::UnableToMakeProgress
}
DataState::Failed(_e) => {
// Have no way to let the user know there is an error nothing we
// can do here
CanMakeProgress::UnableToMakeProgress
}
}
#[cfg(feature = "egui")]
/// Wraps [Self::egui_poll_mut] and returns an immutable reference
pub fn egui_poll(&mut self, ui: &mut egui::Ui, error_btn_text: Option<&str>) -> Option<&T> {
self.egui_poll_mut(ui, error_btn_text).map(|x| &*x)
}

/// Checks to see if the data is ready and if it is returns a new [`Self`]
Expand All @@ -154,6 +168,31 @@ impl<T, E: ErrorBounds> DataState<T, E> {
})
}

/// Returns a reference to the inner data if available otherwise None.
///
/// NOTE: This function does not poll to get the data ready if the state is
/// still awaiting
pub fn present(&self) -> Option<&T> {
if let Self::Present(data) = self {
Some(data)
} else {
None
}
}

/// Returns a mutable reference to the inner data if available otherwise
/// None
///
/// NOTE: This function does not poll to get the data ready if the state is
/// still awaiting
pub fn present_mut(&mut self) -> Option<&mut T> {
if let Self::Present(data) = self {
Some(data)
} else {
None
}
}

/// Returns `true` if the data state is [`Present`].
///
/// [`Present`]: DataState::Present
Expand Down
Loading

0 comments on commit f3fd497

Please sign in to comment.