Skip to main content

shopify_sdk/rest/resources/v2025_10/
page.rs

1//! Page resource implementation.
2//!
3//! This module provides the Page resource, which represents a static page
4//! in a Shopify store. Pages are used for static content like "About Us",
5//! "Contact", or "Privacy Policy" pages.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use shopify_sdk::rest::{RestResource, ResourceResponse};
11//! use shopify_sdk::rest::resources::v2025_10::{Page, PageListParams};
12//!
13//! // Find a single page
14//! let page = Page::find(&client, 123, None).await?;
15//! println!("Page: {}", page.title.as_deref().unwrap_or(""));
16//!
17//! // List pages
18//! let params = PageListParams {
19//!     published_status: Some("published".to_string()),
20//!     limit: Some(50),
21//!     ..Default::default()
22//! };
23//! let pages = Page::all(&client, Some(params)).await?;
24//!
25//! // Create a new page
26//! let mut page = Page {
27//!     title: Some("About Us".to_string()),
28//!     body_html: Some("<p>Welcome to our store!</p>".to_string()),
29//!     ..Default::default()
30//! };
31//! let saved = page.save(&client).await?;
32//!
33//! // Count pages
34//! let count = Page::count(&client, None).await?;
35//! println!("Total pages: {}", count);
36//! ```
37
38use chrono::{DateTime, Utc};
39use serde::{Deserialize, Serialize};
40
41use crate::rest::{ResourceOperation, ResourcePath, RestResource};
42use crate::HttpMethod;
43
44/// A static page in a Shopify store.
45///
46/// Pages are used for static content that doesn't change frequently,
47/// such as "About Us", "Contact", "Privacy Policy", or "Terms of Service" pages.
48///
49/// # Fields
50///
51/// ## Read-Only Fields
52/// - `id` - The unique identifier of the page
53/// - `shop_id` - The ID of the shop the page belongs to
54/// - `created_at` - When the page was created
55/// - `updated_at` - When the page was last updated
56/// - `admin_graphql_api_id` - The GraphQL API ID
57///
58/// ## Writable Fields
59/// - `title` - The title of the page
60/// - `handle` - The URL-friendly handle (auto-generated from title if not set)
61/// - `body_html` - The HTML content of the page
62/// - `author` - The author of the page
63/// - `template_suffix` - The suffix of the Liquid template used for the page
64/// - `published_at` - When the page was published (can be set to future for scheduling)
65#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
66pub struct Page {
67    /// The unique identifier of the page.
68    /// Read-only field.
69    #[serde(skip_serializing)]
70    pub id: Option<u64>,
71
72    /// The ID of the shop the page belongs to.
73    /// Read-only field.
74    #[serde(skip_serializing)]
75    pub shop_id: Option<u64>,
76
77    /// The title of the page.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub title: Option<String>,
80
81    /// The URL-friendly handle of the page.
82    /// Auto-generated from the title if not specified.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub handle: Option<String>,
85
86    /// The HTML content of the page.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub body_html: Option<String>,
89
90    /// The author of the page.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub author: Option<String>,
93
94    /// The suffix of the Liquid template used for the page.
95    /// For example, if the value is "contact", the page uses the
96    /// `page.contact.liquid` template.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub template_suffix: Option<String>,
99
100    /// When the page was or will be published.
101    /// Set to a future date to schedule publication.
102    /// Set to `null` to unpublish.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub published_at: Option<DateTime<Utc>>,
105
106    /// When the page was created.
107    /// Read-only field.
108    #[serde(skip_serializing)]
109    pub created_at: Option<DateTime<Utc>>,
110
111    /// When the page was last updated.
112    /// Read-only field.
113    #[serde(skip_serializing)]
114    pub updated_at: Option<DateTime<Utc>>,
115
116    /// The admin GraphQL API ID for this page.
117    /// Read-only field.
118    #[serde(skip_serializing)]
119    pub admin_graphql_api_id: Option<String>,
120}
121
122impl RestResource for Page {
123    type Id = u64;
124    type FindParams = PageFindParams;
125    type AllParams = PageListParams;
126    type CountParams = PageCountParams;
127
128    const NAME: &'static str = "Page";
129    const PLURAL: &'static str = "pages";
130
131    const PATHS: &'static [ResourcePath] = &[
132        ResourcePath::new(
133            HttpMethod::Get,
134            ResourceOperation::Find,
135            &["id"],
136            "pages/{id}",
137        ),
138        ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "pages"),
139        ResourcePath::new(
140            HttpMethod::Get,
141            ResourceOperation::Count,
142            &[],
143            "pages/count",
144        ),
145        ResourcePath::new(HttpMethod::Post, ResourceOperation::Create, &[], "pages"),
146        ResourcePath::new(
147            HttpMethod::Put,
148            ResourceOperation::Update,
149            &["id"],
150            "pages/{id}",
151        ),
152        ResourcePath::new(
153            HttpMethod::Delete,
154            ResourceOperation::Delete,
155            &["id"],
156            "pages/{id}",
157        ),
158    ];
159
160    fn get_id(&self) -> Option<Self::Id> {
161        self.id
162    }
163}
164
165/// Parameters for finding a single page.
166#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
167pub struct PageFindParams {
168    /// Comma-separated list of fields to include in the response.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub fields: Option<String>,
171}
172
173/// Parameters for listing pages.
174#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
175pub struct PageListParams {
176    /// Filter by page title.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub title: Option<String>,
179
180    /// Filter by page handle.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub handle: Option<String>,
183
184    /// Filter by published status.
185    /// Valid values: `published`, `unpublished`, `any`.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub published_status: Option<String>,
188
189    /// Show pages created after this date.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub created_at_min: Option<DateTime<Utc>>,
192
193    /// Show pages created before this date.
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub created_at_max: Option<DateTime<Utc>>,
196
197    /// Show pages updated after this date.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub updated_at_min: Option<DateTime<Utc>>,
200
201    /// Show pages updated before this date.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub updated_at_max: Option<DateTime<Utc>>,
204
205    /// Show pages published after this date.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub published_at_min: Option<DateTime<Utc>>,
208
209    /// Show pages published before this date.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub published_at_max: Option<DateTime<Utc>>,
212
213    /// Maximum number of results to return (default: 50, max: 250).
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub limit: Option<u32>,
216
217    /// Return pages after this ID.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub since_id: Option<u64>,
220
221    /// Cursor for pagination.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub page_info: Option<String>,
224
225    /// Comma-separated list of fields to include in the response.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub fields: Option<String>,
228}
229
230/// Parameters for counting pages.
231#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
232pub struct PageCountParams {
233    /// Filter by page title.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub title: Option<String>,
236
237    /// Filter by published status.
238    /// Valid values: `published`, `unpublished`, `any`.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub published_status: Option<String>,
241
242    /// Show pages created after this date.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub created_at_min: Option<DateTime<Utc>>,
245
246    /// Show pages created before this date.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub created_at_max: Option<DateTime<Utc>>,
249
250    /// Show pages updated after this date.
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub updated_at_min: Option<DateTime<Utc>>,
253
254    /// Show pages updated before this date.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub updated_at_max: Option<DateTime<Utc>>,
257
258    /// Show pages published after this date.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub published_at_min: Option<DateTime<Utc>>,
261
262    /// Show pages published before this date.
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub published_at_max: Option<DateTime<Utc>>,
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::rest::{get_path, ResourceOperation};
271
272    #[test]
273    fn test_page_struct_serialization() {
274        let page = Page {
275            id: Some(12345),
276            shop_id: Some(67890),
277            title: Some("About Us".to_string()),
278            handle: Some("about-us".to_string()),
279            body_html: Some("<p>Welcome to our store!</p>".to_string()),
280            author: Some("Store Admin".to_string()),
281            template_suffix: Some("contact".to_string()),
282            published_at: Some(
283                DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
284                    .unwrap()
285                    .with_timezone(&Utc),
286            ),
287            created_at: Some(
288                DateTime::parse_from_rfc3339("2024-01-10T08:00:00Z")
289                    .unwrap()
290                    .with_timezone(&Utc),
291            ),
292            updated_at: Some(
293                DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
294                    .unwrap()
295                    .with_timezone(&Utc),
296            ),
297            admin_graphql_api_id: Some("gid://shopify/OnlineStorePage/12345".to_string()),
298        };
299
300        let json = serde_json::to_string(&page).unwrap();
301        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
302
303        // Writable fields should be present
304        assert_eq!(parsed["title"], "About Us");
305        assert_eq!(parsed["handle"], "about-us");
306        assert_eq!(parsed["body_html"], "<p>Welcome to our store!</p>");
307        assert_eq!(parsed["author"], "Store Admin");
308        assert_eq!(parsed["template_suffix"], "contact");
309        assert!(parsed["published_at"].as_str().is_some());
310
311        // Read-only fields should be omitted
312        assert!(parsed.get("id").is_none());
313        assert!(parsed.get("shop_id").is_none());
314        assert!(parsed.get("created_at").is_none());
315        assert!(parsed.get("updated_at").is_none());
316        assert!(parsed.get("admin_graphql_api_id").is_none());
317    }
318
319    #[test]
320    fn test_page_deserialization_from_api_response() {
321        let json = r#"{
322            "id": 131092082,
323            "shop_id": 548380009,
324            "title": "About Us",
325            "handle": "about-us",
326            "body_html": "<p>Welcome to our store!</p>",
327            "author": "Store Admin",
328            "template_suffix": null,
329            "published_at": "2024-01-15T10:30:00Z",
330            "created_at": "2024-01-10T08:00:00Z",
331            "updated_at": "2024-06-20T15:45:00Z",
332            "admin_graphql_api_id": "gid://shopify/OnlineStorePage/131092082"
333        }"#;
334
335        let page: Page = serde_json::from_str(json).unwrap();
336
337        assert_eq!(page.id, Some(131092082));
338        assert_eq!(page.shop_id, Some(548380009));
339        assert_eq!(page.title, Some("About Us".to_string()));
340        assert_eq!(page.handle, Some("about-us".to_string()));
341        assert_eq!(
342            page.body_html,
343            Some("<p>Welcome to our store!</p>".to_string())
344        );
345        assert_eq!(page.author, Some("Store Admin".to_string()));
346        assert!(page.template_suffix.is_none());
347        assert!(page.published_at.is_some());
348        assert!(page.created_at.is_some());
349        assert!(page.updated_at.is_some());
350        assert_eq!(
351            page.admin_graphql_api_id,
352            Some("gid://shopify/OnlineStorePage/131092082".to_string())
353        );
354    }
355
356    #[test]
357    fn test_page_list_params_serialization() {
358        let params = PageListParams {
359            title: Some("Contact".to_string()),
360            handle: Some("contact".to_string()),
361            published_status: Some("published".to_string()),
362            limit: Some(50),
363            since_id: Some(100),
364            ..Default::default()
365        };
366
367        let json = serde_json::to_value(&params).unwrap();
368
369        assert_eq!(json["title"], "Contact");
370        assert_eq!(json["handle"], "contact");
371        assert_eq!(json["published_status"], "published");
372        assert_eq!(json["limit"], 50);
373        assert_eq!(json["since_id"], 100);
374
375        // Fields not set should be omitted
376        assert!(json.get("created_at_min").is_none());
377        assert!(json.get("page_info").is_none());
378    }
379
380    #[test]
381    fn test_page_count_params_serialization() {
382        let params = PageCountParams {
383            title: Some("About".to_string()),
384            published_status: Some("any".to_string()),
385            ..Default::default()
386        };
387
388        let json = serde_json::to_value(&params).unwrap();
389
390        assert_eq!(json["title"], "About");
391        assert_eq!(json["published_status"], "any");
392
393        // Test empty params
394        let empty_params = PageCountParams::default();
395        let empty_json = serde_json::to_value(&empty_params).unwrap();
396        assert_eq!(empty_json, serde_json::json!({}));
397    }
398
399    #[test]
400    fn test_page_path_constants_are_correct() {
401        // Test Find path
402        let find_path = get_path(Page::PATHS, ResourceOperation::Find, &["id"]);
403        assert!(find_path.is_some());
404        assert_eq!(find_path.unwrap().template, "pages/{id}");
405        assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
406
407        // Test All path
408        let all_path = get_path(Page::PATHS, ResourceOperation::All, &[]);
409        assert!(all_path.is_some());
410        assert_eq!(all_path.unwrap().template, "pages");
411        assert_eq!(all_path.unwrap().http_method, HttpMethod::Get);
412
413        // Test Count path
414        let count_path = get_path(Page::PATHS, ResourceOperation::Count, &[]);
415        assert!(count_path.is_some());
416        assert_eq!(count_path.unwrap().template, "pages/count");
417        assert_eq!(count_path.unwrap().http_method, HttpMethod::Get);
418
419        // Test Create path
420        let create_path = get_path(Page::PATHS, ResourceOperation::Create, &[]);
421        assert!(create_path.is_some());
422        assert_eq!(create_path.unwrap().template, "pages");
423        assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
424
425        // Test Update path
426        let update_path = get_path(Page::PATHS, ResourceOperation::Update, &["id"]);
427        assert!(update_path.is_some());
428        assert_eq!(update_path.unwrap().template, "pages/{id}");
429        assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
430
431        // Test Delete path
432        let delete_path = get_path(Page::PATHS, ResourceOperation::Delete, &["id"]);
433        assert!(delete_path.is_some());
434        assert_eq!(delete_path.unwrap().template, "pages/{id}");
435        assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
436
437        // Verify constants
438        assert_eq!(Page::NAME, "Page");
439        assert_eq!(Page::PLURAL, "pages");
440    }
441
442    #[test]
443    fn test_page_get_id_returns_correct_value() {
444        // Page with ID
445        let page_with_id = Page {
446            id: Some(123456789),
447            title: Some("Test Page".to_string()),
448            ..Default::default()
449        };
450        assert_eq!(page_with_id.get_id(), Some(123456789));
451
452        // Page without ID (new page)
453        let page_without_id = Page {
454            id: None,
455            title: Some("New Page".to_string()),
456            ..Default::default()
457        };
458        assert_eq!(page_without_id.get_id(), None);
459    }
460
461    #[test]
462    fn test_page_published_at_for_scheduling() {
463        // A page can be scheduled for future publication
464        let future_date = DateTime::parse_from_rfc3339("2025-12-31T23:59:59Z")
465            .unwrap()
466            .with_timezone(&Utc);
467
468        let page = Page {
469            title: Some("Upcoming Sale".to_string()),
470            body_html: Some("<p>Coming soon!</p>".to_string()),
471            published_at: Some(future_date),
472            ..Default::default()
473        };
474
475        let json = serde_json::to_value(&page).unwrap();
476        assert!(json["published_at"].as_str().is_some());
477        assert!(json["published_at"]
478            .as_str()
479            .unwrap()
480            .contains("2025-12-31"));
481    }
482}