-
Notifications
You must be signed in to change notification settings - Fork 226
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This shows off a potential viaduct replacement that uses new UniFFI features. Check out `components/fairy-bridge/README.md` and `examples/fairy-bridge-demo/README.md` for details. Execute `examples/fairy-bridge-demo/run-demo.py` to test it out yourself. The UniFFI features are still a WIP. This is currently using a branch in my repo. The current plan for getting these into UniFFI main is: - Get the `0.26.0` release out the door - Merge PR #1818 into `main` - Merge my `async-trait-interfaces` branch into main (probably using a few smaller PRs) The Desktop plan needs to be explored more. I believe there should be a way to use Necko in Rust code, but that needs to be verified.
- Loading branch information
Showing
15 changed files
with
950 additions
and
204 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[package] | ||
name = "fairy-bridge" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[features] | ||
default = [] | ||
backend-reqwest = ["dep:reqwest"] | ||
|
||
[dependencies] | ||
async-trait = "0.1" | ||
pollster = "0.3.0" | ||
serde = "1" | ||
serde_json = "1" | ||
thiserror = "1" | ||
tokio = { version = "1", features = ["rt-multi-thread"] } | ||
uniffi = { workspace = true } | ||
url = "2.2" | ||
reqwest = { version = "0.11.23", optional = true } | ||
|
||
[build-dependencies] | ||
uniffi = { workspace = true, features = ["build"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Fairy Bridge | ||
|
||
Fairy Bridge is an HTTP request bridge library that allows requests to be made using various | ||
backends, including: | ||
|
||
- The builtin reqwest backend | ||
- Custom Rust backends | ||
- Custom backends written in the foreign language | ||
|
||
The plan for this is: | ||
- iOS will use the reqwest backend | ||
- Android will use a custom backend in Kotlin using fetch | ||
(https://github.com/mozilla-mobile/firefox-android/tree/35ce01367157440f9e9daa4ed48a8022af80c8f2/android-components/components/concept/fetch) | ||
- Desktop will use a custom backend in Rust that hooks into necko | ||
|
||
## Sync / Async | ||
|
||
The backends are implemented using async code, but there's also the option to block on a request. | ||
This means `fairy-bridge` can be used in both sync and async contexts. | ||
|
||
## Cookies / State | ||
|
||
Cookies and state are outside the scope of this library. Any such functionality is the responsibility of the consumer. | ||
|
||
## Name | ||
|
||
`fairy-bridge` is named after the Fairy Bridge (Xian Ren Qiao) -- the largest known natural bridge in the world, located in northwestern Guangxi Province, China. | ||
|
||
![Picture of the Fairy Bridge](http://www.naturalarches.org/big9_files/FairyBridge1680.jpg) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
use crate::{FairyBridgeError, Request, Response}; | ||
use std::sync::Arc; | ||
|
||
/// Settings for a backend instance | ||
/// | ||
/// Backend constructions should input this in order to configure themselves | ||
#[derive(Debug, uniffi::Record)] | ||
pub struct BackendSettings { | ||
// Connection timeout (in ms) | ||
#[uniffi(default = None)] | ||
pub connect_timeout: Option<u32>, | ||
// Timeout for the entire request (in ms) | ||
#[uniffi(default = None)] | ||
pub timeout: Option<u32>, | ||
// Maximum amount of redirects to follow (0 means redirects are not allowed) | ||
#[uniffi(default = 10)] | ||
pub redirect_limit: u32, | ||
} | ||
|
||
#[uniffi::export(with_callback_interface)] | ||
#[async_trait::async_trait] | ||
pub trait Backend: Send + Sync { | ||
async fn send_request(self: Arc<Self>, request: Request) -> Result<Response, FairyBridgeError>; | ||
} | ||
|
||
#[uniffi::export] | ||
pub fn init_backend(backend: Arc<dyn Backend>) -> Result<(), FairyBridgeError> { | ||
crate::REGISTERED_BACKEND | ||
.set(backend) | ||
.map_err(|_| FairyBridgeError::BackendAlreadyInitialized) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
pub type Result<T> = std::result::Result<T, FairyBridgeError>; | ||
|
||
#[derive(Debug, thiserror::Error, uniffi::Error)] | ||
pub enum FairyBridgeError { | ||
#[error("BackendAlreadyInitialized")] | ||
BackendAlreadyInitialized, | ||
#[error("NoBackendInitialized")] | ||
NoBackendInitialized, | ||
#[error("BackendError({msg})")] | ||
BackendError { msg: String }, | ||
#[error("HttpError({code})")] | ||
HttpError { code: u16 }, | ||
#[error("InvalidRequestHeader({name})")] | ||
InvalidRequestHeader { name: String }, | ||
#[error("InvalidResponseHeader({name})")] | ||
InvalidResponseHeader { name: String }, | ||
#[error("SerializationError({msg})")] | ||
SerializationError { msg: String }, | ||
} | ||
|
||
impl From<serde_json::Error> for FairyBridgeError { | ||
fn from(e: serde_json::Error) -> Self { | ||
Self::SerializationError { msg: e.to_string() } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
use crate::FairyBridgeError; | ||
use std::borrow::Cow; | ||
|
||
/// Normalize / validate a request header | ||
/// | ||
/// This accepts both &str and String. It either returns the lowercase version or | ||
/// `FairyBridgeError::InvalidRequestHeader` | ||
pub fn normalize_request_header<'a>(name: impl Into<Cow<'a, str>>) -> crate::Result<String> { | ||
do_normalize_header(name).map_err(|name| FairyBridgeError::InvalidRequestHeader { name }) | ||
} | ||
|
||
/// Normalize / validate a response header | ||
/// | ||
/// This accepts both &str and String. It either returns the lowercase version or | ||
/// `FairyBridgeError::InvalidRequestHeader` | ||
pub fn normalize_response_header<'a>(name: impl Into<Cow<'a, str>>) -> crate::Result<String> { | ||
do_normalize_header(name).map_err(|name| FairyBridgeError::InvalidResponseHeader { name }) | ||
} | ||
|
||
fn do_normalize_header<'a>(name: impl Into<Cow<'a, str>>) -> Result<String, String> { | ||
// Note: 0 = invalid, 1 = valid, 2 = valid but needs lowercasing. I'd use an | ||
// enum for this, but it would make this LUT *way* harder to look at. This | ||
// includes 0-9, a-z, A-Z (as 2), and ('!' | '#' | '$' | '%' | '&' | '\'' | '*' | ||
// | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~'), matching the field-name | ||
// token production defined at https://tools.ietf.org/html/rfc7230#section-3.2. | ||
static VALID_HEADER_LUT: [u8; 256] = [ | ||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, | ||
0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, | ||
2, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, | ||
1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
]; | ||
|
||
let mut name = name.into(); | ||
|
||
if name.len() == 0 { | ||
return Err(name.to_string()); | ||
} | ||
let mut need_lower_case = false; | ||
for b in name.bytes() { | ||
let validity = VALID_HEADER_LUT[b as usize]; | ||
if validity == 0 { | ||
return Err(name.to_string()); | ||
} | ||
if validity == 2 { | ||
need_lower_case = true; | ||
} | ||
} | ||
if need_lower_case { | ||
// Only do this if needed, since it causes us to own the header. | ||
name.to_mut().make_ascii_lowercase(); | ||
} | ||
Ok(name.to_string()) | ||
} | ||
|
||
// Default headers for easy usage | ||
pub const ACCEPT_ENCODING: &str = "accept-encoding"; | ||
pub const ACCEPT: &str = "accept"; | ||
pub const AUTHORIZATION: &str = "authorization"; | ||
pub const CONTENT_TYPE: &str = "content-type"; | ||
pub const ETAG: &str = "etag"; | ||
pub const IF_NONE_MATCH: &str = "if-none-match"; | ||
pub const USER_AGENT: &str = "user-agent"; | ||
// non-standard, but it's convenient to have these. | ||
pub const RETRY_AFTER: &str = "retry-after"; | ||
pub const X_IF_UNMODIFIED_SINCE: &str = "x-if-unmodified-since"; | ||
pub const X_KEYID: &str = "x-keyid"; | ||
pub const X_LAST_MODIFIED: &str = "x-last-modified"; | ||
pub const X_TIMESTAMP: &str = "x-timestamp"; | ||
pub const X_WEAVE_NEXT_OFFSET: &str = "x-weave-next-offset"; | ||
pub const X_WEAVE_RECORDS: &str = "x-weave-records"; | ||
pub const X_WEAVE_TIMESTAMP: &str = "x-weave-timestamp"; | ||
pub const X_WEAVE_BACKOFF: &str = "x-weave-backoff"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
use std::{ | ||
collections::HashMap, | ||
sync::{Arc, OnceLock}, | ||
}; | ||
|
||
mod backend; | ||
mod error; | ||
pub mod headers; | ||
mod request; | ||
#[cfg(feature = "backend-reqwest")] | ||
mod reqwest_backend; | ||
|
||
pub use backend::*; | ||
pub use error::*; | ||
pub use request::*; | ||
#[cfg(feature = "backend-reqwest")] | ||
pub use reqwest_backend::*; | ||
|
||
static REGISTERED_BACKEND: OnceLock<Arc<dyn Backend>> = OnceLock::new(); | ||
|
||
#[derive(uniffi::Record)] | ||
pub struct Response { | ||
pub url: String, | ||
pub status: u16, | ||
pub headers: HashMap<String, String>, | ||
pub body: Vec<u8>, | ||
} | ||
|
||
uniffi::setup_scaffolding!(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
use crate::{headers, FairyBridgeError, Response, Result}; | ||
use pollster::FutureExt; | ||
use std::borrow::Cow; | ||
use std::collections::HashMap; | ||
use url::Url; | ||
|
||
#[derive(uniffi::Enum)] | ||
pub enum Method { | ||
Get, | ||
Head, | ||
Post, | ||
Put, | ||
Delete, | ||
Connect, | ||
Options, | ||
Trace, | ||
Patch, | ||
} | ||
|
||
#[derive(uniffi::Record)] | ||
pub struct Request { | ||
pub method: Method, | ||
pub url: String, | ||
pub headers: HashMap<String, String>, | ||
pub body: Option<Vec<u8>>, | ||
} | ||
|
||
/// Http request | ||
/// | ||
/// These are created using the builder pattern, then sent over the network using the `send()` | ||
/// method. | ||
impl Request { | ||
pub fn new(method: Method, url: Url) -> Self { | ||
Self { | ||
method, | ||
url: url.to_string(), | ||
headers: HashMap::new(), | ||
body: None, | ||
} | ||
} | ||
|
||
pub async fn send(self) -> crate::Result<Response> { | ||
let mut response = match crate::REGISTERED_BACKEND.get() { | ||
Some(backend) => backend.clone().send_request(self).await, | ||
None => Err(FairyBridgeError::NoBackendInitialized), | ||
}?; | ||
response.headers = response | ||
.headers | ||
.into_iter() | ||
.map(|(name, value)| Ok((headers::normalize_request_header(name)?, value))) | ||
.collect::<crate::Result<HashMap<_, _>>>()?; | ||
Ok(response) | ||
} | ||
|
||
pub fn send_sync(self) -> crate::Result<Response> { | ||
self.send().block_on() | ||
} | ||
|
||
/// Alias for `Request::new(Method::Get, url)`, for convenience. | ||
pub fn get(url: Url) -> Self { | ||
Self::new(Method::Get, url) | ||
} | ||
|
||
/// Alias for `Request::new(Method::Patch, url)`, for convenience. | ||
pub fn patch(url: Url) -> Self { | ||
Self::new(Method::Patch, url) | ||
} | ||
|
||
/// Alias for `Request::new(Method::Post, url)`, for convenience. | ||
pub fn post(url: Url) -> Self { | ||
Self::new(Method::Post, url) | ||
} | ||
|
||
/// Alias for `Request::new(Method::Put, url)`, for convenience. | ||
pub fn put(url: Url) -> Self { | ||
Self::new(Method::Put, url) | ||
} | ||
|
||
/// Alias for `Request::new(Method::Delete, url)`, for convenience. | ||
pub fn delete(url: Url) -> Self { | ||
Self::new(Method::Delete, url) | ||
} | ||
|
||
/// Add all the provided headers to the list of headers to send with this | ||
/// request. | ||
pub fn headers<'a, I, K, V>(mut self, to_add: I) -> crate::Result<Self> | ||
where | ||
I: IntoIterator<Item = (K, V)>, | ||
K: Into<Cow<'a, str>>, | ||
V: Into<String>, | ||
{ | ||
for (name, value) in to_add { | ||
self = self.header(name, value)? | ||
} | ||
Ok(self) | ||
} | ||
|
||
/// Add the provided header to the list of headers to send with this request. | ||
/// | ||
/// This returns `Err` if `val` contains characters that may not appear in | ||
/// the body of a header. | ||
/// | ||
/// ## Example | ||
/// ``` | ||
/// # use fairy_bridge::{Request, headers}; | ||
/// # use url::Url; | ||
/// # fn main() -> fairy_bridge::Result<()> { | ||
/// # let some_url = url::Url::parse("https://www.example.com").unwrap(); | ||
/// Request::post(some_url) | ||
/// .header(headers::CONTENT_TYPE, "application/json")? | ||
/// .header("My-Header", "Some special value")?; | ||
/// // ... | ||
/// # Ok(()) | ||
/// # } | ||
/// ``` | ||
pub fn header<'a>( | ||
mut self, | ||
name: impl Into<Cow<'a, str>>, | ||
val: impl Into<String>, | ||
) -> crate::Result<Self> { | ||
self.headers | ||
.insert(headers::normalize_request_header(name)?, val.into()); | ||
Ok(self) | ||
} | ||
|
||
/// Set this request's body. | ||
pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self { | ||
self.body = Some(body.into()); | ||
self | ||
} | ||
|
||
/// Set body to a json-serialized value and the the Content-Type header to "application/json". | ||
/// | ||
/// Returns an [crate::Error::SerializationError] if there was there was an error serializing the data. | ||
pub fn json(mut self, val: &(impl serde::Serialize + ?Sized)) -> Result<Self> { | ||
self.body = Some(serde_json::to_vec(val)?); | ||
self.headers.insert( | ||
headers::CONTENT_TYPE.to_owned(), | ||
"application/json".to_owned(), | ||
); | ||
Ok(self) | ||
} | ||
} |
Oops, something went wrong.