From 96efa71205657629dd0a4d379c7fc9e83a196684 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 14 Nov 2021 21:13:20 +0100 Subject: [PATCH] going async (wip) --- Cargo.lock | 24 +-- src/feed/client.rs | 31 +--- src/feed/mod.rs | 83 +-------- src/ui/components/lists/feed_list.rs | 45 +++++ src/ui/components/{lists.rs => lists/mod.rs} | 6 +- src/ui/lib/client.rs | 186 +++++++++++++++++++ src/ui/lib/kiosk.rs | 154 +++++++++++++++ src/ui/lib/mod.rs | 32 ++++ src/ui/mod.rs | 2 + 9 files changed, 443 insertions(+), 120 deletions(-) create mode 100644 src/ui/components/lists/feed_list.rs rename src/ui/components/{lists.rs => lists/mod.rs} (98%) create mode 100644 src/ui/lib/client.rs create mode 100644 src/ui/lib/kiosk.rs create mode 100644 src/ui/lib/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 2382fd8..4f6a1a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,9 +81,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.71" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" [[package]] name = "cfg-if" @@ -571,9 +571,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b5ac6078ca424dc1d3ae2328526a76787fecc7f8011f520e3276730e711fc95" +checksum = "dac4581f0fc0e0efd529d069e8189ec7b90b8e7680e21beb35141bdc45f36040" dependencies = [ "log", "ring", @@ -625,9 +625,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8" +checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3" dependencies = [ "itoa", "ryu", @@ -757,9 +757,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" dependencies = [ "tinyvec_macros", ] @@ -794,9 +794,9 @@ dependencies = [ [[package]] name = "tui-realm-stdlib" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1655e08ce23aafc50cf6ef016e36c0e7fc0bac8d741572a3fa7be386e074fa19" +checksum = "d6143455389f67580cbd4aea01fa252af67e430bed3a92b9a0b0ad82aae7a6b5" dependencies = [ "textwrap", "tuirealm", @@ -911,9 +911,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd912a3d096959150c4d71ac752e13f1683085922658c205b89b40fe8ebe07f" +checksum = "c5c448dcb78ec38c7d59ec61f87f70a98ea19171e06c139357e012ee226fec90" dependencies = [ "base64", "chunked_transfer", diff --git a/src/feed/client.rs b/src/feed/client.rs index 1a952aa..d102be2 100644 --- a/src/feed/client.rs +++ b/src/feed/client.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use super::{Feed, FeedError, FeedResult, Kiosk}; +use super::{Feed, FeedError, FeedResult}; use feed_rs::parser as feed_parser; use std::io::Read; @@ -36,24 +36,16 @@ use std::io::Read; pub struct Client; impl Client { - /// ### fetch - /// - /// Fetch a source with the current configuration - pub fn fetch(&self, kiosk: &mut Kiosk, name: &str, url: &str) -> FeedResult<()> { - kiosk.insert_feed(name.to_string(), self.fetch_source(url)?); - Ok(()) - } - - // -- private - /// ### fetch_source /// /// Fetch a single source from remote - fn fetch_source(&self, source: &str) -> FeedResult { + pub fn fetch(&self, source: &str) -> FeedResult { let body = self.get_feed(source)?; self.parse_feed(body) } + // -- private + /// ### get_feed /// /// Get feed via HTTP GET request @@ -99,22 +91,11 @@ mod test { #[test] fn should_fetch_source() { let client = Client::default(); - let mut kiosk = Kiosk::default(); assert!(client - .fetch( - &mut kiosk, - "nytimes", - "https://rss.nytimes.com/services/xml/rss/nyt/World.xml", - ) + .fetch("https://rss.nytimes.com/services/xml/rss/nyt/World.xml",) .is_ok()); - assert!(kiosk.get_feed("nytimes").is_some()); assert!(client - .fetch( - &mut kiosk, - "lefigaro", - "https://www.lefigaro.fr/rss/figaro_actualites.xml", - ) + .fetch("https://www.lefigaro.fr/rss/figaro_actualites.xml",) .is_ok()); - assert!(kiosk.get_feed("lefigaro").is_some()); } } diff --git a/src/feed/mod.rs b/src/feed/mod.rs index 883d425..0e481ac 100644 --- a/src/feed/mod.rs +++ b/src/feed/mod.rs @@ -38,23 +38,12 @@ pub use result::{FeedError, FeedResult}; // -- deps use chrono::{DateTime, Local}; use feed_rs::model::{Entry as RssEntry, Feed as RssFeed}; -use std::collections::HashMap; use std::slice::Iter; -/// ## Kiosk -/// -/// Describes the current feed holder. -/// It contains different sources, each one with its own feed -#[derive(Debug, Default)] -pub struct Kiosk { - /// Association between Source name and Feed - feed: HashMap, -} - /// ## Feed /// /// Contains, for a feed source, the list of articles fetched from remote -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq)] pub struct Feed { articles: Vec
, } @@ -62,7 +51,7 @@ pub struct Feed { /// ## Article /// /// identifies a single article in the feed -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq)] pub struct Article { pub title: Option, pub authors: Vec, @@ -71,31 +60,6 @@ pub struct Article { pub date: Option>, } -// -- impl - -impl Kiosk { - /// ### insert_feed - /// - /// Insert a feed into kiosk - pub fn insert_feed>(&mut self, source: S, feed: Feed) { - self.feed.insert(source.as_ref().to_string(), feed); - } - - /// ### get_feed - /// - /// Get feed from kiosk - pub fn get_feed(&self, source: &str) -> Option<&Feed> { - self.feed.get(source) - } - - /// ### sources - /// - /// Get sources in kiosk - pub fn sources(&self) -> Vec<&String> { - self.feed.keys().into_iter().collect() - } -} - impl Feed { /// ### articles /// @@ -142,49 +106,6 @@ mod test { use feed_rs::model::FeedType; use pretty_assertions::assert_eq; - #[test] - fn should_create_kiosk() { - let kiosk = Kiosk::default(); - assert!(kiosk.feed.is_empty()); - } - - #[test] - fn should_insert_feed_into_kiosk() { - let mut kiosk = Kiosk::default(); - kiosk.insert_feed( - "lefigaro", - Feed { - articles: Vec::default(), - }, - ); - assert_eq!(kiosk.feed.len(), 1); - } - - #[test] - fn should_get_feed_from_kiosk() { - let mut kiosk = Kiosk::default(); - kiosk.insert_feed( - "lefigaro", - Feed { - articles: Vec::default(), - }, - ); - assert!(kiosk.get_feed("lefigaro").is_some()); - assert!(kiosk.get_feed("foobar").is_none()); - } - - #[test] - fn should_get_sources_from_kiosk() { - let mut kiosk = Kiosk::default(); - kiosk.insert_feed( - "lefigaro", - Feed { - articles: Vec::default(), - }, - ); - assert_eq!(kiosk.sources(), vec![&String::from("lefigaro")]); - } - #[test] fn should_get_feed_attributes() { let feed = Feed { diff --git a/src/ui/components/lists/feed_list.rs b/src/ui/components/lists/feed_list.rs new file mode 100644 index 0000000..ddfa770 --- /dev/null +++ b/src/ui/components/lists/feed_list.rs @@ -0,0 +1,45 @@ +//! # Feed list +//! +//! Mock component to implement the feed list + +/** + * MIT License + * + * tuifeed - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use tui_realm_stdlib::{states::ListStates, List}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::props::{ + Alignment, AttrValue, Attribute, Borders, Color, Props, Style, Table, TextModifiers, +}; +use tuirealm::tui::{ + layout::{Corner, Rect}, + text::{Span, Spans}, + widgets::{List as TuiList, ListItem, ListState}, +}; +use tuirealm::{Frame, MockComponent, State, StateValue}; + +/// ## FeedList +/// +/// A list which prepend the fetch state for each source for the feed +pub struct FeedList { + list: List, +} diff --git a/src/ui/components/lists.rs b/src/ui/components/lists/mod.rs similarity index 98% rename from src/ui/components/lists.rs rename to src/ui/components/lists/mod.rs index 08e8505..62cb850 100644 --- a/src/ui/components/lists.rs +++ b/src/ui/components/lists/mod.rs @@ -25,6 +25,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +mod feed_list; + use super::Msg; use tui_realm_stdlib::List; @@ -35,13 +37,13 @@ use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; #[derive(MockComponent)] pub struct FeedList { - component: List, + component: feed_list::FeedList, } impl FeedList { pub fn new(sources: &[&String]) -> Self { Self { - component: List::default() + component: feed_list::FeedList::default() .highlighted_color(Color::LightBlue) .highlighted_str("➤ ") .rewind(true) diff --git a/src/ui/lib/client.rs b/src/ui/lib/client.rs new file mode 100644 index 0000000..10124b4 --- /dev/null +++ b/src/ui/lib/client.rs @@ -0,0 +1,186 @@ +//! # Client +//! +//! Async feed client + +/** + * MIT License + * + * tuifeed - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use crate::feed::{Client, Feed, FeedResult}; +use std::sync::{mpsc, Arc, RwLock}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +#[derive(Debug)] +pub struct FeedClient { + /// Indicates whether the worker should keep running + running: Arc>, + /// Receiver. The worker send a message with the result of the fetch process + recv: mpsc::Receiver<(String, FeedResult)>, + /// Sender. Used to request to the worker to fetch a source (SourceName, Uri) + send: mpsc::Sender<(String, String)>, + /// Join handle + thread: Option>, +} + +impl FeedClient { + /// ### start + /// + /// Start feed client + pub fn start() -> Self { + let (source_send, source_recv) = mpsc::channel(); + let (feed_send, feed_recv) = mpsc::channel(); + let running = Arc::new(RwLock::new(true)); + let running_t = Arc::clone(&running); + // Start thread + let thread = thread::spawn(move || { + Worker::new(source_recv, feed_send, running_t).run(); + }); + Self { + running, + recv: feed_recv, + send: source_send, + thread: Some(thread), + } + } + + /// ### stop + /// + /// Stop event listener + pub fn stop(&mut self) { + { + // NOTE: keep these brackets to drop running after block + if let Ok(running) = self.running.write() { + *running = false; + } + } + // Join thread + let _ = self.thread.take().map(|x| x.join()); + } + + /// ### fetch + /// + /// Fetch source. + /// Panics if fails to send request + pub fn fetch(&self, name: &str, uri: &str) { + if let Err(err) = self.send.send((name.to_string(), uri.to_string())) { + panic!("Runtime error (fetch): {}", err); + } + } + + /// ### poll + /// + /// Poll receiver for fetch results. + /// Panics if fails to poll + pub fn poll(&self) -> Option<(String, FeedResult)> { + match self.recv.recv_timeout(Duration::from_millis(10)) { + Ok(msg) => Some(msg), + Err(mpsc::RecvTimeoutError::Timeout) => None, + Err(err) => panic!("Runtime error (poll): {}", err), + } + } +} + +impl Drop for FeedClient { + fn drop(&mut self) { + let _ = self.stop(); + } +} + +// -- worker + +/// ## Worker +/// +/// Worker thread which fetches async the feed sources +pub struct Worker { + client: Client, + running: Arc>, + recv: mpsc::Receiver<(String, String)>, + send: mpsc::Sender<(String, FeedResult)>, +} + +impl Worker { + /// ### new + /// + /// Instantiates a new `Worker`+ + pub fn new( + recv: mpsc::Receiver<(String, String)>, + send: mpsc::Sender<(String, FeedResult)>, + running: Arc>, + ) -> Self { + Self { + client: Client::default(), + running, + recv, + send, + } + } + + /// ### run + /// + /// Main loop for Worker + pub fn run(&mut self) { + loop { + if !self.running() { + break; + } + } + } + + // -- private + + /// ### process_fetch_requests + /// + /// Process incoming fetch requests. + /// Panics if fails to poll + fn process_fetch_requests(&mut self) { + loop { + // Get source to fetch + let (name, uri) = match self.recv.recv_timeout(Duration::from_millis(10)) { + Ok((name, uri)) => (name, uri), + Err(mpsc::RecvTimeoutError::Timeout) => break, + Err(err) => panic!("Runtime error (process_fetch_requests): {}", err), + }; + // Fetch source + self.fetch_source(name, uri); + } + } + + /// fetch_source + /// + /// Fetch source + fn fetch_source(&mut self, name: String, uri: String) { + if let Err(err) = self.send.send((name, self.client.fetch(uri.as_str()))) { + panic!("Runtime error (fetch_source): {}", err); + } + } + + /// ### running + /// + /// Returns whether should keep running + fn running(&self) -> bool { + if let Ok(lock) = self.running.read() { + return *lock; + } + true + } +} diff --git a/src/ui/lib/kiosk.rs b/src/ui/lib/kiosk.rs new file mode 100644 index 0000000..5128b28 --- /dev/null +++ b/src/ui/lib/kiosk.rs @@ -0,0 +1,154 @@ +//! # Kiosk +//! +//! kiosk entity, a collector for feed sources + +/** + * MIT License + * + * tuifeed - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use crate::feed::{Feed, FeedError}; + +use std::collections::HashMap; + +/// ## Kiosk +/// +/// Describes the current feed holder. +/// It contains different sources, each one with its own feed fetch state +#[derive(Debug, Default)] +pub struct Kiosk { + /// Association between Source name and Feed + feed: HashMap, +} + +/// ## FeedState +/// +/// Describes the current feed state for a source. +#[derive(Debug, PartialEq)] +pub enum FeedState { + /// Feed loaded with success + Success(Feed), + /// Failed to fetch / parse feed + Error(FeedError), + /// Loading feed + Loading, + /// Not fetched yet + None, +} + +impl Kiosk { + /// ### insert_feed + /// + /// Insert a feed into kiosk + pub fn insert_feed>(&mut self, source: S, state: FeedState) { + self.feed.insert(source.as_ref().to_string(), state); + } + + /// ### get_feed_state + /// + /// Get current feed state + pub fn get_feed_state(&self, source: &str) -> Option<&FeedState> { + self.feed.get(source) + } + + /// ### get_feed + /// + /// Get feed from kiosk. + /// Feed is returned only if source exists and if the current feed state is `Success` + pub fn get_feed(&self, source: &str) -> Option<&Feed> { + if let Some(FeedState::Success(feed)) = self.get_feed_state(source) { + Some(feed) + } else { + None + } + } + + /// ### sources + /// + /// Get sources in kiosk + pub fn sources(&self) -> Vec<&String> { + self.feed.keys().into_iter().collect() + } +} + +#[cfg(test)] +mod test { + + use super::*; + + use pretty_assertions::assert_eq; + + #[test] + fn should_create_kiosk() { + let kiosk = Kiosk::default(); + assert!(kiosk.feed.is_empty()); + } + + #[test] + fn should_insert_feed_into_kiosk() { + let mut kiosk = Kiosk::default(); + kiosk.insert_feed( + "lefigaro", + FeedState::Success(Feed { + articles: Vec::default(), + }), + ); + assert_eq!(kiosk.feed.len(), 1); + } + + #[test] + fn should_get_feed_from_kiosk() { + let mut kiosk = Kiosk::default(); + kiosk.insert_feed( + "lefigaro", + FeedState::Success(Feed { + articles: Vec::default(), + }), + ); + assert!(kiosk.get_feed("lefigaro").is_some()); + assert!(kiosk.get_feed("foobar").is_none()); + } + + #[test] + fn should_get_feed_state_from_kiosk() { + let mut kiosk = Kiosk::default(); + kiosk.insert_feed( + "lefigaro", + FeedState::Error(FeedError::Parse(String::from("parse error"))), + ); + assert_eq!( + kiosk.get_feed_state("lefigaro").unwrap(), + &FeedState::Error(FeedError::Parse(String::from("parse error"))) + ); + } + + #[test] + fn should_get_sources_from_kiosk() { + let mut kiosk = Kiosk::default(); + kiosk.insert_feed( + "lefigaro", + FeedState::Success(Feed { + articles: Vec::default(), + }), + ); + assert_eq!(kiosk.sources(), vec![&String::from("lefigaro")]); + } +} diff --git a/src/ui/lib/mod.rs b/src/ui/lib/mod.rs new file mode 100644 index 0000000..bd6bf14 --- /dev/null +++ b/src/ui/lib/mod.rs @@ -0,0 +1,32 @@ +//! # lib +//! +//! ui lib + +/** + * MIT License + * + * tuifeed - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +mod client; +mod kiosk; + +pub use client::FeedClient; +pub use kiosk::{FeedState, Kiosk}; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index badd8b5..2ce5cb5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -26,12 +26,14 @@ * SOFTWARE. */ mod components; +mod lib; mod model; use components::{ErrorPopup, GlobalListener, WaitPopup}; use model::Model; use crate::config::Config; +use lib::{FeedClient, Kiosk}; use std::time::Duration; use tuirealm::{