Skip to content

Commit 6530553

Browse files
Merge #563
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]>
2 parents 80f6326 + 352f846 commit 6530553

File tree

28 files changed

+923
-1294
lines changed

28 files changed

+923
-1294
lines changed

Cargo.toml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,28 @@ log = "0.4"
1818
serde = { version = "1.0", features = ["derive"] }
1919
serde_json = "1.0"
2020
time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing"] }
21-
jsonwebtoken = { version = "9", default-features = false }
2221
yaup = "0.2.0"
2322
either = { version = "1.8.0", features = ["serde"] }
2423
thiserror = "1.0.37"
2524
meilisearch-index-setting-macro = { path = "meilisearch-index-setting-macro", version = "0.25.0" }
25+
pin-project-lite = { version = "0.2.13", optional = true }
26+
reqwest = { version = "0.12.3", optional = true, default-features = false, features = ["rustls-tls", "http2", "stream"] }
27+
bytes = { version = "1.6", optional = true }
28+
uuid = { version = "1.1.2", features = ["v4"] }
29+
futures-io = "0.3.30"
30+
futures = "0.3"
2631

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

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

3940
[features]
40-
default = ["isahc", "isahc", "isahc-static-curl"]
41-
isahc-static-curl = ["isahc", "isahc", "isahc/static-curl"]
42-
isahc-static-ssl = ["isahc/static-ssl"]
41+
default = ["reqwest"]
42+
reqwest = ["dep:reqwest", "pin-project-lite", "bytes"]
4343

4444
[dev-dependencies]
4545
futures-await-test = "0.3"
@@ -56,3 +56,4 @@ lazy_static = "1.4"
5656
web-sys = "0.3"
5757
console_error_panic_hook = "0.1"
5858
big_s = "1.0.2"
59+
insta = "1.38.0"

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,10 @@ struct Movie {
108108
}
109109

110110

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

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

