diff options
author | Joris | 2022-01-09 13:12:45 +0100 |
---|---|---|
committer | Joris | 2022-01-09 13:12:45 +0100 |
commit | 99af88a840bef534540a4b273d24a8a17e7fc9b9 (patch) | |
tree | 20bda6c9eccd66d0c165e1f5d42fe76f80be58d2 | |
parent | bd59a5128c05dcd550e91bbdd0cd9d5996a65586 (diff) |
Split app into modules
-rw-r--r-- | src/app.rs | 371 | ||||
-rw-r--r-- | src/app/app.rs | 57 | ||||
-rw-r--r-- | src/app/calendar.rs | 157 | ||||
-rw-r--r-- | src/app/form.rs | 79 | ||||
-rw-r--r-- | src/app/mod.rs | 38 | ||||
-rw-r--r-- | src/app/style.css (renamed from src/style.css) | 0 | ||||
-rw-r--r-- | src/app/update.rs | 52 | ||||
-rw-r--r-- | src/app/utils.rs | 9 | ||||
-rw-r--r-- | src/db/mod.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | src/model/event.rs | 42 |
11 files changed, 435 insertions, 376 deletions
diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index 0eb2b1e..0000000 --- a/src/app.rs +++ /dev/null @@ -1,371 +0,0 @@ -use gtk4 as gtk; - -use async_channel::{Receiver, Sender}; -use chrono::{Datelike, NaiveDate, NaiveTime, Weekday}; -use gtk::gdk::Display; -use gtk::glib::signal::Inhibit; -use gtk::glib; -use gtk::prelude::*; -use rusqlite::Connection; -use std::future::Future; -use std::rc::Rc; - -use crate::model::event; -use crate::model::event::Event; -use crate::db; - -/// Spawns a task on the default executor, without waiting for it to complete -pub fn spawn<F>(future: F) -where - F: Future<Output = ()> + 'static, -{ - gtk::glib::MainContext::default().spawn_local(future); -} - -fn send_message(tx: Sender<Msg>, msg: Msg) { - spawn(async move { - let _ = tx.send(msg).await; - }) -} - -enum Msg { - ShowAddForm { date: NaiveDate }, - AddEvent { event: Event }, -} - -static DAYS: [&str; 7] = ["LUN", "MAR", "MER", "JEU", "VEN", "SAM", "DIM"]; -static MONTHES: [&str; 12] = [ - "Jan", "Fév", "Mar", "Avr", "Mai", "Juin", "Juil", "Aoû", "Sep", "Oct", "Nov", "Déc", -]; - -pub fn run(conn: Connection) { - let conn = Rc::new(conn); - let app = gtk::Application::new(Some("me.guyonvarch.calendar"), Default::default()); - app.connect_startup(|_| load_style()); - app.connect_activate(move |app| build_ui(conn.clone(), app)); - app.run(); -} - -fn build_ui(conn: Rc<Connection>, app: >k::Application) { - let (tx, rx) = async_channel::unbounded(); - let app = App::new(conn.clone(), app, tx.clone()); - spawn(event_handler(conn, rx, tx, app)) -} - -async fn event_handler(conn: Rc<Connection>, rx: Receiver<Msg>, tx: Sender<Msg>, mut app: App) { - while let Ok(msg) = rx.recv().await { - match msg { - Msg::ShowAddForm { date } => { - add_event_dialog(Rc::clone(&conn), tx.clone(), Rc::clone(&app.window), date).await; - } - Msg::AddEvent { event } => { - let date = event.date.clone(); - - let d = date.signed_duration_since(app.start_date).num_days(); - - app.events.push(event); - - let col = (d % 7) as i32; - let row = 1 + (d / 7) as i32; - - app.grid.attach( - &day_entry(tx.clone(), &date, &app.today, &app.events), - col, - row, - 1, - 1, - ); - } - } - } -} - -struct App { - pub window: Rc<gtk::ApplicationWindow>, - pub grid: gtk::Grid, - pub events: Vec<Event>, - pub today: NaiveDate, - pub start_date: NaiveDate, -} - -impl App { - fn new(conn: Rc<Connection>, app: >k::Application, tx: Sender<Msg>) -> Self { - let window = Rc::new( - gtk::ApplicationWindow::builder() - .application(app) - .title("Calendar") - .default_width(800) - .default_height(600) - .visible(true) - .build(), - ); - - let grid = gtk::Grid::builder().build(); - window.set_child(Some(&grid)); - - for col in 0..7 { - grid.attach(&day_title(col), col, 0, 1, 1); - } - - let today = chrono::offset::Local::today().naive_utc(); - let start_date = - NaiveDate::from_isoywd(today.year(), today.iso_week().week(), Weekday::Mon); - - - let events = db::list(&conn).unwrap_or(vec!()); - show_days(tx, &grid, &start_date, &today, &events); - - window.connect_close_request(move |window| { - if let Some(application) = window.application() { - application.remove_window(window); - } - Inhibit(false) - }); - - Self { - window, - grid, - events: events.clone(), - today, - start_date, - } - } -} - -fn load_style() { - let provider = gtk::CssProvider::new(); - provider.load_from_data(include_bytes!("style.css")); - gtk::StyleContext::add_provider_for_display( - &Display::default().expect("Error initializing gtk css provider."), - &provider, - gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); -} - -fn show_days( - tx: Sender<Msg>, - grid: >k::Grid, - start_day: &NaiveDate, - today: &NaiveDate, - events: &Vec<Event>, -) { - let mut d = *start_day; - for row in 1..5 { - for col in 0..7 { - grid.attach(&day_entry(tx.clone(), &d, &today, &events), col, row, 1, 1); - d = d.succ(); - } - } -} - -fn day_title(col: i32) -> gtk::Box { - let vbox = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - - vbox.add_css_class("g-Calendar__DayTitle"); - - let label = gtk::Label::builder().label(DAYS[col as usize]).build(); - - vbox.append(&label); - - vbox -} - -fn day_entry( - tx: Sender<Msg>, - date: &NaiveDate, - today: &NaiveDate, - events: &Vec<Event>, -) -> gtk::ScrolledWindow { - let vbox = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - - vbox.add_css_class("g-Calendar__Day"); - - let gesture = gtk::GestureClick::new(); - gesture.connect_pressed(glib::clone!(@strong date => move |_, n, _, _| { - if n == 2 { - send_message(tx.clone(), Msg::ShowAddForm { date }); - } - })); - vbox.add_controller(&gesture); - - if date == today { - vbox.add_css_class("g-Calendar__Day--Today"); - } - - vbox.append(&day_label(date)); - - let mut events = events - .iter() - .filter(|e| e.date == *date) - .collect::<Vec<&Event>>(); - events.sort_by_key(|e| e.start); - - if !events.is_empty() { - vbox.append(&day_events(events)); - } - - let scrolled_window = gtk::ScrolledWindow::builder() - .hscrollbar_policy(gtk::PolicyType::Never) - .hexpand(true) - .vexpand(true) - .child(&vbox) - .build(); - - scrolled_window -} - -fn day_label(date: &NaiveDate) -> gtk::Label { - let label = gtk::Label::builder() - .label(&format!( - "{} {}", - date.day(), - MONTHES[date.month0() as usize] - )) - .halign(gtk::Align::Start) - .build(); - - label.add_css_class("g-Calendar__DayNumber"); - - label -} - -fn day_events(events: Vec<&Event>) -> gtk::Box { - let vbox = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - - for event in events { - let hbox = gtk::Box::builder() - .orientation(gtk::Orientation::Horizontal) - .hexpand(true) - .build(); - - let gesture = gtk::GestureClick::new(); - let click_event = event.clone(); - gesture.connect_pressed(move |gesture, _, _, _| { - gesture.set_state(gtk::EventSequenceState::Claimed); - println!("Click: {:?}", click_event); - }); - hbox.add_controller(&gesture); - - hbox.add_css_class("g-Calendar__DayEvent"); - - let event_txt = &event.pprint(); - let label = gtk::Label::builder() - .label(&event_txt) - .ellipsize(gtk::pango::EllipsizeMode::End) - .tooltip_text(&event_txt) - .halign(gtk::Align::Start) - .build(); - - hbox.append(&label); - vbox.append(&hbox); - } - - vbox -} - -static DATE_FORMAT: &str = "%d/%m/%Y"; - -async fn add_event_dialog(conn: Rc<Connection>, tx: Sender<Msg>, window: Rc<gtk::ApplicationWindow>, date: NaiveDate) { - let dialog = gtk::Dialog::builder() - .transient_for(&*window) - .modal(true) - .title("Ajouter") - .css_classes(vec!["g-Form".to_string()]) - .build(); - - let content_area = dialog.content_area(); - - let vbox = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - vbox.add_css_class("g-Form__Inputs"); - content_area.append(&vbox); - - let name = entry(""); - vbox.append(&label("Événement")); - vbox.append(&name); - - let date = entry(&date.format(DATE_FORMAT).to_string()); - vbox.append(&label("Jour")); - vbox.append(&date); - - let start = entry(""); - vbox.append(&label("Début")); - vbox.append(&start); - - let end = entry(""); - vbox.append(&label("Fin")); - vbox.append(&end); - - let button = gtk::Button::with_label("Créer"); - vbox.append(&button); - button.connect_clicked(glib::clone!(@weak dialog => move |_| { - match validate_event(date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text()) { - Some(event) => { - match db::insert(&conn, &event) { - Ok(_) => { - send_message(tx.clone(), Msg::AddEvent { event: event }); - dialog.close() - }, - Err(_) => () - } - }, - None => () - } - })); - - dialog.run_future().await; -} - -fn entry(text: &str) -> gtk::Entry { - gtk::Entry::builder().text(text).margin_bottom(10).build() -} - -fn label(text: &str) -> gtk::Label { - gtk::Label::builder() - .label(text) - .halign(gtk::Align::Start) - .margin_bottom(5) - .build() -} - -fn validate_event(date: String, name: String, start: String, end: String) -> Option<Event> { - let start = validate_time(start)?; - let end = validate_time(end)?; - - match (start, end) { - (Some(s), Some(e)) if s > e => None?, - _ => (), - } - - Some(Event { - date: NaiveDate::parse_from_str(&date, DATE_FORMAT).ok()?, - name: validate_name(name)?, - start, - end, - }) -} - -fn validate_time(time: String) -> Option<Option<NaiveTime>> { - let time = time.trim(); - if time.is_empty() { - Some(None) - } else { - event::parse_time(time).map(|t| Some(t)) - } -} - -fn validate_name(name: String) -> Option<String> { - let name = name.trim(); - if name.is_empty() { - None - } else { - Some(name.to_string()) - } -} diff --git a/src/app/app.rs b/src/app/app.rs new file mode 100644 index 0000000..45904a9 --- /dev/null +++ b/src/app/app.rs @@ -0,0 +1,57 @@ +use gtk4 as gtk; + +use async_channel::Sender; +use chrono::{Datelike, NaiveDate, Weekday}; +use gtk::glib::signal::Inhibit; +use gtk::prelude::*; +use rusqlite::Connection; +use std::rc::Rc; + +use crate::app::calendar; +use crate::app::update::Msg; +use crate::{db, model::event::Event}; + +pub struct App { + pub window: Rc<gtk::ApplicationWindow>, + pub grid: gtk::Grid, + pub events: Vec<Event>, + pub today: NaiveDate, + pub start_date: NaiveDate, +} + +impl App { + pub fn new(conn: Rc<Connection>, app: >k::Application, tx: Sender<Msg>) -> Self { + let window = Rc::new( + gtk::ApplicationWindow::builder() + .application(app) + .title("Calendar") + .default_width(800) + .default_height(600) + .visible(true) + .build(), + ); + + let today = chrono::offset::Local::today().naive_utc(); + let start_date = + NaiveDate::from_isoywd(today.year(), today.iso_week().week(), Weekday::Mon); + let events = db::list(&conn).unwrap_or(vec![]); + let grid = calendar::grid(tx, &today, &start_date, &events); + + window.set_child(Some(&grid)); + + window.connect_close_request(move |window| { + if let Some(application) = window.application() { + application.remove_window(window); + } + Inhibit(false) + }); + + Self { + window, + grid, + events, + today, + start_date, + } + } +} diff --git a/src/app/calendar.rs b/src/app/calendar.rs new file mode 100644 index 0000000..847ea71 --- /dev/null +++ b/src/app/calendar.rs @@ -0,0 +1,157 @@ +use gtk4 as gtk; + +use async_channel::Sender; +use chrono::{Datelike, NaiveDate}; +use gtk::glib; +use gtk::prelude::*; + +use crate::{app::update, app::update::Msg, model::event::Event}; + +static DAYS: [&str; 7] = ["LUN", "MAR", "MER", "JEU", "VEN", "SAM", "DIM"]; +static MONTHES: [&str; 12] = [ + "Jan", "Fév", "Mar", "Avr", "Mai", "Juin", "Juil", "Aoû", "Sep", "Oct", "Nov", "Déc", +]; + +pub fn grid( + tx: Sender<Msg>, + today: &NaiveDate, + start_date: &NaiveDate, + events: &Vec<Event>, +) -> gtk::Grid { + let grid = gtk::Grid::builder().build(); + + for col in 0..7 { + grid.attach(&day_title(col), col, 0, 1, 1); + } + + show_days(tx, &grid, &start_date, &today, &events); + + grid +} + +fn show_days( + tx: Sender<Msg>, + grid: >k::Grid, + start_date: &NaiveDate, + today: &NaiveDate, + events: &Vec<Event>, +) { + let mut d = *start_date; + for row in 1..5 { + for col in 0..7 { + grid.attach(&day_entry(tx.clone(), &d, &today, &events), col, row, 1, 1); + d = d.succ(); + } + } +} + +fn day_title(col: i32) -> gtk::Box { + let vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + + vbox.add_css_class("g-Calendar__DayTitle"); + + let label = gtk::Label::builder().label(DAYS[col as usize]).build(); + + vbox.append(&label); + + vbox +} + +pub fn day_entry( + tx: Sender<Msg>, + date: &NaiveDate, + today: &NaiveDate, + events: &Vec<Event>, +) -> gtk::ScrolledWindow { + let vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + + vbox.add_css_class("g-Calendar__Day"); + + let gesture = gtk::GestureClick::new(); + gesture.connect_pressed(glib::clone!(@strong date => move |_, n, _, _| { + if n == 2 { + update::send(tx.clone(), Msg::ShowAddForm { date }); + } + })); + vbox.add_controller(&gesture); + + if date == today { + vbox.add_css_class("g-Calendar__Day--Today"); + } + + vbox.append(&day_label(date)); + + let mut events = events + .iter() + .filter(|e| e.date == *date) + .collect::<Vec<&Event>>(); + events.sort_by_key(|e| e.start); + + if !events.is_empty() { + vbox.append(&day_events(events)); + } + + let scrolled_window = gtk::ScrolledWindow::builder() + .hscrollbar_policy(gtk::PolicyType::Never) + .hexpand(true) + .vexpand(true) + .child(&vbox) + .build(); + + scrolled_window +} + +fn day_label(date: &NaiveDate) -> gtk::Label { + let label = gtk::Label::builder() + .label(&format!( + "{} {}", + date.day(), + MONTHES[date.month0() as usize] + )) + .halign(gtk::Align::Start) + .build(); + + label.add_css_class("g-Calendar__DayNumber"); + + label +} + +fn day_events(events: Vec<&Event>) -> gtk::Box { + let vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + + for event in events { + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .hexpand(true) + .build(); + + let gesture = gtk::GestureClick::new(); + let click_event = event.clone(); + gesture.connect_pressed(move |gesture, _, _, _| { + gesture.set_state(gtk::EventSequenceState::Claimed); + println!("Click: {:?}", click_event); + }); + hbox.add_controller(&gesture); + + hbox.add_css_class("g-Calendar__DayEvent"); + + let event_txt = &event.pprint(); + let label = gtk::Label::builder() + .label(&event_txt) + .ellipsize(gtk::pango::EllipsizeMode::End) + .tooltip_text(&event_txt) + .halign(gtk::Align::Start) + .build(); + + hbox.append(&label); + vbox.append(&hbox); + } + + vbox +} diff --git a/src/app/form.rs b/src/app/form.rs new file mode 100644 index 0000000..fc3dc83 --- /dev/null +++ b/src/app/form.rs @@ -0,0 +1,79 @@ +use gtk4 as gtk; + +use async_channel::Sender; +use chrono::NaiveDate; +use gtk::glib; +use gtk::prelude::*; +use rusqlite::Connection; +use std::rc::Rc; + +use crate::{app::update, app::update::Msg, db, model::event}; + +pub async fn dialog( + conn: Rc<Connection>, + tx: Sender<Msg>, + window: Rc<gtk::ApplicationWindow>, + date: NaiveDate, +) { + let dialog = gtk::Dialog::builder() + .transient_for(&*window) + .modal(true) + .title("Ajouter") + .css_classes(vec!["g-Form".to_string()]) + .build(); + + let content_area = dialog.content_area(); + + let vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + vbox.add_css_class("g-Form__Inputs"); + content_area.append(&vbox); + + let name = entry(""); + vbox.append(&label("Événement")); + vbox.append(&name); + + let date = entry(&date.format(event::DATE_FORMAT).to_string()); + vbox.append(&label("Jour")); + vbox.append(&date); + + let start = entry(""); + vbox.append(&label("Début")); + vbox.append(&start); + + let end = entry(""); + vbox.append(&label("Fin")); + vbox.append(&end); + + let button = gtk::Button::with_label("Créer"); + vbox.append(&button); + button.connect_clicked(glib::clone!(@weak dialog => move |_| { + match event::validate(date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text()) { + Some(event) => { + match db::insert(&conn, &event) { + Ok(_) => { + update::send(tx.clone(), Msg::AddEvent { event: event }); + dialog.close() + }, + Err(_) => () + } + }, + None => () + } + })); + + dialog.run_future().await; +} + +fn entry(text: &str) -> gtk::Entry { + gtk::Entry::builder().text(text).margin_bottom(10).build() +} + +fn label(text: &str) -> gtk::Label { + gtk::Label::builder() + .label(text) + .halign(gtk::Align::Start) + .margin_bottom(5) + .build() +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..30b59af --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,38 @@ +mod app; +mod calendar; +mod form; +mod update; +mod utils; + +use gtk4 as gtk; + +use gtk::gdk::Display; +use gtk::prelude::*; +use rusqlite::Connection; +use std::rc::Rc; + +use app::App; + +pub fn run(conn: Connection) { + let conn = Rc::new(conn); + let app = gtk::Application::new(Some("me.guyonvarch.calendar"), Default::default()); + app.connect_startup(|_| load_style()); + app.connect_activate(move |app| build_ui(conn.clone(), app)); + app.run(); +} + +fn build_ui(conn: Rc<Connection>, app: >k::Application) { + let (tx, rx) = async_channel::unbounded(); + let app = App::new(conn.clone(), app, tx.clone()); + utils::spawn(update::event_handler(conn, rx, tx, app)) +} + +fn load_style() { + let provider = gtk::CssProvider::new(); + provider.load_from_data(include_bytes!("style.css")); + gtk::StyleContext::add_provider_for_display( + &Display::default().expect("Error initializing gtk css provider."), + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); +} diff --git a/src/style.css b/src/app/style.css index 5cd1394..5cd1394 100644 --- a/src/style.css +++ b/src/app/style.css diff --git a/src/app/update.rs b/src/app/update.rs new file mode 100644 index 0000000..288ec51 --- /dev/null +++ b/src/app/update.rs @@ -0,0 +1,52 @@ +use gtk4 as gtk; + +use async_channel::{Receiver, Sender}; +use chrono::NaiveDate; +use gtk::prelude::*; +use rusqlite::Connection; +use std::rc::Rc; + +use crate::app::calendar; +use crate::app::form; +use crate::app::utils; +use crate::app::App; +use crate::model::event::Event; + +pub fn send(tx: Sender<Msg>, msg: Msg) { + utils::spawn(async move { + let _ = tx.send(msg).await; + }) +} + +pub enum Msg { + ShowAddForm { date: NaiveDate }, + AddEvent { event: Event }, +} + +pub async fn event_handler(conn: Rc<Connection>, rx: Receiver<Msg>, tx: Sender<Msg>, mut app: App) { + while let Ok(msg) = rx.recv().await { + match msg { + Msg::ShowAddForm { date } => { + form::dialog(Rc::clone(&conn), tx.clone(), Rc::clone(&app.window), date).await; + } + Msg::AddEvent { event } => { + let date = event.date.clone(); + + let d = date.signed_duration_since(app.start_date).num_days(); + + app.events.push(event); + + let col = (d % 7) as i32; + let row = 1 + (d / 7) as i32; + + app.grid.attach( + &calendar::day_entry(tx.clone(), &date, &app.today, &app.events), + col, + row, + 1, + 1, + ); + } + } + } +} diff --git a/src/app/utils.rs b/src/app/utils.rs new file mode 100644 index 0000000..673b96e --- /dev/null +++ b/src/app/utils.rs @@ -0,0 +1,9 @@ +use std::future::Future; + +/// Spawns a task on the default executor, without waiting for it to complete +pub fn spawn<F>(future: F) +where + F: Future<Output = ()> + 'static, +{ + gtk4::glib::MainContext::default().spawn_local(future); +} diff --git a/src/db/mod.rs b/src/db/mod.rs index 3348673..6cfa2eb 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -23,14 +23,14 @@ pub fn insert(conn: &Connection, event: &Event) -> Result<()> { pub fn list(conn: &Connection) -> Result<Vec<Event>> { let mut stmt = conn.prepare("SELECT date, start, end, name FROM events")?; - let iter = stmt.query_map([], |row| + let iter = stmt.query_map([], |row| { Ok(Event { date: row.get(0)?, start: row.get(1)?, end: row.get(2)?, name: row.get(3)?, }) - )?; + })?; Ok(iter.map(|r| r.unwrap()).collect()) } diff --git a/src/main.rs b/src/main.rs index f30e38e..f5f4861 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ mod app; -mod model; mod db; +mod model; use anyhow::Result; diff --git a/src/model/event.rs b/src/model/event.rs index 2650c47..a0bdc85 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -1,7 +1,8 @@ use chrono::Timelike; use chrono::{NaiveDate, NaiveTime}; -// #[derive(Debug, Clone, sqlx::FromRow)] +pub static DATE_FORMAT: &str = "%d/%m/%Y"; + #[derive(Debug, Clone)] pub struct Event { pub date: NaiveDate, @@ -34,7 +35,7 @@ fn pprint_time(t: NaiveTime) -> String { } } -pub fn parse_time(t: &str) -> Option<NaiveTime> { +fn parse_time(t: &str) -> Option<NaiveTime> { match t.split('h').collect::<Vec<&str>>()[..] { [hours, minutes] => { if minutes.trim().is_empty() { @@ -46,3 +47,40 @@ pub fn parse_time(t: &str) -> Option<NaiveTime> { _ => None, } } + +// Validation + +pub fn validate(date: String, name: String, start: String, end: String) -> Option<Event> { + let start = validate_time(start)?; + let end = validate_time(end)?; + + match (start, end) { + (Some(s), Some(e)) if s > e => None?, + _ => (), + } + + Some(Event { + date: NaiveDate::parse_from_str(&date, DATE_FORMAT).ok()?, + name: validate_name(name)?, + start, + end, + }) +} + +fn validate_time(time: String) -> Option<Option<NaiveTime>> { + let time = time.trim(); + if time.is_empty() { + Some(None) + } else { + parse_time(time).map(|t| Some(t)) + } +} + +fn validate_name(name: String) -> Option<String> { + let name = name.trim(); + if name.is_empty() { + None + } else { + Some(name.to_string()) + } +} |