bevy_net_backend 0.2.0

Talk to your game's own backend from Bevy: HTTPS JSON requests, uploads and downloads to a file, (feature `ws`) named WebSocket connections, (feature `oauth`) OpenID Connect desktop sign-in and (feature `ssh`, admin tools) SSH commands and SFTP, typed answers as Bevy messages, exactly one answer per request, game-supplied credentials with redacted secrets, proxy and certificate settings, no tokio unless you enable `ssh`, and fake transports for tests.
Documentation
//! The seam between the plugin and the network: the [`HttpTransport`] trait, the
//! [`HttpTransportRes`] resource, the [`FakeHttpTransport`](crate::FakeHttpTransport) (always compiled) and, with feature
//! `http`, the real `UreqTransport`.

use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};

use bevy_ecs::resource::Resource;

use crate::request::{PreparedRequest, RequestId};
use crate::response::{BackendError, RawResponse};

pub(crate) mod fake;
#[cfg(feature = "http")]
pub(crate) mod http_pool;
#[cfg(feature = "http")]
pub(crate) mod tls_connector;

/// What a transport reports for one request: the server's answer (any status; the plugin turns
/// a non-2xx status into [`BackendError::Status`]) or the error that stopped it.
pub type HttpTransportResult = Result<RawResponse, BackendError>;

/// Moves prepared requests to a server and results back. The plugin owns the bookkeeping
/// (pending requests, deadlines, cancel, exit); a transport only has to deliver.
///
/// Rules for an implementation:
/// - [`submit`](Self::submit) and [`poll`](Self::poll) run on the main thread in the schedule:
///   they must never block.
/// - Report each submitted request at most once. A result for an id the plugin already answered
///   (cancelled, timed out) is discarded, so reporting late is harmless.
/// - Never panic.
///
/// **Compatibility rule:** methods are only ever added to this trait with a
/// default implementation.
pub trait HttpTransport: Send + Sync + 'static {
    /// Start `request`. Called in `PostUpdate` ([`BackendSystems::Send`](crate::BackendSystems::Send)).
    fn submit(&mut self, id: RequestId, request: PreparedRequest);

    /// Every result that arrived since the last call. Called once per frame in `First`
    /// ([`BackendSystems::Receive`](crate::BackendSystems::Receive)).
    fn poll(&mut self) -> Vec<(RequestId, HttpTransportResult)>;

    /// The plugin answered `id` without the transport (cancelled or timed out); drop it if it has
    /// not started. Default: nothing.
    fn cancel(&mut self, id: RequestId) {
        let _ = id;
    }

    /// The plugin wants to answer `id` without the transport (a cancel, or its deadline passed).
    /// Return `true` when that is fine (as [`cancel`](Self::cancel)), `false` when it is too late:
    /// the request already completed in a way the answer must report (a download whose file was
    /// already put in place) and its result follows from [`poll`](Self::poll); the plugin then
    /// keeps waiting for that result. The plugin calls this instead of `cancel`. Default: calls
    /// `cancel`, `true`.
    fn try_cancel(&mut self, id: RequestId) -> bool {
        self.cancel(id);
        true
    }

    /// The app is exiting. Called in `Last` before the plugin answers what is still open:
    /// results [`poll`](Self::poll) returns afterwards are delivered as they are, the rest is
    /// answered `Shutdown`. Release threads and connections; never wait for a network timeout
    /// (the built-in transport waits at most 1 s for running downloads to remove their part
    /// files). Default: nothing.
    fn shutdown(&mut self) {}

    /// Whether this transport sends a [`PreparedRequest::streaming_body`] (a multipart form with
    /// files from disk). Default `false`: the plugin then answers such a request `InvalidRequest`
    /// and never hands it over.
    fn streams_bodies(&self) -> bool {
        false
    }

    /// Upload progress since the last call: `(request, bytes sent, total)`, for requests with
    /// [`PreparedRequest::upload_progress`]. Called once per frame in `First`, before
    /// [`poll`](Self::poll). Default: none.
    fn poll_progress(&mut self) -> Vec<(RequestId, u64, Option<u64>)> {
        Vec::new()
    }

    /// Whether this transport writes a [`PreparedRequest::download`] to its file (with
    /// [`HttpDownload::receive`](crate::HttpDownload) for a 2xx answer, the result in
    /// [`RawResponse::file`](crate::RawResponse::file)). Default `false`: the plugin then answers
    /// such a request `InvalidRequest` and never hands it over.
    fn downloads_to_files(&self) -> bool {
        false
    }

    /// Download progress since the last call: `(request, bytes written, total)`, for requests
    /// whose [`PreparedRequest::download`] has progress on. Called once per frame in `First`,
    /// before [`poll`](Self::poll). Default: none.
    fn poll_download_progress(&mut self) -> Vec<(RequestId, u64, Option<u64>)> {
        Vec::new()
    }
}

static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);

/// The installed transport. The plugin inserts the HTTP transport (feature `http`) when the app
/// has none; insert your own (for example a [`FakeHttpTransport`](crate::FakeHttpTransport)) to replace it.
///
/// Without this resource every request is answered with [`BackendError::NoTransport`]. Requests
/// still waiting on a transport that is removed or replaced are answered the same way.
#[derive(Resource)]
pub struct HttpTransportRes {
    inner: Box<dyn HttpTransport>,
    generation: u64,
}

impl fmt::Debug for HttpTransportRes {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("HttpTransportRes").field("generation", &self.generation).finish_non_exhaustive()
    }
}

impl HttpTransportRes {
    /// Wrap a transport.
    pub fn new(transport: impl HttpTransport) -> Self {
        Self { inner: Box::new(transport), generation: NEXT_GENERATION.fetch_add(1, Ordering::Relaxed) }
    }

    pub(crate) fn generation(&self) -> u64 {
        self.generation
    }

    pub(crate) fn get(&self) -> &dyn HttpTransport {
        self.inner.as_ref()
    }

    pub(crate) fn get_mut(&mut self) -> &mut dyn HttpTransport {
        self.inner.as_mut()
    }
}