Skip to main content

tablo_core/resource/
def.rs

1//! [`ResourceDef`]: everything a resource declares, as one value.
2
3use std::sync::Arc;
4
5use toasty::stmt::Path;
6use topcoat::icon::IconData;
7
8use super::{Action, Actions, Relation, Resource};
9use crate::{
10    DeclarationErrorKind,
11    navigation::NavigationItem,
12    policy::{Deny, Policy},
13    schema::Schema,
14    table::Table,
15    tenancy::Tenancy,
16    toasty_compat::model::{self, ModelPath},
17};
18
19/// What a [`Resource`] declares: its names, navigation, policy, tenancy, table, form, view,
20/// relations and actions.
21///
22/// [`Resource::declare`] returns one, and [`Panel::resource_with`](crate::Panel::resource_with)
23/// adjusts it for one panel. Every setting has a default, so a resource sets only what differs:
24///
25/// ```rust
26/// # #[derive(Debug, Clone, toasty::Model)]
27/// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String, featured: bool }
28/// # #[derive(Debug, Clone, tablo_core::RecordForm)]
29/// # #[form(model = Post)]
30/// # struct PostForm { title: String, featured: bool }
31/// # struct PostResource;
32/// use tablo_core::{ReadOnly, RecordForm, Resource, ResourceDef, TernaryFilter};
33///
34/// impl Resource for PostResource {
35///     type Model = Post;
36///     type Form = PostForm;
37///
38///     fn declare() -> ResourceDef<Self> {
39///         ResourceDef::new()
40///             .label("Blog post")
41///             .policy(ReadOnly)
42///             .table(PostForm::table().filters(TernaryFilter::new(Post::fields().featured())))
43///     }
44/// }
45/// ```
46///
47/// The panel builds the def once when it mounts, binding the paths it names to the database
48/// schema, and serves the result to every request.
49pub struct ResourceDef<R: Resource> {
50    pub(crate) slug: Option<String>,
51    pub(crate) label: Option<String>,
52    pub(crate) plural_label: Option<String>,
53    pub(crate) icon: Option<IconData>,
54    pub(crate) navigation_order: i32,
55    pub(crate) navigation: Option<NavigationItem>,
56    pub(crate) policy: Arc<dyn Policy<R::Model>>,
57    pub(crate) tenancy: Tenancy<R::Model>,
58    pub(crate) table: Option<Table<R::Model>>,
59    pub(crate) form: Option<Schema>,
60    pub(crate) view: Option<Schema>,
61    pub(crate) relations: Vec<Relation<R::Model>>,
62    pub(crate) actions: Actions<R>,
63    /// The columns [`Self::create_column`] names, or why a path names no column.
64    pub(crate) create_columns: Vec<Result<String, DeclarationErrorKind>>,
65}
66
67impl<R: Resource> Default for ResourceDef<R> {
68    fn default() -> Self {
69        Self {
70            slug: None,
71            label: None,
72            plural_label: None,
73            icon: None,
74            navigation_order: 0,
75            navigation: None,
76            policy: Arc::new(Deny),
77            tenancy: Tenancy::none(),
78            table: None,
79            form: None,
80            view: None,
81            relations: Vec::new(),
82            actions: Actions::default(),
83            create_columns: Vec::new(),
84        }
85    }
86}
87
88impl<R: Resource> ResourceDef<R> {
89    /// A def with every default: the policy denies all, rows belong to no tenant, and the record
90    /// form derives the table, the form and the view.
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    /// The URL slug for the resource's pages: `"users"` mounts the list at `{panel prefix}/users`.
96    ///
97    /// Defaults to the resource type's name without a trailing `Resource`, pluralized and
98    /// kebab-cased: `UserResource` → `users`, `BlogPostResource` → `blog-posts`.
99    #[must_use]
100    pub fn slug(mut self, slug: impl Into<String>) -> Self {
101        self.slug = Some(slug.into());
102        self
103    }
104
105    /// One record's name, the noun in the "Create {label}" and "Edit {label}" titles.
106    ///
107    /// Defaults to the model's type name.
108    #[must_use]
109    pub fn label(mut self, label: impl Into<String>) -> Self {
110        self.label = Some(label.into());
111        self
112    }
113
114    /// The list title and sidebar label.
115    ///
116    /// Defaults to the pluralized [`label`](Self::label): `Category` → `Categories`, `Person` →
117    /// `People`.
118    #[must_use]
119    pub fn plural_label(mut self, label: impl Into<String>) -> Self {
120        self.plural_label = Some(label.into());
121        self
122    }
123
124    /// The icon of the resource's sidebar entry.
125    #[must_use]
126    pub fn icon(mut self, icon: IconData) -> Self {
127        self.icon = Some(icon);
128        self
129    }
130
131    /// Where the sidebar entry sorts among the panel's entries, lowest first; defaults to `0`, and
132    /// entries of equal order keep their registration order.
133    #[must_use]
134    pub fn navigation_order(mut self, order: i32) -> Self {
135        self.navigation_order = order;
136        self
137    }
138
139    /// Replaces the sidebar entry, for one that links somewhere other than the list page:
140    /// `NavigationItem::at("Drafts", "/admin/posts?f.status=draft")`.
141    #[must_use]
142    pub fn navigation(mut self, item: NavigationItem) -> Self {
143        self.navigation = Some(item);
144        self
145    }
146
147    /// Gates every handler; defaults to [`Deny`]. Row scoping belongs in
148    /// [`Resource::query`], not the policy.
149    #[must_use]
150    pub fn policy(mut self, policy: impl Policy<R::Model>) -> Self {
151        self.policy = Arc::new(policy);
152        self
153    }
154
155    /// How rows belong to a tenant; defaults to [`Tenancy::none`].
156    #[must_use]
157    pub fn tenancy(mut self, tenancy: Tenancy<R::Model>) -> Self {
158        self.tenancy = tenancy;
159        self
160    }
161
162    /// The list table; defaults to the record form's derived table ([`RecordForm::table`]).
163    ///
164    /// [`RecordForm::table`]: crate::RecordForm::table
165    #[must_use]
166    pub fn table(mut self, table: Table<R::Model>) -> Self {
167        self.table = Some(table);
168        self
169    }
170
171    /// The schema the create and edit forms render; defaults to the record form's derived schema
172    /// ([`RecordForm::schema`]).
173    ///
174    /// The panel refuses a record form field this schema does not declare, and a schema on a
175    /// resource whose [`Form`](Resource::Form) is [`NoForm`](crate::NoForm).
176    ///
177    /// [`RecordForm::schema`]: crate::RecordForm::schema
178    #[must_use]
179    pub fn form(mut self, form: Schema) -> Self {
180        self.form = Some(form);
181        self
182    }
183
184    /// The read-only detail schema; defaults to the [`form`](Self::form), and an empty schema
185    /// disables the detail page.
186    #[must_use]
187    pub fn view(mut self, view: Schema) -> Self {
188        self.view = Some(view);
189        self
190    }
191
192    /// Adds a related resource, rendered as its table narrowed to the record on the detail and
193    /// edit pages.
194    #[must_use]
195    pub fn relation(mut self, relation: Relation<R::Model>) -> Self {
196        self.relations.push(relation);
197        self
198    }
199
200    /// Adds the custom [`Action`] `A` after the ones already declared.
201    ///
202    /// An `A::NAME` that is not a single URL path segment does not compile.
203    #[must_use]
204    pub fn action<A: Action<R>>(mut self) -> Self {
205        self.actions = self.actions.add::<A>();
206        self
207    }
208
209    /// Declares a column an overriding [`Resource::create_record`] sets itself, beyond the form's
210    /// fields: `.create_column(Post::fields().slug())`. Call it once per column.
211    ///
212    /// Mounting the panel refuses a path that names no one field of the model, and the tenant
213    /// column, which the framework stamps.
214    #[must_use]
215    pub fn create_column<T>(mut self, path: impl Into<Path<R::Model, T>>) -> Self {
216        let path = ModelPath::of(&path.into());
217        self.create_columns
218            .push(model::field::<R::Model>(&path).map(|field| field.name));
219        self
220    }
221}
222
223impl<R: Resource> std::fmt::Debug for ResourceDef<R> {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        f.debug_struct("ResourceDef")
226            .field("slug", &self.slug)
227            .field("label", &self.label)
228            .field("plural_label", &self.plural_label)
229            .field("relations", &self.relations)
230            .field("actions", &self.actions)
231            .finish_non_exhaustive()
232    }
233}