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