├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── README_end.md ├── README_start.md ├── changelog.md ├── examples ├── json.rs ├── minimal.rs └── post.rs └── src └── lib.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_mod_reqwest" 3 | description = "Bevy http client using reqwest, with a focus on simple usage within the bevy runtime" 4 | version = "0.19.2" 5 | edition = "2021" 6 | readme = "README.md" 7 | authors = ["Kristoffer Ödmark "] 8 | repository = "https://github.com/TotalKrill/bevy_mod_reqwest.git" 9 | license = "MIT" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | [features] 13 | default = ["rustls-tls", "json", "log"] 14 | default-tls = ["reqwest/default-tls"] 15 | json = ["reqwest/json", "serde_json"] 16 | rustls-tls = ["reqwest/rustls-tls"] 17 | msgpack = ["rmp-serde"] 18 | log = ["bevy/bevy_log"] 19 | 20 | [dependencies] 21 | reqwest = { version = "0.12", default-features = false } 22 | rmp-serde = { version = "1.1.2", optional = true} 23 | serde = { version = "1.0.159", features = ["derive"] } 24 | serde_json = { version = "1", optional = true } 25 | 26 | anyhow = "1.0.79" 27 | bytes = "1.6.0" 28 | futures-lite = "2.3.0" 29 | 30 | [dependencies.bevy] 31 | version = "0.16" 32 | # git = "https://github.com/bevyengine/bevy" 33 | default-features = false 34 | 35 | [target.'cfg(target_arch = "wasm32")'.dependencies] 36 | crossbeam-channel = "0.5" 37 | 38 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 39 | async-compat = "0.2" 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kristoffer Ödmark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .FORCE: 2 | 3 | readme: .FORCE 4 | cat README_start.md > README.md 5 | cat examples/minimal.rs >> README.md 6 | cat README_end.md >> README.md 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_mod_reqwest 2 | 3 | [![crates.io](https://img.shields.io/crates/v/bevy_mod_reqwest)](https://crates.io/crates/bevy_mod_reqwest) 4 | [![docs.rs](https://docs.rs/bevy_mod_reqwest/badge.svg)](https://docs.rs/bevy_mod_reqwest) 5 | 6 | This crate helps when trying to use reqwest with bevy, without having to deal with async stuff, and it works on both web and and native 7 | ( only tested on x86_64 and wasm for now) 8 | 9 | | Bevy version | bevy_mod_reqwest version | 10 | | ------------ | ------------------------ | 11 | | 0.16 | 0.19 | 12 | | 0.15 | 0.18 | 13 | | 0.14 | 0.15 - 0.17 | 14 | | 0.13 | 0.14 | 15 | | 0.12 | 0.12 - 0.13 | 16 | 17 | ## Example 18 | 19 | ``` rust 20 | use std::time::Duration; 21 | 22 | use bevy::{log::LogPlugin, prelude::*, time::common_conditions::on_timer}; 23 | use bevy_mod_reqwest::*; 24 | 25 | #[derive(Default, Resource)] 26 | // just a vector that stores all the responses as strings to showcase that the `on_response` methods 27 | // are just regular observersystems, that function very much like regular systems 28 | struct History { 29 | pub responses: Vec, 30 | } 31 | 32 | fn send_requests(mut client: BevyReqwest) { 33 | let url = "https://bored-api.appbrewery.com/random"; 34 | 35 | // use regular reqwest http calls, then poll them to completion. 36 | let reqwest_request = client.get(url).build().unwrap(); 37 | 38 | client 39 | // Sends the created http request 40 | .send(reqwest_request) 41 | // The response from the http request can be reached using an observersystem, 42 | // where the only requirement is that the first parameter in the system is the specific Trigger type 43 | // the rest is the same as a regular system 44 | .on_response( 45 | |trigger: Trigger, mut history: ResMut| { 46 | let response = trigger.event(); 47 | let data = response.as_str(); 48 | let status = response.status(); 49 | // let headers = req.response_headers(); 50 | bevy::log::info!("code: {status}, data: {data:?}"); 51 | if let Ok(data) = data { 52 | history.responses.push(format!("OK: {data}")); 53 | } 54 | }, 55 | ) 56 | // In case of request error, it can be reached using an observersystem as well 57 | .on_error( 58 | |trigger: Trigger, mut history: ResMut| { 59 | let e = &trigger.event().0; 60 | bevy::log::info!("error: {e:?}"); 61 | history.responses.push(format!("ERROR: {e:?}")); 62 | }, 63 | ); 64 | } 65 | 66 | fn main() { 67 | App::new() 68 | .add_plugins(MinimalPlugins) 69 | .add_plugins(LogPlugin::default()) 70 | .add_plugins(ReqwestPlugin::default()) 71 | .init_resource::() 72 | .add_systems( 73 | Update, 74 | send_requests.run_if(on_timer(Duration::from_secs(5))), 75 | ) 76 | .run(); 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /README_end.md: -------------------------------------------------------------------------------- 1 | ``` 2 | -------------------------------------------------------------------------------- /README_start.md: -------------------------------------------------------------------------------- 1 | # bevy_mod_reqwest 2 | 3 | [![crates.io](https://img.shields.io/crates/v/bevy_mod_reqwest)](https://crates.io/crates/bevy_mod_reqwest) 4 | [![docs.rs](https://docs.rs/bevy_mod_reqwest/badge.svg)](https://docs.rs/bevy_mod_reqwest) 5 | 6 | This crate helps when trying to use reqwest with bevy, without having to deal with async stuff, and it works on both web and and native 7 | ( only tested on x86_64 and wasm for now) 8 | 9 | | Bevy version | bevy_mod_reqwest version | 10 | | ------------ | ------------------------ | 11 | | 0.16 | 0.19 | 12 | | 0.15 | 0.18 | 13 | | 0.14 | 0.15 - 0.17 | 14 | | 0.13 | 0.14 | 15 | | 0.12 | 0.12 - 0.13 | 16 | 17 | ## Example 18 | 19 | ``` rust 20 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | # 0.18 4 | - Support bevy 0.15 5 | - Added a `on_json_response` helper method to reduce boilerplate when using json API:s 6 | 7 | # 0.17 8 | - Changed so that the request are polled in `PreUpdate` schedule 9 | 10 | # Pre 0.17 11 | - changes not tracked 12 | -------------------------------------------------------------------------------- /examples/json.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bevy::{log::LogPlugin, prelude::*, time::common_conditions::on_timer}; 4 | use bevy_mod_reqwest::*; 5 | use serde::Deserialize; 6 | 7 | #[derive(Deserialize, Debug)] 8 | pub struct Bored { 9 | pub activity: String, 10 | pub price: f32, 11 | pub participants: f32, 12 | } 13 | 14 | fn send_requests(mut client: BevyReqwest) { 15 | let url = "https://bored-api.appbrewery.com/random"; 16 | 17 | // use regular reqwest http calls, then poll them to completion. 18 | let reqwest_request = client.get(url).build().unwrap(); 19 | 20 | client 21 | // Sends the created http request 22 | .send(reqwest_request) 23 | // The response from the http request can be reached using an observersystem 24 | .on_json_response(|trigger: Trigger>| { 25 | let data: &Bored = &trigger.event().0; 26 | // let headers = req.response_headers(); 27 | bevy::log::info!("data: {data:?}"); 28 | }) 29 | // In case of request error, it can be reached using an observersystem 30 | .on_error(|trigger: Trigger| { 31 | let e = &trigger.event().0; 32 | bevy::log::info!("error: {e:?}"); 33 | }); 34 | } 35 | 36 | fn main() { 37 | App::new() 38 | .add_plugins(MinimalPlugins) 39 | .add_plugins(LogPlugin::default()) 40 | .add_plugins(ReqwestPlugin::default()) 41 | .add_systems( 42 | Update, 43 | send_requests.run_if(on_timer(Duration::from_secs(5))), 44 | ) 45 | .run(); 46 | } 47 | -------------------------------------------------------------------------------- /examples/minimal.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bevy::{log::LogPlugin, prelude::*, time::common_conditions::on_timer}; 4 | use bevy_mod_reqwest::*; 5 | 6 | #[derive(Default, Resource)] 7 | // just a vector that stores all the responses as strings to showcase that the `on_response` methods 8 | // are just regular observersystems, that function very much like regular systems 9 | struct History { 10 | pub responses: Vec, 11 | } 12 | 13 | fn send_requests(mut client: BevyReqwest) { 14 | let url = "https://bored-api.appbrewery.com/random"; 15 | 16 | // use regular reqwest http calls, then poll them to completion. 17 | let reqwest_request = client.get(url).build().unwrap(); 18 | 19 | client 20 | // Sends the created http request 21 | .send(reqwest_request) 22 | // The response from the http request can be reached using an observersystem, 23 | // where the only requirement is that the first parameter in the system is the specific Trigger type 24 | // the rest is the same as a regular system 25 | .on_response( 26 | |trigger: Trigger, mut history: ResMut| { 27 | let response = trigger.event(); 28 | let data = response.as_str(); 29 | let status = response.status(); 30 | // let headers = req.response_headers(); 31 | bevy::log::info!("code: {status}, data: {data:?}"); 32 | if let Ok(data) = data { 33 | history.responses.push(format!("OK: {data}")); 34 | } 35 | }, 36 | ) 37 | // In case of request error, it can be reached using an observersystem as well 38 | .on_error( 39 | |trigger: Trigger, mut history: ResMut| { 40 | let e = &trigger.event().0; 41 | bevy::log::info!("error: {e:?}"); 42 | history.responses.push(format!("ERROR: {e:?}")); 43 | }, 44 | ); 45 | } 46 | 47 | fn main() { 48 | App::new() 49 | .add_plugins(MinimalPlugins) 50 | .add_plugins(LogPlugin::default()) 51 | .add_plugins(ReqwestPlugin::default()) 52 | .init_resource::() 53 | .add_systems( 54 | Update, 55 | send_requests.run_if(on_timer(Duration::from_secs(5))), 56 | ) 57 | .run(); 58 | } 59 | -------------------------------------------------------------------------------- /examples/post.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bevy::{log::LogPlugin, prelude::*, time::common_conditions::on_timer}; 4 | use bevy_mod_reqwest::*; 5 | use serde::Serialize; 6 | 7 | // example towards jsonplaceholder.typicod.com/posts 8 | #[derive(Serialize)] 9 | struct Post { 10 | title: String, 11 | body: String, 12 | user_id: usize, 13 | } 14 | 15 | fn send_requests(mut client: BevyReqwest) { 16 | let url = "https://jsonplaceholder.typicode.com/posts"; 17 | let body = Post { 18 | title: "hello".into(), 19 | body: "world".into(), 20 | user_id: 1, 21 | }; 22 | let req = client.post(url).json(&body).build().unwrap(); 23 | client 24 | .send(req) 25 | .on_response(|req: Trigger| { 26 | let req = req.event(); 27 | let res = req.as_str(); 28 | bevy::log::info!("return data: {res:?}"); 29 | }); 30 | } 31 | 32 | fn main() { 33 | App::new() 34 | .add_plugins(MinimalPlugins) 35 | .add_plugins(LogPlugin::default()) 36 | .add_plugins(ReqwestPlugin::default()) 37 | .add_systems( 38 | Update, 39 | send_requests.run_if(on_timer(Duration::from_secs(2))), 40 | ) 41 | .run(); 42 | } 43 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use bevy::{ 4 | ecs::system::{EntityCommands, IntoObserverSystem, SystemParam}, 5 | prelude::*, 6 | tasks::IoTaskPool, 7 | }; 8 | 9 | pub use reqwest; 10 | 11 | #[cfg(target_family = "wasm")] 12 | use crossbeam_channel::{bounded, Receiver}; 13 | 14 | #[cfg(feature = "json")] 15 | pub use json::*; 16 | 17 | pub use reqwest::header::HeaderMap; 18 | pub use reqwest::{StatusCode, Version}; 19 | 20 | #[cfg(not(target_family = "wasm"))] 21 | use {bevy::tasks::Task, futures_lite::future}; 22 | 23 | /// The [`SystemSet`] that Reqwest systems are added to. 24 | #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] 25 | pub struct ReqwestSet; 26 | 27 | /// Plugin that allows to send http request using the [reqwest](https://crates.io/crates/reqwest) library from 28 | /// inside bevy. 29 | /// 30 | /// The plugin uses [`Observer`] systems to provide callbacks when the http requests finishes. 31 | /// 32 | /// Supports both wasm and native. 33 | pub struct ReqwestPlugin { 34 | /// this enables the plugin to insert a new [`Name`] component onto the entity used to drive 35 | /// the http request to completion, if no such component already exists 36 | pub automatically_name_requests: bool, 37 | } 38 | impl Default for ReqwestPlugin { 39 | fn default() -> Self { 40 | Self { 41 | automatically_name_requests: true, 42 | } 43 | } 44 | } 45 | impl Plugin for ReqwestPlugin { 46 | fn build(&self, app: &mut App) { 47 | app.init_resource::(); 48 | 49 | if self.automatically_name_requests { 50 | // register a hook on the component to add a name to the entity if it doesnt have one already 51 | app.world_mut() 52 | .register_component_hooks::() 53 | .on_insert(|mut world, ctx| { 54 | let url = world 55 | .get::(ctx.entity) 56 | .unwrap() 57 | .url 58 | .clone(); 59 | 60 | if let None = world.get::(ctx.entity) { 61 | let mut commands = world.commands(); 62 | let mut entity = commands.get_entity(ctx.entity).unwrap(); 63 | entity.insert(Name::new(format!("http: {url}"))); 64 | } 65 | }); 66 | } 67 | // 68 | app.add_systems( 69 | PreUpdate, 70 | ( 71 | // These systems are chained, since the poll_inflight_requests will trigger the callback and mark the entity for deletion 72 | 73 | // So if remove_finished_requests runs after poll_inflight_requests_to_bytes 74 | // the entity will be removed before the callback is triggered. 75 | Self::remove_finished_requests, 76 | Self::poll_inflight_requests_to_bytes, 77 | ) 78 | .chain() 79 | .in_set(ReqwestSet), 80 | ); 81 | } 82 | } 83 | 84 | //TODO: Make type generic, and we can create systems for JSON and TEXT requests 85 | impl ReqwestPlugin { 86 | /// despawns finished reqwests if marked to be despawned and does not contain 'ReqwestInflight' component 87 | fn remove_finished_requests( 88 | mut commands: Commands, 89 | q: Query, Without)>, 90 | ) { 91 | for e in q.iter() { 92 | if let Ok(mut ec) = commands.get_entity(e) { 93 | ec.despawn(); 94 | } 95 | } 96 | } 97 | 98 | /// Polls any requests in flight to completion, and then removes the 'ReqwestInflight' component. 99 | fn poll_inflight_requests_to_bytes( 100 | mut commands: Commands, 101 | mut requests: Query<(Entity, &mut ReqwestInflight)>, 102 | ) { 103 | for (entity, mut request) in requests.iter_mut() { 104 | debug!("polling: {entity:?}"); 105 | if let Some((result, parts)) = request.poll() { 106 | match result { 107 | Ok(body) => { 108 | // if the response is ok, the other values are already gotten, its safe to unwrap 109 | let parts = parts.unwrap(); 110 | 111 | commands.trigger_targets( 112 | ReqwestResponseEvent::new(body.clone(), parts.status, parts.headers), 113 | entity.clone(), 114 | ); 115 | } 116 | Err(err) => { 117 | commands.trigger_targets(ReqwestErrorEvent(err), entity.clone()); 118 | } 119 | } 120 | if let Ok(mut ec) = commands.get_entity(entity) { 121 | ec.remove::(); 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | /// Wrapper around EntityCommands to create the on_response and on_error 129 | pub struct BevyReqwestBuilder<'a>(EntityCommands<'a>); 130 | 131 | impl<'a> BevyReqwestBuilder<'a> { 132 | /// Provide a system where the first argument is [`Trigger`] [`ReqwestResponseEvent`] that will run on the 133 | /// response from the http request 134 | /// 135 | /// # Examples 136 | /// 137 | /// ``` 138 | /// use bevy::prelude::Trigger; 139 | /// use bevy_mod_reqwest::ReqwestResponseEvent; 140 | /// |trigger: Trigger| { 141 | /// bevy::log::info!("response: {:?}", trigger.event()); 142 | /// }; 143 | /// ``` 144 | pub fn on_response>( 145 | mut self, 146 | onresponse: OR, 147 | ) -> Self { 148 | self.0.observe(onresponse); 149 | self 150 | } 151 | 152 | /// Provide a system where the first argument is [`Trigger`] [`JsonResponse`] that will run on the 153 | /// response from the http request, skipping some boilerplate of having to manually doing the JSON 154 | /// parsing 155 | /// 156 | /// # Examples 157 | /// ``` 158 | /// use bevy::prelude::Trigger; 159 | /// use bevy_mod_reqwest::JsonResponse; 160 | /// |trigger: Trigger>| { 161 | /// bevy::log::info!("response: {:?}", trigger.event()); 162 | /// }; 163 | /// ``` 164 | #[cfg(feature = "json")] 165 | pub fn on_json_response< 166 | T: std::marker::Sync + std::marker::Send + serde::de::DeserializeOwned + 'static, 167 | RB: Bundle, 168 | RM, 169 | OR: IntoObserverSystem, RB, RM>, 170 | >( 171 | mut self, 172 | onresponse: OR, 173 | ) -> Self { 174 | self.0.observe( 175 | |evt: Trigger, mut commands: Commands| { 176 | let entity = evt.target(); 177 | let evt = evt.event(); 178 | let data = evt.deserialize_json::(); 179 | 180 | match data { 181 | Ok(data) => { 182 | // retrigger a new event with the serialized data 183 | commands.trigger_targets(json::JsonResponse(data), entity); 184 | } 185 | Err(e) => { 186 | bevy::log::error!("deserialization error: {e}"); 187 | bevy::log::debug!( 188 | "tried serializing: {}", 189 | evt.as_str().unwrap_or("failed getting event data") 190 | ); 191 | } 192 | } 193 | }, 194 | ); 195 | self.0.observe(onresponse); 196 | self 197 | } 198 | 199 | /// Provide a system where the first argument is [`Trigger`] [`ReqwestErrorEvent`] that will run on the 200 | /// response from the http request 201 | /// 202 | /// # Examples 203 | /// 204 | /// ``` 205 | /// use bevy::prelude::Trigger; 206 | /// use bevy_mod_reqwest::ReqwestErrorEvent; 207 | /// |trigger: Trigger| { 208 | /// bevy::log::info!("response: {:?}", trigger.event()); 209 | /// }; 210 | /// ``` 211 | pub fn on_error>( 212 | mut self, 213 | onerror: OE, 214 | ) -> Self { 215 | self.0.observe(onerror); 216 | self 217 | } 218 | } 219 | 220 | #[derive(SystemParam)] 221 | /// Systemparam to have a shorthand for creating http calls in systems 222 | pub struct BevyReqwest<'w, 's> { 223 | commands: Commands<'w, 's>, 224 | client: Res<'w, ReqwestClient>, 225 | } 226 | 227 | impl<'w, 's> BevyReqwest<'w, 's> { 228 | /// Starts sending and processing the supplied [`reqwest::Request`] 229 | /// then use the [`BevyReqwestBuilder`] to add handlers for responses and errors 230 | pub fn send(&mut self, req: reqwest::Request) -> BevyReqwestBuilder { 231 | let inflight = self.create_inflight_task(req); 232 | BevyReqwestBuilder(self.commands.spawn((inflight, DespawnReqwestEntity))) 233 | } 234 | 235 | /// Starts sending and processing the supplied [`reqwest::Request`] on the supplied [`Entity`] if it exists 236 | /// and then use the [`BevyReqwestBuilder`] to add handlers for responses and errors 237 | pub fn send_using_entity( 238 | &mut self, 239 | entity: Entity, 240 | req: reqwest::Request, 241 | ) -> Result> { 242 | let inflight = self.create_inflight_task(req); 243 | let mut ec = self.commands.get_entity(entity)?; 244 | info!("inserting request on entity: {:?}", entity); 245 | ec.insert(inflight); 246 | Ok(BevyReqwestBuilder(ec)) 247 | } 248 | 249 | /// get access to the underlying ReqwestClient 250 | pub fn client(&self) -> &reqwest::Client { 251 | &self.client.0 252 | } 253 | 254 | fn create_inflight_task(&self, request: reqwest::Request) -> ReqwestInflight { 255 | let thread_pool = IoTaskPool::get(); 256 | // bevy::log::debug!("Creating: {entity:?}"); 257 | // if we take the data, we can use it 258 | let client = self.client.0.clone(); 259 | let url = request.url().to_string(); 260 | 261 | // wasm implementation 262 | #[cfg(target_family = "wasm")] 263 | let task = { 264 | let (tx, task) = bounded(1); 265 | thread_pool 266 | .spawn(async move { 267 | let r = client.execute(request).await; 268 | let r = match r { 269 | Ok(res) => { 270 | let parts = Parts { 271 | status: res.status(), 272 | headers: res.headers().clone(), 273 | }; 274 | (res.bytes().await, Some(parts)) 275 | } 276 | Err(r) => (Err(r), None), 277 | }; 278 | tx.send(r).ok(); 279 | }) 280 | .detach(); 281 | task 282 | }; 283 | 284 | // otherwise 285 | #[cfg(not(target_family = "wasm"))] 286 | let task = { 287 | thread_pool.spawn(async move { 288 | let task_res = async_compat::Compat::new(async { 289 | let p = client.execute(request).await; 290 | match p { 291 | Ok(res) => { 292 | let parts = Parts { 293 | status: res.status(), 294 | headers: res.headers().clone(), 295 | }; 296 | (res.bytes().await, Some(parts)) 297 | } 298 | Err(e) => (Err(e), None), 299 | } 300 | }) 301 | .await; 302 | task_res 303 | }) 304 | }; 305 | // put it as a component to be polled, and remove the request, it has been handled 306 | ReqwestInflight::new(task, url) 307 | } 308 | } 309 | 310 | impl<'w, 's> Deref for BevyReqwest<'w, 's> { 311 | type Target = reqwest::Client; 312 | 313 | fn deref(&self) -> &Self::Target { 314 | self.client() 315 | } 316 | } 317 | 318 | #[derive(Component)] 319 | /// Marker component that is used to despawn an entity if the reqwest is finshed 320 | pub struct DespawnReqwestEntity; 321 | 322 | #[derive(Resource)] 323 | /// Wrapper around the ReqwestClient, that when inserted as a resource will start connection pools towards 324 | /// the hosts, and also allows all the configuration from the ReqwestLibrary such as setting default headers etc 325 | /// to be used inside the bevy application 326 | pub struct ReqwestClient(pub reqwest::Client); 327 | impl Default for ReqwestClient { 328 | fn default() -> Self { 329 | Self(reqwest::Client::new()) 330 | } 331 | } 332 | 333 | impl std::ops::Deref for ReqwestClient { 334 | type Target = reqwest::Client; 335 | fn deref(&self) -> &Self::Target { 336 | &self.0 337 | } 338 | } 339 | impl DerefMut for ReqwestClient { 340 | fn deref_mut(&mut self) -> &mut Self::Target { 341 | &mut self.0 342 | } 343 | } 344 | 345 | type Resp = (reqwest::Result, Option); 346 | 347 | /// Dont touch these, its just to poll once every request, can be used to detect if there is an active request on the entity 348 | /// but should otherwise NOT be added/removed/changed by a user of this Crate 349 | #[derive(Component)] 350 | #[component(storage = "SparseSet")] 351 | pub struct ReqwestInflight { 352 | // the url this request is handling as a string 353 | pub(crate) url: String, 354 | #[cfg(not(target_family = "wasm"))] 355 | res: Task, 356 | 357 | #[cfg(target_family = "wasm")] 358 | res: Receiver, 359 | } 360 | 361 | impl ReqwestInflight { 362 | fn poll(&mut self) -> Option { 363 | #[cfg(target_family = "wasm")] 364 | if let Ok(v) = self.res.try_recv() { 365 | Some(v) 366 | } else { 367 | None 368 | } 369 | 370 | #[cfg(not(target_family = "wasm"))] 371 | if let Some(v) = future::block_on(future::poll_once(&mut self.res)) { 372 | Some(v) 373 | } else { 374 | None 375 | } 376 | } 377 | 378 | #[cfg(target_family = "wasm")] 379 | pub(crate) fn new(res: Receiver, url: String) -> Self { 380 | Self { url, res } 381 | } 382 | 383 | #[cfg(not(target_family = "wasm"))] 384 | pub(crate) fn new(res: Task, url: String) -> Self { 385 | Self { url, res } 386 | } 387 | } 388 | 389 | #[derive(Component, Debug)] 390 | /// information about the response used to transfer headers between different stages in the async code 391 | struct Parts { 392 | /// the `StatusCode` 393 | pub(crate) status: StatusCode, 394 | 395 | /// the headers of the response 396 | pub(crate) headers: HeaderMap, 397 | } 398 | 399 | #[derive(Clone, Event, Debug)] 400 | /// the resulting data from a finished request is found here 401 | pub struct ReqwestResponseEvent { 402 | bytes: bytes::Bytes, 403 | status: StatusCode, 404 | headers: HeaderMap, 405 | } 406 | 407 | #[derive(Event, Debug)] 408 | pub struct ReqwestErrorEvent(pub reqwest::Error); 409 | 410 | impl ReqwestResponseEvent { 411 | /// retrieve a reference to the body of the response 412 | #[inline] 413 | pub fn body(&self) -> &bytes::Bytes { 414 | &self.bytes 415 | } 416 | 417 | /// try to get the body of the response as_str 418 | pub fn as_str(&self) -> anyhow::Result<&str> { 419 | let s = std::str::from_utf8(&self.bytes)?; 420 | Ok(s) 421 | } 422 | /// try to get the body of the response as an owned string 423 | pub fn as_string(&self) -> anyhow::Result { 424 | Ok(self.as_str()?.to_string()) 425 | } 426 | #[cfg(feature = "json")] 427 | /// try to deserialize the body of the response using json 428 | pub fn deserialize_json<'de, T: serde::Deserialize<'de>>(&'de self) -> anyhow::Result { 429 | Ok(serde_json::from_str(self.as_str()?)?) 430 | } 431 | 432 | #[cfg(feature = "msgpack")] 433 | /// try to deserialize the body of the response using msgpack 434 | pub fn deserialize_msgpack<'de, T: serde::Deserialize<'de>>(&'de self) -> anyhow::Result { 435 | Ok(rmp_serde::decode::from_slice(self.body())?) 436 | } 437 | #[inline] 438 | /// Get the `StatusCode` of this `Response`. 439 | pub fn status(&self) -> StatusCode { 440 | self.status 441 | } 442 | 443 | #[inline] 444 | /// Get the `Headers` of this `Response`. 445 | pub fn response_headers(&self) -> &HeaderMap { 446 | &self.headers 447 | } 448 | } 449 | 450 | #[cfg(feature = "json")] 451 | pub mod json { 452 | use bevy::prelude::Event; 453 | use serde::Deserialize; 454 | #[derive(Deserialize, Event)] 455 | pub struct JsonResponse(pub T); 456 | } 457 | 458 | impl ReqwestResponseEvent { 459 | pub(crate) fn new(bytes: bytes::Bytes, status: StatusCode, headers: HeaderMap) -> Self { 460 | Self { 461 | bytes, 462 | status, 463 | headers, 464 | } 465 | } 466 | } 467 | --------------------------------------------------------------------------------