prov_views/lib.rs
1//! # prov-views
2//!
3//! Declarative views over a [prov](https://docs.rs/prov) workspace: the format
4//! a workspace declares them in, and the traversal that executes one into a
5//! grouped set of rows.
6//!
7//! ## What a view is
8//!
9//! A prov workspace has a **spine** — the single-parent spanning relation that
10//! makes a directory of plain files discoverable by following its own links. A
11//! view is a *second* way through the same documents: "the entries under
12//! `Daily`, by month", "everything tagged, by tag". The same document can
13//! appear under several groups, which is exactly what the spine cannot do and
14//! why a view is worth having.
15//!
16//! ```yaml
17//! views:
18//! daily:
19//! label: Daily
20//! icon: calendar
21//! group: [date_of_document, created, updated]
22//! by: month
23//! under: '[Daily](/Daily/daily_index.md)'
24//! where:
25//! not: { has: draft }
26//! nest: month
27//! ```
28//!
29//! ## Nothing here knows what a date is
30//!
31//! This crate has no `date` grouping, no built-in field chain, and no calendar.
32//! `group:` is an ordered list of field keys and `by:` is a **coarsening** —
33//! `year`/`month`/`day` cut ISO-8601 text, `initial` cuts the first letters for
34//! an A–Z index, and both are the same kind of thing. So the three field names
35//! in the example above are a *declaration the workspace makes*, not a
36//! convention this crate blesses. A workspace that files by `taken_on` writes
37//! that instead, and every prov tool reading the same `views:` block agrees,
38//! rather than each one hardcoding a chain and hoping.
39//!
40//! The reasoning, and the MoReq2010 classification/aggregation split the format
41//! follows, are in [`spec`].
42//!
43//! ## What this crate does not do
44//!
45//! **It cannot write.** Its one dependency is `prov-graph`, the read core,
46//! whose filesystem port has no method that writes a byte — so a view engine is
47//! structurally unable to modify the workspace it reads, rather than merely
48//! intending not to.
49//!
50//! **It has no invariant.** prov's job is what must stay true — inverses
51//! paired, ids registered, links resolvable, fixity honest — and a view is not
52//! that: a wrong view shows the wrong rows and you edit the file. That is why
53//! this is a crate beside prov rather than a feature inside it, and why
54//! [`ViewSpec::nest`] is a *description* of where a frontend should file a new
55//! record rather than something this crate goes and does.
56//!
57//! **It does not render.** A [`RowSet`] is data. Which glyph `icon: calendar`
58//! draws, and what the [ungrouped](RowSet::ungrouped) bucket is called, are
59//! decisions for the frontend that has a screen.
60//!
61//! ## Two halves: select, then group
62//!
63//! [`select`](fn@select) answers *which documents does this view cover?* — scope, then
64//! conditions — and returns a flat, deduplicated [`Selection`] in path order.
65//! [`group`](fn@group) projects that into a [`RowSet`], and is a **pure function**: no
66//! I/O, no workspace, nothing to mock.
67//!
68//! The split is not tidiness. A [`Selection`] is the honest answer to "how many
69//! documents is this view about", which a grouped result cannot give — a
70//! document under two of a multi-valued field's groups is one document in two
71//! places. It also means one selection can be grouped several ways at once,
72//! which is what a frontend's view switcher does, and that every grouping
73//! question is testable without a filesystem.
74//!
75//! ```no_run
76//! use prov_graph::exec::block_on;
77//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
78//! # let graph: prov_graph::Graph<prov_graph::fs::StdFs, prov_graph::index::NoIndex> = todo!();
79//! # let spec: prov_views::ViewSpec = todo!();
80//! let selection = block_on(prov_views::select(&graph, &spec, "index.md"))?;
81//! println!("{} documents", selection.len());
82//!
83//! let rows = prov_views::group(&selection, &spec.group);
84//! for group in &rows.groups {
85//! println!("{} ({})", group.key, group.rows.len());
86//! }
87//! # Ok(())
88//! # }
89//! ```
90
91pub mod error;
92pub mod filter;
93pub mod group;
94pub mod lint;
95pub mod select;
96pub mod spec;
97
98pub use error::{Error, Result};
99pub use filter::{CONDITION_KEYS, Condition};
100pub use group::{Group, RowSet, group};
101pub use lint::{ViewIssue, ViewIssueKind, diagnose_view, diagnose_views};
102pub use select::{Row, Selection, select};
103pub use spec::{GRAINS, Grain, Grouping, VIEW_KEYS, VIEWS_KEY, ViewSpec, humanize, views_from};