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
12#[async_trait]
15pub trait ViewSet: Send + Sync {
16 fn get_basename(&self) -> &str;
18
19 fn get_lookup_field(&self) -> &str {
22 "id"
23 }
24
25 async fn dispatch(&self, request: Request, action: Action) -> Result<Response>;
27
28 fn get_extra_actions(&self) -> Vec<ActionMetadata> {
33 let viewset_type = std::any::type_name::<Self>();
34
35 let mut actions = get_actions_for_viewset(viewset_type);
37
38 let manual_actions = crate::viewsets::registry::get_registered_actions(viewset_type);
40 actions.extend(manual_actions);
41
42 actions
43 }
44
45 fn get_extra_action_url_map(&self) -> HashMap<String, String> {
48 HashMap::new()
49 }
50
51 fn get_current_base_url(&self) -> Option<String> {
53 None
54 }
55
56 fn reverse_action(&self, _action_name: &str, _args: &[&str]) -> Result<String> {
58 Err(reinhardt_core::exception::Error::NotFound(
59 "ViewSet not bound to router".to_string(),
60 ))
61 }
62
63 fn get_middleware(&self) -> Option<Arc<dyn ViewSetMiddleware>> {
66 None
67 }
68
69 fn requires_login(&self) -> bool {
71 false
72 }
73
74 fn get_required_permissions(&self) -> Vec<String> {
76 Vec::new()
77 }
78}
79
80#[allow(dead_code)]
83#[derive(Clone)]
84pub struct GenericViewSet<T> {
85 basename: String,
86 handler: T,
87}
88
89impl<T: 'static> GenericViewSet<T> {
90 pub fn new(basename: impl Into<String>, handler: T) -> Self {
101 Self {
102 basename: basename.into(),
103 handler,
104 }
105 }
106
107 pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self>
121 where
122 T: Send + Sync,
123 {
124 crate::viewsets::builder::ViewSetBuilder::new(self)
125 }
126}
127
128#[async_trait]
129impl<T: Send + Sync> ViewSet for GenericViewSet<T> {
130 fn get_basename(&self) -> &str {
131 &self.basename
132 }
133
134 async fn dispatch(&self, _request: Request, _action: Action) -> Result<Response> {
135 Err(reinhardt_core::exception::Error::NotFound(
138 "Action not implemented".to_string(),
139 ))
140 }
141}
142
143pub struct ModelViewSet<M, S> {
146 basename: String,
147 lookup_field: String,
148 pagination_config: Option<PaginationConfig>,
149 filter_config: Option<FilterConfig>,
150 ordering_config: Option<OrderingConfig>,
151 _model: std::marker::PhantomData<M>,
152 _serializer: std::marker::PhantomData<S>,
153}
154
155impl<M, S> FilterableViewSet for ModelViewSet<M, S>
157where
158 M: Send + Sync,
159 S: Send + Sync,
160{
161 fn get_filter_config(&self) -> Option<FilterConfig> {
162 self.filter_config.clone()
163 }
164
165 fn get_ordering_config(&self) -> Option<OrderingConfig> {
166 self.ordering_config.clone()
167 }
168}
169
170impl<M: 'static, S: 'static> ModelViewSet<M, S> {
171 pub fn new(basename: impl Into<String>) -> Self {
206 Self {
207 basename: basename.into(),
208 lookup_field: "id".to_string(),
209 pagination_config: Some(PaginationConfig::default()),
210 filter_config: None,
211 ordering_config: None,
212 _model: std::marker::PhantomData,
213 _serializer: std::marker::PhantomData,
214 }
215 }
216
217 pub fn with_lookup_field(mut self, field: impl Into<String>) -> Self {
253 self.lookup_field = field.into();
254 self
255 }
256
257 pub fn with_pagination(mut self, config: PaginationConfig) -> Self {
277 self.pagination_config = Some(config);
278 self
279 }
280
281 pub fn without_pagination(mut self) -> Self {
292 self.pagination_config = None;
293 self
294 }
295
296 pub fn with_filters(mut self, config: FilterConfig) -> Self {
311 self.filter_config = Some(config);
312 self
313 }
314
315 pub fn with_ordering(mut self, config: OrderingConfig) -> Self {
330 self.ordering_config = Some(config);
331 self
332 }
333
334 pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self>
337 where
338 M: Send + Sync,
339 S: Send + Sync,
340 {
341 crate::viewsets::builder::ViewSetBuilder::new(self)
342 }
343}
344
345#[async_trait]
346impl<M, S> ViewSet for ModelViewSet<M, S>
347where
348 M: Send + Sync,
349 S: Send + Sync,
350{
351 fn get_basename(&self) -> &str {
352 &self.basename
353 }
354
355 fn get_lookup_field(&self) -> &str {
356 &self.lookup_field
357 }
358
359 async fn dispatch(&self, request: Request, action: Action) -> Result<Response> {
360 match (request.method.clone(), action.detail) {
362 (Method::GET, false) => {
363 self.handle_list(request).await
365 }
366 (Method::GET, true) => {
367 self.handle_retrieve(request).await
369 }
370 (Method::POST, false) => {
371 self.handle_create(request).await
373 }
374 (Method::PUT, true) | (Method::PATCH, true) => {
375 self.handle_update(request).await
377 }
378 (Method::DELETE, true) => {
379 self.handle_destroy(request).await
381 }
382 _ => Err(reinhardt_core::exception::Error::Http(
383 "Method not allowed".to_string(),
384 )),
385 }
386 }
387}
388
389impl<M, S> ModelViewSet<M, S>
390where
391 M: Send + Sync,
392 S: Send + Sync,
393{
394 async fn handle_list(&self, _request: Request) -> Result<Response> {
395 Response::ok()
397 .with_json(&serde_json::json!([]))
398 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
399 }
400
401 async fn handle_retrieve(&self, _request: Request) -> Result<Response> {
402 Response::ok()
404 .with_json(&serde_json::json!({}))
405 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
406 }
407
408 async fn handle_create(&self, _request: Request) -> Result<Response> {
409 Response::created()
411 .with_json(&serde_json::json!({}))
412 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
413 }
414
415 async fn handle_update(&self, _request: Request) -> Result<Response> {
416 Response::ok()
418 .with_json(&serde_json::json!({}))
419 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
420 }
421
422 async fn handle_destroy(&self, _request: Request) -> Result<Response> {
423 Ok(Response::no_content())
425 }
426}
427
428impl<M, S> PaginatedViewSet for ModelViewSet<M, S>
430where
431 M: Send + Sync,
432 S: Send + Sync,
433{
434 fn get_pagination_config(&self) -> Option<PaginationConfig> {
435 self.pagination_config.clone()
436 }
437}
438
439pub struct ReadOnlyModelViewSet<M, S> {
442 basename: String,
443 lookup_field: String,
444 pagination_config: Option<PaginationConfig>,
445 filter_config: Option<FilterConfig>,
446 ordering_config: Option<OrderingConfig>,
447 _model: std::marker::PhantomData<M>,
448 _serializer: std::marker::PhantomData<S>,
449}
450
451impl<M: 'static, S: 'static> ReadOnlyModelViewSet<M, S> {
452 pub fn new(basename: impl Into<String>) -> Self {
487 Self {
488 basename: basename.into(),
489 lookup_field: "id".to_string(),
490 pagination_config: Some(PaginationConfig::default()),
491 filter_config: None,
492 ordering_config: None,
493 _model: std::marker::PhantomData,
494 _serializer: std::marker::PhantomData,
495 }
496 }
497
498 pub fn with_lookup_field(mut self, field: impl Into<String>) -> Self {
500 self.lookup_field = field.into();
501 self
502 }
503
504 pub fn with_pagination(mut self, config: PaginationConfig) -> Self {
506 self.pagination_config = Some(config);
507 self
508 }
509
510 pub fn without_pagination(mut self) -> Self {
512 self.pagination_config = None;
513 self
514 }
515
516 pub fn with_filters(mut self, config: FilterConfig) -> Self {
531 self.filter_config = Some(config);
532 self
533 }
534
535 pub fn with_ordering(mut self, config: OrderingConfig) -> Self {
550 self.ordering_config = Some(config);
551 self
552 }
553
554 pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self>
557 where
558 M: Send + Sync,
559 S: Send + Sync,
560 {
561 crate::viewsets::builder::ViewSetBuilder::new(self)
562 }
563}
564
565#[async_trait]
566impl<M, S> ViewSet for ReadOnlyModelViewSet<M, S>
567where
568 M: Send + Sync,
569 S: Send + Sync,
570{
571 fn get_basename(&self) -> &str {
572 &self.basename
573 }
574
575 fn get_lookup_field(&self) -> &str {
576 &self.lookup_field
577 }
578
579 async fn dispatch(&self, request: Request, action: Action) -> Result<Response> {
580 match (request.method.clone(), action.detail) {
581 (Method::GET, false) => {
582 Response::ok()
584 .with_json(&serde_json::json!([]))
585 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
586 }
587 (Method::GET, true) => {
588 Response::ok()
590 .with_json(&serde_json::json!({}))
591 .map_err(|e| reinhardt_core::exception::Error::Http(e.to_string()))
592 }
593 _ => Err(reinhardt_core::exception::Error::Http(
594 "Method not allowed".to_string(),
595 )),
596 }
597 }
598}
599
600impl<M, S> PaginatedViewSet for ReadOnlyModelViewSet<M, S>
602where
603 M: Send + Sync,
604 S: Send + Sync,
605{
606 fn get_pagination_config(&self) -> Option<PaginationConfig> {
607 self.pagination_config.clone()
608 }
609}
610
611impl<M, S> FilterableViewSet for ReadOnlyModelViewSet<M, S>
613where
614 M: Send + Sync,
615 S: Send + Sync,
616{
617 fn get_filter_config(&self) -> Option<FilterConfig> {
618 self.filter_config.clone()
619 }
620
621 fn get_ordering_config(&self) -> Option<OrderingConfig> {
622 self.ordering_config.clone()
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use hyper::Method;
630 use std::collections::HashMap;
631 use std::sync::Arc;
632
633 #[tokio::test]
634 async fn test_viewset_builder_validation_empty_actions() {
635 let viewset = ModelViewSet::<(), ()>::new("test");
636 let builder = viewset.as_view();
637
638 let result = builder.build();
640 assert!(result.is_err());
641
642 match result {
644 Err(e) => assert!(
645 e.to_string()
646 .contains("The `actions` argument must be provided")
647 ),
648 Ok(_) => panic!("Expected error but got success"),
649 }
650 }
651
652 #[tokio::test]
653 async fn test_viewset_builder_name_suffix_mutual_exclusivity() {
654 let viewset = ModelViewSet::<(), ()>::new("test");
655 let builder = viewset.as_view();
656
657 let result = builder
659 .with_name("test_name")
660 .and_then(|b| b.with_suffix("test_suffix"));
661
662 assert!(result.is_err());
663
664 match result {
666 Err(e) => assert!(e.to_string().contains("received both `name` and `suffix`")),
667 Ok(_) => panic!("Expected error but got success"),
668 }
669 }
670
671 #[tokio::test]
672 async fn test_viewset_builder_successful_build() {
673 let viewset = ModelViewSet::<(), ()>::new("test");
674 let mut actions = HashMap::new();
675 actions.insert(Method::GET, "list".to_string());
676
677 let builder = viewset.as_view();
678 let result = builder.with_actions(actions).build();
679
680 let handler = result.unwrap();
681
682 assert!(Arc::strong_count(&handler) > 0);
685 }
686
687 #[tokio::test]
688 async fn test_viewset_builder_with_name() {
689 let viewset = ModelViewSet::<(), ()>::new("test");
690 let mut actions = HashMap::new();
691 actions.insert(Method::GET, "list".to_string());
692
693 let builder = viewset.as_view();
694 let result = builder
695 .with_actions(actions)
696 .with_name("test_view")
697 .and_then(|b| b.build());
698
699 assert!(result.is_ok());
700 }
701
702 #[tokio::test]
703 async fn test_viewset_builder_with_suffix() {
704 let viewset = ModelViewSet::<(), ()>::new("test");
705 let mut actions = HashMap::new();
706 actions.insert(Method::GET, "list".to_string());
707
708 let builder = viewset.as_view();
709 let result = builder
710 .with_actions(actions)
711 .with_suffix("_list")
712 .and_then(|b| b.build());
713
714 assert!(result.is_ok());
715 }
716}