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}