diff --git a/.changeset/feat-unlisted-api-colon-syntax.md b/.changeset/feat-unlisted-api-colon-syntax.md new file mode 100644 index 00000000..189b8b94 --- /dev/null +++ b/.changeset/feat-unlisted-api-colon-syntax.md @@ -0,0 +1,10 @@ +--- +"@googleworkspace/cli": minor +--- + +feat: implement : syntax for unlisted Discovery APIs + +`gws admob:v1 ` now fetches the Discovery Document +directly from the Discovery Service without requiring a registry entry. +Previously the colon syntax only overrode the version for already-registered +services; unlisted API names were rejected regardless. diff --git a/crates/google-workspace-cli/src/main.rs b/crates/google-workspace-cli/src/main.rs index 41dcc1e1..e00b5f48 100644 --- a/crates/google-workspace-cli/src/main.rs +++ b/crates/google-workspace-cli/src/main.rs @@ -324,6 +324,9 @@ pub fn parse_service_and_version( ) -> Result<(String, String), GwsError> { let mut service_arg = first_arg; let mut version_override: Option = None; + // Tracks whether the user explicitly provided an `:` pair, + // which enables the unlisted-API bypass below. + let mut explicit_version_from_colon = false; // Check for --api-version flag anywhere in args for i in 0..args.len() { @@ -332,17 +335,34 @@ pub fn parse_service_and_version( } } - // Support "service:version" syntax on the service arg itself + // Support "service:version" syntax on the service arg itself. + // Always mark the colon as present so the unlisted-API bypass fires even + // when --api-version was also supplied. if let Some((svc, ver)) = service_arg.split_once(':') { service_arg = svc; + explicit_version_from_colon = true; if version_override.is_none() { version_override = Some(ver.to_string()); } } - let (api_name, default_version) = services::resolve_service(service_arg)?; - let version = version_override.unwrap_or(default_version); - Ok((api_name, version)) + // Try the known-service registry first. If the service isn't registered + // but the caller provided an explicit version via `:`, bypass + // the registry and pass the names directly to the Discovery fetch — the + // Discovery Document URL validation in `fetch_discovery_document` will + // reject invalid identifiers. + match services::resolve_service(service_arg) { + Ok((api_name, default_version)) => { + let version = version_override.unwrap_or(default_version); + Ok((api_name, version)) + } + Err(_) if explicit_version_from_colon => { + // Unlisted API: version comes from the colon syntax or --api-version flag. + let version = version_override.expect("set above when colon was found"); + Ok((service_arg.to_string(), version)) + } + Err(e) => Err(e), + } } pub fn filter_args_for_subcommand(args: &[String], service_name: &str) -> Vec { @@ -525,6 +545,64 @@ fn is_version_flag(arg: &str) -> bool { mod tests { use super::*; + #[test] + fn test_parse_service_and_version_known_service() { + let args: Vec = vec!["gws".into(), "drive".into()]; + let (api, ver) = parse_service_and_version(&args, "drive").unwrap(); + assert_eq!(api, "drive"); + assert_eq!(ver, "v3"); + } + + #[test] + fn test_parse_service_and_version_known_service_colon_override() { + // Colon syntax overrides the default version for a known service. + let args: Vec = vec!["gws".into(), "drive:v2".into()]; + let (api, ver) = parse_service_and_version(&args, "drive:v2").unwrap(); + assert_eq!(api, "drive"); + assert_eq!(ver, "v2"); + } + + #[test] + fn test_parse_service_and_version_unlisted_api_colon_syntax() { + // An unlisted API with explicit version bypasses the registry. + let args: Vec = vec!["gws".into(), "admob:v1".into()]; + let (api, ver) = parse_service_and_version(&args, "admob:v1").unwrap(); + assert_eq!(api, "admob"); + assert_eq!(ver, "v1"); + } + + #[test] + fn test_parse_service_and_version_unlisted_api_no_version_errors() { + // Without a version, an unknown service must still return an error. + let args: Vec = vec!["gws".into(), "admob".into()]; + let err = parse_service_and_version(&args, "admob"); + assert!(err.is_err()); + let msg = err.unwrap_err().to_string(); + assert!(msg.contains("Unknown service")); + assert!(msg.contains(":")); + } + + #[test] + fn test_parse_service_and_version_api_version_flag_unlisted() { + // --api-version flag alone does NOT bypass the registry for unknown services. + let args: Vec = vec!["gws".into(), "admob".into(), "--api-version".into(), "v1".into()]; + let err = parse_service_and_version(&args, "admob"); + assert!(err.is_err()); + } + + #[test] + fn test_parse_service_and_version_colon_plus_api_version_flag_unlisted() { + // : colon syntax bypasses registry even when --api-version is + // also present; --api-version takes precedence for the version value. + let args: Vec = vec![ + "gws".into(), "admob:v1".into(), + "--api-version".into(), "v2".into(), + ]; + let (api, ver) = parse_service_and_version(&args, "admob:v1").unwrap(); + assert_eq!(api, "admob"); + assert_eq!(ver, "v2"); // --api-version wins + } + #[test] fn test_parse_pagination_config_defaults() { let matches = clap::Command::new("test")