1use clap::{Args, Subcommand};
2use schemars::JsonSchema;
3use serde::Serialize;
4use serde_json::{json, Value};
5
6use crate::client::{ControlPlaneClient, Endpoint};
7use crate::CliResult;
8
9#[derive(Subcommand, Debug, Serialize, JsonSchema)]
10#[serde(rename_all = "kebab-case")]
11pub enum ConsensusCommand {
12 #[command(subcommand)]
14 Group(ConsensusGroupCommand),
15 #[command(subcommand)]
17 Binding(ConsensusBindingCommand),
18 Propose(ConsensusProposeArgs),
20 Read(ConsensusReadArgs),
22 #[command(subcommand)]
24 Engine(ConsensusEngineCommand),
25}
26
27#[derive(Subcommand, Debug, Serialize, JsonSchema)]
28#[serde(rename_all = "kebab-case")]
29pub enum ConsensusGroupCommand {
30 Create(ConsensusGroupCreateArgs),
32 Attach(ConsensusGroupAttachArgs),
34 List(ConsensusGroupListArgs),
36 Detach(ConsensusGroupDetachArgs),
38 Delete(ConsensusGroupDeleteArgs),
40 Members(ConsensusGroupMembersArgs),
42 Status(ConsensusGroupStatusArgs),
44}
45
46#[derive(Subcommand, Debug, Serialize, JsonSchema)]
47#[serde(rename_all = "kebab-case")]
48pub enum ConsensusEngineCommand {
49 Status(ConsensusEngineStatusArgs),
51 Config(ConsensusEngineConfigArgs),
53}
54
55#[derive(Subcommand, Debug, Serialize, JsonSchema)]
56#[serde(rename_all = "kebab-case")]
57pub enum ConsensusBindingCommand {
58 List(ConsensusBindingListArgs),
60}
61
62#[derive(Args, Debug, Serialize, JsonSchema)]
63#[serde(rename_all = "kebab-case")]
64pub struct ConsensusGroupCreateArgs {
65 #[arg(long)]
67 pub name: String,
68 #[arg(long = "member")]
70 pub members: Vec<String>,
71 #[arg(long)]
73 pub heartbeat_interval_ms: Option<u64>,
74 #[arg(long)]
76 pub election_timeout_min_ms: Option<u64>,
77 #[arg(long)]
79 pub election_timeout_max_ms: Option<u64>,
80 #[arg(long)]
82 pub snapshot_interval: Option<u64>,
83 #[arg(long)]
85 pub json: bool,
86}
87
88#[derive(Args, Debug, Serialize, JsonSchema)]
89#[serde(rename_all = "kebab-case")]
90pub struct ConsensusGroupAttachArgs {
91 #[arg(long)]
93 pub group: String,
94 #[arg(long)]
96 pub path: String,
97 #[arg(long)]
99 pub json: bool,
100}
101
102#[derive(Args, Debug, Serialize, JsonSchema)]
103#[serde(rename_all = "kebab-case")]
104pub struct ConsensusGroupListArgs {
105 #[arg(long)]
107 pub json: bool,
108}
109
110#[derive(Args, Debug, Serialize, JsonSchema)]
111#[serde(rename_all = "kebab-case")]
112pub struct ConsensusGroupDetachArgs {
113 #[arg(long)]
115 pub group: String,
116 #[arg(long)]
118 pub path: String,
119 #[arg(long)]
121 pub json: bool,
122}
123
124#[derive(Args, Debug, Serialize, JsonSchema)]
125#[serde(rename_all = "kebab-case")]
126pub struct ConsensusGroupDeleteArgs {
127 #[arg(long)]
129 pub name: String,
130 #[arg(long)]
132 pub json: bool,
133}
134
135#[derive(Args, Debug, Serialize, JsonSchema)]
136#[serde(rename_all = "kebab-case")]
137pub struct ConsensusGroupMembersArgs {
138 #[arg(long)]
140 pub group: String,
141 #[arg(long)]
143 pub json: bool,
144}
145
146#[derive(Args, Debug, Serialize, JsonSchema)]
147#[serde(rename_all = "kebab-case")]
148pub struct ConsensusGroupStatusArgs {
149 #[arg(long)]
151 pub group: String,
152 #[arg(long)]
154 pub json: bool,
155}
156
157#[derive(Args, Debug, Serialize, JsonSchema)]
158#[serde(rename_all = "kebab-case")]
159pub struct ConsensusProposeArgs {
160 #[arg(long)]
162 pub group: String,
163 pub payload: String,
165 #[arg(long)]
167 pub json: bool,
168}
169
170#[derive(Args, Debug, Serialize, JsonSchema)]
171#[serde(rename_all = "kebab-case")]
172pub struct ConsensusReadArgs {
173 #[arg(long)]
175 pub group: String,
176 pub query: String,
178 #[arg(long)]
180 pub json: bool,
181}
182
183#[derive(Args, Debug, Serialize, JsonSchema)]
184#[serde(rename_all = "kebab-case")]
185pub struct ConsensusEngineStatusArgs {
186 #[arg(long)]
188 pub json: bool,
189}
190
191#[derive(Args, Debug, Serialize, JsonSchema)]
192#[serde(rename_all = "kebab-case")]
193pub struct ConsensusEngineConfigArgs {
194 #[arg(long)]
196 pub json: bool,
197}
198
199#[derive(Args, Debug, Serialize, JsonSchema)]
200#[serde(rename_all = "kebab-case")]
201pub struct ConsensusBindingListArgs {
202 #[arg(long)]
204 pub json: bool,
205}
206
207fn print_pretty_json(value: &Value) -> CliResult {
208 println!("{}", serde_json::to_string_pretty(value)?);
209 Ok(())
210}
211
212fn value_str<'a>(value: &'a Value, key: &str, default: &'a str) -> &'a str {
213 value.get(key).and_then(|v| v.as_str()).unwrap_or(default)
214}
215
216fn value_u64(value: &Value, key: &str) -> u64 {
217 value.get(key).and_then(|v| v.as_u64()).unwrap_or(0)
218}
219
220fn string_array_joined(value: &Value, key: &str) -> String {
221 value
222 .get(key)
223 .and_then(|v| v.as_array())
224 .map(|list| {
225 list.iter()
226 .filter_map(|v| v.as_str())
227 .collect::<Vec<_>>()
228 .join(", ")
229 })
230 .unwrap_or_default()
231}
232
233fn render_group_actor_rows(rows: &[Value], empty_msg: &str) {
234 if rows.is_empty() {
235 println!("{empty_msg}");
236 return;
237 }
238 for entry in rows {
239 let group = value_str(entry, "group", "unknown");
240 let actors = string_array_joined(entry, "actors");
241 if actors.is_empty() {
242 println!("{group}: (no actors)");
243 } else {
244 println!("{group}: {actors}");
245 }
246 }
247}
248
249pub fn run(cmd: ConsensusCommand, endpoint: &Endpoint) -> CliResult {
250 let mut client = ControlPlaneClient::connect_endpoint(endpoint)?;
251 match cmd {
252 ConsensusCommand::Group(group_cmd) => match group_cmd {
253 ConsensusGroupCommand::Create(args) => {
254 let mut params = serde_json::Map::new();
255 params.insert("name".into(), Value::String(args.name.clone()));
256 if !args.members.is_empty() {
257 params.insert(
258 "members".into(),
259 Value::Array(args.members.iter().cloned().map(Value::String).collect()),
260 );
261 }
262 if let Some(v) = args.heartbeat_interval_ms {
263 params.insert("heartbeat_interval_ms".into(), Value::Number(v.into()));
264 }
265 if let Some(v) = args.election_timeout_min_ms {
266 params.insert("election_timeout_min_ms".into(), Value::Number(v.into()));
267 }
268 if let Some(v) = args.election_timeout_max_ms {
269 params.insert("election_timeout_max_ms".into(), Value::Number(v.into()));
270 }
271 if let Some(v) = args.snapshot_interval {
272 params.insert("snapshot_interval".into(), Value::Number(v.into()));
273 }
274 let result = client.call("consensus.group.create", Value::Object(params))?;
275 if args.json {
276 print_pretty_json(&result)?;
277 } else {
278 println!("Consensus group {} created.", args.name);
279 }
280 }
281 ConsensusGroupCommand::Attach(args) => {
282 let params = json!({
283 "group": args.group,
284 "path": args.path,
285 });
286 let result = client.call("consensus.group.attach", params)?;
287 if args.json {
288 print_pretty_json(&result)?;
289 } else {
290 let group = value_str(&result, "group", "unknown");
291 let path = value_str(&result, "path", "unknown");
292 println!("Attached {path} to consensus group {group}.");
293 }
294 }
295 ConsensusGroupCommand::List(args) => {
296 let result = client.call("consensus.group.list", Value::Null)?;
297 let groups = result.as_array().cloned().unwrap_or_default();
298 if args.json {
299 print_pretty_json(&Value::Array(groups))?;
300 } else {
301 render_group_actor_rows(&groups, "No consensus groups.");
302 }
303 }
304 ConsensusGroupCommand::Detach(args) => {
305 let params = json!({
306 "group": args.group,
307 "path": args.path,
308 });
309 let result = client.call("consensus.group.detach", params)?;
310 if args.json {
311 print_pretty_json(&result)?;
312 } else {
313 let group = value_str(&result, "group", "unknown");
314 let path = value_str(&result, "path", "unknown");
315 println!("Detached {path} from consensus group {group}.");
316 }
317 }
318 ConsensusGroupCommand::Delete(args) => {
319 let result = client.call("consensus.group.delete", json!({"name": args.name}))?;
320 if args.json {
321 print_pretty_json(&result)?;
322 } else {
323 println!("Consensus group {} deleted.", args.name);
324 }
325 }
326 ConsensusGroupCommand::Members(args) => {
327 let result =
328 client.call("consensus.group.members", json!({"group": args.group}))?;
329 if args.json {
330 print_pretty_json(&result)?;
331 } else {
332 let members = string_array_joined(&result, "members");
333 println!(
334 "Members: {}",
335 if members.is_empty() {
336 "(none)".to_string()
337 } else {
338 members
339 }
340 );
341 }
342 }
343 ConsensusGroupCommand::Status(args) => {
344 let result = client.call("consensus.group.status", json!({"group": args.group}))?;
345 if args.json {
346 print_pretty_json(&result)?;
347 } else {
348 let member_count = value_u64(&result, "member_count");
349 let attached = value_u64(&result, "attached_actor_count");
350 println!("Members: {member_count}, attached actors: {attached}");
351 }
352 }
353 },
354 ConsensusCommand::Binding(binding_cmd) => match binding_cmd {
355 ConsensusBindingCommand::List(args) => {
356 let result = client.call("consensus.binding.list", Value::Null)?;
357 let bindings = result.as_array().cloned().unwrap_or_default();
358 if args.json {
359 print_pretty_json(&Value::Array(bindings))?;
360 } else {
361 render_group_actor_rows(&bindings, "No consensus bindings.");
362 }
363 }
364 },
365 ConsensusCommand::Propose(args) => {
366 let payload: Value = serde_json::from_str(&args.payload)?;
367 let result = client.call(
368 "consensus.propose",
369 json!({"group": args.group, "payload": payload}),
370 )?;
371 print_pretty_json(&result)?;
372 }
373 ConsensusCommand::Read(args) => {
374 let query: Value = serde_json::from_str(&args.query)?;
375 let result = client.call(
376 "consensus.read",
377 json!({"group": args.group, "query": query}),
378 )?;
379 print_pretty_json(&result)?;
380 }
381 ConsensusCommand::Engine(cmd) => match cmd {
382 ConsensusEngineCommand::Status(args) => {
383 let result = client.call("consensus.engine.status", Value::Null)?;
384 if args.json {
385 print_pretty_json(&result)?;
386 } else {
387 let kind = value_str(&result, "kind", "unknown");
388 let engine_id = value_str(&result, "engine_id", "unknown");
389 println!("Consensus engine: {kind} ({engine_id})");
390 }
391 }
392 ConsensusEngineCommand::Config(args) => {
393 let result = client.call("consensus.engine.config", Value::Null)?;
394 if args.json {
395 print_pretty_json(&result)?;
396 } else {
397 let kind = value_str(&result, "kind", "unknown");
398 let tcp = value_str(&result, "tcp_addr", "unknown");
399 let quic = value_str(&result, "quic_addr", "-");
400 let prefer_quic = result
401 .get("prefer_quic")
402 .and_then(|v| v.as_bool())
403 .unwrap_or(false);
404 let tls_enabled = result
405 .get("tls_enabled")
406 .and_then(|v| v.as_bool())
407 .unwrap_or(false);
408 println!("Consensus engine: {kind}");
409 println!("tcp: {tcp}");
410 println!("quic: {quic}");
411 println!("prefer_quic: {prefer_quic}");
412 println!("tls_enabled: {tls_enabled}");
413 }
414 }
415 },
416 }
417 Ok(())
418}