tilo_app/framework/views/
kind.rs1use std::any::Any;
6use std::sync::Arc;
7
8use tilo_resource::{SearchBody, SearchEntry, SearchWeight};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum FieldRole {
13 Title,
15 Subtitle,
17 Artwork,
19 Date,
21 Detail,
23 Excerpt,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum Form {
31 Row,
33 Tile,
35 Detail,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum FilterKind {
42 Value,
44 Toggle,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct FieldDecl {
51 pub key: &'static str,
54 pub label: String,
56 pub role: Option<FieldRole>,
57 pub searchable: bool,
59 pub filter: Option<FilterKind>,
61}
62
63impl FieldDecl {
64 #[must_use]
66 pub fn new(key: &'static str, label: impl Into<String>) -> Self {
67 Self {
68 key,
69 label: label.into(),
70 role: None,
71 searchable: false,
72 filter: None,
73 }
74 }
75
76 #[must_use]
78 pub fn title(key: &'static str, label: impl Into<String>) -> Self {
79 Self::new(key, label).role(FieldRole::Title).searchable()
80 }
81
82 #[must_use]
83 pub fn role(mut self, role: FieldRole) -> Self {
84 self.role = Some(role);
85 self
86 }
87
88 #[must_use]
89 pub fn searchable(mut self) -> Self {
90 self.searchable = true;
91 self
92 }
93
94 #[must_use]
96 pub fn filter(mut self, kind: FilterKind) -> Self {
97 self.filter = Some(kind);
98 self
99 }
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct KindDecl {
106 pub kind: &'static str,
108 pub label: String,
110 pub plural: String,
112 pub form: Form,
113 pub fields: Vec<FieldDecl>,
114}
115
116impl KindDecl {
117 #[must_use]
118 pub fn new(
119 kind: &'static str,
120 label: impl Into<String>,
121 plural: impl Into<String>,
122 form: Form,
123 ) -> Self {
124 Self {
125 kind,
126 label: label.into(),
127 plural: plural.into(),
128 form,
129 fields: Vec::new(),
130 }
131 }
132
133 #[must_use]
134 pub fn field(mut self, field: FieldDecl) -> Self {
135 self.fields.push(field);
136 self
137 }
138
139 #[must_use]
140 pub fn field_of(&self, key: &str) -> Option<&FieldDecl> {
141 self.fields.iter().find(|field| field.key == key)
142 }
143}
144
145#[must_use]
148pub fn filters(kinds: &[KindDecl]) -> Vec<&FieldDecl> {
149 let mut found: Vec<&FieldDecl> = Vec::new();
150 for field in kinds
151 .iter()
152 .flat_map(|kind| &kind.fields)
153 .filter(|field| field.filter.is_some())
154 {
155 if !found.iter().any(|seen| seen.key == field.key) {
156 found.push(field);
157 }
158 }
159 found
160}
161
162#[derive(Clone)]
167pub struct Item {
168 pub kind: &'static str,
169 pub id: String,
170 pub fields: Vec<(&'static str, String)>,
171 pub found_by: Vec<(String, SearchWeight)>,
174 pub body: Option<SearchBody>,
178 pub payload: Option<Arc<dyn Any + Send + Sync>>,
179}
180
181impl std::fmt::Debug for Item {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 f.debug_struct("Item")
184 .field("kind", &self.kind)
185 .field("id", &self.id)
186 .field("fields", &self.fields)
187 .field("found_by", &self.found_by)
188 .field("body", &self.body.is_some())
189 .field("payload", &self.payload.is_some())
190 .finish()
191 }
192}
193
194impl PartialEq for Item {
195 fn eq(&self, other: &Self) -> bool {
196 self.kind == other.kind
197 && self.id == other.id
198 && self.fields == other.fields
199 && self.found_by == other.found_by
200 && self.body == other.body
201 }
202}
203
204impl Item {
205 #[must_use]
206 pub fn new(kind: &'static str, id: impl Into<String>) -> Self {
207 Self {
208 kind,
209 id: id.into(),
210 fields: Vec::new(),
211 found_by: Vec::new(),
212 body: None,
213 payload: None,
214 }
215 }
216
217 #[must_use]
225 pub fn from_entry(kinds: &[KindDecl], entry: &SearchEntry) -> Option<Self> {
226 let decl = match entry.group.as_deref() {
227 Some(group) => kinds.iter().find(|kind| kind.plural == group)?,
228 None => kinds.first()?,
229 };
230 let id = entry.anchor.clone().or_else(|| {
231 entry
232 .action
233 .as_ref()
234 .map(|action| format!("{}\u{1f}{}", action.resource, action.action))
235 })?;
236 let role = |role: FieldRole| {
237 decl.fields
238 .iter()
239 .find(|field| field.role == Some(role))
240 .map(|field| field.key)
241 };
242 let mut item = Self::new(decl.kind, id);
243 if let Some(key) = role(FieldRole::Title) {
244 item = item.with(key, entry.title.clone());
245 }
246 if let (Some(key), Some(subtitle)) = (role(FieldRole::Subtitle), &entry.subtitle) {
247 item = item.with(key, subtitle.clone());
248 }
249 for (key, _, value) in &entry.facets {
250 let Some(field) = decl.fields.iter().find(|field| field.key == key) else {
251 continue;
252 };
253 if !item.values(field.key).any(|held| held == value) {
255 item = item.with(field.key, value.clone());
256 }
257 }
258 if !entry.text.trim().is_empty() {
259 item.found_by.push((entry.text.clone(), SearchWeight::Body));
260 }
261 item.found_by.extend(entry.fields.iter().cloned());
262 item.body.clone_from(&entry.body);
263 item.payload = Some(Arc::new(entry.clone()));
264 Some(item)
265 }
266
267 #[must_use]
269 pub fn with(mut self, key: &'static str, value: impl Into<String>) -> Self {
270 let value = value.into();
271 if !value.is_empty() {
272 self.fields.push((key, value));
273 }
274 self
275 }
276
277 #[must_use]
279 pub fn flag(self, key: &'static str, on: bool) -> Self {
280 if on { self.with(key, TOGGLE_ON) } else { self }
281 }
282
283 #[must_use]
285 pub fn payload(mut self, payload: Arc<dyn Any + Send + Sync>) -> Self {
286 self.payload = Some(payload);
287 self
288 }
289
290 #[must_use]
292 pub fn value<'a>(&'a self, key: &'a str) -> Option<&'a str> {
293 self.values(key).next()
294 }
295
296 pub fn values<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'a str> + 'a {
298 self.fields
299 .iter()
300 .filter(move |(field, _)| *field == key)
301 .map(|(_, value)| value.as_str())
302 }
303
304 #[must_use]
306 pub fn is_on(&self, key: &str) -> bool {
307 self.values(key).any(|value| value == TOGGLE_ON)
308 }
309
310 #[must_use]
312 pub fn key(&self) -> (&'static str, &str) {
313 (self.kind, self.id.as_str())
314 }
315
316 #[must_use]
318 pub fn entry(&self) -> Option<&SearchEntry> {
319 self.payload_as()
320 }
321
322 #[must_use]
324 pub fn payload_as<T: Any + Send + Sync>(&self) -> Option<&T> {
325 self.payload.as_deref()?.downcast_ref()
326 }
327}
328
329pub(crate) const TOGGLE_ON: &str = "true";