Skip to content

implement optional async support and remove nested result type in response #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7947db2
split the routes and data types to separate files, make room for asyn…
wfraser Mar 25, 2024
c0cafc2
async http client
wfraser Mar 30, 2024
76b6283
make the sync routes directly available by default, add a feature to …
wfraser Mar 30, 2024
d509bb9
default_client: use ureq's agent to enable connection pooling
wfraser Mar 30, 2024
b2d810a
fix the tests for the new client changes
wfraser Mar 30, 2024
178fa78
add Send to the bound for HttpClient::update_token
wfraser Mar 30, 2024
7b08f88
ugh, set msrv to 1.75 which is only 3 months old
wfraser Mar 30, 2024
4f269b0
make oauth2::obtain_access_token future Send
wfraser Mar 31, 2024
0604e05
slim down the futures crate features
wfraser Mar 31, 2024
26bdcaf
implement async routes
wfraser Mar 31, 2024
a7982ac
add async demo, fix up some things with sync routes turned off
wfraser Mar 31, 2024
802a6fd
restructure modules a little
wfraser Mar 31, 2024
2728db0
release notes, readme updates
wfraser Mar 31, 2024
41000ed
async upload routes take a Bytes as the body now
wfraser Apr 1, 2024
864e346
reexport the client_trait types at the root if sync_route_defaults is…
wfraser Apr 1, 2024
4464d0b
enable sync_routes_default if default_client is enabled
wfraser Apr 1, 2024
97f9411
impl the async client marker traits for the sync ones
wfraser Apr 1, 2024
000ee56
Authorization::obtain_access_token needs to be sync for compatibility
wfraser Apr 1, 2024
7c662e4
fix a couple doc comment links
wfraser Apr 3, 2024
838c15b
turn off default features in reqwest, this stops it from pulling in o…
wfraser Apr 22, 2024
68a3bce
split request_with_body to make a new parse_response function
wfraser Apr 22, 2024
cc3a385
add TokenCache::set_access_token to facilitate testing with expired o…
wfraser Apr 22, 2024
a324511
print out the env variables to set in the demo apps after authorizing
wfraser Apr 23, 2024
391f49b
remove the human-readable text from the HTTP response code
wfraser Apr 22, 2024
89934bb
remove redundant logging in parse_response
wfraser Apr 23, 2024
ce5db46
ureq version needs to be >=2.5.0
wfraser Apr 23, 2024
db5a688
Merge branch 'master' into async-mode
wfraser May 6, 2024
2703b2c
Merge branch 'master' into async-mode
wfraser May 31, 2024
876aa02
flatten errors
wfraser Jun 28, 2024
96be83e
add a BoxedError type alias
wfraser Jun 28, 2024
ebe5088
add #[source] to some Error variants
wfraser Jun 28, 2024
a253d0c
fix intra-doc links
wfraser Jun 28, 2024
eee0bfc
move errors to their own module
wfraser Jul 23, 2024
74abec8
update release notes
wfraser Jul 23, 2024
4316243
Merge branch 'master' into async-mode-flaterror
wfraser Aug 7, 2024
b245159
BoxedError should also be Send + Sync
wfraser Oct 12, 2024
f7a4a49
add a '--stat' mode to the demo examples
wfraser Oct 30, 2024
06cbebe
elide lifetimes in some trait impls
wfraser Oct 31, 2024
32ed229
Box<impl...> isn't necessary, just use impl...
wfraser Oct 31, 2024
f33801c
improve release notes
wfraser Oct 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/cargo-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ jobs:
run: if [[ ! -z `git status --porcelain=v1` ]]; then echo "::error::Workspace is dirty after running generator. Did you remember to check in the generated files?"; exit 1; fi

- name: Install MSRV toolchain
run: rustup install 1.65.0 --profile minimal
run: rustup install 1.75.0 --profile minimal

- name: Run cargo test
run: rustup run 1.65.0 cargo test
run: rustup run 1.75.0 cargo test

- name: Install nightly toolchain
run: |
Expand All @@ -41,3 +41,9 @@ jobs:

- name: Run clippy
run: rustup run nightly cargo clippy --all-targets --all-features -- --deny warnings

