1use anyhow::{Result, anyhow};
9use serde_json::{Value, json};
10use soma_application::{ExecuteActionRequest, SomaApplication};
11use std::path::PathBuf;
12
13use crate::{Command, cli_execution_context};
14
15#[derive(Debug, PartialEq, Eq)]
16pub enum ProviderCommand {
17 Validate,
18 Inspect,
19 Test {
20 action: String,
21 json: Value,
22 },
23 List {
25 dir: Option<PathBuf>,
26 json: bool,
27 },
28 Lint {
30 dir: Option<PathBuf>,
31 json: bool,
32 },
33 Status {
35 dir: Option<PathBuf>,
36 json: bool,
37 },
38 Graduate {
39 source: PathBuf,
40 workspace: PathBuf,
41 fixtures: Option<PathBuf>,
42 },
43 BuildComponent {
44 workspace: PathBuf,
45 component: Option<PathBuf>,
46 },
47 VerifyComponent {
48 component: PathBuf,
49 },
50 Compare {
51 workspace: PathBuf,
52 component: Option<PathBuf>,
53 fixtures: PathBuf,
54 },
55 GraduationStatus {
56 workspace: PathBuf,
57 },
58 Componentize {
59 operation: String,
60 workspace: PathBuf,
61 wheelhouse: Option<PathBuf>,
62 },
63 Activate {
64 workspace: PathBuf,
65 confirmed: bool,
66 },
67 Rollback {
68 workspace: PathBuf,
69 confirmed: bool,
70 },
71}
72
73impl ProviderCommand {
74 pub fn is_non_executing(&self) -> bool {
78 matches!(
79 self,
80 ProviderCommand::List { .. }
81 | ProviderCommand::Lint { .. }
82 | ProviderCommand::Status { .. }
83 )
84 }
85}
86
87pub(crate) async fn run_provider_management_command(
88 command: &ProviderCommand,
89 application: &SomaApplication,
90 destructive_confirmed: bool,
91) -> Result<Value> {
92 match command {
93 ProviderCommand::Validate => Ok(application.provider_validation_summary()),
94 ProviderCommand::Inspect => Ok(application.provider_inspection_report()),
95 ProviderCommand::Test { action, json } => {
96 let provider = application.provider_for_action(action);
97 match application
98 .execute_action(
99 ExecuteActionRequest {
100 action: action.clone(),
101 params: json.clone(),
102 },
103 cli_execution_context(destructive_confirmed),
104 )
105 .await
106 {
107 Ok(output) => Ok(json!({
108 "schema_version": 1,
109 "ok": true,
110 "action": action,
111 "provider": provider,
112 "result": output.output,
113 "request_id": output.request_id,
114 "progress": output.progress,
115 })),
116 Err(error) => Err(anyhow!(error)),
117 }
118 }
119 ProviderCommand::Graduate {
120 source,
121 workspace,
122 fixtures,
123 } => {
124 graduation_action(
125 application,
126 "graduate",
127 workspace,
128 Some(source),
129 None,
130 fixtures.as_ref(),
131 destructive_confirmed,
132 )
133 .await
134 }
135 ProviderCommand::BuildComponent {
136 workspace,
137 component,
138 } => {
139 graduation_action(
140 application,
141 "build-component",
142 workspace,
143 None,
144 component.as_ref(),
145 None,
146 destructive_confirmed,
147 )
148 .await
149 }
150 ProviderCommand::VerifyComponent { component } => {
151 graduation_action(
152 application,
153 "verify-component",
154 component
155 .parent()
156 .unwrap_or_else(|| std::path::Path::new(".")),
157 None,
158 Some(component),
159 None,
160 destructive_confirmed,
161 )
162 .await
163 }
164 ProviderCommand::Compare {
165 workspace,
166 component,
167 fixtures,
168 } => {
169 graduation_action(
170 application,
171 "compare",
172 workspace,
173 None,
174 component.as_ref(),
175 Some(fixtures),
176 destructive_confirmed,
177 )
178 .await
179 }
180 ProviderCommand::Componentize {
181 operation,
182 workspace,
183 wheelhouse,
184 } => {
185 componentize_action(
186 application,
187 operation,
188 workspace,
189 wheelhouse.as_ref(),
190 destructive_confirmed,
191 )
192 .await
193 }
194 ProviderCommand::GraduationStatus { workspace } => application
195 .execute_action(
196 ExecuteActionRequest {
197 action: "python_graduation_status".to_owned(),
198 params: json!({"workspace": workspace}),
199 },
200 cli_execution_context(destructive_confirmed),
201 )
202 .await
203 .map(|response| response.into_surface_value())
204 .map_err(anyhow::Error::from),
205 ProviderCommand::Activate { workspace, .. } => {
206 graduation_action(
207 application,
208 "activate",
209 workspace,
210 None,
211 None,
212 None,
213 destructive_confirmed,
214 )
215 .await
216 }
217 ProviderCommand::Rollback { workspace, .. } => {
218 graduation_action(
219 application,
220 "rollback",
221 workspace,
222 None,
223 None,
224 None,
225 destructive_confirmed,
226 )
227 .await
228 }
229 ProviderCommand::List { .. }
230 | ProviderCommand::Lint { .. }
231 | ProviderCommand::Status { .. } => {
232 unreachable!("non-executing provider commands are handled before registry construction")
233 }
234 }
235}
236
237async fn componentize_action(
238 application: &SomaApplication,
239 operation: &str,
240 workspace: &std::path::Path,
241 wheelhouse: Option<&std::path::PathBuf>,
242 confirmed: bool,
243) -> Result<Value> {
244 let mut params = json!({
245 "operation": operation,
246 "workspace": workspace,
247 "confirm": confirmed,
248 });
249 if let Some(wheelhouse) = wheelhouse {
250 params
251 .as_object_mut()
252 .expect("object")
253 .insert("wheelhouse".to_owned(), json!(wheelhouse));
254 }
255 application
256 .execute_action(
257 ExecuteActionRequest {
258 action: "python_graduation_apply".to_owned(),
259 params,
260 },
261 cli_execution_context(confirmed),
262 )
263 .await
264 .map(|response| response.into_surface_value())
265 .map_err(anyhow::Error::from)
266}
267
268async fn graduation_action(
269 application: &SomaApplication,
270 operation: &str,
271 workspace: &std::path::Path,
272 source: Option<&std::path::PathBuf>,
273 component: Option<&std::path::PathBuf>,
274 fixtures: Option<&std::path::PathBuf>,
275 confirmed: bool,
276) -> Result<Value> {
277 let mut params = json!({
278 "operation": operation,
279 "workspace": workspace,
280 "confirm": confirmed,
281 });
282 let object = params.as_object_mut().expect("object");
283 if let Some(source) = source {
284 object.insert("source".to_owned(), json!(source));
285 }
286 if let Some(component) = component {
287 object.insert("component".to_owned(), json!(component));
288 }
289 if let Some(fixtures) = fixtures {
290 object.insert("fixtures".to_owned(), json!(fixtures));
291 }
292 application
293 .execute_action(
294 ExecuteActionRequest {
295 action: "python_graduation_apply".to_owned(),
296 params,
297 },
298 cli_execution_context(confirmed),
299 )
300 .await
301 .map(|response| response.into_surface_value())
302 .map_err(anyhow::Error::from)
303}
304
305pub(crate) fn parse_providers_command(args: &[String]) -> Result<Command> {
306 match args {
307 [action] if action == "validate" => Ok(Command::Providers(ProviderCommand::Validate)),
308 [action] if action == "inspect" => Ok(Command::Providers(ProviderCommand::Inspect)),
309 [action, provider_action] if action == "test" => {
310 Ok(Command::Providers(ProviderCommand::Test {
311 action: provider_action.clone(),
312 json: json!({}),
313 }))
314 }
315 [action, provider_action, flag, payload] if action == "test" && flag == "--json" => {
316 Ok(Command::Providers(ProviderCommand::Test {
317 action: provider_action.clone(),
318 json: serde_json::from_str(payload).map_err(|error| {
319 anyhow!("providers test {provider_action} --json must be valid JSON: {error}")
320 })?,
321 }))
322 }
323 [action, rest @ ..] if action == "list" || action == "lint" || action == "status" => {
324 let (dir, json) = parse_providers_dir_flags(action, rest)?;
325 Ok(Command::Providers(match action.as_str() {
326 "list" => ProviderCommand::List { dir, json },
327 "lint" => ProviderCommand::Lint { dir, json },
328 _ => ProviderCommand::Status { dir, json },
329 }))
330 }
331 [action, rest @ ..]
332 if matches!(
333 action.as_str(),
334 "graduate"
335 | "build-component"
336 | "verify-component"
337 | "compare"
338 | "graduation-status"
339 | "componentize-scan"
340 | "componentize-bindings"
341 | "componentize-build"
342 | "componentize-validate"
343 | "activate"
344 | "rollback"
345 ) =>
346 {
347 Ok(Command::Providers(parse_graduation_command(action, rest)?))
348 }
349 [] => Err(anyhow!(
350 "providers requires list, lint, status, validate, inspect, test, graduate, \
351 build-component, verify-component, compare, componentize-scan, componentize-bindings, \
352 componentize-build, componentize-validate, graduation-status, activate, or rollback"
353 )),
354 [unexpected, ..] => Err(anyhow!("providers does not accept argument `{unexpected}`")),
355 }
356}
357
358fn parse_graduation_command(command: &str, args: &[String]) -> Result<ProviderCommand> {
359 let required = |flag: &str| -> Result<PathBuf> {
360 args.windows(2)
361 .find(|pair| pair[0] == flag)
362 .map(|pair| PathBuf::from(&pair[1]))
363 .ok_or_else(|| anyhow!("providers {command} {flag} requires a value"))
364 };
365 let expected = match command {
366 "graduate" => &["--source", "--workspace", "--fixtures"][..],
367 "build-component" => &["--workspace", "--component"][..],
368 "verify-component" => &["--component"][..],
369 "compare" => &["--workspace", "--component", "--fixtures"][..],
370 "graduation-status" => &["--workspace"][..],
371 "componentize-scan" => &["--workspace", "--wheelhouse"][..],
372 "componentize-bindings" | "componentize-build" | "componentize-validate" => {
373 &["--workspace"][..]
374 }
375 "activate" | "rollback" => &["--workspace", "--confirm"][..],
376 _ => unreachable!(),
377 };
378 let required_count = match command {
379 "graduate" | "compare" => 2,
380 "build-component" | "componentize-scan" => 1,
381 _ => expected.len(),
382 };
383 if !args.len().is_multiple_of(2)
384 || args.len() < required_count * 2
385 || args.len() > expected.len() * 2
386 || args
387 .chunks_exact(2)
388 .any(|pair| !expected.contains(&pair[0].as_str()) || pair[1].starts_with("--"))
389 || args
390 .chunks_exact(2)
391 .map(|pair| pair[0].as_str())
392 .collect::<std::collections::BTreeSet<_>>()
393 .len()
394 != args.len() / 2
395 {
396 return Err(anyhow!(
397 "providers {command} expects {}",
398 expected
399 .iter()
400 .map(|flag| format!("{flag} PATH"))
401 .collect::<Vec<_>>()
402 .join(" ")
403 ));
404 }
405 match command {
406 "graduate" => Ok(ProviderCommand::Graduate {
407 source: required("--source")?,
408 workspace: required("--workspace")?,
409 fixtures: args
410 .windows(2)
411 .find(|pair| pair[0] == "--fixtures")
412 .map(|pair| PathBuf::from(&pair[1])),
413 }),
414 "build-component" => Ok(ProviderCommand::BuildComponent {
415 workspace: required("--workspace")?,
416 component: args
417 .windows(2)
418 .find(|pair| pair[0] == "--component")
419 .map(|pair| PathBuf::from(&pair[1])),
420 }),
421 "verify-component" => Ok(ProviderCommand::VerifyComponent {
422 component: required("--component")?,
423 }),
424 "compare" => Ok(ProviderCommand::Compare {
425 workspace: required("--workspace")?,
426 component: args
427 .windows(2)
428 .find(|pair| pair[0] == "--component")
429 .map(|pair| PathBuf::from(&pair[1])),
430 fixtures: required("--fixtures")?,
431 }),
432 "graduation-status" => Ok(ProviderCommand::GraduationStatus {
433 workspace: required("--workspace")?,
434 }),
435 "componentize-scan"
436 | "componentize-bindings"
437 | "componentize-build"
438 | "componentize-validate" => Ok(ProviderCommand::Componentize {
439 operation: command.to_owned(),
440 workspace: required("--workspace")?,
441 wheelhouse: args
442 .windows(2)
443 .find(|pair| pair[0] == "--wheelhouse")
444 .map(|pair| PathBuf::from(&pair[1])),
445 }),
446 "activate" => Ok(ProviderCommand::Activate {
447 workspace: required("--workspace")?,
448 confirmed: args
449 .windows(2)
450 .find(|pair| pair[0] == "--confirm")
451 .is_some_and(|pair| pair[1] == "true"),
452 }),
453 "rollback" => Ok(ProviderCommand::Rollback {
454 workspace: required("--workspace")?,
455 confirmed: args
456 .windows(2)
457 .find(|pair| pair[0] == "--confirm")
458 .is_some_and(|pair| pair[1] == "true"),
459 }),
460 _ => unreachable!(),
461 }
462}
463
464fn parse_providers_dir_flags(command: &str, args: &[String]) -> Result<(Option<PathBuf>, bool)> {
465 let mut dir = None;
466 let mut json = false;
467 let mut index = 0;
468 while index < args.len() {
469 match args[index].as_str() {
470 "--dir" => {
471 let value = args
472 .get(index + 1)
473 .ok_or_else(|| anyhow!("providers {command} --dir requires a value"))?;
474 if value.starts_with("--") {
475 return Err(anyhow!("providers {command} --dir requires a value"));
476 }
477 dir = Some(PathBuf::from(value));
478 index += 2;
479 }
480 "--json" => {
481 json = true;
482 index += 1;
483 }
484 unknown => {
485 return Err(anyhow!(
486 "providers {command} does not accept argument `{unknown}`"
487 ));
488 }
489 }
490 }
491 Ok((dir, json))
492}
493
494#[cfg(test)]
495#[path = "provider_command_tests.rs"]
496mod tests;