Skip to main content

Param

Struct Param 

Source
pub struct Param { /* private fields */ }
Expand description

One control at the top of a view.

Implementations§

Source§

impl Param

Source

pub fn select( key: impl Into<String>, label: impl Into<String>, options: impl IntoIterator<Item = impl Into<SelectOption>>, ) -> Self

A choice among options, which are rebuilt on every request like a schema is, so a select can be filled from a table.

Examples found in repository?
examples/library/main.rs (line 577)
550    fn params(&self, ctx: &Context, asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
551        let branches = Self::branches(ctx)?;
552        let options: Vec<SelectOption> = branches
553            .iter()
554            .map(|b| SelectOption::labelled(&b.code, &b.name))
555            .collect();
556        let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
557
558        // A genre and one of its subgenres. The second list follows the first
559        // one's answer, which is why `params` is told what was asked before
560        // anything is settled. A subgenre belonging to the genre chosen last
561        // time is no answer to the question being asked now, and falls back to
562        // every subgenre of the genre chosen this time.
563        let genres: Vec<Genre> = ctx.optional_rows(GENRES_FILE)?;
564        let mut named: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
565        named.sort_unstable();
566        named.dedup();
567
568        let chosen = asked.get_or("genre", "");
569        let mut subgenres: Vec<&str> = genres
570            .iter()
571            .filter(|g| g.genre == chosen)
572            .map(|g| g.subgenre.as_str())
573            .collect();
574        subgenres.sort_unstable();
575
576        Ok(vec![
577            Param::select("branch", "Branch", options).default(first),
578            Param::select("genre", "Genre", Self::any("Every genre", named)).default(""),
579            Param::select(
580                "subgenre",
581                "Subgenre",
582                Self::any("Every subgenre", subgenres),
583            )
584            .default(""),
585            Param::string("author", "Author"),
586        ])
587    }
588
589    fn render(&self, args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
590        let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
591        let branches = Self::branches(ctx)?;
592        let code = args.get_or("branch", "");
593        let branch = branches.iter().find(|b| b.code == code);
594        let named = branch.map(|b| b.name.as_str()).unwrap_or(code);
595
596        let genre = args.get_or("genre", "");
597        let subgenre = args.get_or("subgenre", "");
598        let author = args.get_or("author", "").trim().to_lowercase();
599
600        // The books this branch holds that answer the rest of the question,
601        // whether they are out or not. A parameter left empty asks nothing.
602        let held: Vec<&Book> = books
603            .iter()
604            .filter(|b| b.shelved.contains_key(code))
605            .filter(|b| genre.is_empty() || b.genre == genre)
606            .filter(|b| subgenre.is_empty() || b.subgenre == subgenre)
607            .filter(|b| {
608                author.is_empty()
609                    || format!("{} {}", b.author_first, b.author_last)
610                        .to_lowercase()
611                        .contains(&author)
612            })
613            .collect();
614
615        let now = today();
616        let mut out = Vec::new();
617        let mut overdue = Vec::new();
618        for book in held.iter().filter(|b| b.lent.unwrap_or(false)) {
619            let days = days_from_civil(&book.due).map(|due| now - due).unwrap_or(0);
620            if days > 0 {
621                overdue.push(OnLoan::row(book, days));
622            } else {
623                out.push(OnLoan::row(book, days));
624            }
625        }
626
627        let lent = out.len() + overdue.len();
628        Ok(ViewData::new()
629            .note(format!(
630                "{lent} of {} book(s) at {named} are out on loan. A title links to its catalogue entry.",
631                held.len()
632            ))
633            .section(
634                Section::new(OnLoan::columns())
635                    .heading("Out")
636                    .note("Days left before they are due.")
637                    .rows(out)?,
638            )
639            .section(
640                Section::new(OnLoan::columns())
641                    .heading("Overdue")
642                    .note("Days past due. Chase these.")
643                    .rows(overdue)?,
644            ))
645    }
646}
647
648// ── The Branches view, which is a card per branch ────────────────────────────
649
650/// Every branch as a card, grouped by whether it is open.
651///
652/// A card is what a view answers with when the question is "how do these things
653/// stand" rather than "what are the values of these fields": there are no
654/// columns here, and each card says how one branch stands, what it is called,
655/// and two counts, with the whole card a link to the branch's own page.
656struct BranchCards;
657
658/// How many of the books a branch holds, and how many of those are out.
659fn shelf_counts(books: &[Book], code: &str) -> (usize, usize) {
660    let here: Vec<&Book> = books
661        .iter()
662        .filter(|b| b.shelved.contains_key(code))
663        .collect();
664    let out = here.iter().filter(|b| b.lent.unwrap_or(false)).count();
665    (here.len(), out)
666}
667
668fn librarian(branch: &Branch) -> String {
669    format!("{} {}", branch.librarian_first, branch.librarian_last)
670        .trim()
671        .to_string()
672}
673
674impl ViewLogic for BranchCards {
675    fn name(&self) -> &'static str {
676        "all-branches"
677    }
678
679    fn title(&self) -> &'static str {
680        "All branches"
681    }
682
683    fn render(&self, _args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
684        let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
685        let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
686
687        let card = |branch: &Branch| {
688            let (here, out) = shelf_counts(&books, &branch.code);
689            let open = branch.open.unwrap_or(false);
690            let mut card = Card::new(branch.name.as_str())
691                .status(if open {
692                    Status::new("Open", Tone::Good)
693                } else {
694                    // A shut branch is a fact about it rather than a fault, so
695                    // it is neutral—which is also what reads quieter.
696                    Status::new("Shut", Tone::Neutral)
697                })
698                .identifier(branch.code.as_str())
699                .subtitle(format!(
700                    "{}, {} staff",
701                    librarian(branch),
702                    branch.staff.unwrap_or(0)
703                ))
704                .row("Books here", here)
705                .row("Out on loan", out)
706                .link(ViewLink::new("branch").arg("branch", &branch.code));
707            if branch.hours.is_empty() {
708                card = card.sentence(
709                    "No opening hours are recorded, so nothing here says when it can be visited.",
710                );
711            }
712            card
713        };
714
715        let group = |heading: &str, open: bool| {
716            CardGroup::new(heading).cards(
717                branches
718                    .iter()
719                    .filter(|b| b.open.unwrap_or(false) == open)
720                    .map(&card),
721            )
722        };
723
724        Ok(ViewData::new()
725            .note("A card is a branch. Open one for what it holds and what is out.")
726            .group(group("Open", true))
727            .group(group("Shut", false)))
728    }
729}
730
731// ── The Branch view, which is one branch in detail ───────────────────────────
732
733/// One branch: what it has out, what is on its shelves, and when it is open.
734///
735/// It is reached from a card rather than from the switcher, so its one
736/// parameter is hidden: the page draws no control for it, and the card's link
737/// is what answers it. The parameter is a select all the same, so a link to a
738/// branch that has since gone falls back to the first one rather than to a page
739/// about nothing.
740struct BranchDetail;
741
742impl BranchDetail {
743    /// The books a branch holds, best rated first, which is the order the
744    /// numbered section is a ranking in.
745    fn by_rating<'a>(books: &'a [Book], code: &str, lent: bool) -> Vec<&'a Book> {
746        let mut held: Vec<&Book> = books
747            .iter()
748            .filter(|b| b.shelved.contains_key(code) && b.lent.unwrap_or(false) == lent)
749            .collect();
750        held.sort_by(|a, b| {
751            b.rating
752                .unwrap_or(0.0)
753                .total_cmp(&a.rating.unwrap_or(0.0))
754                .then_with(|| a.title.cmp(&b.title))
755        });
756        held
757    }
758
759    /// What a form asks before a book goes out. The four kinds of field are all
760    /// here, since this is the page the bundle's form controls are developed
761    /// against.
762    fn loan_form(title: &str) -> Form {
763        Form::new("lend-a-book")
764            .arg("title", title)
765            .field(Field::text("borrower", "Borrower"))
766            .field(Field::number("days", "Days out").default(21))
767            .field(Field::date("from", "Date lent").default(civil_from_days(today())))
768            .field(
769                Field::one_of(
770                    "condition",
771                    "Condition it left in",
772                    ["As new", "Good", "Worn"],
773                )
774                .default("Good"),
775            )
776    }
777}
778
779impl ViewLogic for BranchDetail {
780    fn name(&self) -> &'static str {
781        "branch"
782    }
783
784    fn title(&self) -> &'static str {
785        "Branch"
786    }
787
788    /// This page is about one branch, and which one arrives through a card's
789    /// link. A switcher entry for it would open whichever branch the parameter
790    /// happens to default to, which is nobody's question.
791    fn in_switcher(&self) -> bool {
792        false
793    }
794
795    fn params(&self, ctx: &Context, _asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
796        let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
797        let codes: Vec<SelectOption> = branches
798            .iter()
799            .map(|b| SelectOption::labelled(&b.code, &b.name))
800            .collect();
801        let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
802        Ok(vec![
803            Param::select("branch", "Branch", codes)
804                .default(first)
805                .hidden(),
806        ])
807    }
Source

