Skip to main content

reinhardt_views/viewsets/
viewset.rs

1use 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/// ViewSet trait - similar to Django REST Framework's ViewSet
13/// Uses composition of mixins instead of inheritance
14#[async_trait]
15pub trait ViewSet: Send + Sync {
16	/// Get the basename for URL routing
17	fn get_basename(&self) -> &str;
18
19	/// Get the lookup field for detail routes
20	/// Defaults to "id" if not overridden
21	fn get_lookup_field(&self) -> &str {
22		"id"
23	}
24
25	/// Dispatch request to appropriate action
26	async fn dispatch(&self, request: Request, action: Action) -> Result<Response>;
27
28	/// Dispatch request with dependency injection context
29	///
30	/// Get extra actions defined on this ViewSet
31	/// Returns custom actions decorated with `#[action]` or manually registered
32	fn get_extra_actions(&self) -> Vec<ActionMetadata> {
33		let viewset_type = std::any::type_name::<Self>();
34
35		// Try inventory-based registration first
36		let mut actions = get_actions_for_viewset(viewset_type);
37
38		// Also check manual registration
39		let manual_actions = crate::viewsets::registry::get_registered_actions(viewset_type);
40		actions.extend(manual_actions);
41
42		actions
43	}
44
45	/// Get URL map for extra actions
46	/// Returns empty map for uninitialized ViewSets
47	fn get_extra_action_url_map(&self) -> HashMap<String, String> {
48		HashMap::new()
49	}
50
51	/// Get current base URL (only available after initialization)
52	fn get_current_base_url(&self) -> Option<String> {
53		None
54	}
55
56	/// Reverse an action name to a URL
57	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	/// Get middleware for this ViewSet
64	/// Returns None if no middleware is configured
65	fn get_middleware(&self) -> Option<Arc<dyn ViewSetMiddleware>> {
66		None
67	}
68
69	/// Check if login is required for this ViewSet
70	fn requires_login(&self) -> bool {
71		false
72	}
73
74	/// Get required permissions for this ViewSet
75	fn get_required_permissions(&self) -> Vec<String> {
76		Vec::new()
77	}
78}
79
80/// Generic ViewSet implementation
81/// Composes functionality through trait bounds
82#[allow(dead_code)]
83#[derive(Clone)]
84pub struct GenericViewSet<T> {
85	basename: String,
86	handler: T,
87}
88
89impl<T: 'static> GenericViewSet<T> {
90	/// Creates a new `GenericViewSet` with the given basename and handler.
91	///
92	/// # Examples
93	///
94	/// ```
95	/// use reinhardt_views::viewsets::{GenericViewSet, ViewSet};
96	///
97	/// let viewset = GenericViewSet::new("users", ());
98	/// assert_eq!(viewset.get_basename(), "users");
99	/// ```
100	pub fn new(basename: impl Into<String>, handler: T) -> Self {
101		Self {
102			basename: basename.into(),
103			handler,
104		}
105	}
106
107	/// Convert ViewSet to Handler with action mapping
108	/// Returns a ViewSetBuilder for configuration
109	///
110	/// # Examples
111	///
112	/// ```no_run
113	/// use reinhardt_views::{viewset_actions, viewsets::GenericViewSet};
114	/// use hyper::Method;
115	///
116	/// let viewset = GenericViewSet::new("users", ());
117	/// let actions = viewset_actions!(GET => "list");
118	/// let handler = viewset.as_view().with_actions(actions).build();
119	/// ```
120	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		// Default implementation delegates to mixins if available
136		// This would be extended with actual mixin dispatch logic
137		Err(reinhardt_core::exception::Error::NotFound(
138			"Action not implemented".to_string(),
139		))
140	}
141}
142
143/// ModelViewSet - combines all CRUD mixins
144/// Similar to Django REST Framework's ModelViewSet but using composition
145pub 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
155// Implement FilterableViewSet for ModelViewSet
156impl<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	/// Creates a new `ModelViewSet` with the given basename.
172	///
173	/// # Examples
174	///
175	/// ```
176	/// use reinhardt_views::viewsets::{ModelViewSet, ViewSet};
177	/// use reinhardt_db::prelude::Model;
178	/// use serde::{Serialize, Deserialize};
179	///
180	/// #[derive(Serialize, Deserialize, Clone, Debug)]
181	/// struct User {
182	///     id: Option<i64>,
183	///     username: String,
184	/// }
185	///
186	/// #[derive(Clone)]
187	/// struct UserFields;
188	///
189	/// impl reinhardt_db::orm::FieldSelector for UserFields {
190	///     fn with_alias(self, _alias: &str) -> Self { self }
191	/// }
192	///
193	/// impl Model for User {
194	///     type PrimaryKey = i64;
195	///     type Fields = UserFields;
196	///     fn table_name() -> &'static str { "users" }
197	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
198	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
199	///     fn new_fields() -> Self::Fields { UserFields }
200	/// }
201	///
202	/// let viewset = ModelViewSet::<User, reinhardt_rest::serializers::JsonSerializer<User>>::new("users");
203	/// assert_eq!(viewset.get_basename(), "users");
204	/// ```
205	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	/// Set custom lookup field for this ViewSet
218	///
219	/// # Examples
220	///
221	/// ```
222	/// use reinhardt_views::viewsets::{ModelViewSet, ViewSet};
223	/// use reinhardt_db::prelude::Model;
224	/// use serde::{Serialize, Deserialize};
225	///
226	/// #[derive(Serialize, Deserialize, Clone, Debug)]
227	/// struct User {
228	///     id: Option<i64>,
229	///     username: String,
230	/// }
231	///
232	/// #[derive(Clone)]
233	/// struct UserFields;
234	///
235	/// impl reinhardt_db::orm::FieldSelector for UserFields {
236	///     fn with_alias(self, _alias: &str) -> Self { self }
237	/// }
238	///
239	/// impl Model for User {
240	///     type PrimaryKey = i64;
241	///     type Fields = UserFields;
242	///     fn table_name() -> &'static str { "users" }
243	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
244	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
245	///     fn new_fields() -> Self::Fields { UserFields }
246	/// }
247	///
248	/// let viewset = ModelViewSet::<User, ()>::new("users")
249	///     .with_lookup_field("username");
250	/// assert_eq!(viewset.get_lookup_field(), "username");
251	/// ```
252	pub fn with_lookup_field(mut self, field: impl Into<String>) -> Self {
253		self.lookup_field = field.into();
254		self
255	}
256
257	/// Set pagination configuration for this ViewSet
258	///
259	/// # Examples
260	///
261	/// ```
262	/// use reinhardt_views::viewsets::{ModelViewSet, PaginationConfig};
263	///
264	/// // Page number pagination with custom page size
265	/// let viewset = ModelViewSet::<(), ()>::new("items")
266	///     .with_pagination(PaginationConfig::page_number(20, Some(100)));
267	///
268	/// // Limit/offset pagination
269	/// let viewset = ModelViewSet::<(), ()>::new("items")
270	///     .with_pagination(PaginationConfig::limit_offset(25, Some(500)));
271	///
272	/// // Disable pagination
273	/// let viewset = ModelViewSet::<(), ()>::new("items")
274	///     .with_pagination(PaginationConfig::none());
275	/// ```
276	pub fn with_pagination(mut self, config: PaginationConfig) -> Self {
277		self.pagination_config = Some(config);
278		self
279	}
280
281	/// Disable pagination for this ViewSet
282	///
283	/// # Examples
284	///
285	/// ```
286	/// use reinhardt_views::viewsets::ModelViewSet;
287	///
288	/// let viewset = ModelViewSet::<(), ()>::new("items")
289	///     .without_pagination();
290	/// ```
291	pub fn without_pagination(mut self) -> Self {
292		self.pagination_config = None;
293		self
294	}
295
296	/// Set filter configuration for this ViewSet
297	///
298	/// # Examples
299	///
300	/// ```
301	/// use reinhardt_views::viewsets::{ModelViewSet, FilterConfig};
302	///
303	/// let viewset = ModelViewSet::<(), ()>::new("items")
304	///     .with_filters(
305	///         FilterConfig::new()
306	///             .with_filterable_fields(vec!["status", "category"])
307	///             .with_search_fields(vec!["title", "description"])
308	///     );
309	/// ```
310	pub fn with_filters(mut self, config: FilterConfig) -> Self {
311		self.filter_config = Some(config);
312		self
313	}
314
315	/// Set ordering configuration for this ViewSet
316	///
317	/// # Examples
318	///
319	/// ```
320	/// use reinhardt_views::viewsets::{ModelViewSet, OrderingConfig};
321	///
322	/// let viewset = ModelViewSet::<(), ()>::new("items")
323	///     .with_ordering(
324	///         OrderingConfig::new()
325	///             .with_ordering_fields(vec!["created_at", "title", "id"])
326	///             .with_default_ordering(vec!["-created_at"])
327	///     );
328	/// ```
329	pub fn with_ordering(mut self, config: OrderingConfig) -> Self {
330		self.ordering_config = Some(config);
331		self
332	}
333
334	/// Convert ViewSet to Handler with action mapping
335	/// Returns a ViewSetBuilder for configuration
336	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		// Route to appropriate handler based on HTTP method and action
361		match (request.method.clone(), action.detail) {
362			(Method::GET, false) => {
363				// List action
364				self.handle_list(request).await
365			}
366			(Method::GET, true) => {
367				// Retrieve action
368				self.handle_retrieve(request).await
369			}
370			(Method::POST, false) => {
371				// Create action
372				self.handle_create(request).await
373			}
374			(Method::PUT, true) | (Method::PATCH, true) => {
375				// Update action
376				self.handle_update(request).await
377			}
378			(Method::DELETE, true) => {
379				// Destroy action
380				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		// Implementation would query all objects and serialize them
396		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		// Implementation would get object by ID and serialize it
403		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		// Implementation would deserialize, validate, and create object
410		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		// Implementation would deserialize, validate, and update object
417		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		// Implementation would delete object
424		Ok(Response::no_content())
425	}
426}
427
428// Implement PaginatedViewSet for ModelViewSet
429impl<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
439/// ReadOnlyModelViewSet - only list and retrieve
440/// Demonstrates selective composition of mixins
441pub 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	/// Creates a new `ReadOnlyModelViewSet` with the given basename.
453	///
454	/// # Examples
455	///
456	/// ```
457	/// use reinhardt_views::viewsets::{ReadOnlyModelViewSet, ViewSet};
458	/// use reinhardt_db::prelude::Model;
459	/// use serde::{Serialize, Deserialize};
460	///
461	/// #[derive(Serialize, Deserialize, Clone, Debug)]
462	/// struct User {
463	///     id: Option<i64>,
464	///     username: String,
465	/// }
466	///
467	/// #[derive(Clone)]
468	/// struct UserFields;
469	///
470	/// impl reinhardt_db::orm::FieldSelector for UserFields {
471	///     fn with_alias(self, _alias: &str) -> Self { self }
472	/// }
473	///
474	/// impl Model for User {
475	///     type PrimaryKey = i64;
476	///     type Fields = UserFields;
477	///     fn table_name() -> &'static str { "users" }
478	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
479	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
480	///     fn new_fields() -> Self::Fields { UserFields }
481	/// }
482	///
483	/// let viewset = ReadOnlyModelViewSet::<User, reinhardt_rest::serializers::JsonSerializer<User>>::new("users");
484	/// assert_eq!(viewset.get_basename(), "users");
485	/// ```
486	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	/// Set custom lookup field for this ViewSet
499	pub fn with_lookup_field(mut self, field: impl Into<String>) -> Self {
500		self.lookup_field = field.into();
501		self
502	}
503
504	/// Set pagination configuration for this ViewSet
505	pub fn with_pagination(mut self, config: PaginationConfig) -> Self {
506		self.pagination_config = Some(config);
507		self
508	}
509
510	/// Disable pagination for this ViewSet
511	pub fn without_pagination(mut self) -> Self {
512		self.pagination_config = None;
513		self
514	}
515
516	/// Set filter configuration for this ViewSet
517	///
518	/// # Examples
519	///
520	/// ```ignore
521	/// use reinhardt_views::viewsets::{ReadOnlyModelViewSet, FilterConfig};
522	///
523	/// let viewset = ReadOnlyModelViewSet::<MyModel, MySerializer>::new("items")
524	///     .with_filters(
525	///         FilterConfig::new()
526	///             .with_filterable_fields(vec!["status", "category"])
527	///             .with_search_fields(vec!["title", "description"])
528	///     );
529	/// ```
530	pub fn with_filters(mut self, config: FilterConfig) -> Self {
531		self.filter_config = Some(config);
532		self
533	}
534
535	/// Set ordering configuration for this ViewSet
536	///
537	/// # Examples
538	///
539	/// ```ignore
540	/// use reinhardt_views::viewsets::{ReadOnlyModelViewSet, OrderingConfig};
541	///
542	/// let viewset = ReadOnlyModelViewSet::<MyModel, MySerializer>::new("items")
543	///     .with_ordering(
544	///         OrderingConfig::new()
545	///             .with_ordering_fields(vec!["created_at", "title"])
546	///             .with_default_ordering(vec!["-created_at"])
547	///     );
548	/// ```
549	pub fn with_ordering(mut self, config: OrderingConfig) -> Self {
550		self.ordering_config = Some(config);
551		self
552	}
553
554	/// Convert ViewSet to Handler with action mapping
555	/// Returns a ViewSetBuilder for configuration
556	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				// List only
583				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				// Retrieve only
589				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
600// Implement PaginatedViewSet for ReadOnlyModelViewSet
601impl<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
611// Implement FilterableViewSet for ReadOnlyModelViewSet
612impl<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		// Test that empty actions causes build to fail
639		let result = builder.build();
640		assert!(result.is_err());
641
642		// Check error message without unwrapping
643		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		// Test that providing both name and suffix fails
658		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		// Check error message without unwrapping
665		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		// Test that handler is created successfully
683		// Handler should be created without errors
684		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}