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, ViewError};
4use crate::viewsets::metadata::{ActionMetadata, get_actions_for_viewset};
5use crate::viewsets::middleware::{CompositeMiddleware, ViewSetMiddleware};
6use crate::viewsets::pagination_support::{PaginatedViewSet, PaginationConfig};
7use async_trait::async_trait;
8use hyper::Method;
9use reinhardt_auth::Permission;
10use reinhardt_db::orm::{FilterCondition, 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	///
93	/// The default implementation enforces [`Self::requires_login`] and
94	/// [`Self::get_required_permissions`]. Implementations that override this
95	/// method are responsible for composing those declarations into the returned
96	/// middleware.
97	fn get_middleware(&self) -> Option<Arc<dyn ViewSetMiddleware>> {
98		let permissions = self.get_required_permissions();
99		if !self.requires_login() && permissions.is_empty() {
100			return None;
101		}
102
103		let mut middleware = CompositeMiddleware::new();
104		if self.requires_login() {
105			middleware = middleware.with_authentication(true);
106		}
107		if !permissions.is_empty() {
108			middleware = middleware.with_permissions(permissions);
109		}
110		Some(Arc::new(middleware))
111	}
112
113	/// Check if login is required for this ViewSet
114	fn requires_login(&self) -> bool {
115		false
116	}
117
118	/// Get required permissions for this ViewSet
119	fn get_required_permissions(&self) -> Vec<String> {
120		Vec::new()
121	}
122}
123
124/// Generic ViewSet without built-in CRUD logic.
125///
126/// `GenericViewSet<T>` is an extensibility hook for users who want to build a
127/// `ViewSet` from scratch with their own dispatch logic. It does **not**
128/// perform any CRUD by itself; calling `dispatch()` on a bare `GenericViewSet`
129/// always returns a `NotFound` error with guidance pointing to the correct
130/// abstractions.
131///
132/// # Choosing the right ViewSet
133///
134/// - For automatic CRUD against a database `Model`, use [`ModelViewSet`].
135/// - For read-only access (list + retrieve only), use [`ReadOnlyModelViewSet`].
136/// - For fully custom behavior, define your own type and `impl ViewSet for YourType`
137///   with a hand-written `dispatch()`. `GenericViewSet` is rarely the right choice.
138///
139/// # Example: composing a custom ViewSet via the builder
140///
141/// ```
142/// use reinhardt_views::viewsets::{GenericViewSet, ViewSet};
143///
144/// let viewset = GenericViewSet::new("widgets", ());
145/// assert_eq!(viewset.get_basename(), "widgets");
146/// ```
147// Allow dead_code: generic container for composable ViewSet implementations via trait bounds
148#[allow(dead_code)]
149#[derive(Clone)]
150pub struct GenericViewSet<T> {
151	basename: String,
152	handler: T,
153}
154
155impl<T: 'static> GenericViewSet<T> {
156	/// Creates a new `GenericViewSet` with the given basename and handler.
157	///
158	/// # Examples
159	///
160	/// ```
161	/// use reinhardt_views::viewsets::{GenericViewSet, ViewSet};
162	///
163	/// let viewset = GenericViewSet::new("users", ());
164	/// assert_eq!(viewset.get_basename(), "users");
165	/// ```
166	pub fn new(basename: impl Into<String>, handler: T) -> Self {
167		Self {
168			basename: basename.into(),
169			handler,
170		}
171	}
172
173	/// Convert ViewSet to Handler with action mapping
174	/// Returns a ViewSetBuilder for configuration
175	///
176	/// # Examples
177	///
178	/// ```ignore
179	/// use reinhardt_views::{viewset_actions, viewsets::GenericViewSet};
180	/// use hyper::Method;
181	///
182	/// let viewset = GenericViewSet::new("users", ());
183	/// let actions = viewset_actions!(GET => "list");
184	/// let handler = viewset.as_view().with_actions(actions).build();
185	/// ```
186	pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self>
187	where
188		T: Send + Sync,
189	{
190		crate::viewsets::builder::ViewSetBuilder::new(self)
191	}
192}
193
194#[async_trait]
195impl<T: Send + Sync> ViewSet for GenericViewSet<T> {
196	fn get_basename(&self) -> &str {
197		&self.basename
198	}
199
200	async fn dispatch(&self, _request: Request, action: Action) -> Result<Response> {
201		// `GenericViewSet` carries no built-in CRUD logic on purpose. Users who
202		// reach this point typically need one of the concrete ViewSets that *do*
203		// implement CRUD, or a hand-written `impl ViewSet` on their own type.
204		// Returning a guidance-rich error avoids silent placeholder responses
205		// (the regression class behind issue #3985).
206		Err(reinhardt_core::exception::Error::NotFound(format!(
207			"GenericViewSet has no built-in CRUD logic for action {:?}. \
208			 For real CRUD, use ModelViewSet<M, S> or ReadOnlyModelViewSet<M, S>. \
209			 To implement custom logic, define your own struct and \
210			 `impl ViewSet for YourType` with a hand-written dispatch().",
211			action.action_type
212		)))
213	}
214}
215
216/// `ModelViewSet` - combines all CRUD mixins, backed by a real
217/// [`ModelViewSetHandler`] for database-backed CRUD.
218///
219/// Similar to Django REST Framework's `ModelViewSet` but built around Rust
220/// type composition. `dispatch()` routes the standard REST verbs to the
221/// embedded handler's `list` / `retrieve` / `create` / `update` / `destroy`
222/// methods, so registering a `ModelViewSet` with a router yields actual
223/// model-backed responses (not placeholders).
224pub struct ModelViewSet<M, S>
225where
226	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
227	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
228{
229	basename: String,
230	lookup_field: String,
231	pagination_config: Option<PaginationConfig>,
232	filter_config: Option<FilterConfig>,
233	ordering_config: Option<OrderingConfig>,
234	handler: ModelViewSetHandler<M>,
235	_serializer: PhantomData<S>,
236}
237
238// Implement FilterableViewSet for ModelViewSet
239impl<M, S> FilterableViewSet for ModelViewSet<M, S>
240where
241	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
242	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
243{
244	fn get_filter_config(&self) -> Option<FilterConfig> {
245		self.filter_config.clone()
246	}
247
248	fn get_ordering_config(&self) -> Option<OrderingConfig> {
249		self.ordering_config.clone()
250	}
251}
252
253impl<M, S> ModelViewSet<M, S>
254where
255	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
256	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
257{
258	/// Creates a new `ModelViewSet` with the given basename.
259	///
260	/// # Examples
261	///
262	/// ```
263	/// use reinhardt_views::viewsets::{ModelViewSet, ViewSet};
264	/// use reinhardt_db::prelude::Model;
265	/// use serde::{Serialize, Deserialize};
266	///
267	/// #[derive(Serialize, Deserialize, Clone, Debug)]
268	/// struct User {
269	///     id: Option<i64>,
270	///     username: String,
271	/// }
272	///
273	/// #[derive(Clone)]
274	/// struct UserFields;
275	///
276	/// impl reinhardt_db::orm::FieldSelector for UserFields {
277	///     fn with_alias(self, _alias: &str) -> Self { self }
278	/// }
279	///
280	/// impl Model for User {
281	///     type PrimaryKey = i64;
282	///     type Fields = UserFields;
283	///     type Objects = reinhardt_db::orm::Manager<Self>;
284	///     fn table_name() -> &'static str { "users" }
285	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
286	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
287	///     fn new_fields() -> Self::Fields { UserFields }
288	/// }
289	///
290	/// let viewset = ModelViewSet::<User, reinhardt_rest::serializers::JsonSerializer<User>>::new("users");
291	/// assert_eq!(viewset.get_basename(), "users");
292	/// ```
293	pub fn new(basename: impl Into<String>) -> Self {
294		Self {
295			basename: basename.into(),
296			lookup_field: "id".to_string(),
297			pagination_config: Some(PaginationConfig::default()),
298			filter_config: None,
299			ordering_config: None,
300			handler: ModelViewSetHandler::<M>::new().with_serializer(Arc::new(S::default())),
301			_serializer: PhantomData,
302		}
303	}
304
305	/// Set the model field used by detail routes and object queries.
306	///
307	/// # Examples
308	///
309	/// ```
310	/// use reinhardt_views::viewsets::{ModelViewSet, ViewSet};
311	/// use reinhardt_db::prelude::Model;
312	/// use serde::{Serialize, Deserialize};
313	///
314	/// #[derive(Serialize, Deserialize, Clone, Debug)]
315	/// struct User {
316	///     id: Option<i64>,
317	///     username: String,
318	/// }
319	///
320	/// #[derive(Clone)]
321	/// struct UserFields;
322	///
323	/// impl reinhardt_db::orm::FieldSelector for UserFields {
324	///     fn with_alias(self, _alias: &str) -> Self { self }
325	/// }
326	///
327	/// impl Model for User {
328	///     type PrimaryKey = i64;
329	///     type Fields = UserFields;
330	///     type Objects = reinhardt_db::orm::Manager<Self>;
331	///     fn table_name() -> &'static str { "users" }
332	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
333	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
334	///     fn new_fields() -> Self::Fields { UserFields }
335	/// }
336	///
337	/// let viewset = ModelViewSet::<User, reinhardt_rest::serializers::JsonSerializer<User>>::new("users")
338	///     .with_lookup_field("username");
339	/// assert_eq!(viewset.get_lookup_field(), "username");
340	/// ```
341	pub fn with_lookup_field(mut self, field: impl Into<String>) -> Self {
342		self.lookup_field = field.into();
343		self.handler =
344			std::mem::take(&mut self.handler).with_lookup_field(self.lookup_field.clone());
345		self
346	}
347
348	/// Set pagination configuration for this ViewSet
349	///
350	/// # Examples
351	///
352	/// ```
353	/// # use reinhardt_views::viewsets::{ModelViewSet, PaginationConfig};
354	/// # use reinhardt_db::orm::{FieldSelector, Model};
355	/// # use serde::{Deserialize, Serialize};
356	/// # #[derive(Clone, Serialize, Deserialize)]
357	/// # struct Item { id: Option<i64> }
358	/// # #[derive(Clone)] struct ItemFields;
359	/// # impl FieldSelector for ItemFields { fn with_alias(self, _: &str) -> Self { self } }
360	/// # impl Model for Item {
361	/// #     type PrimaryKey = i64; type Fields = ItemFields;
362	/// #     type Objects = reinhardt_db::orm::Manager<Self>;
363	/// #     fn table_name() -> &'static str { "items" }
364	/// #     fn primary_key(&self) -> Option<i64> { self.id }
365	/// #     fn set_primary_key(&mut self, v: i64) { self.id = Some(v); }
366	/// #     fn new_fields() -> Self::Fields { ItemFields }
367	/// # }
368	/// // Page number pagination with custom page size
369	/// let viewset = ModelViewSet::<Item, reinhardt_rest::serializers::JsonSerializer<Item>>::new("items")
370	///     .with_pagination(PaginationConfig::page_number(20, Some(100)));
371	///
372	/// // Limit/offset pagination
373	/// let viewset = ModelViewSet::<Item, reinhardt_rest::serializers::JsonSerializer<Item>>::new("items")
374	///     .with_pagination(PaginationConfig::limit_offset(25, Some(500)));
375	///
376	/// // Disable pagination
377	/// let viewset = ModelViewSet::<Item, reinhardt_rest::serializers::JsonSerializer<Item>>::new("items")
378	///     .with_pagination(PaginationConfig::none());
379	/// ```
380	pub fn with_pagination(mut self, config: PaginationConfig) -> Self {
381		self.pagination_config = Some(config);
382		self
383	}
384
385	/// Disable pagination for this ViewSet
386	///
387	/// # Examples
388	///
389	/// ```
390	/// # use reinhardt_views::viewsets::ModelViewSet;
391	/// # use reinhardt_db::orm::{FieldSelector, Model};
392	/// # use serde::{Deserialize, Serialize};
393	/// # #[derive(Clone, Serialize, Deserialize)]
394	/// # struct Item { id: Option<i64> }
395	/// # #[derive(Clone)] struct ItemFields;
396	/// # impl FieldSelector for ItemFields { fn with_alias(self, _: &str) -> Self { self } }
397	/// # impl Model for Item {
398	/// #     type PrimaryKey = i64; type Fields = ItemFields;
399	/// #     type Objects = reinhardt_db::orm::Manager<Self>;
400	/// #     fn table_name() -> &'static str { "items" }
401	/// #     fn primary_key(&self) -> Option<i64> { self.id }
402	/// #     fn set_primary_key(&mut self, v: i64) { self.id = Some(v); }
403	/// #     fn new_fields() -> Self::Fields { ItemFields }
404	/// # }
405	/// let viewset = ModelViewSet::<Item, reinhardt_rest::serializers::JsonSerializer<Item>>::new("items")
406	///     .without_pagination();
407	/// ```
408	pub fn without_pagination(mut self) -> Self {
409		self.pagination_config = None;
410		self
411	}
412
413	/// Set filter configuration for this ViewSet
414	///
415	/// # Examples
416	///
417	/// ```
418	/// # use reinhardt_views::viewsets::{ModelViewSet, FilterConfig};
419	/// # use reinhardt_db::orm::{FieldSelector, Model};
420	/// # use serde::{Deserialize, Serialize};
421	/// # #[derive(Clone, Serialize, Deserialize)]
422	/// # struct Item { id: Option<i64> }
423	/// # #[derive(Clone)] struct ItemFields;
424	/// # impl FieldSelector for ItemFields { fn with_alias(self, _: &str) -> Self { self } }
425	/// # impl Model for Item {
426	/// #     type PrimaryKey = i64; type Fields = ItemFields;
427	/// #     type Objects = reinhardt_db::orm::Manager<Self>;
428	/// #     fn table_name() -> &'static str { "items" }
429	/// #     fn primary_key(&self) -> Option<i64> { self.id }
430	/// #     fn set_primary_key(&mut self, v: i64) { self.id = Some(v); }
431	/// #     fn new_fields() -> Self::Fields { ItemFields }
432	/// # }
433	/// let viewset = ModelViewSet::<Item, reinhardt_rest::serializers::JsonSerializer<Item>>::new("items")
434	///     .with_filters(
435	///         FilterConfig::new()
436	///             .with_filterable_fields(vec!["status", "category"])
437	///             .with_search_fields(vec!["title", "description"])
438	///     );
439	/// ```
440	pub fn with_filters(mut self, config: FilterConfig) -> Self {
441		self.filter_config = Some(config);
442		self
443	}
444
445	/// Set ordering configuration for this ViewSet
446	///
447	/// # Examples
448	///
449	/// ```
450	/// # use reinhardt_views::viewsets::{ModelViewSet, OrderingConfig};
451	/// # use reinhardt_db::orm::{FieldSelector, Model};
452	/// # use serde::{Deserialize, Serialize};
453	/// # #[derive(Clone, Serialize, Deserialize)]
454	/// # struct Item { id: Option<i64> }
455	/// # #[derive(Clone)] struct ItemFields;
456	/// # impl FieldSelector for ItemFields { fn with_alias(self, _: &str) -> Self { self } }
457	/// # impl Model for Item {
458	/// #     type PrimaryKey = i64; type Fields = ItemFields;
459	/// #     type Objects = reinhardt_db::orm::Manager<Self>;
460	/// #     fn table_name() -> &'static str { "items" }
461	/// #     fn primary_key(&self) -> Option<i64> { self.id }
462	/// #     fn set_primary_key(&mut self, v: i64) { self.id = Some(v); }
463	/// #     fn new_fields() -> Self::Fields { ItemFields }
464	/// # }
465	/// let viewset = ModelViewSet::<Item, reinhardt_rest::serializers::JsonSerializer<Item>>::new("items")
466	///     .with_ordering(
467	///         OrderingConfig::new()
468	///             .with_ordering_fields(vec!["created_at", "title", "id"])
469	///             .with_default_ordering(vec!["-created_at"])
470	///     );
471	/// ```
472	pub fn with_ordering(mut self, config: OrderingConfig) -> Self {
473		self.ordering_config = Some(config);
474		self
475	}
476
477	/// Set the database connection pool used by CRUD handlers.
478	///
479	/// Without a pool, list/retrieve fall back to the in-memory queryset (if
480	/// any), and create/update/destroy will operate only on the queryset.
481	pub fn with_pool(mut self, pool: Arc<sqlx::AnyPool>) -> Self {
482		self.handler = std::mem::take(&mut self.handler).with_pool(pool);
483		self
484	}
485
486	/// Set the database backend type (PostgreSQL, MySQL, SQLite).
487	pub fn with_db_backend(mut self, backend: DbBackend) -> Self {
488		self.handler = std::mem::take(&mut self.handler).with_db_backend(backend);
489		self
490	}
491
492	/// Set a custom serializer used by CRUD handlers.
493	pub fn with_serializer(
494		mut self,
495		serializer: Arc<dyn Serializer<Input = M, Output = String> + Send + Sync>,
496	) -> Self {
497		self.handler = std::mem::take(&mut self.handler).with_serializer(serializer);
498		self
499	}
500
501	/// Provide an in-memory queryset used when no database pool is set.
502	pub fn with_queryset(mut self, items: Vec<M>) -> Self {
503		self.handler = std::mem::take(&mut self.handler).with_queryset(items);
504		self
505	}
506
507	/// Scope database queries using the current request.
508	///
509	/// The synchronous, fallible hook returns one [`FilterCondition`] and requires
510	/// [`Self::with_pool`]. It scopes list, retrieve, update, and destroy, but not
511	/// create; assign ownership during create in the serializer, permission layer,
512	/// or database. Middleware must resolve asynchronous scope data before
513	/// dispatch, and the hook reads application-defined request extensions.
514	/// [`Self::with_queryset`] static `Vec` data is separate and is not filtered.
515	/// Scoped-out objects and malformed detail lookup values produce 404.
516	pub fn with_queryset_fn<F>(mut self, queryset_fn: F) -> Self
517	where
518		F: Fn(&Request) -> std::result::Result<FilterCondition, ViewError> + Send + Sync + 'static,
519	{
520		self.handler = std::mem::take(&mut self.handler).with_queryset_fn(queryset_fn);
521		self
522	}
523
524	/// Add a permission class enforced before each request.
525	pub fn add_permission(mut self, permission: Arc<dyn Permission>) -> Self {
526		self.handler = std::mem::take(&mut self.handler).add_permission(permission);
527		self
528	}
529
530	/// Add a filter backend applied to list requests.
531	pub fn add_filter_backend(mut self, backend: Arc<dyn FilterBackend>) -> Self {
532		self.handler = std::mem::take(&mut self.handler).add_filter_backend(backend);
533		self
534	}
535
536	/// Convert ViewSet to Handler with action mapping
537	/// Returns a ViewSetBuilder for configuration
538	pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self> {
539		crate::viewsets::builder::ViewSetBuilder::new(self)
540	}
541}
542
543#[async_trait]
544impl<M, S> ViewSet for ModelViewSet<M, S>
545where
546	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
547	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
548{
549	fn get_basename(&self) -> &str {
550		&self.basename
551	}
552
553	fn get_lookup_field(&self) -> &str {
554		&self.lookup_field
555	}
556
557	async fn dispatch(&self, request: Request, action: Action) -> Result<Response> {
558		// Route to the embedded `ModelViewSetHandler<M>` for real CRUD.
559		// Path params have already been populated by the router using the
560		// `lookup_field` placeholder, e.g. `/items/{id}/`.
561		match (request.method.clone(), action.detail) {
562			(Method::GET, false) => self.handler.list(&request).await.map_err(Into::into),
563			(Method::POST, false) => self.handler.create(&request).await.map_err(Into::into),
564			(Method::GET, true) => {
565				let pk = extract_pk(&request, &self.lookup_field)?;
566				self.handler
567					.retrieve(&request, pk)
568					.await
569					.map_err(Into::into)
570			}
571			(Method::PUT, true) | (Method::PATCH, true) => {
572				let pk = extract_pk(&request, &self.lookup_field)?;
573				self.handler.update(&request, pk).await.map_err(Into::into)
574			}
575			(Method::DELETE, true) => {
576				let pk = extract_pk(&request, &self.lookup_field)?;
577				self.handler.destroy(&request, pk).await.map_err(Into::into)
578			}
579			_ => Err(method_not_allowed(&request.method)),
580		}
581	}
582}
583
584// Implement PaginatedViewSet for ModelViewSet
585impl<M, S> PaginatedViewSet for ModelViewSet<M, S>
586where
587	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
588	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
589{
590	fn get_pagination_config(&self) -> Option<PaginationConfig> {
591		self.pagination_config.clone()
592	}
593}
594
595/// `ReadOnlyModelViewSet` - exposes only `list` and `retrieve` against a real
596/// [`ModelViewSetHandler`].
597///
598/// Other HTTP verbs (POST/PUT/PATCH/DELETE) return `MethodNotAllowed`.
599pub struct ReadOnlyModelViewSet<M, S>
600where
601	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
602	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
603{
604	basename: String,
605	lookup_field: String,
606	pagination_config: Option<PaginationConfig>,
607	filter_config: Option<FilterConfig>,
608	ordering_config: Option<OrderingConfig>,
609	handler: ModelViewSetHandler<M>,
610	_serializer: PhantomData<S>,
611}
612
613impl<M, S> ReadOnlyModelViewSet<M, S>
614where
615	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
616	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
617{
618	/// Creates a new `ReadOnlyModelViewSet` with the given basename.
619	///
620	/// # Examples
621	///
622	/// ```
623	/// use reinhardt_views::viewsets::{ReadOnlyModelViewSet, ViewSet};
624	/// use reinhardt_db::prelude::Model;
625	/// use serde::{Serialize, Deserialize};
626	///
627	/// #[derive(Serialize, Deserialize, Clone, Debug)]
628	/// struct User {
629	///     id: Option<i64>,
630	///     username: String,
631	/// }
632	///
633	/// #[derive(Clone)]
634	/// struct UserFields;
635	///
636	/// impl reinhardt_db::orm::FieldSelector for UserFields {
637	///     fn with_alias(self, _alias: &str) -> Self { self }
638	/// }
639	///
640	/// impl Model for User {
641	///     type PrimaryKey = i64;
642	///     type Fields = UserFields;
643	///     type Objects = reinhardt_db::orm::Manager<Self>;
644	///     fn table_name() -> &'static str { "users" }
645	///     fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
646	///     fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
647	///     fn new_fields() -> Self::Fields { UserFields }
648	/// }
649	///
650	/// let viewset = ReadOnlyModelViewSet::<User, reinhardt_rest::serializers::JsonSerializer<User>>::new("users");
651	/// assert_eq!(viewset.get_basename(), "users");
652	/// ```
653	pub fn new(basename: impl Into<String>) -> Self {
654		Self {
655			basename: basename.into(),
656			lookup_field: "id".to_string(),
657			pagination_config: Some(PaginationConfig::default()),
658			filter_config: None,
659			ordering_config: None,
660			handler: ModelViewSetHandler::<M>::new().with_serializer(Arc::new(S::default())),
661			_serializer: PhantomData,
662		}
663	}
664
665	/// Set the model field used by detail routes and object queries.
666	pub fn with_lookup_field(mut self, field: impl Into<String>) -> Self {
667		self.lookup_field = field.into();
668		self.handler =
669			std::mem::take(&mut self.handler).with_lookup_field(self.lookup_field.clone());
670		self
671	}
672
673	/// Set pagination configuration for this ViewSet
674	pub fn with_pagination(mut self, config: PaginationConfig) -> Self {
675		self.pagination_config = Some(config);
676		self
677	}
678
679	/// Disable pagination for this ViewSet
680	pub fn without_pagination(mut self) -> Self {
681		self.pagination_config = None;
682		self
683	}
684
685	/// Set filter configuration for this ViewSet
686	///
687	/// # Examples
688	///
689	/// ```ignore
690	/// use reinhardt_views::viewsets::{ReadOnlyModelViewSet, FilterConfig};
691	///
692	/// let viewset = ReadOnlyModelViewSet::<MyModel, MySerializer>::new("items")
693	///     .with_filters(
694	///         FilterConfig::new()
695	///             .with_filterable_fields(vec!["status", "category"])
696	///             .with_search_fields(vec!["title", "description"])
697	///     );
698	/// ```
699	pub fn with_filters(mut self, config: FilterConfig) -> Self {
700		self.filter_config = Some(config);
701		self
702	}
703
704	/// Set ordering configuration for this ViewSet
705	///
706	/// # Examples
707	///
708	/// ```ignore
709	/// use reinhardt_views::viewsets::{ReadOnlyModelViewSet, OrderingConfig};
710	///
711	/// let viewset = ReadOnlyModelViewSet::<MyModel, MySerializer>::new("items")
712	///     .with_ordering(
713	///         OrderingConfig::new()
714	///             .with_ordering_fields(vec!["created_at", "title"])
715	///             .with_default_ordering(vec!["-created_at"])
716	///     );
717	/// ```
718	pub fn with_ordering(mut self, config: OrderingConfig) -> Self {
719		self.ordering_config = Some(config);
720		self
721	}
722
723	/// Set the database connection pool used by read handlers.
724	pub fn with_pool(mut self, pool: Arc<sqlx::AnyPool>) -> Self {
725		self.handler = std::mem::take(&mut self.handler).with_pool(pool);
726		self
727	}
728
729	/// Set the database backend type (PostgreSQL, MySQL, SQLite).
730	pub fn with_db_backend(mut self, backend: DbBackend) -> Self {
731		self.handler = std::mem::take(&mut self.handler).with_db_backend(backend);
732		self
733	}
734
735	/// Set a custom serializer used by read handlers.
736	pub fn with_serializer(
737		mut self,
738		serializer: Arc<dyn Serializer<Input = M, Output = String> + Send + Sync>,
739	) -> Self {
740		self.handler = std::mem::take(&mut self.handler).with_serializer(serializer);
741		self
742	}
743
744	/// Provide an in-memory queryset used when no database pool is set.
745	pub fn with_queryset(mut self, items: Vec<M>) -> Self {
746		self.handler = std::mem::take(&mut self.handler).with_queryset(items);
747		self
748	}
749
750	/// Scope database queries using the current request.
751	///
752	/// The synchronous, fallible hook returns one [`FilterCondition`] and requires
753	/// [`Self::with_pool`]. It scopes list and retrieve. Middleware must resolve
754	/// asynchronous scope data before dispatch, and the hook reads
755	/// application-defined request extensions. [`Self::with_queryset`] static
756	/// `Vec` data is separate and is not filtered. Scoped-out objects and malformed
757	/// detail lookup values produce 404.
758	pub fn with_queryset_fn<F>(mut self, queryset_fn: F) -> Self
759	where
760		F: Fn(&Request) -> std::result::Result<FilterCondition, ViewError> + Send + Sync + 'static,
761	{
762		self.handler = std::mem::take(&mut self.handler).with_queryset_fn(queryset_fn);
763		self
764	}
765
766	/// Add a permission class enforced before each request.
767	pub fn add_permission(mut self, permission: Arc<dyn Permission>) -> Self {
768		self.handler = std::mem::take(&mut self.handler).add_permission(permission);
769		self
770	}
771
772	/// Add a filter backend applied to list requests.
773	pub fn add_filter_backend(mut self, backend: Arc<dyn FilterBackend>) -> Self {
774		self.handler = std::mem::take(&mut self.handler).add_filter_backend(backend);
775		self
776	}
777
778	/// Convert ViewSet to Handler with action mapping
779	/// Returns a ViewSetBuilder for configuration
780	pub fn as_view(self) -> crate::viewsets::builder::ViewSetBuilder<Self> {
781		crate::viewsets::builder::ViewSetBuilder::new(self)
782	}
783}
784
785#[async_trait]
786impl<M, S> ViewSet for ReadOnlyModelViewSet<M, S>
787where
788	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
789	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
790{
791	fn get_basename(&self) -> &str {
792		&self.basename
793	}
794
795	fn get_lookup_field(&self) -> &str {
796		&self.lookup_field
797	}
798
799	async fn dispatch(&self, request: Request, action: Action) -> Result<Response> {
800		match (request.method.clone(), action.detail) {
801			(Method::GET, false) => self.handler.list(&request).await.map_err(Into::into),
802			(Method::GET, true) => {
803				let pk = extract_pk(&request, &self.lookup_field)?;
804				self.handler
805					.retrieve(&request, pk)
806					.await
807					.map_err(Into::into)
808			}
809			_ => Err(method_not_allowed(&request.method)),
810		}
811	}
812}
813
814// Implement PaginatedViewSet for ReadOnlyModelViewSet
815impl<M, S> PaginatedViewSet for ReadOnlyModelViewSet<M, S>
816where
817	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
818	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
819{
820	fn get_pagination_config(&self) -> Option<PaginationConfig> {
821		self.pagination_config.clone()
822	}
823}
824
825// Implement FilterableViewSet for ReadOnlyModelViewSet
826impl<M, S> FilterableViewSet for ReadOnlyModelViewSet<M, S>
827where
828	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
829	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
830{
831	fn get_filter_config(&self) -> Option<FilterConfig> {
832		self.filter_config.clone()
833	}
834
835	fn get_ordering_config(&self) -> Option<OrderingConfig> {
836		self.ordering_config.clone()
837	}
838}
839
840// Manually re-assert the `UnwindSafe` / `RefUnwindSafe` auto traits for the
841// public viewset structs. The new `Arc<dyn Serializer ...>` / `Arc<dyn
842// Permission>` / `Arc<dyn FilterBackend>` fields introduced by this PR do
843// not propagate these markers because trait objects do not implement them
844// by default, which would otherwise surface as cargo-semver-checks
845// `auto_trait_impl_removed` under the RC phase's no-breaking-change policy.
846// The trait objects are only accessed via `&self` / `Arc::clone`, and the
847// `Send + Sync` supertraits already guarantee thread safety, so manually
848// re-implementing the markers preserves the pre-PR public-API contract.
849impl<M, S> std::panic::UnwindSafe for ModelViewSet<M, S>
850where
851	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
852	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
853{
854}
855impl<M, S> std::panic::RefUnwindSafe for ModelViewSet<M, S>
856where
857	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
858	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
859{
860}
861
862impl<M, S> std::panic::UnwindSafe for ReadOnlyModelViewSet<M, S>
863where
864	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
865	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
866{
867}
868impl<M, S> std::panic::RefUnwindSafe for ReadOnlyModelViewSet<M, S>
869where
870	M: Model + Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
871	S: Serializer<Input = M, Output = String> + Default + Send + Sync + 'static,
872{
873}
874
875#[cfg(test)]
876mod tests {
877	use super::*;
878	use hyper::Method;
879	use reinhardt_db::orm::{FieldSelector, Filter, FilterOperator, Model};
880	use serde::{Deserialize, Serialize};
881	use std::collections::HashMap;
882	use std::sync::Arc;
883
884	/// Minimal `Model` implementation used to satisfy the `ModelViewSet` trait
885	/// bounds in unit tests.
886	#[derive(Debug, Clone, Serialize, Deserialize)]
887	struct DummyModel {
888		id: Option<i64>,
889		secret: String,
890	}
891
892	#[derive(Clone)]
893	struct DummyFields;
894
895	impl FieldSelector for DummyFields {
896		fn with_alias(self, _alias: &str) -> Self {
897			self
898		}
899	}
900
901	impl Model for DummyModel {
902		type PrimaryKey = i64;
903		type Fields = DummyFields;
904		type Objects = reinhardt_db::orm::Manager<Self>;
905		fn table_name() -> &'static str {
906			"dummy"
907		}
908		fn primary_key(&self) -> Option<Self::PrimaryKey> {
909			self.id
910		}
911		fn set_primary_key(&mut self, value: Self::PrimaryKey) {
912			self.id = Some(value);
913		}
914		fn new_fields() -> Self::Fields {
915			DummyFields
916		}
917	}
918
919	#[derive(Default)]
920	struct RedactingDummySerializer;
921
922	impl Serializer for RedactingDummySerializer {
923		type Input = DummyModel;
924		type Output = String;
925
926		fn serialize(
927			&self,
928			input: &Self::Input,
929		) -> std::result::Result<Self::Output, reinhardt_rest::serializers::SerializerError> {
930			serde_json::to_string(&serde_json::json!({ "id": input.id })).map_err(|e| {
931				reinhardt_rest::serializers::SerializerError::Serde {
932					message: format!("Serialization error: {}", e),
933				}
934			})
935		}
936
937		fn deserialize(
938			&self,
939			output: &Self::Output,
940		) -> std::result::Result<Self::Input, reinhardt_rest::serializers::SerializerError> {
941			let value: serde_json::Value = serde_json::from_str(output).map_err(|e| {
942				reinhardt_rest::serializers::SerializerError::Serde {
943					message: format!("Deserialization error: {}", e),
944				}
945			})?;
946			if value.get("secret").is_some() {
947				return Err(reinhardt_rest::serializers::SerializerError::Serde {
948					message: "secret is not writable".to_string(),
949				});
950			}
951			Ok(DummyModel {
952				id: value.get("id").and_then(serde_json::Value::as_i64),
953				secret: String::new(),
954			})
955		}
956	}
957
958	#[test]
959	fn queryset_fn_builders_preserve_viewset_object_safety() {
960		let model: Arc<dyn ViewSet> = Arc::new(
961			ModelViewSet::<DummyModel, RedactingDummySerializer>::new("test").with_queryset_fn(
962				|_| Ok(Filter::new("organization_id", FilterOperator::Eq, 1_i64.into()).into()),
963			),
964		);
965		let read_only: Arc<dyn ViewSet> = Arc::new(
966			ReadOnlyModelViewSet::<DummyModel, RedactingDummySerializer>::new("test")
967				.with_queryset_fn(|_| {
968					Ok(Filter::new("organization_id", FilterOperator::Eq, 1_i64.into()).into())
969				}),
970		);
971
972		assert_eq!(model.get_basename(), "test");
973		assert_eq!(read_only.get_basename(), "test");
974	}
975
976	#[tokio::test]
977	async fn test_model_viewset_new_wires_declared_serializer() {
978		let viewset = ModelViewSet::<DummyModel, RedactingDummySerializer>::new("test")
979			.with_queryset(vec![DummyModel {
980				id: Some(7),
981				secret: "hidden".to_string(),
982			}]);
983		let request = Request::builder()
984			.method(Method::GET)
985			.uri("/test/")
986			.body(bytes::Bytes::new())
987			.build()
988			.unwrap();
989
990		let response = viewset.dispatch(request, Action::list()).await.unwrap();
991
992		assert_eq!(response.status, hyper::StatusCode::OK);
993		assert_eq!(response.body, bytes::Bytes::from_static(br#"[{"id":7}]"#));
994	}
995
996	#[tokio::test]
997	async fn test_viewset_builder_validation_empty_actions() {
998		let viewset = ModelViewSet::<
999			DummyModel,
1000			reinhardt_rest::serializers::JsonSerializer<DummyModel>,
1001		>::new("test");
1002		let builder = viewset.as_view();
1003
1004		// Test that empty actions causes build to fail
1005		let result = builder.build();
1006		assert!(result.is_err());
1007
1008		// Check error message without unwrapping
1009		match result {
1010			Err(e) => assert!(
1011				e.to_string()
1012					.contains("The `actions` argument must be provided")
1013			),
1014			Ok(_) => panic!("Expected error but got success"),
1015		}
1016	}
1017
1018	#[tokio::test]
1019	async fn test_viewset_builder_name_suffix_mutual_exclusivity() {
1020		let viewset = ModelViewSet::<
1021			DummyModel,
1022			reinhardt_rest::serializers::JsonSerializer<DummyModel>,
1023		>::new("test");
1024		let builder = viewset.as_view();
1025
1026		// Test that providing both name and suffix fails
1027		let result = builder
1028			.with_name("test_name")
1029			.and_then(|b| b.with_suffix("test_suffix"));
1030
1031		assert!(result.is_err());
1032
1033		// Check error message without unwrapping
1034		match result {
1035			Err(e) => assert!(e.to_string().contains("received both `name` and `suffix`")),
1036			Ok(_) => panic!("Expected error but got success"),
1037		}
1038	}
1039
1040	#[tokio::test]
1041	async fn test_viewset_builder_successful_build() {
1042		let viewset = ModelViewSet::<
1043			DummyModel,
1044			reinhardt_rest::serializers::JsonSerializer<DummyModel>,
1045		>::new("test");
1046		let mut actions = HashMap::new();
1047		actions.insert(Method::GET, "list".to_string());
1048
1049		let builder = viewset.as_view();
1050		let result = builder.with_actions(actions).build();
1051
1052		let handler = result.unwrap();
1053
1054		// Test that handler is created successfully
1055		// Handler should be created without errors
1056		assert!(Arc::strong_count(&handler) > 0);
1057	}
1058
1059	#[tokio::test]
1060	async fn test_viewset_builder_with_name() {
1061		let viewset = ModelViewSet::<
1062			DummyModel,
1063			reinhardt_rest::serializers::JsonSerializer<DummyModel>,
1064		>::new("test");
1065		let mut actions = HashMap::new();
1066		actions.insert(Method::GET, "list".to_string());
1067
1068		let builder = viewset.as_view();
1069		let result = builder
1070			.with_actions(actions)
1071			.with_name("test_view")
1072			.and_then(|b| b.build());
1073
1074		assert!(result.is_ok());
1075	}
1076
1077	#[tokio::test]
1078	async fn test_viewset_builder_with_suffix() {
1079		let viewset = ModelViewSet::<
1080			DummyModel,
1081			reinhardt_rest::serializers::JsonSerializer<DummyModel>,
1082		>::new("test");
1083		let mut actions = HashMap::new();
1084		actions.insert(Method::GET, "list".to_string());
1085
1086		let builder = viewset.as_view();
1087		let result = builder
1088			.with_actions(actions)
1089			.with_suffix("_list")
1090			.and_then(|b| b.build());
1091
1092		assert!(result.is_ok());
1093	}
1094}