pub struct ApiError {
pub status: u16,
pub message: String,
}Expand description
A failure with the HTTP status to report it under. Body and parse problems are 400; a write that does not come from a page this server served is 403 or 415; an endpoint or an action nobody offers is 404; an endpoint reached by the wrong method is 405; a body past the size cap is 413; and filesystem and serialization failures are 500.
Fields§
§status: u16§message: StringImplementations§
Source§impl ApiError
impl ApiError
Sourcepub fn new(status: u16, message: impl Into<String>) -> Self
pub fn new(status: u16, message: impl Into<String>) -> Self
Examples found in repository?
examples/library/main.rs (line 816)
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 }Sourcepub fn bad_request(message: impl Into<String>) -> Self
pub fn bad_request(message: impl Into<String>) -> Self
A malformed request the client should not repeat unchanged (400).
Examples found in repository?
examples/library/main.rs (line 951)
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 }Sourcepub fn server(message: impl Into<String>) -> Self
pub fn server(message: impl Into<String>) -> Self
A failure on this side of the wire (500).
Examples found in repository?
examples/library/main.rs (line 983)
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 }Sourcepub fn from_parse(file: &str, err: &ParseError) -> Self
pub fn from_parse(file: &str, err: &ParseError) -> Self
An unparseable line of a stored table (500): the file is the server’s to keep readable, so a client cannot fix it by retrying.
Trait Implementations§
impl Eq for ApiError
Source§impl Error for ApiError
impl Error for ApiError
1.30.0 · Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
Returns the lower-level source of this error, if any. Read more
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
👎Deprecated since 1.42.0:
use the Display impl or to_string()
impl StructuralPartialEq for ApiError
Auto Trait Implementations§
impl Freeze for ApiError
impl RefUnwindSafe for ApiError
impl Send for ApiError
impl Sync for ApiError
impl Unpin for ApiError
impl UnsafeUnpin for ApiError
impl UnwindSafe for ApiError
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more