1use crate::registry::PatternRegistry;
6use once_cell::sync::Lazy;
7
8pub static API_PATTERNS: Lazy<PatternRegistry> = Lazy::new(|| {
10 let mut registry = PatternRegistry::new();
11
12 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 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 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 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 registry
90 .register(
91 "storefront_api",
92 r"(?i)(storefront|StorefrontAccessToken|X-Shopify-Storefront-Access-Token)",
93 )
94 .unwrap();
95
96 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
116pub const GRAPHQL_KEYS: &[&str] = &[
118 "graphql_query",
119 "graphql_client",
120 "graphql_file",
121 "admin_api_graphql",
122];
123
124pub const REST_KEYS: &[&str] = &[
126 "rest_products",
127 "rest_orders",
128 "rest_customers",
129 "rest_generic",
130 "rest_client",
131];
132
133pub 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
142pub 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
152fn 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#[derive(Debug, Clone, Default)]
167pub struct ApiUsageStatus {
168 pub uses_graphql: bool,
170 pub uses_rest: bool,
172 pub deprecated_endpoints: Vec<String>,
174 pub uses_storefront: bool,
176}
177
178impl ApiUsageStatus {
179 pub fn is_compliant(&self) -> bool {
181 self.uses_graphql && !self.uses_rest && self.deprecated_endpoints.is_empty()
182 }
183
184 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}