Skip to content

Commit

Permalink
Merge #563
Browse files Browse the repository at this point in the history
563: Use reqwest by default r=irevoire a=irevoire

# Pull Request

## Related issue
Fixes #530 because we won’t depend on curl anymore

## What does this PR do?
- Get rid of `isahc` by default in favor of `reqwest`, which is way more used in the ecosystem

## PR checklist
Please check if your PR fulfills the following requirements:
- [x] Create an example changing the default HTTP client
- [x] Simplify the `HttpClient` trait to only require one method to be implemented
- [x] Stores a basic `reqwest::Client` in the `meilisearch_sdk::Client` instead of re-creating it from scratch for every request
- [x] Double check it works with wasm
- [x] Remove all the unwraps in `request`
- [x] Do not use the `User-Agent` when in wasm as it is often blocked by the browser


Co-authored-by: Tamo <[email protected]>
  • Loading branch information
meili-bors[bot] and irevoire authored Apr 15, 2024
2 parents 80f6326 + 352f846 commit 6530553
Show file tree
Hide file tree
Showing 28 changed files with 923 additions and 1,294 deletions.
23 changes: 12 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,28 @@ log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing"] }
jsonwebtoken = { version = "9", default-features = false }
yaup = "0.2.0"
either = { version = "1.8.0", features = ["serde"] }
thiserror = "1.0.37"
meilisearch-index-setting-macro = { path = "meilisearch-index-setting-macro", version = "0.25.0" }
pin-project-lite = { version = "0.2.13", optional = true }
reqwest = { version = "0.12.3", optional = true, default-features = false, features = ["rustls-tls", "http2", "stream"] }
bytes = { version = "1.6", optional = true }
uuid = { version = "1.1.2", features = ["v4"] }
futures-io = "0.3.30"
futures = "0.3"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
futures = "0.3"
futures-io = "0.3.26"
isahc = { version = "1.0", features = ["http2", "text-decoding"], optional = true, default_features = false }
uuid = { version = "1.1.2", features = ["v4"] }
jsonwebtoken = { version = "9", default-features = false }

[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = "0.3.47"
web-sys = { version = "0.3", features = ["RequestInit", "Headers", "Window", "Response", "console"] }
wasm-bindgen = "0.2"
uuid = { version = "1.8.0", default-features = false, features = ["v4", "js"] }
web-sys = "0.3"
wasm-bindgen-futures = "0.4"

[features]
default = ["isahc", "isahc", "isahc-static-curl"]
isahc-static-curl = ["isahc", "isahc", "isahc/static-curl"]
isahc-static-ssl = ["isahc/static-ssl"]
default = ["reqwest"]
reqwest = ["dep:reqwest", "pin-project-lite", "bytes"]

[dev-dependencies]
futures-await-test = "0.3"
Expand All @@ -56,3 +56,4 @@ lazy_static = "1.4"
web-sys = "0.3"
console_error_panic_hook = "0.1"
big_s = "1.0.2"
insta = "1.38.0"
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,10 @@ struct Movie {
}


fn main() { block_on(async move {
#[tokio::main(flavor = "current_thread")]
async fn main() {
// Create a client (without sending any request so that can't fail)
let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY));
let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();

// An index is where the documents are stored.
let movies = client.index("movies");
Expand All @@ -124,7 +125,7 @@ fn main() { block_on(async move {
Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] },
Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] },
], Some("id")).await.unwrap();
})}
}
```

With the `uid`, you can check the status (`enqueued`, `canceled`, `processing`, `succeeded` or `failed`) of your documents addition using the [task](https://www.meilisearch.com/docs/reference/api/tasks#get-task).
Expand Down Expand Up @@ -238,11 +239,11 @@ Json output:
}
```

#### Using users customized HttpClient <!-- omit in TOC -->
#### Customize the `HttpClient` <!-- omit in TOC -->

