Move tag.rs module, add basic --json

The `--json` flag is currently only respected by `mensa meals`.
Relates to #4.
This commit is contained in:
Malte Tammena 2021-10-20 10:50:02 +02:00
parent b5171922f6
commit fcf81a71db
5 changed files with 59 additions and 28 deletions

View file

@ -7,7 +7,7 @@ use std::path::PathBuf;
use crate::{ use crate::{
error::{Error, Result}, error::{Error, Result},
meal::tag::Tag, tag::Tag,
}; };
use super::PriceTags; use super::PriceTags;
@ -40,6 +40,10 @@ pub struct Args {
)] )]
pub color: ColorWhen, pub color: ColorWhen,
/// Print plain json. Useful for shell scripts.
#[structopt(long, global = true, takes_value = false)]
pub json: bool,
#[structopt(subcommand)] #[structopt(subcommand)]
pub command: Option<Command>, pub command: Option<Command>,
} }
@ -92,7 +96,11 @@ pub struct MealsCommand {
pub canteen_id: Option<usize>, pub canteen_id: Option<usize>,
/// Specify which price tags should be displayed /// Specify which price tags should be displayed
#[structopt(long, short, env = "MENSA_PRICES", possible_values = &PriceTags::variants())] #[structopt(long,
short,
env = "MENSA_PRICES",
possible_values = &PriceTags::variants(),
case_insensitive = true)]
pub price: Option<Vec<PriceTags>>, pub price: Option<Vec<PriceTags>>,
#[structopt(long, env = "MENSA_OVERWRITE_FILTER", takes_value = false)] #[structopt(long, env = "MENSA_OVERWRITE_FILTER", takes_value = false)]

View file

@ -4,7 +4,8 @@ use std::convert::TryFrom;
use crate::{ use crate::{
error::{Error, Result}, error::{Error, Result},
meal::{tag::Tag, Meal}, meal::Meal,
tag::Tag,
}; };
#[derive(Debug, Clone, Default, Deserialize)] #[derive(Debug, Clone, Default, Deserialize)]

View file

