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