Skip to main content

akribes_sdk/sub/
projects.rs

1use std::sync::Arc;
2
3use crate::client::{AkribesClient, Inner};
4use crate::error::Result;
5use crate::models::*;
6
7/// Sub-client for project management. Obtained via [`AkribesClient::projects()`].
8///
9/// Does **not** require a `project_id` on the parent client.
10#[derive(Clone, Debug)]
11pub struct ProjectsClient {
12    pub(crate) inner: Arc<Inner>,
13}
14
15impl ProjectsClient {
16    pub(crate) fn new(inner: Arc<Inner>) -> Self {
17        Self { inner }
18    }
19
20    /// Wrap as an AkribesClient to reuse HTTP helpers.
21    fn c(&self) -> AkribesClient {
22        AkribesClient {
23            inner: Arc::clone(&self.inner),
24        }
25    }
26
27    pub async fn list(&self) -> Result<Vec<Project>> {
28        let url = format!("{}/projects", self.inner.base_url);
29        self.c().get_list(&url).await
30    }
31
32    pub async fn get(&self, project_id: i64) -> Result<Option<Project>> {
33        let url = format!("{}/projects/{}", self.inner.base_url, project_id);
34        self.c().get_opt(&url).await
35    }
36
37    pub async fn create(&self, name: &str) -> Result<Project> {
38        let url = format!("{}/projects", self.inner.base_url);
39        self.c().post(&url, &CreateProjectRequest { name }).await
40    }
41
42    pub async fn update(&self, project_id: i64, name: &str) -> Result<Project> {
43        let url = format!("{}/projects/{}", self.inner.base_url, project_id);
44        self.c().patch(&url, &UpdateProjectRequest { name }).await
45    }
46
47    /// Look up a project by numeric id (if `id_or_name` parses as `i64`) or
48    /// by exact name. Returns `None` when no project matches.
49    ///
50    /// Numeric-looking inputs are treated as ids first; a caller that really
51    /// wants to look up a project *named* e.g. `"42"` must currently fetch
52    /// the full list and filter themselves.
53    pub async fn resolve(&self, id_or_name: &str) -> Result<Option<Project>> {
54        if let Ok(id) = id_or_name.parse::<i64>() {
55            return self.get(id).await;
56        }
57        let list = self.list().await?;
58        Ok(list.into_iter().find(|p| p.name == id_or_name))
59    }
60
61    pub async fn delete(&self, project_id: i64) -> Result<()> {
62        let url = format!("{}/projects/{}", self.inner.base_url, project_id);
63        self.c().delete(&url).await?;
64        Ok(())
65    }
66
67    /// Duplicate a project (including all scripts). Server picks the copy
68    /// name. Requires a wildcard-scoped identity.
69    pub async fn duplicate(&self, project_id: i64) -> Result<Project> {
70        let url = format!("{}/projects/{}/duplicate", self.inner.base_url, project_id);
71        self.c().post(&url, &serde_json::json!({})).await
72    }
73
74    /// Set the global project ordering. `order` is the list of project IDs
75    /// in the desired order. Requires a wildcard-scoped identity.
76    pub async fn reorder(&self, order: Vec<i64>) -> Result<()> {
77        let url = format!("{}/projects/reorder", self.inner.base_url);
78        self.c().put_empty(&url, &ReorderRequest { order }).await
79    }
80
81    // ── Flat cross-project script ops ───────────────────────────────────
82    //
83    // The project-scoped chain (`client.project(id).scripts().X`) is the
84    // primary surface for script management, but admin-style code that
85    // touches several projects in a row reads more naturally with flat,
86    // cross-project ops on `projects` itself — same as TS `projects.*`.
87    // Both surfaces coexist; these delegate to the equivalent server
88    // endpoints directly to avoid an extra constructor allocation.
89
90    fn script_url(&self, project_id: i64, script_name: &str) -> String {
91        format!(
92            "{}/projects/{}/scripts/{}",
93            self.inner.base_url,
94            project_id,
95            urlencoding::encode(script_name)
96        )
97    }
98
99    /// List scripts in a specific project. Flat alternative to
100    /// `client.project(id).scripts().list()`.
101    pub async fn list_scripts(&self, project_id: i64) -> Result<Vec<Script>> {
102        let url = format!("{}/projects/{}/scripts", self.inner.base_url, project_id);
103        self.c().get_list(&url).await
104    }
105
106    /// Move a script from `src_project_id` to `dest_project_id`. Flat
107    /// alternative to `client.project(src).scripts().move_to(name, dest)`.
108    pub async fn move_script(
109        &self,
110        src_project_id: i64,
111        src_script_name: &str,
112        dest_project_id: i64,
113    ) -> Result<Script> {
114        let url = format!("{}/move", self.script_url(src_project_id, src_script_name));
115        self.c()
116            .post(
117                &url,
118                &MoveScriptRequest {
119                    target_project_id: dest_project_id,
120                },
121            )
122            .await
123    }
124
125    /// Rename a script in `project_id`. Flat alternative to
126    /// `client.project(id).scripts().rename(current, new)`.
127    pub async fn rename_script(
128        &self,
129        project_id: i64,
130        current_name: &str,
131        new_name: &str,
132    ) -> Result<()> {
133        let url = self.script_url(project_id, current_name);
134        self.c()
135            .patch_empty(&url, &RenameScriptRequest { new_name })
136            .await
137    }
138
139    /// Delete a script in `project_id`. Flat alternative to
140    /// `client.project(id).scripts().delete(name)`.
141    pub async fn delete_script(&self, project_id: i64, script_name: &str) -> Result<()> {
142        let url = self.script_url(project_id, script_name);
143        self.c().delete(&url).await?;
144        Ok(())
145    }
146
147    /// Duplicate a script within `project_id`. The server picks the copy
148    /// name; `new_name` is accepted for parity with the TypeScript SDK and
149    /// will be honored once the server supports it. Flat alternative to
150    /// `client.project(id).scripts().duplicate(name)`.
151    pub async fn duplicate_script(
152        &self,
153        project_id: i64,
154        script_name: &str,
155        _new_name: Option<&str>,
156    ) -> Result<Script> {
157        let url = format!("{}/duplicate", self.script_url(project_id, script_name));
158        self.c().post(&url, &serde_json::json!({})).await
159    }
160
161    /// List channels for `script_name` in `project_id` (#1141). Flat
162    /// cross-project alternative to
163    /// `client.project(id).channels().list(name)`. Mirrors TS
164    /// `projects.listChannels`.
165    pub async fn list_channels(
166        &self,
167        project_id: i64,
168        script_name: &str,
169    ) -> Result<Vec<ScriptChannel>> {
170        let url = format!("{}/channels", self.script_url(project_id, script_name));
171        self.c().get_list(&url).await
172    }
173
174    /// Reorder scripts within `project_id` (#1141). `order` is the list of
175    /// script IDs in the desired order. Flat cross-project alternative to
176    /// `client.project(id).scripts().reorder(order)`. Mirrors TS
177    /// `projects.reorderScripts`.
178    pub async fn reorder_scripts(&self, project_id: i64, order: Vec<i64>) -> Result<()> {
179        let url = format!(
180            "{}/projects/{}/scripts/reorder",
181            self.inner.base_url, project_id
182        );
183        self.c().put_empty(&url, &ReorderRequest { order }).await
184    }
185}