frontend-utils: Initial recents reader/writer

This commit is contained in:
sleepycatcoding 2024-04-09 00:58:58 +03:00 committed by Nathan Adams
parent 3667d1eb74
commit 7c80091488
6 changed files with 323 additions and 9 deletions

View File

@ -5,8 +5,6 @@ pub use write::BookmarksWriter;
use url::Url;
pub static INVALID_URL: &str = "invalid:///";
#[derive(Debug, PartialEq)]
pub struct Bookmark {
pub url: Url,
@ -15,7 +13,7 @@ pub struct Bookmark {
impl Bookmark {
pub fn is_invalid(&self) -> bool {
self.url.as_str() == INVALID_URL
self.url.as_str() == crate::INVALID_URL
}
}

View File

@ -1,4 +1,4 @@
use crate::bookmarks::{Bookmark, Bookmarks, INVALID_URL};
use crate::bookmarks::{Bookmark, Bookmarks};
use crate::parse::{DocumentHolder, ParseContext, ParseDetails, ParseWarning, ReadExt};
use toml_edit::DocumentMut;
use url::Url;
@ -21,7 +21,7 @@ pub fn read_bookmarks(input: &str) -> ParseDetails<Bookmarks> {
for bookmark in bookmarks.iter() {
let url = match bookmark.parse_from_str(cx, "url") {
Some(value) => value,
None => Url::parse(INVALID_URL).expect("Url is constant and valid"),
None => Url::parse(crate::INVALID_URL).expect("Url is constant and valid"),
};
let name = match bookmark.parse_from_str(cx, "name") {
@ -61,7 +61,7 @@ mod tests {
let result = read_bookmarks("[[bookmark]]");
assert_eq!(
&vec![Bookmark {
url: Url::parse(INVALID_URL).unwrap(),
url: Url::parse(crate::INVALID_URL).unwrap(),
name: "".to_string(),
}],
result.values()
@ -71,7 +71,7 @@ mod tests {
let result = read_bookmarks("[[bookmark]]\nurl = \"invalid\"");
assert_eq!(
&vec![Bookmark {
url: Url::parse(INVALID_URL).unwrap(),
url: Url::parse(crate::INVALID_URL).unwrap(),
name: "".to_string(),
}],
result.values()
@ -144,11 +144,11 @@ mod tests {
name: "example.swf".to_string(),
},
Bookmark {
url: Url::parse(INVALID_URL).unwrap(),
url: Url::parse(crate::INVALID_URL).unwrap(),
name: "".to_string(),
},
Bookmark {
url: Url::parse(INVALID_URL).unwrap(),
url: Url::parse(crate::INVALID_URL).unwrap(),
name: "".to_string(),
},
Bookmark {

View File

@ -1,6 +1,7 @@
pub mod bookmarks;
pub mod bundle;
pub mod parse;
pub mod recents;
pub mod write;
pub mod backends;
@ -9,6 +10,8 @@ pub mod content;
use std::borrow::Cow;
use url::Url;
pub static INVALID_URL: &str = "invalid:///";
pub fn url_to_readable_name(url: &Url) -> Cow<'_, str> {
let name = url
.path_segments()

View File

@ -0,0 +1,35 @@
mod read;
mod write;
pub use read::read_recents;
pub use write::RecentsWriter;
use url::Url;
#[derive(Debug, PartialEq)]
pub struct Recent {
pub url: Url,
}
impl Recent {
pub fn is_invalid(&self) -> bool {
self.url.as_str() == crate::INVALID_URL
}
/// Checks if a recent entry is available.
///
/// If the URL is local file, it will be checked if it exists, otherwise returns `true`.
pub fn is_available(&self) -> bool {
if self.url.scheme() == "file" {
return match self.url.to_file_path() {
Ok(path) => path.exists(),
Err(()) => false,
};
}
true
}
}
/// Recent entries, stored from oldest to newest.
pub type Recents = Vec<Recent>;

View File

@ -0,0 +1,183 @@
use crate::parse::{DocumentHolder, ParseContext, ParseDetails, ParseWarning, ReadExt};
use crate::recents::{Recent, Recents};
use toml_edit::DocumentMut;
use url::Url;
pub fn read_recents(input: &str) -> ParseDetails<Recents> {
let document = match input.parse::<DocumentMut>() {
Ok(document) => document,
Err(e) => {
return ParseDetails {
result: Default::default(),
warnings: vec![ParseWarning::InvalidToml(e)],
}
}
};
let mut result = Vec::new();
let mut cx = ParseContext::default();
document.get_array_of_tables(&mut cx, "recent", |cx, recents| {
for recent in recents.iter() {
let url = match recent.parse_from_str(cx, "url") {
Some(url) => url,
None => Url::parse(crate::INVALID_URL).expect("Url is constant and valid"),
};
result.push(Recent { url });
}
});
ParseDetails {
warnings: cx.warnings,
result: DocumentHolder::new(result, document),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty() {
let result = read_recents("");
assert_eq!(&Vec::<Recent>::new(), result.values());
assert_eq!(Vec::<ParseWarning>::new(), result.warnings);
}
#[test]
fn invalid_array_type() {
let result = read_recents("[recent]");
assert_eq!(&Vec::<Recent>::new(), result.values());
assert_eq!(
vec![ParseWarning::UnexpectedType {
expected: "array of tables",
actual: "table",
path: "recent".to_string()
}],
result.warnings
);
}
#[test]
fn empty_entry() {
let result = read_recents("[[recent]]");
assert_eq!(
&vec![Recent {
url: Url::parse(crate::INVALID_URL).unwrap(),
}],
result.values()
);
assert_eq!(Vec::<ParseWarning>::new(), result.warnings);
}
#[test]
fn invalid_url() {
let result = read_recents("[[recent]]\nurl = \"invalid\"");
assert_eq!(
&vec![Recent {
url: Url::parse(crate::INVALID_URL).unwrap(),
}],
result.values()
);
assert_eq!(
vec![ParseWarning::UnsupportedValue {
value: "invalid".to_string(),
path: "recent.url".to_string()
}],
result.warnings,
);
}
#[test]
fn valid_entry() {
let result = read_recents("[[recent]]\nurl = \"https://ruffle.rs/logo-anim.swf\"\n");
assert_eq!(
&vec![Recent {
url: Url::parse("https://ruffle.rs/logo-anim.swf").unwrap(),
}],
result.values()
);
assert_eq!(Vec::<ParseWarning>::new(), result.warnings);
}
#[test]
fn multiple() {
let result = read_recents(
r#"
[[recent]]
url = "file:///first.swf"
[[recent]]
url = "file:///second.swf"
"#,
);
assert_eq!(
&vec![
Recent {
url: Url::parse("file:///first.swf").unwrap(),
},
Recent {
url: Url::parse("file:///second.swf").unwrap(),
}
],
result.values()
);
assert_eq!(Vec::<ParseWarning>::new(), result.warnings);
}
#[test]
fn multiple_with_invalid_entries() {
let result = read_recents(
r#"
[[recent]]
url = "file:///first.swf"
[[recent]]
[[recent]]
url = 10
[[recent]]
url = "yes"
[[recent]]
url = "file:///second.swf"
"#,
);
assert_eq!(
&vec![
Recent {
url: Url::parse("file:///first.swf").unwrap(),
},
Recent {
url: Url::parse(crate::INVALID_URL).unwrap(),
},
Recent {
url: Url::parse(crate::INVALID_URL).unwrap(),
},
Recent {
url: Url::parse(crate::INVALID_URL).unwrap(),
},
Recent {
url: Url::parse("file:///second.swf").unwrap(),
},
],
result.values()
);
assert_eq!(
vec![
ParseWarning::UnexpectedType {
expected: "string",
actual: "integer",
path: "recent.url".to_string()
},
ParseWarning::UnsupportedValue {
value: "yes".to_string(),
path: "recent.url".to_string()
}
],
result.warnings
);
}
}

View File

@ -0,0 +1,95 @@
use crate::parse::DocumentHolder;
use crate::recents::{Recent, Recents};
use crate::write::TableExt;
use toml_edit::{value, Table};
pub struct RecentsWriter<'a>(&'a mut DocumentHolder<Recents>);
impl<'a> RecentsWriter<'a> {
pub fn new(recents: &'a mut DocumentHolder<Recents>) -> Self {
Self(recents)
}
/// Pushes a new recent entry on the entry stack, if same entry already exists, it will get moved to the top.
pub fn push(&mut self, recent: Recent, limit: usize) {
self.0.edit(|values, toml_document| {
let array = toml_document.get_or_create_array_of_tables("recent");
// First, lets check if we already have existing entry with the same URL and move it to the top.
let existing = values.iter().position(|x| x.url == recent.url);
if let Some(index) = existing {
// Existing entry, just move it to the top.
// Update TOML first, then internal values.
// TODO: Unfortunately, ArrayOfTables does not return the removed entry, so we need to recreate it.
// https://github.com/toml-rs/toml/issues/712
array.remove(index);
let mut table = Table::new();
table["url"] = value(recent.url.as_str());
array.push(table);
let recent = values.remove(index);
values.push(recent);
} else {
// New entry.
// Evict old entries, if we are at or over the limit.
if values.len() >= limit {
// Remove n elements over limit plus 1, since we need to push a new one too.
let elements_to_remove = (values.len() - limit) + 1;
// yes, this is inefficient, but this is not hot code :D (usually we only need to remove 1 element, unless the limit changed)
for _ in 0..elements_to_remove {
array.remove(0);
values.remove(0);
}
}
// Create a new table and push it.
let mut table = Table::new();
table["url"] = value(recent.url.as_str());
array.push(table);
values.push(recent);
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::recents::read_recents;
use url::Url;
crate::define_serialization_test_helpers!(read_recents, Recents, RecentsWriter);
#[test]
fn simple_push() {
test(
"",
|writer| {
writer.push(
Recent {
url: Url::parse("file:///1.swf").unwrap(),
},
10,
)
},
"[[recent]]\nurl = \"file:///1.swf\"\n",
);
}
#[test]
fn test_limit() {
test("[[recent]]\nurl = \"file:///1.swf\"\n[[recent]]\nurl = \"file:///2.swf\"\n[[recent]]\nurl = \"file:///3.swf\"\n", |writer| writer.push(Recent {
url: Url::parse("file:///very_important_file.swf").unwrap(),
}, 2), "[[recent]]\nurl = \"file:///3.swf\"\n\n[[recent]]\nurl = \"file:///very_important_file.swf\"\n");
}
#[test]
fn test_move_to_top() {
test("[[recent]]\nurl = \"file:///very_important_file.swf\"\n[[recent]]\nurl = \"file:///2.swf\"\n[[recent]]\nurl = \"file:///3.swf\"\n", |writer| writer.push(Recent {
url: Url::parse("file:///very_important_file.swf").unwrap(),
}, 3), "[[recent]]\nurl = \"file:///2.swf\"\n[[recent]]\nurl = \"file:///3.swf\"\n\n[[recent]]\nurl = \"file:///very_important_file.swf\"\n");
}
}