Skip to main content

app_store_server_library/models/
upload_message_request_body.rs

1use serde::{Deserialize, Serialize};
2
3use crate::models::bullet_point::BulletPoint;
4use crate::models::header_position::HeaderPosition;
5use crate::models::upload_message_image::UploadMessageImage;
6
7const MAXIMUM_HEADER_LENGTH: usize = 66;
8const MAXIMUM_BODY_LENGTH: usize = 144;
9const MAXIMUM_BULLET_POINTS_COUNT: usize = 5;
10
11/// The request body for uploading a message, which includes the message text and an optional image reference.
12///
13/// [UploadMessageRequestBody](https://developer.apple.com/documentation/retentionmessaging/uploadmessagerequestbody)
14#[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq)]
15pub struct UploadMessageRequestBody {
16    /// The header text of the retention message that the system displays to customers.
17    ///
18    /// [header](https://developer.apple.com/documentation/retentionmessaging/header)
19    pub header: String,
20
21    /// The body text of the retention message that the system displays to customers.
22    ///
23    /// [body](https://developer.apple.com/documentation/retentionmessaging/body)
24    pub body: String,
25
26    /// The optional image identifier and its alternative text to appear as part of a text-based message with an image.
27    ///
28    /// [UploadMessageImage](https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage)
29    pub image: Option<UploadMessageImage>,
30
31    /// The bulleted list to display as part of the retention message.
32    ///
33    /// [bulletPoints](https://developer.apple.com/documentation/retentionmessaging/bulletpoints)
34    #[serde(rename = "bulletPoints")]
35    pub bullet_points: Option<Vec<BulletPoint>>,
36
37    /// The position of the header relative to the body and image.
38    ///
39    /// [headerPosition](https://developer.apple.com/documentation/retentionmessaging/headerposition)
40    #[serde(rename = "headerPosition")]
41    pub header_position: Option<HeaderPosition>,
42}
43
44impl UploadMessageRequestBody {
45    /// Creates a new UploadMessageRequestBody with validation.
46    ///
47    /// # Errors
48    ///
49    /// Returns `ValidationError::HeaderTooLong` if header exceeds 66 characters.
50    /// Returns `ValidationError::BodyTooLong` if body exceeds 144 characters.
51    /// Returns `ValidationError::TooManyBulletPoints` if more than 5 bullet points are provided.
52    pub fn new(
53        header: String,
54        body: String,
55        image: Option<UploadMessageImage>,
56        bullet_points: Option<Vec<BulletPoint>>,
57        header_position: Option<HeaderPosition>,
58    ) -> Result<Self, ValidationError> {
59        if header.chars().count() > MAXIMUM_HEADER_LENGTH {
60            return Err(ValidationError::HeaderTooLong);
61        }
62        if body.chars().count() > MAXIMUM_BODY_LENGTH {
63            return Err(ValidationError::BodyTooLong);
64        }
65        if let Some(bullet_points) = &bullet_points {
66            if bullet_points.len() > MAXIMUM_BULLET_POINTS_COUNT {
67                return Err(ValidationError::TooManyBulletPoints);
68            }
69        }
70        Ok(Self {
71            header,
72            body,
73            image,
74            bullet_points,
75            header_position,
76        })
77    }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum ValidationError {
82    HeaderTooLong,
83    BodyTooLong,
84    TooManyBulletPoints,
85}
86
87impl std::fmt::Display for ValidationError {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        match self {
90            ValidationError::HeaderTooLong => {
91                write!(
92                    f,
93                    "Header exceeds maximum length of {} characters",
94                    MAXIMUM_HEADER_LENGTH
95                )
96            }
97            ValidationError::BodyTooLong => {
98                write!(
99                    f,
100                    "Body exceeds maximum length of {} characters",
101                    MAXIMUM_BODY_LENGTH
102                )
103            }
104            ValidationError::TooManyBulletPoints => {
105                write!(
106                    f,
107                    "Bullet points exceed maximum count of {}",
108                    MAXIMUM_BULLET_POINTS_COUNT
109                )
110            }
111        }
112    }
113}
114
115impl std::error::Error for ValidationError {}
116
117#[cfg(test)]
118mod tests {
119    use uuid::Uuid;
120
121    use super::*;
122
123    fn bullet_point() -> BulletPoint {
124        BulletPoint::new("text".to_string(), Uuid::new_v4(), "alt".to_string()).unwrap()
125    }
126
127    #[test]
128    fn test_header_and_body_count_characters_not_bytes() {
129        // "日" is 3 bytes: a byte-based check would reject these at a third of the real limit.
130        let header = "日".repeat(MAXIMUM_HEADER_LENGTH);
131        let body = "日".repeat(MAXIMUM_BODY_LENGTH);
132        assert_eq!(header.len(), MAXIMUM_HEADER_LENGTH * 3);
133        assert_eq!(body.len(), MAXIMUM_BODY_LENGTH * 3);
134        assert!(UploadMessageRequestBody::new(header, body, None, None, None).is_ok());
135    }
136
137    #[test]
138    fn test_header_too_long() {
139        let header = "日".repeat(MAXIMUM_HEADER_LENGTH + 1);
140        assert_eq!(
141            UploadMessageRequestBody::new(header, "body".to_string(), None, None, None),
142            Err(ValidationError::HeaderTooLong)
143        );
144    }
145
146    #[test]
147    fn test_body_too_long() {
148        let body = "日".repeat(MAXIMUM_BODY_LENGTH + 1);
149        assert_eq!(
150            UploadMessageRequestBody::new("header".to_string(), body, None, None, None),
151            Err(ValidationError::BodyTooLong)
152        );
153    }
154
155    #[test]
156    fn test_bullet_points_at_maximum_allowed() {
157        let bullet_points = vec![bullet_point(); MAXIMUM_BULLET_POINTS_COUNT];
158        assert!(UploadMessageRequestBody::new(
159            "header".to_string(),
160            "body".to_string(),
161            None,
162            Some(bullet_points),
163            None,
164        )
165        .is_ok());
166    }
167
168    #[test]
169    fn test_too_many_bullet_points() {
170        let bullet_points = vec![bullet_point(); MAXIMUM_BULLET_POINTS_COUNT + 1];
171        assert_eq!(
172            UploadMessageRequestBody::new(
173                "header".to_string(),
174                "body".to_string(),
175                None,
176                Some(bullet_points),
177                None,
178            ),
179            Err(ValidationError::TooManyBulletPoints)
180        );
181    }
182
183    #[test]
184    fn test_bullet_points_absent_is_allowed() {
185        assert!(UploadMessageRequestBody::new("header".to_string(), "body".to_string(), None, None, None,).is_ok());
186    }
187}