If you want to change the `HttpClient` you can incorporate using the `Client::new_with_client` method.
To use it, you need to implement the `HttpClient Trait`(`isahc` is used by default).
There are [using-reqwest-example](./examples/cli-app-with-reqwest) of using `reqwest`.
By default, the SDK uses [`reqwest`](https://docs.rs/reqwest/latest/reqwest/) to make http calls.
The SDK lets you customize the http client by implementing the `HttpClient` trait yourself and
initializing the `Client` with the `new_with_client` method.

## 🌐 Running in the Browser with WASM <!-- omit in TOC -->

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "cli-app-with-reqwest"
name = "cli-app-with-awc"
version = "0.0.0"
edition = "2021"
publish = false
Expand All @@ -12,6 +12,9 @@ futures = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
lazy_static = "1.4.0"
reqwest = "0.11.16"
awc = "3.4"
async-trait = "0.1.51"
tokio = { version = "1.27.0", features = ["full"] }
tokio = { version = "1.27.0", features = ["full"] }
yaup = "0.2.0"
tokio-util = { version = "0.7.10", features = ["full"] }
actix-rt = "2.9.0"
257 changes: 257 additions & 0 deletions examples/cli-app-with-awc/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
use async_trait::async_trait;
use meilisearch_sdk::errors::Error;
use meilisearch_sdk::request::{parse_response, HttpClient, Method};
use meilisearch_sdk::{client::*, settings::Settings};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::io::stdin;

#[derive(Debug, Clone)]
pub struct AwcClient {
api_key: Option<String>,
}

impl AwcClient {
pub fn new(api_key: Option<&str>) -> Result<Self, Error> {
Ok(AwcClient {
api_key: api_key.map(|key| key.to_string()),
})
}
}

#[async_trait(?Send)]
impl HttpClient for AwcClient {
async fn stream_request<
Query: Serialize + Send + Sync,
Body: futures::AsyncRead + Send + Sync + 'static,
Output: DeserializeOwned + 'static,
>(
&self,
url: &str,
method: Method<Query, Body>,
content_type: &str,
expected_status_code: u16,
) -> Result<Output, Error> {
let mut builder = awc::ClientBuilder::new();
if let Some(ref api_key) = self.api_key {
builder = builder.bearer_auth(api_key);
}
builder = builder.add_default_header(("User-Agent", "Rust client with Awc"));
let client = builder.finish();

let query = method.query();
let query = yaup::to_string(query)?;

let url = if query.is_empty() {
url.to_string()
} else {
format!("{url}?{query}")
};

let url = add_query_parameters(&url, method.query())?;
let request = client.request(verb(&method), &url);

let mut response = if let Some(body) = method.into_body() {
let reader = tokio_util::compat::FuturesAsyncReadCompatExt::compat(body);
let stream = tokio_util::io::ReaderStream::new(reader);
request
.content_type(content_type)
.send_stream(stream)
.await
.map_err(|err| Error::Other(Box::new(err)))?
} else {
request
.send()
.await
.map_err(|err| Error::Other(Box::new(err)))?
};

let status = response.status().as_u16();
let mut body = String::from_utf8(
response
.body()
.await
.map_err(|err| Error::Other(Box::new(err)))?
.to_vec(),
)
.map_err(|err| Error::Other(Box::new(err)))?;

if body.is_empty() {
body = "null".to_string();
}

parse_response(status, expected_status_code, &body, url.to_string())
}
}

#[actix_rt::main]
async fn main() {
let http_client = AwcClient::new(Some("masterKey")).unwrap();
let client = Client::new_with_client("http://localhost:7700", Some("masterKey"), http_client);

// build the index
build_index(&client).await;

// enter in search queries or quit
loop {
println!("Enter a search query or type \"q\" or \"quit\" to quit:");
let mut input_string = String::new();
stdin()
.read_line(&mut input_string)
.expect("Failed to read line");
match input_string.trim() {
"quit" | "q" | "" => {
println!("exiting...");
break;
}
_ => {
search(&client, input_string.trim()).await;
}
}
}
// get rid of the index at the end, doing this only so users don't have the index without knowing
let _ = client.delete_index("clothes").await.unwrap();
}

async fn search(client: &Client<AwcClient>, query: &str) {
// make the search query, which excutes and serializes hits into the
// ClothesDisplay struct
let query_results = client
.index("clothes")
.search()
.with_query(query)
.execute::<ClothesDisplay>()
.await
.unwrap()
.hits;

// display the query results
if query_results.is_empty() {
println!("no results...");
} else {
for clothes in query_results {
let display = clothes.result;
println!("{}", format_args!("{}", display));
}
}
}

async fn build_index(client: &Client<AwcClient>) {
// reading and parsing the file
let content = include_str!("../assets/clothes.json");

// serialize the string to clothes objects
let clothes: Vec<Clothes> = serde_json::from_str(content).unwrap();

//create displayed attributes
let displayed_attributes = ["article", "cost", "size", "pattern"];

// Create ranking rules
let ranking_rules = ["words", "typo", "attribute", "exactness", "cost:asc"];

//create searchable attributes
let searchable_attributes = ["seaon", "article", "size", "pattern"];

// create the synonyms hashmap
let mut synonyms = std::collections::HashMap::new();
synonyms.insert("sweater", vec!["cardigan", "long-sleeve"]);
synonyms.insert("sweat pants", vec!["joggers", "gym pants"]);
synonyms.insert("t-shirt", vec!["tees", "tshirt"]);

//create the settings struct
let settings = Settings::new()
.with_ranking_rules(ranking_rules)
.with_searchable_attributes(searchable_attributes)
.with_displayed_attributes(displayed_attributes)
.with_synonyms(synonyms);

//add the settings to the index
let result = client
.index("clothes")
.set_settings(&settings)
.await
.unwrap()
.wait_for_completion(client, None, None)
.await
.unwrap();

if result.is_failure() {
panic!(
"Encountered an error while setting settings for index: {:?}",
result.unwrap_failure()
);
}

// add the documents
let result = client
.index("clothes")
.add_or_update(&clothes, Some("id"))
.await
.unwrap()
.wait_for_completion(client, None, None)
.await
.unwrap();

if result.is_failure() {
panic!(
"Encountered an error while sending the documents: {:?}",
result.unwrap_failure()
);
}
}

/// Base search object.
#[derive(Serialize, Deserialize, Debug)]
pub struct Clothes {
id: usize,
seaon: String,
article: String,
cost: f32,
size: String,
pattern: String,
}

/// Search results get serialized to this struct
#[derive(Serialize, Deserialize, Debug)]
pub struct ClothesDisplay {
article: String,
cost: f32,
size: String,
pattern: String,
}

impl fmt::Display for ClothesDisplay {
// This trait requires `fmt` with this exact signature.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Write strictly the first element into the supplied output
// stream: `f`. Returns `fmt::Result` which indicates whether the
// operation succeeded or failed. Note that `write!` uses syntax which
// is very similar to `println!`.
write!(
f,
"result\n article: {},\n price: {},\n size: {},\n pattern: {}\n",
self.article, self.cost, self.size, self.pattern
)
}
}

fn add_query_parameters<Query: Serialize>(url: &str, query: &Query) -> Result<String, Error> {
let query = yaup::to_string(query)?;

if query.is_empty() {
Ok(url.to_string())
} else {
Ok(format!("{url}?{query}"))
}
}

fn verb<Q, B>(method: &Method<Q, B>) -> awc::http::Method {
match method {
Method::Get { .. } => awc::http::Method::GET,
Method::Delete { .. } => awc::http::Method::DELETE,
Method::Post { .. } => awc::http::Method::POST,
Method::Put { .. } => awc::http::Method::PUT,
Method::Patch { .. } => awc::http::Method::PATCH,
}
}
Loading

0 comments on commit 6530553

Please sign in to comment.