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