Skip to main content

gatekpr_patterns/
api.rs

1//! API-related patterns for Shopify API usage detection
2//!
3//! Includes patterns for GraphQL vs REST API usage, deprecated endpoints, and API best practices.
4
5use crate::registry::PatternRegistry;
6use once_cell::sync::Lazy;
7
8/// Pre-built API pattern registry
9pub static API_PATTERNS: Lazy<PatternRegistry> = Lazy::new(|| {
10    let mut registry = PatternRegistry::new();
11
12    // GraphQL patterns
13    registry
14        .register(
15            "graphql_query",
16            r"(?i)(query\s*\{|mutation\s*\{|gql`|graphql\()",
17        )
18        .unwrap();
19
20    registry
21        .register(
22            "graphql_client",
23            r"(?i)(GraphQL|AdminGraphqlClient|@shopify/shopify-api.*graphql|shopifyGraphQL)",
24        )
25        .unwrap();
26
27    registry.register("graphql_file", r"\.graphql$").unwrap();
28
29    registry
30        .register("admin_api_graphql", r"(?i)admin/api/\d{4}-\d{2}/graphql")
31        .unwrap();
32
33    // REST API patterns (deprecated for new apps)
34    registry
35        .register(
36            "rest_products",
37            r"(?i)/admin/(api/)?\d{4}-\d{2}/products\.json",
38        )
39        .unwrap();
40
41    registry
42        .register("rest_orders", r"(?i)/admin/(api/)?\d{4}-\d{2}/orders\.json")
43        .unwrap();
44
45    registry
46        .register(
47            "rest_customers",
48            r"(?i)/admin/(api/)?\d{4}-\d{2}/customers\.json",
49        )
50        .unwrap();
51
52    registry
53        .register("rest_generic", r"(?i)/admin/(api/)?\d{4}-\d{2}/\w+\.json")
54        .unwrap();
55
56    registry
57        .register(
58            "rest_client",
59            r"(?i)(shopify\.rest\.|RestClient|@shopify/shopify-api.*rest)",
60        )
61        .unwrap();
62
63    // Deprecated API versions (2021, 2022)
64    registry
65        .register("deprecated_api_2021", r"(?i)admin/api/2021-\d{2}")
66        .unwrap();
67
68    registry
69        .register("deprecated_api_2022", r"(?i)admin/api/2022-\d{2}")
70        .unwrap();
71
72    registry
73        .register("deprecated_api_2023_01", r"(?i)admin/api/2023-01")
74        .unwrap();
75
76    // Deprecated endpoints
77    registry
78        .register("deprecated_shop_json", r"(?i)/admin/shop\.json")
79        .unwrap();
80
81    registry
82        .register(
83            "deprecated_application_charges",
84            r"(?i)/admin/application_charges",
85        )
86        .unwrap();
87
88    // Storefront API
89    registry
90        .register(
91            "storefront_api",
92            r"(?i)(storefront|StorefrontAccessToken|X-Shopify-Storefront-Access-Token)",
93        )
94        .unwrap();
95
96    // API scope patterns
97    registry
98        .register("read_products_scope", r"(?i)read_products")
99        .unwrap();
100
101    registry
102        .register("write_products_scope", r"(?i)write_products")
103        .unwrap();
104
105    registry
106        .register("read_customers_scope", r"(?i)read_customers")
107        .unwrap();
108
109    registry
110        .register("write_customers_scope", r"(?i)write_customers")
111        .unwrap();
112
113    registry
114});
115
116/// Pattern keys for GraphQL usage
117pub const GRAPHQL_KEYS: &[&str] = &[
118    "graphql_query",
119    "graphql_client",
120    "graphql_file",
121    "admin_api_graphql",
122];
123
124/// Pattern keys for REST API usage (deprecated)
125pub const REST_KEYS: &[&str] = &[
126    "rest_products",
127    "rest_orders",
128    "rest_customers",
129    "rest_generic",
130    "rest_client",
131];
132
133/// Pattern keys for deprecated API versions/endpoints
134pub const DEPRECATED_KEYS: &[&str] = &[
135    "deprecated_api_2021",
136    "deprecated_api_2022",
137    "deprecated_api_2023_01",
138    "deprecated_shop_json",
139    "deprecated_application_charges",
140];
141
142/// Check API usage in code
143pub fn check_api_usage(text: &str) -> ApiUsageStatus {
144    ApiUsageStatus {
145        uses_graphql: API_PATTERNS.any_match(GRAPHQL_KEYS, text),
146        uses_rest: API_PATTERNS.any_match(REST_KEYS, text),
147        deprecated_endpoints: find_deprecated_endpoints(text),
148        uses_storefront: API_PATTERNS.is_match("storefront_api", text),
149    }
150}
151
152/// Find deprecated endpoints in code
153fn find_deprecated_endpoints(text: &str) -> Vec<String> {
154    let mut deprecated = Vec::new();
155
156    for key in DEPRECATED_KEYS {
157        if API_PATTERNS.is_match(key, text) {
158            deprecated.push(key.to_string());
159        }
160    }
161
162    deprecated
163}
164
165/// API usage status
166#[derive(Debug, Clone, Default)]
167pub struct ApiUsageStatus {
168    /// Whether GraphQL API is used
169    pub uses_graphql: bool,
170    /// Whether REST API is used (deprecated for new apps)
171    pub uses_rest: bool,
172    /// List of deprecated endpoints/versions found
173    pub deprecated_endpoints: Vec<String>,
174    /// Whether Storefront API is used
175    pub uses_storefront: bool,
176}
177
178impl ApiUsageStatus {
179    /// Check if the app is API compliant (uses GraphQL, no deprecated endpoints)
180    pub fn is_compliant(&self) -> bool {
181        self.uses_graphql && !self.uses_rest && self.deprecated_endpoints.is_empty()
182    }
183
184    /// Get compliance status string
185    pub fn status(&self) -> &'static str {
186        if self.uses_rest || !self.deprecated_endpoints.is_empty() {
187            "fail"
188        } else if self.uses_graphql {
189            "pass"
190        } else {
191            "warning"
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_graphql_detection() {
202        let code = r#"
203            const response = await client.query({
204                data: gql`query { shop { name } }`,
205            });
206        "#;
207
208        let status = check_api_usage(code);
209        assert!(status.uses_graphql);
210        assert!(!status.uses_rest);
211    }
212
213    #[test]
214    fn test_rest_detection() {
215        let code = r#"
216            const response = await fetch('/admin/api/2024-01/products.json');
217        "#;
218
219        let status = check_api_usage(code);
220        assert!(status.uses_rest);
221    }
222
223    #[test]
224    fn test_deprecated_api() {
225        let code = r#"
226            const response = await fetch('/admin/api/2022-01/products.json');
227        "#;
228
229        let status = check_api_usage(code);
230        assert!(!status.deprecated_endpoints.is_empty());
231        assert!(!status.is_compliant());
232    }
233
234    #[test]
235    fn test_compliant_code() {
236        let code = r#"
237            const client = new AdminGraphqlClient();
238            const result = await client.query({
239                data: gql`query { products { edges { node { id title } } } }`,
240            });
241        "#;
242
243        let status = check_api_usage(code);
244        assert!(status.is_compliant());
245    }
246}