| 1 | //! String to enum mapping for values that arrive as text from a command |
| 2 | //! line or a tool call. |
| 3 | //! |
| 4 | //! Each mapper's error lists the accepted values. That matters more for |
| 5 | //! the MCP surface than the CLI one: clap rejects a bad value before the |
| 6 | //! program runs, but a model that guessed wrong only learns from the |
| 7 | //! error text, and a bare "invalid value" costs it another round trip. |
| 8 | |
| 9 | use smbcloud_ascapi_aso::app_screenshot_set::ScreenshotDisplayType; |
| 10 | use smbcloud_ascapi_aso::app_store_version::Platform; |
| 11 | use smbcloud_ascapi_aso::bundle_id::BundleIdPlatform; |
| 12 | |
| 13 | pub fn platform_from_str(value: &str) -> Result<Platform, String> { |
| 14 | match value.to_ascii_lowercase().as_str() { |
| 15 | "ios" => Ok(Platform::Ios), |
| 16 | "mac_os" | "macos" => Ok(Platform::MacOs), |
| 17 | "tv_os" | "tvos" => Ok(Platform::TvOs), |
| 18 | "vision_os" | "visionos" => Ok(Platform::VisionOs), |
| 19 | other => Err(format!( |
| 20 | "unknown platform {other:?}; expected one of ios, mac_os, tv_os, vision_os" |
| 21 | )), |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | pub fn bundle_id_platform_from_str(value: &str) -> Result<BundleIdPlatform, String> { |
| 26 | match value.to_ascii_lowercase().as_str() { |
| 27 | // visionOS shares the iOS identifier namespace, so there is no |
| 28 | // separate value to offer here. |
| 29 | "ios" => Ok(BundleIdPlatform::Ios), |
| 30 | "mac_os" | "macos" => Ok(BundleIdPlatform::MacOs), |
| 31 | "universal" => Ok(BundleIdPlatform::Universal), |
| 32 | // Accepted so a round trip through this mapper does not lose a |
| 33 | // value the API itself returns. Registering one is a different |
| 34 | // flow than an app bundle ID, and Apple rejects it here. |
| 35 | "services" => Ok(BundleIdPlatform::Services), |
| 36 | other => Err(format!( |
| 37 | "unknown bundle ID platform {other:?}; expected one of ios, mac_os, universal" |
| 38 | )), |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | /// App Store Connect's display types are already screaming snake case, so |
| 43 | /// this accepts them verbatim and only normalizes the casing. |
| 44 | pub fn display_type_from_str(value: &str) -> Result<ScreenshotDisplayType, String> { |
| 45 | let upper = value.to_ascii_uppercase(); |
| 46 | serde_json::from_value::<ScreenshotDisplayType>(serde_json::Value::String(upper.clone())) |
| 47 | .map_err(|_| { |
| 48 | format!( |
| 49 | "unknown screenshot display type {value:?}; expected an App Store Connect \ |
| 50 | display type such as APP_IPHONE_67, APP_IPAD_PRO_129, APP_APPLE_VISION_PRO, \ |
| 51 | or APP_DESKTOP" |
| 52 | ) |
| 53 | }) |
| 54 | } |
| 55 | |
| 56 | #[cfg(test)] |
| 57 | mod tests { |
| 58 | use super::*; |
| 59 | |
| 60 | #[test] |
| 61 | fn platform_accepts_both_spellings() { |
| 62 | assert_eq!(platform_from_str("visionos").unwrap(), Platform::VisionOs); |
| 63 | assert_eq!(platform_from_str("VISION_OS").unwrap(), Platform::VisionOs); |
| 64 | } |
| 65 | |
| 66 | #[test] |
| 67 | fn errors_name_the_accepted_values() { |
| 68 | // A model that guessed wrong has to be able to fix itself from the |
| 69 | // error alone, without another call. |
| 70 | let error = platform_from_str("watchos").unwrap_err(); |
| 71 | assert!( |
| 72 | error.contains("ios") && error.contains("vision_os"), |
| 73 | "{error}" |
| 74 | ); |
| 75 | |
| 76 | let error = display_type_from_str("iphone").unwrap_err(); |
| 77 | assert!(error.contains("APP_IPHONE_67"), "{error}"); |
| 78 | |
| 79 | let error = bundle_id_platform_from_str("visionos").unwrap_err(); |
| 80 | assert!(error.contains("universal"), "{error}"); |
| 81 | } |
| 82 | |
| 83 | #[test] |
| 84 | fn display_type_round_trips_a_real_value() { |
| 85 | assert_eq!( |
| 86 | display_type_from_str("app_iphone_67").unwrap(), |
| 87 | ScreenshotDisplayType::Iphone67 |
| 88 | ); |
| 89 | } |
| 90 | } |