Skip to content

Commit 86f1dde

Browse files
authored
feat: create Client and ClientBuilder structs (#11)
* feat: create Client and ClientBuilder structs * refactor: associate HttpLowLevel with struct from the http crate * use bytes::Bytes instead of Vec<u8> Co-authored-by: Luis Moreno <[email protected]>
1 parent a6573df commit 86f1dde

File tree

9 files changed

+288
-31
lines changed

9 files changed

+288
-31
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ crate-type = ["cdylib", "rlib"]
1313

1414
[dependencies]
1515
async-trait = "0.1.50"
16+
bytes = "1.0.1"
1617
http = "0.2.4"
1718
thiserror = "1.0.24"
1819

mocks/get_api_key_header.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
when:
2+
method: GET
3+
path: /test_api_key
4+
header:
5+
- name: X-TYPESENSE-API-KEY
6+
value: VerySecretKey
7+
then:
8+
status: 200
9+
header:
10+
- name: content-type
11+
value: text/html
12+
- name: Access-Control-Allow-Origin
13+
value: \*
14+
body: "Test with api key successful"

mocks/post_api_key.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
when:
2+
method: POST
3+
path: /test_api_key
4+
header:
5+
- name: X-TYPESENSE-API-KEY
6+
value: VerySecretKey
7+
body: "Test with api key successful"
8+
then:
9+
status: 200
10+
header:
11+
- name: content-type
12+
value: text/html
13+
body: "Test with api key successful"

src/client/builder.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use super::Client;
2+
use crate::transport::Transport;
3+
4+
#[cfg(target_arch = "wasm32")]
5+
use crate::transport::WasmClient;
6+
7+
use crate::{Result, TypesenseError};
8+
/// Builder for the Typesense [`Client`]
9+
pub struct ClientBuilder<'a, T> {
10+
transport: Option<Transport<T>>,
11+
host: Option<&'a str>,
12+
api_key: Option<&'a str>,
13+
}
14+
15+
impl<'a, T> ClientBuilder<'a, T> {
16+
/// build [`Client`] with the current configurations. Return [`typesense::TypesenseError::ConfigError`]
17+
/// if a configuration is missing.
18+
pub fn build(self) -> Result<Client<'a, T>> {
19+
Ok(Client {
20+
transport: self.transport.ok_or_else(|| {
21+
TypesenseError::ConfigError("missing client transport".to_string())
22+
})?,
23+
host: self
24+
.host
25+
.ok_or_else(|| TypesenseError::ConfigError("missing client host".to_string()))?,
26+
api_key: self
27+
.api_key
28+
.ok_or_else(|| TypesenseError::ConfigError("missing client api key".to_string()))?,
29+
})
30+
}
31+
32+
/// Set host
33+
pub fn host(mut self, host: &'a str) -> Self {
34+
self.host = Some(host);
35+
self
36+
}
37+
38+
/// Set api key
39+
pub fn api_key(mut self, api_key: &'a str) -> Self {
40+
self.api_key = Some(api_key);
41+
self
42+
}
43+
44+
/// Set transport
45+
pub fn transport(mut self, transport: Transport<T>) -> Self {
46+
self.transport = Some(transport);
47+
self
48+
}
49+
}
50+
51+
impl<'a, T> Default for ClientBuilder<'a, T> {
52+
fn default() -> Self {
53+
Self {
54+
transport: None,
55+
host: None,
56+
api_key: None,
57+
}
58+
}
59+
}
60+
61+
#[cfg(all(feature = "tokio-rt", not(target_arch = "wasm32")))]
62+
#[cfg_attr(
63+
docsrs,
64+
doc(cfg(all(feature = "tokio-rt", not(target_arch = "wasm32"))))
65+
)]
66+
impl<'a> ClientBuilder<'a, crate::transport::HyperHttpsClient> {
67+
/// Create client builder with a [`hyper`](https://docs.rs/hyper) client.
68+
/// The connector used is [`HttpsConnector`](hyper_tls::HttpsConnector).
69+
pub fn new_hyper() -> Self {
70+
let transport = Some(crate::transport::TransportBuilder::new_hyper().build());
71+
Self {
72+
transport,
73+
host: None,
74+
api_key: None,
75+
}
76+
}
77+
}
78+
79+
#[cfg(target_arch = "wasm32")]
80+
#[cfg_attr(docsrs, doc(cfg(target_arch = "wasm32")))]
81+
impl<'a> ClientBuilder<'a, WasmClient> {
82+
/// Create client builder using default wasm client
83+
pub fn new_wasm() -> Self {
84+
let transport = Some(crate::transport::TransportBuilder::new_wasm().build());
85+
Self {
86+
transport,
87+
host: None,
88+
api_key: None,
89+
}
90+
}
91+
}

