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((r.items, r.pagination.pages, r.pagination.current))
202        })
203        .await
204    }
205
206    /// Create an application.
207    ///
208    /// Routes the write through the raw JSON path and [`decode_application`], standing in for the
209    /// generated `create_application` — whose typed [`models::ApplicationItemResponse`] return
210    /// fails to deserialize on every real success payload, misreporting a *completed* create as
211    /// an error.
212    pub async fn create(
213        &self,
214        body: models::ApplicationCreateBody,
215    ) -> Result<ApplicationItem, ManagerError> {
216        let body = serde_json::to_value(&body)
217            .map_err(|e| ManagerError::InvalidArgument(e.to_string()))?;
218        decode_item_envelope(self.create_raw(body).await?)
219    }
220
221    /// Create an application, reading the response **raw** — for callers that want the
222    /// server's JSON verbatim instead of the decoded [`ApplicationItem`].
223    pub async fn create_raw(&self, body: Value) -> Result<Value, ManagerError> {
224        let cfg = self.cfg.get().await?;
225        let cfg = cfg.as_ref();
226        // Non-idempotent create → no retry on ambiguous failures.
227        with_retry(&self.retry, false, || {
228            post_json_body(cfg, "/api/v2/applications", &body)
229        })
230        .await
231    }
232
233    /// Get an application by id.
234    ///
235    /// Reads the response **raw** and decodes via [`decode_application`], standing in for the
236    /// generated `get_application` — whose typed [`models::Application`] return fails to
237    /// deserialize on every real payload.
238    pub async fn get(&self, id: &str) -> Result<ApplicationItem, ManagerError> {
239        let cfg = self.cfg.get().await?;
240        let cfg = cfg.as_ref();
241        let path = format!(
242            "/api/v2/applications/{}",
243            crate::gen::manager::apis::urlencode(id)
244        );
245        with_retry(&self.retry, true, || async {
246            decode_item_envelope(get_json(cfg, &path).await?)
247        })
248        .await
249    }
250
251    /// Update an application.
252    ///
253    /// Routes the write through the raw JSON path and [`decode_application`], standing in for the
254    /// generated `update_application` — same typed-response decode failure as [`Self::create`].
255    pub async fn update(
256        &self,
257        id: &str,
258        body: models::ApplicationUpdateBody,
259    ) -> Result<ApplicationItem, ManagerError> {
260        let body = serde_json::to_value(&body)
261            .map_err(|e| ManagerError::InvalidArgument(e.to_string()))?;
262        decode_item_envelope(self.update_raw(id, body).await?)
263    }
264
265    /// Update an application, reading the response **raw** (the PUT sibling of
266    /// [`Self::create_raw`]).
267    pub async fn update_raw(&self, id: &str, body: Value) -> Result<Value, ManagerError> {
268        let cfg = self.cfg.get().await?;
269        let cfg = cfg.as_ref();
270        let path = format!(
271            "/api/v2/applications/{}",
272            crate::gen::manager::apis::urlencode(id)
273        );
274        // Non-idempotent write → no retry on ambiguous failures.
275        with_retry(&self.retry, false, || put_json(cfg, &path, &body)).await
276    }
277
278    /// Delete an application.
279    pub async fn delete(&self, id: &str) -> Result<(), ManagerError> {
280        let cfg = self.cfg.get().await?;
281        let cfg = cfg.as_ref();
282        with_retry(&self.retry, false, || api::delete_application(cfg, id))
283            .await
284            .map_err(map_manager_err)?;
285        Ok(())
286    }
287
288    /// Delete several applications by id.
289    pub async fn delete_many(
290        &self,
291        ids: Vec<String>,
292    ) -> Result<models::DefaultV2MessageResponse, ManagerError> {
293        let cfg = self.cfg.get().await?;
294        let cfg = cfg.as_ref();
295        let body = models::DeleteManyApplicationsRequest { ids };
296        with_retry(&self.retry, false, || {
297            api::delete_many_applications(cfg, body.clone())
298        })
299        .await
300        .map_err(map_manager_err)
301    }
302
303    /// List the available application modules.
304    pub async fn list_modules(&self) -> Result<models::ListModulesResponse, ManagerError> {
305        let cfg = self.cfg.get().await?;
306        let cfg = cfg.as_ref();
307        with_retry(&self.retry, true, || api::list_modules(cfg))
308            .await
309            .map_err(map_manager_err)
310    }
311
312    /// Dispatch an application's local automations at a given position. `is_async` runs without
313    /// waiting for the result.
314    pub async fn dispatch(
315        &self,
316        id: &str,
317        position: &str,
318        is_async: bool,
319        body: models::LocalAutomationDispatch,
320    ) -> Result<models::DispatchLocalAutomationsResponse, ManagerError> {
321        let cfg = self.cfg.get().await?;
322        let cfg = cfg.as_ref();
323        with_retry(&self.retry, false, || {
324            manager_api::dispatch_local_automations(cfg, id, position, is_async, Some(body.clone()))
325        })
326        .await
327        .map_err(map_manager_err)
328    }
329
330    /// Clone an application by id.
331    ///
332    /// Routes the write through the raw JSON path and [`decode_application`], standing in for the
333    /// generated `clone_application` — same typed-response decode failure as [`Self::create`].
334    pub async fn clone(&self, id: &str) -> Result<ApplicationItem, ManagerError> {
335        decode_item_envelope(self.clone_raw(id).await?)
336    }
337
338    /// Clone an application by id, reading the response **raw** — for callers that want the
339    /// server's JSON verbatim instead of the decoded [`ApplicationItem`].
340    pub async fn clone_raw(&self, id: &str) -> Result<Value, ManagerError> {
341        let cfg = self.cfg.get().await?;
342        let cfg = cfg.as_ref();
343        let path = format!(
344            "/api/v2/applications/{}/clone",
345            crate::gen::manager::apis::urlencode(id)
346        );
347        // Non-idempotent action → no retry on ambiguous failures.
348        with_retry(&self.retry, false, || post_json(cfg, &path)).await
349    }
350
351    /// Bulk-update several applications in one request.
352    pub async fn bulk_update(
353        &self,
354        body: models::BulkUpdateApplicationsRequest,
355    ) -> Result<models::DefaultV2MessageResponse, ManagerError> {
356        let cfg = self.cfg.get().await?;
357        let cfg = cfg.as_ref();
358        with_retry(&self.retry, false, || {
359            api::bulk_update_applications(cfg, body.clone())
360        })
361        .await
362        .map_err(map_manager_err)
363    }
364
365    /// List the actions available across applications.
366    pub async fn list_app_actions(&self) -> Result<models::ObjectListResponse, ManagerError> {
367        let cfg = self.cfg.get().await?;
368        let cfg = cfg.as_ref();
369        with_retry(&self.retry, true, || {
370            api::list_application_actions(cfg, None, None)
371        })
372        .await
373        .map_err(map_manager_err)
374    }
375
376    /// List application errors.
377    pub async fn list_errors(&self) -> Result<models::ObjectListResponse, ManagerError> {
378        let cfg = self.cfg.get().await?;
379        let cfg = cfg.as_ref();
380        with_retry(&self.retry, true, || api::list_application_errors(cfg))
381            .await
382            .map_err(map_manager_err)
383    }
384}
385
386/// Per-application local automations — `/api/v2/applications/{applicationId}/actions`.
387pub struct LocalAutomationsResource {
388    pub(crate) cfg: SharedCfg<Configuration>,
389    pub(crate) retry: RetryPolicy,
390}
391
392impl LocalAutomationsResource {
393    /// List all of an application's local automations (auto-paginated).
394    pub async fn list_all(
395        &self,
396        application_id: &str,
397    ) -> Result<Vec<models::LocalAutomation>, ManagerError> {
398        let cfg = self.cfg.get().await?;
399        let cfg = cfg.as_ref();
400        collect_all(&self.retry, map_manager_err, |page| async move {
401            let r =
402                local_automation_api::list_local_automations(cfg, application_id, Some(page), None)
403                    .await?;
404            Ok((r.items, r.pagination.pages, r.pagination.current))
405        })
406        .await
407    }
408
409    /// Create a local automation on an application.
410    pub async fn create(
411        &self,
412        application_id: &str,
413        body: models::RestCreateLocalAutomation,
414    ) -> Result<models::LocalAutomationItemResponse, ManagerError> {
415        let cfg = self.cfg.get().await?;
416        let cfg = cfg.as_ref();
417        with_retry(&self.retry, false, || {
418            local_automation_api::create_local_automation(cfg, application_id, body.clone())
419        })
420        .await
421        .map_err(map_manager_err)
422    }
423
424    /// Get a local automation by id.
425    pub async fn get(
426        &self,
427        application_id: &str,
428        id: &str,
429    ) -> Result<models::LocalAutomationItemResponse, ManagerError> {
430        let cfg = self.cfg.get().await?;
431        let cfg = cfg.as_ref();
432        with_retry(&self.retry, true, || {
433            local_automation_api::get_local_automation(cfg, application_id, id)
434        })
435        .await
436        .map_err(map_manager_err)
437    }
438
439    /// Update a local automation.
440    pub async fn update(
441        &self,
442        application_id: &str,
443        id: &str,
444        body: models::RestUpdateLocalAutomation,
445    ) -> Result<models::LocalAutomationItemResponse, ManagerError> {
446        let cfg = self.cfg.get().await?;
447        let cfg = cfg.as_ref();
448        with_retry(&self.retry, false, || {
449            local_automation_api::update_local_automation(cfg, application_id, id, body.clone())
450        })
451        .await
452        .map_err(map_manager_err)
453    }
454
455    /// Delete a local automation.
456    pub async fn delete(&self, application_id: &str, id: &str) -> Result<(), ManagerError> {
457        let cfg = self.cfg.get().await?;
458        let cfg = cfg.as_ref();
459        with_retry(&self.retry, false, || {
460            local_automation_api::delete_local_automation(cfg, application_id, id)
461        })
462        .await
463        .map_err(map_manager_err)?;
464        Ok(())
465    }
466}