reinhardt_views/viewsets/
viewset.rs1use crate::viewsets::actions::Action;
2use crate::viewsets::filtering_support::{FilterConfig, FilterableViewSet, OrderingConfig};
3use crate::viewsets::metadata::{ActionMetadata, get_actions_for_viewset};
4use crate::viewsets::middleware::ViewSetMiddleware;
5use crate::viewsets::pagination_support::{PaginatedViewSet, PaginationConfig};
6use async_trait::async_trait;
7use hyper::Method;
8use reinhardt_http::{Request, Response, Result};
9use std::collections::HashMap;
10use std::sync::Arc;
11
12fn method_not_allowed(method: &Method) -> reinhardt_core::exception::Error {
14 reinhardt_core::exception::Error::MethodNotAllowed(format!("Method {} not allowed", method))
15}
16
17#[async_trait]
20pub trait ViewSet: Send + Sync {
21 fn get_basename(&self) -> &str;
23
24 fn get_lookup_field(&self) -> &str {
27 "id"
28 }
29
30 async fn dispatch(&self, request: Request, action: Action) -> Result<Response>;
32
33 fn get_extra_actions(&self) -> Vec<ActionMetadata> {
38 let viewset_type = std::any::type_name::<Self>();
39
40 let mut actions = get_actions_for_viewset(viewset_type);
42
43 let manual_actions = crate::viewsets::registry::get_registered_actions(viewset_type);
45 actions.extend(manual_actions);
46
47 actions
48 }
49
50 fn get_extra_action_url_map(&self) -> HashMap<String, String> {
53 HashMap::new()
54 }
55
56 fn get_current_base_url(&self) -> Option<String> {
58 None
59 }
60
61 fn reverse_action(&self, _action_name: &str, _args: &[&str]) -> Result<String> {
63 Err(reinhardt_core::exception::Error::NotFound(
64 "ViewSet not bound to router".to_string(),
65 ))
66 }
67
68 fn get_middleware(&self) -> Option<Arc<dyn ViewSetMiddleware>> {
71 None
72 }
73
74 fn requires_login(&self) -> bool {
76 false
77 }
78
79 fn get_required_permissions(&self) -> Vec<String> {
81 Vec::new()
82 }
83}
84
85#[allow(dead_code)]
89#[derive(Clone)]
90pub struct GenericViewSet<T> {
91 basename: String,
92 handler: T,
93}
94
95impl<T: 'static> GenericViewSet<T> {
96 pub fn new(basename: impl Into<String>, handler: T) -> Self {
107 Self {
108 basename: basename.into(),
109 handler,
110 }
111 }
112
113 pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self>
127 where
128 T: Send + Sync,
129 {
130 crate::viewsets::builder::ViewSetBuilder::new(self)
131 }
132}
133
134#[async_trait]
135impl<T: Send + Sync> ViewSet for GenericViewSet<T> {
136 fn get_basename(&self) -> &str {
137 &self.basename
138 }
139
140 async fn dispatch(&self, _request: Request, _action: Action) -> Result<Response> {
141 Err(reinhardt_core::exception::Error::NotFound(
144 "Action not implemented".to_string(),
145 ))
146 }
147}
148
149pub struct ModelViewSet<M, S> {
152 basename: String,
153 lookup_field: String,
154 pagination_config: Option<PaginationConfig>,
155 filter_config: Option<FilterConfig>,
156 ordering_config: Option<OrderingConfig>,
157 _model: std::marker::PhantomData<M>,
158 _serializer: std::marker::PhantomData<S>,
159}
160
161impl<M, S> FilterableViewSet for ModelViewSet<M, S>
163where
164 M: Send + Sync,
165 S: Send + Sync,
166{
167 fn get_filter_config(&self) -> Option<FilterConfig> {
168 self.filter_config.clone()
169 }
170
171 fn get_ordering_config(&self) -> Option<OrderingConfig> {
172 self.ordering_config.clone()
173 }
174}
175
176impl<M: 'static, S: 'static> ModelViewSet<M, S> {
177 pub fn new(basename: impl Into<String>) -> Self {
212 Self {
213 basename: basename.into(),
214 lookup_field: "id".to_string(),
215 pagination_config: Some(PaginationConfig::default()),
216 filter_config: None,
217 ordering_config: None,
218 _model: std::marker::PhantomData,
219 _serializer: std::marker::PhantomData,
220 }
221 }
222
223 pub fn with_lookup_field(mut self, field: impl Into<String>) -> Self {
259 self.lookup_field = field.into();
260 self
261 }
262
263 pub fn with_pagination(mut self, config: PaginationConfig) -> Self {
283 self.pagination_config = Some(config);
284 self
285 }
286
287 pub fn without_pagination(mut self) -> Self {
298 self.pagination_config = None;
299 self
300 }
301
302 pub fn with_filters(mut self, config: FilterConfig) -> Self {
317 self.filter_config = Some(config);
318 self
319 }
320
321 pub fn with_ordering(mut self, config: OrderingConfig) -> Self {
336 self.ordering_config = Some(config);
337 self
338 }
339
340 pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self>
343 where
344 M: Send + Sync,
345 S: Send + Sync,
346 {
347 crate::viewsets::builder::ViewSetBuilder::new(self)
348 }
349}
350
351#[async_trait]
352impl<M, S> ViewSet for ModelViewSet<M, S>
353where
354 M: Send + Sync,
355 S: Send + Sync,
356{
357 fn get_basename(&self) -> &str {
358 &self.basename
359 }
360
361 fn get_lookup_field(&self) -> &str {
362 &self.lookup_field
363 }
364
365 async fn dispatch(&self, request: Request, action: Action) -> Result<Response> {
366 match (request.method.clone(), action.detail) {
368 (Method::GET, false) => {
369 self.handle_list(request).await
371 }
372 (Method::GET, true) => {
373 self.handle_retrieve(request).await
375 }
376 (Method::POST, false) => {
377 self.handle_create(request).await
379 }
380 (Method::PUT, true) | (Method::PATCH, true) => {
381 self.handle_update(request).await
383 }
384 (Method::DELETE, true) => {
385 self.handle_destroy(request).await
387 }
388 _ => Err(method_not_allowed(&request.method)),
389 }
390 }
391}
392
393impl<M, S> ModelViewSet<M, S>
394where
395 M: Send + Sync,
396 S: Send + Sync,
397{
398 async fn handle_list(&self, _request: Request) -> Result<Response> {
399 Response::ok()
401 .with_json(&serde_json::json!([]))
402 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
403 }
404
405 async fn handle_retrieve(&self, _request: Request) -> Result<Response> {
406 Response::ok()
408 .with_json(&serde_json::json!({}))
409 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
410 }
411
412 async fn handle_create(&self, _request: Request) -> Result<Response> {
413 Response::created()
415 .with_json(&serde_json::json!({}))
416 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
417 }
418
419 async fn handle_update(&self, _request: Request) -> Result<Response> {
420 Response::ok()
422 .with_json(&serde_json::json!({}))
423 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
424 }
425
426 async fn handle_destroy(&self, _request: Request) -> Result<Response> {
427 Ok(Response::no_content())
429 }
430}
431
432impl<M, S> PaginatedViewSet for ModelViewSet<M, S>
434where
435 M: Send + Sync,
436 S: Send + Sync,
437{
438 fn get_pagination_config(&self) -> Option<PaginationConfig> {
439 self.pagination_config.clone()
440 }
441}
442
443pub struct ReadOnlyModelViewSet<M, S> {
446 basename: String,
447 lookup_field: String,
448 pagination_config: Option<PaginationConfig>,
449 filter_config: Option<FilterConfig>,
450 ordering_config: Option<OrderingConfig>,
451 _model: std::marker::PhantomData<M>,
452 _serializer: std::marker::PhantomData<S>,
453}
454
455impl<M: 'static, S: 'static> ReadOnlyModelViewSet<M, S> {
456 pub fn new(basename: impl Into<String>) -> Self {
491 Self {
492 basename: basename.into(),
493 lookup_field: "id".to_string(),
494 pagination_config: Some(PaginationConfig::default()),
495 filter_config: None,
496 ordering_config: None,
497 _model: std::marker::PhantomData,
498 _serializer: std::marker::PhantomData,
499 }
500 }
501
502 pub fn with_lookup_field(mut self, field: impl Into<String>) -> Self {
504 self.lookup_field = field.into();
505 self
506 }
507
508 pub fn with_pagination(mut self, config: PaginationConfig) -> Self {
510 self.pagination_config = Some(config);
511 self
512 }
513
514 pub fn without_pagination(mut self) -> Self {
516 self.pagination_config = None;
517 self
518 }
519
520 pub fn with_filters(mut self, config: FilterConfig) -> Self {
535 self.filter_config = Some(config);
536 self
537 }
538
539 pub fn with_ordering(mut self, config: OrderingConfig) -> Self {
554 self.ordering_config = Some(config);
555 self
556 }
557
558 pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self>
561 where
562 M: Send + Sync,
563 S: Send + Sync,
564 {
565 crate::viewsets::builder::ViewSetBuilder::new(self)
566 }
567}
568
569#[async_trait]
570impl<M, S> ViewSet for ReadOnlyModelViewSet<M, S>
571where
572 M: Send + Sync,
573 S: Send + Sync,
574{
575 fn get_basename(&self) -> &str {
576 &self.basename
577 }
578
579 fn get_lookup_field(&self) -> &str {
580 &self.lookup_field
581 }
582
583 async fn dispatch(&self, request: Request, action: Action) -> Result<Response> {
584 match (request.method.clone(), action.detail) {
585 (Method::GET, false) => {
586 Response::ok()
588 .with_json(&serde_json::json!([]))
589 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
590 }
591 (Method::GET, true) => {
592 Response::ok()
594 .with_json(&serde_json::json!({}))
595 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
596 }
597 _ => Err(method_not_allowed(&request.method)),
598 }
599 }
600}
601
602impl<M, S> PaginatedViewSet for ReadOnlyModelViewSet<M, S>
604where
605 M: Send + Sync,
606 S: Send + Sync,
607{
608 fn get_pagination_config(&self) -> Option<PaginationConfig> {
609 self.pagination_config.clone()
610 }
611}
612
613impl<M, S> FilterableViewSet for ReadOnlyModelViewSet<M, S>
615where
616 M: Send + Sync,
617 S: Send + Sync,
618{
619 fn get_filter_config(&self) -> Option<FilterConfig> {
620 self.filter_config.clone()
621 }
622
623 fn get_ordering_config(&self) -> Option<OrderingConfig> {
624 self.ordering_config.clone()
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631 use hyper::Method;
632 use std::collections::HashMap;
633 use std::sync::Arc;
634
635 #[tokio::test]
636 async fn test_viewset_builder_validation_empty_actions() {
637 let viewset = ModelViewSet::<(), ()>::new("test");
638 let builder = viewset.as_view();
639
640 let result = builder.build();
642 assert!(result.is_err());
643
644 match result {
646 Err(e) => assert!(
647 e.to_string()
648 .contains("The `actions` argument must be provided")
649 ),
650 Ok(_) => panic!("Expected error but got success"),
651 }
652 }
653
654 #[tokio::test]
655 async fn test_viewset_builder_name_suffix_mutual_exclusivity() {
656 let viewset = ModelViewSet::<(), ()>::new("test");
657 let builder = viewset.as_view();
658
659 let result = builder
661 .with_name("test_name")
662 .and_then(|b| b.with_suffix("test_suffix"));
663
664 assert!(result.is_err());
665
666 match result {
668 Err(e) => assert!(e.to_string().contains("received both `name` and `suffix`")),
669 Ok(_) => panic!("Expected error but got success"),
670 }
671 }
672
673 #[tokio::test]
674 async fn test_viewset_builder_successful_build() {
675 let viewset = ModelViewSet::<(), ()>::new("test");
676 let mut actions = HashMap::new();
677 actions.insert(Method::GET, "list".to_string());
678
679 let builder = viewset.as_view();
680 let result = builder.with_actions(actions).build();
681
682 let handler = result.unwrap();
683
684 assert!(Arc::strong_count(&handler) > 0);
687 }
688
689 #[tokio::test]
690 async fn test_viewset_builder_with_name() {
691 let viewset = ModelViewSet::<(), ()>::new("test");
692 let mut actions = HashMap::new();
693 actions.insert(Method::GET, "list".to_string());
694
695 let builder = viewset.as_view();
696 let result = builder
697 .with_actions(actions)
698 .with_name("test_view")
699 .and_then(|b| b.build());
700
701 assert!(result.is_ok());
702 }
703
704 #[tokio::test]
705 async fn test_viewset_builder_with_suffix() {
706 let viewset = ModelViewSet::<(), ()>::new("test");
707 let mut actions = HashMap::new();
708 actions.insert(Method::GET, "list".to_string());
709
710 let builder = viewset.as_view();
711 let result = builder
712 .with_actions(actions)
713 .with_suffix("_list")
714 .and_then(|b| b.build());
715
716 assert!(result.is_ok());
717 }
718}