Skip to main content

quicknode_sdk/kvstore/
mod.rs

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