Skip to content
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

adds AWC as client option. Need test work for Document #426

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ 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.22.1" }

awc = {version = "3.1.0" }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
futures = "0.3"
Expand All @@ -37,9 +37,11 @@ web-sys = { version = "0.3", features = ["RequestInit", "Headers", "Window", "Re
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"


[features]
default = ["isahc-static-curl"]
isahc-static-curl = ["isahc/static-curl"]
awc = ["awc/rustls"]

[dev-dependencies]
env_logger = "0.10"
Expand Down
52 changes: 47 additions & 5 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,24 @@ pub enum Error {
InvalidTenantToken(#[from] jsonwebtoken::errors::Error),

/// The http client encountered an error.
#[cfg(feature = "isahc-static-curl")]
#[cfg(not(target_arch = "wasm32"))]
#[error("HTTP request failed: {}", .0)]
HttpError(isahc::Error),

/// The http client encountered an error.
#[cfg(feature = "awc")]
#[cfg(not(target_arch = "wasm32"))]
#[error("HTTP request failed: {}", .0)]
HttpError(awc::error::SendRequestError),

/// The http client encountered an error parsing response.
#[cfg(feature = "awc")]
#[cfg(not(target_arch = "wasm32"))]
#[error("HTTP request failed: {}", .0)]
// TODO: ? make two error types (`awc::error::SendRequestError`, `awc::error::JsonPayloadError`) or map jsonerror into parse error?
ResponseParseError(awc::error::JsonPayloadError),

/// The http client encountered an error.
#[cfg(target_arch = "wasm32")]
#[error("HTTP request failed: {}", .0)]
Expand Down Expand Up @@ -240,6 +254,7 @@ impl std::fmt::Display for ErrorCode {
}
}

#[cfg(feature = "isahc-static-curl")]
#[cfg(not(target_arch = "wasm32"))]
impl From<isahc::Error> for Error {
fn from(error: isahc::Error) -> Error {
Expand All @@ -250,6 +265,24 @@ impl From<isahc::Error> for Error {
}
}
}
#[cfg(feature = "awc")]
#[cfg(not(target_arch = "wasm32"))]
impl From<awc::error::SendRequestError> for Error {
fn from(error: awc::error::SendRequestError) -> Error {
use awc::error::SendRequestError::*;
match error {
Url(_) => Error::InvalidRequest,
Send(e) => {
if e.kind() == std::io::ErrorKind::ConnectionRefused {
Error::UnreachableServer
} else {
Error::HttpError(awc::error::SendRequestError::Send(e))
}
}
other => Error::HttpError(other),
}
}
}

#[cfg(test)]
mod test {
Expand Down Expand Up @@ -351,11 +384,20 @@ mod test {
"Error parsing response JSON: invalid type: map, expected a string at line 2 column 8"
);

let error = Error::HttpError(isahc::post("test_url", "test_body").unwrap_err());
assert_eq!(
error.to_string(),
"HTTP request failed: failed to resolve host name"
);
#[cfg(feature = "isahc-static-curl")]
{
let error = Error::HttpError(isahc::post("test_url", "test_body").unwrap_err());
assert_eq!(
error.to_string(),
"HTTP request failed: failed to resolve host name"
);
}

#[cfg(feature = "awc")]
{
// TODO
}


let error = Error::InvalidTenantToken(jsonwebtoken::errors::Error::from(InvalidToken));
assert_eq!(
Expand Down
2 changes: 2 additions & 0 deletions src/indexes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ impl Index {
/// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
/// # });
/// ```
#[cfg(feature = "isahc-static-curl")] // AWC: TODO
#[cfg(not(target_arch = "wasm32"))]
pub async fn add_or_replace_unchecked_payload<
T: futures_io::AsyncRead + Send + Sync + 'static,
Expand Down Expand Up @@ -752,6 +753,7 @@ impl Index {
/// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
/// # });
/// ```
#[cfg(feature = "isahc-static-curl")] // AWC: TODO
#[cfg(not(target_arch = "wasm32"))]
pub async fn add_or_update_unchecked_payload<
T: futures_io::AsyncRead + Send + Sync + 'static,
Expand Down
184 changes: 184 additions & 0 deletions src/request.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::errors::{Error, MeilisearchError};
use log::{error, trace, warn};
use serde::{de::DeserializeOwned, Serialize};
#[cfg(not(feature = "awc"))]
use serde_json::{from_str, to_string};

#[derive(Debug)]
Expand All @@ -23,6 +24,7 @@ pub fn add_query_parameters<Query: Serialize>(url: &str, query: &Query) -> Resul
}
}

#[cfg(feature = "isahc-static-curl")]
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn request<
Query: Serialize,
Expand Down Expand Up @@ -115,6 +117,7 @@ pub(crate) async fn request<
parse_response(status, expected_status_code, body)
}

#[cfg(feature = "isahc-static-curl")]
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn stream_request<
'a,
Expand Down Expand Up @@ -318,6 +321,187 @@ pub(crate) async fn request<
}
}

#[cfg(feature = "awc")]
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn request<
Query: Serialize,
Body: Serialize,
Output: DeserializeOwned + 'static,
>(
url: &str,
apikey: &str,
method: Method<Query, Body>,
expected_status_code: u16,
) -> Result<Output, Error> {
use awc::{error::JsonPayloadError, Client};

let client = Client::builder()
.add_default_header(("User-Agent".to_string(), qualified_version()))
.bearer_auth(apikey)
.finish();

let mut response = match &method {
Method::Get { query } => {
let url = add_query_parameters(url, query)?;

client.get(url).send().await?
}
Method::Delete { query } => {
let url = add_query_parameters(url, query)?;

client.delete(url).send().await?
}
Method::Post { query, body } => {
let url = add_query_parameters(url, query)?;
client.post(url).send_json(body).await?
}
Method::Patch { query, body } => {
let url = add_query_parameters(url, query)?;
client.patch(url).send_json(body).await?
}
Method::Put { query, body } => {
let url = add_query_parameters(url, query)?;
client.put(url).send_json(body).await?
}
};

let status_code = response.status().as_u16();
if status_code == expected_status_code {
response.json::<Output>().await.map_err(|e| {
error!("Request succeeded but failed to parse response");

match e {
JsonPayloadError::Deserialize(err) => Error::ParseError(err),
other => Error::ResponseParseError(other),
}
})
} else {
// TODO: create issue where it is clear what the HTTP error is
// ParseError(Error("invalid type: null, expected struct MeilisearchError", line: 1, column: 4))

warn!(
"Expected response code {}, got {}",
expected_status_code, status_code
);
match response.json::<MeilisearchError>().await {
Ok(e) => Err(Error::from(e)),
Err(e) => match e {
JsonPayloadError::Deserialize(err) => Err(Error::ParseError(err)),
other => Err(Error::ResponseParseError(other)),
},
}
}
}

#[cfg(target_arch = "wasm32")]
pub fn add_query_parameters<Query: Serialize>(
mut url: String,
query: &Query,
) -> Result<String, Error> {
let query = yaup::to_string(query)?;

if !query.is_empty() {
url = format!("{}?{}", url, query);
};
return Ok(url);
}
#[cfg(target_arch = "wasm32")]
pub(crate) async fn request<
Query: Serialize,
Body: Serialize,
Output: DeserializeOwned + 'static,
>(
url: &str,
apikey: &str,
method: Method<Query, Body>,
expected_status_code: u16,
) -> Result<Output, Error> {
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Headers, RequestInit, Response};

const CONTENT_TYPE: &str = "Content-Type";
const JSON: &str = "application/json";

// The 2 following unwraps should not be able to fail
let mut mut_url = url.clone().to_string();
let headers = Headers::new().unwrap();
headers
.append("Authorization", format!("Bearer {}", apikey).as_str())
.unwrap();
headers
.append("X-Meilisearch-Client", qualified_version().as_str())
.unwrap();

let mut request: RequestInit = RequestInit::new();
request.headers(&headers);

match &method {
Method::Get { query } => {
mut_url = add_query_parameters(mut_url, &query)?;

request.method("GET");
}
Method::Delete { query } => {
mut_url = add_query_parameters(mut_url, &query)?;
request.method("DELETE");
}
Method::Patch { query, body } => {
mut_url = add_query_parameters(mut_url, &query)?;
request.method("PATCH");
headers.append(CONTENT_TYPE, JSON).unwrap();
request.body(Some(&JsValue::from_str(&to_string(body).unwrap())));
}
Method::Post { query, body } => {
mut_url = add_query_parameters(mut_url, &query)?;
request.method("POST");
headers.append(CONTENT_TYPE, JSON).unwrap();
request.body(Some(&JsValue::from_str(&to_string(body).unwrap())));
}
Method::Put { query, body } => {
mut_url = add_query_parameters(mut_url, &query)?;
request.method("PUT");
headers.append(CONTENT_TYPE, JSON).unwrap();
request.body(Some(&JsValue::from_str(&to_string(body).unwrap())));
}
}

let window = web_sys::window().unwrap(); // TODO remove this unwrap
let response =
match JsFuture::from(window.fetch_with_str_and_init(mut_url.as_str(), &request)).await {
Ok(response) => Response::from(response),
Err(e) => {
error!("Network error: {:?}", e);
return Err(Error::UnreachableServer);
}
};
let status = response.status() as u16;
let text = match response.text() {
Ok(text) => match JsFuture::from(text).await {
Ok(text) => text,
Err(e) => {
error!("Invalid response: {:?}", e);
return Err(Error::HttpError("Invalid response".to_string()));
}
},
Err(e) => {
error!("Invalid response: {:?}", e);
return Err(Error::HttpError("Invalid response".to_string()));
}
};

if let Some(t) = text.as_string() {
if t.is_empty() {
parse_response(status, expected_status_code, String::from("null"))
} else {
parse_response(status, expected_status_code, t)
}
} else {
error!("Invalid response");
Err(Error::HttpError("Invalid utf8".to_string()))
}
}
#[cfg(not(feature = "awc"))]
fn parse_response<Output: DeserializeOwned>(
status_code: u16,
expected_status_code: u16,
Expand Down