ralph_workflow/agents/ccs/
parsing.rs1pub const CCS_PREFIX: &str = "ccs/";
5
6#[must_use]
20pub fn parse_ccs_ref(agent_name: &str) -> Option<&str> {
21 if agent_name == "ccs" {
22 Some("")
23 } else if let Some(alias) = agent_name.strip_prefix(CCS_PREFIX) {
24 Some(alias)
25 } else {
26 None
27 }
28}
29
30#[must_use]
32pub fn is_ccs_ref(agent_name: &str) -> bool {
33 parse_ccs_ref(agent_name).is_some()
34}
35
36fn looks_like_ccs_executable(cmd0: &str) -> bool {
41 Path::new(cmd0)
42 .file_name()
43 .and_then(|n| n.to_str())
44 .is_some_and(|n| n == "ccs" || n == "ccs.exe")
45}
46
47pub(super) fn ccs_profile_from_command(original_cmd: &str) -> Option<String> {
54 let parts = split_command(original_cmd).ok()?;
55 if !parts.first().is_some_and(|p| looks_like_ccs_executable(p)) {
56 return None;
57 }
58 if parts.get(1).is_some_and(|p| p == "api") {
62 parts.get(2).cloned()
63 } else {
64 parts.get(1).cloned()
65 }
66}
67
68fn choose_best_profile_guess<'a>(input: &str, suggestions: &'a [String]) -> Option<&'a str> {
69 if suggestions.is_empty() {
70 return None;
71 }
72 let input_lower = input.to_lowercase();
73 if let Some(exact) = suggestions
74 .iter()
75 .find(|s| s.to_lowercase() == input_lower)
76 .map(std::string::String::as_str)
77 {
78 return Some(exact);
79 }
80 if suggestions.len() == 1 {
81 return Some(suggestions.first()?.as_str());
82 }
83 if let Some(starts) = suggestions
84 .iter()
85 .find(|s| s.to_lowercase().starts_with(&input_lower))
86 .map(std::string::String::as_str)
87 {
88 return Some(starts);
89 }
90 Some(suggestions.first()?.as_str())
91}
92
93pub(super) fn load_ccs_env_vars_with_guess(
94 profile: &str,
95) -> Result<(HashMap<String, String>, Option<String>), CcsEnvVarsError> {
96 match load_ccs_env_vars(profile) {
97 Ok(vars) => Ok((vars, None)),
98 Err(err @ CcsEnvVarsError::ProfileNotFound { .. }) => {
99 let suggestions = find_ccs_profile_suggestions(profile);
100 let Some(best) = choose_best_profile_guess(profile, &suggestions) else {
101 return Err(err);
102 };
103 let vars = load_ccs_env_vars(best)?;
104 Ok((vars, Some(best.to_string())))
105 }
106 Err(err) => Err(err),
107 }
108}
109
110#[cfg(test)]
111mod proptest_parsers {
112 use super::{parse_ccs_ref, CCS_PREFIX};
113 use proptest::prelude::*;
114
115 proptest! {
116 #[test]
118 fn parse_ccs_ref_never_panics(s in ".*") {
119 let _ = parse_ccs_ref(&s);
120 }
121
122 #[test]
124 fn parse_ccs_ref_exact_ccs_returns_empty(s in Just("ccs".to_string())) {
125 prop_assert_eq!(parse_ccs_ref(&s), Some(""));
126 }
127
128 #[test]
130 fn parse_ccs_ref_ccs_slash_alias_returns_alias(alias in "[a-zA-Z][a-zA-Z0-9_-]{0,20}") {
131 let name = format!("{CCS_PREFIX}{alias}");
132 let result = parse_ccs_ref(&name);
133 prop_assert_eq!(result, Some(alias.as_str()));
134 }
135
136 #[test]
138 fn parse_ccs_ref_non_ccs_returns_none(s in "[^c].*|c[^c].*|cc[^s].*") {
139 prop_assert_eq!(parse_ccs_ref(&s), None);
140 }
141 }
142}