From e83357ad2757140320805bce263f952398dbcc4b Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 20 Dec 2023 16:51:13 -0500 Subject: [PATCH] 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. --- Cargo.lock | 329 +++++++----------- Cargo.toml | 3 +- components/fairy-bridge/Cargo.toml | 22 ++ components/fairy-bridge/README.md | 29 ++ components/fairy-bridge/src/backend.rs | 35 ++ components/fairy-bridge/src/error.rs | 29 ++ components/fairy-bridge/src/headers.rs | 81 +++++ components/fairy-bridge/src/lib.rs | 33 ++ components/fairy-bridge/src/request.rs | 147 ++++++++ .../fairy-bridge/src/reqwest_backend.rs | 122 +++++++ examples/fairy-bridge-demo/Cargo.toml | 18 + examples/fairy-bridge-demo/README.md | 93 +++++ examples/fairy-bridge-demo/run-demo.py | 43 +++ examples/fairy-bridge-demo/src/demo.py | 93 +++++ examples/fairy-bridge-demo/src/lib.rs | 77 ++++ 15 files changed, 950 insertions(+), 204 deletions(-) create mode 100644 components/fairy-bridge/Cargo.toml create mode 100644 components/fairy-bridge/README.md create mode 100644 components/fairy-bridge/src/backend.rs create mode 100644 components/fairy-bridge/src/error.rs create mode 100644 components/fairy-bridge/src/headers.rs create mode 100644 components/fairy-bridge/src/lib.rs create mode 100644 components/fairy-bridge/src/request.rs create mode 100644 components/fairy-bridge/src/reqwest_backend.rs create mode 100644 examples/fairy-bridge-demo/Cargo.toml create mode 100644 examples/fairy-bridge-demo/README.md create mode 100755 examples/fairy-bridge-demo/run-demo.py create mode 100644 examples/fairy-bridge-demo/src/demo.py create mode 100644 examples/fairy-bridge-demo/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index afd8c11e49..98c7466b33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1390,6 +1390,32 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fairy-bridge" +version = "0.1.0" +dependencies = [ + "async-trait", + "pollster", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "uniffi", + "url", +] + +[[package]] +name = "fairy-bridge-demo" +version = "0.1.0" +dependencies = [ + "fairy-bridge", + "serde", + "serde_json", + "uniffi", + "url", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1633,19 +1659,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows", -] - [[package]] name = "generic-array" version = "0.14.5" @@ -1881,7 +1894,7 @@ dependencies = [ "httpdate", "itoa 1.0.9", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -2129,9 +2142,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libloading" @@ -2277,20 +2290,6 @@ dependencies = [ "url", ] -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if 1.0.0", - "generator", - "pin-utils", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -2300,15 +2299,6 @@ dependencies = [ "libc", ] -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - [[package]] name = "matches" version = "0.1.9" @@ -2446,9 +2436,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2705,16 +2695,6 @@ dependencies = [ "nss_build_common", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num" version = "0.2.1" @@ -2879,18 +2859,14 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] -name = "oneshot" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" -dependencies = [ - "loom", -] +name = "oneshot-uniffi" +version = "0.1.5" +source = "git+https://github.com/bendk/oneshot.git?branch=main#e1ff3a4b54e3e38d2ae1771aeef3e3d8710128c5" [[package]] name = "oorandom" @@ -2976,12 +2952,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot" version = "0.12.1" @@ -3055,9 +3025,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -3169,6 +3139,12 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -3509,7 +3485,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata 0.3.7", - "regex-syntax 0.7.5", + "regex-syntax", ] [[package]] @@ -3517,9 +3493,6 @@ name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] [[package]] name = "regex-automata" @@ -3529,15 +3502,9 @@ checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.7.5" @@ -3562,9 +3529,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "async-compression", "base64 0.21.2", @@ -3588,6 +3555,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-native-tls", "tokio-util", @@ -3904,15 +3872,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "1.1.0" @@ -3993,6 +3952,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "sql-support" version = "0.1.0" @@ -4161,6 +4130,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "systest" version = "0.1.0" @@ -4274,6 +4264,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.31" @@ -4294,16 +4295,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if 1.0.0", - "once_cell", -] - [[package]] name = "time" version = "0.1.44" @@ -4359,27 +4350,26 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.29.1" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", - "socket2", + "socket2 0.5.5", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", @@ -4495,21 +4485,9 @@ dependencies = [ "cfg-if 1.0.0", "log", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", -] - [[package]] name = "tracing-core" version = "0.1.31" @@ -4517,36 +4495,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", ] [[package]] @@ -4610,12 +4558,9 @@ checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-linebreak" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" -dependencies = [ - "regex", -] +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" @@ -4634,9 +4579,9 @@ checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode-xid" @@ -4646,9 +4591,8 @@ checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "uniffi" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e192430644d99babe02bede25316eee84fa154b1e5f8cfe99406c028b8c577" +version = "0.25.3" +source = "git+https://github.com/bendk/uniffi-rs.git?branch=async-trait-interfaces#af8f5043b6f657d51b6b8100a5cf1b1d4f6c0d0d" dependencies = [ "anyhow", "camino", @@ -4661,9 +4605,8 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c235355da41bc8347b2d5851e1060d4652dfbdc6d7d6ccddaabebe25e3c32a4" +version = "0.25.3" +source = "git+https://github.com/bendk/uniffi-rs.git?branch=async-trait-interfaces#af8f5043b6f657d51b6b8100a5cf1b1d4f6c0d0d" dependencies = [ "anyhow", "askama", @@ -4677,6 +4620,7 @@ dependencies = [ "once_cell", "paste", "serde", + "textwrap 0.16.0", "toml", "uniffi_meta", "uniffi_testing", @@ -4685,9 +4629,8 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81049ed7015a8a66b085aca3fb0c0011fdae4dd9ab8c38f5751f7861d60eb0f4" +version = "0.25.3" +source = "git+https://github.com/bendk/uniffi-rs.git?branch=async-trait-interfaces#af8f5043b6f657d51b6b8100a5cf1b1d4f6c0d0d" dependencies = [ "anyhow", "camino", @@ -4696,9 +4639,8 @@ dependencies = [ [[package]] name = "uniffi_checksum_derive" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce082833f0fcaf6fc221fbab26720440daf99381f5a71e89b6e23375eb6ea770" +version = "0.25.3" +source = "git+https://github.com/bendk/uniffi-rs.git?branch=async-trait-interfaces#af8f5043b6f657d51b6b8100a5cf1b1d4f6c0d0d" dependencies = [ "quote", "syn 2.0.26", @@ -4706,25 +4648,23 @@ dependencies = [ [[package]] name = "uniffi_core" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389bbe4d8334b3370c7cc998788d7a9619e0b61b58f1cbcd4a6a8606ab0a6f7d" +version = "0.25.3" +source = "git+https://github.com/bendk/uniffi-rs.git?branch=async-trait-interfaces#af8f5043b6f657d51b6b8100a5cf1b1d4f6c0d0d" dependencies = [ "anyhow", "bytes", "camino", "log", "once_cell", - "oneshot", + "oneshot-uniffi", "paste", "static_assertions", ] [[package]] name = "uniffi_macros" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa170f970d42d8fbe205f5794b83f72d6617835a73b91ed1869e1eba5dd06c" +version = "0.25.3" +source = "git+https://github.com/bendk/uniffi-rs.git?branch=async-trait-interfaces#af8f5043b6f657d51b6b8100a5cf1b1d4f6c0d0d" dependencies = [ "bincode", "camino", @@ -4741,9 +4681,8 @@ dependencies = [ [[package]] name = "uniffi_meta" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ef337c28a379ed6962eae0cb0824ab31202b21b8ae3bf6c2a706f5e7285f5f" +version = "0.25.3" +source = "git+https://github.com/bendk/uniffi-rs.git?branch=async-trait-interfaces#af8f5043b6f657d51b6b8100a5cf1b1d4f6c0d0d" dependencies = [ "anyhow", "bytes", @@ -4753,9 +4692,8 @@ dependencies = [ [[package]] name = "uniffi_testing" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f2e218997229b4ed6e08c1abc9e277dde817f68a633babd3ebbfc77e32db302" +version = "0.25.3" +source = "git+https://github.com/bendk/uniffi-rs.git?branch=async-trait-interfaces#af8f5043b6f657d51b6b8100a5cf1b1d4f6c0d0d" dependencies = [ "anyhow", "camino", @@ -4766,11 +4704,11 @@ dependencies = [ [[package]] name = "uniffi_udl" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb29909e50256f32986ea3b3c32d2c49dece14ae4b3428c047913696ed200b2a" +version = "0.25.3" +source = "git+https://github.com/bendk/uniffi-rs.git?branch=async-trait-interfaces#af8f5043b6f657d51b6b8100a5cf1b1d4f6c0d0d" dependencies = [ "anyhow", + "textwrap 0.16.0", "uniffi_meta", "uniffi_testing", "weedle2", @@ -4823,12 +4761,6 @@ dependencies = [ "serde", ] -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "vcpkg" version = "0.2.15" @@ -5106,8 +5038,7 @@ dependencies = [ [[package]] name = "weedle2" version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741" +source = "git+https://github.com/bendk/uniffi-rs.git?branch=async-trait-interfaces#af8f5043b6f657d51b6b8100a5cf1b1d4f6c0d0d" dependencies = [ "nom", ] @@ -5179,15 +5110,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.36.1" @@ -5356,11 +5278,12 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if 1.0.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5a3a8a1e91..3115ec9c2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "components/as-ohttp-client", "components/autofill", "components/crashtest", + "components/fairy-bridge", "components/fxa-client", "components/logins", "components/nimbus", @@ -122,7 +123,7 @@ default-members = [ [workspace.dependencies] rusqlite = "0.30.0" libsqlite3-sys = "0.27.0" -uniffi = "0.25.2" +uniffi = { version = "0.25.3", git = "https://github.com/bendk/uniffi-rs.git", branch = "async-trait-interfaces" } [profile.release] opt-level = "s" diff --git a/components/fairy-bridge/Cargo.toml b/components/fairy-bridge/Cargo.toml new file mode 100644 index 0000000000..a7fd68ce50 --- /dev/null +++ b/components/fairy-bridge/Cargo.toml @@ -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"] } diff --git a/components/fairy-bridge/README.md b/components/fairy-bridge/README.md new file mode 100644 index 0000000000..99b83bf2a4 --- /dev/null +++ b/components/fairy-bridge/README.md @@ -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) diff --git a/components/fairy-bridge/src/backend.rs b/components/fairy-bridge/src/backend.rs new file mode 100644 index 0000000000..1a344c2e70 --- /dev/null +++ b/components/fairy-bridge/src/backend.rs @@ -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, + // Timeout for the entire request (in ms) + #[uniffi(default = None)] + pub timeout: Option, + // 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, request: Request) -> Result; +} + +#[uniffi::export] +pub fn init_backend(backend: Arc) -> Result<(), FairyBridgeError> { + crate::REGISTERED_BACKEND + .set(backend) + .map_err(|_| FairyBridgeError::BackendAlreadyInitialized) +} diff --git a/components/fairy-bridge/src/error.rs b/components/fairy-bridge/src/error.rs new file mode 100644 index 0000000000..9c13cb1a3b --- /dev/null +++ b/components/fairy-bridge/src/error.rs @@ -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 = std::result::Result; + +#[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 for FairyBridgeError { + fn from(e: serde_json::Error) -> Self { + Self::SerializationError { msg: e.to_string() } + } +} diff --git a/components/fairy-bridge/src/headers.rs b/components/fairy-bridge/src/headers.rs new file mode 100644 index 0000000000..3c7de7824f --- /dev/null +++ b/components/fairy-bridge/src/headers.rs @@ -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>) -> crate::Result { + 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>) -> crate::Result { + do_normalize_header(name).map_err(|name| FairyBridgeError::InvalidResponseHeader { name }) +} + +fn do_normalize_header<'a>(name: impl Into>) -> Result { + // 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"; diff --git a/components/fairy-bridge/src/lib.rs b/components/fairy-bridge/src/lib.rs new file mode 100644 index 0000000000..8b866f1a4e --- /dev/null +++ b/components/fairy-bridge/src/lib.rs @@ -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> = OnceLock::new(); + +#[derive(uniffi::Record)] +pub struct Response { + pub url: String, + pub status: u16, + pub headers: HashMap, + pub body: Vec, +} + +uniffi::setup_scaffolding!(); diff --git a/components/fairy-bridge/src/request.rs b/components/fairy-bridge/src/request.rs new file mode 100644 index 0000000000..ab8ad9f7e4 --- /dev/null +++ b/components/fairy-bridge/src/request.rs @@ -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, + pub body: Option>, +} + +/// 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 { + 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::>>()?; + Ok(response) + } + + pub fn send_sync(self) -> crate::Result { + 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 + where + I: IntoIterator, + K: Into>, + V: Into, + { + 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>, + val: impl Into, + ) -> crate::Result { + 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>) -> 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.body = Some(serde_json::to_vec(val)?); + self.headers.insert( + headers::CONTENT_TYPE.to_owned(), + "application/json".to_owned(), + ); + Ok(self) + } +} diff --git a/components/fairy-bridge/src/reqwest_backend.rs b/components/fairy-bridge/src/reqwest_backend.rs new file mode 100644 index 0000000000..90842577bb --- /dev/null +++ b/components/fairy-bridge/src/reqwest_backend.rs @@ -0,0 +1,122 @@ +/* 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::{init_backend, Backend, BackendSettings, FairyBridgeError, Method, Request, Response}; +use std::{sync::Arc, time::Duration}; + +struct ReqwestBackend { + runtime: tokio::runtime::Runtime, + client: reqwest::Client, +} + +#[uniffi::export] +pub fn init_reqwest_backend(settings: BackendSettings) -> Result<(), FairyBridgeError> { + // Create a multi-threaded runtime, with 1 worker thread. + // + // This creates and manages a single worker thread. + // + // Tokio also provides the current thread runtime, which is a "single-threaded future executor". + // However that means it needs to block a thread to run tasks. + // I.e. `send_request` would block to while executing the request. + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .build() + .unwrap(); + let mut client_builder = reqwest::Client::builder(); + if let Some(timeout) = settings.connect_timeout { + client_builder = client_builder.connect_timeout(Duration::from_millis(timeout as u64)) + } + if let Some(timeout) = settings.timeout { + client_builder = client_builder.timeout(Duration::from_millis(timeout as u64)) + } + client_builder = client_builder.redirect(reqwest::redirect::Policy::limited( + settings.redirect_limit as usize, + )); + let client = client_builder.build()?; + let backend = Arc::new(ReqwestBackend { runtime, client }); + init_backend(backend) +} + +#[async_trait::async_trait] +impl Backend for ReqwestBackend { + async fn send_request(self: Arc, request: Request) -> Result { + let handle = self.runtime.handle().clone(); + match handle + .spawn(async move { + self.convert_response(self.make_request(request).await?) + .await + }) + .await + { + Ok(result) => result, + Err(e) => Err(FairyBridgeError::BackendError { + msg: format!("tokio error: {e}"), + }), + } + } +} + +impl ReqwestBackend { + async fn make_request(&self, request: Request) -> Result { + let method = match request.method { + Method::Get => reqwest::Method::GET, + Method::Head => reqwest::Method::HEAD, + Method::Post => reqwest::Method::POST, + Method::Put => reqwest::Method::PUT, + Method::Delete => reqwest::Method::DELETE, + Method::Connect => reqwest::Method::CONNECT, + Method::Options => reqwest::Method::OPTIONS, + Method::Trace => reqwest::Method::TRACE, + Method::Patch => reqwest::Method::PATCH, + }; + let mut builder = self.client.request(method, request.url); + for (key, value) in request.headers { + builder = builder.header(key, value); + } + if let Some(body) = request.body { + builder = builder.body(body) + } + Ok(builder.send().await?) + } + + async fn convert_response( + &self, + response: reqwest::Response, + ) -> Result { + let url = response.url().to_string(); + let status = response.status().as_u16(); + let headers = response + .headers() + .iter() + .map(|(k, v)| { + ( + k.as_str().to_owned(), + String::from_utf8_lossy(v.as_bytes()).to_string(), + ) + }) + .collect(); + let body = response.bytes().await?.into(); + + Ok(Response { + url, + status, + headers, + body, + }) + } +} + +impl From for FairyBridgeError { + fn from(error: reqwest::Error) -> Self { + match error.status() { + Some(status) => FairyBridgeError::HttpError { + code: status.as_u16(), + }, + None => FairyBridgeError::BackendError { + msg: format!("reqwest error: {error}"), + }, + } + } +} diff --git a/examples/fairy-bridge-demo/Cargo.toml b/examples/fairy-bridge-demo/Cargo.toml new file mode 100644 index 0000000000..fb45c66269 --- /dev/null +++ b/examples/fairy-bridge-demo/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "fairy-bridge-demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib"] + +[dependencies] +fairy-bridge = { path = "../../components/fairy-bridge", features = ["backend-reqwest"] } +serde = {version = "1", features=["derive"] } +serde_json = "1" +uniffi = { workspace = true } +url = "2.2" + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } diff --git a/examples/fairy-bridge-demo/README.md b/examples/fairy-bridge-demo/README.md new file mode 100644 index 0000000000..7fca61b143 --- /dev/null +++ b/examples/fairy-bridge-demo/README.md @@ -0,0 +1,93 @@ +# Fairy bridge demo + +This example is meant to demonstrate the fairy-bridge library. + +## Usage + +`./run-demo.py` + +This will perform a request against the `https://httpbin.org/anything` endpoint and printout the +response. + +Arguments: + + * `--python`: Use the Python implemented backend (default is the reqwest backend) + * `--sync`: Run the request in sync mode (default is async mode) + * `--post`: Perform a `POST` request (default is `GET`) + * `--conn-timeout CONN_TIMEOUT`: set the connection timeout + * `--timeout TIMEOUT`: set the total request timeout + * `--redirect-limit REDIRECT_LIMIT`: set the redirect limits + +## Example output + +### ./run-demo.py + +Performs a GET request using the reqwest backend + +``` +GET https://httpbin.org/anything (async) +got response +status: 200 +response: +{ + "args": {}, + "data": "", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Host": "httpbin.org", + "User-Agent": "fairy-bridge-demo", + "X-Amzn-Trace-Id": "Root=1-65848c2f-46df949b3229b84833aa445f", + "X-Foo": "bar" + }, + "json": null, + "method": "GET", + "origin": "8.9.85.40", + "url": "https://httpbin.org/anything" +} +``` + + +### ./run-demo.py --python --post --sync + +Perform a POST request using the Python backend in a non-async context. + +``` +POST https://httpbin.org/anything (sync) +got response +status: 200 +response: +{ + "args": {}, + "data": "{\"guid\":\"abcdef1234\",\"foo\":\"Bar\"}", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "33", + "Content-Type": "application/json", + "Host": "httpbin.org", + "User-Agent": "fairy-bridge-demo", + "X-Amzn-Trace-Id": "Root=1-65848ca7-2017c7cf112af7fa76c9c2e7", + "X-Foo": "bar" + }, + "json": { + "foo": "Bar", + "guid": "abcdef1234" + }, + "method": "POST", + "origin": "8.9.85.40", + "url": "https://httpbin.org/anything" +} +``` + +### ./run-demo.py --conn-timeout 0 + +Perform a GET request with a 0 ms timeout to force a failure + +``` +GET http://httpbin.org/anything (async) +error: BackendError(reqwest error: error sending request for url (http://httpbin.org/anything): error trying to connect: operation timed out) +``` diff --git a/examples/fairy-bridge-demo/run-demo.py b/examples/fairy-bridge-demo/run-demo.py new file mode 100755 index 0000000000..754fd9929c --- /dev/null +++ b/examples/fairy-bridge-demo/run-demo.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import shutil +import sys +import subprocess + +crate_dir = Path(__file__).parent +root_dir = crate_dir.parent.parent +target_debug = root_dir / 'target' / 'debug' +work_dir = root_dir / 'target' / 'fairy-bridge-demo' + +# build everything +def find_dylib(): + for prefix in ["lib", ""]: + for ext in ["so", "DLL", "dylib"]: + lib_path = target_debug / f"{prefix}fairy_bridge_demo.{ext}" + if lib_path.exists(): + return lib_path +if work_dir.exists(): + shutil.rmtree(str(work_dir)) +work_dir.mkdir() +subprocess.check_call(["cargo", "build"], cwd=crate_dir) +shutil.copy(crate_dir / "src" / "demo.py", work_dir) +dylib_path = find_dylib() +shutil.copy(dylib_path, work_dir) +subprocess.check_call( + [ + "cargo", "run", "-p", "embedded-uniffi-bindgen", "--", "generate", "-l", "python", + "--library", dylib_path.absolute(), + "--out-dir", work_dir.absolute(), + ], + cwd=root_dir, +) + +# run it +print() +print() +subprocess.check_call( + ["/usr/bin/env", "python3", "demo.py"] + sys.argv[1:], + cwd = work_dir +) + diff --git a/examples/fairy-bridge-demo/src/demo.py b/examples/fairy-bridge-demo/src/demo.py new file mode 100644 index 0000000000..e369c05b34 --- /dev/null +++ b/examples/fairy-bridge-demo/src/demo.py @@ -0,0 +1,93 @@ +import aiohttp +import argparse +import asyncio +import fairy_bridge +import fairy_bridge_demo + +parser = argparse.ArgumentParser() +parser.add_argument("--python", action="store_true") +parser.add_argument("--sync", action="store_true") +parser.add_argument("--post", action="store_true") +parser.add_argument("--conn-timeout", type=int) +parser.add_argument("--timeout", type=int) +parser.add_argument("--redirect-limit", type=int) +args = parser.parse_args() + +class PyBackend: + METHOD_MAP = { + fairy_bridge.Method.GET: "GET", + fairy_bridge.Method.HEAD: "HEAD", + fairy_bridge.Method.POST: "POST", + fairy_bridge.Method.PUT: "PUT", + fairy_bridge.Method.DELETE: "DELETE", + fairy_bridge.Method.CONNECT: "CONNECT", + fairy_bridge.Method.OPTIONS: "OPTIONS", + fairy_bridge.Method.TRACE: "TRACE", + fairy_bridge.Method.PATCH: "PATCH", + } + def __init__(self, settings: fairy_bridge.BackendSettings): + self.session_kwargs = dict( + timeout = aiohttp.ClientTimeout( + connect = PyBackend.convert_timeout(settings.connect_timeout), + total = PyBackend.convert_timeout(settings.timeout), + ) + ) + self.request_kwargs = dict( + allow_redirects = True if settings.redirect_limit > 0 else False, + max_redirects = settings.redirect_limit, + ) + + @staticmethod + def convert_timeout(settings_timeout): + return None if settings_timeout is None else settings_timeout / 1000.0 + + async def send_request(self, request: fairy_bridge.Request) -> fairy_bridge.Response: + async with aiohttp.ClientSession(**self.session_kwargs) as session: + method = self.METHOD_MAP[request.method] + url = request.url + kwargs = { + "headers": request.headers, + **self.request_kwargs + } + if request.body is not None: + kwargs["data"] = request.body + async with session.request(method, url, **kwargs) as response: + return fairy_bridge.Response( + url = str(response.url), + status = response.status, + headers = response.headers, + body = await response.read()) + +settings = fairy_bridge.BackendSettings() +if args.conn_timeout is not None: + settings.connect_timeout = args.conn_timeout +if args.timeout is not None: + settings.timeout = args.timeout +if args.redirect_limit is not None: + settings.redirect_limit = args.redirect_limit + +if args.python: + fairy_bridge.init_backend(PyBackend(settings)) +else: + fairy_bridge.init_reqwest_backend(settings) +# Always startup an event loop. Even if we're running in sync mode, `PyBackend` still needs it +# running. This mimics a typical app-services setup. Our component is sync, but the app that +# consumes it is running an async runtime. +async def run_demo(): + if args.sync: + loop = asyncio.get_running_loop() + # Call `uniffi_set_event_loop` so that it can run async code from the spawned thread. + # Note: this is only needed for Python. Both Swift and Kotlin have the concept of a global + # runtime. + fairy_bridge.uniffi_set_event_loop(loop) + # Run the sync code in an executor to avoid blocking the eventloop thread + if args.post: + await loop.run_in_executor(None, fairy_bridge_demo.run_demo_sync_post) + else: + await loop.run_in_executor(None, fairy_bridge_demo.run_demo_sync) + else: + if args.post: + await fairy_bridge_demo.run_demo_async_post() + else: + await fairy_bridge_demo.run_demo_async() +asyncio.run(run_demo()) diff --git a/examples/fairy-bridge-demo/src/lib.rs b/examples/fairy-bridge-demo/src/lib.rs new file mode 100644 index 0000000000..7085ae5e56 --- /dev/null +++ b/examples/fairy-bridge-demo/src/lib.rs @@ -0,0 +1,77 @@ +pub use fairy_bridge; + +use fairy_bridge::{headers, Request, Response}; +use url::Url; + +fn make_request() -> Request { + let url = Url::parse("http://httpbin.org/anything").unwrap(); + Request::get(url) + .header(headers::USER_AGENT, "fairy-bridge-demo") + .unwrap() + .header("X-Foo", "bar") + .unwrap() +} + +#[derive(serde::Serialize)] +struct TestPostData { + guid: String, + foo: String, +} + +fn make_post_request() -> Request { + let url = Url::parse("http://httpbin.org/anything").unwrap(); + Request::post(url) + .header(headers::USER_AGENT, "fairy-bridge-demo") + .unwrap() + .header("X-Foo", "bar") + .unwrap() + .json(&TestPostData { + guid: "abcdef1234".to_owned(), + foo: "Bar".to_owned(), + }) + .unwrap() +} + +#[uniffi::export] +async fn run_demo_async() { + println!("GET http://httpbin.org/anything (async)"); + let response = make_request().send().await; + print_response(response); +} + +#[uniffi::export] +fn run_demo_sync() { + println!("GET http://httpbin.org/anything (sync)"); + let response = make_request().send_sync(); + print_response(response); +} + +#[uniffi::export] +async fn run_demo_async_post() { + println!("POST http://httpbin.org/anything (async)"); + let response = make_post_request().send().await; + print_response(response); +} + +#[uniffi::export] +fn run_demo_sync_post() { + println!("POST http://httpbin.org/anything (sync)"); + let response = make_post_request().send_sync(); + print_response(response); +} + +fn print_response(response: fairy_bridge::Result) { + match response { + Ok(response) => { + println!("got response"); + println!("status: {}", response.status); + println!("response:"); + println!("{}", String::from_utf8(response.body).unwrap()); + } + Err(e) => { + println!("error: {e}"); + } + } +} + +uniffi::setup_scaffolding!();