Skip to main content

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