frontend-utils: Initial recents reader/writer
This commit is contained in:
parent
3667d1eb74
commit
7c80091488
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>;
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue