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