1use std::collections::BTreeMap;
15
16use serde::Serialize;
17
18use crate::context::Context;
19use crate::error::ApiError;
20use crate::schema::{Column, SelectOption};
21
22#[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#[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 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 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 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#[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 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 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 pub fn get_or<'a>(&'a self, key: &str, fallback: &'a str) -> &'a str {
145 self.get(key).unwrap_or(fallback)
146 }
147
148 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#[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 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#[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
245pub trait ViewLogic: Send + Sync + 'static {
251 fn name(&self) -> &'static str;
255
256 fn title(&self) -> &'static str;
258
259 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
274pub trait View: Send + Sync {
278 fn route(&self) -> &'static str;
280
281 fn heading(&self) -> &'static str;
283
284 fn param_keys(&self, ctx: &Context) -> Result<Vec<String>, ApiError>;
287
288 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 let params = self.params(ctx, &ViewArgs::from_query(query))?;
322 let args = ViewArgs::resolve(query, ¶ms);
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#[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")]), ¶ms);
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(&[]), ¶ms);
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(&[]), ¶ms);
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 let params = vec![Param::select("subgenre", "Subgenre", ["Memoir"]).default("Memoir")];
393 let args = ViewArgs::resolve(&query(&[("subgenre", "Natural History")]), ¶ms);
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")]), ¶ms);
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")]), ¶ms);
408 assert_eq!(args.get("branch"), Some("anything"));
409 }
410
411 #[test]
412 fn a_parameter_cleared_on_purpose_stays_cleared() {
413 let params = vec![Param::string("who", "Borrower").default("Ada")];
416 let args = ViewArgs::resolve(&query(&[("who", "")]), ¶ms);
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")]), ¶ms);
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(¶m).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(§ion).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}