Skip to main content

shopify_sdk/rest/resources/v2025_10/
theme.rs

1//! Theme resource implementation.
2//!
3//! This module provides the Theme resource, which represents a theme in a Shopify store.
4//! Themes define the look and feel of an online store.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use shopify_sdk::rest::{RestResource, ResourceResponse};
10//! use shopify_sdk::rest::resources::v2025_10::{Theme, ThemeListParams, ThemeRole};
11//!
12//! // List all themes
13//! let themes = Theme::all(&client, None).await?;
14//! for theme in themes.iter() {
15//!     println!("Theme: {} ({})", theme.name.as_deref().unwrap_or(""),
16//!         theme.role.map(|r| format!("{:?}", r)).unwrap_or_default());
17//! }
18//!
19//! // Find a specific theme
20//! let theme = Theme::find(&client, 123, None).await?;
21//! println!("Theme: {}", theme.name.as_deref().unwrap_or(""));
22//!
23//! // Create a new theme
24//! let mut theme = Theme {
25//!     name: Some("My Custom Theme".to_string()),
26//!     role: Some(ThemeRole::Unpublished),
27//!     ..Default::default()
28//! };
29//! let saved = theme.save(&client).await?;
30//! ```
31
32use chrono::{DateTime, Utc};
33use serde::{Deserialize, Serialize};
34
35use crate::rest::{ResourceOperation, ResourcePath, RestResource};
36use crate::HttpMethod;
37
38// Re-export ThemeRole from common
39pub use super::common::ThemeRole;
40
41/// A theme in a Shopify store.
42///
43/// Themes define the look and feel of an online store, including the
44/// layout, colors, and typography. A store can have multiple themes,
45/// but only one can be published (main) at a time.
46///
47/// # Fields
48///
49/// ## Read-Only Fields
50/// - `id` - The unique identifier of the theme
51/// - `created_at` - When the theme was created
52/// - `updated_at` - When the theme was last updated
53/// - `admin_graphql_api_id` - The GraphQL API ID
54///
55/// ## Writable Fields
56/// - `name` - The name of the theme
57/// - `role` - The role of the theme (Main, Unpublished, Demo, Development)
58///
59/// ## Status Fields (Read-Only)
60/// - `previewable` - Whether the theme can be previewed
61/// - `processing` - Whether the theme is being processed
62#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
63pub struct Theme {
64    /// The unique identifier of the theme.
65    /// Read-only field.
66    #[serde(skip_serializing)]
67    pub id: Option<u64>,
68
69    /// The name of the theme.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub name: Option<String>,
72
73    /// The role of the theme in the store.
74    /// Determines whether the theme is published, in development, etc.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub role: Option<ThemeRole>,
77
78    /// Whether the theme can be previewed.
79    /// Read-only field.
80    #[serde(skip_serializing)]
81    pub previewable: Option<bool>,
82
83    /// Whether the theme is currently being processed.
84    /// Read-only field.
85    #[serde(skip_serializing)]
86    pub processing: Option<bool>,
87
88    /// When the theme was created.
89    /// Read-only field.
90    #[serde(skip_serializing)]
91    pub created_at: Option<DateTime<Utc>>,
92
93    /// When the theme was last updated.
94    /// Read-only field.
95    #[serde(skip_serializing)]
96    pub updated_at: Option<DateTime<Utc>>,
97
98    /// The admin GraphQL API ID for this theme.
99    /// Read-only field.
100    #[serde(skip_serializing)]
101    pub admin_graphql_api_id: Option<String>,
102}
103
104impl RestResource for Theme {
105    type Id = u64;
106    type FindParams = ThemeFindParams;
107    type AllParams = ThemeListParams;
108    type CountParams = ();
109
110    const NAME: &'static str = "Theme";
111    const PLURAL: &'static str = "themes";
112
113    /// Paths for the Theme resource.
114    ///
115    /// The Theme resource supports standard CRUD operations.
116    /// Note: There is no count endpoint for themes.
117    const PATHS: &'static [ResourcePath] = &[
118        ResourcePath::new(
119            HttpMethod::Get,
120            ResourceOperation::Find,
121            &["id"],
122            "themes/{id}",
123        ),
124        ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "themes"),
125        ResourcePath::new(HttpMethod::Post, ResourceOperation::Create, &[], "themes"),
126        ResourcePath::new(
127            HttpMethod::Put,
128            ResourceOperation::Update,
129            &["id"],
130            "themes/{id}",
131        ),
132        ResourcePath::new(
133            HttpMethod::Delete,
134            ResourceOperation::Delete,
135            &["id"],
136            "themes/{id}",
137        ),
138    ];
139
140    fn get_id(&self) -> Option<Self::Id> {
141        self.id
142    }
143}
144
145/// Parameters for finding a single theme.
146#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
147pub struct ThemeFindParams {
148    /// Comma-separated list of fields to include in the response.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub fields: Option<String>,
151}
152
153/// Parameters for listing themes.
154#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
155pub struct ThemeListParams {
156    /// Filter by theme role.
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub role: Option<ThemeRole>,
159
160    /// Maximum number of results to return.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub limit: Option<u32>,
163
164    /// Return themes after this ID.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub since_id: Option<u64>,
167
168    /// Cursor for pagination.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub page_info: Option<String>,
171
172    /// Comma-separated list of fields to include in the response.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub fields: Option<String>,
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::rest::{get_path, ResourceOperation};
181
182    #[test]
183    fn test_theme_struct_serialization() {
184        let theme = Theme {
185            id: Some(12345),
186            name: Some("My Custom Theme".to_string()),
187            role: Some(ThemeRole::Unpublished),
188            previewable: Some(true),
189            processing: Some(false),
190            created_at: Some(
191                DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
192                    .unwrap()
193                    .with_timezone(&Utc),
194            ),
195            updated_at: Some(
196                DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
197                    .unwrap()
198                    .with_timezone(&Utc),
199            ),
200            admin_graphql_api_id: Some("gid://shopify/Theme/12345".to_string()),
201        };
202
203        let json = serde_json::to_string(&theme).unwrap();
204        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
205
206        // Writable fields should be present
207        assert_eq!(parsed["name"], "My Custom Theme");
208        assert_eq!(parsed["role"], "unpublished");
209
210        // Read-only fields should be omitted
211        assert!(parsed.get("id").is_none());
212        assert!(parsed.get("previewable").is_none());
213        assert!(parsed.get("processing").is_none());
214        assert!(parsed.get("created_at").is_none());
215        assert!(parsed.get("updated_at").is_none());
216        assert!(parsed.get("admin_graphql_api_id").is_none());
217    }
218
219    #[test]
220    fn test_theme_deserialization_from_api_response() {
221        let json = r#"{
222            "id": 828155753,
223            "name": "Dawn",
224            "role": "main",
225            "previewable": true,
226            "processing": false,
227            "created_at": "2024-01-15T10:30:00Z",
228            "updated_at": "2024-06-20T15:45:00Z",
229            "admin_graphql_api_id": "gid://shopify/Theme/828155753"
230        }"#;
231
232        let theme: Theme = serde_json::from_str(json).unwrap();
233
234        assert_eq!(theme.id, Some(828155753));
235        assert_eq!(theme.name, Some("Dawn".to_string()));
236        assert_eq!(theme.role, Some(ThemeRole::Main));
237        assert_eq!(theme.previewable, Some(true));
238        assert_eq!(theme.processing, Some(false));
239        assert!(theme.created_at.is_some());
240        assert!(theme.updated_at.is_some());
241        assert_eq!(
242            theme.admin_graphql_api_id,
243            Some("gid://shopify/Theme/828155753".to_string())
244        );
245    }
246
247    #[test]
248    fn test_theme_role_enum_variants() {
249        // Test all role variants deserialize correctly
250        let main: ThemeRole = serde_json::from_str("\"main\"").unwrap();
251        assert_eq!(main, ThemeRole::Main);
252
253        let unpublished: ThemeRole = serde_json::from_str("\"unpublished\"").unwrap();
254        assert_eq!(unpublished, ThemeRole::Unpublished);
255
256        let demo: ThemeRole = serde_json::from_str("\"demo\"").unwrap();
257        assert_eq!(demo, ThemeRole::Demo);
258
259        let development: ThemeRole = serde_json::from_str("\"development\"").unwrap();
260        assert_eq!(development, ThemeRole::Development);
261    }
262
263    #[test]
264    fn test_theme_paths() {
265        // Find path
266        let find_path = get_path(Theme::PATHS, ResourceOperation::Find, &["id"]);
267        assert!(find_path.is_some());
268        assert_eq!(find_path.unwrap().template, "themes/{id}");
269
270        // All path
271        let all_path = get_path(Theme::PATHS, ResourceOperation::All, &[]);
272        assert!(all_path.is_some());
273        assert_eq!(all_path.unwrap().template, "themes");
274
275        // Create path
276        let create_path = get_path(Theme::PATHS, ResourceOperation::Create, &[]);
277        assert!(create_path.is_some());
278        assert_eq!(create_path.unwrap().template, "themes");
279
280        // Update path
281        let update_path = get_path(Theme::PATHS, ResourceOperation::Update, &["id"]);
282        assert!(update_path.is_some());
283        assert_eq!(update_path.unwrap().template, "themes/{id}");
284
285        // Delete path
286        let delete_path = get_path(Theme::PATHS, ResourceOperation::Delete, &["id"]);
287        assert!(delete_path.is_some());
288        assert_eq!(delete_path.unwrap().template, "themes/{id}");
289
290        // No count path for themes
291        let count_path = get_path(Theme::PATHS, ResourceOperation::Count, &[]);
292        assert!(count_path.is_none());
293    }
294
295    #[test]
296    fn test_theme_list_params_serialization() {
297        let params = ThemeListParams {
298            role: Some(ThemeRole::Main),
299            limit: Some(50),
300            since_id: Some(12345),
301            page_info: None,
302            fields: Some("id,name,role".to_string()),
303        };
304
305        let json = serde_json::to_value(&params).unwrap();
306
307        assert_eq!(json["role"], "main");
308        assert_eq!(json["limit"], 50);
309        assert_eq!(json["since_id"], 12345);
310        assert_eq!(json["fields"], "id,name,role");
311        assert!(json.get("page_info").is_none());
312
313        // Test empty params
314        let empty_params = ThemeListParams::default();
315        let empty_json = serde_json::to_value(&empty_params).unwrap();
316        assert_eq!(empty_json, serde_json::json!({}));
317    }
318
319    #[test]
320    fn test_theme_get_id_returns_correct_value() {
321        // Theme with ID
322        let theme_with_id = Theme {
323            id: Some(123456789),
324            name: Some("Test Theme".to_string()),
325            ..Default::default()
326        };
327        assert_eq!(theme_with_id.get_id(), Some(123456789));
328
329        // Theme without ID (new theme)
330        let theme_without_id = Theme {
331            id: None,
332            name: Some("New Theme".to_string()),
333            ..Default::default()
334        };
335        assert_eq!(theme_without_id.get_id(), None);
336    }
337
338    #[test]
339    fn test_theme_constants() {
340        assert_eq!(Theme::NAME, "Theme");
341        assert_eq!(Theme::PLURAL, "themes");
342    }
343
344    #[test]
345    fn test_theme_status_fields() {
346        let json = r#"{
347            "id": 123,
348            "name": "Processing Theme",
349            "role": "unpublished",
350            "previewable": false,
351            "processing": true
352        }"#;
353
354        let theme: Theme = serde_json::from_str(json).unwrap();
355
356        assert_eq!(theme.previewable, Some(false));
357        assert_eq!(theme.processing, Some(true));
358
359        // When serialized, status fields should be omitted
360        let serialized = serde_json::to_value(&theme).unwrap();
361        assert!(serialized.get("previewable").is_none());
362        assert!(serialized.get("processing").is_none());
363    }
364}