Skip to main content

tango/resources/
meta.rs

1//! Meta endpoints — `/api/version/` and `/api/api-keys/`.
2//!
3//! These are top-level utility endpoints that don't fit under any one
4//! resource family.
5
6use crate::client::Client;
7use crate::error::Result;
8use crate::internal::apply_pagination;
9use crate::Record;
10use bon::Builder;
11use std::collections::BTreeMap;
12
13/// Options for [`Client::list_api_keys`].
14///
15/// `/api/api-keys/` doesn't paginate in practice today, but we accept the
16/// standard pagination/shape knobs anyway so callers can stay consistent
17/// with every other list endpoint, and so the SDK doesn't need a breaking
18/// change if the server starts paginating later.
19#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
20#[non_exhaustive]
21pub struct ListApiKeysOptions {
22    /// 1-based page number (forwarded if the server starts paginating).
23    #[builder(into)]
24    pub page: Option<u32>,
25    /// Page size.
26    #[builder(into)]
27    pub limit: Option<u32>,
28    /// Keyset cursor.
29    #[builder(into)]
30    pub cursor: Option<String>,
31    /// Comma-separated field selector.
32    #[builder(into)]
33    pub shape: Option<String>,
34    /// Collapse nested objects into dot-separated keys.
35    #[builder(default)]
36    pub flat: bool,
37    /// When [`flat`](Self::flat) is also true, flatten list-valued fields.
38    #[builder(default)]
39    pub flat_lists: bool,
40
41    /// Escape hatch for keys not first-classed here.
42    #[builder(default)]
43    pub extra: BTreeMap<String, String>,
44}
45
46impl ListApiKeysOptions {
47    fn to_query(&self) -> Vec<(String, String)> {
48        let mut q = Vec::new();
49        apply_pagination(
50            &mut q,
51            self.page,
52            self.limit,
53            self.cursor.as_deref(),
54            self.shape.as_deref(),
55            self.flat,
56            self.flat_lists,
57        );
58        for (k, v) in &self.extra {
59            if !v.is_empty() {
60                q.push((k.clone(), v.clone()));
61            }
62        }
63        q
64    }
65}
66
67impl Client {
68    /// `GET /api/version/` — return the API server's version metadata
69    /// (build commit, deployed-at, etc.).
70    ///
71    /// Single-value endpoint: the server returns a JSON object, not a
72    /// paginated envelope, so this returns a [`Record`] directly.
73    pub async fn get_version(&self) -> Result<Record> {
74        self.get_json::<Record>("/api/version/", &[]).await
75    }
76
77    /// `GET /api/api-keys/` — return the authenticated caller's API keys
78    /// and associated metadata.
79    ///
80    /// The server returns a single structured object today (no `count` /
81    /// `results` envelope), so this returns a [`Record`] rather than a
82    /// [`Page`](crate::Page). Mirrors the Go SDK's `ListAPIKeys` shape.
83    pub async fn list_api_keys(&self, opts: ListApiKeysOptions) -> Result<Record> {
84        let q = opts.to_query();
85        self.get_json::<Record>("/api/api-keys/", &q).await
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
94        q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
95    }
96
97    #[test]
98    fn api_keys_opts_emit_pagination() {
99        let opts = ListApiKeysOptions::builder()
100            .page(3u32)
101            .limit(25u32)
102            .build();
103        let q = opts.to_query();
104        assert_eq!(get_q(&q, "page").as_deref(), Some("3"));
105        assert_eq!(get_q(&q, "limit").as_deref(), Some("25"));
106    }
107
108    #[test]
109    fn api_keys_opts_empty_default() {
110        let opts = ListApiKeysOptions::default();
111        assert!(opts.to_query().is_empty());
112    }
113
114    #[test]
115    fn api_keys_opts_extra_passthrough() {
116        let mut extra = BTreeMap::new();
117        extra.insert("scope".into(), "read".into());
118        let opts = ListApiKeysOptions::builder().extra(extra).build();
119        let q = opts.to_query();
120        assert_eq!(get_q(&q, "scope").as_deref(), Some("read"));
121    }
122}