Skip to main content

soma_cli/
provider_command.rs

1//! `soma providers validate|inspect|test` — dispatches through the *live,
2//! loaded application provider catalog; executes handlers.
3//!
4//! Distinct from the `providers` module (`soma providers list|lint|status`),
5//! which is non-executing filesystem inspection that never touches the
6//! registry.
7
8use anyhow::{anyhow, Result};
9use serde_json::{json, Value};
10use soma_application::{ExecuteActionRequest, SomaApplication};
11use std::path::PathBuf;
12
13use crate::{cli_execution_context, Command};
14
15#[derive(Debug, PartialEq, Eq)]
16pub enum ProviderCommand {
17    Validate,
18    Inspect,
19    Test {
20        action: String,
21        json: Value,
22    },
23    /// Non-executing: lists drop-in provider files without loading the registry.
24    List {
25        dir: Option<PathBuf>,
26        json: bool,
27    },
28    /// Non-executing: lints drop-in provider files without loading the registry.
29    Lint {
30        dir: Option<PathBuf>,
31        json: bool,
32    },
33    /// Non-executing: summarizes drop-in provider files without loading the registry.
34    Status {
35        dir: Option<PathBuf>,
36        json: bool,
37    },
38}
39
40impl ProviderCommand {
41    /// The three non-executing variants never touch the live registry — they
42    /// only parse manifests on disk, so `run()` short-circuits before any
43    /// client/service/registry construction for these.
44    pub fn is_non_executing(&self) -> bool {
45        matches!(
46            self,
47            ProviderCommand::List { .. }
48                | ProviderCommand::Lint { .. }
49                | ProviderCommand::Status { .. }
50        )
51    }
52}
53
54pub(crate) async fn run_provider_management_command(
55    command: &ProviderCommand,
56    application: &SomaApplication,
57    destructive_confirmed: bool,
58) -> Result<Value> {
59    match command {
60        ProviderCommand::Validate => Ok(application.provider_validation_summary()),
61        ProviderCommand::Inspect => Ok(application.provider_inspection_report()),
62        ProviderCommand::Test { action, json } => {
63            let provider = application.provider_for_action(action);
64            match application
65                .execute_action(
66                    ExecuteActionRequest {
67                        action: action.clone(),
68                        params: json.clone(),
69                    },
70                    cli_execution_context(destructive_confirmed),
71                )
72                .await
73            {
74                Ok(output) => Ok(json!({
75                    "schema_version": 1,
76                    "ok": true,
77                    "action": action,
78                    "provider": provider,
79                    "result": output.output
80                })),
81                Err(error) => Err(anyhow!(error)),
82            }
83        }
84        ProviderCommand::List { .. }
85        | ProviderCommand::Lint { .. }
86        | ProviderCommand::Status { .. } => {
87            unreachable!("non-executing provider commands are handled before registry construction")
88        }
89    }
90}
91
92pub(crate) fn parse_providers_command(args: &[String]) -> Result<Command> {
93    match args {
94        [action] if action == "validate" => Ok(Command::Providers(ProviderCommand::Validate)),
95        [action] if action == "inspect" => Ok(Command::Providers(ProviderCommand::Inspect)),
96        [action, provider_action] if action == "test" => {
97            Ok(Command::Providers(ProviderCommand::Test {
98                action: provider_action.clone(),
99                json: json!({}),
100            }))
101        }
102        [action, provider_action, flag, payload] if action == "test" && flag == "--json" => {
103            Ok(Command::Providers(ProviderCommand::Test {
104                action: provider_action.clone(),
105                json: serde_json::from_str(payload).map_err(|error| {
106                    anyhow!("providers test {provider_action} --json must be valid JSON: {error}")
107                })?,
108            }))
109        }
110        [action, rest @ ..] if action == "list" || action == "lint" || action == "status" => {
111            let (dir, json) = parse_providers_dir_flags(action, rest)?;
112            Ok(Command::Providers(match action.as_str() {
113                "list" => ProviderCommand::List { dir, json },
114                "lint" => ProviderCommand::Lint { dir, json },
115                _ => ProviderCommand::Status { dir, json },
116            }))
117        }
118        [] => Err(anyhow!(
119            "providers requires list, lint, status, validate, inspect, or test ACTION"
120        )),
121        [unexpected, ..] => Err(anyhow!("providers does not accept argument `{unexpected}`")),
122    }
123}
124
125fn parse_providers_dir_flags(command: &str, args: &[String]) -> Result<(Option<PathBuf>, bool)> {
126    let mut dir = None;
127    let mut json = false;
128    let mut index = 0;
129    while index < args.len() {
130        match args[index].as_str() {
131            "--dir" => {
132                let value = args
133                    .get(index + 1)
134                    .ok_or_else(|| anyhow!("providers {command} --dir requires a value"))?;
135                if value.starts_with("--") {
136                    return Err(anyhow!("providers {command} --dir requires a value"));
137                }
138                dir = Some(PathBuf::from(value));
139                index += 2;
140            }
141            "--json" => {
142                json = true;
143                index += 1;
144            }
145            unknown => {
146                return Err(anyhow!(
147                    "providers {command} does not accept argument `{unknown}`"
148                ))
149            }
150        }
151    }
152    Ok((dir, json))
153}
154
155#[cfg(test)]
156#[path = "provider_command_tests.rs"]
157mod tests;