use meerkat_core::connection::{AuthBindingRef, BindingId, IdentityError, ProfileId, RealmId};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum CliError {
#[error("--auth-binding requires `realm:binding[:profile]`; got `{raw}` with no `:` separator")]
MissingBinding { raw: String },
#[error(
"--auth-binding takes at most three components (`realm:binding[:profile]`); got `{raw}` with {extra_segments} extra segment(s)"
)]
TooManySegments { raw: String, extra_segments: usize },
#[error("--auth-binding realm component `{component}` is not a valid slug: {source}")]
InvalidRealm {
component: String,
#[source]
source: IdentityError,
},
#[error("--auth-binding binding component `{component}` is not a valid slug: {source}")]
InvalidBinding {
component: String,
#[source]
source: IdentityError,
},
#[error("--auth-binding profile component `{component}` is not a valid slug: {source}")]
InvalidProfile {
component: String,
#[source]
source: IdentityError,
},
}
pub fn parse_auth_binding_user_input(raw: &str) -> Result<AuthBindingRef, CliError> {
let trimmed = raw.trim();
let mut parts = trimmed.splitn(4, ':');
let realm_str = parts
.next()
.expect("splitn always yields at least one element");
let Some(binding_str) = parts.next() else {
return Err(CliError::MissingBinding {
raw: raw.to_owned(),
});
};
let profile_str = parts.next();
if let Some(extra) = parts.next() {
let extra_segments = extra.matches(':').count() + 1;
return Err(CliError::TooManySegments {
raw: raw.to_owned(),
extra_segments,
});
}
let realm = RealmId::parse(realm_str).map_err(|source| CliError::InvalidRealm {
component: realm_str.to_owned(),
source,
})?;
let binding = BindingId::parse(binding_str).map_err(|source| CliError::InvalidBinding {
component: binding_str.to_owned(),
source,
})?;
let profile = match profile_str {
None => None,
Some(p) => Some(
ProfileId::parse(p).map_err(|source| CliError::InvalidProfile {
component: p.to_owned(),
source,
})?,
),
};
Ok(AuthBindingRef {
realm,
binding,
profile,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_realm_and_binding() {
let cref = parse_auth_binding_user_input("dev:openai").expect("valid realm:binding");
assert_eq!(cref.realm.as_str(), "dev");
assert_eq!(cref.binding.as_str(), "openai");
assert!(cref.profile.is_none());
}
#[test]
fn parses_realm_binding_profile() {
let cref = parse_auth_binding_user_input("prod:anthropic:paid")
.expect("valid three-component form");
assert_eq!(cref.realm.as_str(), "prod");
assert_eq!(cref.binding.as_str(), "anthropic");
assert_eq!(cref.profile.as_ref().map(|p| p.as_str()), Some("paid"));
}
#[test]
fn trims_surrounding_whitespace() {
let cref = parse_auth_binding_user_input(" dev:openai ").expect("whitespace is trimmed");
assert_eq!(cref.realm.as_str(), "dev");
assert_eq!(cref.binding.as_str(), "openai");
}
#[test]
fn rejects_missing_binding() {
let err = parse_auth_binding_user_input("onlyrealm").expect_err("no colon");
assert!(matches!(err, CliError::MissingBinding { .. }));
}
#[test]
fn rejects_four_segments() {
let err = parse_auth_binding_user_input("a:b:c:d").expect_err("fourth segment is rejected");
assert!(matches!(err, CliError::TooManySegments { .. }));
}
#[test]
fn rejects_invalid_realm_character() {
let err =
parse_auth_binding_user_input("dev$:openai").expect_err("`$` is not a valid slug char");
assert!(matches!(err, CliError::InvalidRealm { .. }));
}
#[test]
fn rejects_invalid_binding_character() {
let err = parse_auth_binding_user_input("dev:open ai")
.expect_err("space is not a valid slug char");
assert!(matches!(err, CliError::InvalidBinding { .. }));
}
#[test]
fn rejects_invalid_profile_character() {
let err = parse_auth_binding_user_input("dev:openai:pa!d")
.expect_err("`!` is not a valid slug char");
assert!(matches!(err, CliError::InvalidProfile { .. }));
}
#[test]
fn rejects_empty_realm() {
let err = parse_auth_binding_user_input(":openai").expect_err("empty realm");
assert!(matches!(err, CliError::InvalidRealm { .. }));
}
#[test]
fn rejects_empty_binding() {
let err = parse_auth_binding_user_input("dev:").expect_err("empty binding");
assert!(matches!(err, CliError::InvalidBinding { .. }));
}
}