#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedModelSpec {
pub provider: Option<String>,
pub model: String,
}
pub fn parse_model_spec(raw: &str) -> Result<Option<ParsedModelSpec>, String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Ok(None);
}
match trimmed.split_once(':') {
Some((p, m)) => {
let (p, m) = (p.trim(), m.trim());
if p.is_empty() || m.is_empty() {
return Err(format!("'{trimmed}' must be 'provider:model'"));
}
Ok(Some(ParsedModelSpec {
provider: Some(p.to_string()),
model: m.to_string(),
}))
}
None => Ok(Some(ParsedModelSpec {
provider: None,
model: trimmed.to_string(),
})),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blank_is_none() {
assert_eq!(parse_model_spec("").unwrap(), None);
assert_eq!(parse_model_spec(" ").unwrap(), None);
}
#[test]
fn colon_form_splits_both_halves() {
assert_eq!(
parse_model_spec("openai:gpt-4o").unwrap(),
Some(ParsedModelSpec {
provider: Some("openai".into()),
model: "gpt-4o".into(),
})
);
}
#[test]
fn bare_model_has_no_provider() {
assert_eq!(
parse_model_spec("gpt-4o").unwrap(),
Some(ParsedModelSpec {
provider: None,
model: "gpt-4o".into(),
})
);
}
#[test]
fn surrounding_and_inner_whitespace_is_trimmed() {
assert_eq!(
parse_model_spec(" openai : gpt-4o ").unwrap(),
Some(ParsedModelSpec {
provider: Some("openai".into()),
model: "gpt-4o".into(),
})
);
assert_eq!(
parse_model_spec(" gpt-4o ").unwrap(),
Some(ParsedModelSpec {
provider: None,
model: "gpt-4o".into(),
})
);
}
#[test]
fn empty_provider_half_errors() {
assert!(parse_model_spec(":gpt-4o").is_err());
}
#[test]
fn empty_model_half_errors() {
assert!(parse_model_spec("openai:").is_err());
}
}