1use std::fmt;
34
35use crate::error::{AddonLoaderError, AddonLoaderResult};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub enum ResourceAction {
40 Index,
42 Create,
44 Edit,
46 Read,
48 Save,
50 Update,
52 Delete,
54}
55
56impl ResourceAction {
57 pub fn as_str(&self) -> &'static str {
59 match self {
60 ResourceAction::Index => "index",
61 ResourceAction::Create => "create",
62 ResourceAction::Edit => "edit",
63 ResourceAction::Read => "read",
64 ResourceAction::Save => "save",
65 ResourceAction::Update => "update",
66 ResourceAction::Delete => "delete",
67 }
68 }
69
70 pub fn http_method(&self) -> HttpMethod {
72 match self {
73 ResourceAction::Index
74 | ResourceAction::Create
75 | ResourceAction::Edit
76 | ResourceAction::Read => HttpMethod::Get,
77 ResourceAction::Save => HttpMethod::Post,
78 ResourceAction::Update => HttpMethod::Put,
79 ResourceAction::Delete => HttpMethod::Delete,
80 }
81 }
82
83 pub fn url_suffix(&self) -> &'static str {
85 match self {
86 ResourceAction::Index | ResourceAction::Save => "",
87 ResourceAction::Create => "/create",
88 ResourceAction::Edit => "/<id>/edit",
89 ResourceAction::Read | ResourceAction::Update | ResourceAction::Delete => "/<id>",
90 }
91 }
92
93 pub fn controller_method(&self) -> &'static str {
95 self.as_str()
96 }
97
98 pub fn all() -> &'static [ResourceAction] {
100 &[
101 ResourceAction::Index,
102 ResourceAction::Create,
103 ResourceAction::Edit,
104 ResourceAction::Read,
105 ResourceAction::Save,
106 ResourceAction::Update,
107 ResourceAction::Delete,
108 ]
109 }
110
111 pub fn parse_name(name: &str) -> Option<ResourceAction> {
115 match name {
116 "index" => Some(ResourceAction::Index),
117 "create" => Some(ResourceAction::Create),
118 "edit" => Some(ResourceAction::Edit),
119 "read" => Some(ResourceAction::Read),
120 "save" => Some(ResourceAction::Save),
121 "update" => Some(ResourceAction::Update),
122 "delete" => Some(ResourceAction::Delete),
123 _ => None,
124 }
125 }
126}
127
128impl std::str::FromStr for ResourceAction {
130 type Err = String;
131
132 fn from_str(s: &str) -> Result<Self, Self::Err> {
133 Self::parse_name(s).ok_or_else(|| format!("unknown ResourceAction: {}", s))
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum HttpMethod {
140 Get,
142 Post,
144 Put,
146 Delete,
148 Patch,
150}
151
152impl HttpMethod {
153 pub fn as_str(&self) -> &'static str {
155 match self {
156 HttpMethod::Get => "GET",
157 HttpMethod::Post => "POST",
158 HttpMethod::Put => "PUT",
159 HttpMethod::Delete => "DELETE",
160 HttpMethod::Patch => "PATCH",
161 }
162 }
163}
164
165impl fmt::Display for HttpMethod {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 write!(f, "{}", self.as_str())
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct ResourceRouteEntry {
174 pub action: ResourceAction,
176 pub method: HttpMethod,
178 pub url: String,
180 pub controller_method: String,
182}
183
184#[derive(Debug, Clone, Default)]
186pub struct ResourceOptions {
187 pub only: Vec<ResourceAction>,
189 pub except: Vec<ResourceAction>,
191}
192
193impl ResourceOptions {
194 pub fn new() -> Self {
196 Self::default()
197 }
198
199 pub fn with_only(mut self, only: Vec<ResourceAction>) -> Self {
201 self.only = only;
202 self
203 }
204
205 pub fn with_except(mut self, except: Vec<ResourceAction>) -> Self {
207 self.except = except;
208 self
209 }
210
211 pub fn should_include(&self, action: ResourceAction) -> bool {
217 if !self.only.is_empty() && !self.only.contains(&action) {
218 return false;
219 }
220 if self.except.contains(&action) {
221 return false;
222 }
223 true
224 }
225}
226
227#[derive(Debug, Clone)]
229pub struct ResourceRoute {
230 pub addon: String,
232 pub controller: String,
234 pub base_url: String,
236 pub entries: Vec<ResourceRouteEntry>,
238}
239
240impl ResourceRoute {
241 pub fn get(&self, action: ResourceAction) -> Option<&ResourceRouteEntry> {
243 self.entries.iter().find(|e| e.action == action)
244 }
245
246 pub fn len(&self) -> usize {
248 self.entries.len()
249 }
250
251 pub fn is_empty(&self) -> bool {
253 self.entries.is_empty()
254 }
255}
256
257#[tracing::instrument]
291pub fn build_resource_routes(
292 addon: &str,
293 controller: &str,
294 options: &ResourceOptions,
295) -> AddonLoaderResult<ResourceRoute> {
296 if addon.is_empty() {
297 return Err(AddonLoaderError::RouteParse {
298 url: String::new(),
299 reason: "addon cannot be empty for resource route".to_string(),
300 });
301 }
302 if controller.is_empty() {
303 return Err(AddonLoaderError::RouteParse {
304 url: String::new(),
305 reason: "controller cannot be empty for resource route".to_string(),
306 });
307 }
308
309 let base_url = build_resource_base_url(addon, controller);
311
312 let mut entries = Vec::new();
314 for &action in ResourceAction::all() {
315 if !options.should_include(action) {
316 continue;
317 }
318 let url = format!("{}{}", base_url, action.url_suffix());
319 entries.push(ResourceRouteEntry {
320 action,
321 method: action.http_method(),
322 url,
323 controller_method: action.controller_method().to_string(),
324 });
325 }
326
327 Ok(ResourceRoute {
328 addon: addon.to_string(),
329 controller: controller.to_string(),
330 base_url,
331 entries,
332 })
333}
334
335fn build_resource_base_url(addon: &str, controller: &str) -> String {
357 if !controller.contains('.') {
358 return format!("/addons/{}/{}", addon, controller);
359 }
360
361 let mut parts: Vec<&str> = controller.split('.').collect();
363 let last = parts.pop().unwrap_or(controller);
364
365 let mut segments: Vec<String> = Vec::new();
367 for val in &parts {
368 segments.push(format!("{}/<{}_id>", val, val));
369 }
370 segments.push(last.to_string());
371
372 format!("/addons/{}/{}", addon, segments.join("/"))
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
382 fn test_resource_action_as_str() {
383 assert_eq!(ResourceAction::Index.as_str(), "index");
384 assert_eq!(ResourceAction::Create.as_str(), "create");
385 assert_eq!(ResourceAction::Edit.as_str(), "edit");
386 assert_eq!(ResourceAction::Read.as_str(), "read");
387 assert_eq!(ResourceAction::Save.as_str(), "save");
388 assert_eq!(ResourceAction::Update.as_str(), "update");
389 assert_eq!(ResourceAction::Delete.as_str(), "delete");
390 }
391
392 #[test]
393 fn test_resource_action_http_method() {
394 assert_eq!(ResourceAction::Index.http_method(), HttpMethod::Get);
395 assert_eq!(ResourceAction::Create.http_method(), HttpMethod::Get);
396 assert_eq!(ResourceAction::Edit.http_method(), HttpMethod::Get);
397 assert_eq!(ResourceAction::Read.http_method(), HttpMethod::Get);
398 assert_eq!(ResourceAction::Save.http_method(), HttpMethod::Post);
399 assert_eq!(ResourceAction::Update.http_method(), HttpMethod::Put);
400 assert_eq!(ResourceAction::Delete.http_method(), HttpMethod::Delete);
401 }
402
403 #[test]
404 fn test_resource_action_url_suffix() {
405 assert_eq!(ResourceAction::Index.url_suffix(), "");
406 assert_eq!(ResourceAction::Create.url_suffix(), "/create");
407 assert_eq!(ResourceAction::Edit.url_suffix(), "/<id>/edit");
408 assert_eq!(ResourceAction::Read.url_suffix(), "/<id>");
409 assert_eq!(ResourceAction::Save.url_suffix(), "");
410 assert_eq!(ResourceAction::Update.url_suffix(), "/<id>");
411 assert_eq!(ResourceAction::Delete.url_suffix(), "/<id>");
412 }
413
414 #[test]
415 fn test_resource_action_controller_method() {
416 assert_eq!(ResourceAction::Index.controller_method(), "index");
417 assert_eq!(ResourceAction::Save.controller_method(), "save");
418 assert_eq!(ResourceAction::Delete.controller_method(), "delete");
419 }
420
421 #[test]
422 fn test_resource_action_all_returns_seven() {
423 let all = ResourceAction::all();
424 assert_eq!(all.len(), 7);
425 assert_eq!(all[0], ResourceAction::Index);
426 assert_eq!(all[1], ResourceAction::Create);
427 assert_eq!(all[2], ResourceAction::Edit);
428 assert_eq!(all[3], ResourceAction::Read);
429 assert_eq!(all[4], ResourceAction::Save);
430 assert_eq!(all[5], ResourceAction::Update);
431 assert_eq!(all[6], ResourceAction::Delete);
432 }
433
434 #[test]
435 fn test_resource_action_parse_name_valid() {
436 assert_eq!(
437 ResourceAction::parse_name("index"),
438 Some(ResourceAction::Index)
439 );
440 assert_eq!(
441 ResourceAction::parse_name("create"),
442 Some(ResourceAction::Create)
443 );
444 assert_eq!(
445 ResourceAction::parse_name("edit"),
446 Some(ResourceAction::Edit)
447 );
448 assert_eq!(
449 ResourceAction::parse_name("read"),
450 Some(ResourceAction::Read)
451 );
452 assert_eq!(
453 ResourceAction::parse_name("save"),
454 Some(ResourceAction::Save)
455 );
456 assert_eq!(
457 ResourceAction::parse_name("update"),
458 Some(ResourceAction::Update)
459 );
460 assert_eq!(
461 ResourceAction::parse_name("delete"),
462 Some(ResourceAction::Delete)
463 );
464 }
465
466 #[test]
467 fn test_resource_action_parse_name_invalid() {
468 assert_eq!(ResourceAction::parse_name("unknown"), None);
469 assert_eq!(ResourceAction::parse_name(""), None);
470 assert_eq!(ResourceAction::parse_name("INDEX"), None);
471 }
472
473 #[test]
474 fn test_resource_action_from_str_trait() {
475 use std::str::FromStr;
477 assert_eq!(ResourceAction::from_str("index"), Ok(ResourceAction::Index));
478 assert_eq!(
479 ResourceAction::from_str("delete"),
480 Ok(ResourceAction::Delete)
481 );
482 assert!(ResourceAction::from_str("unknown").is_err());
483 assert!(ResourceAction::from_str("").is_err());
484 }
485
486 #[test]
489 fn test_http_method_as_str() {
490 assert_eq!(HttpMethod::Get.as_str(), "GET");
491 assert_eq!(HttpMethod::Post.as_str(), "POST");
492 assert_eq!(HttpMethod::Put.as_str(), "PUT");
493 assert_eq!(HttpMethod::Delete.as_str(), "DELETE");
494 assert_eq!(HttpMethod::Patch.as_str(), "PATCH");
495 }
496
497 #[test]
498 fn test_http_method_display() {
499 assert_eq!(format!("{}", HttpMethod::Get), "GET");
500 assert_eq!(format!("{}", HttpMethod::Post), "POST");
501 assert_eq!(format!("{}", HttpMethod::Put), "PUT");
502 assert_eq!(format!("{}", HttpMethod::Delete), "DELETE");
503 assert_eq!(format!("{}", HttpMethod::Patch), "PATCH");
504 }
505
506 #[test]
509 fn test_resource_options_new() {
510 let opts = ResourceOptions::new();
511 assert!(opts.only.is_empty());
512 assert!(opts.except.is_empty());
513 }
514
515 #[test]
516 fn test_resource_options_with_only() {
517 let opts =
518 ResourceOptions::new().with_only(vec![ResourceAction::Index, ResourceAction::Read]);
519 assert_eq!(opts.only.len(), 2);
520 assert!(opts.except.is_empty());
521 }
522
523 #[test]
524 fn test_resource_options_with_except() {
525 let opts = ResourceOptions::new().with_except(vec![ResourceAction::Delete]);
526 assert!(opts.only.is_empty());
527 assert_eq!(opts.except.len(), 1);
528 }
529
530 #[test]
531 fn test_resource_options_with_only_and_except() {
532 let opts = ResourceOptions::new()
533 .with_only(vec![
534 ResourceAction::Index,
535 ResourceAction::Read,
536 ResourceAction::Delete,
537 ])
538 .with_except(vec![ResourceAction::Delete]);
539 assert_eq!(opts.only.len(), 3);
540 assert_eq!(opts.except.len(), 1);
541 }
542
543 #[test]
544 fn test_should_include_no_filter() {
545 let opts = ResourceOptions::new();
546 for &action in ResourceAction::all() {
547 assert!(
548 opts.should_include(action),
549 "{:?} should be included",
550 action
551 );
552 }
553 }
554
555 #[test]
556 fn test_should_include_only_filter() {
557 let opts =
558 ResourceOptions::new().with_only(vec![ResourceAction::Index, ResourceAction::Read]);
559 assert!(opts.should_include(ResourceAction::Index));
560 assert!(opts.should_include(ResourceAction::Read));
561 assert!(!opts.should_include(ResourceAction::Create));
562 assert!(!opts.should_include(ResourceAction::Edit));
563 assert!(!opts.should_include(ResourceAction::Save));
564 assert!(!opts.should_include(ResourceAction::Update));
565 assert!(!opts.should_include(ResourceAction::Delete));
566 }
567
568 #[test]
569 fn test_should_include_except_filter() {
570 let opts = ResourceOptions::new()
571 .with_except(vec![ResourceAction::Delete, ResourceAction::Create]);
572 assert!(opts.should_include(ResourceAction::Index));
573 assert!(!opts.should_include(ResourceAction::Create));
574 assert!(opts.should_include(ResourceAction::Edit));
575 assert!(opts.should_include(ResourceAction::Read));
576 assert!(opts.should_include(ResourceAction::Save));
577 assert!(opts.should_include(ResourceAction::Update));
578 assert!(!opts.should_include(ResourceAction::Delete));
579 }
580
581 #[test]
582 fn test_should_include_only_and_except_combined() {
583 let opts = ResourceOptions::new()
585 .with_only(vec![
586 ResourceAction::Index,
587 ResourceAction::Read,
588 ResourceAction::Delete,
589 ])
590 .with_except(vec![ResourceAction::Delete]);
591 assert!(opts.should_include(ResourceAction::Index));
592 assert!(opts.should_include(ResourceAction::Read));
593 assert!(!opts.should_include(ResourceAction::Delete));
594 assert!(!opts.should_include(ResourceAction::Create));
595 }
596
597 #[test]
598 fn test_should_include_empty_only_includes_all() {
599 let opts = ResourceOptions::new().with_only(vec![]);
601 for &action in ResourceAction::all() {
602 assert!(opts.should_include(action));
603 }
604 }
605
606 #[test]
609 fn test_build_resource_base_url_simple() {
610 let url = build_resource_base_url("operate", "Blog");
611 assert_eq!(url, "/addons/operate/Blog");
612 }
613
614 #[test]
615 fn test_build_resource_base_url_nested_two_levels() {
616 let url = build_resource_base_url("operate", "blog.comment");
617 assert_eq!(url, "/addons/operate/blog/<blog_id>/comment");
618 }
619
620 #[test]
621 fn test_build_resource_base_url_nested_three_levels() {
622 let url = build_resource_base_url("operate", "blog.post.comment");
623 assert_eq!(url, "/addons/operate/blog/<blog_id>/post/<post_id>/comment");
624 }
625
626 #[test]
627 fn test_build_resource_base_url_nested_four_levels() {
628 let url = build_resource_base_url("operate", "a.b.c.d");
629 assert_eq!(url, "/addons/operate/a/<a_id>/b/<b_id>/c/<c_id>/d");
630 }
631
632 #[test]
635 fn test_build_resource_routes_default_seven() {
636 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
637 assert_eq!(route.addon, "operate");
638 assert_eq!(route.controller, "Blog");
639 assert_eq!(route.base_url, "/addons/operate/Blog");
640 assert_eq!(route.len(), 7);
641 assert!(!route.is_empty());
642 }
643
644 #[test]
645 fn test_build_resource_routes_index_entry() {
646 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
647 let entry = route.get(ResourceAction::Index).unwrap();
648 assert_eq!(entry.action, ResourceAction::Index);
649 assert_eq!(entry.method, HttpMethod::Get);
650 assert_eq!(entry.url, "/addons/operate/Blog");
651 assert_eq!(entry.controller_method, "index");
652 }
653
654 #[test]
655 fn test_build_resource_routes_create_entry() {
656 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
657 let entry = route.get(ResourceAction::Create).unwrap();
658 assert_eq!(entry.action, ResourceAction::Create);
659 assert_eq!(entry.method, HttpMethod::Get);
660 assert_eq!(entry.url, "/addons/operate/Blog/create");
661 assert_eq!(entry.controller_method, "create");
662 }
663
664 #[test]
665 fn test_build_resource_routes_edit_entry() {
666 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
667 let entry = route.get(ResourceAction::Edit).unwrap();
668 assert_eq!(entry.action, ResourceAction::Edit);
669 assert_eq!(entry.method, HttpMethod::Get);
670 assert_eq!(entry.url, "/addons/operate/Blog/<id>/edit");
671 assert_eq!(entry.controller_method, "edit");
672 }
673
674 #[test]
675 fn test_build_resource_routes_read_entry() {
676 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
677 let entry = route.get(ResourceAction::Read).unwrap();
678 assert_eq!(entry.action, ResourceAction::Read);
679 assert_eq!(entry.method, HttpMethod::Get);
680 assert_eq!(entry.url, "/addons/operate/Blog/<id>");
681 assert_eq!(entry.controller_method, "read");
682 }
683
684 #[test]
685 fn test_build_resource_routes_save_entry() {
686 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
687 let entry = route.get(ResourceAction::Save).unwrap();
688 assert_eq!(entry.action, ResourceAction::Save);
689 assert_eq!(entry.method, HttpMethod::Post);
690 assert_eq!(entry.url, "/addons/operate/Blog");
691 assert_eq!(entry.controller_method, "save");
692 }
693
694 #[test]
695 fn test_build_resource_routes_update_entry() {
696 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
697 let entry = route.get(ResourceAction::Update).unwrap();
698 assert_eq!(entry.action, ResourceAction::Update);
699 assert_eq!(entry.method, HttpMethod::Put);
700 assert_eq!(entry.url, "/addons/operate/Blog/<id>");
701 assert_eq!(entry.controller_method, "update");
702 }
703
704 #[test]
705 fn test_build_resource_routes_delete_entry() {
706 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
707 let entry = route.get(ResourceAction::Delete).unwrap();
708 assert_eq!(entry.action, ResourceAction::Delete);
709 assert_eq!(entry.method, HttpMethod::Delete);
710 assert_eq!(entry.url, "/addons/operate/Blog/<id>");
711 assert_eq!(entry.controller_method, "delete");
712 }
713
714 #[test]
715 fn test_build_resource_routes_all_urls() {
716 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
717 let urls: Vec<&str> = route.entries.iter().map(|e| e.url.as_str()).collect();
718 assert_eq!(
719 urls,
720 vec![
721 "/addons/operate/Blog",
722 "/addons/operate/Blog/create",
723 "/addons/operate/Blog/<id>/edit",
724 "/addons/operate/Blog/<id>",
725 "/addons/operate/Blog",
726 "/addons/operate/Blog/<id>",
727 "/addons/operate/Blog/<id>",
728 ]
729 );
730 }
731
732 #[test]
733 fn test_build_resource_routes_all_methods() {
734 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
735 let methods: Vec<HttpMethod> = route.entries.iter().map(|e| e.method).collect();
736 assert_eq!(
737 methods,
738 vec![
739 HttpMethod::Get,
740 HttpMethod::Get,
741 HttpMethod::Get,
742 HttpMethod::Get,
743 HttpMethod::Post,
744 HttpMethod::Put,
745 HttpMethod::Delete,
746 ]
747 );
748 }
749
750 #[test]
751 fn test_build_resource_routes_only_index_read() {
752 let opts =
753 ResourceOptions::new().with_only(vec![ResourceAction::Index, ResourceAction::Read]);
754 let route = build_resource_routes("operate", "Blog", &opts).unwrap();
755 assert_eq!(route.len(), 2);
756 assert!(route.get(ResourceAction::Index).is_some());
757 assert!(route.get(ResourceAction::Read).is_some());
758 assert!(route.get(ResourceAction::Create).is_none());
759 assert!(route.get(ResourceAction::Edit).is_none());
760 assert!(route.get(ResourceAction::Save).is_none());
761 assert!(route.get(ResourceAction::Update).is_none());
762 assert!(route.get(ResourceAction::Delete).is_none());
763 }
764
765 #[test]
766 fn test_build_resource_routes_except_delete() {
767 let opts = ResourceOptions::new().with_except(vec![ResourceAction::Delete]);
768 let route = build_resource_routes("operate", "Blog", &opts).unwrap();
769 assert_eq!(route.len(), 6);
770 assert!(route.get(ResourceAction::Delete).is_none());
771 assert!(route.get(ResourceAction::Index).is_some());
772 }
773
774 #[test]
775 fn test_build_resource_routes_only_create_save() {
776 let opts =
777 ResourceOptions::new().with_only(vec![ResourceAction::Create, ResourceAction::Save]);
778 let route = build_resource_routes("operate", "Blog", &opts).unwrap();
779 assert_eq!(route.len(), 2);
780 assert_eq!(route.entries[0].action, ResourceAction::Create);
782 assert_eq!(route.entries[1].action, ResourceAction::Save);
783 }
784
785 #[test]
786 fn test_build_resource_routes_except_all() {
787 let opts = ResourceOptions::new().with_except(ResourceAction::all().to_vec());
788 let route = build_resource_routes("operate", "Blog", &opts).unwrap();
789 assert_eq!(route.len(), 0);
790 assert!(route.is_empty());
791 }
792
793 #[test]
794 fn test_build_resource_routes_only_empty() {
795 let opts = ResourceOptions::new().with_only(vec![]);
796 let route = build_resource_routes("operate", "Blog", &opts).unwrap();
797 assert_eq!(route.len(), 7);
798 }
799
800 #[test]
801 fn test_build_resource_routes_nested_resource() {
802 let route =
803 build_resource_routes("operate", "blog.comment", &ResourceOptions::new()).unwrap();
804 assert_eq!(route.base_url, "/addons/operate/blog/<blog_id>/comment");
805 assert_eq!(route.len(), 7);
806
807 let index = route.get(ResourceAction::Index).unwrap();
808 assert_eq!(index.url, "/addons/operate/blog/<blog_id>/comment");
809
810 let read = route.get(ResourceAction::Read).unwrap();
811 assert_eq!(read.url, "/addons/operate/blog/<blog_id>/comment/<id>");
812
813 let create = route.get(ResourceAction::Create).unwrap();
814 assert_eq!(create.url, "/addons/operate/blog/<blog_id>/comment/create");
815
816 let edit = route.get(ResourceAction::Edit).unwrap();
817 assert_eq!(edit.url, "/addons/operate/blog/<blog_id>/comment/<id>/edit");
818 }
819
820 #[test]
821 fn test_build_resource_routes_nested_three_levels() {
822 let route =
823 build_resource_routes("operate", "blog.post.comment", &ResourceOptions::new()).unwrap();
824 assert_eq!(
825 route.base_url,
826 "/addons/operate/blog/<blog_id>/post/<post_id>/comment"
827 );
828
829 let read = route.get(ResourceAction::Read).unwrap();
830 assert_eq!(
831 read.url,
832 "/addons/operate/blog/<blog_id>/post/<post_id>/comment/<id>"
833 );
834 }
835
836 #[test]
837 fn test_build_resource_routes_different_addon() {
838 let route = build_resource_routes("cashier", "Order", &ResourceOptions::new()).unwrap();
839 assert_eq!(route.base_url, "/addons/cashier/Order");
840 assert_eq!(route.addon, "cashier");
841 assert_eq!(route.controller, "Order");
842 }
843
844 #[test]
845 fn test_build_resource_routes_empty_addon() {
846 let result = build_resource_routes("", "Blog", &ResourceOptions::new());
847 assert!(result.is_err());
848 match result {
849 Err(AddonLoaderError::RouteParse { reason, .. }) => {
850 assert!(reason.contains("addon cannot be empty"));
851 }
852 _ => panic!("expected RouteParse error"),
853 }
854 }
855
856 #[test]
857 fn test_build_resource_routes_empty_controller() {
858 let result = build_resource_routes("operate", "", &ResourceOptions::new());
859 assert!(result.is_err());
860 match result {
861 Err(AddonLoaderError::RouteParse { reason, .. }) => {
862 assert!(reason.contains("controller cannot be empty"));
863 }
864 _ => panic!("expected RouteParse error"),
865 }
866 }
867
868 #[test]
869 fn test_build_resource_routes_get_returns_none_for_excluded() {
870 let opts = ResourceOptions::new().with_only(vec![ResourceAction::Index]);
871 let route = build_resource_routes("operate", "Blog", &opts).unwrap();
872 assert!(route.get(ResourceAction::Index).is_some());
873 assert!(route.get(ResourceAction::Read).is_none());
874 }
875
876 #[test]
877 fn test_build_resource_routes_entries_order() {
878 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
880 let actions: Vec<ResourceAction> = route.entries.iter().map(|e| e.action).collect();
881 assert_eq!(
882 actions,
883 vec![
884 ResourceAction::Index,
885 ResourceAction::Create,
886 ResourceAction::Edit,
887 ResourceAction::Read,
888 ResourceAction::Save,
889 ResourceAction::Update,
890 ResourceAction::Delete,
891 ]
892 );
893 }
894
895 #[test]
898 fn test_resource_route_len() {
899 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
900 assert_eq!(route.len(), 7);
901 }
902
903 #[test]
904 fn test_resource_route_is_empty_false() {
905 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
906 assert!(!route.is_empty());
907 }
908
909 #[test]
910 fn test_resource_route_is_empty_true() {
911 let opts = ResourceOptions::new().with_except(ResourceAction::all().to_vec());
912 let route = build_resource_routes("operate", "Blog", &opts).unwrap();
913 assert!(route.is_empty());
914 }
915
916 #[test]
917 fn test_resource_route_get_existing() {
918 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
919 let entry = route.get(ResourceAction::Save).unwrap();
920 assert_eq!(entry.method, HttpMethod::Post);
921 }
922
923 #[test]
924 fn test_resource_route_get_nonexistent() {
925 let opts = ResourceOptions::new().with_only(vec![ResourceAction::Index]);
926 let route = build_resource_routes("operate", "Blog", &opts).unwrap();
927 assert!(route.get(ResourceAction::Save).is_none());
928 }
929
930 #[test]
933 fn test_r5_thinkphp_rest_default_mapping() {
934 let expected: &[(ResourceAction, HttpMethod, &str, &str)] = &[
937 (ResourceAction::Index, HttpMethod::Get, "", "index"),
938 (ResourceAction::Create, HttpMethod::Get, "/create", "create"),
939 (ResourceAction::Edit, HttpMethod::Get, "/<id>/edit", "edit"),
940 (ResourceAction::Read, HttpMethod::Get, "/<id>", "read"),
941 (ResourceAction::Save, HttpMethod::Post, "", "save"),
942 (ResourceAction::Update, HttpMethod::Put, "/<id>", "update"),
943 (
944 ResourceAction::Delete,
945 HttpMethod::Delete,
946 "/<id>",
947 "delete",
948 ),
949 ];
950
951 for &(action, method, suffix, ctrl_method) in expected {
952 assert_eq!(
953 action.http_method(),
954 method,
955 "{:?} HTTP method mismatch",
956 action
957 );
958 assert_eq!(
959 action.url_suffix(),
960 suffix,
961 "{:?} URL suffix mismatch",
962 action
963 );
964 assert_eq!(
965 action.controller_method(),
966 ctrl_method,
967 "{:?} controller method mismatch",
968 action
969 );
970 }
971 }
972
973 #[test]
974 fn test_r5_thinkphp_resource_seven_routes() {
975 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
977 assert_eq!(route.len(), 7, "ThinkPHP 6 resource must generate 7 routes");
978 }
979
980 #[test]
981 fn test_r5_thinkphp_only_filter() {
982 let opts =
984 ResourceOptions::new().with_only(vec![ResourceAction::Index, ResourceAction::Read]);
985 let route = build_resource_routes("operate", "Blog", &opts).unwrap();
986 assert_eq!(route.len(), 2);
987 }
988
989 #[test]
990 fn test_r5_thinkphp_except_filter() {
991 let opts = ResourceOptions::new().with_except(vec![ResourceAction::Delete]);
993 let route = build_resource_routes("operate", "Blog", &opts).unwrap();
994 assert_eq!(route.len(), 6);
995 }
996
997 #[test]
998 fn test_r5_thinkphp_nested_resource() {
999 let route =
1002 build_resource_routes("operate", "blog.comment", &ResourceOptions::new()).unwrap();
1003 assert_eq!(route.base_url, "/addons/operate/blog/<blog_id>/comment");
1004 }
1005
1006 #[test]
1007 fn test_r5_thinkphp_no_add_action() {
1008 assert!(ResourceAction::parse_name("add").is_none());
1011 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
1012 assert!(route.get(ResourceAction::Save).is_some());
1013 assert_eq!(
1015 route.get(ResourceAction::Save).unwrap().method,
1016 HttpMethod::Post
1017 );
1018 }
1019
1020 #[test]
1021 fn test_r5_thinkphp_update_uses_put_not_patch() {
1022 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
1024 let update = route.get(ResourceAction::Update).unwrap();
1025 assert_eq!(update.method, HttpMethod::Put);
1026 }
1027
1028 #[test]
1029 fn test_r5_thinkphp_index_and_save_share_url() {
1030 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
1033 let index = route.get(ResourceAction::Index).unwrap();
1034 let save = route.get(ResourceAction::Save).unwrap();
1035 assert_eq!(index.url, save.url);
1036 assert_ne!(index.method, save.method);
1037 }
1038
1039 #[test]
1040 fn test_r5_thinkphp_read_update_delete_share_url() {
1041 let route = build_resource_routes("operate", "Blog", &ResourceOptions::new()).unwrap();
1044 let read = route.get(ResourceAction::Read).unwrap();
1045 let update = route.get(ResourceAction::Update).unwrap();
1046 let delete = route.get(ResourceAction::Delete).unwrap();
1047 assert_eq!(read.url, update.url);
1048 assert_eq!(read.url, delete.url);
1049 assert_ne!(read.method, update.method);
1050 assert_ne!(read.method, delete.method);
1051 assert_ne!(update.method, delete.method);
1052 }
1053
1054 #[test]
1055 fn test_r5_addon_prefix_injection() {
1056 let route = build_resource_routes("cashier", "Order", &ResourceOptions::new()).unwrap();
1058 assert!(route.base_url.starts_with("/addons/cashier/"));
1059 for entry in &route.entries {
1060 assert!(entry.url.starts_with("/addons/cashier/Order"));
1061 }
1062 }
1063
1064 #[test]
1065 fn test_r5_addon_prefix_injection_nested() {
1066 let route =
1068 build_resource_routes("operate", "blog.comment", &ResourceOptions::new()).unwrap();
1069 assert!(route.base_url.starts_with("/addons/operate/"));
1070 for entry in &route.entries {
1071 assert!(entry.url.starts_with("/addons/operate/blog/"));
1072 }
1073 }
1074}