Skip to main content

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