| 1 | use serde::{Deserialize, Serialize}; |
| 2 | |
| 3 | /// Release notes for one language on a track release. |
| 4 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] |
| 5 | pub struct LocalizedText { |
| 6 | pub language: String, |
| 7 | pub text: String, |
| 8 | } |
| 9 | |
| 10 | /// One release on a track. Fields this crate does not edit are carried |
| 11 | /// through untouched so a `tracks.update` does not drop them. |
| 12 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 13 | #[serde(rename_all = "camelCase")] |
| 14 | pub struct TrackRelease { |
| 15 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 16 | pub name: Option<String>, |
| 17 | #[serde(default)] |
| 18 | pub version_codes: Vec<String>, |
| 19 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 20 | pub status: Option<String>, |
| 21 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 22 | pub user_fraction: Option<f64>, |
| 23 | #[serde(default)] |
| 24 | pub release_notes: Vec<LocalizedText>, |
| 25 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 26 | pub country_targeting: Option<serde_json::Value>, |
| 27 | } |
| 28 | |
| 29 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 30 | pub struct Track { |
| 31 | pub track: String, |
| 32 | #[serde(default)] |
| 33 | pub releases: Vec<TrackRelease>, |
| 34 | } |
| 35 | |
| 36 | impl Track { |
| 37 | /// Set `locale`'s release notes on the release carrying `version_code`, |
| 38 | /// replacing any existing note for that language. Returns an error if no |
| 39 | /// release carries the code, or the text is over Play's limit. |
| 40 | pub fn set_release_notes( |
| 41 | &mut self, |
| 42 | version_code: &str, |
| 43 | locale: &str, |
| 44 | text: &str, |
| 45 | ) -> std::result::Result<(), String> { |
| 46 | let len = text.chars().count(); |
| 47 | if len > crate::listing::RELEASE_NOTES_MAX_LEN { |
| 48 | return Err(format!( |
| 49 | "release notes are {len} characters; Google Play accepts at most {}", |
| 50 | crate::listing::RELEASE_NOTES_MAX_LEN |
| 51 | )); |
| 52 | } |
| 53 | let release = self |
| 54 | .releases |
| 55 | .iter_mut() |
| 56 | .find(|r| r.version_codes.iter().any(|c| c == version_code)) |
| 57 | .ok_or_else(|| { |
| 58 | format!( |
| 59 | "no release on track {} carries version code {version_code}", |
| 60 | self.track |
| 61 | ) |
| 62 | })?; |
| 63 | release.release_notes.retain(|n| n.language != locale); |
| 64 | release.release_notes.push(LocalizedText { |
| 65 | language: locale.to_string(), |
| 66 | text: text.to_string(), |
| 67 | }); |
| 68 | Ok(()) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | #[cfg(test)] |
| 73 | mod tests { |
| 74 | use super::*; |
| 75 | |
| 76 | fn track() -> Track { |
| 77 | serde_json::from_str( |
| 78 | r#"{"track":"internal","releases":[{"name":"1.2.0","versionCodes":["120"], |
| 79 | "status":"draft","releaseNotes":[{"language":"en-US","text":"old"}, |
| 80 | {"language":"id","text":"lama"}]}]}"#, |
| 81 | ) |
| 82 | .unwrap() |
| 83 | } |
| 84 | |
| 85 | #[test] |
| 86 | fn notes_replace_only_the_given_language() { |
| 87 | let mut t = track(); |
| 88 | t.set_release_notes("120", "en-US", "new").unwrap(); |
| 89 | let notes = &t.releases[0].release_notes; |
| 90 | assert_eq!(notes.len(), 2); |
| 91 | assert!(notes.iter().any(|n| n.language == "id" && n.text == "lama")); |
| 92 | assert!(notes |
| 93 | .iter() |
| 94 | .any(|n| n.language == "en-US" && n.text == "new")); |
| 95 | } |
| 96 | |
| 97 | #[test] |
| 98 | fn an_unknown_version_code_is_an_error() { |
| 99 | assert!(track().set_release_notes("999", "en-US", "x").is_err()); |
| 100 | } |
| 101 | |
| 102 | #[test] |
| 103 | fn notes_over_five_hundred_characters_are_rejected() { |
| 104 | assert!(track() |
| 105 | .set_release_notes("120", "en-US", &"x".repeat(501)) |
| 106 | .is_err()); |
| 107 | } |
| 108 | |
| 109 | #[test] |
| 110 | fn untouched_release_fields_survive_a_round_trip() { |
| 111 | let mut t = track(); |
| 112 | t.set_release_notes("120", "id", "baru").unwrap(); |
| 113 | let value = serde_json::to_value(&t).unwrap(); |
| 114 | assert_eq!(value["releases"][0]["status"], "draft"); |
| 115 | assert_eq!(value["releases"][0]["name"], "1.2.0"); |
| 116 | } |
| 117 | } |