Skip to main content

babelforce_manager_sdk/resources/
applications.rs

1use serde_json::Value;
2
3use crate::error::{map_manager_err, ManagerError};
4use crate::gen::manager::apis::configuration::Configuration;
5use crate::gen::manager::apis::{application_api as api, local_automation_api, manager_api};
6use crate::gen::manager::models;
7use crate::http::{collect_all, fetch_page, Page};
8use crate::resources::raw::{get_json, post_json, post_json_body, put_json};
9use crate::retry::{with_retry, RetryPolicy};
10use crate::token::SharedCfg;
11
12/// One application as returned by the API — typed when this SDK version knows its `module`,
13/// preserved raw when it doesn't.
14///
15/// The applications list mixes module types, and the platform can ship modules newer than this
16/// SDK. Instead of failing a whole list (or get) on the first unfamiliar `module`, such items are
17/// returned as [`ApplicationItem::Unknown`] with their JSON intact.
18#[derive(Debug, Clone, PartialEq)]
19#[non_exhaustive]
20pub enum ApplicationItem {
21    /// A module type this SDK version knows.
22    Typed(models::Application),
23    /// A module type unknown to this SDK version — raw JSON preserved.
24    Unknown {
25        /// The `module` discriminator the server sent (empty if it sent none).
26        module: String,
27        /// The full application object, verbatim.
28        raw: serde_json::Value,
29    },
30}
31
32impl ApplicationItem {
33    /// The typed application, when this SDK version knows its module.
34    pub fn as_typed(&self) -> Option<&models::Application> {
35        match self {
36            ApplicationItem::Typed(app) => Some(app),
37            ApplicationItem::Unknown { .. } => None,
38        }
39    }
40
41    /// The `module` string, for both variants.
42    pub fn module(&self) -> &str {
43        use models::Application as App;
44        match self {
45            ApplicationItem::Typed(app) => match app {
46                App::AgentQueue(_) => "agentQueue",
47                App::Agentic(_) => "agentic",
48                App::AudioPlayer(_) => "audioPlayer",
49                App::ConsumerQueue(_) => "consumerQueue",
50                App::InputReader(_) => "inputReader",
51                App::InputReaderV2(_) => "inputReader.v2",
52                App::PromptPlayer(_) => "promptPlayer",
53                App::Recording(_) => "recording",
54                App::SimpleMenu(_) => "simpleMenu",
55                App::SpeechToText(_) => "speechToText",
56                App::SwitchNode(_) => "switchNode",
57                App::TextToSpeech(_) => "textToSpeech",
58                App::Transfer(_) => "transfer",
59            },
60            ApplicationItem::Unknown { module, .. } => module,
61        }
62    }
63}
64
65/// Decode one application object **variant-directly**.
66///
67/// The generated [`models::Application`] is an internally-tagged enum (`#[serde(tag = "module")]`),
68/// so serde strips the tag before deserializing the variant content — but every generated variant
69/// struct *requires* its own `module` field, making the derived `Deserialize` fail on **every**
70/// real payload (an openapi-generator discriminator artifact). The facade reads the tag itself and
71/// deserializes the full (tag-carrying) object straight into the variant struct — no generated
72/// code touched. Modules this SDK version doesn't know come back as [`ApplicationItem::Unknown`]
73/// rather than an error; only items that aren't JSON objects at all still fail.
74fn decode_application(v: Value) -> Result<ApplicationItem, ManagerError> {
75    use models::Application as App;
76    fn var<T: serde::de::DeserializeOwned>(v: Value) -> Result<Box<T>, ManagerError> {
77        serde_json::from_value(v)
78            .map(Box::new)
79            .map_err(|e| ManagerError::Decode(e.to_string()))
80    }
81    if !v.is_object() {
82        return Err(ManagerError::Decode(
83            "application item is not a JSON object".into(),
84        ));
85    }
86    let module = v
87        .get("module")
88        .and_then(Value::as_str)
89        .unwrap_or_default()
90        .to_string();
91    Ok(ApplicationItem::Typed(match module.as_str() {
92        "agentQueue" => App::AgentQueue(var(v)?),
93        "agentic" => App::Agentic(var(v)?),
94        "audioPlayer" => App::AudioPlayer(var(v)?),
95        "consumerQueue" => App::ConsumerQueue(var(v)?),
96        "inputReader" => App::InputReader(var(v)?),
97        "inputReader.v2" => App::InputReaderV2(var(v)?),
98        "promptPlayer" => App::PromptPlayer(var(v)?),
99        "recording" => App::Recording(var(v)?),
100        "simpleMenu" => App::SimpleMenu(var(v)?),
101        "speechToText" => App::SpeechToText(var(v)?),
102        "switchNode" => App::SwitchNode(var(v)?),
103        "textToSpeech" => App::TextToSpeech(var(v)?),
104        "transfer" => App::Transfer(var(v)?),
105        _ => return Ok(ApplicationItem::Unknown { module, raw: v }),
106    }))
107}
108
109/// Decode a `{ item, … }` envelope (get/create/update/clone responses) into an
110/// [`ApplicationItem`].
111fn decode_item_envelope(v: Value) -> Result<ApplicationItem, ManagerError> {
112    let item = v
113        .get("item")
114        .cloned()
115        .ok_or_else(|| ManagerError::Decode("response had no `item`".into()))?;
116    decode_application(item)
117}
118
119/// One raw page of `/api/v2/applications`, decoded item-by-item.
120///
121/// Stands in for the generated `list_applications`: that returns the internally-tagged
122/// [`models::Application`] enum, which fails to deserialize on every real payload (see
123/// [`decode_application`]), so the facade reads raw and decodes each item itself.
124async fn applications_page(
125    cfg: &Configuration,
126    page: i32,
127    per_page: Option<i32>,
128) -> Result<(Vec<ApplicationItem>, i32, i32, Option<i32>), ManagerError> {
129    let mut path = format!("/api/v2/applications?page={page}");
130    if let Some(max) = per_page {
131        path.push_str(&format!("&max={max}"));
132    }
133    let v = get_json(cfg, &path).await?;
134    let items = v
135        .get("items")
136        .and_then(Value::as_array)
137        .cloned()
138        .unwrap_or_default()
139        .into_iter()
140        .map(decode_application)
141        .collect::<Result<Vec<_>, _>>()?;
142    let p = v.get("pagination").cloned().unwrap_or(Value::Null);
143    let num = |k: &str| p.get(k).and_then(Value::as_i64).map(|n| n as i32);
144    Ok((
145        items,
146        num("pages").unwrap_or(1),
147        num("current").unwrap_or(page),
148        num("total"),
149    ))
150}
151
152/// Applications — `/api/v2/applications`, with a nested `local_automations` sub-resource.
153pub struct ApplicationsResource {
154    pub(crate) cfg: SharedCfg<Configuration>,
155    pub(crate) retry: RetryPolicy,
156    /// Per-application local automations — `/api/v2/applications/{applicationId}/actions`.
157    pub local_automations: LocalAutomationsResource,
158}
159
160impl ApplicationsResource {
161    /// List all applications (auto-paginated).
162    pub async fn list_all(&self) -> Result<Vec<ApplicationItem>, ManagerError> {
163        let cfg = self.cfg.get().await?;
164        let cfg = cfg.as_ref();
165        collect_all(
166            &self.retry,
167            |e| e,
168            |page| async move {
169                applications_page(cfg, page, None)
170                    .await
171                    .map(|(items, pages, current, _)| (items, pages, current))
172            },
173        )
174        .await
175    }
176
177    /// List one page of applications.
178    pub async fn list_page(
179        &self,
180        page: i32,
181        per_page: Option<i32>,
182    ) -> Result<Page<ApplicationItem>, ManagerError> {
183        let cfg = self.cfg.get().await?;
184        let cfg = cfg.as_ref();
185        fetch_page(
186            &self.retry,
187            |e| e,
188            || async move { applications_page(cfg, page, per_page).await },
189        )
190        .await
191    }
192
193    /// List all local automations across applications (auto-paginated).
194    pub async fn all_local_automations(
195        &self,
196    ) -> Result<Vec<models::LocalAutomation>, ManagerError> {
197        let cfg = self.cfg.get().await?;
198        let cfg = cfg.as_ref();
199        collect_all(&self.retry, map_manager_err, |page| async move {
200            let r = local_automation_api::list_all_local_automations(cfg, Some(page), None).await?;
201            Ok((
202                r.items,
203                r.pagination.pages.unwrap_or(1),
204                r.pagination.current.unwrap_or(1),
205            ))
206        })
207        .await
208    }
209
210    /// Create an application.
211    ///
212    /// Routes the write through the raw JSON path and [`decode_application`], standing in for the
213    /// generated `create_application` — whose typed [`models::ApplicationItemResponse`] return
214    /// fails to deserialize on every real success payload, misreporting a *completed* create as
215    /// an error.
216    pub async fn create(
217        &self,
218        body: models::ApplicationCreateBody,
219    ) -> Result<ApplicationItem, ManagerError> {
220        let body = serde_json::to_value(&body)
221            .map_err(|e| ManagerError::InvalidArgument(e.to_string()))?;
222        decode_item_envelope(self.create_raw(body).await?)
223    }
224
225    /// Create an application, reading the response **raw** — for callers that want the
226    /// server's JSON verbatim instead of the decoded [`ApplicationItem`].
227    pub async fn create_raw(&self, body: Value) -> Result<Value, ManagerError> {
228        let cfg = self.cfg.get().await?;
229        let cfg = cfg.as_ref();
230        // Non-idempotent create → no retry on ambiguous failures.
231        with_retry(&self.retry, false, || {
232            post_json_body(cfg, "/api/v2/applications", &body)
233        })
234        .await
235    }
236
237    /// Get an application by id.
238    ///
239    /// Reads the response **raw** and decodes via [`decode_application`], standing in for the
240    /// generated `get_application` — whose typed [`models::Application`] return fails to
241    /// deserialize on every real payload.
242    pub async fn get(&self, id: &str) -> Result<ApplicationItem, ManagerError> {
243        let cfg = self.cfg.get().await?;
244        let cfg = cfg.as_ref();
245        let path = format!(
246            "/api/v2/applications/{}",
247            crate::gen::manager::apis::urlencode(id)
248        );
249        with_retry(&self.retry, true, || async {
250            decode_item_envelope(get_json(cfg, &path).await?)
251        })
252        .await
253    }
254
255    /// Update an application.
256    ///
257    /// Routes the write through the raw JSON path and [`decode_application`], standing in for the
258    /// generated `update_application` — same typed-response decode failure as [`Self::create`].
259    pub async fn update(
260        &self,
261        id: &str,
262        body: models::ApplicationUpdateBody,
263    ) -> Result<ApplicationItem, ManagerError> {
264        let body = serde_json::to_value(&body)
265            .map_err(|e| ManagerError::InvalidArgument(e.to_string()))?;
266        decode_item_envelope(self.update_raw(id, body).await?)
267    }
268
269    /// Update an application, reading the response **raw** (the PUT sibling of
270    /// [`Self::create_raw`]).
271    pub async fn update_raw(&self, id: &str, body: Value) -> Result<Value, ManagerError> {
272        let cfg = self.cfg.get().await?;
273        let cfg = cfg.as_ref();
274        let path = format!(
275            "/api/v2/applications/{}",
276            crate::gen::manager::apis::urlencode(id)
277        );
278        // Non-idempotent write → no retry on ambiguous failures.
279        with_retry(&self.retry, false, || put_json(cfg, &path, &body)).await
280    }
281
282    /// Delete an application.
283    pub async fn delete(&self, id: &str) -> Result<(), ManagerError> {
284        let cfg = self.cfg.get().await?;
285        let cfg = cfg.as_ref();
286        with_retry(&self.retry, false, || api::delete_application(cfg, id))
287            .await
288            .map_err(map_manager_err)?;
289        Ok(())
290    }
291
292    /// Delete several applications by id.
293    pub async fn delete_many(
294        &self,
295        ids: Vec<String>,
296    ) -> Result<models::DefaultV2MessageResponse, ManagerError> {
297        let cfg = self.cfg.get().await?;
298        let cfg = cfg.as_ref();
299        let body = models::DeleteManyApplicationsRequest { ids };
300        with_retry(&self.retry, false, || {
301            api::delete_many_applications(cfg, body.clone())
302        })
303        .await
304        .map_err(map_manager_err)
305    }
306
307    /// List the available application modules.
308    pub async fn list_modules(&self) -> Result<models::ListModulesResponse, ManagerError> {
309        let cfg = self.cfg.get().await?;
310        let cfg = cfg.as_ref();
311        with_retry(&self.retry, true, || api::list_modules(cfg))
312            .await
313            .map_err(map_manager_err)
314    }
315
316    /// Dispatch an application's local automations at a given position. `is_async` runs without
317    /// waiting for the result.
318    pub async fn dispatch(
319        &self,
320        id: &str,
321        position: &str,
322        is_async: bool,
323        body: models::LocalAutomationDispatch,
324    ) -> Result<models::DispatchLocalAutomationsResponse, ManagerError> {
325        let cfg = self.cfg.get().await?;
326        let cfg = cfg.as_ref();
327        with_retry(&self.retry, false, || {
328            manager_api::dispatch_local_automations(cfg, id, position, is_async, Some(body.clone()))
329        })
330        .await
331        .map_err(map_manager_err)
332    }
333
334    /// Clone an application by id.
335    ///
336    /// Routes the write through the raw JSON path and [`decode_application`], standing in for the
337    /// generated `clone_application` — same typed-response decode failure as [`Self::create`].
338    pub async fn clone(&self, id: &str) -> Result<ApplicationItem, ManagerError> {
339        decode_item_envelope(self.clone_raw(id).await?)
340    }
341
342    /// Clone an application by id, reading the response **raw** — for callers that want the
343    /// server's JSON verbatim instead of the decoded [`ApplicationItem`].
344    pub async fn clone_raw(&self, id: &str) -> Result<Value, ManagerError> {
345        let cfg = self.cfg.get().await?;
346        let cfg = cfg.as_ref();
347        let path = format!(
348            "/api/v2/applications/{}/clone",
349            crate::gen::manager::apis::urlencode(id)
350        );
351        // Non-idempotent action → no retry on ambiguous failures.
352        with_retry(&self.retry, false, || post_json(cfg, &path)).await
353    }
354
355    /// Bulk-update several applications in one request.
356    pub async fn bulk_update(
357        &self,
358        body: models::BulkUpdateApplicationsRequest,
359    ) -> Result<models::DefaultV2MessageResponse, ManagerError> {
360        let cfg = self.cfg.get().await?;
361        let cfg = cfg.as_ref();
362        with_retry(&self.retry, false, || {
363            api::bulk_update_applications(cfg, body.clone())
364        })
365        .await
366        .map_err(map_manager_err)
367    }
368
369    /// List the actions available across applications.
370    pub async fn list_app_actions(&self) -> Result<models::ObjectListResponse, ManagerError> {
371        let cfg = self.cfg.get().await?;
372        let cfg = cfg.as_ref();
373        with_retry(&self.retry, true, || {
374            api::list_application_actions(cfg, None, None)
375        })
376        .await
377        .map_err(map_manager_err)
378    }
379
380    /// List application errors.
381    pub async fn list_errors(&self) -> Result<models::ObjectListResponse, ManagerError> {
382        let cfg = self.cfg.get().await?;
383        let cfg = cfg.as_ref();
384        with_retry(&self.retry, true, || api::list_application_errors(cfg))
385            .await
386            .map_err(map_manager_err)
387    }
388}
389
390/// Per-application local automations — `/api/v2/applications/{applicationId}/actions`.
391pub struct LocalAutomationsResource {
392    pub(crate) cfg: SharedCfg<Configuration>,
393    pub(crate) retry: RetryPolicy,
394}
395
396impl LocalAutomationsResource {
397    /// List all of an application's local automations (auto-paginated).
398    pub async fn list_all(
399        &self,
400        application_id: &str,
401    ) -> Result<Vec<models::LocalAutomation>, ManagerError> {
402        let cfg = self.cfg.get().await?;
403        let cfg = cfg.as_ref();
404        collect_all(&self.retry, map_manager_err, |page| async move {
405            let r =
406                local_automation_api::list_local_automations(cfg, application_id, Some(page), None)
407                    .await?;
408            Ok((
409                r.items,
410                r.pagination.pages.unwrap_or(1),
411                r.pagination.current.unwrap_or(1),
412            ))
413        })
414        .await
415    }
416
417    /// Create a local automation on an application.
418    pub async fn create(
419        &self,
420        application_id: &str,
421        body: models::RestCreateLocalAutomation,
422    ) -> Result<models::LocalAutomationItemResponse, ManagerError> {
423        let cfg = self.cfg.get().await?;
424        let cfg = cfg.as_ref();
425        with_retry(&self.retry, false, || {
426            local_automation_api::create_local_automation(cfg, application_id, body.clone())
427        })
428        .await
429        .map_err(map_manager_err)
430    }
431
432    /// Get a local automation by id.
433    pub async fn get(
434        &self,
435        application_id: &str,
436        id: &str,
437    ) -> Result<models::LocalAutomationItemResponse, ManagerError> {
438        let cfg = self.cfg.get().await?;
439        let cfg = cfg.as_ref();
440        with_retry(&self.retry, true, || {
441            local_automation_api::get_local_automation(cfg, application_id, id)
442        })
443        .await
444        .map_err(map_manager_err)
445    }
446
447    /// Update a local automation.
448    pub async fn update(
449        &self,
450        application_id: &str,
451        id: &str,
452        body: models::RestUpdateLocalAutomation,
453    ) -> Result<models::LocalAutomationItemResponse, ManagerError> {
454        let cfg = self.cfg.get().await?;
455        let cfg = cfg.as_ref();
456        with_retry(&self.retry, false, || {
457            local_automation_api::update_local_automation(cfg, application_id, id, body.clone())
458        })
459        .await
460        .map_err(map_manager_err)
461    }
462
463    /// Delete a local automation.
464    pub async fn delete(&self, application_id: &str, id: &str) -> Result<(), ManagerError> {
465        let cfg = self.cfg.get().await?;
466        let cfg = cfg.as_ref();
467        with_retry(&self.retry, false, || {
468            local_automation_api::delete_local_automation(cfg, application_id, id)
469        })
470        .await
471        .map_err(map_manager_err)?;
472        Ok(())
473    }
474}