use std::collections::BTreeMap;
use serde::Serialize;
use crate::context::Context;
use crate::error::ApiError;
use crate::schema::{Column, SelectOption};
#[derive(Debug, Clone, Serialize)]
pub struct Param {
key: String,
label: String,
#[serde(rename = "type")]
kind: ParamKind,
#[serde(skip_serializing_if = "Vec::is_empty")]
options: Vec<SelectOption>,
#[serde(skip_serializing_if = "Option::is_none")]
default: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum ParamKind {
Select,
String,
}
impl Param {
pub fn select(
key: impl Into<String>,
label: impl Into<String>,
options: impl IntoIterator<Item = impl Into<SelectOption>>,
) -> Self {
Self {
key: key.into(),
label: label.into(),
kind: ParamKind::Select,
options: options.into_iter().map(Into::into).collect(),
default: None,
}
}
pub fn string(key: impl Into<String>, label: impl Into<String>) -> Self {
Self {
key: key.into(),
label: label.into(),
kind: ParamKind::String,
options: Vec::new(),
default: None,
}
}
pub fn default(mut self, value: impl Into<String>) -> Self {
self.default = Some(value.into());
self
}
pub fn key(&self) -> &str {
&self.key
}
pub(crate) fn fallback(&self) -> Option<&str> {
self.default.as_deref()
}
pub(crate) fn offers(&self, value: &str) -> bool {
self.kind != ParamKind::Select
|| self.options.is_empty()
|| self.options.iter().any(|option| option.value == value)
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ViewArgs(BTreeMap<String, String>);
impl ViewArgs {
pub(crate) fn from_query(query: &BTreeMap<String, String>) -> Self {
Self(query.clone())
}
pub(crate) fn resolve(query: &BTreeMap<String, String>, params: &[Param]) -> Self {
let mut args = query.clone();
for param in params {
let asked = args.get(param.key());
let keep = match asked {
Some(value) => param.offers(value),
None => false,
};
if keep {
continue;
}
if let Some(fallback) = param.fallback() {
args.insert(param.key().to_string(), fallback.to_string());
}
}
Self(args)
}
pub fn get(&self, key: &str) -> Option<&str> {
self.0.get(key).map(String::as_str)
}
pub fn get_or<'a>(&'a self, key: &str, fallback: &'a str) -> &'a str {
self.get(key).unwrap_or(fallback)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Section {
#[serde(skip_serializing_if = "Option::is_none")]
heading: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
note: Option<String>,
columns: Vec<Column>,
rows: Vec<serde_json::Value>,
}
impl Section {
pub fn new(columns: impl IntoIterator<Item = Column>) -> Self {
Self {
heading: None,
note: None,
columns: columns.into_iter().collect(),
rows: Vec::new(),
}
}
pub fn heading(mut self, heading: impl Into<String>) -> Self {
self.heading = Some(heading.into());
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn rows<T: Serialize>(
mut self,
rows: impl IntoIterator<Item = T>,
) -> Result<Self, ApiError> {
self.rows = rows
.into_iter()
.map(|row| serde_json::to_value(row))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
let what = self.heading.as_deref().unwrap_or("a section");
ApiError::server(format!("could not serialize the rows of {what}: {e}"))
})?;
Ok(self)
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ViewData {
#[serde(skip_serializing_if = "Option::is_none")]
note: Option<String>,
sections: Vec<Section>,
}
impl ViewData {
pub fn new() -> Self {
Self::default()
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn section(mut self, section: Section) -> Self {
self.sections.push(section);
self
}
}
pub trait ViewLogic: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn title(&self) -> &'static str;
fn params(&self, _ctx: &Context, _asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
Ok(Vec::new())
}
fn render(&self, args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError>;
}
pub trait View: Send + Sync {
fn route(&self) -> &'static str;
fn heading(&self) -> &'static str;
fn param_keys(&self, ctx: &Context) -> Result<Vec<String>, ApiError>;
fn handle_get(
&self,
query: &BTreeMap<String, String>,
ctx: &Context,
) -> Result<String, ApiError>;
}
impl<V: ViewLogic> View for V {
fn route(&self) -> &'static str {
self.name()
}
fn heading(&self) -> &'static str {
self.title()
}
fn param_keys(&self, ctx: &Context) -> Result<Vec<String>, ApiError> {
Ok(self
.params(ctx, &ViewArgs::default())?
.iter()
.map(|param| param.key().to_string())
.collect())
}
fn handle_get(
&self,
query: &BTreeMap<String, String>,
ctx: &Context,
) -> Result<String, ApiError> {
let params = self.params(ctx, &ViewArgs::from_query(query))?;
let args = ViewArgs::resolve(query, ¶ms);
let data = self.render(&args, ctx)?;
serde_json::to_string(&ViewPayload {
view: self.name(),
title: self.title(),
params,
args,
note: data.note,
sections: data.sections,
})
.map_err(|e| ApiError::server(e.to_string()))
}
}
#[derive(Serialize)]
struct ViewPayload<'a> {
view: &'a str,
title: &'a str,
params: Vec<Param>,
args: ViewArgs,
#[serde(skip_serializing_if = "Option::is_none")]
note: Option<String>,
sections: Vec<Section>,
}
#[cfg(test)]
mod tests {
use serde::Serialize;
use serde_json::json;
use super::*;
fn query(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect()
}
#[test]
fn an_address_that_names_a_parameter_is_taken_at_its_word() {
let params = vec![Param::select("branch", "Branch", ["cen", "est"]).default("cen")];
let args = ViewArgs::resolve(&query(&[("branch", "est")]), ¶ms);
assert_eq!(args.get("branch"), Some("est"));
}
#[test]
fn a_parameter_the_address_leaves_out_falls_back_to_its_default() {
let params = vec![Param::select("branch", "Branch", ["cen"]).default("cen")];
let args = ViewArgs::resolve(&query(&[]), ¶ms);
assert_eq!(args.get("branch"), Some("cen"));
}
#[test]
fn a_parameter_with_no_default_is_simply_absent() {
let params = vec![Param::string("who", "Borrower")];
let args = ViewArgs::resolve(&query(&[]), ¶ms);
assert_eq!(args.get("who"), None);
assert_eq!(args.get_or("who", "anyone"), "anyone");
assert!(args.is_empty());
}
#[test]
fn a_value_the_options_no_longer_offer_falls_back_to_the_default() {
let params = vec![Param::select("subgenre", "Subgenre", ["Memoir"]).default("Memoir")];
let args = ViewArgs::resolve(&query(&[("subgenre", "Natural History")]), ¶ms);
assert_eq!(args.get("subgenre"), Some("Memoir"));
}
#[test]
fn a_value_nothing_offers_and_nothing_replaces_is_left_alone() {
let params = vec![Param::select("subgenre", "Subgenre", ["Memoir"])];
let args = ViewArgs::resolve(&query(&[("subgenre", "Natural History")]), ¶ms);
assert_eq!(args.get("subgenre"), Some("Natural History"));
}
#[test]
fn a_select_that_offers_nothing_takes_whatever_it_is_given() {
let params = vec![Param::select("branch", "Branch", Vec::<String>::new()).default("cen")];
let args = ViewArgs::resolve(&query(&[("branch", "anything")]), ¶ms);
assert_eq!(args.get("branch"), Some("anything"));
}
#[test]
fn a_parameter_cleared_on_purpose_stays_cleared() {
let params = vec![Param::string("who", "Borrower").default("Ada")];
let args = ViewArgs::resolve(&query(&[("who", "")]), ¶ms);
assert_eq!(args.get("who"), Some(""));
}
#[test]
fn a_key_no_parameter_names_is_kept_for_a_view_that_wants_it() {
let params = vec![Param::select("branch", "Branch", ["cen"]).default("cen")];
let args = ViewArgs::resolve(&query(&[("sort", "due")]), ¶ms);
assert_eq!(args.get("sort"), Some("due"));
assert_eq!(args.get("branch"), Some("cen"));
assert_eq!(
args.iter().collect::<Vec<_>>(),
vec![("branch", "cen"), ("sort", "due")]
);
assert_eq!(args.len(), 2);
}
#[test]
fn a_parameter_serializes_to_the_documented_shape() {
let param = Param::select(
"branch",
"Branch",
[SelectOption::labelled("cen", "Central")],
)
.default("cen");
assert_eq!(
serde_json::to_value(¶m).unwrap(),
json!({ "key": "branch", "label": "Branch", "type": "select",
"options": [{ "value": "cen", "label": "Central" }],
"default": "cen" })
);
assert_eq!(
serde_json::to_value(Param::string("who", "Borrower")).unwrap(),
json!({ "key": "who", "label": "Borrower", "type": "string" })
);
}
#[test]
fn a_section_omits_what_it_was_not_given() {
let bare = Section::new([Column::string("title", "Title")]);
assert_eq!(
serde_json::to_value(&bare).unwrap(),
json!({ "columns": [{ "field": "title", "label": "Title", "type": "string" }],
"rows": [] })
);
let full = Section::new([Column::string("title", "Title")])
.heading("Out")
.note("Due back this week.")
.rows(vec![json!({ "title": "A Field Guide to Moss" })])
.unwrap();
assert_eq!(
serde_json::to_value(&full).unwrap(),
json!({ "heading": "Out", "note": "Due back this week.",
"columns": [{ "field": "title", "label": "Title", "type": "string" }],
"rows": [{ "title": "A Field Guide to Moss" }] })
);
}
#[test]
fn a_section_takes_a_repositorys_own_type_for_its_rows() {
#[derive(Serialize)]
struct Loan {
title: &'static str,
days: u32,
}
let section = Section::new([Column::string("title", "Title")])
.rows([Loan {
title: "Nine Doors",
days: 25,
}])
.unwrap();
assert_eq!(
serde_json::to_value(§ion).unwrap()["rows"],
json!([{ "title": "Nine Doors", "days": 25 }])
);
}
#[test]
fn a_row_that_cannot_be_serialized_names_the_section_it_was_in() {
struct Awkward;
impl Serialize for Awkward {
fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("no"))
}
}
let failure = Section::new([Column::string("title", "Title")])
.heading("Out")
.rows([Awkward])
.unwrap_err();
assert_eq!(failure.status, 500);
assert!(failure.message.contains("Out"), "{}", failure.message);
}
}