pub fn string(key: impl Into<String>, label: impl Into<String>) -> Self

Examples found in repository?
examples/library/main.rs (line 585)
550    fn params(&self, ctx: &Context, asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
551        let branches = Self::branches(ctx)?;
552        let options: Vec<SelectOption> = branches
553            .iter()
554            .map(|b| SelectOption::labelled(&b.code, &b.name))
555            .collect();
556        let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
557
558        // A genre and one of its subgenres. The second list follows the first
559        // one's answer, which is why `params` is told what was asked before
560        // anything is settled. A subgenre belonging to the genre chosen last
561        // time is no answer to the question being asked now, and falls back to
562        // every subgenre of the genre chosen this time.
563        let genres: Vec<Genre> = ctx.optional_rows(GENRES_FILE)?;
564        let mut named: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
565        named.sort_unstable();
566        named.dedup();
567
568        let chosen = asked.get_or("genre", "");
569        let mut subgenres: Vec<&str> = genres
570            .iter()
571            .filter(|g| g.genre == chosen)
572            .map(|g| g.subgenre.as_str())
573            .collect();
574        subgenres.sort_unstable();
575
576        Ok(vec![
577            Param::select("branch", "Branch", options).default(first),
578            Param::select("genre", "Genre", Self::any("Every genre", named)).default(""),
579            Param::select(
580                "subgenre",
581                "Subgenre",
582                Self::any("Every subgenre", subgenres),
583            )
584            .default(""),
585            Param::string("author", "Author"),
586        ])
587    }
Source