src/client/mod.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use bytes::Bytes;
2+
3+
use crate::transport::HttpLowLevel;
4+
use crate::transport::Transport;
5+
use crate::Result;
6+
7+
mod builder;
8+
pub use builder::ClientBuilder;
9+
10+
#[allow(dead_code)]
11+
pub const TYPESENSE_API_KEY_HEADER_NAME: &str = "X-TYPESENSE-API-KEY";
12+
13+
/// Root client for top level APIs
14+
pub struct Client<'a, T> {
15+
transport: Transport<T>,
16+
host: &'a str,
17+
api_key: &'a str,
18+
}
19+
20+
impl<'a, T> Client<'a, T> {
21+
/// Gets the transport of the client
22+
pub fn transport(&self) -> &Transport<T> {
23+
&self.transport
24+
}
25+
}
26+
27+
#[allow(dead_code)]
28+
impl<'a, C> Client<'a, C>
29+
where
30+
C: HttpLowLevel,
31+
{
32+
pub(crate) async fn send(
33+
&self,
34+
method: http::Method,
35+
path: &str,
36+
body: Bytes,
37+
) -> Result<C::Response> {
38+
let uri = format!("{}{}", self.host, path);
39+
let mut headers = http::HeaderMap::default();
40+
headers.insert(TYPESENSE_API_KEY_HEADER_NAME, self.api_key.parse().unwrap());
41+
self.transport.send(method, &uri, headers, body).await
42+
}
43+
44+
pub(crate) async fn get(&self, path: &str) -> Result<C::Response> {
45+
self.send(http::Method::GET, path, Bytes::new()).await
46+
}
47+
48+
pub(crate) async fn post(&self, path: &str, body: Bytes) -> Result<C::Response> {
49+
self.send(http::Method::POST, path, body).await
50+
}
51+
}
52+
53+
#[cfg(all(test, feature = "tokio-rt", not(target_arch = "wasm32")))]
54+
mod hyper_tests {
55+
use http::StatusCode;
56+
57+
use super::*;
58+
59+
#[tokio::test]
60+
async fn hyper() -> crate::Result<()> {
61+
let body = String::from("Test with api key successful");
62+
let host = "http://localhost:5000";
63+
let api_key = "VerySecretKey";
64+
65+
let client = ClientBuilder::new_hyper()
66+
.host(host)
67+
.api_key(api_key)
68+
.build()
69+
.unwrap();
70+
71+
let response = client.get("/test_api_key").await?;
72+
73+
assert_eq!(response.status(), StatusCode::OK);
74+
let bytes = hyper::body::to_bytes(response).await?;
75+
assert_eq!(bytes, body.as_bytes());
76+
77+
let response = client.post("/test_api_key", body.clone().into()).await?;
78+
79+
assert_eq!(response.status(), StatusCode::OK);
80+
let bytes = hyper::body::to_bytes(response).await?;
81+
assert_eq!(bytes, body.as_bytes());
82+
83+
Ok(())
84+
}
85+
}
86+
87+
#[cfg(all(test, target_arch = "wasm32"))]
88+
mod wasm_test {
89+
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
90+
91+
use http::StatusCode;
92+
use wasm_bindgen::prelude::*;
93+
use wasm_bindgen_test::wasm_bindgen_test;
94+
95+
use super::*;
96+
97+
#[wasm_bindgen]
98+
extern "C" {
99+
#[wasm_bindgen(js_namespace = console)]
100+
fn log(s: &str);
101+
}
102+
103+
macro_rules! console_log {
104+
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
105+
}
106+
107+
#[wasm_bindgen_test]
108+
async fn wasm() {
109+
console_error_panic_hook::set_once();
110+
111+
console_log!("Test Started.");
112+
match try_wasm().await {
113+
Ok(_) => console_log!("Test Successful."),
114+
Err(e) => console_log!("Test failed: {:?}", e),
115+
}
116+
}
117+
118+
async fn try_wasm() -> crate::Result<()> {
119+
let body = String::from("Test with api key successful");
120+
121+
let host = "http://localhost:5000";
122+
let api_key = "VerySecretKey";
123+
124+
let client = ClientBuilder::new_wasm()
125+
.host(host)
126+
.api_key(api_key)
127+
.build()
128+
.unwrap();
129+
130+
let response = client.get("/test_api_key").await?;
131+
132+
assert_eq!(response.status(), StatusCode::OK);
133+
assert_eq!(response.body(), body.as_bytes());
134+
135+
let response = client.post("/test_api_key", body.clone().into()).await?;
136+
137+
assert_eq!(response.status(), StatusCode::OK);
138+
assert_eq!(response.body(), body.as_bytes());
139+
140+
Ok(())
141+
}
142+
}

