Skip to main content

table_editor/
view.rs

1//! Read-only pages the server computes and the browser renders.
2//!
3//! A view is the other half of what a repository serves: a table is where the
4//! writing happens, and a view is where the reading does. It answers a
5//! question the tables can only be read to answer—which books are out on loan
6//! and how late they are—by computing rows that are in no file.
7//!
8//! What a view sends is the column schema the tables send, so the browser
9//! renders a view with what it already knows and learns nothing about what a
10//! row means. There is no writing, no sorting, no filtering, and no state: a
11//! parameter changes, the page is fetched again, and what comes back is what
12//! is shown.
13
14use std::collections::BTreeMap;
15
16use serde::Serialize;
17
18use crate::context::Context;
19use crate::error::ApiError;
20use crate::schema::{Column, SelectOption};
21
22/// One control at the top of a view.
23#[derive(Debug, Clone, Serialize)]
24pub struct Param {
25    key: String,
26    label: String,
27    #[serde(rename = "type")]
28    kind: ParamKind,
29    #[serde(skip_serializing_if = "Vec::is_empty")]
30    options: Vec<SelectOption>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    default: Option<String>,
33}
34
35/// How a parameter is asked for. It is the `type` the payload carries and
36/// nothing a consumer names: a parameter is built by [`Param::select`] or
37/// [`Param::string`], which is what decides it.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
39#[serde(rename_all = "lowercase")]
40pub(crate) enum ParamKind {
41    Select,
42    String,
43}
44
45impl Param {
46    /// A choice among options, which are rebuilt on every request like a
47    /// schema is, so a select can be filled from a table.
48    pub fn select(
49        key: impl Into<String>,
50        label: impl Into<String>,
51        options: impl IntoIterator<Item = impl Into<SelectOption>>,
52    ) -> Self {
53        Self {
54            key: key.into(),
55            label: label.into(),
56            kind: ParamKind::Select,
57            options: options.into_iter().map(Into::into).collect(),
58            default: None,
59        }
60    }
61
62    pub fn string(key: impl Into<String>, label: impl Into<String>) -> Self {
63        Self {
64            key: key.into(),
65            label: label.into(),
66            kind: ParamKind::String,
67            options: Vec::new(),
68            default: None,
69        }
70    }
71
72    /// What the view is asked for when the address names nothing.
73    pub fn default(mut self, value: impl Into<String>) -> Self {
74        self.default = Some(value.into());
75        self
76    }
77
78    pub fn key(&self) -> &str {
79        &self.key
80    }
81
82    pub(crate) fn fallback(&self) -> Option<&str> {
83        self.default.as_deref()
84    }
85
86    /// Whether this parameter would offer `value`. A select with no options
87    /// offers whatever it is given, since it has named nothing to choose from.
88    pub(crate) fn offers(&self, value: &str) -> bool {
89        self.kind != ParamKind::Select
90            || self.options.is_empty()
91            || self.options.iter().any(|option| option.value == value)
92    }
93}
94
95/// What a view was asked for.
96///
97/// It holds the address's parameters, with a declared parameter the address
98/// left out filled in from its default. Everything the address carried is
99/// kept, including keys no parameter names, so a view may read more than it
100/// declares; [`ViewArgs::iter`] walks all of it.
101///
102/// A parameter the address gave a value its options no longer offer falls back
103/// to the default. That is what happens when one parameter's options depend on
104/// another's value and the other has just changed: a subgenre that belonged to
105/// the genre before this one is not an answer to the question being asked now.
106/// An empty value is a value: a parameter cleared on purpose stays cleared
107/// rather than filling itself in again.
108#[derive(Debug, Clone, Default, Serialize)]
109pub struct ViewArgs(BTreeMap<String, String>);
110
111impl ViewArgs {
112    pub(crate) fn from_query(query: &BTreeMap<String, String>) -> Self {
113        Self(query.clone())
114    }
115
116    /// Settle what the view is rendering from, now that its parameters are
117    /// known. See the type's own description for the rules.
118    pub(crate) fn resolve(query: &BTreeMap<String, String>, params: &[Param]) -> Self {
119        let mut args = query.clone();
120        for param in params {
121            let asked = args.get(param.key());
122            let keep = match asked {
123                Some(value) => param.offers(value),
124                None => false,
125            };
126            if keep {
127                continue;
128            }
129            // With nothing to fall back to, an answer nobody offers is left
130            // where it is rather than replaced with a guess.
131            if let Some(fallback) = param.fallback() {
132                args.insert(param.key().to_string(), fallback.to_string());
133            }
134        }
135        Self(args)
136    }
137
138    pub fn get(&self, key: &str) -> Option<&str> {
139        self.0.get(key).map(String::as_str)
140    }
141
142    /// The value of `key`, or `fallback` where the address and the parameter's
143    /// own default both said nothing.
144    pub fn get_or<'a>(&'a self, key: &str, fallback: &'a str) -> &'a str {
145        self.get(key).unwrap_or(fallback)
146    }
147
148    /// Every key and value, in order, including those no parameter declares.
149    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
150        self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
151    }
152
153    pub fn is_empty(&self) -> bool {
154        self.0.is_empty()
155    }
156
157    pub fn len(&self) -> usize {
158        self.0.len()
159    }
160}
161
162/// One run of rows under a heading of its own.
163///
164/// Columns belong to a section rather than to the view, so two sections can
165/// differ: a section of what is overdue wants a column of how late, and a
166/// section of what is merely out does not. Sections that should line up are
167/// given the same columns.
168#[derive(Debug, Clone, Serialize)]
169pub struct Section {
170    #[serde(skip_serializing_if = "Option::is_none")]
171    heading: Option<String>,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    note: Option<String>,
174    columns: Vec<Column>,
175    rows: Vec<serde_json::Value>,
176}
177
178impl Section {
179    pub fn new(columns: impl IntoIterator<Item = Column>) -> Self {
180        Self {
181            heading: None,
182            note: None,
183            columns: columns.into_iter().collect(),
184            rows: Vec::new(),
185        }
186    }
187
188    pub fn heading(mut self, heading: impl Into<String>) -> Self {
189        self.heading = Some(heading.into());
190        self
191    }
192
193    pub fn note(mut self, note: impl Into<String>) -> Self {
194        self.note = Some(note.into());
195        self
196    }
197
198    /// The rows themselves, which may be a repository's own types: whatever
199    /// serializes to an object keyed by the fields the columns name.
200    ///
201    /// A row that cannot be serialized is a 500 naming the section, since a
202    /// page that quietly dropped a row would be worse than one that did not
203    /// render.
204    pub fn rows<T: Serialize>(
205        mut self,
206        rows: impl IntoIterator<Item = T>,
207    ) -> Result<Self, ApiError> {
208        self.rows = rows
209            .into_iter()
210            .map(|row| serde_json::to_value(row))
211            .collect::<Result<Vec<_>, _>>()
212            .map_err(|e| {
213                let what = self.heading.as_deref().unwrap_or("a section");
214                ApiError::server(format!("could not serialize the rows of {what}: {e}"))
215            })?;
216        Ok(self)
217    }
218}
219
220/// What one render of a view produced: a note about the whole of it, and its
221/// sections in the order they are to be read.
222#[derive(Debug, Clone, Default, Serialize)]
223pub struct ViewData {
224    #[serde(skip_serializing_if = "Option::is_none")]
225    note: Option<String>,
226    sections: Vec<Section>,
227}
228
229impl ViewData {
230    pub fn new() -> Self {
231        Self::default()
232    }
233
234    pub fn note(mut self, note: impl Into<String>) -> Self {
235        self.note = Some(note.into());
236        self
237    }
238
239    pub fn section(mut self, section: Section) -> Self {
240        self.sections.push(section);
241        self
242    }
243}
244
245/// One repository's view.
246///
247/// `params` and `render` both run on every request, against one [`Context`],
248/// so a view reading three tables reads each of them once however many of its
249/// parts consult them.
250pub trait ViewLogic: Send + Sync + 'static {
251    /// The route segment and `?view=` value. It must be made of unreserved URL
252    /// characters, and may not take a reserved name or one a table has;
253    /// building a [`crate::Server`] over such a view panics.
254    fn name(&self) -> &'static str;
255
256    /// The heading the page and the shell's switcher show.
257    fn title(&self) -> &'static str;
258
259    /// The controls at the top of the page, rebuilt per request like a schema,
260    /// so a select can be filled from a table.
261    ///
262    /// `asked` is what the address carried, before defaults are filled in, so
263    /// one parameter's options may depend on another's value. A parameter
264    /// whose options are built that way should give a default drawn from the
265    /// same values: an answer the new options do not offer is replaced by that
266    /// default rather than kept.
267    fn params(&self, _ctx: &Context, _asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
268        Ok(Vec::new())
269    }
270
271    fn render(&self, args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError>;
272}
273
274/// The object-safe façade the router dispatches through, as [`crate::Table`]
275/// is for tables. A blanket implementation covers every [`ViewLogic`], so
276/// nothing outside this module implements it.
277pub trait View: Send + Sync {
278    /// The route segment, from [`ViewLogic::name`].
279    fn route(&self) -> &'static str;
280
281    /// The shell's heading, from [`ViewLogic::title`].
282    fn heading(&self) -> &'static str;
283
284    /// The keys of the parameters this view declares, which the server checks
285    /// against the ones the address itself uses.
286    fn param_keys(&self, ctx: &Context) -> Result<Vec<String>, ApiError>;
287
288    /// `GET /api/views/<view>`: the parameters, what they resolved to, and the
289    /// sections rendered from them.
290    fn handle_get(
291        &self,
292        query: &BTreeMap<String, String>,
293        ctx: &Context,
294    ) -> Result<String, ApiError>;
295}
296
297impl<V: ViewLogic> View for V {
298    fn route(&self) -> &'static str {
299        self.name()
300    }
301
302    fn heading(&self) -> &'static str {
303        self.title()
304    }
305
306    fn param_keys(&self, ctx: &Context) -> Result<Vec<String>, ApiError> {
307        Ok(self
308            .params(ctx, &ViewArgs::default())?
309            .iter()
310            .map(|param| param.key().to_string())
311            .collect())
312    }
313
314    fn handle_get(
315        &self,
316        query: &BTreeMap<String, String>,
317        ctx: &Context,
318    ) -> Result<String, ApiError> {
319        // The parameters are built from what was asked, so that one may depend
320        // on another; what was asked is then settled against them.
321        let params = self.params(ctx, &ViewArgs::from_query(query))?;
322        let args = ViewArgs::resolve(query, &params);
323        let data = self.render(&args, ctx)?;
324
325        serde_json::to_string(&ViewPayload {
326            view: self.name(),
327            title: self.title(),
328            params,
329            args,
330            note: data.note,
331            sections: data.sections,
332        })
333        .map_err(|e| ApiError::server(e.to_string()))
334    }
335}
336
337/// `GET /api/views/<view>`. The parameters travel with every answer rather
338/// than from an endpoint of their own: they are rebuilt from the tables each
339/// time and may have changed, and one round trip is enough.
340#[derive(Serialize)]
341struct ViewPayload<'a> {
342    view: &'a str,
343    title: &'a str,
344    params: Vec<Param>,
345    args: ViewArgs,
346    #[serde(skip_serializing_if = "Option::is_none")]
347    note: Option<String>,
348    sections: Vec<Section>,
349}
350
351#[cfg(test)]
352mod tests {
353    use serde::Serialize;
354    use serde_json::json;
355
356    use super::*;
357
358    fn query(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
359        pairs
360            .iter()
361            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
362            .collect()
363    }
364
365    #[test]
366    fn an_address_that_names_a_parameter_is_taken_at_its_word() {
367        let params = vec![Param::select("branch", "Branch", ["cen", "est"]).default("cen")];
368        let args = ViewArgs::resolve(&query(&[("branch", "est")]), &params);
369        assert_eq!(args.get("branch"), Some("est"));
370    }
371
372    #[test]
373    fn a_parameter_the_address_leaves_out_falls_back_to_its_default() {
374        let params = vec![Param::select("branch", "Branch", ["cen"]).default("cen")];
375        let args = ViewArgs::resolve(&query(&[]), &params);
376        assert_eq!(args.get("branch"), Some("cen"));
377    }
378
379    #[test]
380    fn a_parameter_with_no_default_is_simply_absent() {
381        let params = vec![Param::string("who", "Borrower")];
382        let args = ViewArgs::resolve(&query(&[]), &params);
383        assert_eq!(args.get("who"), None);
384        assert_eq!(args.get_or("who", "anyone"), "anyone");
385        assert!(args.is_empty());
386    }
387
388    #[test]
389    fn a_value_the_options_no_longer_offer_falls_back_to_the_default() {
390        // Which is what a dependent parameter needs: the subgenre chosen under
391        // the last genre is no answer to the genre being asked about now.
392        let params = vec![Param::select("subgenre", "Subgenre", ["Memoir"]).default("Memoir")];
393        let args = ViewArgs::resolve(&query(&[("subgenre", "Natural History")]), &params);
394        assert_eq!(args.get("subgenre"), Some("Memoir"));
395    }
396
397    #[test]
398    fn a_value_nothing_offers_and_nothing_replaces_is_left_alone() {
399        let params = vec![Param::select("subgenre", "Subgenre", ["Memoir"])];
400        let args = ViewArgs::resolve(&query(&[("subgenre", "Natural History")]), &params);
401        assert_eq!(args.get("subgenre"), Some("Natural History"));
402    }
403
404    #[test]
405    fn a_select_that_offers_nothing_takes_whatever_it_is_given() {
406        let params = vec![Param::select("branch", "Branch", Vec::<String>::new()).default("cen")];
407        let args = ViewArgs::resolve(&query(&[("branch", "anything")]), &params);
408        assert_eq!(args.get("branch"), Some("anything"));
409    }
410
411    #[test]
412    fn a_parameter_cleared_on_purpose_stays_cleared() {
413        // An empty value is a value: refilling the default would make a text
414        // parameter impossible to clear.
415        let params = vec![Param::string("who", "Borrower").default("Ada")];
416        let args = ViewArgs::resolve(&query(&[("who", "")]), &params);
417        assert_eq!(args.get("who"), Some(""));
418    }
419
420    #[test]
421    fn a_key_no_parameter_names_is_kept_for_a_view_that_wants_it() {
422        let params = vec![Param::select("branch", "Branch", ["cen"]).default("cen")];
423        let args = ViewArgs::resolve(&query(&[("sort", "due")]), &params);
424        assert_eq!(args.get("sort"), Some("due"));
425        assert_eq!(args.get("branch"), Some("cen"));
426        assert_eq!(
427            args.iter().collect::<Vec<_>>(),
428            vec![("branch", "cen"), ("sort", "due")]
429        );
430        assert_eq!(args.len(), 2);
431    }
432
433    #[test]
434    fn a_parameter_serializes_to_the_documented_shape() {
435        let param = Param::select(
436            "branch",
437            "Branch",
438            [SelectOption::labelled("cen", "Central")],
439        )
440        .default("cen");
441
442        assert_eq!(
443            serde_json::to_value(&param).unwrap(),
444            json!({ "key": "branch", "label": "Branch", "type": "select",
445                    "options": [{ "value": "cen", "label": "Central" }],
446                    "default": "cen" })
447        );
448
449        assert_eq!(
450            serde_json::to_value(Param::string("who", "Borrower")).unwrap(),
451            json!({ "key": "who", "label": "Borrower", "type": "string" })
452        );
453    }
454
455    #[test]
456    fn a_section_omits_what_it_was_not_given() {
457        let bare = Section::new([Column::string("title", "Title")]);
458        assert_eq!(
459            serde_json::to_value(&bare).unwrap(),
460            json!({ "columns": [{ "field": "title", "label": "Title", "type": "string" }],
461                    "rows": [] })
462        );
463
464        let full = Section::new([Column::string("title", "Title")])
465            .heading("Out")
466            .note("Due back this week.")
467            .rows(vec![json!({ "title": "A Field Guide to Moss" })])
468            .unwrap();
469        assert_eq!(
470            serde_json::to_value(&full).unwrap(),
471            json!({ "heading": "Out", "note": "Due back this week.",
472                    "columns": [{ "field": "title", "label": "Title", "type": "string" }],
473                    "rows": [{ "title": "A Field Guide to Moss" }] })
474        );
475    }
476
477    #[test]
478    fn a_section_takes_a_repositorys_own_type_for_its_rows() {
479        #[derive(Serialize)]
480        struct Loan {
481            title: &'static str,
482            days: u32,
483        }
484
485        let section = Section::new([Column::string("title", "Title")])
486            .rows([Loan {
487                title: "Nine Doors",
488                days: 25,
489            }])
490            .unwrap();
491        assert_eq!(
492            serde_json::to_value(&section).unwrap()["rows"],
493            json!([{ "title": "Nine Doors", "days": 25 }])
494        );
495    }
496
497    #[test]
498    fn a_row_that_cannot_be_serialized_names_the_section_it_was_in() {
499        struct Awkward;
500        impl Serialize for Awkward {
501            fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
502                Err(serde::ser::Error::custom("no"))
503            }
504        }
505
506        let failure = Section::new([Column::string("title", "Title")])
507            .heading("Out")
508            .rows([Awkward])
509            .unwrap_err();
510        assert_eq!(failure.status, 500);
511        assert!(failure.message.contains("Out"), "{}", failure.message);
512    }
513}