Skip to main content

shopify_sdk/rest/resources/v2025_10/
access_scope.rs

1//! AccessScope resource implementation.
2//!
3//! This module provides the [`AccessScope`] resource for retrieving the access
4//! scopes associated with the current access token.
5//!
6//! # Read-Only Resource
7//!
8//! AccessScopes implement [`ReadOnlyResource`](crate::rest::ReadOnlyResource) - they
9//! can only be listed, not created, updated, or deleted through the API.
10//! Access scopes are determined during OAuth authorization.
11//!
12//! # Special OAuth Endpoint
13//!
14//! This resource uses the `/admin/oauth/access_scopes.json` endpoint, which is
15//! different from the standard `/admin/api/{version}/` prefix used by other resources.
16//!
17//! # Example
18//!
19//! ```rust,ignore
20//! use shopify_sdk::rest::resources::v2025_10::AccessScope;
21//!
22//! // List all access scopes for the current token
23//! let scopes = AccessScope::all(&client).await?;
24//! for scope in scopes.iter() {
25//!     println!("Scope: {}", scope.handle.as_deref().unwrap_or(""));
26//! }
27//! ```
28
29use serde::{Deserialize, Serialize};
30
31use crate::clients::RestClient;
32use crate::rest::{ReadOnlyResource, ResourceError, ResourceResponse, RestResource, ResourceOperation, ResourcePath};
33use crate::HttpMethod;
34
35/// An OAuth access scope associated with an access token.
36///
37/// Access scopes define what permissions the current access token has.
38/// They are read-only and determined during the OAuth authorization process.
39///
40/// # Read-Only Resource
41///
42/// This resource implements [`ReadOnlyResource`] - only GET operations are
43/// available. Access scopes cannot be modified through the API.
44///
45/// # Special Endpoint
46///
47/// This resource uses `oauth/access_scopes` instead of the standard
48/// API-versioned endpoint.
49///
50/// # Fields
51///
52/// - `handle` - The scope identifier (e.g., "read_products", "write_orders")
53#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
54pub struct AccessScope {
55    /// The scope identifier (e.g., "read_products", "write_orders").
56    #[serde(skip_serializing)]
57    pub handle: Option<String>,
58}
59
60impl AccessScope {
61    /// Lists all access scopes for the current access token.
62    ///
63    /// This uses the special `/admin/oauth/access_scopes.json` endpoint.
64    ///
65    /// # Example
66    ///
67    /// ```rust,ignore
68    /// let scopes = AccessScope::all(&client).await?;
69    /// for scope in &scopes {
70    ///     println!("Has scope: {}", scope.handle.as_deref().unwrap_or(""));
71    /// }
72    /// ```
73    pub async fn all(client: &RestClient) -> Result<ResourceResponse<Vec<Self>>, ResourceError> {
74        // AccessScopes uses a special OAuth endpoint, not the standard API-versioned path
75        let url = "oauth/access_scopes";
76        let response = client.get(url, None).await?;
77
78        if !response.is_ok() {
79            return Err(ResourceError::from_http_response(
80                response.code,
81                &response.body,
82                Self::NAME,
83                None,
84                response.request_id(),
85            ));
86        }
87
88        let key = Self::PLURAL;
89        ResourceResponse::from_http_response(response, key)
90    }
91}
92
93impl RestResource for AccessScope {
94    type Id = String;
95    type FindParams = ();
96    type AllParams = ();
97    type CountParams = ();
98
99    const NAME: &'static str = "AccessScope";
100    const PLURAL: &'static str = "access_scopes";
101
102    /// Paths for the AccessScope resource.
103    ///
104    /// Note: The actual endpoint is `oauth/access_scopes`, not the standard
105    /// API-versioned path. Use the `all()` method instead of the trait method.
106    const PATHS: &'static [ResourcePath] = &[
107        // Special path - uses oauth prefix instead of api version
108        ResourcePath::new(
109            HttpMethod::Get,
110            ResourceOperation::All,
111            &[],
112            "oauth/access_scopes",
113        ),
114        // Note: No Find, Count, Create, Update, or Delete - list only
115    ];
116
117    fn get_id(&self) -> Option<Self::Id> {
118        self.handle.clone()
119    }
120}
121
122impl ReadOnlyResource for AccessScope {}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::rest::{get_path, ReadOnlyResource, ResourceOperation, RestResource};
128
129    #[test]
130    fn test_access_scope_implements_read_only_resource() {
131        fn assert_read_only<T: ReadOnlyResource>() {}
132        assert_read_only::<AccessScope>();
133    }
134
135    #[test]
136    fn test_access_scope_deserialization() {
137        let json = r#"{
138            "handle": "read_products"
139        }"#;
140
141        let scope: AccessScope = serde_json::from_str(json).unwrap();
142
143        assert_eq!(scope.handle, Some("read_products".to_string()));
144    }
145
146    #[test]
147    fn test_access_scope_list_deserialization() {
148        let json = r#"[
149            {"handle": "read_products"},
150            {"handle": "write_products"},
151            {"handle": "read_orders"}
152        ]"#;
153
154        let scopes: Vec<AccessScope> = serde_json::from_str(json).unwrap();
155
156        assert_eq!(scopes.len(), 3);
157        assert_eq!(scopes[0].handle, Some("read_products".to_string()));
158        assert_eq!(scopes[1].handle, Some("write_products".to_string()));
159        assert_eq!(scopes[2].handle, Some("read_orders".to_string()));
160    }
161
162    #[test]
163    fn test_access_scope_special_oauth_path() {
164        // All path uses oauth prefix
165        let all_path = get_path(AccessScope::PATHS, ResourceOperation::All, &[]);
166        assert!(all_path.is_some());
167        assert_eq!(all_path.unwrap().template, "oauth/access_scopes");
168    }
169
170    #[test]
171    fn test_access_scope_list_only_no_other_operations() {
172        // No Find path
173        let find_path = get_path(AccessScope::PATHS, ResourceOperation::Find, &["id"]);
174        assert!(find_path.is_none());
175
176        // No Count path
177        let count_path = get_path(AccessScope::PATHS, ResourceOperation::Count, &[]);
178        assert!(count_path.is_none());
179
180        // No Create path
181        let create_path = get_path(AccessScope::PATHS, ResourceOperation::Create, &[]);
182        assert!(create_path.is_none());
183
184        // No Update path
185        let update_path = get_path(AccessScope::PATHS, ResourceOperation::Update, &["id"]);
186        assert!(update_path.is_none());
187
188        // No Delete path
189        let delete_path = get_path(AccessScope::PATHS, ResourceOperation::Delete, &["id"]);
190        assert!(delete_path.is_none());
191    }
192
193    #[test]
194    fn test_access_scope_constants() {
195        assert_eq!(AccessScope::NAME, "AccessScope");
196        assert_eq!(AccessScope::PLURAL, "access_scopes");
197    }
198
199    #[test]
200    fn test_access_scope_get_id_returns_handle() {
201        let scope_with_handle = AccessScope {
202            handle: Some("read_products".to_string()),
203        };
204        assert_eq!(scope_with_handle.get_id(), Some("read_products".to_string()));
205
206        let scope_without_handle = AccessScope::default();
207        assert_eq!(scope_without_handle.get_id(), None);
208    }
209
210    #[test]
211    fn test_access_scope_all_fields_are_read_only() {
212        // All fields should be skipped during serialization
213        let scope = AccessScope {
214            handle: Some("write_orders".to_string()),
215        };
216
217        let json = serde_json::to_value(&scope).unwrap();
218        // All fields should be omitted (empty object)
219        assert_eq!(json, serde_json::json!({}));
220    }
221}