src/error.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ use thiserror::Error;
66
pub type Result<T> = std::result::Result<T, TypesenseError>;
77

88
/// Represents an error that can occur while using the library.
9+
#[non_exhaustive]
910
#[derive(Error, Debug)]
1011
pub enum TypesenseError {
1112
/// TypesenseClientError
1213
#[error("typesense client error")]
1314
TypesenseClientError,
1415

1516
/// Config error.
16-
#[error("config error")]
17-
ConfigError,
17+
#[error("config error: {0}")]
18+
ConfigError(String),
1819

1920
/// Timeout.
2021
#[error("timeout")]

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
//!
66
//! Welcome to typesense, the rust library for the Typesense API.
77
8+
mod client;
89
mod error;
910
pub mod transport;
1011

12+
pub use client::{Client, ClientBuilder};
1113
pub use error::{Result, TypesenseError};

src/transport/http_low_level.rs

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use async_trait::async_trait;
2+
use bytes::Bytes;
23

34
#[cfg(not(target_arch = "wasm32"))]
45
pub(crate) type HyperClient<C> = hyper::Client<C, hyper::Body>;
@@ -14,26 +15,17 @@ pub(crate) struct WasmClient;
1415

1516
/// A low level HTTP trait.
1617
#[async_trait(?Send)]
17-
pub trait HttpLowLevel {
18-
/// HTTP Method type.
19-
type Method;
20-
21-
/// HTTP Header map type.
22-
type HeaderMap;
23-
24-
/// HTTP Body type.
25-
type Body;
26-
18+
pub trait HttpLowLevel<M = http::Method, H = http::HeaderMap> {
2719
/// HTTP Response type.
2820
type Response;
2921

3022
/// Send a request and receive a response.
3123
async fn send(
3224
&self,
33-
method: Self::Method,
25+
method: M,
3426
uri: &str,
35-
headers: Self::HeaderMap,
36-
body: Self::Body,
27+
headers: H,
28+
body: Bytes,
3729
) -> crate::Result<Self::Response>;
3830
}
3931

@@ -43,17 +35,14 @@ impl<C> HttpLowLevel for HyperClient<C>
4335
where
4436
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
4537
{
46-
type Method = http::Method;
47-
type HeaderMap = http::HeaderMap;
48-
type Body = Vec<u8>;
4938
type Response = http::Response<hyper::Body>;
5039

5140
async fn send(
5241
&self,
53-
method: Self::Method,
42+
method: http::Method,
5443
uri: &str,
55-
headers: Self::HeaderMap,
56-
body: Self::Body,
44+
headers: http::HeaderMap,
45+
body: Bytes,
5746
) -> crate::Result<Self::Response> {
5847
// Making a builder
5948
let mut builder = http::Request::builder().method(method).uri(uri);
@@ -78,17 +67,14 @@ where
7867
#[cfg(target_arch = "wasm32")]
7968
#[async_trait(?Send)]
8069
impl HttpLowLevel for WasmClient {
81-
type Method = http::Method;
82-
type HeaderMap = http::HeaderMap;
83-
type Body = Vec<u8>;
84-
type Response = http::Response<Self::Body>;
70+
type Response = http::Response<Vec<u8>>;
8571

8672
async fn send(
8773
&self,
88-
method: Self::Method,
74+
method: http::Method,
8975
uri: &str,
90-
headers: Self::HeaderMap,
91-
body: Self::Body,
76+
headers: http::HeaderMap,
77+
body: Bytes,
9278
) -> crate::Result<Self::Response> {
9379
use js_sys::{Array, ArrayBuffer, Reflect, Uint8Array};
9480
use wasm_bindgen::JsCast;

0 commit comments

Comments
 (0)