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