@ -109,15 +109,18 @@ mod config;
mod error; mod error;
mod geoip; mod geoip;
mod meal; mod meal;
mod tag;
use config::{ use crate::{
args::{parse_human_date, Args, MealsCommand}, canteen::Canteen,
State, config::{
args::{parse_human_date, Args, Command, MealsCommand},
State,
},
error::{Error, Result, ResultExt},
meal::Meal,
tag::Tag,
}; };
use error::{Error, Result, ResultExt};
use meal::{tag::Tag, Meal};
use crate::{canteen::Canteen, config::args::Command};
const ENDPOINT: &str = "https://openmensa.org/api/v2"; const ENDPOINT: &str = "https://openmensa.org/api/v2";
const MIN_TERM_WIDTH: usize = 20; const MIN_TERM_WIDTH: usize = 20;
@ -152,7 +155,7 @@ fn real_main() -> Result<()> {
Some(Command::Meals(cmd)) => { Some(Command::Meals(cmd)) => {
let state = State::from(state, cmd); let state = State::from(state, cmd);
let meals = Meal::fetch(&state)?; let meals = Meal::fetch(&state)?;
Meal::print_all(&state, &meals); Meal::print_all(&state, &meals)?;
} }
Some(Command::Tomorrow(cmd)) => { Some(Command::Tomorrow(cmd)) => {
// Works like the meals command. But we replace the date with tomorrow! // Works like the meals command. But we replace the date with tomorrow!
@ -160,7 +163,7 @@ fn real_main() -> Result<()> {
cmd.date = parse_human_date("tomorrow").unwrap(); cmd.date = parse_human_date("tomorrow").unwrap();
let state = State::from(state, &cmd); let state = State::from(state, &cmd);
let meals = Meal::fetch(&state)?; let meals = Meal::fetch(&state)?;
Meal::print_all(&state, &meals); Meal::print_all(&state, &meals)?;
} }
Some(Command::Canteens(cmd)) => { Some(Command::Canteens(cmd)) => {
let state = State::from(state, cmd); let state = State::from(state, cmd);
@ -172,7 +175,7 @@ fn real_main() -> Result<()> {
let cmd = MealsCommand::default(); let cmd = MealsCommand::default();
let state = State::from(state, &cmd); let state = State::from(state, &cmd);
let meals = Meal::fetch(&state)?; let meals = Meal::fetch(&state)?;
Meal::print_all(&state, &meals); Meal::print_all(&state, &meals)?;
} }
} }
Ok(()) Ok(())

View file

@ -2,8 +2,7 @@ use chrono::Duration;
use core::fmt; use core::fmt;
use itertools::Itertools; use itertools::Itertools;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use owo_colors::{OwoColorize, Stream}; use serde::{Deserialize, Serialize};
use serde::Deserialize;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use std::collections::HashSet; use std::collections::HashSet;
@ -11,14 +10,12 @@ use std::collections::HashSet;
use crate::{ use crate::{
cache::fetch_json, cache::fetch_json,
config::{args::MealsCommand, MealsState, PriceTags}, config::{args::MealsCommand, MealsState, PriceTags},
error::{pass_info, Result}, error::{pass_info, Error, Result},
get_sane_terminal_dimensions, State, ENDPOINT, get_sane_terminal_dimensions,
tag::Tag,
State, ENDPOINT,
}; };
pub mod tag;
use self::tag::Tag;
const NAME_PRE: &str = " ╭───╴"; const NAME_PRE: &str = " ╭───╴";
const NAME_PRE_PLAIN: &str = " - "; const NAME_PRE_PLAIN: &str = " - ";
const NAME_CONTINUE_PRE: &str = ""; const NAME_CONTINUE_PRE: &str = "";
@ -36,9 +33,10 @@ lazy_static! {
static ref TTL_MEALS: Duration = Duration::hours(1); static ref TTL_MEALS: Duration = Duration::hours(1);
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(from = "RawMeal")] #[serde(from = "RawMeal")]
pub struct Meal { pub struct Meal {
#[serde(rename = "id")]
pub _id: usize, pub _id: usize,
pub name: String, pub name: String,
pub tags: HashSet<Tag>, pub tags: HashSet<Tag>,
@ -57,7 +55,7 @@ struct RawMeal {
category: String, category: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(debug, serde(deny_unknown_fields))] #[cfg_attr(debug, serde(deny_unknown_fields))]
pub struct Prices { pub struct Prices {
students: f32, students: f32,
@ -123,17 +121,24 @@ impl Meal {
/// Print the given meals. /// Print the given meals.
/// ///
/// Thi will respect passed cli arguments and the configuration. /// Thi will respect passed cli arguments and the configuration.
pub fn print_all(state: &MealsState, meals: &[Self]) { pub fn print_all(state: &MealsState, meals: &[Self]) -> Result<()> {
// Load the filter which is used to select which meals to print. // Load the filter which is used to select which meals to print.
let filter = state.get_filter(); let filter = state.get_filter();
// Load the favourites which will be used for marking meals. // Load the favourites which will be used for marking meals.
let favs = state.get_favs_rule(); let favs = state.get_favs_rule();
for meal in meals { let meals = meals.iter().filter(|meal| filter.is_match(meal));
if filter.is_match(meal) { if state.args.json {
let stdout = std::io::stdout();
let output = stdout.lock();
serde_json::to_writer_pretty(output, &meals.collect::<Vec<_>>())
.map_err(|why| Error::Serializing(why, "writing meals as json"))
} else {
for meal in meals {
let is_fav = favs.is_match(meal); let is_fav = favs.is_match(meal);
println!(); println!();
meal.print(state, is_fav); meal.print(state, is_fav);
} }
Ok(())
} }
} }

View file

@ -1,7 +1,7 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use num_enum::{IntoPrimitive, TryFromPrimitive}; use num_enum::{IntoPrimitive, TryFromPrimitive};
use regex::RegexSet; use regex::RegexSet;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter}; use strum::{Display, EnumIter};
use crate::config::State; use crate::config::State;
@ -38,6 +38,9 @@ lazy_static! {
.unwrap(); .unwrap();
} }
/// A tag describing a meal.
///
/// Contains allergy information, descriptions and categories.
#[derive( #[derive(
Debug, Debug,
Clone, Clone,
@ -49,6 +52,7 @@ lazy_static! {
PartialOrd, PartialOrd,
IntoPrimitive, IntoPrimitive,
TryFromPrimitive, TryFromPrimitive,
Serialize,
Deserialize, Deserialize,
EnumIter, EnumIter,
Display, Display,
@ -84,6 +88,7 @@ pub enum Tag {
} }
impl Tag { impl Tag {
/// Try deriving [`Tag`]s from the `raw` tag.
pub fn parse_str(raw: &str) -> Vec<Self> { pub fn parse_str(raw: &str) -> Vec<Self> {
TAG_RE TAG_RE
.matches(raw) .matches(raw)
@ -92,6 +97,9 @@ impl Tag {
.collect() .collect()
} }
/// Is this a primary tag?
///
/// Primary tags have an associated emoji and are not allergy information.
pub fn is_primary(&self) -> bool { pub fn is_primary(&self) -> bool {
use Tag::*; use Tag::*;
match self { match self {
@ -102,10 +110,15 @@ impl Tag {
} }
} }
/// Is this **not** a primary tag?
pub fn is_secondary(&self) -> bool { pub fn is_secondary(&self) -> bool {
!self.is_primary() !self.is_primary()
} }
/// Describe this [`Tag`] with english words.
///
/// This should add information where the enum variant itself
/// does not suffice.
pub fn describe(&self) -> &'static str { pub fn describe(&self) -> &'static str {
match self { match self {
Self::Alcohol => "Contains alcohol", Self::Alcohol => "Contains alcohol",
@ -140,7 +153,8 @@ impl Tag {
/// This formats an identifier for this tag. /// This formats an identifier for this tag.
/// ///
/// Will respect any settings given. /// Will respect any settings given, i.e. emojis will be used
/// unless the output should be plain.
pub fn as_id<Cmd>(&self, state: &State<Cmd>) -> String { pub fn as_id<Cmd>(&self, state: &State<Cmd>) -> String {
match self { match self {
Self::Vegan => if_plain!(state: "🌱".into(), "Vegan".into()), Self::Vegan => if_plain!(state: "🌱".into(), "Vegan".into()),