- name: Run clippy with minimal features (sync)
run: rustup run nightly cargo clippy --no-default-features --features sync_routes,dbx_files

- name: Run clippy with minimal features (async)
run: rustup run nightly cargo clippy --no-default-features --features default_async_client,dbx_files
51 changes: 46 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dropbox-sdk"
version = "0.18.1"
version = "0.19.0-beta1"
authors = ["Bill Fraser <[email protected]>"]
edition = "2018"
description = "Rust bindings to the Dropbox API, generated by Stone from the official spec."
Expand All @@ -12,20 +12,34 @@ readme = "README.md"

[package.metadata]
# Keep this at least 1 year old.
msrv = "1.65.0" # Nov 3, 2022
# (or not... 1.75 is required for "-> impl Trait" sadly)
msrv = "1.75.0" # Dec 28, 2023

[dependencies]
async-lock = "3.3.0"
atty = "0.2.14"
base64 = "0.22"
bytes = "1.6.0"
log = "0.4"
ring = "0.17"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
url = "2.1"

[dependencies.futures]
version = "0.3.30"
default-features = false
features = ["std"]

[dependencies.reqwest]
version = "0.12.2"
optional = true
default-features = false
features = ["http2", "rustls-tls", "stream"]

[dependencies.ureq]
version = "2.0.0"
version = "2.5.0"
optional = true
default-features = false
features = ["tls"]
Expand All @@ -36,6 +50,15 @@ chrono = "0.4"
parallel_reader = "0.1"
threadpool = "1.8"

[dev-dependencies.tokio]
version = "1.37.0"
features = ["rt-multi-thread", "macros", "io-std"]

[dev-dependencies.tokio-util]
version = "0.7.10"
default-features = false
features = ["compat"]

[[example]]
name = "demo"
required-features = ["dbx_files", "default_client"]
Expand All @@ -44,6 +67,10 @@ required-features = ["dbx_files", "default_client"]
name = "large-file-upload"
required-features = ["dbx_files", "default_client"]

[[example]]
name = "demo-async"
required-features = ["dbx_files", "default_async_client"]

[features]
# dbx_* features each correspond to one Stone spec file.
# The lists of dependencies must be kept in sync with the 'import' statements in them.
Expand All @@ -68,12 +95,24 @@ dbx_team_policies = []
dbx_users = ["dbx_common", "dbx_team_common", "dbx_team_policies", "dbx_users_common"]
dbx_users_common = ["dbx_common"]

default_client = ["ureq"]
default_async_client = ["async_routes", "dep:reqwest"]
default_client = ["sync_routes", "sync_routes_default", "dep:ureq"]

# Enable unstable ("preview") API routes.
unstable = []

# Include everything by default.
# Enable sync routes under `dropbox_sdk::routes::{namespace}`
sync_routes = []

# Enable async routes under `dropbox_sdk::async_routes::{namespace}`
async_routes = []

# Re-export the sync routes as `dropbox_sdk::{namsepace}` directly (matches pre-v0.19 structure).
# If disabled, export the async routes there instead.
sync_routes_default = ["sync_routes"]

