1use std::collections::HashMap;
2
3#[cfg(feature = "rust")]
4use bon::Builder;
5#[cfg(feature = "node")]
6use napi_derive::napi;
7#[cfg(feature = "python")]
8use pyo3::{pyclass, pymethods};
9#[cfg(feature = "python")]
10use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
11use serde::{Deserialize, Deserializer, Serialize};
12
13use crate::{config::KvStoreConfig, errors::SdkError, SdkConfig};
14
15fn null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
18where
19 D: Deserializer<'de>,
20 T: Deserialize<'de> + Default,
21{
22 Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
23}
24
25const KV_STORE_BASE_URL: &str = "https://api.quicknode.com/kv/rest/v1/";
26
27pub(crate) struct ResolvedKvStoreConfig {
30 pub(crate) base_url: reqwest::Url,
31}
32
33impl ResolvedKvStoreConfig {
34 pub(crate) fn from_config(config: Option<&KvStoreConfig>) -> Result<Self, SdkError> {
35 let url_str = config
36 .and_then(|s| s.base_url.as_deref())
37 .unwrap_or(KV_STORE_BASE_URL);
38 let mut base_url =
39 reqwest::Url::parse(url_str).map_err(|e| SdkError::Config(e.to_string()))?;
40 if !base_url.path().ends_with('/') {
41 base_url.set_path(&format!("{}/", base_url.path()));
42 }
43 Ok(Self { base_url })
44 }
45}
46
47#[cfg_attr(feature = "rust", derive(Builder))]
51#[cfg_attr(feature = "node", napi(object))]
52#[cfg_attr(not(feature = "node"), derive(Clone))]
53#[derive(Debug, Serialize, Deserialize)]
54pub struct CreateSetParams {
55 pub key: String,
57 pub value: String,
59}
60
61#[cfg_attr(feature = "rust", derive(Builder))]
63#[cfg_attr(feature = "node", napi(object))]
64#[cfg_attr(not(feature = "node"), derive(Clone))]
65#[derive(Debug, Default, Serialize, Deserialize)]
66pub struct GetSetsParams {
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub limit: Option<i64>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub cursor: Option<String>,
73}
74
75#[cfg_attr(feature = "rust", derive(Builder))]
77#[cfg_attr(feature = "node", napi(object))]
78#[cfg_attr(not(feature = "node"), derive(Clone))]
79#[derive(Debug, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct BulkSetsParams {
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub add_sets: Option<HashMap<String, String>>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub delete_sets: Option<Vec<String>>,
88}
89
90#[cfg_attr(feature = "rust", derive(Builder))]
92#[cfg_attr(feature = "node", napi(object))]
93#[cfg_attr(not(feature = "node"), derive(Clone))]
94#[derive(Debug, Serialize, Deserialize)]
95pub struct CreateListParams {
96 pub key: String,
98 pub items: Vec<String>,
100}
101
102#[cfg_attr(feature = "rust", derive(Builder))]
104#[cfg_attr(feature = "node", napi(object))]
105#[cfg_attr(not(feature = "node"), derive(Clone))]
106#[derive(Debug, Default, Serialize, Deserialize)]
107pub struct GetListsParams {
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub limit: Option<i64>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub cursor: Option<String>,
114}
115
116#[cfg_attr(feature = "rust", derive(Builder))]
118#[cfg_attr(feature = "node", napi(object))]
119#[cfg_attr(not(feature = "node"), derive(Clone))]
120#[derive(Debug, Default, Serialize, Deserialize)]
121pub struct GetListParams {
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub limit: Option<i64>,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub cursor: Option<String>,
128}
129
130#[cfg_attr(feature = "rust", derive(Builder))]
132#[cfg_attr(feature = "node", napi(object))]
133#[cfg_attr(not(feature = "node"), derive(Clone))]
134#[derive(Debug, Default, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct UpdateListParams {
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub add_items: Option<Vec<String>>,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub remove_items: Option<Vec<String>>,
143}
144
145#[cfg_attr(feature = "rust", derive(Builder))]
147#[cfg_attr(feature = "node", napi(object))]
148#[cfg_attr(not(feature = "node"), derive(Clone))]
149#[derive(Debug, Serialize, Deserialize)]
150pub struct AddListItemParams {
151 pub item: String,
153}
154
155#[cfg_attr(feature = "python", gen_stub_pyclass)]
159#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
160#[cfg_attr(feature = "node", napi(object))]
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct KvSetEntry {
163 pub key: String,
165 pub value: String,
167}
168
169#[cfg(feature = "python")]
170#[gen_stub_pymethods]
171#[pymethods]
172impl KvSetEntry {
173 #[new]
174 pub fn new(key: String, value: String) -> Self {
175 Self { key, value }
176 }
177}
178
179#[cfg_attr(feature = "python", gen_stub_pyclass)]
182#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
183#[cfg_attr(feature = "node", napi(object))]
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct GetSetsResponse {
186 #[serde(default, deserialize_with = "null_as_default")]
188 pub data: Vec<KvSetEntry>,
189 pub cursor: String,
191}
192
193#[cfg(feature = "python")]
194#[gen_stub_pymethods]
195#[pymethods]
196impl GetSetsResponse {
197 #[new]
198 pub fn new(data: Vec<KvSetEntry>, cursor: String) -> Self {
199 Self { data, cursor }
200 }
201}
202
203#[cfg_attr(feature = "python", gen_stub_pyclass)]
206#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
207#[cfg_attr(feature = "node", napi(object))]
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct GetSetResponse {
210 pub value: String,
212}
213
214#[cfg(feature = "python")]
215#[gen_stub_pymethods]
216#[pymethods]
217impl GetSetResponse {
218 #[new]
219 pub fn new(value: String) -> Self {
220 Self { value }
221 }
222}
223
224#[cfg_attr(feature = "python", gen_stub_pyclass)]
227#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
228#[cfg_attr(feature = "node", napi(object))]
229#[derive(Debug, Clone, Default, Serialize, Deserialize)]
230pub struct GetListsData {
231 pub keys: Vec<String>,
233}
234
235#[cfg(feature = "python")]
236#[gen_stub_pymethods]
237#[pymethods]
238impl GetListsData {
239 #[new]
240 pub fn new(keys: Vec<String>) -> Self {
241 Self { keys }
242 }
243}
244
245#[cfg_attr(feature = "python", gen_stub_pyclass)]
248#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
249#[cfg_attr(feature = "node", napi(object))]
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct GetListsResponse {
252 #[serde(default, deserialize_with = "null_as_default")]
254 pub data: GetListsData,
255 pub cursor: String,
257}
258
259#[cfg(feature = "python")]
260#[gen_stub_pymethods]
261#[pymethods]
262impl GetListsResponse {
263 #[new]
264 pub fn new(data: GetListsData, cursor: String) -> Self {
265 Self { data, cursor }
266 }
267}
268
269#[cfg_attr(feature = "python", gen_stub_pyclass)]
272#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
273#[cfg_attr(feature = "node", napi(object))]
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct GetListData {
276 pub items: Vec<String>,
278}
279
280#[cfg(feature = "python")]
281#[gen_stub_pymethods]
282#[pymethods]
283impl GetListData {
284 #[new]
285 pub fn new(items: Vec<String>) -> Self {
286 Self { items }
287 }
288}
289
290#[cfg_attr(feature = "python", gen_stub_pyclass)]
293#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
294#[cfg_attr(feature = "node", napi(object))]
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct GetListResponse {
297 pub data: GetListData,
299 pub cursor: String,
301}
302
303#[cfg(feature = "python")]
304#[gen_stub_pymethods]
305#[pymethods]
306impl GetListResponse {
307 #[new]
308 pub fn new(data: GetListData, cursor: String) -> Self {
309 Self { data, cursor }
310 }
311}
312
313#[cfg_attr(feature = "python", gen_stub_pyclass)]
315#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
316#[cfg_attr(feature = "node", napi(object))]
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ListContainsItemResponse {
319 pub exists: bool,
321}
322
323#[cfg(feature = "python")]
324#[gen_stub_pymethods]
325#[pymethods]
326impl ListContainsItemResponse {
327 #[new]
328 pub fn new(exists: bool) -> Self {
329 Self { exists }
330 }
331}
332
333#[derive(Debug, Serialize, Deserialize)]
336struct ApiResponse<T> {
337 data: T,
338}
339
340#[derive(Debug, Clone)]
346pub struct KvStoreApiClient {
347 config: SdkConfig,
348}
349
350impl KvStoreApiClient {
351 pub fn new(config: SdkConfig) -> Self {
352 Self { config }
353 }
354
355 pub async fn create_set(&self, params: &CreateSetParams) -> Result<(), SdkError> {
359 let url = self.config.kvstore().base_url.join("sets")?;
360 let resp = self
361 .config
362 .http_client()
363 .post(url)
364 .json(params)
365 .send()
366 .await
367 .map_err(SdkError::Http)?;
368 let status = resp.status();
369 if !status.is_success() {
370 let body = resp.text().await.map_err(SdkError::Http)?;
371 return Err(SdkError::Api { status, body });
372 }
373 Ok(())
374 }
375
376 pub async fn get_sets(&self, params: &GetSetsParams) -> Result<GetSetsResponse, SdkError> {
379 let mut url = self.config.kvstore().base_url.join("sets")?;
380 {
381 let mut pairs = url.query_pairs_mut();
382 if let Some(v) = params.limit {
383 pairs.append_pair("limit", &v.to_string());
384 }
385 if let Some(v) = ¶ms.cursor {
386 pairs.append_pair("cursor", v);
387 }
388 }
389 let resp = self
390 .config
391 .http_client()
392 .get(url)
393 .send()
394 .await
395 .map_err(SdkError::Http)?;
396 let status = resp.status();
397 let body = resp.text().await.map_err(SdkError::Http)?;
398 if !status.is_success() {
399 return Err(SdkError::Api { status, body });
400 }
401 serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
402 }
403
404 pub async fn get_set(&self, key: &str) -> Result<GetSetResponse, SdkError> {
406 let url = self
407 .config
408 .kvstore()
409 .base_url
410 .join(&format!("sets/{key}"))?;
411 let resp = self
412 .config
413 .http_client()
414 .get(url)
415 .send()
416 .await
417 .map_err(SdkError::Http)?;
418 let status = resp.status();
419 let body = resp.text().await.map_err(SdkError::Http)?;
420 if !status.is_success() {
421 return Err(SdkError::Api { status, body });
422 }
423 let wrapper: ApiResponse<GetSetResponse> =
424 serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })?;
425 Ok(wrapper.data)
426 }
427
428 pub async fn bulk_sets(&self, params: &BulkSetsParams) -> Result<(), SdkError> {
431 let url = self.config.kvstore().base_url.join("sets/bulk")?;
432 let resp = self
433 .config
434 .http_client()
435 .post(url)
436 .json(params)
437 .send()
438 .await
439 .map_err(SdkError::Http)?;
440 let status = resp.status();
441 if !status.is_success() {
442 let body = resp.text().await.map_err(SdkError::Http)?;
443 return Err(SdkError::Api { status, body });
444 }
445 Ok(())
446 }
447
448 pub async fn delete_set(&self, key: &str) -> Result<(), SdkError> {
450 let url = self
451 .config
452 .kvstore()
453 .base_url
454 .join(&format!("sets/{key}"))?;
455 let resp = self
456 .config
457 .http_client()
458 .delete(url)
459 .send()
460 .await
461 .map_err(SdkError::Http)?;
462 let status = resp.status();
463 if !status.is_success() {
464 let body = resp.text().await.map_err(SdkError::Http)?;
465 return Err(SdkError::Api { status, body });
466 }
467 Ok(())
468 }
469
470 pub async fn create_list(&self, params: &CreateListParams) -> Result<(), SdkError> {
474 let url = self.config.kvstore().base_url.join("lists")?;
475 let resp = self
476 .config
477 .http_client()
478 .post(url)
479 .json(params)
480 .send()
481 .await
482 .map_err(SdkError::Http)?;
483 let status = resp.status();
484 if !status.is_success() {
485 let body = resp.text().await.map_err(SdkError::Http)?;
486 return Err(SdkError::Api { status, body });
487 }
488 Ok(())
489 }
490
491 pub async fn get_lists(&self, params: &GetListsParams) -> Result<GetListsResponse, SdkError> {
494 let mut url = self.config.kvstore().base_url.join("lists")?;
495 {
496 let mut pairs = url.query_pairs_mut();
497 if let Some(v) = params.limit {
498 pairs.append_pair("limit", &v.to_string());
499 }
500 if let Some(v) = ¶ms.cursor {
501 pairs.append_pair("cursor", v);
502 }
503 }
504 let resp = self
505 .config
506 .http_client()
507 .get(url)
508 .send()
509 .await
510 .map_err(SdkError::Http)?;
511 let status = resp.status();
512 let body = resp.text().await.map_err(SdkError::Http)?;
513 if !status.is_success() {
514 return Err(SdkError::Api { status, body });
515 }
516 serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
517 }
518
519 pub async fn get_list(
522 &self,
523 key: &str,
524 params: &GetListParams,
525 ) -> Result<GetListResponse, SdkError> {
526 let mut url = self
527 .config
528 .kvstore()
529 .base_url
530 .join(&format!("lists/{key}"))?;
531 {
532 let mut pairs = url.query_pairs_mut();
533 if let Some(v) = params.limit {
534 pairs.append_pair("limit", &v.to_string());
535 }
536 if let Some(v) = ¶ms.cursor {
537 pairs.append_pair("cursor", v);
538 }
539 }
540 let resp = self
541 .config
542 .http_client()
543 .get(url)
544 .send()
545 .await
546 .map_err(SdkError::Http)?;
547 let status = resp.status();
548 let body = resp.text().await.map_err(SdkError::Http)?;
549 if !status.is_success() {
550 return Err(SdkError::Api { status, body });
551 }
552 serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
553 }
554
555 pub async fn update_list(&self, key: &str, params: &UpdateListParams) -> Result<(), SdkError> {
558 let url = self
559 .config
560 .kvstore()
561 .base_url
562 .join(&format!("lists/{key}"))?;
563 let resp = self
564 .config
565 .http_client()
566 .patch(url)
567 .json(params)
568 .send()
569 .await
570 .map_err(SdkError::Http)?;
571 let status = resp.status();
572 if !status.is_success() {
573 let body = resp.text().await.map_err(SdkError::Http)?;
574 return Err(SdkError::Api { status, body });
575 }
576 Ok(())
577 }
578
579 pub async fn add_list_item(
581 &self,
582 key: &str,
583 params: &AddListItemParams,
584 ) -> Result<(), SdkError> {
585 let url = self
586 .config
587 .kvstore()
588 .base_url
589 .join(&format!("lists/{key}/items"))?;
590 let resp = self
591 .config
592 .http_client()
593 .post(url)
594 .json(params)
595 .send()
596 .await
597 .map_err(SdkError::Http)?;
598 let status = resp.status();
599 if !status.is_success() {
600 let body = resp.text().await.map_err(SdkError::Http)?;
601 return Err(SdkError::Api { status, body });
602 }
603 Ok(())
604 }
605
606 pub async fn list_contains_item(
608 &self,
609 key: &str,
610 item: &str,
611 ) -> Result<ListContainsItemResponse, SdkError> {
612 let url = self
613 .config
614 .kvstore()
615 .base_url
616 .join(&format!("lists/{key}/contains/{item}"))?;
617 let resp = self
618 .config
619 .http_client()
620 .get(url)
621 .send()
622 .await
623 .map_err(SdkError::Http)?;
624 let status = resp.status();
625 let body = resp.text().await.map_err(SdkError::Http)?;
626 if !status.is_success() {
627 return Err(SdkError::Api { status, body });
628 }
629 let wrapper: ApiResponse<ListContainsItemResponse> =
630 serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })?;
631 Ok(wrapper.data)
632 }
633
634 pub async fn delete_list_item(&self, key: &str, item: &str) -> Result<(), SdkError> {
636 let url = self
637 .config
638 .kvstore()
639 .base_url
640 .join(&format!("lists/{key}/items/{item}"))?;
641 let resp = self
642 .config
643 .http_client()
644 .delete(url)
645 .send()
646 .await
647 .map_err(SdkError::Http)?;
648 let status = resp.status();
649 if !status.is_success() {
650 let body = resp.text().await.map_err(SdkError::Http)?;
651 return Err(SdkError::Api { status, body });
652 }
653 Ok(())
654 }
655
656 pub async fn delete_list(&self, key: &str) -> Result<(), SdkError> {
658 let url = self
659 .config
660 .kvstore()
661 .base_url
662 .join(&format!("lists/{key}"))?;
663 let resp = self
664 .config
665 .http_client()
666 .delete(url)
667 .send()
668 .await
669 .map_err(SdkError::Http)?;
670 let status = resp.status();
671 if !status.is_success() {
672 let body = resp.text().await.map_err(SdkError::Http)?;
673 return Err(SdkError::Api { status, body });
674 }
675 Ok(())
676 }
677}
678
679#[cfg(test)]
682#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
683mod tests {
684 use super::*;
685 use crate::{KvStoreConfig, QuicknodeSdk, SdkFullConfig};
686 use wiremock::matchers::{body_json, method, path};
687 use wiremock::{Mock, MockServer, ResponseTemplate};
688
689 fn make_sdk(base_url: String) -> QuicknodeSdk {
690 QuicknodeSdk::new(&SdkFullConfig {
691 api_key: "test-key".to_string(),
692 http: None,
693 admin: None,
694 streams: None,
695 webhooks: None,
696 kvstore: Some(KvStoreConfig {
697 base_url: Some(base_url),
698 }),
699 sql: None,
700 rpc: None,
701 })
702 .unwrap()
703 }
704
705 #[tokio::test]
708 async fn create_set_success() {
709 let server = MockServer::start().await;
710 Mock::given(method("POST"))
711 .and(path("/sets"))
712 .respond_with(
713 ResponseTemplate::new(201)
714 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
715 )
716 .mount(&server)
717 .await;
718 let sdk = make_sdk(format!("{}/", server.uri()));
719 sdk.kvstore
720 .create_set(&CreateSetParams {
721 key: "k".to_string(),
722 value: "v".to_string(),
723 })
724 .await
725 .unwrap();
726 }
727
728 #[tokio::test]
729 async fn create_set_api_error() {
730 let server = MockServer::start().await;
731 Mock::given(method("POST"))
732 .and(path("/sets"))
733 .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
734 .mount(&server)
735 .await;
736 let sdk = make_sdk(format!("{}/", server.uri()));
737 let err = sdk
738 .kvstore
739 .create_set(&CreateSetParams {
740 key: "k".to_string(),
741 value: "v".to_string(),
742 })
743 .await
744 .unwrap_err();
745 assert!(matches!(err, SdkError::Api { .. }));
746 }
747
748 #[tokio::test]
749 async fn create_set_server_error() {
750 let server = MockServer::start().await;
751 Mock::given(method("POST"))
752 .and(path("/sets"))
753 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
754 .mount(&server)
755 .await;
756 let sdk = make_sdk(format!("{}/", server.uri()));
757 let err = sdk
758 .kvstore
759 .create_set(&CreateSetParams {
760 key: "k".to_string(),
761 value: "v".to_string(),
762 })
763 .await
764 .unwrap_err();
765 assert!(matches!(err, SdkError::Api { .. }));
766 }
767
768 #[tokio::test]
769 async fn get_sets_success() {
770 let server = MockServer::start().await;
771 Mock::given(method("GET"))
772 .and(path("/sets"))
773 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"data": [{"key": "k1", "value": "v1"}, {"key": "k2", "value": "v2"}], "cursor": ""})))
774 .mount(&server)
775 .await;
776 let sdk = make_sdk(format!("{}/", server.uri()));
777 let resp = sdk
778 .kvstore
779 .get_sets(&GetSetsParams::default())
780 .await
781 .unwrap();
782 assert_eq!(resp.data.len(), 2);
783 assert_eq!(resp.data[0].key, "k1");
784 }
785
786 #[tokio::test]
787 async fn get_sets_null_data_empty_store() {
788 let server = MockServer::start().await;
789 Mock::given(method("GET"))
790 .and(path("/sets"))
791 .respond_with(ResponseTemplate::new(200).set_body_json(
792 serde_json::json!({"code": 200, "msg": "", "data": null, "cursor": ""}),
793 ))
794 .mount(&server)
795 .await;
796 let sdk = make_sdk(format!("{}/", server.uri()));
797 let resp = sdk
798 .kvstore
799 .get_sets(&GetSetsParams::default())
800 .await
801 .unwrap();
802 assert!(resp.data.is_empty());
803 assert_eq!(resp.cursor, "");
804 }
805
806 #[tokio::test]
807 async fn get_sets_api_error() {
808 let server = MockServer::start().await;
809 Mock::given(method("GET"))
810 .and(path("/sets"))
811 .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
812 .mount(&server)
813 .await;
814 let sdk = make_sdk(format!("{}/", server.uri()));
815 let err = sdk
816 .kvstore
817 .get_sets(&GetSetsParams::default())
818 .await
819 .unwrap_err();
820 assert!(matches!(err, SdkError::Api { .. }));
821 }
822
823 #[tokio::test]
824 async fn get_sets_server_error() {
825 let server = MockServer::start().await;
826 Mock::given(method("GET"))
827 .and(path("/sets"))
828 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
829 .mount(&server)
830 .await;
831 let sdk = make_sdk(format!("{}/", server.uri()));
832 let err = sdk
833 .kvstore
834 .get_sets(&GetSetsParams::default())
835 .await
836 .unwrap_err();
837 assert!(matches!(err, SdkError::Api { .. }));
838 }
839
840 #[tokio::test]
841 async fn get_set_success() {
842 let server = MockServer::start().await;
843 Mock::given(method("GET"))
844 .and(path("/sets/my-key"))
845 .respond_with(
846 ResponseTemplate::new(200)
847 .set_body_json(serde_json::json!({"data": {"value": "my-value"}})),
848 )
849 .mount(&server)
850 .await;
851 let sdk = make_sdk(format!("{}/", server.uri()));
852 let resp = sdk.kvstore.get_set("my-key").await.unwrap();
853 assert_eq!(resp.value, "my-value");
854 }
855
856 #[tokio::test]
857 async fn get_set_not_found() {
858 let server = MockServer::start().await;
859 Mock::given(method("GET"))
860 .and(path("/sets/my-key"))
861 .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
862 .mount(&server)
863 .await;
864 let sdk = make_sdk(format!("{}/", server.uri()));
865 let err = sdk.kvstore.get_set("my-key").await.unwrap_err();
866 assert!(matches!(err, SdkError::Api { .. }));
867 }
868
869 #[tokio::test]
870 async fn get_set_server_error() {
871 let server = MockServer::start().await;
872 Mock::given(method("GET"))
873 .and(path("/sets/my-key"))
874 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
875 .mount(&server)
876 .await;
877 let sdk = make_sdk(format!("{}/", server.uri()));
878 let err = sdk.kvstore.get_set("my-key").await.unwrap_err();
879 assert!(matches!(err, SdkError::Api { .. }));
880 }
881
882 #[tokio::test]
883 async fn bulk_sets_success() {
884 let server = MockServer::start().await;
885 Mock::given(method("POST"))
886 .and(path("/sets/bulk"))
887 .respond_with(
888 ResponseTemplate::new(201)
889 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
890 )
891 .mount(&server)
892 .await;
893 let sdk = make_sdk(format!("{}/", server.uri()));
894 let mut add = HashMap::new();
895 add.insert("k1".to_string(), "v1".to_string());
896 sdk.kvstore
897 .bulk_sets(&BulkSetsParams {
898 add_sets: Some(add),
899 delete_sets: None,
900 })
901 .await
902 .unwrap();
903 }
904
905 #[tokio::test]
906 async fn bulk_sets_api_error() {
907 let server = MockServer::start().await;
908 Mock::given(method("POST"))
909 .and(path("/sets/bulk"))
910 .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
911 .mount(&server)
912 .await;
913 let sdk = make_sdk(format!("{}/", server.uri()));
914 let err = sdk
915 .kvstore
916 .bulk_sets(&BulkSetsParams {
917 add_sets: None,
918 delete_sets: None,
919 })
920 .await
921 .unwrap_err();
922 assert!(matches!(err, SdkError::Api { .. }));
923 }
924
925 #[tokio::test]
926 async fn bulk_sets_server_error() {
927 let server = MockServer::start().await;
928 Mock::given(method("POST"))
929 .and(path("/sets/bulk"))
930 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
931 .mount(&server)
932 .await;
933 let sdk = make_sdk(format!("{}/", server.uri()));
934 let err = sdk
935 .kvstore
936 .bulk_sets(&BulkSetsParams {
937 add_sets: None,
938 delete_sets: None,
939 })
940 .await
941 .unwrap_err();
942 assert!(matches!(err, SdkError::Api { .. }));
943 }
944
945 #[tokio::test]
949 async fn bulk_sets_wire_body_add_sets() {
950 let server = MockServer::start().await;
951 Mock::given(method("POST"))
952 .and(path("/sets/bulk"))
953 .and(body_json(serde_json::json!({
954 "addSets": {"k1": "v1"}
955 })))
956 .respond_with(
957 ResponseTemplate::new(201)
958 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
959 )
960 .mount(&server)
961 .await;
962 let sdk = make_sdk(format!("{}/", server.uri()));
963 let mut add = HashMap::new();
964 add.insert("k1".to_string(), "v1".to_string());
965 sdk.kvstore
966 .bulk_sets(&BulkSetsParams {
967 add_sets: Some(add),
968 delete_sets: None,
969 })
970 .await
971 .unwrap();
972 }
973
974 #[tokio::test]
975 async fn bulk_sets_wire_body_delete_sets() {
976 let server = MockServer::start().await;
977 Mock::given(method("POST"))
978 .and(path("/sets/bulk"))
979 .and(body_json(serde_json::json!({
980 "deleteSets": ["k1", "k2"]
981 })))
982 .respond_with(
983 ResponseTemplate::new(201)
984 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
985 )
986 .mount(&server)
987 .await;
988 let sdk = make_sdk(format!("{}/", server.uri()));
989 sdk.kvstore
990 .bulk_sets(&BulkSetsParams {
991 add_sets: None,
992 delete_sets: Some(vec!["k1".to_string(), "k2".to_string()]),
993 })
994 .await
995 .unwrap();
996 }
997
998 #[tokio::test]
999 async fn delete_set_success() {
1000 let server = MockServer::start().await;
1001 Mock::given(method("DELETE"))
1002 .and(path("/sets/my-key"))
1003 .respond_with(
1004 ResponseTemplate::new(200)
1005 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1006 )
1007 .mount(&server)
1008 .await;
1009 let sdk = make_sdk(format!("{}/", server.uri()));
1010 sdk.kvstore.delete_set("my-key").await.unwrap();
1011 }
1012
1013 #[tokio::test]
1014 async fn delete_set_not_found() {
1015 let server = MockServer::start().await;
1016 Mock::given(method("DELETE"))
1017 .and(path("/sets/my-key"))
1018 .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1019 .mount(&server)
1020 .await;
1021 let sdk = make_sdk(format!("{}/", server.uri()));
1022 let err = sdk.kvstore.delete_set("my-key").await.unwrap_err();
1023 assert!(matches!(err, SdkError::Api { .. }));
1024 }
1025
1026 #[tokio::test]
1027 async fn delete_set_server_error() {
1028 let server = MockServer::start().await;
1029 Mock::given(method("DELETE"))
1030 .and(path("/sets/my-key"))
1031 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1032 .mount(&server)
1033 .await;
1034 let sdk = make_sdk(format!("{}/", server.uri()));
1035 let err = sdk.kvstore.delete_set("my-key").await.unwrap_err();
1036 assert!(matches!(err, SdkError::Api { .. }));
1037 }
1038
1039 #[tokio::test]
1042 async fn create_list_success() {
1043 let server = MockServer::start().await;
1044 Mock::given(method("POST"))
1045 .and(path("/lists"))
1046 .respond_with(
1047 ResponseTemplate::new(201)
1048 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1049 )
1050 .mount(&server)
1051 .await;
1052 let sdk = make_sdk(format!("{}/", server.uri()));
1053 sdk.kvstore
1054 .create_list(&CreateListParams {
1055 key: "my-list".to_string(),
1056 items: vec!["item1".to_string()],
1057 })
1058 .await
1059 .unwrap();
1060 }
1061
1062 #[tokio::test]
1063 async fn create_list_api_error() {
1064 let server = MockServer::start().await;
1065 Mock::given(method("POST"))
1066 .and(path("/lists"))
1067 .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1068 .mount(&server)
1069 .await;
1070 let sdk = make_sdk(format!("{}/", server.uri()));
1071 let err = sdk
1072 .kvstore
1073 .create_list(&CreateListParams {
1074 key: "my-list".to_string(),
1075 items: vec![],
1076 })
1077 .await
1078 .unwrap_err();
1079 assert!(matches!(err, SdkError::Api { .. }));
1080 }
1081
1082 #[tokio::test]
1083 async fn create_list_server_error() {
1084 let server = MockServer::start().await;
1085 Mock::given(method("POST"))
1086 .and(path("/lists"))
1087 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1088 .mount(&server)
1089 .await;
1090 let sdk = make_sdk(format!("{}/", server.uri()));
1091 let err = sdk
1092 .kvstore
1093 .create_list(&CreateListParams {
1094 key: "my-list".to_string(),
1095 items: vec![],
1096 })
1097 .await
1098 .unwrap_err();
1099 assert!(matches!(err, SdkError::Api { .. }));
1100 }
1101
1102 #[tokio::test]
1103 async fn get_lists_success() {
1104 let server = MockServer::start().await;
1105 Mock::given(method("GET"))
1106 .and(path("/lists"))
1107 .respond_with(ResponseTemplate::new(200).set_body_json(
1108 serde_json::json!({"data": {"keys": ["list1", "list2"]}, "cursor": ""}),
1109 ))
1110 .mount(&server)
1111 .await;
1112 let sdk = make_sdk(format!("{}/", server.uri()));
1113 let resp = sdk
1114 .kvstore
1115 .get_lists(&GetListsParams::default())
1116 .await
1117 .unwrap();
1118 assert_eq!(resp.data.keys, vec!["list1", "list2"]);
1119 }
1120
1121 #[tokio::test]
1122 async fn get_lists_null_data_empty_store() {
1123 let server = MockServer::start().await;
1124 Mock::given(method("GET"))
1125 .and(path("/lists"))
1126 .respond_with(ResponseTemplate::new(200).set_body_json(
1127 serde_json::json!({"code": 200, "msg": "", "data": null, "cursor": ""}),
1128 ))
1129 .mount(&server)
1130 .await;
1131 let sdk = make_sdk(format!("{}/", server.uri()));
1132 let resp = sdk
1133 .kvstore
1134 .get_lists(&GetListsParams::default())
1135 .await
1136 .unwrap();
1137 assert!(resp.data.keys.is_empty());
1138 assert_eq!(resp.cursor, "");
1139 }
1140
1141 #[tokio::test]
1142 async fn get_lists_api_error() {
1143 let server = MockServer::start().await;
1144 Mock::given(method("GET"))
1145 .and(path("/lists"))
1146 .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1147 .mount(&server)
1148 .await;
1149 let sdk = make_sdk(format!("{}/", server.uri()));
1150 let err = sdk
1151 .kvstore
1152 .get_lists(&GetListsParams::default())
1153 .await
1154 .unwrap_err();
1155 assert!(matches!(err, SdkError::Api { .. }));
1156 }
1157
1158 #[tokio::test]
1159 async fn get_lists_server_error() {
1160 let server = MockServer::start().await;
1161 Mock::given(method("GET"))
1162 .and(path("/lists"))
1163 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1164 .mount(&server)
1165 .await;
1166 let sdk = make_sdk(format!("{}/", server.uri()));
1167 let err = sdk
1168 .kvstore
1169 .get_lists(&GetListsParams::default())
1170 .await
1171 .unwrap_err();
1172 assert!(matches!(err, SdkError::Api { .. }));
1173 }
1174
1175 #[tokio::test]
1176 async fn get_list_success() {
1177 let server = MockServer::start().await;
1178 Mock::given(method("GET"))
1179 .and(path("/lists/my-list"))
1180 .respond_with(ResponseTemplate::new(200).set_body_json(
1181 serde_json::json!({"data": {"items": ["item1", "item2"]}, "cursor": ""}),
1182 ))
1183 .mount(&server)
1184 .await;
1185 let sdk = make_sdk(format!("{}/", server.uri()));
1186 let resp = sdk
1187 .kvstore
1188 .get_list("my-list", &GetListParams::default())
1189 .await
1190 .unwrap();
1191 assert_eq!(resp.data.items, vec!["item1", "item2"]);
1192 }
1193
1194 #[tokio::test]
1195 async fn get_list_not_found() {
1196 let server = MockServer::start().await;
1197 Mock::given(method("GET"))
1198 .and(path("/lists/my-list"))
1199 .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1200 .mount(&server)
1201 .await;
1202 let sdk = make_sdk(format!("{}/", server.uri()));
1203 let err = sdk
1204 .kvstore
1205 .get_list("my-list", &GetListParams::default())
1206 .await
1207 .unwrap_err();
1208 assert!(matches!(err, SdkError::Api { .. }));
1209 }
1210
1211 #[tokio::test]
1212 async fn get_list_server_error() {
1213 let server = MockServer::start().await;
1214 Mock::given(method("GET"))
1215 .and(path("/lists/my-list"))
1216 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1217 .mount(&server)
1218 .await;
1219 let sdk = make_sdk(format!("{}/", server.uri()));
1220 let err = sdk
1221 .kvstore
1222 .get_list("my-list", &GetListParams::default())
1223 .await
1224 .unwrap_err();
1225 assert!(matches!(err, SdkError::Api { .. }));
1226 }
1227
1228 #[tokio::test]
1229 async fn update_list_success() {
1230 let server = MockServer::start().await;
1231 Mock::given(method("PATCH"))
1232 .and(path("/lists/my-list"))
1233 .respond_with(
1234 ResponseTemplate::new(200)
1235 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1236 )
1237 .mount(&server)
1238 .await;
1239 let sdk = make_sdk(format!("{}/", server.uri()));
1240 sdk.kvstore
1241 .update_list(
1242 "my-list",
1243 &UpdateListParams {
1244 add_items: Some(vec!["item3".to_string()]),
1245 remove_items: None,
1246 },
1247 )
1248 .await
1249 .unwrap();
1250 }
1251
1252 #[tokio::test]
1256 async fn update_list_wire_body() {
1257 let server = MockServer::start().await;
1258 Mock::given(method("PATCH"))
1259 .and(path("/lists/my-list"))
1260 .and(body_json(serde_json::json!({
1261 "addItems": ["c"],
1262 "removeItems": ["a"]
1263 })))
1264 .respond_with(
1265 ResponseTemplate::new(200)
1266 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1267 )
1268 .mount(&server)
1269 .await;
1270 let sdk = make_sdk(format!("{}/", server.uri()));
1271 sdk.kvstore
1272 .update_list(
1273 "my-list",
1274 &UpdateListParams {
1275 add_items: Some(vec!["c".to_string()]),
1276 remove_items: Some(vec!["a".to_string()]),
1277 },
1278 )
1279 .await
1280 .unwrap();
1281 }
1282
1283 #[tokio::test]
1284 async fn update_list_api_error() {
1285 let server = MockServer::start().await;
1286 Mock::given(method("PATCH"))
1287 .and(path("/lists/my-list"))
1288 .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1289 .mount(&server)
1290 .await;
1291 let sdk = make_sdk(format!("{}/", server.uri()));
1292 let err = sdk
1293 .kvstore
1294 .update_list("my-list", &UpdateListParams::default())
1295 .await
1296 .unwrap_err();
1297 assert!(matches!(err, SdkError::Api { .. }));
1298 }
1299
1300 #[tokio::test]
1301 async fn update_list_server_error() {
1302 let server = MockServer::start().await;
1303 Mock::given(method("PATCH"))
1304 .and(path("/lists/my-list"))
1305 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1306 .mount(&server)
1307 .await;
1308 let sdk = make_sdk(format!("{}/", server.uri()));
1309 let err = sdk
1310 .kvstore
1311 .update_list("my-list", &UpdateListParams::default())
1312 .await
1313 .unwrap_err();
1314 assert!(matches!(err, SdkError::Api { .. }));
1315 }
1316
1317 #[tokio::test]
1318 async fn add_list_item_success() {
1319 let server = MockServer::start().await;
1320 Mock::given(method("POST"))
1321 .and(path("/lists/my-list/items"))
1322 .respond_with(
1323 ResponseTemplate::new(201)
1324 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1325 )
1326 .mount(&server)
1327 .await;
1328 let sdk = make_sdk(format!("{}/", server.uri()));
1329 sdk.kvstore
1330 .add_list_item(
1331 "my-list",
1332 &AddListItemParams {
1333 item: "item1".to_string(),
1334 },
1335 )
1336 .await
1337 .unwrap();
1338 }
1339
1340 #[tokio::test]
1341 async fn add_list_item_api_error() {
1342 let server = MockServer::start().await;
1343 Mock::given(method("POST"))
1344 .and(path("/lists/my-list/items"))
1345 .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1346 .mount(&server)
1347 .await;
1348 let sdk = make_sdk(format!("{}/", server.uri()));
1349 let err = sdk
1350 .kvstore
1351 .add_list_item(
1352 "my-list",
1353 &AddListItemParams {
1354 item: "item1".to_string(),
1355 },
1356 )
1357 .await
1358 .unwrap_err();
1359 assert!(matches!(err, SdkError::Api { .. }));
1360 }
1361
1362 #[tokio::test]
1363 async fn add_list_item_server_error() {
1364 let server = MockServer::start().await;
1365 Mock::given(method("POST"))
1366 .and(path("/lists/my-list/items"))
1367 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1368 .mount(&server)
1369 .await;
1370 let sdk = make_sdk(format!("{}/", server.uri()));
1371 let err = sdk
1372 .kvstore
1373 .add_list_item(
1374 "my-list",
1375 &AddListItemParams {
1376 item: "item1".to_string(),
1377 },
1378 )
1379 .await
1380 .unwrap_err();
1381 assert!(matches!(err, SdkError::Api { .. }));
1382 }
1383
1384 #[tokio::test]
1385 async fn list_contains_item_success() {
1386 let server = MockServer::start().await;
1387 Mock::given(method("GET"))
1388 .and(path("/lists/my-list/contains/item1"))
1389 .respond_with(
1390 ResponseTemplate::new(200)
1391 .set_body_json(serde_json::json!({"data": {"exists": true}})),
1392 )
1393 .mount(&server)
1394 .await;
1395 let sdk = make_sdk(format!("{}/", server.uri()));
1396 let resp = sdk
1397 .kvstore
1398 .list_contains_item("my-list", "item1")
1399 .await
1400 .unwrap();
1401 assert!(resp.exists);
1402 }
1403
1404 #[tokio::test]
1405 async fn list_contains_item_not_found() {
1406 let server = MockServer::start().await;
1407 Mock::given(method("GET"))
1408 .and(path("/lists/my-list/contains/item1"))
1409 .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1410 .mount(&server)
1411 .await;
1412 let sdk = make_sdk(format!("{}/", server.uri()));
1413 let err = sdk
1414 .kvstore
1415 .list_contains_item("my-list", "item1")
1416 .await
1417 .unwrap_err();
1418 assert!(matches!(err, SdkError::Api { .. }));
1419 }
1420
1421 #[tokio::test]
1422 async fn list_contains_item_server_error() {
1423 let server = MockServer::start().await;
1424 Mock::given(method("GET"))
1425 .and(path("/lists/my-list/contains/item1"))
1426 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1427 .mount(&server)
1428 .await;
1429 let sdk = make_sdk(format!("{}/", server.uri()));
1430 let err = sdk
1431 .kvstore
1432 .list_contains_item("my-list", "item1")
1433 .await
1434 .unwrap_err();
1435 assert!(matches!(err, SdkError::Api { .. }));
1436 }
1437
1438 #[tokio::test]
1439 async fn delete_list_item_success() {
1440 let server = MockServer::start().await;
1441 Mock::given(method("DELETE"))
1442 .and(path("/lists/my-list/items/item1"))
1443 .respond_with(
1444 ResponseTemplate::new(200)
1445 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1446 )
1447 .mount(&server)
1448 .await;
1449 let sdk = make_sdk(format!("{}/", server.uri()));
1450 sdk.kvstore
1451 .delete_list_item("my-list", "item1")
1452 .await
1453 .unwrap();
1454 }
1455
1456 #[tokio::test]
1457 async fn delete_list_item_not_found() {
1458 let server = MockServer::start().await;
1459 Mock::given(method("DELETE"))
1460 .and(path("/lists/my-list/items/item1"))
1461 .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1462 .mount(&server)
1463 .await;
1464 let sdk = make_sdk(format!("{}/", server.uri()));
1465 let err = sdk
1466 .kvstore
1467 .delete_list_item("my-list", "item1")
1468 .await
1469 .unwrap_err();
1470 assert!(matches!(err, SdkError::Api { .. }));
1471 }
1472
1473 #[tokio::test]
1474 async fn delete_list_item_server_error() {
1475 let server = MockServer::start().await;
1476 Mock::given(method("DELETE"))
1477 .and(path("/lists/my-list/items/item1"))
1478 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1479 .mount(&server)
1480 .await;
1481 let sdk = make_sdk(format!("{}/", server.uri()));
1482 let err = sdk
1483 .kvstore
1484 .delete_list_item("my-list", "item1")
1485 .await
1486 .unwrap_err();
1487 assert!(matches!(err, SdkError::Api { .. }));
1488 }
1489
1490 #[tokio::test]
1491 async fn delete_list_success() {
1492 let server = MockServer::start().await;
1493 Mock::given(method("DELETE"))
1494 .and(path("/lists/my-list"))
1495 .respond_with(
1496 ResponseTemplate::new(200)
1497 .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1498 )
1499 .mount(&server)
1500 .await;
1501 let sdk = make_sdk(format!("{}/", server.uri()));
1502 sdk.kvstore.delete_list("my-list").await.unwrap();
1503 }
1504
1505 #[tokio::test]
1506 async fn delete_list_not_found() {
1507 let server = MockServer::start().await;
1508 Mock::given(method("DELETE"))
1509 .and(path("/lists/my-list"))
1510 .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1511 .mount(&server)
1512 .await;
1513 let sdk = make_sdk(format!("{}/", server.uri()));
1514 let err = sdk.kvstore.delete_list("my-list").await.unwrap_err();
1515 assert!(matches!(err, SdkError::Api { .. }));
1516 }
1517
1518 #[tokio::test]
1519 async fn delete_list_server_error() {
1520 let server = MockServer::start().await;
1521 Mock::given(method("DELETE"))
1522 .and(path("/lists/my-list"))
1523 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1524 .mount(&server)
1525 .await;
1526 let sdk = make_sdk(format!("{}/", server.uri()));
1527 let err = sdk.kvstore.delete_list("my-list").await.unwrap_err();
1528 assert!(matches!(err, SdkError::Api { .. }));
1529 }
1530}