Skip to main content

ViewArgs

Struct ViewArgs 

Source
pub struct ViewArgs(/* private fields */);
Expand description

What a view was asked for.

It holds the address’s parameters, with a declared parameter the address left out filled in from its default. Everything the address carried is kept, including keys no parameter names, so a view may read more than it declares; ViewArgs::iter walks all of it.

A parameter the address gave a value its options no longer offer falls back to the default. That is what happens when one parameter’s options depend on another’s value and the other has just changed: a subgenre that belonged to the genre before this one is not an answer to the question being asked now. An empty value is a value: a parameter cleared on purpose stays cleared rather than filling itself in again.

An action is asked with the same arguments the page it was on was asked with, and with the arguments its own form carried, so ViewLogic::act reads which thing it is writing about the same way ViewLogic::render reads which thing it is drawing.

Implementations§

Source§

impl ViewArgs

Source

pub fn get(&self, key: &str) -> Option<&str>

Source

pub fn get_or<'a>(&'a self, key: &str, fallback: &'a str) -> &'a str

The value of key, or fallback where the address and the parameter’s own default both said nothing.

Examples found in repository?
examples/library/main.rs (line 568)
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    }
808
809    fn render(&self, args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
810        let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
811        let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
812        let code = args.get_or("branch", "");
813        let branch = branches
814            .iter()
815            .find(|b| b.code == code)
816            .ok_or_else(|| ApiError::new(404, format!("no branch has the code \"{code}\"")))?;
817
818        let (here, out) = shelf_counts(&books, code);
819        let now = today();
820
821        // What is out, with how it stands against its due date. A row's own
822        // link is the catalogue entry, and one book's link is a `javascript:`
823        // URL, which the page shows as text rather than following.
824        let mut lent = DetailSection::main("Out on loan");
825        for book in BranchDetail::by_rating(&books, code, true) {
826            let late = days_from_civil(&book.due).map(|due| now - due);
827            let mut row = DetailRow::new(book.title.as_str()).link(book.link.as_str());
828            if !book.due.is_empty() {
829                row = row.fact(format!("due {}", book.due));
830            }
831            row = match late {
832                Some(days) if days > 0 => row.fact(format!("{days} day(s) late")),
833                Some(days) => row.fact(format!("{} day(s) to go", -days)),
834                None => row.note("No due date is recorded."),
835            };
836            lent = lent.row(row.button(Button::disabled("Chase the borrower", "Not built yet")));
837        }
838        if out == 0 {
839            lent = lent.note("Everything this branch holds is on the shelf.");
840        }
841
842        // What can go out, ranked, with the one action this example offers.
843        let mut shelf = DetailSection::main("On the shelf")
844            .numbered()
845            .note("Best rated first.");
846        for book in BranchDetail::by_rating(&books, code, false) {
847            let mut row = DetailRow::new(book.title.as_str());
848            if let Some(rating) = book.rating {
849                row = row.fact(format!("rated {rating}"));
850            }
851            if let Some(year) = book.year {
852                row = row.fact(year);
853            }
854            if !book.notes.trim().is_empty() {
855                row = row.note(book.notes.as_str());
856            }
857            shelf = shelf.row(
858                row.button(Button::link("Catalogue", book.link.as_str()))
859                    .button(Button::form(
860                        "Lend it out",
861                        BranchDetail::loan_form(&book.title),
862                    )),
863            );
864        }
865        if here == out {
866            shelf = shelf.note("Nothing this branch holds is on the shelf today.");
867        }
868
869        let mut hours = DetailSection::side("When it is open").collapsed_on_phone();
870        for (day, open) in &branch.hours {
871            hours = hours.row(DetailRow::new(day.as_str()).fact(open.as_str()));
872        }
873        if branch.hours.is_empty() {
874            hours = hours.note("No hours are recorded for this branch.");
875        }
876
877        // A count per genre, which is a section of facts rather than of things
878        // to do, so it sits beside the page rather than in it.
879        let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
880        for book in books.iter().filter(|b| b.shelved.contains_key(code)) {
881            *counts.entry(book.genre.as_str()).or_default() += 1;
882        }
883        let mut genres = DetailSection::side("Genres here");
884        for (genre, count) in counts {
885            genres = genres.row(DetailRow::new(genre).fact(count));
886        }
887
888        Ok(ViewData::new().detail(
889            Detail::new(branch.name.as_str())
890                .status(if branch.open.unwrap_or(false) {
891                    Status::new("Open", Tone::Good)
892                } else {
893                    Status::new("Shut", Tone::Neutral)
894                })
895                .subtitle(format!(
896                    "{}, {} staff, {here} book(s) here, {out} out",
897                    librarian(branch),
898                    branch.staff.unwrap_or(0)
899                ))
900                .back(ViewLink::new("all-branches"))
901                .section(lent)
902                .section(shelf)
903                .section(hours)
904                .section(genres),
905        ))
906    }
907
908    /// Which action was asked for. The router only lets through a name a
909    /// button on this page offers, so the arm at the end is unreachable; it is
910    /// written all the same, because a page with two actions on it decides
911    /// between them here and that is what a consumer copies.
912    fn act(
913        &self,
914        name: &str,
915        fields: &Fields,
916        args: &ViewArgs,
917        ctx: &Context,
918    ) -> Result<String, ApiError> {
919        match name {
920            "lend-a-book" => BranchDetail::lend(fields, args, ctx),
921            _ => Err(ApiError::new(
922                404,
923                format!("this page has no action called \"{name}\""),
924            )),
925        }
926    }
927}
928
929impl BranchDetail {
930    /// Lend a book out: mark it lent, work out when it is due back, and write
931    /// the loan into the row's notes.
932    ///
933    /// The write is the one a table's save makes: the rows are read through the
934    /// same context the page was rendered from, the table's own `serialize`
935    /// turns them back into JSONL, and `Context::write` replaces the file
936    /// through a temporary one. The rows are validated first and a write that
937    /// would introduce an error is refused, because an action has no cell to
938    /// show the error beside.
939    ///
940    /// The note is the loan's, so it replaces whatever the row said. An example
941    /// that appended would grow its own committed data file every time the
942    /// action was tried.
943    fn lend(fields: &Fields, args: &ViewArgs, ctx: &Context) -> Result<String, ApiError> {
944        let title = args.get_or("title", "");
945        let borrower = fields.text("borrower");
946        let days = fields.integer("days")?;
947        let from = fields.date("from")?;
948        let condition = fields.text("condition").to_lowercase();
949
950        if borrower.is_empty() {
951            return Err(ApiError::bad_request("a loan needs a borrower"));
952        }
953        if days < 1 {
954            return Err(ApiError::bad_request("a loan is at least one day long"));
955        }
956        let start = days_from_civil(from).ok_or_else(|| {
957            ApiError::bad_request(format!("{from} is not a date in the calendar"))
958        })?;
959        let due = civil_from_days(start + days);
960
961        let mut books: Vec<Book> = ctx.rows(BOOKS_FILE)?;
962        let book = books
963            .iter_mut()
964            .find(|b| b.title == title)
965            .ok_or_else(|| ApiError::bad_request(format!("no book is called \"{title}\"")))?;
966        if book.lent.unwrap_or(false) {
967            return Err(ApiError::bad_request(format!("\"{title}\" is already out")));
968        }
969        book.lent = Some(true);
970        book.due = due.clone();
971        book.notes = format!("Lent to {borrower} on {from}, {condition}.");
972
973        let problems = Books.validate(&books, ctx)?;
974        if let Some(first) = problems.first() {
975            return Err(ApiError::bad_request(format!(
976                "the loan would leave row {} in a state the table refuses: {}",
977                first.line, first.message
978            )));
979        }
980
981        let text = Books
982            .serialize(&books)
983            .map_err(|e| ApiError::server(format!("could not serialize {BOOKS_FILE}: {e}")))?;
984        ctx.write(BOOKS_FILE, &text)?;
985
986        Ok(format!("\"{title}\" is out to {borrower} until {due}."))
987    }
Source

pub fn iter(&self) -> impl Iterator<Item = (&str, &str)>

Every key and value, in order, including those no parameter declares.

Source

pub fn is_empty(&self) -> bool

Source

pub fn len(&self) -> usize

Trait Implementations§

Source§

impl Clone for ViewArgs

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 ViewArgs

Source§

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

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

impl Default for ViewArgs

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl Serialize for ViewArgs

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§

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.