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