Skip to main content

shopify_sdk/rest/resources/v2025_10/
user.rs

1//! User resource implementation.
2//!
3//! This module provides the [`User`] resource for retrieving staff users
4//! in a Shopify store.
5//!
6//! # Read-Only Resource
7//!
8//! Users implement [`ReadOnlyResource`](crate::rest::ReadOnlyResource) - they
9//! can only be retrieved, not created, updated, or deleted through the API.
10//! Staff accounts are managed through the Shopify admin.
11//!
12//! # Special Operations
13//!
14//! - `User::current()` - Get the currently authenticated user
15//!
16//! # Example
17//!
18//! ```rust,ignore
19//! use shopify_sdk::rest::{RestResource, ResourceResponse};
20//! use shopify_sdk::rest::resources::v2025_10::{User, UserListParams};
21//!
22//! // Get the current authenticated user
23//! let current = User::current(&client).await?;
24//! println!("Logged in as: {} {}",
25//!     current.first_name.as_deref().unwrap_or(""),
26//!     current.last_name.as_deref().unwrap_or(""));
27//!
28//! // List all staff users
29//! let users = User::all(&client, None).await?;
30//! for user in users.iter() {
31//!     println!("{} {} <{}>",
32//!         user.first_name.as_deref().unwrap_or(""),
33//!         user.last_name.as_deref().unwrap_or(""),
34//!         user.email.as_deref().unwrap_or(""));
35//! }
36//! ```
37
38use serde::{Deserialize, Serialize};
39
40use crate::clients::RestClient;
41use crate::rest::{ReadOnlyResource, ResourceError, ResourceOperation, ResourcePath, ResourceResponse, RestResource};
42use crate::HttpMethod;
43
44/// A staff user in a Shopify store.
45///
46/// Users represent staff accounts that can access the Shopify admin.
47/// They are read-only through the API.
48///
49/// # Read-Only Resource
50///
51/// This resource implements [`ReadOnlyResource`] - only GET operations are
52/// available. User accounts are managed through the Shopify admin.
53///
54/// # Fields
55///
56/// All fields are read-only:
57/// - `id` - The unique identifier
58/// - `first_name` - User's first name
59/// - `last_name` - User's last name
60/// - `email` - User's email address
61/// - `phone` - User's phone number
62/// - `url` - User's Shopify admin URL
63/// - `bio` - User's biography
64/// - `im` - Instant messenger handle
65/// - `screen_name` - Screen name
66/// - `locale` - User's preferred locale
67/// - `user_type` - Type of user account
68/// - `account_owner` - Whether user owns the account
69/// - `receive_announcements` - Whether user receives announcements
70/// - `permissions` - Array of permission strings
71#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
72pub struct User {
73    /// The unique identifier of the user.
74    #[serde(skip_serializing)]
75    pub id: Option<u64>,
76
77    /// The user's first name.
78    #[serde(skip_serializing)]
79    pub first_name: Option<String>,
80
81    /// The user's last name.
82    #[serde(skip_serializing)]
83    pub last_name: Option<String>,
84
85    /// The user's email address.
86    #[serde(skip_serializing)]
87    pub email: Option<String>,
88
89    /// The user's phone number.
90    #[serde(skip_serializing)]
91    pub phone: Option<String>,
92
93    /// URL to the user's Shopify admin page.
94    #[serde(skip_serializing)]
95    pub url: Option<String>,
96
97    /// The user's biography.
98    #[serde(skip_serializing)]
99    pub bio: Option<String>,
100
101    /// The user's instant messenger handle.
102    #[serde(skip_serializing)]
103    pub im: Option<String>,
104
105    /// The user's screen name.
106    #[serde(skip_serializing)]
107    pub screen_name: Option<String>,
108
109    /// The user's preferred locale.
110    #[serde(skip_serializing)]
111    pub locale: Option<String>,
112
113    /// The type of user: "regular", "restricted", "invited".
114    #[serde(skip_serializing)]
115    pub user_type: Option<String>,
116
117    /// Whether this user owns the account.
118    #[serde(skip_serializing)]
119    pub account_owner: Option<bool>,
120
121    /// Whether the user receives announcements.
122    #[serde(skip_serializing)]
123    pub receive_announcements: Option<i32>,
124
125    /// The user's permissions.
126    #[serde(skip_serializing)]
127    pub permissions: Option<Vec<String>>,
128
129    /// The admin GraphQL API ID for this user.
130    #[serde(skip_serializing)]
131    pub admin_graphql_api_id: Option<String>,
132}
133
134impl User {
135    /// Gets the currently authenticated user.
136    ///
137    /// This returns information about the user whose access token
138    /// is being used for the request.
139    ///
140    /// # Example
141    ///
142    /// ```rust,ignore
143    /// let current = User::current(&client).await?;
144    /// println!("Current user: {} {}", current.first_name.unwrap_or(""), current.last_name.unwrap_or(""));
145    /// ```
146    pub async fn current(client: &RestClient) -> Result<ResourceResponse<Self>, ResourceError> {
147        let url = "users/current";
148        let response = client.get(url, None).await?;
149
150        if !response.is_ok() {
151            return Err(ResourceError::from_http_response(
152                response.code,
153                &response.body,
154                Self::NAME,
155                Some("current"),
156                response.request_id(),
157            ));
158        }
159
160        let key = Self::resource_key();
161        ResourceResponse::from_http_response(response, &key)
162    }
163}
164
165impl RestResource for User {
166    type Id = u64;
167    type FindParams = UserFindParams;
168    type AllParams = UserListParams;
169    type CountParams = ();
170
171    const NAME: &'static str = "User";
172    const PLURAL: &'static str = "users";
173
174    /// Paths for the User resource.
175    ///
176    /// Only GET operations - users are read-only.
177    /// No Count endpoint.
178    const PATHS: &'static [ResourcePath] = &[
179        ResourcePath::new(
180            HttpMethod::Get,
181            ResourceOperation::Find,
182            &["id"],
183            "users/{id}",
184        ),
185        ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "users"),
186        // Note: No Count path
187        // Note: No Create, Update, or Delete paths - read-only resource
188    ];
189
190    fn get_id(&self) -> Option<Self::Id> {
191        self.id
192    }
193}
194
195impl ReadOnlyResource for User {}
196
197/// Parameters for finding a single user.
198#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
199pub struct UserFindParams {
200    /// Comma-separated list of fields to include in the response.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub fields: Option<String>,
203}
204
205/// Parameters for listing users.
206#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
207pub struct UserListParams {
208    /// Maximum number of results to return.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub limit: Option<u32>,
211
212    /// Cursor for pagination.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub page_info: Option<String>,
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::rest::{get_path, ReadOnlyResource, ResourceOperation, RestResource};
221
222    #[test]
223    fn test_user_implements_read_only_resource() {
224        fn assert_read_only<T: ReadOnlyResource>() {}
225        assert_read_only::<User>();
226    }
227
228    #[test]
229    fn test_user_deserialization() {
230        let json = r#"{
231            "id": 548380009,
232            "first_name": "John",
233            "last_name": "Smith",
234            "email": "john@example.com",
235            "phone": "+1-555-0100",
236            "url": "https://store.myshopify.com/admin/users/548380009",
237            "bio": "Store manager",
238            "im": null,
239            "screen_name": null,
240            "locale": "en",
241            "user_type": "regular",
242            "account_owner": false,
243            "receive_announcements": 1,
244            "permissions": ["full"],
245            "admin_graphql_api_id": "gid://shopify/StaffMember/548380009"
246        }"#;
247
248        let user: User = serde_json::from_str(json).unwrap();
249
250        assert_eq!(user.id, Some(548380009));
251        assert_eq!(user.first_name, Some("John".to_string()));
252        assert_eq!(user.last_name, Some("Smith".to_string()));
253        assert_eq!(user.email, Some("john@example.com".to_string()));
254        assert_eq!(user.phone, Some("+1-555-0100".to_string()));
255        assert_eq!(user.locale, Some("en".to_string()));
256        assert_eq!(user.user_type, Some("regular".to_string()));
257        assert_eq!(user.account_owner, Some(false));
258        assert_eq!(user.receive_announcements, Some(1));
259        assert_eq!(user.permissions, Some(vec!["full".to_string()]));
260    }
261
262    #[test]
263    fn test_user_read_only_paths() {
264        // Find
265        let find_path = get_path(User::PATHS, ResourceOperation::Find, &["id"]);
266        assert!(find_path.is_some());
267        assert_eq!(find_path.unwrap().template, "users/{id}");
268
269        // All
270        let all_path = get_path(User::PATHS, ResourceOperation::All, &[]);
271        assert!(all_path.is_some());
272        assert_eq!(all_path.unwrap().template, "users");
273
274        // No count path
275        let count_path = get_path(User::PATHS, ResourceOperation::Count, &[]);
276        assert!(count_path.is_none());
277
278        // No create, update, or delete paths
279        let create_path = get_path(User::PATHS, ResourceOperation::Create, &[]);
280        assert!(create_path.is_none());
281
282        let update_path = get_path(User::PATHS, ResourceOperation::Update, &["id"]);
283        assert!(update_path.is_none());
284
285        let delete_path = get_path(User::PATHS, ResourceOperation::Delete, &["id"]);
286        assert!(delete_path.is_none());
287    }
288
289    #[test]
290    fn test_user_current_method_exists() {
291        // The current() method is a static method that returns the current user
292        // We can't test the actual HTTP call, but we verify the method exists
293        // by checking the struct has the expected signature
294        let _: fn(&RestClient) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ResourceResponse<User>, ResourceError>> + Send + '_>> = |client| Box::pin(User::current(client));
295    }
296
297    #[test]
298    fn test_user_constants() {
299        assert_eq!(User::NAME, "User");
300        assert_eq!(User::PLURAL, "users");
301    }
302
303    #[test]
304    fn test_user_get_id() {
305        let user_with_id = User {
306            id: Some(548380009),
307            first_name: Some("John".to_string()),
308            ..Default::default()
309        };
310        assert_eq!(user_with_id.get_id(), Some(548380009));
311
312        let user_without_id = User::default();
313        assert_eq!(user_without_id.get_id(), None);
314    }
315
316    #[test]
317    fn test_user_all_fields_are_read_only() {
318        // All fields should be skipped during serialization
319        let user = User {
320            id: Some(548380009),
321            first_name: Some("John".to_string()),
322            last_name: Some("Smith".to_string()),
323            email: Some("john@example.com".to_string()),
324            phone: Some("+1-555-0100".to_string()),
325            locale: Some("en".to_string()),
326            user_type: Some("regular".to_string()),
327            account_owner: Some(true),
328            receive_announcements: Some(1),
329            permissions: Some(vec!["full".to_string()]),
330            ..Default::default()
331        };
332
333        let json = serde_json::to_value(&user).unwrap();
334        // All fields should be omitted (empty object)
335        assert_eq!(json, serde_json::json!({}));
336    }
337}