Skip to content

Commit e83357a

Browse files
committed
Fairy bridge demo
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.
1 parent 813beb7 commit e83357a

File tree

15 files changed

+950
-204
lines changed

15 files changed

+950
-204
lines changed

Cargo.lock

Lines changed: 126 additions & 203 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"components/as-ohttp-client",
77
"components/autofill",
88
"components/crashtest",
9+
"components/fairy-bridge",
910
"components/fxa-client",
1011
"components/logins",
1112
"components/nimbus",
@@ -122,7 +123,7 @@ default-members = [
122123
[workspace.dependencies]
123124
rusqlite = "0.30.0"
124125
libsqlite3-sys = "0.27.0"
125-
uniffi = "0.25.2"
126+
uniffi = { version = "0.25.3", git = "https://github.com/bendk/uniffi-rs.git", branch = "async-trait-interfaces" }
126127

127128
[profile.release]
128129
opt-level = "s"

components/fairy-bridge/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "fairy-bridge"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[features]
7+
default = []
8+
backend-reqwest = ["dep:reqwest"]
9+
10+
[dependencies]
11+
async-trait = "0.1"
12+
pollster = "0.3.0"
13+
serde = "1"
14+
serde_json = "1"
15+
thiserror = "1"
16+
tokio = { version = "1", features = ["rt-multi-thread"] }
17+
uniffi = { workspace = true }
18+
url = "2.2"
19+
reqwest = { version = "0.11.23", optional = true }
20+
21+
[build-dependencies]
22+
uniffi = { workspace = true, features = ["build"] }

components/fairy-bridge/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Fairy Bridge
2+
3+
Fairy Bridge is an HTTP request bridge library that allows requests to be made using various
4+
backends, including:
5+
6+
- The builtin reqwest backend
7+
- Custom Rust backends
8+
- Custom backends written in the foreign language
9+
10+
The plan for this is:
11+
- iOS will use the reqwest backend
12+
- Android will use a custom backend in Kotlin using fetch
13+
(https://github.com/mozilla-mobile/firefox-android/tree/35ce01367157440f9e9daa4ed48a8022af80c8f2/android-components/components/concept/fetch)
14+
- Desktop will use a custom backend in Rust that hooks into necko
15+
16+
## Sync / Async
17+
18+
The backends are implemented using async code, but there's also the option to block on a request.
19+
This means `fairy-bridge` can be used in both sync and async contexts.
20+
21+
## Cookies / State
22+
23+
Cookies and state are outside the scope of this library. Any such functionality is the responsibility of the consumer.
24+
25+
## Name
26+
27+
`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.
28+
29+
![Picture of the Fairy Bridge](http://www.naturalarches.org/big9_files/FairyBridge1680.jpg)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
use crate::{FairyBridgeError, Request, Response};
6+
use std::sync::Arc;
7+
8+
/// Settings for a backend instance
9+
///
10+
/// Backend constructions should input this in order to configure themselves
11+
#[derive(Debug, uniffi::Record)]
12+
pub struct BackendSettings {
13+
// Connection timeout (in ms)
14+
#[uniffi(default = None)]
15+
pub connect_timeout: Option<u32>,
16+
// Timeout for the entire request (in ms)
17+
#[uniffi(default = None)]
18+
pub timeout: Option<u32>,
19+
// Maximum amount of redirects to follow (0 means redirects are not allowed)
20+
#[uniffi(default = 10)]
21+
pub redirect_limit: u32,
22+
}
23+
24+
#[uniffi::export(with_callback_interface)]
25+
#[async_trait::async_trait]
26+
pub trait Backend: Send + Sync {
27+
async fn send_request(self: Arc<Self>, request: Request) -> Result<Response, FairyBridgeError>;
28+
}
29+
30+
#[uniffi::export]
31+
pub fn init_backend(backend: Arc<dyn Backend>) -> Result<(), FairyBridgeError> {
32+
crate::REGISTERED_BACKEND
33+
.set(backend)
34+
.map_err(|_| FairyBridgeError::BackendAlreadyInitialized)
35+
}

components/fairy-bridge/src/error.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
pub type Result<T> = std::result::Result<T, FairyBridgeError>;
6+
7+
#[derive(Debug, thiserror::Error, uniffi::Error)]
8+
pub enum FairyBridgeError {
9+
#[error("BackendAlreadyInitialized")]
10+
BackendAlreadyInitialized,
11+
#[error("NoBackendInitialized")]
12+
NoBackendInitialized,
13+
#[error("BackendError({msg})")]
14+
BackendError { msg: String },
15+
#[error("HttpError({code})")]
16+
HttpError { code: u16 },
17+
#[error("InvalidRequestHeader({name})")]
18+
InvalidRequestHeader { name: String },
19+
#[error("InvalidResponseHeader({name})")]
20+
InvalidResponseHeader { name: String },
21+
#[error("SerializationError({msg})")]
22+
SerializationError { msg: String },
23+
}
24+
25+
impl From<serde_json::Error> for FairyBridgeError {
26+
fn from(e: serde_json::Error) -> Self {
27+
Self::SerializationError { msg: e.to_string() }
28+
}
29+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
use crate::FairyBridgeError;
6+
use std::borrow::Cow;
7+
8+
/// Normalize / validate a request header
9+
///
10+
/// This accepts both &str and String. It either returns the lowercase version or
11+
/// `FairyBridgeError::InvalidRequestHeader`
12+
pub fn normalize_request_header<'a>(name: impl Into<Cow<'a, str>>) -> crate::Result<String> {
13+
do_normalize_header(name).map_err(|name| FairyBridgeError::InvalidRequestHeader { name })
14+
}
15+
16+
/// Normalize / validate a response header
17+
///
18+
/// This accepts both &str and String. It either returns the lowercase version or
19+
/// `FairyBridgeError::InvalidRequestHeader`
20+
pub fn normalize_response_header<'a>(name: impl Into<Cow<'a, str>>) -> crate::Result<String> {
21+
do_normalize_header(name).map_err(|name| FairyBridgeError::InvalidResponseHeader { name })
22+
}
23+
24+
fn do_normalize_header<'a>(name: impl Into<Cow<'a, str>>) -> Result<String, String> {
25+
// Note: 0 = invalid, 1 = valid, 2 = valid but needs lowercasing. I'd use an
26+
// enum for this, but it would make this LUT *way* harder to look at. This
27+
// includes 0-9, a-z, A-Z (as 2), and ('!' | '#' | '$' | '%' | '&' | '\'' | '*'
28+
// | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~'), matching the field-name
29+
// token production defined at https://tools.ietf.org/html/rfc7230#section-3.2.
30+
static VALID_HEADER_LUT: [u8; 256] = [
31+
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,
32+
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,
33+
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,
34+
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,
35+
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,
36+
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,
37+
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,
38+
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,
39+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
40+
];
41+
42+
let mut name = name.into();
43+
44+
if name.len() == 0 {
45+
return Err(name.to_string());
46+
}
47+
let mut need_lower_case = false;
48+
for b in name.bytes() {
49+
let validity = VALID_HEADER_LUT[b as usize];
50+
if validity == 0 {
51+
return Err(name.to_string());
52+
}
53+
if validity == 2 {
54+
need_lower_case = true;
55+
}
56+
}
57+
if need_lower_case {
58+
// Only do this if needed, since it causes us to own the header.
59+
name.to_mut().make_ascii_lowercase();
60+
}
61+
Ok(name.to_string())
62+
}
63+
64+
// Default headers for easy usage
65+
pub const ACCEPT_ENCODING: &str = "accept-encoding";
66+
pub const ACCEPT: &str = "accept";
67+
pub const AUTHORIZATION: &str = "authorization";
68+
pub const CONTENT_TYPE: &str = "content-type";
69+
pub const ETAG: &str = "etag";
70+
pub const IF_NONE_MATCH: &str = "if-none-match";
71+
pub const USER_AGENT: &str = "user-agent";
72+
// non-standard, but it's convenient to have these.
73+
pub const RETRY_AFTER: &str = "retry-after";
74+
pub const X_IF_UNMODIFIED_SINCE: &str = "x-if-unmodified-since";
75+
pub const X_KEYID: &str = "x-keyid";
76+
pub const X_LAST_MODIFIED: &str = "x-last-modified";
77+
pub const X_TIMESTAMP: &str = "x-timestamp";
78+
pub const X_WEAVE_NEXT_OFFSET: &str = "x-weave-next-offset";
79+
pub const X_WEAVE_RECORDS: &str = "x-weave-records";
80+
pub const X_WEAVE_TIMESTAMP: &str = "x-weave-timestamp";
81+
pub const X_WEAVE_BACKOFF: &str = "x-weave-backoff";

components/fairy-bridge/src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
use std::{
6+
collections::HashMap,
7+
sync::{Arc, OnceLock},
8+
};
9+
10+
mod backend;
11+
mod error;
12+
pub mod headers;
13+
mod request;
14+
#[cfg(feature = "backend-reqwest")]
15+
mod reqwest_backend;
16+
17+
pub use backend::*;
18+
pub use error::*;
19+
pub use request::*;
20+
#[cfg(feature = "backend-reqwest")]
21+
pub use reqwest_backend::*;
22+
23+
static REGISTERED_BACKEND: OnceLock<Arc<dyn Backend>> = OnceLock::new();
24+
25+
#[derive(uniffi::Record)]
26+
pub struct Response {
27+
pub url: String,
28+
pub status: u16,
29+
pub headers: HashMap<String, String>,
30+
pub body: Vec<u8>,
31+
}
32+
33+
uniffi::setup_scaffolding!();
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
use crate::{headers, FairyBridgeError, Response, Result};
6+
use pollster::FutureExt;
7+
use std::borrow::Cow;
8+
use std::collections::HashMap;
9+
use url::Url;
10+
11+
#[derive(uniffi::Enum)]
12+
pub enum Method {
13+
Get,
14+
Head,
15+
Post,
16+
Put,
17+
Delete,
18+
Connect,
19+
Options,
20+
Trace,
21+
Patch,
22+
}
23+
24+
#[derive(uniffi::Record)]
25+
pub struct Request {
26+
pub method: Method,
27+
pub url: String,
28+
pub headers: HashMap<String, String>,
29+
pub body: Option<Vec<u8>>,
30+
}
31+
32+
/// Http request
33+
///
34+
/// These are created using the builder pattern, then sent over the network using the `send()`
35+
/// method.
36+
impl Request {
37+
pub fn new(method: Method, url: Url) -> Self {
38+
Self {
39+
method,
40+
url: url.to_string(),
41+
headers: HashMap::new(),
42+
body: None,
43+
}
44+
}
45+
46+
pub async fn send(self) -> crate::Result<Response> {
47+
let mut response = match crate::REGISTERED_BACKEND.get() {
48+
Some(backend) => backend.clone().send_request(self).await,
49+
None => Err(FairyBridgeError::NoBackendInitialized),
50+
}?;
51+
response.headers = response
52+
.headers
53+
.into_iter()
54+
.map(|(name, value)| Ok((headers::normalize_request_header(name)?, value)))
55+
.collect::<crate::Result<HashMap<_, _>>>()?;
56+
Ok(response)
57+
}
58+
59+
pub fn send_sync(self) -> crate::Result<Response> {
60+
self.send().block_on()
61+
}
62+
63+
/// Alias for `Request::new(Method::Get, url)`, for convenience.
64+
pub fn get(url: Url) -> Self {
65+
Self::new(Method::Get, url)
66+
}
67+
68+
/// Alias for `Request::new(Method::Patch, url)`, for convenience.
69+
pub fn patch(url: Url) -> Self {
70+
Self::new(Method::Patch, url)
71+
}
72+
73+
/// Alias for `Request::new(Method::Post, url)`, for convenience.
74+
pub fn post(url: Url) -> Self {
75+
Self::new(Method::Post, url)
76+
}
77+
78+
/// Alias for `Request::new(Method::Put, url)`, for convenience.
79+
pub fn put(url: Url) -> Self {
80+
Self::new(Method::Put, url)
81+
}
82+
83+
/// Alias for `Request::new(Method::Delete, url)`, for convenience.
84+
pub fn delete(url: Url) -> Self {
85+
Self::new(Method::Delete, url)
86+
}
87+
88+
/// Add all the provided headers to the list of headers to send with this
89+
/// request.
90+
pub fn headers<'a, I, K, V>(mut self, to_add: I) -> crate::Result<Self>
91+
where
92+
I: IntoIterator<Item = (K, V)>,
93+
K: Into<Cow<'a, str>>,
94+
V: Into<String>,
95+
{
96+
for (name, value) in to_add {
97+
self = self.header(name, value)?
98+
}
99+
Ok(self)
100+
}
101+
102+
/// Add the provided header to the list of headers to send with this request.
103+
///
104+
/// This returns `Err` if `val` contains characters that may not appear in
105+
/// the body of a header.
106+
///
107+
/// ## Example
108+
/// ```
109+
/// # use fairy_bridge::{Request, headers};
110+
/// # use url::Url;
111+
/// # fn main() -> fairy_bridge::Result<()> {
112+
/// # let some_url = url::Url::parse("https://www.example.com").unwrap();
113+
/// Request::post(some_url)
114+
/// .header(headers::CONTENT_TYPE, "application/json")?
115+
/// .header("My-Header", "Some special value")?;
116+
/// // ...
117+
/// # Ok(())
118+
/// # }
119+
/// ```
120+
pub fn header<'a>(
121+
mut self,
122+
name: impl Into<Cow<'a, str>>,
123+
val: impl Into<String>,
124+
) -> crate::Result<Self> {
125+
self.headers
126+
.insert(headers::normalize_request_header(name)?, val.into());
127+
Ok(self)
128+
}
129+
130+
/// Set this request's body.
131+
pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
132+
self.body = Some(body.into());
133+
self
134+
}
135+
136+
/// Set body to a json-serialized value and the the Content-Type header to "application/json".
137+
///
138+
/// Returns an [crate::Error::SerializationError] if there was there was an error serializing the data.
139+
pub fn json(mut self, val: &(impl serde::Serialize + ?Sized)) -> Result<Self> {
140+
self.body = Some(serde_json::to_vec(val)?);
141+
self.headers.insert(
142+
headers::CONTENT_TYPE.to_owned(),
143+
"application/json".to_owned(),
144+
);
145+
Ok(self)
146+
}
147+
}

0 commit comments

Comments
 (0)