130131
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).
@@ -238,11 +239,11 @@ Json output:
238239
}
239240
```
240241

241-
#### Using users customized HttpClient <!-- omit in TOC -->
242+
#### Customize the `HttpClient` <!-- omit in TOC -->
242243

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

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

examples/cli-app-with-reqwest/Cargo.toml renamed to examples/cli-app-with-awc/Cargo.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "cli-app-with-reqwest"
2+
name = "cli-app-with-awc"
33
version = "0.0.0"
44
edition = "2021"
55
publish = false
@@ -12,6 +12,9 @@ futures = "0.3"
1212
serde = { version = "1.0", features = ["derive"] }
1313
serde_json = "1.0"
1414
lazy_static = "1.4.0"
15-
reqwest = "0.11.16"
15+
awc = "3.4"
1616
async-trait = "0.1.51"
17-
tokio = { version = "1.27.0", features = ["full"] }
17+
tokio = { version = "1.27.0", features = ["full"] }
18+
yaup = "0.2.0"
19+
tokio-util = { version = "0.7.10", features = ["full"] }
20+
actix-rt = "2.9.0"

examples/cli-app-with-awc/src/main.rs

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
use async_trait::async_trait;
2+
use meilisearch_sdk::errors::Error;
3+
use meilisearch_sdk::request::{parse_response, HttpClient, Method};
4+
use meilisearch_sdk::{client::*, settings::Settings};
5+
use serde::de::DeserializeOwned;
6+
use serde::{Deserialize, Serialize};
7+
use std::fmt;
8+
use std::io::stdin;
9+
10+
#[derive(Debug, Clone)]
11+
pub struct AwcClient {
12+
api_key: Option<String>,
13+
}
14+
15+
impl AwcClient {
16+
pub fn new(api_key: Option<&str>) -> Result<Self, Error> {
17+
Ok(AwcClient {
18+
api_key: api_key.map(|key| key.to_string()),
19+
})
20+
}
21+
}
22+
23+
#[async_trait(?Send)]
24+
impl HttpClient for AwcClient {
25+
async fn stream_request<
26+
Query: Serialize + Send + Sync,
27+
Body: futures::AsyncRead + Send + Sync + 'static,
28+
Output: DeserializeOwned + 'static,
29+
>(
30+
&self,
31+
url: &str,
32+
method: Method<Query, Body>,
33+
content_type: &str,
34+
expected_status_code: u16,
35+
) -> Result<Output, Error> {
36+
let mut builder = awc::ClientBuilder::new();
37+
if let Some(ref api_key) = self.api_key {
38+
builder = builder.bearer_auth(api_key);
39+
}
40+
builder = builder.add_default_header(("User-Agent", "Rust client with Awc"));
41+
let client = builder.finish();
42+
43+
let query = method.query();
44+
let query = yaup::to_string(query)?;
45+
46+
let url = if query.is_empty() {
47+
url.to_string()
48+
} else {
49+
format!("{url}?{query}")
50+
};
51+
52+
let url = add_query_parameters(&url, method.query())?;
53+
let request = client.request(verb(&method), &url);
54+
55+
let mut response = if let Some(body) = method.into_body() {
56+
let reader = tokio_util::compat::FuturesAsyncReadCompatExt::compat(body);
57+
let stream = tokio_util::io::ReaderStream::new(reader);
58+
request
59+
.content_type(content_type)
60+
.send_stream(stream)
61+
.await
62+
.map_err(|err| Error::Other(Box::new(err)))?
63+
} else {
64+
request
65+
.send()
66+
.await
67+
.map_err(|err| Error::Other(Box::new(err)))?
68+
};
69+
70+
let status = response.status().as_u16();
71+
let mut body = String::from_utf8(
72+
response
73+
.body()
74+
.await
75+
.map_err(|err| Error::Other(Box::new(err)))?
76+
.to_vec(),
77+
)
78+
.map_err(|err| Error::Other(Box::new(err)))?;
79+
80+
if body.is_empty() {
81+
body = "null".to_string();
82+
}
83+
84+
parse_response(status, expected_status_code, &body, url.to_string())
85+
}
86+
}
87+
88+
#[actix_rt::main]
89+
async fn main() {
90+
let http_client = AwcClient::new(Some("masterKey")).unwrap();
91+
let client = Client::new_with_client("http://localhost:7700", Some("masterKey"), http_client);
92+
93+
// build the index
94+
build_index(&client).await;
95+
96+
// enter in search queries or quit
97+
loop {
98+
println!("Enter a search query or type \"q\" or \"quit\" to quit:");
99+
let mut input_string = String::new();
100+
stdin()
101+
.read_line(&mut input_string)
102+
.expect("Failed to read line");
103+
match input_string.trim() {
104+
"quit" | "q" | "" => {
105+
println!("exiting...");
106+
break;
107+
}
108+
_ => {
109+
search(&client, input_string.trim()).await;
110+
}
111+
}
112+
}
113+
// get rid of the index at the end, doing this only so users don't have the index without knowing
114+
let _ = client.delete_index("clothes").await.unwrap();
115+
}
116+
117+
async fn search(client: &Client<AwcClient>, query: &str) {
118+
// make the search query, which excutes and serializes hits into the
119+
// ClothesDisplay struct
120+
let query_results = client
121+
.index("clothes")
122+
.search()
123+
.with_query(query)
124+
.execute::<ClothesDisplay>()
125+
.await
126+
.unwrap()
127+
.hits;
128+
129+
// display the query results
130+
if query_results.is_empty() {
131+
println!("no results...");
132+
} else {
133+
for clothes in query_results {
134+
let display = clothes.result;
135+
println!("{}", format_args!("{}", display));
136+
}
137+
}
138+
}
139+
140+
async fn build_index(client: &Client<AwcClient>) {
141+
// reading and parsing the file
142+
let content = include_str!("../assets/clothes.json");
143+
144+
// serialize the string to clothes objects
145+
let clothes: Vec<Clothes> = serde_json::from_str(content).unwrap();
146+
147+
//create displayed attributes
148+
let displayed_attributes = ["article", "cost", "size", "pattern"];
149+
150+
// Create ranking rules
151+
let ranking_rules = ["words", "typo", "attribute", "exactness", "cost:asc"];
152+
153+
//create searchable attributes
154+
let searchable_attributes = ["seaon", "article", "size", "pattern"];
155+
156+
// create the synonyms hashmap
157+
let mut synonyms = std::collections::HashMap::new();
158+
synonyms.insert("sweater", vec!["cardigan", "long-sleeve"]);
159+
synonyms.insert("sweat pants", vec!["joggers", "gym pants"]);
160+
synonyms.insert("t-shirt", vec!["tees", "tshirt"]);
161+
162+
//create the settings struct
163+
let settings = Settings::new()
164+
.with_ranking_rules(ranking_rules)
165+
.with_searchable_attributes(searchable_attributes)
166+
.with_displayed_attributes(displayed_attributes)
167+
.with_synonyms(synonyms);
168+
169+
//add the settings to the index
170+
let result = client
171+
.index("clothes")
172+
.set_settings(&settings)
173+
.await
174+
.unwrap()
175+
.wait_for_completion(client, None, None)
176+
.await
177+
.unwrap();
178+
179+
if result.is_failure() {
180+
panic!(
181+
"Encountered an error while setting settings for index: {:?}",
182+
result.unwrap_failure()
183+
);
184+
}
185+
186+
// add the documents
187+
let result = client
188+
.index("clothes")
189+
.add_or_update(&clothes, Some("id"))
190+
.await
191+
.unwrap()
192+
.wait_for_completion(client, None, None)
193+
.await
194+
.unwrap();
195+
196+
if result.is_failure() {
197+
panic!(
198+
"Encountered an error while sending the documents: {:?}",
199+
result.unwrap_failure()
200+
);
201+
}
202+
}
203+
204+
/// Base search object.
205+
#[derive(Serialize, Deserialize, Debug)]
206+
pub struct Clothes {
207+
id: usize,
208+
seaon: String,
209+
article: String,
210+
cost: f32,
211+
size: String,
212+
pattern: String,
213+
}
214+
215+
/// Search results get serialized to this struct
216+
#[derive(Serialize, Deserialize, Debug)]
217+
pub struct ClothesDisplay {
218+
article: String,
219+
cost: f32,
220+
size: String,
221+
pattern: String,
222+
}
223+
224+
impl fmt::Display for ClothesDisplay {
225+
// This trait requires `fmt` with this exact signature.
226+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
227+
// Write strictly the first element into the supplied output
228+
// stream: `f`. Returns `fmt::Result` which indicates whether the
229+
// operation succeeded or failed. Note that `write!` uses syntax which
230+
// is very similar to `println!`.
231+
write!(
232+
f,
233+
"result\n article: {},\n price: {},\n size: {},\n pattern: {}\n",
234+
self.article, self.cost, self.size, self.pattern
235+
)
236+
}
237+
}
238+
239+
fn add_query_parameters<Query: Serialize>(url: &str, query: &Query) -> Result<String, Error> {
240+
let query = yaup::to_string(query)?;
241+
242+
if query.is_empty() {
243+
Ok(url.to_string())
244+
} else {
245+
Ok(format!("{url}?{query}"))
246+
}
247+
}
248+
249+
fn verb<Q, B>(method: &Method<Q, B>) -> awc::http::Method {
250+
match method {
251+
Method::Get { .. } => awc::http::Method::GET,
252+
Method::Delete { .. } => awc::http::Method::DELETE,
253+
Method::Post { .. } => awc::http::Method::POST,
254+
Method::Put { .. } => awc::http::Method::PUT,
255+
Method::Patch { .. } => awc::http::Method::PATCH,
256+
}
257+
}

0 commit comments

Comments
 (0)