Skip to content

Commit f3fd497

Browse files
authored
Merge pull request #13 from c-git/develop
0.6.0
2 parents 1a9e2ac + c4a66c1 commit f3fd497

File tree

5 files changed

+149
-121
lines changed

5 files changed

+149
-121
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "reqwest-cross"
3-
version = "0.5.1"
3+
version = "0.6.0"
44
authors = ["One <[email protected]>"]
55
categories = ["web-programming::http-client", "wasm"]
66
documentation = "https://docs.rs/reqwest-cross"

examples/loop_yield_data_state.rs

+26-24
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// DataState type.
44

55
use anyhow::Context;
6-
use reqwest_cross::{fetch_plus, reqwest, Awaiting, DataState};
6+
use reqwest_cross::{fetch_plus, oneshot, reqwest, DataState};
77

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

2424
async fn common_code() -> Result<(), Box<dyn std::error::Error>> {
25-
// Allows for one iteration where we see no progress but next loop should go
26-
// into first branch
27-
let mut seen_no_progress = false;
28-
2925
let client = reqwest::Client::new();
3026
let mut state = DataState::None;
3127

@@ -34,34 +30,40 @@ async fn common_code() -> Result<(), Box<dyn std::error::Error>> {
3430
// This loop would normally be a game loop, or the executor of an immediate mode
3531
// GUI.
3632
loop {
37-
if let DataState::Present(status_code) = state.as_ref() {
33+
if state.is_none() {
34+
let client = client.clone();
35+
let can_make_progress =
36+
state.start_request(|| make_request(client, "https://httpbin.org/get"));
37+
assert!(can_make_progress.is_able_to_make_progress());
38+
}
39+
if let Some(status_code) = state.poll().present() {
3840
println!("Response received");
3941
assert_eq!(status_code, &200);
4042
break;
41-
} else {
42-
let outcome = state.get(|| {
43-
let req = client.get("https://httpbin.org/get");
44-
let response_handler = |resp: reqwest::Result<reqwest::Response>| async {
45-
resp.map(|resp| resp.status())
46-
.context("Request failed, got an error back")
47-
};
48-
let ui_notify = || {
49-
println!("Request Completed, this is where you would wake up your UI thread");
50-
};
51-
Awaiting(fetch_plus(req, response_handler, ui_notify))
52-
});
53-
assert!(!seen_no_progress);
54-
if outcome.is_unable_to_make_progress() {
55-
// We should never get into this branch again
56-
seen_no_progress = true;
57-
}
58-
reqwest_cross::yield_now().await;
5943
}
44+
reqwest_cross::yield_now().await;
6045
}
6146
println!("Exited loop");
6247
Ok(())
6348
}
6449

50+
fn make_request(
51+
client: reqwest::Client,
52+
url: impl reqwest::IntoUrl,
53+
) -> oneshot::Receiver<anyhow::Result<reqwest::StatusCode>> {
54+
let req = client.get(url);
55+
let response_handler = |resp: reqwest::Result<reqwest::Response>| async {
56+
resp.map(|resp| resp.status())
57+
.context("Request failed, got an error back")
58+
};
59+
let ui_notify = || {
60+
println!("Request Completed, this is where you would wake up your UI thread.
61+
If using egui version of the functions the associated methods add spinners which will keep the loop going so no wake up is needed.
62+
Passing an empty closure would suffice.");
63+
};
64+
fetch_plus(req, response_handler, ui_notify)
65+
}
66+
6567
#[cfg(all(test, not(target_arch = "wasm32")))]
6668
mod tests {
6769

src/data_state.rs

+96-57
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ pub enum CanMakeProgress {
3737
/// Used to represent data that is pending being available
3838
#[derive(Debug)]
3939
pub struct Awaiting<T, E: ErrorBounds>(pub oneshot::Receiver<Result<T, E>>);
40+
impl<T, E: ErrorBounds> From<oneshot::Receiver<Result<T, E>>> for Awaiting<T, E> {
41+
fn from(value: oneshot::Receiver<Result<T, E>>) -> Self {
42+
Self(value)
43+
}
44+
}
4045

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

5661
impl<T, E: ErrorBounds> DataState<T, E> {
5762
#[cfg(feature = "egui")]
58-
/// Attempts to load the data and displays appropriate UI if applicable.
59-
/// Some branches lead to no UI being displayed, in particular when the data
60-
/// or an error is received (On the expectation it will show next frame).
61-
/// When in an error state the error messages will show as applicable.
62-
/// If called an already has data present this function does nothing and
63-
/// returns [CanMakeProgress::UnableToMakeProgress]
63+
/// Calls [Self::start_request] and adds a spinner if progress can be made
64+
pub fn egui_start_request<F, R>(&mut self, ui: &mut egui::Ui, fetch_fn: F) -> CanMakeProgress
65+
where
66+
F: FnOnce() -> R,
67+
R: Into<Awaiting<T, E>>,
68+
{
69+
let result = self.start_request(fetch_fn);
70+
if result.is_able_to_make_progress() {
71+
ui.spinner();
72+
}
73+
result
74+
}
75+
76+
/// Starts a new request. Only intended to be on [Self::None] and if state
77+
/// is any other value it returns [CanMakeProgress::UnableToMakeProgress]
78+
#[must_use]
79+
pub fn start_request<F, R>(&mut self, fetch_fn: F) -> CanMakeProgress
80+
where
81+
F: FnOnce() -> R,
82+
R: Into<Awaiting<T, E>>,
83+
{
84+
if self.is_none() {
85+
*self = DataState::AwaitingResponse(fetch_fn().into());
86+
CanMakeProgress::AbleToMakeProgress
87+
} else {
88+
debug_assert!(
89+
false,
90+
"No known good reason this path should be hit other than logic error"
91+
);
92+
CanMakeProgress::UnableToMakeProgress
93+
}
94+
}
95+
96+
/// Convenience method that will try to make progress if in
97+
/// [Self::AwaitingResponse] and does nothing otherwise. Returns a reference
98+
/// to self for chaining
99+
pub fn poll(&mut self) -> &mut Self {
100+
if let DataState::AwaitingResponse(rx) = self {
101+
if let Some(new_state) = Self::await_data(rx) {
102+
*self = new_state;
103+
}
104+
}
105+
self
106+
}
107+
108+
#[cfg(feature = "egui")]
109+
/// Meant to be a simple method to just provide the data if it's ready or
110+
/// help with UI and polling to get it ready if it's not.
64111
///
65-
/// If a `retry_msg` is provided then it overrides the default
112+
/// WARNING: Does nothing if `self` is [Self::None]
66113
///
67-
/// Note see [`Self::get`] for more info.
68-
#[must_use]
69-
pub fn egui_get<F>(
114+
/// If a `error_btn_text` is provided then it overrides the default
115+
pub fn egui_poll_mut(
70116
&mut self,
71117
ui: &mut egui::Ui,
72-
retry_msg: Option<&str>,
73-
fetch_fn: F,
74-
) -> CanMakeProgress
75-
where
76-
F: FnOnce() -> Awaiting<T, E>,
77-
{
118+
error_btn_text: Option<&str>,
119+
) -> Option<&mut T> {
78120
match self {
79-
DataState::None => {
80-
ui.spinner();
81-
self.get(fetch_fn)
82-
}
121+
DataState::None => {}
83122
DataState::AwaitingResponse(_) => {
84123
ui.spinner();
85-
self.get(fetch_fn)
124+
self.poll();
86125
}
87-
DataState::Present(_data) => {
88-
// Does nothing as data is already present
89-
CanMakeProgress::UnableToMakeProgress
126+
DataState::Present(data) => {
127+
return Some(data);
90128
}
91129
DataState::Failed(e) => {
92130
ui.colored_label(ui.visuals().error_fg_color, e.to_string());
93-
if ui.button(retry_msg.unwrap_or("Retry Request")).clicked() {
131+
if ui
132+
.button(error_btn_text.unwrap_or("Clear Error Status"))
133+
.clicked()
134+
{
94135
*self = DataState::default();
95136
}
96-
CanMakeProgress::AbleToMakeProgress
97137
}
98138
}
139+
None
99140
}
100141

101-
/// Attempts to load the data and returns if it is able to make progress.
102-
///
103-
/// Note: F needs to return `AwaitingType<T>` and not T because it needs to
104-
/// be able to be pending if T is not ready.
105-
#[must_use]
106-
pub fn get<F>(&mut self, fetch_fn: F) -> CanMakeProgress
107-
where
108-
F: FnOnce() -> Awaiting<T, E>,
109-
{
110-
match self {
111-
DataState::None => {
112-
let rx = fetch_fn();
113-
*self = DataState::AwaitingResponse(rx);
114-
CanMakeProgress::AbleToMakeProgress
115-
}
116-
DataState::AwaitingResponse(rx) => {
117-
if let Some(new_state) = Self::await_data(rx) {
118-
*self = new_state;
119-
}
120-
CanMakeProgress::AbleToMakeProgress
121-
}
122-
DataState::Present(_data) => {
123-
// Does nothing data is already present
124-
CanMakeProgress::UnableToMakeProgress
125-
}
126-
DataState::Failed(_e) => {
127-
// Have no way to let the user know there is an error nothing we
128-
// can do here
129-
CanMakeProgress::UnableToMakeProgress
130-
}
131-
}
142+
#[cfg(feature = "egui")]
143+
/// Wraps [Self::egui_poll_mut] and returns an immutable reference
144+
pub fn egui_poll(&mut self, ui: &mut egui::Ui, error_btn_text: Option<&str>) -> Option<&T> {
145+
self.egui_poll_mut(ui, error_btn_text).map(|x| &*x)
132146
}
133147

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

171+
/// Returns a reference to the inner data if available otherwise None.
172+
///
173+
/// NOTE: This function does not poll to get the data ready if the state is
174+
/// still awaiting
175+
pub fn present(&self) -> Option<&T> {
176+
if let Self::Present(data) = self {
177+
Some(data)
178+
} else {
179+
None
180+
}
181+
}
182+
183+
/// Returns a mutable reference to the inner data if available otherwise
184+
/// None
185+
///
186+
/// NOTE: This function does not poll to get the data ready if the state is
187+
/// still awaiting
188+
pub fn present_mut(&mut self) -> Option<&mut T> {
189+
if let Self::Present(data) = self {
190+
Some(data)
191+
} else {
192+
None
193+
}
194+
}
195+
157196
/// Returns `true` if the data state is [`Present`].
158197
///
159198
/// [`Present`]: DataState::Present

0 commit comments

Comments
 (0)