# Include all namespaces by default.
# Enable sync default client, sync routes, and make the sync routes default, to match pre-v0.19.
default = [
"dbx_account",
"dbx_async",
Expand All @@ -96,6 +135,8 @@ default = [
"dbx_users",
"dbx_users_common",
"default_client",
"sync_routes",
"sync_routes_default",
]

[package.metadata.docs.rs]
Expand Down
34 changes: 28 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ However, that said,
* We are happy to get feedback and/or pull requests from the community! See
[contributing](CONTRIBUTING.md) for more information.

## Sync and Async

The routes (functions) come in two variants: sync, which do blocking network
I/O and return their value directly; and async, which return futures.

The sync routes (in the `dropbox_sdk::sync_routes` module) are enabled by
default or with the `sync_routes` feature, and likewise the async ones are
in the `dropbox_sdk::async_routes` module and can be enabled with the
`async_routes` feature.

Additionally, if the `sync_routes_default` feature is on (as it is by
default), the sync routes are available directly as `dropbox_sdk::{namespace}`,
which matches the original structure before the async routes were added.

## HTTP Client

To actually use the API calls, you need a HTTP client -- all functions take a
Expand All @@ -35,13 +49,18 @@ pass it as the client argument.

If you don't want to implement your own, this SDK comes with an optional
default client that uses `ureq` and `rustls`. To use it, build with the
`default_client` feature flag, and then there will be a set of clents in the
`default_client` feature flag, and then there will be a set of clients in the
`dropbox_sdk::default_client` module that you can use, corresponding to each of
the authentication types Dropbox uses (see below). The default client needs a
Dropbox API token; how you get one is up to you and your program. See the
programs under [examples/](examples/) for examples, and see the helper code in
the [oauth2](src/oauth2.rs) module.

Async clients can be implemented using a parallel set of traits located in the
`dropbox_sdk::async_client_trait` module. A default implementation (which uses
`reqwest` can be enabled with the `default_async_client` feature and is located
at `dropbox_sdk::default_async_client`.

## Authentication Types

The Dropbox API has a number of different [authentication types]. Each route
Expand Down Expand Up @@ -109,11 +128,9 @@ Some implementation notes, limitations, and TODOs:
* Stone allows structures to inherit from other structures and be polymorphic.
Rust doesn't have these paradigms, so instead this SDK represents
polymorphic parent structs as enums, and the inherited fields are put in all
variants. See `dropbox_sdk::files::Metadata` for an example.
* This crate only supports synchronous I/O. Eventually we probably want to
support async I/O, which will require making incompatible changes to the
types returned by routes. This should probably wait until the futures
ecosystem and async/await have stabilized some more.
variants. See `dropbox_sdk::files::Metadata` for an example. Upcasting is
supported using generated `From` implementations which either construct the
right enum variant or copy the subset of common fields.
* This code does not use `serde_derive` for the most part, and instead uses
manually-emitted serialization code. Previous work on this crate did attempt
to use `serde_derive`, but the way the Dropbox API serializes unions
Expand All @@ -125,5 +142,10 @@ Some implementation notes, limitations, and TODOs:
* Types with constraints (such as strings with patterns or min/max lengths, or
integers with a range) do not check that the data being stored in them meets
the constraints.
* The sync routes and clients are actually implemented in terms of the async
client interfaces, but all the futures returned are `std::future::ready()`,
which is then removed using `now_or_never()` before being returned to
callers. Even though futures are passed around and async functions are used,
no executor is actually needed because of this.

## Happy Dropboxing!
15 changes: 15 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# v0.19.0-beta1
xxxx-yy-zz
* **BIG CHANGE: async support added**
* HTTP client traits completely redesigned
* Should actually be much simpler to implement now, as the work of setting the right headers has been extracted out
into client_helpers code, and implementations now just need to provide a generic HttpRequest type which can set
any header.
* The default sync client is still the default enabled feature.
* To switch to the async mode, enable the `default_async_client` feature (and disable the `default_client`, `sync_routes`, `sync_routes_default` features).
* **BIG CHANGE: no more nested Results**
* Functions which used to return `Result<Result<T, E>, dropbox_sdk::Error>` now return `Result<T, dropbox_sdk::Error<E>>`.
* in other words, `Ok(Err(e))` is now written `Err(dropbox_sdk::Error::Api(e))` and `Ok(Ok(v))` is just `Ok(v)`.
* `dropbox_sdk::Error` now has a type parameter which differs depending on the function being called.
* MSRV raised to 1.71.0

# v0.18.1
2024-05-06
* fixed bug when using oauth2 refresh tokens using client secret instead of PKCE:
Expand Down
177 changes: 177 additions & 0 deletions examples/demo-async.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#![deny(rust_2018_idioms)]

//! This example illustrates a few basic Dropbox API operations: getting an OAuth2 token, listing
//! the contents of a folder recursively, and fetching a file given its path.

use tokio_util::compat::FuturesAsyncReadCompatExt;
use dropbox_sdk::default_async_client::{NoauthDefaultClient, UserAuthDefaultClient};
use dropbox_sdk::async_routes::files;

enum Operation {
Usage,
List(String),
Download(String),
Stat(String),
}

fn parse_args() -> Operation {
let mut ctor: Option<fn(String) -> Operation> = None;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--help" | "-h" => return Operation::Usage,
"--list" => {
ctor = Some(Operation::List);
}
"--download" => {
ctor = Some(Operation::Download);
}
"--stat" => {
ctor = Some(Operation::Stat);
}
path if path.starts_with('/') => {
return if let Some(ctor) = ctor {
ctor(arg)
} else {
eprintln!("Either --download or --list must be specified");
Operation::Usage
};
}
_ => {
eprintln!("Unrecognized option {arg:?}");
eprintln!();
return Operation::Usage;
}
}
}
Operation::Usage
}

