1use super::*;
4
5#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
7#[serde(deny_unknown_fields)]
8pub struct CiscoIpPhoneDirectoryEntry {
9 #[serde(rename = "Name", default, skip_serializing_if = "Option::is_none")]
10 pub name: Option<String>,
11 #[serde(rename = "Telephone", default, skip_serializing_if = "Option::is_none")]
12 pub telephone: Option<String>,
13}
14
15#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
17#[serde(rename = "CiscoIPPhoneDirectory", deny_unknown_fields)]
18pub struct CiscoIpPhoneDirectory {
19 #[serde(
20 rename = "@keypadTarget",
21 default,
22 skip_serializing_if = "Option::is_none"
23 )]
24 pub keypad_target: Option<PhoneKeypadTarget>,
25 #[serde(rename = "@appId", default, skip_serializing_if = "Option::is_none")]
26 pub application_id: Option<String>,
27 #[serde(
28 rename = "@onAppFocusLost",
29 default,
30 skip_serializing_if = "Option::is_none"
31 )]
32 pub on_focus_lost: Option<String>,
33 #[serde(
34 rename = "@onAppFocusGained",
35 default,
36 skip_serializing_if = "Option::is_none"
37 )]
38 pub on_focus_gained: Option<String>,
39 #[serde(
40 rename = "@onAppMinimized",
41 default,
42 skip_serializing_if = "Option::is_none"
43 )]
44 pub on_minimized: Option<String>,
45 #[serde(
46 rename = "@onAppClosed",
47 default,
48 skip_serializing_if = "Option::is_none"
49 )]
50 pub on_closed: Option<String>,
51 #[serde(rename = "Title", default, skip_serializing_if = "Option::is_none")]
52 pub title: Option<String>,
53 #[serde(rename = "Prompt", default, skip_serializing_if = "Option::is_none")]
54 pub prompt: Option<String>,
55 #[serde(rename = "SoftKeyItem", default)]
56 pub soft_keys: Vec<CiscoIpPhoneSoftKeyItem>,
57 #[serde(rename = "KeyItem", default)]
58 pub key_items: Vec<CiscoIpPhoneKeyItem>,
59 #[serde(rename = "DirectoryEntry", default)]
60 pub entries: Vec<CiscoIpPhoneDirectoryEntry>,
61}
62
63impl CiscoIpPhoneDirectory {
64 pub fn new(
66 title: impl Into<String>,
67 prompt: impl Into<String>,
68 entries: Vec<CiscoIpPhoneDirectoryEntry>,
69 ) -> Result<Self, PhoneXmlError> {
70 let document = Self {
71 keypad_target: None,
72 application_id: None,
73 on_focus_lost: None,
74 on_focus_gained: None,
75 on_minimized: None,
76 on_closed: None,
77 title: Some(title.into()),
78 prompt: Some(prompt.into()),
79 soft_keys: Vec::new(),
80 key_items: Vec::new(),
81 entries,
82 };
83 document.validate()?;
84 Ok(document)
85 }
86
87 pub fn validate(&self) -> Result<(), PhoneXmlError> {
89 validate_count(
90 "directory entries",
91 self.entries.len(),
92 PHONE_DIRECTORY_MAX_ENTRIES,
93 )?;
94 validate_count("directory soft keys", self.soft_keys.len(), 16)?;
95 validate_count("directory key items", self.key_items.len(), 32)?;
96 validate_optional_text("directory title", self.title.as_deref(), 0, 32)?;
97 validate_optional_text("directory prompt", self.prompt.as_deref(), 0, 32)?;
98 validate_optional_text(
99 "directory application id",
100 self.application_id.as_deref(),
101 1,
102 64,
103 )?;
104 for value in [
105 self.on_focus_lost.as_deref(),
106 self.on_focus_gained.as_deref(),
107 self.on_minimized.as_deref(),
108 self.on_closed.as_deref(),
109 ] {
110 validate_optional_text("directory lifecycle URL", value, 1, PHONE_XML_URL_MAX_CHARS)?;
111 }
112 validate_internal_action("directory onAppClosed action", self.on_closed.as_deref())?;
113 for entry in &self.entries {
114 validate_optional_text(
115 "directory entry name",
116 entry.name.as_deref(),
117 0,
118 PHONE_DIRECTORY_TEXT_MAX_CHARS,
119 )?;
120 validate_optional_text(
121 "directory entry telephone",
122 entry.telephone.as_deref(),
123 0,
124 PHONE_DIRECTORY_TEXT_MAX_CHARS,
125 )?;
126 }
127 for soft_key in &self.soft_keys {
128 validate_optional_text("directory soft-key name", soft_key.name.as_deref(), 0, 32)?;
129 validate_optional_text(
130 "directory soft-key URL",
131 soft_key.url.as_deref(),
132 0,
133 PHONE_XML_URL_MAX_CHARS,
134 )?;
135 validate_optional_text(
136 "directory soft-key down URL",
137 soft_key.url_down.as_deref(),
138 0,
139 PHONE_XML_URL_MAX_CHARS,
140 )?;
141 validate_internal_action("directory soft-key URLDown", soft_key.url_down.as_deref())?;
142 }
143 for key_item in &self.key_items {
144 validate_optional_text(
145 "directory key URL",
146 key_item.url.as_deref(),
147 0,
148 PHONE_XML_URL_MAX_CHARS,
149 )?;
150 validate_optional_text(
151 "directory key down URL",
152 key_item.url_down.as_deref(),
153 0,
154 PHONE_XML_URL_MAX_CHARS,
155 )?;
156 validate_internal_action("directory key URLDown", key_item.url_down.as_deref())?;
157 }
158 Ok(())
159 }
160}
161
162pub(super) fn validate_optional_text(
163 field: &'static str,
164 value: Option<&str>,
165 minimum: usize,
166 maximum: usize,
167) -> Result<(), PhoneXmlError> {
168 let Some(value) = value else {
169 return Ok(());
170 };
171 if !text_length_is_within(value, minimum, maximum) {
172 return Err(PhoneXmlError::InvalidField {
173 field,
174 expected: match (minimum, maximum) {
175 (0, 32) => "at most 32 characters",
176 (0, 256) => "at most 256 characters",
177 (1, 64) => "between 1 and 64 characters",
178 (1, 256) => "between 1 and 256 characters",
179 _ => "within the schema length bounds",
180 },
181 });
182 }
183 if !has_only_xml_characters(value) {
184 return Err(PhoneXmlError::InvalidField {
185 field,
186 expected: "valid XML text without forbidden control characters",
187 });
188 }
189 Ok(())
190}
191
192pub(super) fn has_only_xml_characters(value: &str) -> bool {
193 value.chars().all(|character| {
194 matches!(
195 character as u32,
196 0x9 | 0xA | 0xD | 0x20..=0xD7FF | 0xE000..=0xFFFD | 0x10000..=0x10FFFF
197 )
198 })
199}
200
201pub(super) fn action_kind(value: &str) -> PhoneActionKind {
202 match value.split_once(':').map(|(scheme, _)| scheme) {
203 Some(scheme)
204 if scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https") =>
205 {
206 PhoneActionKind::Http
207 }
208 _ => PhoneActionKind::Internal,
209 }
210}
211
212pub(super) fn validate_internal_action(
213 field: &'static str,
214 value: Option<&str>,
215) -> Result<(), PhoneXmlError> {
216 if value.is_some_and(|value| action_kind(value) == PhoneActionKind::Http) {
217 Err(PhoneXmlError::InvalidField {
218 field,
219 expected: "an internal phone action, not HTTP or HTTPS",
220 })
221 } else {
222 Ok(())
223 }
224}
225
226pub(super) fn validate_displayable(
227 title: Option<&str>,
228 prompt: Option<&str>,
229 application_id: Option<&str>,
230 lifecycle_urls: [Option<&str>; 4],
231 soft_keys: &[CiscoIpPhoneSoftKeyItem],
232 key_items: &[CiscoIpPhoneKeyItem],
233) -> Result<(), PhoneXmlError> {
234 validate_optional_text("display title", title, 0, 32)?;
235 validate_optional_text("display prompt", prompt, 0, 32)?;
236 validate_optional_text("display application id", application_id, 1, 64)?;
237 let [on_focus_lost, on_focus_gained, on_minimized, on_closed] = lifecycle_urls;
238 for url in [on_focus_lost, on_focus_gained, on_minimized, on_closed] {
239 validate_optional_text("display lifecycle URL", url, 1, PHONE_XML_URL_MAX_CHARS)?;
240 }
241 validate_internal_action("display onAppClosed action", on_closed)?;
242 validate_count("display soft keys", soft_keys.len(), 16)?;
243 for soft_key in soft_keys {
244 validate_optional_text("display soft-key name", soft_key.name.as_deref(), 0, 32)?;
245 validate_optional_text(
246 "display soft-key URL",
247 soft_key.url.as_deref(),
248 0,
249 PHONE_XML_URL_MAX_CHARS,
250 )?;
251 validate_optional_text(
252 "display soft-key down URL",
253 soft_key.url_down.as_deref(),
254 0,
255 PHONE_XML_URL_MAX_CHARS,
256 )?;
257 validate_internal_action("display soft-key URLDown", soft_key.url_down.as_deref())?;
258 }
259 validate_count("display key items", key_items.len(), 32)?;
260 for key_item in key_items {
261 validate_optional_text(
262 "display key URL",
263 key_item.url.as_deref(),
264 0,
265 PHONE_XML_URL_MAX_CHARS,
266 )?;
267 validate_optional_text(
268 "display key down URL",
269 key_item.url_down.as_deref(),
270 0,
271 PHONE_XML_URL_MAX_CHARS,
272 )?;
273 validate_internal_action("display key URLDown", key_item.url_down.as_deref())?;
274 }
275 Ok(())
276}
277
278pub(super) fn validate_image_display(
279 title: Option<&str>,
280 prompt: Option<&str>,
281 application_id: Option<&str>,
282 lifecycle_urls: [Option<&str>; 4],
283 soft_keys: &[CiscoIpPhoneSoftKeyItem],
284 key_items: &[CiscoIpPhoneKeyItem],
285) -> Result<(), PhoneXmlError> {
286 validate_displayable(
287 title,
288 prompt,
289 application_id,
290 lifecycle_urls,
291 soft_keys,
292 key_items,
293 )
294}
295
296pub(super) fn validate_bitmap_image(
297 location_x: Option<i16>,
298 location_y: Option<i16>,
299 width: u16,
300 height: u16,
301 depth: u16,
302 data: Option<&PhoneBitmapData>,
303) -> Result<(), PhoneXmlError> {
304 if location_x.is_some_and(|value| !(-1..=132).contains(&value)) {
305 return Err(PhoneXmlError::InvalidField {
306 field: "bitmap image horizontal location",
307 expected: "between -1 and 132",
308 });
309 }
310 if location_y.is_some_and(|value| !(-1..=64).contains(&value)) {
311 return Err(PhoneXmlError::InvalidField {
312 field: "bitmap image vertical location",
313 expected: "between -1 and 64",
314 });
315 }
316 if !(1..=133).contains(&width) {
317 return Err(PhoneXmlError::InvalidField {
318 field: "bitmap image width",
319 expected: "between 1 and 133",
320 });
321 }
322 if !(1..=65).contains(&height) {
323 return Err(PhoneXmlError::InvalidField {
324 field: "bitmap image height",
325 expected: "between 1 and 65",
326 });
327 }
328 if !(1..=2).contains(&depth) {
329 return Err(PhoneXmlError::InvalidField {
330 field: "bitmap image depth",
331 expected: "between 1 and 2",
332 });
333 }
334 if let Some(data) = data {
335 validate_count(
336 "bitmap image data bytes",
337 data.as_bytes().len(),
338 PHONE_IMAGE_BITMAP_MAX_BYTES,
339 )?;
340 }
341 Ok(())
342}
343
344pub(super) fn validate_file_image_location(
345 location_x: Option<i16>,
346 location_y: Option<i16>,
347) -> Result<(), PhoneXmlError> {
348 if location_x.is_some_and(|value| !(-1..=297).contains(&value)) {
349 return Err(PhoneXmlError::InvalidField {
350 field: "image-file horizontal location",
351 expected: "between -1 and 297",
352 });
353 }
354 if location_y.is_some_and(|value| !(-1..=167).contains(&value)) {
355 return Err(PhoneXmlError::InvalidField {
356 field: "image-file vertical location",
357 expected: "between -1 and 167",
358 });
359 }
360 Ok(())
361}
362
363pub(super) fn validate_icon_menu_items(
364 items: &[CiscoIpPhoneIconMenuItem],
365) -> Result<(), PhoneXmlError> {
366 validate_count("icon menu items", items.len(), PHONE_ICON_MENU_MAX_ITEMS)?;
367 for item in items {
368 validate_optional_text("icon menu item name", item.name.as_deref(), 0, 64)?;
369 validate_optional_text(
370 "icon menu item URL",
371 item.url.as_deref(),
372 0,
373 PHONE_XML_URL_MAX_CHARS,
374 )?;
375 if item.icon_index.is_some_and(|index| index > 9) {
376 return Err(PhoneXmlError::InvalidField {
377 field: "icon menu item index",
378 expected: "between 0 and 9",
379 });
380 }
381 }
382 Ok(())
383}
384
385pub(super) fn validate_bitmap_icon(icon: &CiscoIpPhoneIconItem) -> Result<(), PhoneXmlError> {
386 if !(1..=16).contains(&icon.width) {
387 return Err(PhoneXmlError::InvalidField {
388 field: "bitmap icon width",
389 expected: "between 1 and 16",
390 });
391 }
392 if !(1..=10).contains(&icon.height) {
393 return Err(PhoneXmlError::InvalidField {
394 field: "bitmap icon height",
395 expected: "between 1 and 10",
396 });
397 }
398 if !(1..=2).contains(&icon.depth) {
399 return Err(PhoneXmlError::InvalidField {
400 field: "bitmap icon depth",
401 expected: "between 1 and 2",
402 });
403 }
404 if let Some(data) = &icon.data
405 && (data.len() > 80
406 || data.len() % 2 != 0
407 || !data.bytes().all(|byte| byte.is_ascii_hexdigit()))
408 {
409 return Err(PhoneXmlError::InvalidField {
410 field: "bitmap icon data",
411 expected: "at most 40 hexadecimal bytes",
412 });
413 }
414 Ok(())
415}
416
417#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
419#[serde(deny_unknown_fields)]
420pub struct CiscoIpPhoneMenuItem {
421 #[serde(rename = "Name", default, skip_serializing_if = "Option::is_none")]
422 pub name: Option<String>,
423 #[serde(rename = "URL", default, skip_serializing_if = "Option::is_none")]
424 pub url: Option<String>,
425}
426
427#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
429#[serde(rename = "CiscoIPPhoneMenu", deny_unknown_fields)]
430pub struct CiscoIpPhoneMenu {
431 #[serde(
432 rename = "@keypadTarget",
433 default,
434 skip_serializing_if = "Option::is_none"
435 )]
436 pub keypad_target: Option<PhoneKeypadTarget>,
437 #[serde(rename = "@appId", default, skip_serializing_if = "Option::is_none")]
438 pub application_id: Option<String>,
439 #[serde(
440 rename = "@onAppFocusLost",
441 default,
442 skip_serializing_if = "Option::is_none"
443 )]
444 pub on_focus_lost: Option<String>,
445 #[serde(
446 rename = "@onAppFocusGained",
447 default,
448 skip_serializing_if = "Option::is_none"
449 )]
450 pub on_focus_gained: Option<String>,
451 #[serde(
452 rename = "@onAppMinimized",
453 default,
454 skip_serializing_if = "Option::is_none"
455 )]
456 pub on_minimized: Option<String>,
457 #[serde(
458 rename = "@onAppClosed",
459 default,
460 skip_serializing_if = "Option::is_none"
461 )]
462 pub on_closed: Option<String>,
463 #[serde(rename = "Title", default, skip_serializing_if = "Option::is_none")]
464 pub title: Option<String>,
465 #[serde(rename = "Prompt", default, skip_serializing_if = "Option::is_none")]
466 pub prompt: Option<String>,
467 #[serde(rename = "SoftKeyItem", default)]
468 pub soft_keys: Vec<CiscoIpPhoneSoftKeyItem>,
469 #[serde(rename = "KeyItem", default)]
470 pub key_items: Vec<CiscoIpPhoneKeyItem>,
471 #[serde(rename = "MenuItem", default)]
472 pub items: Vec<CiscoIpPhoneMenuItem>,
473}
474
475impl CiscoIpPhoneMenu {
476 pub fn new(
478 title: impl Into<String>,
479 prompt: impl Into<String>,
480 items: Vec<CiscoIpPhoneMenuItem>,
481 ) -> Result<Self, PhoneXmlError> {
482 let document = Self {
483 keypad_target: None,
484 application_id: None,
485 on_focus_lost: None,
486 on_focus_gained: None,
487 on_minimized: None,
488 on_closed: None,
489 title: Some(title.into()),
490 prompt: Some(prompt.into()),
491 soft_keys: Vec::new(),
492 key_items: Vec::new(),
493 items,
494 };
495 document.validate()?;
496 Ok(document)
497 }
498
499 pub fn validate(&self) -> Result<(), PhoneXmlError> {
501 validate_displayable(
502 self.title.as_deref(),
503 self.prompt.as_deref(),
504 self.application_id.as_deref(),
505 [
506 self.on_focus_lost.as_deref(),
507 self.on_focus_gained.as_deref(),
508 self.on_minimized.as_deref(),
509 self.on_closed.as_deref(),
510 ],
511 &self.soft_keys,
512 &self.key_items,
513 )?;
514 validate_count("menu items", self.items.len(), PHONE_MENU_MAX_ITEMS)?;
515 for item in &self.items {
516 validate_optional_text("menu item name", item.name.as_deref(), 0, 64)?;
517 validate_optional_text(
518 "menu item URL",
519 item.url.as_deref(),
520 0,
521 PHONE_XML_URL_MAX_CHARS,
522 )?;
523 }
524 Ok(())
525 }
526}
527
528#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
530#[serde(deny_unknown_fields)]
531pub struct CiscoIpPhoneIconItem {
532 #[serde(rename = "Index")]
533 pub index: u16,
534 #[serde(rename = "Width")]
535 pub width: u16,
537 #[serde(rename = "Height")]
538 pub height: u16,
540 #[serde(rename = "Depth")]
541 pub depth: u16,
543 #[serde(rename = "Data", default, skip_serializing_if = "Option::is_none")]
544 pub data: Option<String>,
546}
547
548#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
550#[serde(deny_unknown_fields)]
551pub struct CiscoIpPhoneIconFileItem {
552 #[serde(rename = "Index")]
553 pub index: u16,
554 #[serde(rename = "URL")]
555 pub url: String,
557}
558
559#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
561#[serde(deny_unknown_fields)]
562pub struct CiscoIpPhoneIconMenuItem {
563 #[serde(rename = "Name", default, skip_serializing_if = "Option::is_none")]
564 pub name: Option<String>,
565 #[serde(rename = "URL", default, skip_serializing_if = "Option::is_none")]
566 pub url: Option<String>,
567 #[serde(rename = "IconIndex", default, skip_serializing_if = "Option::is_none")]
568 pub icon_index: Option<u16>,
570}
571
572#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
574#[serde(deny_unknown_fields)]
575pub struct CiscoIpPhoneIconTitle {
576 #[serde(
577 rename = "@IconIndex",
578 default,
579 skip_serializing_if = "Option::is_none"
580 )]
581 pub icon_index: Option<u16>,
583 #[serde(rename = "$text", default)]
584 pub text: String,
585}
586
587#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
589#[serde(rename = "CiscoIPPhoneIconMenu", deny_unknown_fields)]
590pub struct CiscoIpPhoneIconMenu {
591 #[serde(
592 rename = "@keypadTarget",
593 default,
594 skip_serializing_if = "Option::is_none"
595 )]
596 pub keypad_target: Option<PhoneKeypadTarget>,
597 #[serde(rename = "@appId", default, skip_serializing_if = "Option::is_none")]
598 pub application_id: Option<String>,
599 #[serde(
600 rename = "@onAppFocusLost",
601 default,
602 skip_serializing_if = "Option::is_none"
603 )]
604 pub on_focus_lost: Option<String>,
605 #[serde(
606 rename = "@onAppFocusGained",
607 default,
608 skip_serializing_if = "Option::is_none"
609 )]
610 pub on_focus_gained: Option<String>,
611 #[serde(
612 rename = "@onAppMinimized",
613 default,
614 skip_serializing_if = "Option::is_none"
615 )]
616 pub on_minimized: Option<String>,
617 #[serde(
618 rename = "@onAppClosed",
619 default,
620 skip_serializing_if = "Option::is_none"
621 )]
622 pub on_closed: Option<String>,
623 #[serde(rename = "Title", default, skip_serializing_if = "Option::is_none")]
624 pub title: Option<String>,
625 #[serde(rename = "Prompt", default, skip_serializing_if = "Option::is_none")]
626 pub prompt: Option<String>,
627 #[serde(rename = "SoftKeyItem", default)]
628 pub soft_keys: Vec<CiscoIpPhoneSoftKeyItem>,
629 #[serde(rename = "KeyItem", default)]
630 pub key_items: Vec<CiscoIpPhoneKeyItem>,
631 #[serde(rename = "MenuItem", default)]
632 pub items: Vec<CiscoIpPhoneIconMenuItem>,
633 #[serde(rename = "IconItem", default)]
634 pub icons: Vec<CiscoIpPhoneIconItem>,
635}
636
637impl CiscoIpPhoneIconMenu {
638 pub fn new(
640 title: impl Into<String>,
641 prompt: impl Into<String>,
642 items: Vec<CiscoIpPhoneIconMenuItem>,
643 icons: Vec<CiscoIpPhoneIconItem>,
644 ) -> Result<Self, PhoneXmlError> {
645 let document = Self {
646 keypad_target: None,
647 application_id: None,
648 on_focus_lost: None,
649 on_focus_gained: None,
650 on_minimized: None,
651 on_closed: None,
652 title: Some(title.into()),
653 prompt: Some(prompt.into()),
654 soft_keys: Vec::new(),
655 key_items: Vec::new(),
656 items,
657 icons,
658 };
659 document.validate()?;
660 Ok(document)
661 }
662
663 pub fn validate(&self) -> Result<(), PhoneXmlError> {
665 validate_displayable(
666 self.title.as_deref(),
667 self.prompt.as_deref(),
668 self.application_id.as_deref(),
669 [
670 self.on_focus_lost.as_deref(),
671 self.on_focus_gained.as_deref(),
672 self.on_minimized.as_deref(),
673 self.on_closed.as_deref(),
674 ],
675 &self.soft_keys,
676 &self.key_items,
677 )?;
678 validate_icon_menu_items(&self.items)?;
679 validate_count(
680 "icon menu icons",
681 self.icons.len(),
682 PHONE_ICON_MENU_MAX_ICONS,
683 )?;
684 for icon in &self.icons {
685 validate_bitmap_icon(icon)?;
686 }
687 Ok(())
688 }
689}
690
691#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, Serialize)]
693#[serde(rename = "CiscoIPPhoneIconFileMenu", deny_unknown_fields)]
694pub struct CiscoIpPhoneIconFileMenu {
695 #[serde(
696 rename = "@keypadTarget",
697 default,
698 skip_serializing_if = "Option::is_none"
699 )]
700 pub keypad_target: Option<PhoneKeypadTarget>,
701 #[serde(rename = "@appId", default, skip_serializing_if = "Option::is_none")]
702 pub application_id: Option<String>,
703 #[serde(
704 rename = "@onAppFocusLost",
705 default,
706 skip_serializing_if = "Option::is_none"
707 )]
708 pub on_focus_lost: Option<String>,
709 #[serde(
710 rename = "@onAppFocusGained",
711 default,
712 skip_serializing_if = "Option::is_none"
713 )]
714 pub on_focus_gained: Option<String>,
715 #[serde(
716 rename = "@onAppMinimized",
717 default,
718 skip_serializing_if = "Option::is_none"
719 )]
720 pub on_minimized: Option<String>,
721 #[serde(
722 rename = "@onAppClosed",
723 default,
724 skip_serializing_if = "Option::is_none"
725 )]
726 pub on_closed: Option<String>,
727 #[serde(
728 rename = "@IconIndex",
729 default,
730 skip_serializing_if = "Option::is_none"
731 )]
732 pub icon_index: Option<u16>,
734 #[serde(rename = "Title", default, skip_serializing_if = "Option::is_none")]
735 pub title: Option<CiscoIpPhoneIconTitle>,
736 #[serde(rename = "Prompt", default, skip_serializing_if = "Option::is_none")]
737 pub prompt: Option<String>,
738 #[serde(rename = "SoftKeyItem", default)]
739 pub soft_keys: Vec<CiscoIpPhoneSoftKeyItem>,
740 #[serde(rename = "KeyItem", default)]
741 pub key_items: Vec<CiscoIpPhoneKeyItem>,
742 #[serde(rename = "MenuItem", default)]
743 pub items: Vec<CiscoIpPhoneIconMenuItem>,
744 #[serde(rename = "IconItem", default)]
745 pub icons: Vec<CiscoIpPhoneIconFileItem>,
746}
747
748impl CiscoIpPhoneIconFileMenu {
749 pub fn validate(&self) -> Result<(), PhoneXmlError> {
751 validate_displayable(
752 self.title.as_ref().map(|title| title.text.as_str()),
753 self.prompt.as_deref(),
754 self.application_id.as_deref(),
755 [
756 self.on_focus_lost.as_deref(),
757 self.on_focus_gained.as_deref(),
758 self.on_minimized.as_deref(),
759 self.on_closed.as_deref(),
760 ],
761 &self.soft_keys,
762 &self.key_items,
763 )?;
764 validate_icon_menu_items(&self.items)?;
765 validate_count(
766 "icon-file menu icons",
767 self.icons.len(),
768 PHONE_ICON_MENU_MAX_ICONS,
769 )?;
770 for icon in &self.icons {
771 if icon.index > 9 {
772 return Err(PhoneXmlError::InvalidField {
773 field: "icon-file index",
774 expected: "between 0 and 9",
775 });
776 }
777 validate_optional_text("icon-file URL", Some(&icon.url), 1, PHONE_XML_URL_MAX_CHARS)?;
778 }
779 Ok(())
780 }
781}