diff options
Diffstat (limited to 'src')
| -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()) +    } +} | 
