Skip to main content

cellos_ctl/
model.rs

1//! Wire-level types for the cellos-server HTTP API.
2//!
3//! These mirror the projector's response shape. cellctl is a thin client — these
4//! types exist only to deserialize responses and re-serialize for `--output json`.
5//! No client-side derivations, no state caching.
6//!
7//! Fields are flexible (`#[serde(default)]`) so that adding fields server-side
8//! does not break older clients. This is the same compatibility contract kubectl
9//! has with the kube-apiserver.
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14/// Cell view returned by `GET /v1/cells` / `GET /v1/cells/<id>`.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Cell {
17    #[serde(default)]
18    pub id: String,
19    #[serde(default)]
20    pub name: String,
21    #[serde(default)]
22    pub formation_id: Option<String>,
23    /// PENDING | ADMITTED | RUNNING | COMPLETED | FAILED | KILLED
24    #[serde(default)]
25    pub state: String,
26    #[serde(default)]
27    pub image: Option<String>,
28    #[serde(default)]
29    pub critical: Option<bool>,
30    #[serde(default)]
31    pub created_at: Option<String>,
32    #[serde(default)]
33    pub started_at: Option<String>,
34    #[serde(default)]
35    pub finished_at: Option<String>,
36    /// Most-recent CloudEvent outcome, if known.
37    #[serde(default)]
38    pub outcome: Option<String>,
39    /// Arbitrary extra fields the server may attach.
40    #[serde(flatten, default)]
41    pub extra: serde_json::Map<String, Value>,
42}
43
44/// Formation view returned by `GET /v1/formations` / `GET /v1/formations/<id>`.
45///
46/// Wire contract drift note (red-team wave 2, HIGH-W2D-2): the server's
47/// [`cellos_server::state::FormationRecord`] serialises its lifecycle
48/// field as `status` (UPPERCASE enum: `PENDING` / `RUNNING` / `SUCCEEDED`
49/// / `FAILED` / `CANCELLED`), while older clients and the table-render
50/// code in `output.rs` expect `state` (PascalCase string:
51/// `PENDING` / `LAUNCHING` / …). The `state` field below carries
52/// `#[serde(alias = "status")]` so cellctl deserialises both shapes
53/// without a wire break — until cellos-server is renamed to expose
54/// `state` directly, this alias is load-bearing.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Formation {
57    #[serde(default)]
58    pub id: String,
59    #[serde(default)]
60    pub name: String,
61    /// PENDING | LAUNCHING | RUNNING | DEGRADED | COMPLETED | FAILED
62    ///
63    /// Accepts both `state` (forward-looking canonical) and `status` (the
64    /// shape cellos-server emits today) on the wire.
65    #[serde(default, alias = "status")]
66    pub state: String,
67    #[serde(default)]
68    pub tenant: Option<String>,
69    #[serde(default)]
70    pub cells: Vec<Cell>,
71    #[serde(default)]
72    pub created_at: Option<String>,
73    #[serde(default)]
74    pub updated_at: Option<String>,
75    #[serde(flatten, default)]
76    pub extra: serde_json::Map<String, Value>,
77}
78
79/// CloudEvent envelope — projector emits these on the stream and on
80/// `GET /v1/cells/<id>/events` plus the `/ws/events` socket.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct CloudEvent {
83    #[serde(default, alias = "specversion")]
84    pub spec_version: Option<String>,
85    #[serde(default)]
86    pub id: Option<String>,
87    #[serde(default)]
88    pub source: Option<String>,
89    #[serde(default, alias = "type")]
90    pub event_type: Option<String>,
91    #[serde(default)]
92    pub time: Option<String>,
93    #[serde(default)]
94    pub subject: Option<String>,
95    #[serde(default)]
96    pub data: Option<Value>,
97    #[serde(flatten, default)]
98    pub extra: serde_json::Map<String, Value>,
99}
100
101/// Server response envelope for list endpoints. Some endpoints return a bare
102/// array; others wrap in `{"items": [...]}`. `List<T>` accepts either via custom deserialize.
103#[derive(Debug, Clone, Serialize)]
104pub struct List<T> {
105    pub items: Vec<T>,
106}
107
108impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for List<T> {
109    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
110        #[derive(Deserialize)]
111        #[serde(untagged)]
112        enum Wire<T> {
113            Bare(Vec<T>),
114            Wrapped { items: Vec<T> },
115        }
116        Ok(match Wire::<T>::deserialize(d)? {
117            Wire::Bare(items) => List { items },
118            Wire::Wrapped { items } => List { items },
119        })
120    }
121}