1#![forbid(unsafe_code)]
55
56pub mod autoload;
57pub mod error;
58pub mod loader;
59pub mod manifest;
60pub mod registry;
61pub mod resource;
62pub mod route;
63
64pub use autoload::AddonAutoload;
66pub use error::{AddonLoaderError, AddonLoaderResult};
67pub use loader::AddonLoader;
68pub use manifest::AddonManifest;
69pub use registry::AddonRegistry;
70pub use resource::{
71 build_resource_routes, HttpMethod, ResourceAction, ResourceOptions, ResourceRoute,
72 ResourceRouteEntry,
73};
74pub use route::AddonRoute;
75
76#[cfg(test)]
77mod integration_tests {
78 use super::*;
79 use std::fs;
80
81 #[test]
91 fn test_end_to_end_lifecycle() {
92 let tmp = tempfile::tempdir().expect("create tempdir");
93 let addons_path = tmp.path().join("addons");
94
95 let operate_dir = addons_path.join("operate");
97 fs::create_dir_all(&operate_dir).unwrap();
98 fs::write(
99 operate_dir.join("Plugin.php"),
100 r#"<?php
101namespace addons\operate;
102use think\Addons;
103class Plugin extends Addons {
104 public $info = [
105 'name' => 'operate',
106 'title' => '运营管理',
107 'identifier' => 'operate.addon',
108 'icon' => 'fa-cog',
109 'author' => 'sz',
110 'version' => '1.0.0',
111 'admin' => 'operate/index/index',
112 'status' => 1,
113 ];
114 public function install() {}
115 public function uninstall() {}
116}
117"#,
118 )
119 .unwrap();
120
121 let controller_dir = operate_dir.join("controller");
123 fs::create_dir_all(&controller_dir).unwrap();
124 fs::write(controller_dir.join("Order.php"), "<?php // stub").unwrap();
125
126 let admin_dir = controller_dir.join("admin");
128 fs::create_dir_all(&admin_dir).unwrap();
129 fs::write(admin_dir.join("Order.php"), "<?php // stub").unwrap();
130
131 let disabled_dir = addons_path.join("disabled");
133 fs::create_dir_all(&disabled_dir).unwrap();
134 fs::write(
135 disabled_dir.join("Plugin.php"),
136 r#"
137public $info = [
138 'name' => 'disabled',
139 'status' => 0,
140];
141"#,
142 )
143 .unwrap();
144
145 let loader = AddonLoader::new(&addons_path);
147 assert_eq!(loader.count(), 0);
148
149 let errors = loader.register().unwrap();
151 assert!(errors.is_empty(), "register errors: {:?}", errors);
152 assert_eq!(loader.count(), 2);
153
154 assert!(loader.is_enabled("operate").unwrap());
156 assert!(!loader.is_enabled("disabled").unwrap());
157
158 let manifest = loader.get_manifest("operate").unwrap();
160 assert_eq!(manifest.name, "operate");
161 assert_eq!(manifest.title, "运营管理");
162 assert_eq!(manifest.version, "1.0.0");
163 assert!(manifest.is_enabled());
164
165 let route = loader.parse_route("/addons/operate/Order/index").unwrap();
167 assert_eq!(route.addon, "operate");
168 assert_eq!(route.controller, "Order");
169 assert_eq!(route.action, "index");
170 assert_eq!(route.controller_class, "addons\\operate\\controller\\Order");
171 assert!(route.controller_file.is_some());
172 assert!(route.controller_file.unwrap().exists());
173
174 let route2 = loader
176 .parse_route("/addons/operate/admin.Order/index")
177 .unwrap();
178 assert_eq!(route2.controller, "admin.Order");
179 assert_eq!(
180 route2.controller_class,
181 "addons\\operate\\controller\\admin\\Order"
182 );
183 assert!(route2.controller_file.is_some());
184
185 let result = loader.parse_route("/addons/disabled/Order/index");
187 assert!(matches!(result, Err(AddonLoaderError::AddonDisabled(_))));
188
189 let result = loader.parse_route("/addons/ghost/Order/index");
191 assert!(matches!(result, Err(AddonLoaderError::AddonNotFound(_))));
192 }
193
194 #[test]
196 fn test_mixed_enabled_disabled_addons() {
197 let tmp = tempfile::tempdir().unwrap();
198 let addons_path = tmp.path().join("addons");
199
200 for (name, status) in [
201 ("enabled1", 1),
202 ("enabled2", 1),
203 ("disabled1", 0),
204 ("disabled2", 0),
205 ] {
206 let dir = addons_path.join(name);
207 fs::create_dir_all(&dir).unwrap();
208 fs::write(
209 dir.join("Plugin.php"),
210 format!(
211 r#"
212public $info = [
213 'name' => '{}',
214 'status' => {},
215];
216"#,
217 name, status
218 ),
219 )
220 .unwrap();
221 }
222
223 let loader = AddonLoader::new(&addons_path);
224 loader.register().unwrap();
225
226 assert_eq!(loader.count(), 4);
227 assert_eq!(loader.enabled_addons().len(), 2);
228 assert_eq!(loader.disabled_addons().len(), 2);
229 assert_eq!(
230 loader.names(),
231 vec!["disabled1", "disabled2", "enabled1", "enabled2"]
232 );
233 }
234
235 #[test]
237 fn test_dynamic_status_toggle() {
238 let tmp = tempfile::tempdir().unwrap();
239 let addons_path = tmp.path().join("addons");
240 let dir = addons_path.join("operate");
241 fs::create_dir_all(&dir).unwrap();
242 fs::write(
243 dir.join("Plugin.php"),
244 r#"
245public $info = [
246 'name' => 'operate',
247 'status' => 1,
248];
249"#,
250 )
251 .unwrap();
252
253 let loader = AddonLoader::new(&addons_path);
254 loader.register().unwrap();
255
256 assert!(loader.is_enabled("operate").unwrap());
257
258 loader.registry().set_enabled("operate", false).unwrap();
260 assert!(!loader.is_enabled("operate").unwrap());
261
262 loader.registry().set_enabled("operate", true).unwrap();
264 assert!(loader.is_enabled("operate").unwrap());
265 }
266
267 #[test]
269 fn test_autoload_registry_collaboration() {
270 let tmp = tempfile::tempdir().unwrap();
271 let addons_path = tmp.path().join("addons");
272 let operate_dir = addons_path.join("operate");
273 fs::create_dir_all(&operate_dir).unwrap();
274 fs::write(operate_dir.join("Plugin.php"), "<?php // stub").unwrap();
275 fs::write(
276 operate_dir.join("Plugin.php"),
277 r#"
278public $info = [
279 'name' => 'operate',
280 'status' => 1,
281];
282"#,
283 )
284 .unwrap();
285
286 let controller_dir = operate_dir.join("controller");
287 fs::create_dir_all(&controller_dir).unwrap();
288 fs::write(controller_dir.join("Order.php"), "<?php // stub").unwrap();
289
290 let loader = AddonLoader::new(&addons_path);
291 loader.register().unwrap();
292
293 let path = loader
295 .resolve_class("addons\\operate\\controller\\Order")
296 .unwrap();
297 assert!(path.is_some());
298 assert!(path.unwrap().exists());
299
300 let plugin_path = loader.resolve_plugin("operate").unwrap();
302 assert!(plugin_path.is_some());
303 assert!(plugin_path.unwrap().exists());
304 }
305
306 #[test]
308 fn test_multiple_addons_scan() {
309 let tmp = tempfile::tempdir().unwrap();
310 let addons_path = tmp.path().join("addons");
311
312 for name in &["operate", "cashier", "food", "erp"] {
313 let dir = addons_path.join(name);
314 fs::create_dir_all(&dir).unwrap();
315 fs::write(
316 dir.join("Plugin.php"),
317 format!(
318 r#"
319public $info = [
320 'name' => '{}',
321 'status' => 1,
322];
323"#,
324 name
325 ),
326 )
327 .unwrap();
328 }
329
330 let loader = AddonLoader::new(&addons_path);
331 let errors = loader.register().unwrap();
332 assert!(errors.is_empty());
333 assert_eq!(loader.count(), 4);
334 assert_eq!(loader.names(), vec!["cashier", "erp", "food", "operate"]);
335 }
336
337 #[test]
339 fn test_partial_failure_continues_scan() {
340 let tmp = tempfile::tempdir().unwrap();
341 let addons_path = tmp.path().join("addons");
342
343 let good_dir = addons_path.join("good");
345 fs::create_dir_all(&good_dir).unwrap();
346 fs::write(
347 good_dir.join("Plugin.php"),
348 r#"
349public $info = [
350 'name' => 'good',
351 'status' => 1,
352];
353"#,
354 )
355 .unwrap();
356
357 let bad_dir = addons_path.join("bad");
359 fs::create_dir_all(&bad_dir).unwrap();
360 fs::write(bad_dir.join("Plugin.php"), "<?php class Plugin {}").unwrap();
361
362 let loader = AddonLoader::new(&addons_path);
363 let errors = loader.register().unwrap();
364
365 assert!(!errors.is_empty());
367 assert_eq!(loader.count(), 1);
369 assert!(loader.exists("good"));
370 assert!(!loader.exists("bad"));
371 }
372
373 #[test]
375 fn test_empty_addons_directory() {
376 let tmp = tempfile::tempdir().unwrap();
377 let addons_path = tmp.path().join("addons");
378 fs::create_dir_all(&addons_path).unwrap();
379
380 let loader = AddonLoader::new(&addons_path);
381 let errors = loader.register().unwrap();
382 assert!(errors.is_empty());
383 assert_eq!(loader.count(), 0);
384 }
385}