1use std::path::Path;
8
9use serde_json::Value;
10
11use crate::client::CellosClient;
12use crate::exit::{CtlError, CtlResult};
13use crate::model::Formation;
14
15pub async fn run(client: &CellosClient, path: &Path) -> CtlResult<()> {
16 let yaml = std::fs::read_to_string(path)
17 .map_err(|e| CtlError::usage(format!("read {}: {e}", path.display())))?;
18
19 let body: Value = serde_yaml::from_str(&yaml)?;
20 validate_formation_spec(&body, path)?;
21
22 let created: Formation = client.post_json("/v1/formations", &body).await?;
23 let name = if !created.name.is_empty() {
24 &created.name
25 } else {
26 &created.id
27 };
28 println!("formation/{name} created (state={})", created.state);
29 Ok(())
30}
31
32fn validate_formation_spec(v: &Value, path: &Path) -> CtlResult<()> {
35 let Some(obj) = v.as_object() else {
36 return Err(CtlError::validation(format!(
37 "{}: top-level must be a YAML mapping",
38 path.display()
39 )));
40 };
41
42 if let Some(kind) = obj.get("kind").and_then(|k| k.as_str()) {
45 if kind != "Formation" {
46 return Err(CtlError::validation(format!(
47 "{}: kind must be \"Formation\" (got \"{kind}\")",
48 path.display()
49 )));
50 }
51 }
52
53 let has_name = obj.get("name").and_then(|n| n.as_str()).is_some()
55 || obj
56 .get("metadata")
57 .and_then(|m| m.get("name"))
58 .and_then(|n| n.as_str())
59 .is_some();
60 if !has_name {
61 return Err(CtlError::validation(format!(
62 "{}: missing required field `name` (or `metadata.name`)",
63 path.display()
64 )));
65 }
66 Ok(())
67}