development
rs 167 lines 5.19 KB
Raw
1 use serde::{Deserialize, Serialize};
2
3 /// Maximum `title` length Google Play accepts, in characters.
4 pub const TITLE_MAX_LEN: usize = 30;
5 /// Maximum `shortDescription` length, in characters.
6 pub const SHORT_DESCRIPTION_MAX_LEN: usize = 80;
7 /// Maximum `fullDescription` length, in characters.
8 pub const FULL_DESCRIPTION_MAX_LEN: usize = 4000;
9 /// Maximum length of one release note, per language, in characters.
10 pub const RELEASE_NOTES_MAX_LEN: usize = 500;
11
12 /// One localized store listing, as Play returns it.
13 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
14 #[serde(rename_all = "camelCase")]
15 pub struct Listing {
16 #[serde(default)]
17 pub language: String,
18 #[serde(default)]
19 pub title: Option<String>,
20 #[serde(default)]
21 pub short_description: Option<String>,
22 #[serde(default)]
23 pub full_description: Option<String>,
24 #[serde(default)]
25 pub video: Option<String>,
26 }
27
28 /// The fields of a listing patch. Unset fields are left off the body, so Play
29 /// keeps their current value.
30 #[derive(Debug, Clone, Default, Serialize)]
31 #[serde(rename_all = "camelCase")]
32 pub struct ListingFields {
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub title: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub short_description: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub full_description: Option<String>,
39 }
40
41 impl ListingFields {
42 pub fn is_empty(&self) -> bool {
43 self.title.is_none() && self.short_description.is_none() && self.full_description.is_none()
44 }
45
46 /// Check the fields against Play's length limits before anything is sent.
47 /// Returns `Err` naming the first field over its limit.
48 pub fn validate(&self) -> std::result::Result<(), String> {
49 let limits = [
50 ("title", &self.title, TITLE_MAX_LEN),
51 (
52 "shortDescription",
53 &self.short_description,
54 SHORT_DESCRIPTION_MAX_LEN,
55 ),
56 (
57 "fullDescription",
58 &self.full_description,
59 FULL_DESCRIPTION_MAX_LEN,
60 ),
61 ];
62 for (name, value, max) in limits {
63 let len = value.as_deref().map_or(0, |v| v.chars().count());
64 if len > max {
65 return Err(format!(
66 "{name} is {len} characters; Google Play accepts at most {max}"
67 ));
68 }
69 }
70 Ok(())
71 }
72
73 /// Names of the fields sent whose stored value is not an exact match.
74 pub fn diff(&self, stored: &Listing) -> Vec<&'static str> {
75 [
76 ("title", &self.title, &stored.title),
77 (
78 "shortDescription",
79 &self.short_description,
80 &stored.short_description,
81 ),
82 (
83 "fullDescription",
84 &self.full_description,
85 &stored.full_description,
86 ),
87 ]
88 .into_iter()
89 .filter(|(_, sent, stored)| match sent {
90 Some(sent) => stored.as_deref() != Some(sent.as_str()),
91 None => false,
92 })
93 .map(|(name, _, _)| name)
94 .collect()
95 }
96 }
97
98 #[cfg(test)]
99 mod tests {
100 use super::*;
101
102 #[test]
103 fn each_field_over_its_limit_is_rejected_by_name() {
104 let cases = [
105 (
106 ListingFields {
107 title: Some("x".repeat(31)),
108 ..Default::default()
109 },
110 "title",
111 ),
112 (
113 ListingFields {
114 short_description: Some("x".repeat(81)),
115 ..Default::default()
116 },
117 "shortDescription",
118 ),
119 (
120 ListingFields {
121 full_description: Some("x".repeat(4001)),
122 ..Default::default()
123 },
124 "fullDescription",
125 ),
126 ];
127 for (fields, name) in cases {
128 let message = fields.validate().unwrap_err();
129 assert!(message.starts_with(name), "{message}");
130 }
131 }
132
133 #[test]
134 fn fields_at_their_limit_pass_and_lengths_count_characters() {
135 let fields = ListingFields {
136 title: Some("é".repeat(30)),
137 short_description: Some("x".repeat(80)),
138 full_description: Some("x".repeat(4000)),
139 };
140 assert!(fields.validate().is_ok());
141 }
142
143 #[test]
144 fn unset_fields_are_left_off_the_body() {
145 let fields = ListingFields {
146 title: Some("T".into()),
147 ..Default::default()
148 };
149 assert_eq!(serde_json::to_string(&fields).unwrap(), r#"{"title":"T"}"#);
150 }
151
152 #[test]
153 fn diff_names_only_the_fields_that_were_sent_and_differ() {
154 let sent = ListingFields {
155 title: Some("A".into()),
156 short_description: Some("S".into()),
157 full_description: None,
158 };
159 let stored = Listing {
160 title: Some("A".into()),
161 short_description: Some("S ".into()),
162 full_description: Some("other".into()),
163 ..Default::default()
164 };
165 assert_eq!(sent.diff(&stored), vec!["shortDescription"]);
166 }
167 }