#[tokio::main]
async fn main() {
env_logger::init();

let op = parse_args();

if let Operation::Usage = op {
eprintln!("usage: {} [option]", std::env::args().next().unwrap());
eprintln!(" options:");
eprintln!(" --help | -h view this text");
eprintln!(" --download <path> copy the contents of <path> to stdout");
eprintln!(" --list <path> recursively list all files under <path>");
eprintln!(" --stat <path> list all metadata of <path>");
eprintln!();
eprintln!(" If a Dropbox OAuth token is given in the environment variable");
eprintln!(" DBX_OAUTH_TOKEN, it will be used, otherwise you will be prompted for");
eprintln!(" authentication interactively.");
std::process::exit(1);
}

let mut auth = dropbox_sdk::oauth2::get_auth_from_env_or_prompt();
if auth.save().is_none() {
auth.obtain_access_token_async(NoauthDefaultClient::default()).await.unwrap();
eprintln!("Next time set these environment variables to reuse this authorization:");
eprintln!(" DBX_CLIENT_ID={}", auth.client_id());
eprintln!(" DBX_OAUTH={}", auth.save().unwrap());
}
let client = UserAuthDefaultClient::new(auth);

match op {
Operation::Usage => (), // handled above
Operation::Download(path) => {
eprintln!("Copying file to stdout: {}", path);
eprintln!();

match files::download(&client, &files::DownloadArg::new(path), None, None).await {
Ok(result) => {
match tokio::io::copy(
&mut result.body.expect("there must be a response body")
.compat(),
&mut tokio::io::stdout(),
).await {
Ok(n) => {
eprintln!("Downloaded {n} bytes");
}
Err(e) => {
eprintln!("I/O error: {e}");
}
}
}
Err(e) => {
eprintln!("Error from files/download: {e}");
}
}
}
Operation::List(mut path) => {
eprintln!("Listing recursively: {path}");

// Special case: the root folder is empty string. All other paths need to start with '/'.
if path == "/" {
path.clear();
}

let mut result = match files::list_folder(
&client,
&files::ListFolderArg::new(path).with_recursive(true),
).await {
Ok(result) => result,
Err(e) => {
eprintln!("Error from files/list_folder: {e}");
return;
}
};

let mut num_entries = result.entries.len();
let mut num_pages = 1;

loop {
for entry in result.entries {
match entry {
files::Metadata::Folder(entry) => {
println!("Folder: {}", entry.path_display.unwrap_or(entry.name));
}
files::Metadata::File(entry) => {
println!("File: {}", entry.path_display.unwrap_or(entry.name));
}
files::Metadata::Deleted(entry) => {
panic!("unexpected deleted entry: {:?}", entry);
}
}
}

if !result.has_more {
break;
}

result = match files::list_folder_continue(
&client,
&files::ListFolderContinueArg::new(result.cursor),
).await {
Ok(result) => {
num_pages += 1;
num_entries += result.entries.len();
result
}
Err(e) => {
eprintln!("Error from files/list_folder_continue: {e}");
break;
}
}
}

eprintln!("{num_entries} entries from {num_pages} result pages");
}
Operation::Stat(path) => {
eprintln!("listing metadata for: {path}");

let arg = files::GetMetadataArg::new(path)
.with_include_media_info(true)
.with_include_deleted(true)
.with_include_has_explicit_shared_members(true);

match files::get_metadata(&client, &arg).await {
Ok(result) => println!("{result:#?}"),
Err(e) => eprintln!("Error from files/get_metadata: {e}"),
}
}
}
}
Loading