171 lines
7.2 KiB
Rust
171 lines
7.2 KiB
Rust
//! The read commands over a `.base` file: `base.list`, `base.read`, `base.properties`, `base.query`.
|
|
|
|
mod common;
|
|
|
|
use common::{seed, temp_vault};
|
|
use notescraft_engine::{Registry, Session, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
const BOOKS: &str = "filters:
|
|
and:
|
|
- 'file.ext == \"md\"'
|
|
- 'note.type == \"book\"'
|
|
properties:
|
|
note.year:
|
|
displayName: Year
|
|
views:
|
|
- type: table
|
|
name: Books
|
|
order: [file.name, note.year, note.rating]
|
|
sort:
|
|
- property: note.year
|
|
direction: DESC
|
|
summaries:
|
|
note.rating: Sum
|
|
- type: table
|
|
name: By status
|
|
order: [file.name, note.status]
|
|
groupBy:
|
|
property: note.status
|
|
direction: ASC
|
|
limit: 6
|
|
";
|
|
|
|
struct Fixture {
|
|
_vault: common::TempVault,
|
|
session: Session,
|
|
reg: Registry<Session>,
|
|
}
|
|
|
|
impl Fixture {
|
|
fn run(&mut self, id: &str, params: &Value) -> Value {
|
|
self.reg.run(id, &mut self.session, params).unwrap_or_else(|error| panic!("{id}: {error:?}"))
|
|
}
|
|
|
|
fn fail(&mut self, id: &str, params: &Value) -> (String, String) {
|
|
let error = self.reg.run(id, &mut self.session, params).expect_err("must fail");
|
|
match error {
|
|
notescraft_engine::Error::Command { source, .. } => (source.code, source.message),
|
|
other => panic!("{id}: not a command error: {other:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fixture(name: &str) -> Fixture {
|
|
let vault = temp_vault(name);
|
|
let mut files: Vec<(String, String)> = (0..15)
|
|
.map(|i| {
|
|
let status = if i % 2 == 0 { "read" } else { "todo" };
|
|
(format!("Books/book-{i:02}.md"), format!("---\ntype: book\nyear: {}\nrating: {}\nstatus: {status}\n---\n# Book {i}\n", 1950 + i * 5, i % 5 + 1))
|
|
})
|
|
.collect();
|
|
files.extend((0..5).map(|i| (format!("Articles/art-{i}.md"), format!("---\ntype: article\nyear: {}\n---\nbody {i}\n", 2000 + i))));
|
|
files.push(("Pics/cover.png".into(), "x".into()));
|
|
files.push(("Pics/cover.png".into(), "x".into()));
|
|
files.push(("Books.base".into(), BOOKS.into()));
|
|
files.push(("Broken.base".into(), "views: [\n".into()));
|
|
let borrowed: Vec<(&str, &str)> = files.iter().map(|(p, c)| (p.as_str(), c.as_str())).collect();
|
|
seed(vault.path(), &borrowed);
|
|
let session = Session::open(vault.path()).expect("open");
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register");
|
|
Fixture { _vault: vault, session, reg }
|
|
}
|
|
|
|
fn names(result: &Value) -> Vec<String> {
|
|
result["rows"].as_array().expect("rows").iter().map(|r| r["cells"][0].as_str().unwrap_or_default().to_owned()).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn list_read_and_properties() {
|
|
let mut f = fixture("base_read");
|
|
let list = f.run("base.list", &Value::Null);
|
|
let list = list.as_array().expect("array");
|
|
assert_eq!(list.len(), 2);
|
|
assert_eq!(list[0]["path"], "Books.base");
|
|
assert_eq!(list[0]["views"], json!(["Books", "By status"]));
|
|
assert!(list[1]["error"].as_str().is_some(), "a broken base is listed with its error");
|
|
|
|
let model = f.run("base.read", &json!({ "path": "Books.base" }));
|
|
assert_eq!(model["views"][1]["limit"], 6);
|
|
assert_eq!(model["properties"]["note.year"]["displayName"], "Year");
|
|
assert_eq!(model["path"], "Books.base");
|
|
assert_eq!(model["text"], BOOKS, "the raw file text rides along for embeds");
|
|
|
|
let props = f.run("base.properties", &Value::Null);
|
|
let find = |name: &str| props["properties"].as_array().expect("array").iter().find(|p| p["name"] == name).cloned().expect("property");
|
|
assert_eq!(find("year"), json!({ "name": "year", "type": "number", "count": 20 }));
|
|
assert_eq!(find("status")["count"], 15);
|
|
assert_eq!(find("type")["type"], "text");
|
|
}
|
|
|
|
#[test]
|
|
fn filter_sort_summary() {
|
|
let mut f = fixture("base_query");
|
|
let result = f.run("base.query", &json!({ "path": "Books.base" }));
|
|
assert_eq!(result["total"], 15);
|
|
assert_eq!(result["columns"][1]["name"], "Year");
|
|
let first = names(&result);
|
|
assert_eq!(first.len(), 15);
|
|
assert_eq!(first[0], "book-14.md", "newest first");
|
|
assert_eq!(first[14], "book-00.md");
|
|
assert_eq!(result["rows"][0]["cells"][1], 2020);
|
|
// ratings 1..5 repeated three times
|
|
assert_eq!(result["summaries"][2], 45);
|
|
assert_eq!(result["errors"], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn groups_view_by_name_and_slices() {
|
|
let mut f = fixture("base_groups");
|
|
let result = f.run("base.query", &json!({ "path": "Books.base", "view": "By status" }));
|
|
assert_eq!(result["total"], 15);
|
|
let groups = result["groups"].as_array().expect("groups");
|
|
assert_eq!(groups.len(), 1, "limit 6 cuts after the first group: read has 8 rows");
|
|
assert_eq!(groups[0]["key"], "read");
|
|
assert_eq!(groups[0]["count"], 6);
|
|
|
|
let slice = f.run("base.query", &json!({ "path": "Books.base", "offset": 2, "limit": 3 }));
|
|
assert_eq!(names(&slice), ["book-12.md", "book-11.md", "book-10.md"]);
|
|
let by_index = f.run("base.query", &json!({ "path": "Books.base", "view": 1, "limit": 2 }));
|
|
assert_eq!(by_index["rows"].as_array().expect("rows").len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn inline_yaml_and_this() {
|
|
let mut f = fixture("base_inline");
|
|
let yaml = "filters: 'file.folder == \"Articles\"'\nviews:\n - type: table\n order: [file.name, note.year]\n";
|
|
let result = f.run("base.query", &json!({ "yaml": yaml }));
|
|
assert_eq!(result["total"], 5);
|
|
let by_this = "filters: 'note.year == this.year'\nviews:\n - type: table\n order: [file.name]\n";
|
|
let result = f.run("base.query", &json!({ "yaml": by_this, "this": "Articles/art-2.md" }));
|
|
assert_eq!(names(&result), ["art-2.md"]);
|
|
let sizes = "filters: 'file.size > 0'\nviews:\n - type: table\n order: [file.name]\n";
|
|
assert_eq!(f.run("base.query", &json!({ "yaml": sizes }))["total"], 20, "the 20 notes carry a size; other files are not stat-ed");
|
|
}
|
|
|
|
#[test]
|
|
fn bad_input_has_a_code() {
|
|
let mut f = fixture("base_errors");
|
|
let (code, message) = f.fail("base.query", &json!({ "yaml": "views:\n - type: [\n" }));
|
|
assert_eq!(code, "invalid_params");
|
|
assert!(message.contains("line"), "{message}");
|
|
assert_eq!(f.fail("base.query", &json!({ "path": "Nope.base" })).0, "not_found");
|
|
assert_eq!(f.fail("base.read", &json!({ "path": "Books/book-00.md" })).0, "invalid_path");
|
|
assert_eq!(f.fail("base.query", &json!({ "path": "Books.base", "limit": 2001 })).0, "invalid_params");
|
|
assert_eq!(f.fail("base.query", &json!({ "path": "Books.base", "view": "Missing" })).0, "invalid_params");
|
|
assert_eq!(f.fail("base.query", &json!({})).0, "invalid_params");
|
|
assert_eq!(f.fail("base.query", &json!({ "path": "Books.base", "yaml": "views: []" })).0, "invalid_params");
|
|
assert_eq!(f.fail("base.read", &json!({ "path": "Broken.base" })).0, "invalid_params");
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_names_attachments_and_the_root() {
|
|
let mut f = fixture("base_resolve");
|
|
let result = f.run("base.resolve", &json!({ "from": "Books/book-00.md", "targets": ["cover.png", "Pics/cover.png", "nope.png", 3] }));
|
|
assert_eq!(result["paths"], json!(["Pics/cover.png", "Pics/cover.png", null, null]));
|
|
assert!(result["root"].as_str().is_some_and(|root| !root.is_empty()));
|
|
let too_many: Vec<Value> = (0..65).map(|i| json!(format!("{i}.png"))).collect();
|
|
assert_eq!(f.fail("base.resolve", &json!({ "from": "a.md", "targets": too_many })).0, "invalid_params");
|
|
}
|