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::handler::ModelViewSetHandler;
4use crate::viewsets::metadata::{ActionMetadata, get_actions_for_viewset};
5use crate::viewsets::middleware::ViewSetMiddleware;
6use crate::viewsets::pagination_support::{PaginatedViewSet, PaginationConfig};
7use async_trait::async_trait;
8use hyper::Method;
9use reinhardt_auth::Permission;
10use reinhardt_db::orm::{Model, query_types::DbBackend};
11use reinhardt_http::{Request, Response, Result};
12use reinhardt_rest::filters::FilterBackend;
13use reinhardt_rest::serializers::Serializer;
14use serde::Serialize;
15use serde::de::DeserializeOwned;
16use std::collections::HashMap;
17use std::marker::PhantomData;
18use std::sync::Arc;
19
20/// Extract the primary key value from request path parameters by lookup field
21/// name. Returns a JSON string value suitable for `ModelViewSetHandler` methods.
22fn extract_pk(request: &Request, lookup_field: &str) -> Result<serde_json::Value> {
23	request
24		.path_params
25		.get(lookup_field)
26		.map(|v| serde_json::Value::String(v.clone()))
27		.ok_or_else(|| {
28			reinhardt_core::exception::Error::Http(format!(
29				"Missing path parameter: {}",
30				lookup_field
31			))
32		})
33}
34
35/// Create a `MethodNotAllowed` error for the given HTTP method.
36fn method_not_allowed(method: &Method) -> reinhardt_core::exception::Error {
37	reinhardt_core::exception::Error::MethodNotAllowed(format!("Method {} not allowed", method))
38}
39
40/// ViewSet trait - similar to Django REST Framework's ViewSet
41/// Uses composition of mixins instead of inheritance
42#[async_trait]
43pub trait ViewSet: Send + Sync {
44	/// Get the basename for URL routing
45	fn get_basename(&self) -> &str;
46
47	/// Get the lookup field for detail routes
48	/// Defaults to "id" if not overridden
49	fn get_lookup_field(&self) -> &str {
50		"id"
51	}
52
53	/// Dispatch request to appropriate action
54	async fn dispatch(&self, request: Request, action: Action) -> Result<Response>;
55
56	/// Dispatch request with dependency injection context
57	///
58	/// Get extra actions defined on this ViewSet
59	/// Returns custom actions decorated with `#[action]` or manually registered
60	fn get_extra_actions(&self) -> Vec<ActionMetadata> {
61		let viewset_type = std::any::type_name::<Self>();
62
63		// Try inventory-based registration first
64		let mut actions = get_actions_for_viewset(viewset_type);
65
66		// Also check manual registration
67		let manual_actions = crate::viewsets::registry::get_registered_actions(viewset_type);
68		actions.extend(manual_actions);
69
70		actions
71	}
72
73	/// Get URL map for extra actions
74	/// Returns empty map for uninitialized ViewSets
75	fn get_extra_action_url_map(&self) -> HashMap<String, String> {
76		HashMap::new()
77	}
78
79	/// Get current base URL (only available after initialization)
80	fn get_current_base_url(&self) -> Option<String> {
81		None
82	}
83
84	/// Reverse an action name to a URL
85	fn reverse_action(&self, _action_name: &str, _args: &[&str]) -> Result<String> {
86		Err(reinhardt_core::exception::Error::NotFound(
87			"ViewSet not bound to router".to_string(),
88		))
89	}
90
91	/// Get middleware for this ViewSet
92	/// Returns None if no middleware is configured
93	fn get_middleware(&self) -> Option<Arc<dyn ViewSetMiddleware>> {
94		None
95	}
96
97	/// Check if login is required for this ViewSet
98	fn requires_login(&self) -> bool {
99		false
100	}
101
102	/// Get required permissions for this ViewSet
103	fn get_required_permissions(&self) -> Vec<String> {
104		Vec::new()
105	}
106}
107
108/// Generic ViewSet without built-in CRUD logic.
109///
110/// `GenericViewSet<T>` is an extensibility hook for users who want to build a
111/// `ViewSet` from scratch with their own dispatch logic. It does **not**
112/// perform any CRUD by itself; calling `dispatch()` on a bare `GenericViewSet`
113/// always returns a `NotFound` error with guidance pointing to the correct
114/// abstractions.
115///
116/// # Choosing the right ViewSet
117///
118/// - For automatic CRUD against a database `Model`, use [`ModelViewSet`].
119/// - For read-only access (list + retrieve only), use [`ReadOnlyModelViewSet`].
120/// - For fully custom behavior, define your own type and `impl ViewSet for YourType`
121///   with a hand-written `dispatch()`. `GenericViewSet` is rarely the right choice.
122///
123/// # Example: composing a custom ViewSet via the builder
124///
125/// ```
126/// use reinhardt_views::viewsets::{GenericViewSet, ViewSet};
127///
128/// let viewset = GenericViewSet::new("widgets", ());
129/// assert_eq!(viewset.get_basename(), "widgets");
130/// ```
131// Allow dead_code: generic container for composable ViewSet implementations via trait bounds
132#[allow(dead_code)]
133#[derive(Clone)]
134pub struct GenericViewSet<T> {
135	basename: String,
136	handler: T,
137}
138
139impl<T: 'static> GenericViewSet<T> {
140	/// Creates a new `GenericViewSet` with the given basename and handler.
141	///
142	/// # Examples
143	///
144	/// ```
145	/// use reinhardt_views::viewsets::{GenericViewSet, ViewSet};
146	///
147	/// let viewset = GenericViewSet::new("users", ());
148	/// assert_eq!(viewset.get_basename(), "users");
149	/// ```
150	pub fn new(basename: impl Into<String>, handler: T) -> Self {
151		Self {
152			basename: basename.into(),
153			handler,
154		}
155	}
156
157	/// Convert ViewSet to Handler with action mapping
158	/// Returns a ViewSetBuilder for configuration
159	///
160	/// # Examples
161	///
162	/// ```ignore
163	/// use reinhardt_views::{viewset_actions, viewsets::GenericViewSet};
164	/// use hyper::Method;
165	///
166	/// let viewset = GenericViewSet::new("users", ());
167	/// let actions = viewset_actions!(GET => "list");
168	/// let handler = viewset.as_view().with_actions(actions).build();
169	/// ```
170	pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self>
171	where
172		T: Send + Sync,
173	{
174		crate::viewsets::builder::ViewSetBuilder::new(self)
175	}
176}
177
178#[async_trait]
179impl<T: Send + Sync> ViewSet for GenericViewSet<T> {
180	fn get_basename(&self) -> &str {
181		&self.basename
182	}
183
184	async fn dispatch(&self, _request: Request, action: Action) -> Result<Response> {
185		// `GenericViewSet` carries no built-in CRUD logic on purpose. Users who
186		// reach this point typically need one of the concrete ViewSets that *do*
187		// implement CRUD, or a hand-written `impl ViewSet` on their own type.
188		// Returning a guidance-rich error avoids silent placeholder responses
189		// (the regression class behind issue #3985).
190		Err(reinhardt_core::exception::Error::NotFound(format!(
191			"GenericViewSet has no built-in CRUD logic for action {:?}. \
192			 For real CRUD, use ModelViewSet<M, S> or ReadOnlyModelViewSet<M, S>. \
193			 To implement custom logic, define your own struct and \
194			 `impl ViewSet for YourType` with a hand-written dispatch().",
195			action.action_type
196		)))
197	}
198}
199
200/// `ModelViewSet` - combines all CRUD mixins, backed by a real
201/// [`ModelViewSetHandler`] for database-backed CRUD.
202///
203/// Similar to Django REST Framework's `ModelViewSet` but built around Rust
204/// type composition. `dispatch()` routes the standard REST verbs to the
205/// embedded handler's `list` / `retrieve` / `create` / `update` / `destroy`
206/// methods, so registering a `ModelViewSet` with a router yields actual
207/// model-backed responses (not placeholders).
208pub struct ModelViewSet<M, S>
209where
210	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
211	S: Send + Sync + 'static,
212{
213	basename: String,
214	lookup_field: String,
215	pagination_config: Option<PaginationConfig>,
216	filter_config: Option<FilterConfig>,
217	ordering_config: Option<OrderingConfig>,
218	handler: ModelViewSetHandler<M>,
219	_serializer: PhantomData<S>,
220}
221
222// Implement FilterableViewSet for ModelViewSet
223impl<M, S> FilterableViewSet for ModelViewSet<M, S>
224where
225	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
226	S: Send + Sync + 'static,
227{
228	fn get_filter_config(&self) -> Option<FilterConfig> {
229		self.filter_config.clone()
230	}
231
232	fn get_ordering_config(&self) -> Option<OrderingConfig> {
233		self.ordering_config.clone()
234	}
235}
236
237impl<M, S> ModelViewSet<M, S>
238where
239	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
240	S: Send + Sync + 'static,
241{
242	/// Creates a new `ModelViewSet` with the given basename.
243	///
244	/// # Examples
245	///
246	/// ```
247	/// use reinhardt_views::viewsets::{ModelViewSet, ViewSet};
248	/// use reinhardt_db::prelude::Model;
249	/// use serde::{Serialize, Deserialize};
250	///
251	/// #[derive(Serialize, Deserialize, Clone, Debug)]
252	/// struct User {
253	///     id: Option<i64>,
254	///     username: String,
255	/// }
256	///
257	/// #[derive(Clone)]
258	/// struct UserFields;
259	///
260	/// impl reinhardt_db::orm::FieldSelector for UserFields {
261	///     fn with_alias(self, _alias: &str) -> Self { self }
262	/// }
263	///
264	/// impl Model for User {
265	///     type PrimaryKey = i64;
266	///     type Fields = UserFields;
267	///     fn table_name() -> &'static str { "users" }
268	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
269	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
270	///     fn new_fields() -> Self::Fields { UserFields }
271	/// }
272	///
273	/// let viewset = ModelViewSet::<User, reinhardt_rest::serializers::JsonSerializer<User>>::new("users");
274	/// assert_eq!(viewset.get_basename(), "users");
275	/// ```
276	pub fn new(basename: impl Into<String>) -> Self {
277		Self {
278			basename: basename.into(),
279			lookup_field: "id".to_string(),
280			pagination_config: Some(PaginationConfig::default()),
281			filter_config: None,
282			ordering_config: None,
283			handler: ModelViewSetHandler::<M>::new(),
284			_serializer: PhantomData,
285		}
286	}
287
288	/// Set custom lookup field for this ViewSet
289	///
290	/// # Examples
291	///
292	/// ```
293	/// use reinhardt_views::viewsets::{ModelViewSet, ViewSet};
294	/// use reinhardt_db::prelude::Model;
295	/// use serde::{Serialize, Deserialize};
296	///
297	/// #[derive(Serialize, Deserialize, Clone, Debug)]
298	/// struct User {
299	///     id: Option<i64>,
300	///     username: String,
301	/// }
302	///
303	/// #[derive(Clone)]
304	/// struct UserFields;
305	///
306	/// impl reinhardt_db::orm::FieldSelector for UserFields {
307	///     fn with_alias(self, _alias: &str) -> Self { self }
308	/// }
309	///
310	/// impl Model for User {
311	///     type PrimaryKey = i64;
312	///     type Fields = UserFields;
313	///     fn table_name() -> &'static str { "users" }
314	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
315	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
316	///     fn new_fields() -> Self::Fields { UserFields }
317	/// }
318	///
319	/// let viewset = ModelViewSet::<User, ()>::new("users")
320	///     .with_lookup_field("username");
321	/// assert_eq!(viewset.get_lookup_field(), "username");
322	/// ```
323	pub fn with_lookup_field(mut self, field: impl Into<String>) -> Self {
324		self.lookup_field = field.into();
325		self
326	}
327
328	/// Set pagination configuration for this ViewSet
329	///
330	/// # Examples
331	///
332	/// ```
333	/// # use reinhardt_views::viewsets::{ModelViewSet, PaginationConfig};
334	/// # use reinhardt_db::orm::{FieldSelector, Model};
335	/// # use serde::{Deserialize, Serialize};
336	/// # #[derive(Clone, Serialize, Deserialize)]
337	/// # struct Item { id: Option<i64> }
338	/// # #[derive(Clone)] struct ItemFields;
339	/// # impl FieldSelector for ItemFields { fn with_alias(self, _: &str) -> Self { self } }
340	/// # impl Model for Item {
341	/// #     type PrimaryKey = i64; type Fields = ItemFields;
342	/// #     fn table_name() -> &'static str { "items" }
343	/// #     fn primary_key(&self) -> Option<i64> { self.id }
344	/// #     fn set_primary_key(&mut self, v: i64) { self.id = Some(v); }
345	/// #     fn new_fields() -> Self::Fields { ItemFields }
346	/// # }
347	/// // Page number pagination with custom page size
348	/// let viewset = ModelViewSet::<Item, ()>::new("items")
349	///     .with_pagination(PaginationConfig::page_number(20, Some(100)));
350	///
351	/// // Limit/offset pagination
352	/// let viewset = ModelViewSet::<Item, ()>::new("items")
353	///     .with_pagination(PaginationConfig::limit_offset(25, Some(500)));
354	///
355	/// // Disable pagination
356	/// let viewset = ModelViewSet::<Item, ()>::new("items")
357	///     .with_pagination(PaginationConfig::none());
358	/// ```
359	pub fn with_pagination(mut self, config: PaginationConfig) -> Self {
360		self.pagination_config = Some(config);
361		self
362	}
363
364	/// Disable pagination for this ViewSet
365	///
366	/// # Examples
367	///
368	/// ```
369	/// # use reinhardt_views::viewsets::ModelViewSet;
370	/// # use reinhardt_db::orm::{FieldSelector, Model};
371	/// # use serde::{Deserialize, Serialize};
372	/// # #[derive(Clone, Serialize, Deserialize)]
373	/// # struct Item { id: Option<i64> }
374	/// # #[derive(Clone)] struct ItemFields;
375	/// # impl FieldSelector for ItemFields { fn with_alias(self, _: &str) -> Self { self } }
376	/// # impl Model for Item {
377	/// #     type PrimaryKey = i64; type Fields = ItemFields;
378	/// #     fn table_name() -> &'static str { "items" }
379	/// #     fn primary_key(&self) -> Option<i64> { self.id }
380	/// #     fn set_primary_key(&mut self, v: i64) { self.id = Some(v); }
381	/// #     fn new_fields() -> Self::Fields { ItemFields }
382	/// # }
383	/// let viewset = ModelViewSet::<Item, ()>::new("items")
384	///     .without_pagination();
385	/// ```
386	pub fn without_pagination(mut self) -> Self {
387		self.pagination_config = None;
388		self
389	}
390
391	/// Set filter configuration for this ViewSet
392	///
393	/// # Examples
394	///
395	/// ```
396	/// # use reinhardt_views::viewsets::{ModelViewSet, FilterConfig};
397	/// # use reinhardt_db::orm::{FieldSelector, Model};
398	/// # use serde::{Deserialize, Serialize};
399	/// # #[derive(Clone, Serialize, Deserialize)]
400	/// # struct Item { id: Option<i64> }
401	/// # #[derive(Clone)] struct ItemFields;
402	/// # impl FieldSelector for ItemFields { fn with_alias(self, _: &str) -> Self { self } }
403	/// # impl Model for Item {
404	/// #     type PrimaryKey = i64; type Fields = ItemFields;
405	/// #     fn table_name() -> &'static str { "items" }
406	/// #     fn primary_key(&self) -> Option<i64> { self.id }
407	/// #     fn set_primary_key(&mut self, v: i64) { self.id = Some(v); }
408	/// #     fn new_fields() -> Self::Fields { ItemFields }
409	/// # }
410	/// let viewset = ModelViewSet::<Item, ()>::new("items")
411	///     .with_filters(
412	///         FilterConfig::new()
413	///             .with_filterable_fields(vec!["status", "category"])
414	///             .with_search_fields(vec!["title", "description"])
415	///     );
416	/// ```
417	pub fn with_filters(mut self, config: FilterConfig) -> Self {
418		self.filter_config = Some(config);
419		self
420	}
421
422	/// Set ordering configuration for this ViewSet
423	///
424	/// # Examples
425	///
426	/// ```
427	/// # use reinhardt_views::viewsets::{ModelViewSet, OrderingConfig};
428	/// # use reinhardt_db::orm::{FieldSelector, Model};
429	/// # use serde::{Deserialize, Serialize};
430	/// # #[derive(Clone, Serialize, Deserialize)]
431	/// # struct Item { id: Option<i64> }
432	/// # #[derive(Clone)] struct ItemFields;
433	/// # impl FieldSelector for ItemFields { fn with_alias(self, _: &str) -> Self { self } }
434	/// # impl Model for Item {
435	/// #     type PrimaryKey = i64; type Fields = ItemFields;
436	/// #     fn table_name() -> &'static str { "items" }
437	/// #     fn primary_key(&self) -> Option<i64> { self.id }
438	/// #     fn set_primary_key(&mut self, v: i64) { self.id = Some(v); }
439	/// #     fn new_fields() -> Self::Fields { ItemFields }
440	/// # }
441	/// let viewset = ModelViewSet::<Item, ()>::new("items")
442	///     .with_ordering(
443	///         OrderingConfig::new()
444	///             .with_ordering_fields(vec!["created_at", "title", "id"])
445	///             .with_default_ordering(vec!["-created_at"])
446	///     );
447	/// ```
448	pub fn with_ordering(mut self, config: OrderingConfig) -> Self {
449		self.ordering_config = Some(config);
450		self
451	}
452
453	/// Set the database connection pool used by CRUD handlers.
454	///
455	/// Without a pool, list/retrieve fall back to the in-memory queryset (if
456	/// any), and create/update/destroy will operate only on the queryset.
457	pub fn with_pool(mut self, pool: Arc<sqlx::AnyPool>) -> Self {
458		self.handler = std::mem::take(&mut self.handler).with_pool(pool);
459		self
460	}
461
462	/// Set the database backend type (PostgreSQL, MySQL, SQLite).
463	pub fn with_db_backend(mut self, backend: DbBackend) -> Self {
464		self.handler = std::mem::take(&mut self.handler).with_db_backend(backend);
465		self
466	}
467
468	/// Set a custom serializer used by CRUD handlers.
469	pub fn with_serializer(
470		mut self,
471		serializer: Arc<dyn Serializer<Input = M, Output = String> + Send + Sync>,
472	) -> Self {
473		self.handler = std::mem::take(&mut self.handler).with_serializer(serializer);
474		self
475	}
476
477	/// Provide an in-memory queryset used when no database pool is set.
478	pub fn with_queryset(mut self, items: Vec<M>) -> Self {
479		self.handler = std::mem::take(&mut self.handler).with_queryset(items);
480		self
481	}
482
483	/// Add a permission class enforced before each request.
484	pub fn add_permission(mut self, permission: Arc<dyn Permission>) -> Self {
485		self.handler = std::mem::take(&mut self.handler).add_permission(permission);
486		self
487	}
488
489	/// Add a filter backend applied to list requests.
490	pub fn add_filter_backend(mut self, backend: Arc<dyn FilterBackend>) -> Self {
491		self.handler = std::mem::take(&mut self.handler).add_filter_backend(backend);
492		self
493	}
494
495	/// Convert ViewSet to Handler with action mapping
496	/// Returns a ViewSetBuilder for configuration
497	pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self> {
498		crate::viewsets::builder::ViewSetBuilder::new(self)
499	}
500}
501
502#[async_trait]
503impl<M, S> ViewSet for ModelViewSet<M, S>
504where
505	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
506	S: Send + Sync + 'static,
507{
508	fn get_basename(&self) -> &str {
509		&self.basename
510	}
511
512	fn get_lookup_field(&self) -> &str {
513		&self.lookup_field
514	}
515
516	async fn dispatch(&self, request: Request, action: Action) -> Result<Response> {
517		// Route to the embedded `ModelViewSetHandler<M>` for real CRUD.
518		// Path params have already been populated by the router using the
519		// `lookup_field` placeholder, e.g. `/items/{id}/`.
520		match (request.method.clone(), action.detail) {
521			(Method::GET, false) => self.handler.list(&request).await.map_err(Into::into),
522			(Method::POST, false) => self.handler.create(&request).await.map_err(Into::into),
523			(Method::GET, true) => {
524				let pk = extract_pk(&request, &self.lookup_field)?;
525				self.handler
526					.retrieve(&request, pk)
527					.await
528					.map_err(Into::into)
529			}
530			(Method::PUT, true) | (Method::PATCH, true) => {
531				let pk = extract_pk(&request, &self.lookup_field)?;
532				self.handler.update(&request, pk).await.map_err(Into::into)
533			}
534			(Method::DELETE, true) => {
535				let pk = extract_pk(&request, &self.lookup_field)?;
536				self.handler.destroy(&request, pk).await.map_err(Into::into)
537			}
538			_ => Err(method_not_allowed(&request.method)),
539		}
540	}
541}
542
543// Implement PaginatedViewSet for ModelViewSet
544impl<M, S> PaginatedViewSet for ModelViewSet<M, S>
545where
546	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
547	S: Send + Sync + 'static,
548{
549	fn get_pagination_config(&self) -> Option<PaginationConfig> {
550		self.pagination_config.clone()
551	}
552}
553
554/// `ReadOnlyModelViewSet` - exposes only `list` and `retrieve` against a real
555/// [`ModelViewSetHandler`].
556///
557/// Other HTTP verbs (POST/PUT/PATCH/DELETE) return `MethodNotAllowed`.
558pub struct ReadOnlyModelViewSet<M, S>
559where
560	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
561	S: Send + Sync + 'static,
562{
563	basename: String,
564	lookup_field: String,
565	pagination_config: Option<PaginationConfig>,
566	filter_config: Option<FilterConfig>,
567	ordering_config: Option<OrderingConfig>,
568	handler: ModelViewSetHandler<M>,
569	_serializer: PhantomData<S>,
570}
571
572impl<M, S> ReadOnlyModelViewSet<M, S>
573where
574	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
575	S: Send + Sync + 'static,
576{
577	/// Creates a new `ReadOnlyModelViewSet` with the given basename.
578	///
579	/// # Examples
580	///
581	/// ```
582	/// use reinhardt_views::viewsets::{ReadOnlyModelViewSet, ViewSet};
583	/// use reinhardt_db::prelude::Model;
584	/// use serde::{Serialize, Deserialize};
585	///
586	/// #[derive(Serialize, Deserialize, Clone, Debug)]
587	/// struct User {
588	///     id: Option<i64>,
589	///     username: String,
590	/// }
591	///
592	/// #[derive(Clone)]
593	/// struct UserFields;
594	///
595	/// impl reinhardt_db::orm::FieldSelector for UserFields {
596	///     fn with_alias(self, _alias: &str) -> Self { self }
597	/// }
598	///
599	/// impl Model for User {
600	///     type PrimaryKey = i64;
601	///     type Fields = UserFields;
602	///     fn table_name() -> &'static str { "users" }
603	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
604	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
605	///     fn new_fields() -> Self::Fields { UserFields }
606	/// }
607	///
608	/// let viewset = ReadOnlyModelViewSet::<User, reinhardt_rest::serializers::JsonSerializer<User>>::new("users");
609	/// assert_eq!(viewset.get_basename(), "users");
610	/// ```
611	pub fn new(basename: impl Into<String>) -> Self {
612		Self {
613			basename: basename.into(),
614			lookup_field: "id".to_string(),
615			pagination_config: Some(PaginationConfig::default()),
616			filter_config: None,
617			ordering_config: None,
618			handler: ModelViewSetHandler::<M>::new(),
619			_serializer: PhantomData,
620		}
621	}
622
623	/// Set custom lookup field for this ViewSet
624	pub fn with_lookup_field(mut self, field: impl Into<String>) -> Self {
625		self.lookup_field = field.into();
626		self
627	}
628
629	/// Set pagination configuration for this ViewSet
630	pub fn with_pagination(mut self, config: PaginationConfig) -> Self {
631		self.pagination_config = Some(config);
632		self
633	}
634
635	/// Disable pagination for this ViewSet
636	pub fn without_pagination(mut self) -> Self {
637		self.pagination_config = None;
638		self
639	}
640
641	/// Set filter configuration for this ViewSet
642	///
643	/// # Examples
644	///
645	/// ```ignore
646	/// use reinhardt_views::viewsets::{ReadOnlyModelViewSet, FilterConfig};
647	///
648	/// let viewset = ReadOnlyModelViewSet::<MyModel, MySerializer>::new("items")
649	///     .with_filters(
650	///         FilterConfig::new()
651	///             .with_filterable_fields(vec!["status", "category"])
652	///             .with_search_fields(vec!["title", "description"])
653	///     );
654	/// ```
655	pub fn with_filters(mut self, config: FilterConfig) -> Self {
656		self.filter_config = Some(config);
657		self
658	}
659
660	/// Set ordering configuration for this ViewSet
661	///
662	/// # Examples
663	///
664	/// ```ignore
665	/// use reinhardt_views::viewsets::{ReadOnlyModelViewSet, OrderingConfig};
666	///
667	/// let viewset = ReadOnlyModelViewSet::<MyModel, MySerializer>::new("items")
668	///     .with_ordering(
669	///         OrderingConfig::new()
670	///             .with_ordering_fields(vec!["created_at", "title"])
671	///             .with_default_ordering(vec!["-created_at"])
672	///     );
673	/// ```
674	pub fn with_ordering(mut self, config: OrderingConfig) -> Self {
675		self.ordering_config = Some(config);
676		self
677	}
678
679	/// Set the database connection pool used by read handlers.
680	pub fn with_pool(mut self, pool: Arc<sqlx::AnyPool>) -> Self {
681		self.handler = std::mem::take(&mut self.handler).with_pool(pool);
682		self
683	}
684
685	/// Set the database backend type (PostgreSQL, MySQL, SQLite).
686	pub fn with_db_backend(mut self, backend: DbBackend) -> Self {
687		self.handler = std::mem::take(&mut self.handler).with_db_backend(backend);
688		self
689	}
690
691	/// Set a custom serializer used by read handlers.
692	pub fn with_serializer(
693		mut self,
694		serializer: Arc<dyn Serializer<Input = M, Output = String> + Send + Sync>,
695	) -> Self {
696		self.handler = std::mem::take(&mut self.handler).with_serializer(serializer);
697		self
698	}
699
700	/// Provide an in-memory queryset used when no database pool is set.
701	pub fn with_queryset(mut self, items: Vec<M>) -> Self {
702		self.handler = std::mem::take(&mut self.handler).with_queryset(items);
703		self
704	}
705
706	/// Add a permission class enforced before each request.
707	pub fn add_permission(mut self, permission: Arc<dyn Permission>) -> Self {
708		self.handler = std::mem::take(&mut self.handler).add_permission(permission);
709		self
710	}
711
712	/// Add a filter backend applied to list requests.
713	pub fn add_filter_backend(mut self, backend: Arc<dyn FilterBackend>) -> Self {
714		self.handler = std::mem::take(&mut self.handler).add_filter_backend(backend);
715		self
716	}
717
718	/// Convert ViewSet to Handler with action mapping
719	/// Returns a ViewSetBuilder for configuration
720	pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self> {
721		crate::viewsets::builder::ViewSetBuilder::new(self)
722	}
723}
724
725#[async_trait]
726impl<M, S> ViewSet for ReadOnlyModelViewSet<M, S>
727where
728	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
729	S: Send + Sync + 'static,
730{
731	fn get_basename(&self) -> &str {
732		&self.basename
733	}
734
735	fn get_lookup_field(&self) -> &str {
736		&self.lookup_field
737	}
738
739	async fn dispatch(&self, request: Request, action: Action) -> Result<Response> {
740		match (request.method.clone(), action.detail) {
741			(Method::GET, false) => self.handler.list(&request).await.map_err(Into::into),
742			(Method::GET, true) => {
743				let pk = extract_pk(&request, &self.lookup_field)?;
744				self.handler
745					.retrieve(&request, pk)
746					.await
747					.map_err(Into::into)
748			}
749			_ => Err(method_not_allowed(&request.method)),
750		}
751	}
752}
753
754// Implement PaginatedViewSet for ReadOnlyModelViewSet
755impl<M, S> PaginatedViewSet for ReadOnlyModelViewSet<M, S>
756where
757	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
758	S: Send + Sync + 'static,
759{
760	fn get_pagination_config(&self) -> Option<PaginationConfig> {
761		self.pagination_config.clone()
762	}
763}
764
765// Implement FilterableViewSet for ReadOnlyModelViewSet
766impl<M, S> FilterableViewSet for ReadOnlyModelViewSet<M, S>
767where
768	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
769	S: Send + Sync + 'static,
770{
771	fn get_filter_config(&self) -> Option<FilterConfig> {
772		self.filter_config.clone()
773	}
774
775	fn get_ordering_config(&self) -> Option<OrderingConfig> {
776		self.ordering_config.clone()
777	}
778}
779
780// Manually re-assert the `UnwindSafe` / `RefUnwindSafe` auto traits for the
781// public viewset structs. The new `Arc<dyn Serializer ...>` / `Arc<dyn
782// Permission>` / `Arc<dyn FilterBackend>` fields introduced by this PR do
783// not propagate these markers because trait objects do not implement them
784// by default, which would otherwise surface as cargo-semver-checks
785// `auto_trait_impl_removed` under the RC phase's no-breaking-change policy.
786// The trait objects are only accessed via `&self` / `Arc::clone`, and the
787// `Send + Sync` supertraits already guarantee thread safety, so manually
788// re-implementing the markers preserves the pre-PR public-API contract.
789impl<M, S> std::panic::UnwindSafe for ModelViewSet<M, S>
790where
791	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
792	S: Send + Sync + 'static,
793{
794}
795impl<M, S> std::panic::RefUnwindSafe for ModelViewSet<M, S>
796where
797	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
798	S: Send + Sync + 'static,
799{
800}
801
802impl<M, S> std::panic::UnwindSafe for ReadOnlyModelViewSet<M, S>
803where
804	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
805	S: Send + Sync + 'static,
806{
807}
808impl<M, S> std::panic::RefUnwindSafe for ReadOnlyModelViewSet<M, S>
809where
810	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
811	S: Send + Sync + 'static,
812{
813}
814
815#[cfg(test)]
816mod tests {
817	use super::*;
818	use hyper::Method;
819	use reinhardt_db::orm::{FieldSelector, Model};
820	use serde::{Deserialize, Serialize};
821	use std::collections::HashMap;
822	use std::sync::Arc;
823
824	/// Minimal `Model` implementation used to satisfy the `ModelViewSet` trait
825	/// bounds in unit tests. The previous tests used `ModelViewSet::<(), ()>`,
826	/// but bare `()` does not implement `Model` once the bounds were tightened.
827	#[derive(Debug, Clone, Serialize, Deserialize)]
828	struct DummyModel {
829		id: Option<i64>,
830	}
831
832	#[derive(Clone)]
833	struct DummyFields;
834
835	impl FieldSelector for DummyFields {
836		fn with_alias(self, _alias: &str) -> Self {
837			self
838		}
839	}
840
841	impl Model for DummyModel {
842		type PrimaryKey = i64;
843		type Fields = DummyFields;
844		fn table_name() -> &'static str {
845			"dummy"
846		}
847		fn primary_key(&self) -> Option<Self::PrimaryKey> {
848			self.id
849		}
850		fn set_primary_key(&mut self, value: Self::PrimaryKey) {
851			self.id = Some(value);
852		}
853		fn new_fields() -> Self::Fields {
854			DummyFields
855		}
856	}
857
858	#[tokio::test]
859	async fn test_viewset_builder_validation_empty_actions() {
860		let viewset = ModelViewSet::<DummyModel, ()>::new("test");
861		let builder = viewset.as_view();
862
863		// Test that empty actions causes build to fail
864		let result = builder.build();
865		assert!(result.is_err());
866
867		// Check error message without unwrapping
868		match result {
869			Err(e) => assert!(
870				e.to_string()
871					.contains("The `actions` argument must be provided")
872			),
873			Ok(_) => panic!("Expected error but got success"),
874		}
875	}
876
877	#[tokio::test]
878	async fn test_viewset_builder_name_suffix_mutual_exclusivity() {
879		let viewset = ModelViewSet::<DummyModel, ()>::new("test");
880		let builder = viewset.as_view();
881
882		// Test that providing both name and suffix fails
883		let result = builder
884			.with_name("test_name")
885			.and_then(|b| b.with_suffix("test_suffix"));
886
887		assert!(result.is_err());
888
889		// Check error message without unwrapping
890		match result {
891			Err(e) => assert!(e.to_string().contains("received both `name` and `suffix`")),
892			Ok(_) => panic!("Expected error but got success"),
893		}
894	}
895
896	#[tokio::test]
897	async fn test_viewset_builder_successful_build() {
898		let viewset = ModelViewSet::<DummyModel, ()>::new("test");
899		let mut actions = HashMap::new();
900		actions.insert(Method::GET, "list".to_string());
901
902		let builder = viewset.as_view();
903		let result = builder.with_actions(actions).build();
904
905		let handler = result.unwrap();
906
907		// Test that handler is created successfully
908		// Handler should be created without errors
909		assert!(Arc::strong_count(&handler) > 0);
910	}
911
912	#[tokio::test]
913	async fn test_viewset_builder_with_name() {
914		let viewset = ModelViewSet::<DummyModel, ()>::new("test");
915		let mut actions = HashMap::new();
916		actions.insert(Method::GET, "list".to_string());
917
918		let builder = viewset.as_view();
919		let result = builder
920			.with_actions(actions)
921			.with_name("test_view")
922			.and_then(|b| b.build());
923
924		assert!(result.is_ok());
925	}
926
927	#[tokio::test]
928	async fn test_viewset_builder_with_suffix() {
929		let viewset = ModelViewSet::<DummyModel, ()>::new("test");
930		let mut actions = HashMap::new();
931		actions.insert(Method::GET, "list".to_string());
932
933		let builder = viewset.as_view();
934		let result = builder
935			.with_actions(actions)
936			.with_suffix("_list")
937			.and_then(|b| b.build());
938
939		assert!(result.is_ok());
940	}
941}