pub fn default(self, value: impl Into<String>) -> Self

What the view is asked for when the address names nothing.

Examples found in repository?
examples/library/main.rs (line 577)
550    fn params(&self, ctx: &Context, asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
551        let branches = Self::branches(ctx)?;
552        let options: Vec<SelectOption> = branches
553            .iter()
554            .map(|b| SelectOption::labelled(&b.code, &b.name))
555            .collect();
556        let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
557
558        // A genre and one of its subgenres. The second list follows the first
559        // one's answer, which is why `params` is told what was asked before
560        // anything is settled. A subgenre belonging to the genre chosen last
561        // time is no answer to the question being asked now, and falls back to
562        // every subgenre of the genre chosen this time.
563        let genres: Vec<Genre> = ctx.optional_rows(GENRES_FILE)?;
564        let mut named: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
565        named.sort_unstable();
566        named.dedup();
567
568        let chosen = asked.get_or("genre", "");
569        let mut subgenres: Vec<&str> = genres
570            .iter()
571            .filter(|g| g.genre == chosen)
572            .map(|g| g.subgenre.as_str())
573            .collect();
574        subgenres.sort_unstable();
575
576        Ok(vec![
577            Param::select("branch", "Branch", options).default(first),
578            Param::select("genre", "Genre", Self::any("Every genre", named)).default(""),
579            Param::select(
580                "subgenre",
581                "Subgenre",
582                Self::any("Every subgenre", subgenres),
583            )
584            .default(""),
585            Param::string("author", "Author"),
586        ])
587    }
588
589    fn render(&self, args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
590        let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
591        let branches = Self::branches(ctx)?;
592        let code = args.get_or("branch", "");
593        let branch = branches.iter().find(|b| b.code == code);
594        let named = branch.map(|b| b.name.as_str()).unwrap_or(code);
595
596        let genre = args.get_or("genre", "");
597        let subgenre = args.get_or("subgenre", "");
598        let author = args.get_or("author", "").trim().to_lowercase();
599
600        // The books this branch holds that answer the rest of the question,
601        // whether they are out or not. A parameter left empty asks nothing.
602        let held: Vec<&Book> = books
603            .iter()
604            .filter(|b| b.shelved.contains_key(code))
605            .filter(|b| genre.is_empty() || b.genre == genre)
606            .filter(|b| subgenre.is_empty() || b.subgenre == subgenre)
607            .filter(|b| {
608                author.is_empty()
609                    || format!("{} {}", b.author_first, b.author_last)
610                        .to_lowercase()
611                        .contains(&author)
612            })
613            .collect();
614
615        let now = today();
616        let mut out = Vec::new();
617        let mut overdue = Vec::new();
618        for book in held.iter().filter(|b| b.lent.unwrap_or(false)) {
619            let days = days_from_civil(&book.due).map(|due| now - due).unwrap_or(0);
620            if days > 0 {
621                overdue.push(OnLoan::row(book, days));
622            } else {
623                out.push(OnLoan::row(book, days));
624            }
625        }
626
627        let lent = out.len() + overdue.len();
628        Ok(ViewData::new()
629            .note(format!(
630                "{lent} of {} book(s) at {named} are out on loan. A title links to its catalogue entry.",
631                held.len()
632            ))
633            .section(
634                Section::new(OnLoan::columns())
635                    .heading("Out")
636                    .note("Days left before they are due.")
637                    .rows(out)?,
638            )
639            .section(
640                Section::new(OnLoan::columns())
641                    .heading("Overdue")
642                    .note("Days past due. Chase these.")
643                    .rows(overdue)?,
644            ))
645    }
646}
647
648// ── The Branches view, which is a card per branch ────────────────────────────
649
650/// Every branch as a card, grouped by whether it is open.
651///
652/// A card is what a view answers with when the question is "how do these things
653/// stand" rather than "what are the values of these fields": there are no
654/// columns here, and each card says how one branch stands, what it is called,
655/// and two counts, with the whole card a link to the branch's own page.
656struct BranchCards;
657
658/// How many of the books a branch holds, and how many of those are out.
659fn shelf_counts(books: &[Book], code: &str) -> (usize, usize) {
660    let here: Vec<&Book> = books
661        .iter()
662        .filter(|b| b.shelved.contains_key(code))
663        .collect();
664    let out = here.iter().filter(|b| b.lent.unwrap_or(false)).count();
665    (here.len(), out)
666}
667
668fn librarian(branch: &Branch) -> String {
669    format!("{} {}", branch.librarian_first, branch.librarian_last)
670        .trim()
671        .to_string()
672}
673
674impl ViewLogic for BranchCards {
675    fn name(&self) -> &'static str {
676        "all-branches"
677    }
678
679    fn title(&self) -> &'static str {
680        "All branches"
681    }
682
683    fn render(&self, _args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
684        let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
685        let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
686
687        let card = |branch: &Branch| {
688            let (here, out) = shelf_counts(&books, &branch.code);
689            let open = branch.open.unwrap_or(false);
690            let mut card = Card::new(branch.name.as_str())
691                .status(if open {
692                    Status::new("Open", Tone::Good)
693                } else {
694                    // A shut branch is a fact about it rather than a fault, so
695                    // it is neutral—which is also what reads quieter.
696                    Status::new("Shut", Tone::Neutral)
697                })
698                .identifier(branch.code.as_str())
699                .subtitle(format!(
700                    "{}, {} staff",
701                    librarian(branch),
702                    branch.staff.unwrap_or(0)
703                ))
704                .row("Books here", here)
705                .row("Out on loan", out)
706                .link(ViewLink::new("branch").arg("branch", &branch.code));
707            if branch.hours.is_empty() {
708                card = card.sentence(
709                    "No opening hours are recorded, so nothing here says when it can be visited.",
710                );
711            }
712            card
713        };
714
715        let group = |heading: &str, open: bool| {
716            CardGroup::new(heading).cards(
717                branches
718                    .iter()
719                    .filter(|b| b.open.unwrap_or(false) == open)
720                    .map(&card),
721            )
722        };
723
724        Ok(ViewData::new()
725            .note("A card is a branch. Open one for what it holds and what is out.")
726            .group(group("Open", true))
727            .group(group("Shut", false)))
728    }
729}
730
731// ── The Branch view, which is one branch in detail ───────────────────────────
732
733/// One branch: what it has out, what is on its shelves, and when it is open.
734///
735/// It is reached from a card rather than from the switcher, so its one
736/// parameter is hidden: the page draws no control for it, and the card's link
737/// is what answers it. The parameter is a select all the same, so a link to a
738/// branch that has since gone falls back to the first one rather than to a page
739/// about nothing.
740struct BranchDetail;
741
742impl BranchDetail {
743    /// The books a branch holds, best rated first, which is the order the
744    /// numbered section is a ranking in.
745    fn by_rating<'a>(books: &'a [Book], code: &str, lent: bool) -> Vec<&'a Book> {
746        let mut held: Vec<&Book> = books
747            .iter()
748            .filter(|b| b.shelved.contains_key(code) && b.lent.unwrap_or(false) == lent)
749            .collect();
750        held.sort_by(|a, b| {
751            b.rating
752                .unwrap_or(0.0)
753                .total_cmp(&a.rating.unwrap_or(0.0))
754                .then_with(|| a.title.cmp(&b.title))
755        });
756        held
757    }
758
759    /// What a form asks before a book goes out. The four kinds of field are all
760    /// here, since this is the page the bundle's form controls are developed
761    /// against.
762    fn loan_form(title: &str) -> Form {
763        Form::new("lend-a-book")
764            .arg("title", title)
765            .field(Field::text("borrower", "Borrower"))
766            .field(Field::number("days", "Days out").default(21))
767            .field(Field::date("from", "Date lent").default(civil_from_days(today())))
768            .field(
769                Field::one_of(
770                    "condition",
771                    "Condition it left in",
772                    ["As new", "Good", "Worn"],
773                )
774                .default("Good"),
775            )
776    }
777}
778
779impl ViewLogic for BranchDetail {
780    fn name(&self) -> &'static str {
781        "branch"
782    }
783
784    fn title(&self) -> &'static str {
785        "Branch"
786    }
787
788    /// This page is about one branch, and which one arrives through a card's
789    /// link. A switcher entry for it would open whichever branch the parameter
790    /// happens to default to, which is nobody's question.
791    fn in_switcher(&self) -> bool {
792        false
793    }
794
795    fn params(&self, ctx: &Context, _asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
796        let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
797        let codes: Vec<SelectOption> = branches
798            .iter()
799            .map(|b| SelectOption::labelled(&b.code, &b.name))
800            .collect();
801        let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
802        Ok(vec![
803            Param::select("branch", "Branch", codes)
804                .default(first)
805                .hidden(),
806        ])
807    }
Source

pub fn hidden(self) -> Self

Draw no control for this parameter. It is for a parameter that arrives through a link rather than through the page—which story a detail page is about—where a control would be a second way to ask a question the reader has already asked. It is still declared, so it takes a default and is still handed to the view.

Examples found in repository?
examples/library/main.rs (line 805)
795    fn params(&self, ctx: &Context, _asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
796        let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
797        let codes: Vec<SelectOption> = branches
798            .iter()
799            .map(|b| SelectOption::labelled(&b.code, &b.name))
800            .collect();
801        let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
802        Ok(vec![
803            Param::select("branch", "Branch", codes)
804                .default(first)
805                .hidden(),
806        ])
807    }
Source

pub fn key(&self) -> &str

Trait Implementations§

Source§

impl Clone for Param

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Param

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Serialize for Param

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

§

impl Freeze for Param

§

impl RefUnwindSafe for Param

§

impl Send for Param

§

impl Sync for Param

§

impl Unpin for Param

§

impl UnsafeUnpin for Param

§

impl UnwindSafe for Param

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.