Skip to content

Commit 3dd879a

Browse files
Keys api (#20)
* Introduced actions, and changed to Arc<String> * Updated documentation
1 parent 86f1dde commit 3dd879a

File tree

8 files changed

+275
-50
lines changed

8 files changed

+275
-50
lines changed

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ crate-type = ["cdylib", "rlib"]
1313

1414
[dependencies]
1515
async-trait = "0.1.50"
16-
bytes = "1.0.1"
16+
base64 = "0.13.0"
17+
hmac = "0.11.0"
1718
http = "0.2.4"
19+
serde = { version = "1", features = ["derive"] }
20+
serde_json = "1"
21+
sha2 = "0.9.5"
1822
thiserror = "1.0.24"
1923

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

src/client/builder.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::Arc;
2+
13
use super::Client;
24
use crate::transport::Transport;
35

@@ -6,16 +8,16 @@ use crate::transport::WasmClient;
68

79
use crate::{Result, TypesenseError};
810
/// Builder for the Typesense [`Client`]
9-
pub struct ClientBuilder<'a, T> {
11+
pub struct ClientBuilder<T> {
1012
transport: Option<Transport<T>>,
11-
host: Option<&'a str>,
12-
api_key: Option<&'a str>,
13+
host: Option<Arc<String>>,
14+
api_key: Option<Arc<String>>,
1315
}
1416

15-
impl<'a, T> ClientBuilder<'a, T> {
17+
impl<T> ClientBuilder<T> {
1618
/// build [`Client`] with the current configurations. Return [`typesense::TypesenseError::ConfigError`]
1719
/// if a configuration is missing.
18-
pub fn build(self) -> Result<Client<'a, T>> {
20+
pub fn build(self) -> Result<Client<T>> {
1921
Ok(Client {
2022
transport: self.transport.ok_or_else(|| {
2123
TypesenseError::ConfigError("missing client transport".to_string())
@@ -30,14 +32,14 @@ impl<'a, T> ClientBuilder<'a, T> {
3032
}
3133

3234
/// Set host
33-
pub fn host(mut self, host: &'a str) -> Self {
34-
self.host = Some(host);
35+
pub fn host(mut self, host: impl AsRef<str>) -> Self {
36+
self.host = Some(Arc::new(host.as_ref().to_string()));
3537
self
3638
}
3739

3840
/// Set api key
39-
pub fn api_key(mut self, api_key: &'a str) -> Self {
40-
self.api_key = Some(api_key);
41+
pub fn api_key(mut self, api_key: impl AsRef<str>) -> Self {
42+
self.api_key = Some(Arc::new(api_key.as_ref().to_string()));
4143
self
4244
}
4345

@@ -48,7 +50,7 @@ impl<'a, T> ClientBuilder<'a, T> {
4850
}
4951
}
5052

51-
impl<'a, T> Default for ClientBuilder<'a, T> {
53+
impl<T> Default for ClientBuilder<T> {
5254
fn default() -> Self {
5355
Self {
5456
transport: None,
@@ -63,7 +65,7 @@ impl<'a, T> Default for ClientBuilder<'a, T> {
6365
docsrs,
6466
doc(cfg(all(feature = "tokio-rt", not(target_arch = "wasm32"))))
6567
)]
66-
impl<'a> ClientBuilder<'a, crate::transport::HyperHttpsClient> {
68+
impl ClientBuilder<crate::transport::HyperHttpsClient> {
6769
/// Create client builder with a [`hyper`](https://docs.rs/hyper) client.
6870
/// The connector used is [`HttpsConnector`](hyper_tls::HttpsConnector).
6971
pub fn new_hyper() -> Self {
@@ -78,7 +80,7 @@ impl<'a> ClientBuilder<'a, crate::transport::HyperHttpsClient> {
7880

7981
#[cfg(target_arch = "wasm32")]
8082
#[cfg_attr(docsrs, doc(cfg(target_arch = "wasm32")))]
81-
impl<'a> ClientBuilder<'a, WasmClient> {
83+
impl ClientBuilder<WasmClient> {
8284
/// Create client builder using default wasm client
8385
pub fn new_wasm() -> Self {
8486
let transport = Some(crate::transport::TransportBuilder::new_wasm().build());

src/client/keys.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
//! Module containing everything related to Keys API.
2+
//!
3+
//! More info [here](https://typesense.org/docs/0.20.0/api/api-keys.html).
4+
5+
use hmac::{Hmac, Mac, NewMac};
6+
use serde::{Deserialize, Serialize};
7+
use sha2::Sha256;
8+
9+
use super::Client;
10+
use crate::transport::HttpLowLevel;
11+
12+
/// To interact with the Keys API.
13+
pub struct ClientKeys<T> {
14+
pub(super) client: Client<T>,
15+
}
16+
17+
impl<T> ClientKeys<T>
18+
where
19+
T: HttpLowLevel,
20+
{
21+
/// Create an API Key.
22+
///
23+
/// More info [here](https://typesense.org/docs/0.20.0/api/api-keys.html#create-an-api-key).
24+
pub async fn create(
25+
&self,
26+
actions: Vec<Actions>,
27+
collections: Vec<String>,
28+
description: impl Into<Option<String>>,
29+
expires_at: impl Into<Option<usize>>,
30+
) -> crate::Result<ClientKeyCreate> {
31+
let create = Create {
32+
actions,
33+
collections,
34+
description: description.into(),
35+
expires_at: expires_at.into(),
36+
};
37+
38+
let response = self
39+
.client
40+
.post("/keys", serde_json::to_vec(&create)?)
41+
.await?;
42+
43+
let body = response.into_body();
44+
Ok(serde_json::from_slice(&body)?)
45+
}
46+
47+
/// Retrieve (metadata about) a key.
48+
///
49+
/// More info [here](https://typesense.org/docs/0.20.0/api/api-keys.html#retrieve-an-api-key).
50+
pub async fn retrieve(&self, n: usize) -> crate::Result<ClientKeyRetrieve> {
51+
let response = self.client.get(format!("/keys/{}", n).as_str()).await?;
52+
53+
let body = response.into_body();
54+
Ok(serde_json::from_slice(&body)?)
55+
}
56+
57+
/// Retrieve (metadata about) all keys.
58+
///
59+
/// More info [here](https://typesense.org/docs/0.20.0/api/api-keys.html#list-all-keys).
60+
pub async fn retrieve_all(&self) -> crate::Result<ClientKeyRetrieveAll> {
61+
let response = self.client.get("/keys").await?;
62+
63+
let body = response.into_body();
64+
Ok(serde_json::from_slice(&body)?)
65+
}
66+
67+
/// Delete an API key given its ID.
68+
///
69+
/// More info [here](https://typesense.org/docs/0.20.0/api/api-keys.html#delete-api-key).
70+
pub async fn delete(&self, n: usize) -> crate::Result<ClientKeyDelete> {
71+
let response = self.client.delete(format!("/keys/{}", n).as_str()).await?;
72+
73+
let body = response.into_body();
74+
Ok(serde_json::from_slice(&body)?)
75+
}
76+
77+
/// Generate a scoped search API key that can have embedded search parameters in them.
78+
///
79+
/// More info [here](https://typesense.org/docs/0.20.0/api/api-keys.html#generate-scoped-search-key).
80+
pub async fn generate_scoped_search_key(
81+
key: impl AsRef<str>,
82+
filter_by: impl AsRef<str>,
83+
expires_at: usize,
84+
) -> crate::Result<String> {
85+
let generate_scoped_search_key = GenerateScopedSearchKey {
86+
filter_by: filter_by.as_ref().to_string(),
87+
expires_at,
88+
};
89+
let params = serde_json::to_string(&generate_scoped_search_key)?;
90+
91+
let mut mac = Hmac::<Sha256>::new_from_slice(key.as_ref().as_bytes()).unwrap();
92+
mac.update(params.as_bytes());
93+
let result = mac.finalize();
94+
let digest = base64::encode(result.into_bytes());
95+
96+
let key_prefix = &key.as_ref()[0..4];
97+
let raw_scoped_key = format!("{}{}{}", digest, key_prefix, params);
98+
99+
Ok(base64::encode(raw_scoped_key.as_bytes()))
100+
}
101+
}
102+
103+
/// Enum over the possible list of Actions.
104+
///
105+
/// More info [here](https://typesense.org/docs/0.19.0/api/api-keys.html#sample-actions).
106+
#[derive(Serialize, Deserialize)]
107+
pub enum Actions {
108+
/// Allows only search requests.
109+
#[serde(rename = "documents:search")]
110+
DocumentsSearch,
111+
112+
/// Allows fetching a single document.
113+
#[serde(rename = "documents:get")]
114+
DocumentsGet,
115+
116+
/// Allow all kinds of collection related operations.
117+
#[serde(rename = "documents:*")]
118+
DocumentsAll,
119+
120+
/// Allows a collection to be deleted.
121+
#[serde(rename = "collections:delete")]
122+
CollectionsDelete,
123+
124+
/// Allows a collection to be created.
125+
#[serde(rename = "collections:create")]
126+
CollectionsCreate,
127+
128+
/// Allow all kinds of collection related operations.
129+
#[serde(rename = "collections:*")]
130+
CollectionsAll,
131+
132+
/// Allows all operations.
133+
#[serde(rename = "*")]
134+
All,
135+
}
136+
137+
/// Structure returned by [`ClientKeys::create`] function.
138+
#[derive(Serialize, Deserialize)]
139+
pub struct ClientKeyCreate {
140+
/// Key ID
141+
pub id: usize,
142+
143+
/// Key Actions
144+
pub actions: Vec<Actions>,
145+
146+
/// Key Collections
147+
pub collections: Vec<String>,
148+
149+
/// Key Value
150+
pub value: String,
151+
152+
/// Key Description
153+
pub description: String,
154+
}
155+
156+
/// Structure returned by [`ClientKeys::retrieve`] function.
157+
#[derive(Serialize, Deserialize)]
158+
pub struct ClientKeyRetrieve {
159+
/// Key Actions
160+
pub actions: Vec<Actions>,
161+
162+
/// Key Collections
163+
pub collections: Vec<String>,
164+
165+
/// Key Description
166+
pub description: String,
167+
168+
/// Key ID
169+
pub id: usize,
170+
171+
/// Key Value Prefix
172+
pub value_prefix: String,
173+
}
174+
175+
/// Structure returned by [`ClientKeys::retrieve_all`] function.
176+
#[derive(Serialize, Deserialize)]
177+
pub struct ClientKeyRetrieveAll {
178+
/// Vector of all the Keys
179+
pub keys: Vec<ClientKeyRetrieve>,
180+
}
181+
182+
/// Structure returned by [`ClientKeys::delete`] function.
183+
#[derive(Serialize, Deserialize)]
184+
pub struct ClientKeyDelete {
185+
/// ID of the deleted Key
186+
pub id: usize,
187+
}
188+
189+
#[derive(Serialize)]
190+
struct Create {
191+
actions: Vec<Actions>,
192+
collections: Vec<String>,
193+
description: Option<String>,
194+
expires_at: Option<usize>,
195+
}
196+
197+
#[derive(Serialize)]
198+
struct GenerateScopedSearchKey {
199+
filter_by: String,
200+
expires_at: usize,
201+
}

src/client/mod.rs

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,75 @@
1-
use bytes::Bytes;
1+
use std::sync::Arc;
2+
3+
use http::Response;
24

35
use crate::transport::HttpLowLevel;
46
use crate::transport::Transport;
57
use crate::Result;
68

79
mod builder;
10+
pub mod keys;
11+
812
pub use builder::ClientBuilder;
13+
pub use keys::ClientKeys;
914

1015
#[allow(dead_code)]
1116
pub const TYPESENSE_API_KEY_HEADER_NAME: &str = "X-TYPESENSE-API-KEY";
1217

1318
/// Root client for top level APIs
14-
pub struct Client<'a, T> {
19+
#[derive(Clone)]
20+
pub struct Client<T> {
1521
transport: Transport<T>,
16-
host: &'a str,
17-
api_key: &'a str,
22+
host: Arc<String>,
23+
api_key: Arc<String>,
1824
}
1925

20-
impl<'a, T> Client<'a, T> {
26+
impl<T> Client<T> {
2127
/// Gets the transport of the client
2228
pub fn transport(&self) -> &Transport<T> {
2329
&self.transport
2430
}
2531
}
2632

33+
impl<T> Client<T>
34+
where
35+
T: Clone,
36+
{
37+
/// Make the ClientKeys struct, to interact with the Keys API.
38+
pub fn keys(&self) -> ClientKeys<T> {
39+
ClientKeys {
40+
client: self.clone(),
41+
}
42+
}
43+
}
44+
2745
#[allow(dead_code)]
28-
impl<'a, C> Client<'a, C>
46+
impl<C> Client<C>
2947
where
3048
C: HttpLowLevel,
3149
{
3250
pub(crate) async fn send(
3351
&self,
3452
method: http::Method,
3553
path: &str,
36-
body: Bytes,
37-
) -> Result<C::Response> {
54+
body: Vec<u8>,
55+
) -> Result<Response<Vec<u8>>> {
3856
let uri = format!("{}{}", self.host, path);
3957
let mut headers = http::HeaderMap::default();
4058
headers.insert(TYPESENSE_API_KEY_HEADER_NAME, self.api_key.parse().unwrap());
4159
self.transport.send(method, &uri, headers, body).await
4260
}
4361

44-
pub(crate) async fn get(&self, path: &str) -> Result<C::Response> {
45-
self.send(http::Method::GET, path, Bytes::new()).await
62+
pub(crate) async fn get(&self, path: &str) -> Result<Response<Vec<u8>>> {
63+
self.send(http::Method::GET, path, Vec::new()).await
4664
}
4765

48-
pub(crate) async fn post(&self, path: &str, body: Bytes) -> Result<C::Response> {
66+
pub(crate) async fn post(&self, path: &str, body: Vec<u8>) -> Result<Response<Vec<u8>>> {
4967
self.send(http::Method::POST, path, body).await
5068
}
69+
70+
pub(crate) async fn delete(&self, path: &str) -> Result<Response<Vec<u8>>> {
71+
self.send(http::Method::DELETE, path, Vec::new()).await
72+
}
5173
}
5274

5375
#[cfg(all(test, feature = "tokio-rt", not(target_arch = "wasm32")))]
@@ -71,14 +93,12 @@ mod hyper_tests {
7193
let response = client.get("/test_api_key").await?;
7294

7395
assert_eq!(response.status(), StatusCode::OK);
74-
let bytes = hyper::body::to_bytes(response).await?;
75-
assert_eq!(bytes, body.as_bytes());
96+
assert_eq!(response.into_body(), body.as_bytes());
7697

7798
let response = client.post("/test_api_key", body.clone().into()).await?;
7899

79100
assert_eq!(response.status(), StatusCode::OK);
80-
let bytes = hyper::body::to_bytes(response).await?;
81-
assert_eq!(bytes, body.as_bytes());
101+
assert_eq!(response.into_body(), body.as_bytes());
82102

83103
Ok(())
84104
}

0 commit comments

Comments
 (0)