Skip to content

Commit

Permalink
Merge pull request #458 from gelendir/wasm-support
Browse files Browse the repository at this point in the history
Support wasm32-unknown-unknown architecture
  • Loading branch information
ramsayleung authored Feb 29, 2024
2 parents 4d4d090 + 9bf39b4 commit 0849ff3
Show file tree
Hide file tree
Showing 20 changed files with 533 additions and 93 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,30 @@ jobs:
with:
command: test
args: -p rspotify -p rspotify-http -p rspotify-model -p rspotify-macros --no-default-features --features=${{ matrix.features }}

test-wasm:
name: Test WASM client
runs-on: ubuntu-latest
env:
RUST_BACKTRACE: 1

steps:
- name: Checkout sources
uses: actions/checkout@v2

- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: clippy

- name: Install node
uses: actions/setup-node@v4

- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

- name: Run wasm-pack test
run: wasm-pack test --node
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.13.0 (2024.02.28)

**New features**
- ([#458](https://github.com/ramsayleung/rspotify/pull/458)) Support for the `wasm32-unknown-unknown` build target

## 0.12.1 (Unreleased)
**Bugfixes**
- ([#440](https://github.com/ramsayleung/rspotify/issues/440)) Add Smartwatch device type, fix for json parse error: unknown variant Smartwatch.
Expand Down
17 changes: 15 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ base64 = "0.21.2"
chrono = { version = "0.4.19", features = ["serde"] }
dotenvy = { version = "0.15.0", optional = true }
futures = { version = "0.3.17", optional = true }
getrandom = "0.2.3"

log = "0.4.14"
maybe-async = "0.2.6"
serde = { version = "1.0.130", default-features = false }
Expand All @@ -46,10 +46,23 @@ thiserror = "1.0.29"
url = "2.2.2"
webbrowser = { version = "0.8.0", optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2.3", features = ["js"] }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
getrandom = "0.2.3"

[dev-dependencies]
env_logger = { version = "0.11.0", default-features = false }
tokio = { version = "1.11.0", features = ["rt-multi-thread", "macros"] }
futures-util = "0.3.17"
wasm-bindgen-test = "0.3.34"

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "1.11.0", features = ["rt-multi-thread", "macros"] }

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
tokio = { version = "1.11.0", features = ["rt", "macros"] }
dotenvy_macro = { version = "0.15.7" }

[features]
default = ["client-reqwest", "reqwest-default-tls"]
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ $ cargo build --all-features

Because in order to switch between clients, the different clients have to implement the same base trait in [src/http/mod.rs](https://github.com/ramsayleung/rspotify/blob/master/src/http/mod.rs), so if you build with all features, you'll get `duplicate definitions` error. As every coin has two sides, you can only have one side at a time, not all sides of it.

## WASM support

RSpotify supports building for the `wasm32-unknown-unknown` target. It should be as easy as:

```sh
$ cargo build --target wasm32-unknown-unknown
```

Refer to the [documentation](https://docs.rs/rspotify/latest/rspotify/#webassembly) for more details

## License

[MIT](./LICENSE)
3 changes: 2 additions & 1 deletion rspotify-http/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ pub type Form<'a> = HashMap<&'a str, &'a str>;
/// different ways (`Value::Null`, an empty `Value::Object`...), so this removes
/// redundancy and edge cases (a `Some(Value::Null), for example, doesn't make
/// much sense).
#[maybe_async]
#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
pub trait BaseHttpClient: Send + Default + Clone + fmt::Debug {
type Error;

Expand Down
17 changes: 16 additions & 1 deletion rspotify-http/src/reqwest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
use super::{BaseHttpClient, Form, Headers, Query};

use std::convert::TryInto;

#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;

use maybe_async::async_impl;
Expand Down Expand Up @@ -56,6 +58,7 @@ pub struct ReqwestClient {
client: reqwest::Client,
}

#[cfg(not(target_arch = "wasm32"))]
impl Default for ReqwestClient {
fn default() -> Self {
let client = reqwest::ClientBuilder::new()
Expand All @@ -67,6 +70,17 @@ impl Default for ReqwestClient {
}
}

#[cfg(target_arch = "wasm32")]
impl Default for ReqwestClient {
fn default() -> Self {
let client = reqwest::ClientBuilder::new()
.build()
// building with these options cannot fail
.unwrap();
Self { client }
}
}

impl ReqwestClient {
async fn request<D>(
&self,
Expand Down Expand Up @@ -109,7 +123,8 @@ impl ReqwestClient {
}
}

#[async_impl]
#[cfg_attr(target_arch = "wasm32", async_impl(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_impl)]
impl BaseHttpClient for ReqwestClient {
type Error = ReqwestError;

Expand Down
6 changes: 4 additions & 2 deletions src/auth_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ pub struct AuthCodeSpotify {
}

/// This client has access to the base methods.
#[maybe_async]
#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
impl BaseClient for AuthCodeSpotify {
fn get_http(&self) -> &HttpClient {
&self.http
Expand Down Expand Up @@ -126,7 +127,8 @@ impl BaseClient for AuthCodeSpotify {

/// This client includes user authorization, so it has access to the user
/// private endpoints in [`OAuthClient`].
#[maybe_async]
#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
impl OAuthClient for AuthCodeSpotify {
fn get_oauth(&self) -> &OAuth {
&self.oauth
Expand Down
6 changes: 4 additions & 2 deletions src/auth_code_pkce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ pub struct AuthCodePkceSpotify {
}

/// This client has access to the base methods.
#[maybe_async]
#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
impl BaseClient for AuthCodePkceSpotify {
fn get_http(&self) -> &HttpClient {
&self.http
Expand Down Expand Up @@ -88,7 +89,8 @@ impl BaseClient for AuthCodePkceSpotify {

/// This client includes user authorization, so it has access to the user
/// private endpoints in [`OAuthClient`].
#[maybe_async]
#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
impl OAuthClient for AuthCodePkceSpotify {
fn get_oauth(&self) -> &OAuth {
&self.oauth
Expand Down
3 changes: 2 additions & 1 deletion src/client_creds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ pub struct ClientCredsSpotify {
}

/// This client has access to the base methods.
#[maybe_async]
#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
impl BaseClient for ClientCredsSpotify {
fn get_http(&self) -> &HttpClient {
&self.http
Expand Down
3 changes: 2 additions & 1 deletion src/clients/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ use serde_json::Value;
/// This trait implements the basic endpoints from the Spotify API that may be
/// accessed without user authorization, including parts of the authentication
/// flow that are shared, and the endpoints.
#[maybe_async]
#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
pub trait BaseClient
where
Self: Send + Sync + Default + Clone + fmt::Debug,
Expand Down
3 changes: 2 additions & 1 deletion src/clients/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ use url::Url;
/// [`user_playlist`](crate::clients::BaseClient::user_playlist). This trait
/// only separates endpoints that *always* need authorization from the base
/// ones.
#[maybe_async]
#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
pub trait OAuthClient: BaseClient {
fn get_oauth(&self) -> &OAuth;

Expand Down
12 changes: 10 additions & 2 deletions src/clients/pagination/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,18 @@
#[cfg(feature = "__sync")]
mod iter;
#[cfg(feature = "__async")]

#[cfg(all(feature = "__async", not(target_arch = "wasm32")))]
mod stream;

#[cfg(all(feature = "__async", target_arch = "wasm32"))]
mod wasm_stream;

#[cfg(feature = "__sync")]
pub use iter::{paginate, paginate_with_ctx, Paginator};
#[cfg(feature = "__async")]

#[cfg(all(feature = "__async", not(target_arch = "wasm32")))]
pub use stream::{paginate, paginate_with_ctx, Paginator};

#[cfg(all(feature = "__async", target_arch = "wasm32"))]
pub use wasm_stream::{paginate, paginate_with_ctx, Paginator};
62 changes: 62 additions & 0 deletions src/clients/pagination/wasm_stream.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Asynchronous implementation of automatic pagination requests.
use crate::{model::Page, ClientResult};

use std::pin::Pin;

use futures::{future::Future, stream::Stream};

/// Alias for `futures::stream::Stream<Item = T>`, since async mode is enabled.
pub type Paginator<'a, T> = Pin<Box<dyn Stream<Item = T> + 'a>>;

pub type RequestFuture<'a, T> = Pin<Box<dyn 'a + Future<Output = ClientResult<Page<T>>>>>;

/// This is used to handle paginated requests automatically.
pub fn paginate_with_ctx<'a, Ctx: 'a, T, Request>(
ctx: Ctx,
req: Request,
page_size: u32,
) -> Paginator<'a, ClientResult<T>>
where
T: 'a + Unpin,
Request: 'a + for<'ctx> Fn(&'ctx Ctx, u32, u32) -> RequestFuture<'ctx, T>,
{
use async_stream::stream;
let mut offset = 0;
Box::pin(stream! {
loop {
let request = req(&ctx, page_size, offset);
let page = request.await?;
offset += page.items.len() as u32;
for item in page.items {
yield Ok(item);
}
if page.next.is_none() {
break;
}
}
})
}

pub fn paginate<'a, T, Fut, Request>(req: Request, page_size: u32) -> Paginator<'a, ClientResult<T>>
where
T: 'a + Unpin,
Fut: Future<Output = ClientResult<Page<T>>>,
Request: 'a + Fn(u32, u32) -> Fut,
{
use async_stream::stream;
let mut offset = 0;
Box::pin(stream! {
loop {
let request = req(page_size, offset);
let page = request.await?;
offset += page.items.len() as u32;
for item in page.items {
yield Ok(item);
}
if page.next.is_none() {
break;
}
}
})
}
20 changes: 19 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@
//! the [`.env` file](https://github.com/ramsayleung/rspotify/blob/master/.env)
//! for more details.
//!
//! ### WebAssembly
//!
//! RSpotify supports the `wasm32-unknown-unknown` target in combination
//! with the `client-reqwest` feature. HTTP requests must be processed async.
//! Other HTTP client configurations are not supported.
//!
//! [Spotify recommends][spotify-auth-flows] using [`AuthCodePkceSpotify`] for
//! authorization flows on the web.
//!
//! Importing the Client ID via `RSPOTIFY_CLIENT_ID` is not possible since WASM
//! web runtimes are isolated from the host environment. The client ID must be
//! passed explicitly to [`Credentials::new_pkce`]. Alternatively, it can be
//! embedded at compile time with the [`std::env!`] or
//! [`dotenv!`](https://crates.io/crates/dotenvy) macros.
//!
//! ### Examples
//!
//! There are some [available examples on the GitHub
Expand Down Expand Up @@ -442,11 +457,13 @@ impl OAuth {
}

#[cfg(test)]
mod test {
pub mod test {
use crate::{alphabets, generate_random_string, Credentials};
use std::collections::HashSet;
use wasm_bindgen_test::*;

#[test]
#[wasm_bindgen_test]
fn test_generate_random_string() {
let mut containers = HashSet::new();
for _ in 1..101 {
Expand All @@ -456,6 +473,7 @@ mod test {
}

#[test]
#[wasm_bindgen_test]
fn test_basic_auth() {
let creds = Credentials::new_pkce("ramsay");
let headers = creds.auth_headers();
Expand Down
Loading

0 comments on commit 0849ff3

Please sign in to comment.