cloudflare_workers_kv_sdk_rs/
lib.rs

1use log::warn;
2use reqwest::header::HeaderMap;
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6use std::time::Duration;
7
8const CF_API_URL: &str = "https://api.cloudflare.com/client/v4/";
9
10async fn convert_string_to_error(s: &str) -> Box<dyn std::error::Error> {
11    Box::new(std::io::Error::new(std::io::ErrorKind::Other, s))
12}
13
14async fn check_success(resp_json: Value) -> Result<bool, Box<dyn std::error::Error>> {
15    match resp_json.get("success") {
16        Some(success) => match success.as_bool() {
17            Some(true) => Ok(true),
18            Some(false) => Ok(false),
19            None => Err(convert_string_to_error(
20                "The returned 'success' field is not a boolean value.",
21            )
22            .await),
23        },
24        None => Err(convert_string_to_error(
25            "The returned JSON does not contain the 'success' field.",
26        )
27        .await),
28    }
29}
30
31#[derive(Clone)]
32pub struct KvClient {
33    pub account_id: String,
34    pub api_key: String,
35    client: Client,
36    url: String,
37    header_map: HeaderMap,
38}
39
40#[derive(Clone, Debug)]
41pub struct Namespace {
42    pub id: String,
43    pub title: String,
44}
45
46impl KvClient {
47    pub fn new(account_id: &str, api_key: &str) -> Self {
48        let headers = HeaderMap::from_iter([
49            (
50                "Authorization".parse().unwrap(),
51                format!("Bearer {}", api_key).parse().unwrap(),
52            ),
53            (
54                "Content-Type".parse().unwrap(),
55                "application/json".parse().unwrap(),
56            ),
57        ]);
58
59        KvClient {
60            account_id: account_id.to_string(),
61            api_key: api_key.to_string(),
62            client: Client::builder()
63                .connect_timeout(Duration::from_secs(5))
64                .build()
65                .unwrap(),
66            url: format!(
67                "{}{}{}{}",
68                CF_API_URL, "accounts/", account_id, "/storage/kv/namespaces"
69            ),
70            header_map: headers,
71        }
72    }
73
74    pub async fn list_namespaces(&self) -> Result<Vec<Namespace>, Box<dyn std::error::Error>> {
75        let resp = self
76            .client
77            .get(self.url.clone())
78            .headers(self.header_map.clone())
79            .send()
80            .await?;
81
82        if resp.status().is_success() == false {
83            warn!("Cloudflare returned an ERROR httpcode.")
84        }
85
86        let resp_json = resp.json::<Value>().await?;
87
88        if check_success(resp_json.clone()).await? == false {
89            return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
90        }
91
92        match resp_json.get("result") {
93            Some(result) => match result.as_array() {
94                Some(namespaces) => {
95                    let mut namespace_list = Vec::new();
96                    for namespace in namespaces {
97                        let id = namespace["id"].as_str().unwrap().to_string();
98                        let title = namespace["title"].as_str().unwrap().to_string();
99                        namespace_list.push(Namespace { id, title });
100                    }
101                    Ok(namespace_list)
102                }
103                None => Err(convert_string_to_error(
104                    "The 'results' field cannot be converted to an array.",
105                )
106                .await),
107            },
108            None => Err(convert_string_to_error(
109                "The returned JSON does not contain the 'result' field.",
110            )
111            .await),
112        }
113    }
114
115    pub async fn create_namespace(
116        &self,
117        title: &str,
118    ) -> Result<Namespace, Box<dyn std::error::Error>> {
119        let payload = json!({
120            "title": title
121        });
122        let resp = self
123            .client
124            .post(self.url.clone())
125            .headers(self.header_map.clone())
126            .json(&payload)
127            .send()
128            .await?;
129
130        if resp.status().is_success() == false {
131            warn!("Cloudflare returned an ERROR httpcode.")
132        }
133
134        let resp_json = resp.json::<Value>().await?;
135
136        if check_success(resp_json.clone()).await? == false {
137            return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
138        }
139
140        match resp_json.get("result") {
141            Some(result) => {
142                let id = match result.get("id") {
143                    Some(id) => match id.as_str() {
144                        Some(id) => id,
145                        None => {
146                            return Err(convert_string_to_error(
147                                "The 'id' field cannot be converted to a string.",
148                            )
149                            .await)
150                        }
151                    },
152                    None => {
153                        return Err(convert_string_to_error(
154                            "The 'id' field cannot be found in the 'result' field.",
155                        )
156                        .await)
157                    }
158                };
159
160                let title = match result.get("title") {
161                    Some(title) => match title.as_str() {
162                        Some(title) => title,
163                        None => {
164                            return Err(convert_string_to_error(
165                                "The 'title' field cannot be converted to a string.",
166                            )
167                            .await)
168                        }
169                    },
170                    None => {
171                        return Err(convert_string_to_error(
172                            "The 'title' field cannot be found in the'result' field.",
173                        )
174                        .await)
175                    }
176                };
177
178                Ok(Namespace {
179                    id: id.to_string(),
180                    title: title.to_string(),
181                })
182            }
183            None => Err(convert_string_to_error(
184                "The returned JSON does not contain the 'result' field.",
185            )
186            .await),
187        }
188    }
189}
190
191#[derive(Clone, Debug)]
192pub struct KvNamespaceClient {
193    pub account_id: String,
194    pub api_key: String,
195    pub namespace_id: String,
196    client: Client,
197    url: String,
198    header_map: HeaderMap,
199}
200
201impl KvNamespaceClient {
202    pub fn new(account_id: &str, api_key: &str, namespace_id: &str) -> Self {
203        let headers = HeaderMap::from_iter([
204            (
205                "Authorization".parse().unwrap(),
206                format!("Bearer {}", api_key).parse().unwrap(),
207            ),
208            (
209                "Content-Type".parse().unwrap(),
210                "application/json".parse().unwrap(),
211            ),
212        ]);
213
214        KvNamespaceClient {
215            account_id: account_id.to_string(),
216            api_key: api_key.to_string(),
217            namespace_id: namespace_id.to_string(),
218            client: Client::builder()
219                .connect_timeout(Duration::from_secs(5))
220                .build()
221                .unwrap(),
222            url: format!(
223                "{}{}{}{}{}",
224                CF_API_URL, "accounts/", account_id, "/storage/kv/namespaces/", namespace_id
225            ),
226            header_map: headers,
227        }
228    }
229
230    pub fn from_kvclient(kvclient: &KvClient, namespace_id: &str) -> Self {
231        KvNamespaceClient {
232            account_id: kvclient.account_id.clone(),
233            api_key: kvclient.api_key.clone(),
234            namespace_id: namespace_id.to_string(),
235            client: kvclient.client.clone(),
236            url: format!("{}/{}", kvclient.url.clone(), namespace_id),
237            header_map: kvclient.header_map.clone(),
238        }
239    }
240
241    pub async fn delete_namespace(&self) -> Result<(), Box<dyn std::error::Error>> {
242        let resp = self
243            .client
244            .delete(self.url.clone())
245            .headers(self.header_map.clone())
246            .send()
247            .await?;
248
249        if resp.status().is_success() == false {
250            warn!("Cloudflare returned an ERROR httpcode.")
251        }
252
253        let resp_json = resp.json::<Value>().await?;
254
255        if check_success(resp_json.clone()).await? == false {
256            return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
257        }
258        Ok(())
259    }
260
261    pub async fn rename_namespace(
262        &self,
263        new_title: &str,
264    ) -> Result<(), Box<dyn std::error::Error>> {
265        let payload = json!({
266            "title": new_title
267        });
268
269        let resp = self
270            .client
271            .put(self.url.clone())
272            .headers(self.header_map.clone())
273            .json(&payload)
274            .send()
275            .await?;
276
277        if resp.status().is_success() == false {
278            warn!("Cloudflare returned an ERROR httpcode.")
279        }
280
281        let resp_json = resp.json::<Value>().await?;
282
283        if check_success(resp_json.clone()).await? == false {
284            return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
285        }
286
287        Ok(())
288    }
289    pub async fn write(&self, payload: KvRequest) -> Result<(), Box<dyn std::error::Error>> {
290        let url = format!("{}/bulk", self.url);
291
292        let payload_vec = vec![payload];
293
294        let resp = self
295            .client
296            .put(url)
297            .headers(self.header_map.clone())
298            .json(&payload_vec)
299            .send()
300            .await?;
301
302        if resp.status().is_success() == false {
303            warn!("Cloudflare returned an ERROR httpcode.")
304        }
305
306        let resp_json = resp.json::<Value>().await?;
307        if check_success(resp_json.clone()).await? == false {
308            return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
309        }
310
311        Ok(())
312    }
313
314    pub async fn write_multiple(
315        &self,
316        payload: Vec<KvRequest>,
317    ) -> Result<(), Box<dyn std::error::Error>> {
318        let url = format!("{}/bulk", self.url);
319        let resp = self
320            .client
321            .put(url)
322            .headers(self.header_map.clone())
323            .json(&payload)
324            .send()
325            .await?;
326
327        if resp.status().is_success() == false {
328            warn!("Cloudflare returned an ERROR httpcode.")
329        }
330
331        let resp_json = resp.json::<Value>().await?;
332
333        if check_success(resp_json.clone()).await? == false {
334            return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
335        }
336
337        Ok(())
338    }
339
340    pub async fn delete(&self, key: &str) -> Result<(), Box<dyn std::error::Error>> {
341        let url = format!("{}/bulk/delete", self.url);
342        let payload = json!([key]);
343
344        let resp = self
345            .client
346            .post(url)
347            .headers(self.header_map.clone())
348            .json(&payload)
349            .send()
350            .await?;
351
352        if resp.status().is_success() == false {
353            warn!("Cloudflare returned an ERROR httpcode.")
354        }
355
356        let resp_json = resp.json::<Value>().await?;
357
358        if check_success(resp_json.clone()).await? == false {
359            return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
360        }
361
362        Ok(())
363    }
364
365    pub async fn delete_multiple(&self, keys: Vec<&str>) -> Result<(), Box<dyn std::error::Error>> {
366        let url = format!("{}/bulk/delete", self.url);
367        let payload = json!(keys);
368
369        let resp = self
370            .client
371            .post(url)
372            .headers(self.header_map.clone())
373            .json(&payload)
374            .send()
375            .await?;
376
377        if resp.status().is_success() == false {
378            warn!("Cloudflare returned an ERROR httpcode.")
379        }
380
381        let resp_json = resp.json::<Value>().await?;
382
383        if check_success(resp_json.clone()).await? == false {
384            return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
385        }
386
387        Ok(())
388    }
389
390    pub async fn list_all_keys(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
391        let url = format!("{}/keys", self.url);
392        let mut keys = Vec::new();
393        let mut cursor = "".to_string();
394        loop {
395            let url = format!("{}?cursor={}", url, cursor);
396            let resp = self
397                .client
398                .get(url.clone())
399                .headers(self.header_map.clone())
400                .send()
401                .await?;
402            if resp.status().is_success() == false {
403                warn!("Cloudflare returned an ERROR httpcode.")
404            }
405            let resp_json = resp.json::<Value>().await?;
406
407            if check_success(resp_json.clone()).await? == false {
408                return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
409            }
410
411            let results = match resp_json.get("result") {
412                Some(result) => match result.as_array() {
413                    Some(result) => result,
414                    None => {
415                        return Err(convert_string_to_error("No result found in response.").await);
416                    }
417                },
418                None => {
419                    return Err(convert_string_to_error("No result found in response.").await);
420                }
421            };
422
423            for result in results {
424                match result.get("name") {
425                    Some(name) => {
426                        let name = match name.as_str() {
427                            Some(name) => name,
428                            None => {
429                                return Err(
430                                    convert_string_to_error("No name found in response.").await
431                                );
432                            }
433                        };
434                        keys.push(name.to_string());
435                    }
436                    None => {
437                        return Err(convert_string_to_error("No name found in response.").await);
438                    }
439                }
440            }
441
442            let (cursor_tmp, _cursor_count) = match resp_json.get("result_info") {
443                Some(result_info) => {
444                    let cursor_tmp = match result_info.get("cursor") {
445                        Some(cursor) => match cursor.as_str() {
446                            Some(cursor) => cursor.to_string(),
447                            None => {
448                                return Err(convert_string_to_error(
449                                    "No cursor found in response.",
450                                )
451                                .await);
452                            }
453                        },
454                        None => {
455                            return Err(
456                                convert_string_to_error("No cursor found in response.").await
457                            );
458                        }
459                    };
460                    let cursor_count = match result_info.get("count") {
461                        Some(count) => match count.as_u64() {
462                            Some(count) => count,
463                            None => {
464                                return Err(
465                                    convert_string_to_error("No count found in response.").await
466                                );
467                            }
468                        },
469                        None => {
470                            return Err(
471                                convert_string_to_error("No count found in response.").await
472                            );
473                        }
474                    };
475                    (cursor_tmp, cursor_count)
476                }
477                None => {
478                    return Err(convert_string_to_error("No result_info found in response.").await);
479                }
480            };
481
482
483            if cursor_tmp.is_empty() {
484                break;
485            } else {
486                cursor = cursor_tmp;
487                continue;
488            }
489        }
490        Ok(keys)
491    }
492
493    pub async fn read_metadata(&self, key: &str) -> Result<Value, Box<dyn std::error::Error>> {
494        let url = format!("{}/metadata/{}", self.url, key);
495
496        let resp = self
497            .client
498            .get(url)
499            .headers(self.header_map.clone())
500            .send()
501            .await?;
502
503        if resp.status().is_success() == false {
504            warn!("Cloudflare returned an ERROR httpcode.")
505        }
506
507        let resp_json = resp.json::<Value>().await?;
508
509        if check_success(resp_json.clone()).await? == false {
510            return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
511        }
512
513        match resp_json.get("result") {
514            Some(result) => Ok(result.clone()),
515            None => {
516                Err(convert_string_to_error("No result found in response.").await)
517            }
518        }
519    }
520
521    pub async fn get(&self, key: &str) -> Result<String, Box<dyn std::error::Error>> {
522        let url = format!("{}/values/{}", self.url, key);
523
524        let resp = self
525            .client
526            .get(url)
527            .headers(self.header_map.clone())
528            .send()
529            .await?;
530
531        if resp.status().is_success() == false {
532            warn!("Cloudflare returned an ERROR httpcode.")
533        }
534
535        if resp.status().as_u16() == 404 {
536            let resp_json = resp.json::<Value>().await?;
537            log::error!("Key: {} Not Found", key);
538            return Err(convert_string_to_error(resp_json.to_string().as_str()).await);
539        }
540
541        let resp_value = resp.text().await?;
542
543        Ok(resp_value)
544    }
545}
546
547#[derive(Serialize, Deserialize, Debug)]
548pub struct KvRequest {
549    key: String,
550    value: String,
551    base64: bool,
552    expiration: Option<u64>,
553    expiration_ttl: Option<u64>,
554    metadata: Option<Value>,
555}
556
557impl KvRequest {
558    pub fn new(key: &str, value: &str) -> Self {
559        KvRequest {
560            key: key.to_string(),
561            value: value.to_string(),
562            base64: false,
563            expiration: None,
564            expiration_ttl: None,
565            metadata: None,
566        }
567    }
568
569    pub fn enable_base64(&self) -> Self {
570        KvRequest {
571            base64: true,
572            key: self.key.clone(),
573            value: self.value.clone(),
574            expiration: self.expiration,
575            expiration_ttl: self.expiration_ttl,
576            metadata: self.metadata.clone(),
577        }
578    }
579
580    pub fn ttl_sec(&self, ttl_sec: u64) -> Self {
581        KvRequest {
582            base64: self.base64,
583            key: self.key.clone(),
584            value: self.value.clone(),
585            expiration: self.expiration,
586            expiration_ttl: Some(ttl_sec),
587            metadata: self.metadata.clone(),
588        }
589    }
590
591    pub fn ttl_timestemp(&self, ttl_timestemp: u64) -> Self {
592        KvRequest {
593            base64: self.base64,
594            key: self.key.clone(),
595            value: self.value.clone(),
596            expiration: Some(ttl_timestemp),
597            expiration_ttl: self.expiration_ttl,
598            metadata: self.metadata.clone(),
599        }
600    }
601
602    pub fn metadata(&self, metadata: Value) -> Self {
603        KvRequest {
604            base64: self.base64,
605            key: self.key.clone(),
606            value: self.value.clone(),
607            expiration: self.expiration,
608            expiration_ttl: self.expiration_ttl,
609            metadata: Some(metadata),
610        }
611    }
612}