Files
Marcos Paulo aeac86ddd2
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: Bases model, evaluator, function library and map data model
.base model (filters, formulas, properties, views; unknown keys kept in order; limits and invalid-value errors), evaluator with the D5 type rules, lazy if and boolean operators, memoised formulas with cycle errors, depth cap 200, string/number/object/list/date/link/file/regexp functions with lambdas (regex through notescraft-regex), and the map model (Web Mercator, tile ids, viewport tiles, grid clustering, coordinate and colour parsing, OSM attribution constants; Paris z10 is tile 518/352). Bases tasks 2, 5, 6 (Claude Sonnet) and 8 (MiniMax M3.1 Flash, OpenCode Zen free route). Verified: notescraft-bases suites all pass, clippy -D warnings, xtask layers and tree clean. Known gaps: toFixed rounds ties to even, list.unique is quadratic.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 02:58:07 -03:00

87 lines
4.0 KiB
Rust

//! The `.base` model: parse, write, unknown keys, limits, invalid values.
use notescraft_bases::{Base, Direction, Error, Filter, ViewKind};
const DOC: &str = include_str!("fixtures/Bases_syntax_1.yaml");
#[test]
fn docs_example_round_trips_byte_equal_once_normalised() {
let base = Base::parse(DOC).unwrap_or_else(|e| panic!("{e}"));
let once = base.to_yaml();
let again = Base::parse(&once).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(again, base);
assert_eq!(again.to_yaml(), once);
let view = &base.views()[0];
assert_eq!(view.kind, ViewKind::Table);
assert_eq!(view.name.as_deref(), Some("My table"));
assert_eq!(view.limit, Some(10));
assert_eq!(view.group_by.as_ref().map(|g| g.direction), Some(Direction::Desc));
assert_eq!(base.formulas.len(), 2);
assert!(matches!(&base.filters, Some(Filter::Or(m)) if m.len() == 3));
assert_eq!(base.properties[1].1.display_name.as_deref(), Some("Price"));
}
#[test]
fn canonical_text_is_byte_equal() {
let text = "filters:\n and:\n - \"a > 1\"\n - not:\n - \"b\"\nformulas:\n f: \"a + 1\"\nviews:\n - type: table\n name: T\n limit: 5\n sort:\n - property: a\n direction: DESC\n";
let text = text.replace("\"a > 1\"", "a > 1").replace("- \"b\"", "- b").replace("\"a + 1\"", "a + 1");
let base = Base::parse(&text).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(base.to_yaml(), text);
}
#[test]
fn unknown_keys_and_view_types_survive_in_order() {
let src = "zeta: 1\nviews:\n - type: kanban\n columnSize: 3\n name: K\n weird:\n - x\nalpha: [1, 2]\n";
let base = Base::parse(src).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(base.views()[0].kind, ViewKind::Other("kanban".into()));
assert_eq!(base.views()[0].extra.len(), 2);
assert!(base.views()[0].extra_get("columnSize").is_some());
let out = base.to_yaml();
let pos = |k: &str| out.find(k).unwrap_or(usize::MAX);
assert!(pos("zeta") < pos("views") && pos("views") < pos("alpha"));
assert!(pos("columnSize") < pos("name:") && pos("name:") < pos("weird"));
assert_eq!(Base::parse(&out).unwrap_or_else(|e| panic!("{e}")), base);
}
#[test]
fn limits_are_refused() {
let views: String = (0..101).map(|_| " - type: table\n").collect();
assert!(matches!(Base::parse(&format!("views:\n{views}")), Err(Error::Limit(_))));
let formulas: String = (0..201).fold(String::new(), |acc, i| acc + " f" + &i.to_string() + ": x\n");
assert!(matches!(Base::parse(&format!("formulas:\n{formulas}")), Err(Error::Limit(_))));
let long = "x".repeat(4097);
assert!(matches!(Base::parse(&format!("formulas:\n f: {long}\n")), Err(Error::Limit(_))));
assert!(matches!(Base::parse(&format!("filters: {long}\n")), Err(Error::Limit(_))));
}
#[test]
fn invalid_values_are_errors() {
for bad in [
"views:\n - type: table\n limit: -1\n",
"views:\n - type: table\n limit: 2.5\n",
"views:\n - type: table\n limit: many\n",
"views:\n - type: table\n sort:\n - property: a\n direction: SIDEWAYS\n",
"views:\n - type: table\n groupBy:\n property: a\n direction: up\n",
"views:\n - name: no type\n",
"filters:\n xor:\n - a\n",
"views: 3\n",
"[1, 2]\n",
] {
assert!(matches!(Base::parse(bad), Err(Error::Invalid(_))), "{bad}");
}
assert!(matches!(Base::parse("a:\n\tb: 1\n"), Err(Error::Syntax { .. })));
}
#[test]
fn missing_views_gives_one_default_table_not_written_until_edited() {
let mut base = Base::parse("formulas:\n f: a\n").unwrap_or_else(|e| panic!("{e}"));
assert_eq!(base.views().len(), 1);
assert_eq!(base.views()[0].kind, ViewKind::Table);
assert!(!base.to_yaml().contains("views"));
base.views_mut()[0].limit = Some(3);
assert!(base.to_yaml().contains("views:"));
let empty = Base::parse("").unwrap_or_else(|e| panic!("{e}"));
assert_eq!(empty.views().len(), 1);
assert!(!empty.to_yaml().contains("views"));
}