use std::collections::{HashMap, HashSet};
use fig::Value;
pub use fig_schema::Seg;
use crate::tree::{VKind, key_to_string, preview, value_at};
pub const INLINE_MAX: usize = 6;
pub const PAGE_INLINE_MAX: usize = 20;
pub const FIT_MAX_DEPTH: usize = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InlineBudget {
pub rows: usize,
pub depth: usize,
pub page_rows: usize,
}
impl InlineBudget {
pub fn new(rows: usize, depth: usize) -> Self {
Self {
rows,
depth,
page_rows: rows.max(PAGE_INLINE_MAX),
}
}
pub fn fitting(root: &Value, room: usize) -> Self {
let (rows, depth) = subtree_shape(root);
if rows > 0 && rows <= room && depth <= FIT_MAX_DEPTH {
return Self {
rows,
depth,
page_rows: rows,
};
}
Self {
page_rows: room.max(INLINE_MAX),
..Self::default()
}
}
}
impl Default for InlineBudget {
fn default() -> Self {
Self::new(INLINE_MAX, 1)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ItemKind {
Scalar,
Drill { count: usize },
GroupHeader { count: usize },
}
#[derive(Clone, Debug)]
pub struct PageItem {
pub path: Vec<Seg>,
pub label: String,
pub vkind: VKind,
pub preview: String,
pub kind: ItemKind,
pub inset: usize,
pub title: Option<String>,
pub descend_to: Vec<Seg>,
pub demoted: bool,
pub summary: Option<String>,
}
impl PageItem {
pub fn is_drill(&self) -> bool {
matches!(self.kind, ItemKind::Drill { .. })
}
pub fn is_container(&self) -> bool {
matches!(
self.kind,
ItemKind::Drill { .. } | ItemKind::GroupHeader { .. }
)
}
pub fn is_scalar(&self) -> bool {
matches!(self.kind, ItemKind::Scalar)
}
pub fn can_rename(&self) -> bool {
matches!(self.path.last(), Some(Seg::Key(_)))
}
pub fn is_compressed(&self) -> bool {
self.descend_to.len() > self.path.len()
}
pub fn chain_labels(&self) -> Vec<String> {
std::iter::once(self.label.clone())
.chain(
self.descend_to[self.path.len().min(self.descend_to.len())..]
.iter()
.map(seg_label),
)
.collect()
}
}
#[derive(Clone, Debug, Default)]
pub struct Page {
pub focus: Vec<Seg>,
pub items: Vec<PageItem>,
pub title: Option<String>,
pub demoted: bool,
}
impl Page {
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn position_of(&self, path: &[Seg]) -> Option<usize> {
self.items
.iter()
.position(|i| i.path == path || i.descend_to == path)
}
pub fn has_drills(&self) -> bool {
self.items.iter().any(PageItem::is_drill)
}
pub fn has_choice(&self) -> bool {
self.items.len() >= 2
}
pub fn partitioned(&self) -> (Vec<&PageItem>, Vec<&PageItem>) {
self.items.iter().partition(|i| !i.demoted)
}
pub fn breadcrumb(&self, root_label: &str) -> String {
if self.focus.is_empty() {
return root_label.to_string();
}
let mut parts: Vec<String> = self.focus.iter().map(seg_label).collect();
if let (Some(title), Some(last)) = (&self.title, parts.last_mut()) {
*last = title.clone();
}
parts.join(" › ")
}
}
pub fn seg_label(seg: &Seg) -> String {
match seg {
Seg::Key(k) => k.clone(),
Seg::Index(i) => format!("[{i}]"),
}
}
pub fn is_container(v: &Value) -> bool {
matches!(v, Value::Map(_) | Value::Seq(_))
}
fn child_count(v: &Value) -> usize {
match v {
Value::Map(entries) => entries.len(),
Value::Seq(items) => items.len(),
_ => 0,
}
}
pub fn inlines(v: &Value, budget: InlineBudget) -> bool {
let (rows, depth) = subtree_shape(v);
rows > 0 && rows <= budget.rows && depth <= budget.depth
}
fn seq_inlines(items: &[Value], budget: InlineBudget) -> bool {
let mut rows = 0usize;
for item in items {
if is_container(item) {
if !inlines(item, budget) {
return false;
}
rows += 1 + subtree_shape(item).0;
} else {
rows += 1;
}
}
rows <= budget.page_rows
}
fn subtree_shape(v: &Value) -> (usize, usize) {
let children: Box<dyn Iterator<Item = &Value>> = match v {
Value::Map(entries) => Box::new(entries.iter().map(|(_, c)| c)),
Value::Seq(items) => Box::new(items.iter()),
_ => return (0, 0),
};
let (mut rows, mut depth) = (0, 0);
for child in children {
let (r, d) = subtree_shape(child);
rows += 1 + r;
depth = depth.max(d);
}
(rows, depth + 1)
}
const NAME_KEYS: [&str; 5] = ["name", "title", "id", "label", "key"];
pub fn title_keys(items: &[Value]) -> Vec<String> {
let mut order: Vec<String> = Vec::new();
let mut stats: HashMap<String, (usize, HashSet<String>)> = HashMap::new();
let mut mappings = 0usize;
for item in items {
let Value::Map(entries) = item else { continue };
mappings += 1;
for (k, v) in entries {
if is_container(v) {
continue;
}
let key = key_to_string(k);
let seen = stats.entry(key.clone()).or_insert_with(|| {
order.push(key);
(0, HashSet::new())
});
seen.0 += 1;
seen.1.insert(preview(v));
}
}
if mappings == 0 {
return Vec::new();
}
let mut ranked: Vec<(f64, usize, usize, &String)> = order
.iter()
.enumerate()
.map(|(doc_order, key)| {
let (present, values) = &stats[key];
let coverage = *present as f64 / mappings as f64;
let distinctness = values.len() as f64 / *present as f64;
let convention = NAME_KEYS.iter().position(|n| n.eq_ignore_ascii_case(key));
let score =
coverage + 1.5 * distinctness + if convention.is_some() { 2.0 } else { 0.0 };
(score, convention.unwrap_or(NAME_KEYS.len()), doc_order, key)
})
.collect();
ranked.sort_by(|a, b| {
b.0.total_cmp(&a.0)
.then_with(|| a.1.cmp(&b.1))
.then_with(|| a.2.cmp(&b.2))
});
ranked.into_iter().map(|(_, _, _, k)| k.clone()).collect()
}
pub fn title_of(ranking: &[String], item: &Value) -> Option<String> {
title_entry_of(ranking, item).map(|(_, title)| title)
}
pub fn title_entry_of<'r>(ranking: &'r [String], item: &Value) -> Option<(&'r str, String)> {
let Value::Map(entries) = item else {
return None;
};
ranking.iter().find_map(|want| {
entries.iter().find_map(|(k, v)| {
(!is_container(v) && key_to_string(k) == *want).then(|| (want.as_str(), preview(v)))
})
})
}
pub const SUMMARY_BUDGET: usize = 72;
pub fn flow(v: &Value, budget: usize) -> Option<String> {
let rendered = match v {
Value::Map(entries) => {
let parts = entries
.iter()
.map(|(k, val)| Some(format!("{}: {}", key_to_string(k), flow(val, budget)?)))
.collect::<Option<Vec<_>>>()?;
format!("{{{}}}", parts.join(", "))
}
Value::Seq(items) => {
let parts = items
.iter()
.map(|i| flow(i, budget))
.collect::<Option<Vec<_>>>()?;
format!("[{}]", parts.join(", "))
}
scalar => preview(scalar),
};
(rendered.chars().count() <= budget).then_some(rendered)
}
fn flow_without(v: &Value, budget: usize, omit: &str) -> Option<String> {
let Value::Map(entries) = v else {
return flow(v, budget);
};
let parts = entries
.iter()
.filter(|(k, _)| key_to_string(k) != omit)
.map(|(k, val)| Some(format!("{}: {}", key_to_string(k), flow(val, budget)?)))
.collect::<Option<Vec<_>>>()?;
if parts.is_empty() {
return None;
}
let rendered = format!("{{{}}}", parts.join(", "));
(rendered.chars().count() <= budget).then_some(rendered)
}
pub fn build_page(
root: &Value,
focus: &[Seg],
hidden_top_level: &HashSet<String>,
demoted_top_level: &HashSet<String>,
budget: InlineBudget,
) -> Page {
let mut page = Page {
focus: focus.to_vec(),
items: Vec::new(),
title: page_title(root, focus),
demoted: under_demoted_root(focus, demoted_top_level),
};
let Some(node) = value_at(root, focus) else {
return page;
};
let at_root = focus.is_empty();
let (uniform, ranking) = match node {
Value::Seq(items) => (Some(seq_inlines(items, budget)), title_keys(items)),
_ => (None, Vec::new()),
};
for (label, path, child) in children_of(node, focus) {
if at_root && hidden_top_level.contains(&label) {
continue;
}
let demoted = under_demoted_root(&path, demoted_top_level);
let title = title_of(&ranking, child);
if !is_container(child) {
page.items.push(item(
label,
path,
child,
ItemKind::Scalar,
0,
title,
demoted,
));
} else if uniform.unwrap_or(true) && inlines(child, budget) {
push_inline(&mut page.items, label, path, child, 0, title, demoted);
} else {
let (descend_to, deep) = compress(&path, child, budget);
let count = child_count(deep);
let mut row = item(
label,
path,
deep,
ItemKind::Drill { count },
0,
title,
demoted,
);
if let Some((key, _)) = title_entry_of(&ranking, child) {
row.summary = flow_without(child, SUMMARY_BUDGET, key);
}
row.descend_to = descend_to;
page.items.push(row);
}
}
page
}
fn push_inline(
items: &mut Vec<PageItem>,
label: String,
path: Vec<Seg>,
v: &Value,
inset: usize,
title: Option<String>,
demoted: bool,
) {
let count = child_count(v);
items.push(item(
label,
path.clone(),
v,
ItemKind::GroupHeader { count },
inset,
title,
demoted,
));
let ranking = match v {
Value::Seq(members) => title_keys(members),
_ => Vec::new(),
};
for (sub_label, sub_path, sub) in children_of(v, &path) {
let sub_title = title_of(&ranking, sub);
if is_container(sub) {
push_inline(
items,
sub_label,
sub_path,
sub,
inset + 1,
sub_title,
demoted,
);
} else {
items.push(item(
sub_label,
sub_path,
sub,
ItemKind::Scalar,
inset + 1,
sub_title,
demoted,
));
}
}
}
fn lone_drill_child(v: &Value, budget: InlineBudget) -> Option<(Seg, &Value)> {
let (seg, child) = match v {
Value::Map(entries) if entries.len() == 1 => {
let (k, c) = entries.iter().next()?;
(Seg::Key(key_to_string(k)), c)
}
Value::Seq(items) if items.len() == 1 => (Seg::Index(0), items.first()?),
_ => return None,
};
(is_container(child) && !inlines(child, budget)).then_some((seg, child))
}
fn compress<'v>(base: &[Seg], v: &'v Value, budget: InlineBudget) -> (Vec<Seg>, &'v Value) {
let mut descend_to = base.to_vec();
let mut deep = v;
while let Some((seg, next)) = lone_drill_child(deep, budget) {
descend_to.push(seg);
deep = next;
}
(descend_to, deep)
}
pub fn is_compressed_past(
root: &Value,
focus: &[Seg],
hidden: &HashSet<String>,
budget: InlineBudget,
) -> bool {
let page = build_page(root, focus, hidden, &HashSet::new(), budget);
page.items.len() == 1 && page.items[0].is_drill()
}
fn under_demoted_root(path: &[Seg], demoted_top_level: &HashSet<String>) -> bool {
matches!(path.first(), Some(Seg::Key(k)) if demoted_top_level.contains(k))
}
fn page_title(root: &Value, focus: &[Seg]) -> Option<String> {
let Some(Seg::Index(i)) = focus.last() else {
return None;
};
let Value::Seq(items) = value_at(root, &focus[..focus.len() - 1])? else {
return None;
};
title_of(&title_keys(items), items.get(*i)?)
}
fn item(
label: String,
path: Vec<Seg>,
v: &Value,
kind: ItemKind,
inset: usize,
title: Option<String>,
demoted: bool,
) -> PageItem {
let descend_to = path.clone();
PageItem {
path,
descend_to,
label,
vkind: VKind::of(v),
preview: preview(v),
kind,
inset,
title,
demoted,
summary: is_container(v).then(|| flow(v, SUMMARY_BUDGET)).flatten(),
}
}
fn children_of<'v>(node: &'v Value, base: &[Seg]) -> Vec<(String, Vec<Seg>, &'v Value)> {
let extend = |seg: Seg| {
let mut p = base.to_vec();
p.push(seg);
p
};
match node {
Value::Map(entries) => entries
.iter()
.map(|(k, v)| {
let key = key_to_string(k);
(key.clone(), extend(Seg::Key(key)), v)
})
.collect(),
Value::Seq(items) => items
.iter()
.enumerate()
.map(|(i, v)| (format!("[{i}]"), extend(Seg::Index(i)), v))
.collect(),
_ => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use fig::Format;
const SAMPLE: &str = "\
title = \"flower\"
version = 1
enabled = true
[server]
host = \"localhost\"
port = 8080
tags = [\"alpha\", \"beta\"]
[server.limits]
max_connections = 100
timeout = 30.5
";
fn value_of(src: &str, fmt: Format) -> Value {
fig::Document::parse(src.as_bytes(), fmt)
.expect("parse")
.to_value()
.expect("to_value")
}
fn sample() -> Value {
value_of(SAMPLE, Format::Toml)
}
fn page_of(root: &Value, focus: &[Seg]) -> Page {
build_page(
root,
focus,
&HashSet::new(),
&HashSet::new(),
InlineBudget::default(),
)
}
fn budgeted(root: &Value, focus: &[Seg], budget: InlineBudget) -> Page {
build_page(root, focus, &HashSet::new(), &HashSet::new(), budget)
}
fn demoting(root: &Value, focus: &[Seg], demoted: &[&str]) -> Page {
let set: HashSet<String> = demoted.iter().map(|s| s.to_string()).collect();
build_page(root, focus, &HashSet::new(), &set, InlineBudget::default())
}
fn key(k: &str) -> Seg {
Seg::Key(k.to_string())
}
fn shape(page: &Page) -> Vec<(String, usize, &'static str)> {
page.items
.iter()
.map(|i| {
let kind = match i.kind {
ItemKind::Scalar => "scalar",
ItemKind::Drill { .. } => "drill",
ItemKind::GroupHeader { .. } => "group",
};
(i.label.clone(), i.inset, kind)
})
.collect()
}
#[test]
fn the_root_page_lists_one_level_and_drills_the_rest() {
let root = sample();
assert_eq!(
shape(&page_of(&root, &[])),
vec![
("title".into(), 0, "scalar"),
("version".into(), 0, "scalar"),
("enabled".into(), 0, "scalar"),
("server".into(), 0, "drill"),
]
);
}
#[test]
fn small_all_scalar_containers_inline_into_the_page() {
let root = sample();
assert_eq!(
shape(&page_of(&root, &[key("server")])),
vec![
("host".into(), 0, "scalar"),
("port".into(), 0, "scalar"),
("tags".into(), 0, "group"),
("[0]".into(), 1, "scalar"),
("[1]".into(), 1, "scalar"),
("limits".into(), 0, "group"),
("max_connections".into(), 1, "scalar"),
("timeout".into(), 1, "scalar"),
]
);
}
#[test]
fn an_inlined_member_keeps_its_own_path() {
let root = sample();
let page = page_of(&root, &[key("server")]);
let timeout = page
.items
.iter()
.find(|i| i.label == "timeout")
.expect("timeout on server's page");
assert_eq!(
timeout.path,
vec![key("server"), key("limits"), key("timeout")]
);
}
#[test]
fn a_container_too_big_to_inline_drills() {
let mut src = String::from("[big]\n");
for i in 0..=INLINE_MAX {
src.push_str(&format!("k{i} = {i}\n"));
}
let root = value_of(&src, Format::Toml);
assert_eq!(
shape(&page_of(&root, &[])),
vec![("big".into(), 0, "drill")]
);
let trimmed = src
.rsplit_once('\n')
.unwrap()
.0
.rsplit_once('\n')
.unwrap()
.0;
let root = value_of(&format!("{trimmed}\n"), Format::Toml);
assert_eq!(page_of(&root, &[]).items[0].inset, 0);
assert!(matches!(
page_of(&root, &[]).items[0].kind,
ItemKind::GroupHeader { .. }
));
}
#[test]
fn a_container_holding_a_container_drills_however_small() {
let root = value_of("{\"a\": {\"b\": {\"c\": 1}}}", Format::Json);
assert_eq!(shape(&page_of(&root, &[])), vec![("a".into(), 0, "drill")]);
assert_eq!(
shape(&page_of(&root, &[key("a")])),
vec![("b".into(), 0, "group"), ("c".into(), 1, "scalar")]
);
}
fn list_of(n: usize) -> Value {
let items: Vec<String> = (0..n)
.map(|i| format!(r#"{{"name": "r{i}", "lang": "rust"}}"#))
.collect();
value_of(
&format!(r#"{{"repo": [{}]}}"#, items.join(", ")),
Format::Json,
)
}
#[test]
fn a_list_long_enough_to_bury_the_page_is_listed_rather_than_expanded() {
let root = list_of(8);
assert!(inlines(
&value_of(r#"{"name": "r0", "lang": "rust"}"#, Format::Json),
InlineBudget::default()
));
let page = page_of(&root, &[key("repo")]);
assert!(
page.items.iter().all(PageItem::is_drill),
"{:?}",
shape(&page)
);
assert_eq!(page.items.len(), 8);
}
#[test]
fn the_page_limit_is_asked_once_per_page_not_once_per_item() {
let root = list_of(8);
let roomy = InlineBudget {
page_rows: 24,
..InlineBudget::default()
};
let page = budgeted(&root, &[key("repo")], roomy);
assert_eq!(page.items.iter().filter(|i| i.inset == 0).count(), 8);
assert!(!page.has_drills(), "{:?}", shape(&page));
let tight = InlineBudget {
page_rows: 23,
..InlineBudget::default()
};
let page = budgeted(&root, &[key("repo")], tight);
assert!(page.items.iter().all(PageItem::is_drill));
}
#[test]
fn a_mapping_is_not_held_to_the_page_limit() {
let root = value_of(
r#"{"a": {"x": 1, "y": 2, "z": 3}, "b": {"x": 1, "y": 2, "z": 3}}"#,
Format::Json,
);
let page = budgeted(
&root,
&[],
InlineBudget {
page_rows: 4,
..InlineBudget::default()
},
);
assert!(!page.has_drills(), "{:?}", shape(&page));
}
#[test]
fn a_long_list_of_scalars_is_still_just_its_items() {
let items: Vec<String> = (0..40).map(|i| i.to_string()).collect();
let root = value_of(&format!("{{\"ns\": [{}]}}", items.join(", ")), Format::Json);
let page = page_of(&root, &[key("ns")]);
assert_eq!(page.items.len(), 40);
assert!(page.items.iter().all(PageItem::is_scalar));
}
#[test]
fn a_document_that_fits_the_room_needs_no_navigation_at_all() {
let root = sample();
let page = budgeted(&root, &[], InlineBudget::fitting(&root, 12));
assert_eq!(page.items.len(), 12);
assert!(!page.has_drills(), "{:?}", shape(&page));
}
#[test]
fn a_document_one_row_too_big_falls_back_to_the_founding_rule() {
let root = sample();
let budget = InlineBudget::fitting(&root, 11);
assert_eq!(budget.rows, INLINE_MAX);
assert_eq!(budget.depth, 1);
assert_eq!(budget.page_rows, 11);
assert!(budgeted(&root, &[], budget).has_drills());
}
#[test]
fn a_document_too_deep_to_read_drills_however_short_it_is() {
let root = value_of(r#"{"a": {"b": {"c": {"d": 1}}}}"#, Format::Json);
let budget = InlineBudget::fitting(&root, 100);
assert_eq!(budget.depth, 1);
assert!(budgeted(&root, &[], budget).has_drills());
}
#[test]
fn a_room_of_nothing_still_leaves_the_founding_rule_intact() {
let budget = InlineBudget::fitting(&sample(), 0);
assert_eq!(budget.page_rows, INLINE_MAX);
assert_eq!(
shape(&budgeted(&sample(), &[key("server")], budget)),
shape(&page_of(&sample(), &[key("server")]))
);
}
#[test]
fn raising_the_subtree_limit_raises_the_page_limit_with_it() {
assert_eq!(InlineBudget::new(99, 8).page_rows, 99);
assert_eq!(InlineBudget::new(2, 1).page_rows, PAGE_INLINE_MAX);
}
#[test]
fn a_page_of_one_row_is_a_label_rather_than_a_choice() {
let root = value_of(
r#"{"repo": [{"a": 1, "b": 2, "c": 3, "d": 4}]}"#,
Format::Json,
);
assert!(!page_of(&root, &[]).has_choice());
assert!(page_of(&root, &[key("repo"), Seg::Index(0)]).has_choice());
}
#[test]
fn a_deeper_budget_inlines_a_nested_container_rank_by_rank() {
let root = value_of("{\"a\": {\"b\": {\"c\": 1}}}", Format::Json);
let page = budgeted(&root, &[], InlineBudget::new(6, 2));
assert_eq!(
shape(&page),
vec![
("a".into(), 0, "group"),
("b".into(), 1, "group"),
("c".into(), 2, "scalar"),
]
);
let page = budgeted(&root, &[], InlineBudget::new(6, 1));
assert_eq!(shape(&page), vec![("a".into(), 0, "drill")]);
}
#[test]
fn the_row_limit_counts_the_whole_subtree_headers_included() {
let root = value_of(
r#"{"outer": {"g": {"x": 1, "y": 2}, "z": 3}}"#,
Format::Json,
);
let fits = InlineBudget::new(4, 2);
assert!(matches!(
budgeted(&root, &[], fits).items[0].kind,
ItemKind::GroupHeader { .. }
));
let short = InlineBudget::new(3, 2);
assert!(budgeted(&root, &[], short).items[0].is_drill());
}
#[test]
fn a_generous_budget_puts_the_whole_document_on_the_root_page() {
let root = sample();
let page = budgeted(&root, &[], InlineBudget::new(99, 8));
assert_eq!(
shape(&page),
vec![
("title".into(), 0, "scalar"),
("version".into(), 0, "scalar"),
("enabled".into(), 0, "scalar"),
("server".into(), 0, "group"),
("host".into(), 1, "scalar"),
("port".into(), 1, "scalar"),
("tags".into(), 1, "group"),
("[0]".into(), 2, "scalar"),
("[1]".into(), 2, "scalar"),
("limits".into(), 1, "group"),
("max_connections".into(), 2, "scalar"),
("timeout".into(), 2, "scalar"),
]
);
assert!(!page.has_drills(), "nothing left to navigate to");
}
#[test]
fn an_inlined_subtree_keeps_every_paths_own_address() {
let root = sample();
let page = budgeted(&root, &[], InlineBudget::new(99, 8));
let timeout = page
.items
.iter()
.find(|i| i.label == "timeout")
.expect("timeout inlined onto the root page");
assert_eq!(
timeout.path,
vec![key("server"), key("limits"), key("timeout")]
);
}
#[test]
fn a_budget_that_admits_a_chain_inlines_it_instead_of_compressing() {
let root = value_of(LONE, Format::Json);
let page = budgeted(&root, &[], InlineBudget::new(99, 8));
let exports = &page.items[0];
assert!(matches!(exports.kind, ItemKind::GroupHeader { .. }));
assert!(!exports.is_compressed());
}
#[test]
fn a_sequence_of_nested_mappings_inlines_uniformly_under_a_deep_budget() {
let root = value_of(STEPS, Format::Json);
let page = budgeted(&root, &[key("steps")], InlineBudget::new(20, 2));
let headers: Vec<_> = page
.items
.iter()
.filter(|i| i.inset == 0)
.map(|i| {
(
i.title.clone(),
matches!(i.kind, ItemKind::GroupHeader { .. }),
)
})
.collect();
assert_eq!(headers.len(), 4);
assert!(headers.iter().all(|(_, is_group)| *is_group));
assert_eq!(headers[0].0.as_deref(), Some("actions/checkout@v7"));
let with = page.items.iter().find(|i| i.label == "with").expect("with");
assert_eq!(with.inset, 1);
assert!(matches!(with.kind, ItemKind::GroupHeader { .. }));
}
#[test]
fn demotion_still_folds_a_deeply_inlined_subtree_in_one_run() {
let root = sample();
let set: HashSet<String> = ["server".to_string()].into();
let page = build_page(&root, &[], &HashSet::new(), &set, InlineBudget::new(99, 8));
let (primary, advanced) = page.partitioned();
assert_eq!(
primary.iter().map(|i| i.label.as_str()).collect::<Vec<_>>(),
["title", "version", "enabled"]
);
assert_eq!(advanced.len(), 9);
assert!(advanced.iter().all(|i| i.demoted));
}
#[test]
fn an_empty_container_drills_rather_than_inlining_as_a_headless_group() {
let root = value_of("{\"empty\": {}, \"none\": []}", Format::Json);
assert_eq!(
shape(&page_of(&root, &[])),
vec![("empty".into(), 0, "drill"), ("none".into(), 0, "drill")]
);
assert!(page_of(&root, &[key("empty")]).is_empty());
}
#[test]
fn hiding_is_scoped_to_the_root_page() {
let root = value_of(
"{\"id\": 1, \"inner\": {\"id\": 2, \"keep\": 3}}",
Format::Json,
);
let hidden = HashSet::from(["id".to_string()]);
let rooted = build_page(
&root,
&[],
&hidden,
&HashSet::new(),
InlineBudget::default(),
);
assert_eq!(
shape(&rooted),
vec![
("inner".into(), 0, "group"),
("id".into(), 1, "scalar"),
("keep".into(), 1, "scalar")
]
);
let inner = build_page(
&root,
&[key("inner")],
&hidden,
&HashSet::new(),
InlineBudget::default(),
);
assert_eq!(
shape(&inner),
vec![("id".into(), 0, "scalar"), ("keep".into(), 0, "scalar")]
);
}
#[test]
fn a_page_that_cannot_be_listed_is_empty_rather_than_a_panic() {
let root = sample();
assert!(page_of(&root, &[key("nope")]).is_empty());
assert!(page_of(&root, &[key("title")]).is_empty());
}
#[test]
fn breadcrumbs_name_the_lineage() {
let root = sample();
assert_eq!(page_of(&root, &[]).breadcrumb("‹document›"), "‹document›");
assert_eq!(
page_of(&root, &[key("server"), key("limits")]).breadcrumb("‹document›"),
"server › limits"
);
assert_eq!(
page_of(&root, &[key("server"), key("tags")]).breadcrumb("x"),
"server › tags"
);
}
#[test]
fn a_flat_document_has_nothing_to_drill_into() {
let flat = value_of("{\"a\": 1, \"b\": 2}", Format::Json);
assert!(!page_of(&flat, &[]).has_drills());
assert!(page_of(&sample(), &[]).has_drills());
}
const STEPS: &str = r#"{"steps": [
{"uses": "actions/checkout@v7"},
{"uses": "dtolnay/rust-toolchain@stable", "if": "always"},
{"uses": "Swatinem/rust-cache@v2", "if": "always", "with": {"key": "a"}},
{"run": "cargo xtask ci", "shell": "bash"}
]}"#;
fn titles(page: &Page) -> Vec<Option<String>> {
page.items.iter().map(|i| i.title.clone()).collect()
}
#[test]
fn a_sequence_item_is_titled_by_the_field_that_distinguishes_it() {
let root = value_of(STEPS, Format::Json);
let page = page_of(&root, &[key("steps")]);
assert_eq!(
titles(&page),
vec![
Some("actions/checkout@v7".into()),
Some("dtolnay/rust-toolchain@stable".into()),
Some("Swatinem/rust-cache@v2".into()),
Some("cargo xtask ci".into()),
]
);
}
#[test]
fn a_key_that_reads_the_same_on_every_item_loses_to_one_that_does_not() {
let root = value_of(STEPS, Format::Json);
let Value::Map(entries) = &root else {
unreachable!()
};
let Value::Seq(items) = &entries[0].1 else {
unreachable!()
};
let ranking = title_keys(items);
let uses = ranking.iter().position(|k| k == "uses").expect("uses");
let cond = ranking.iter().position(|k| k == "if").expect("if");
assert!(uses < cond, "{ranking:?}");
assert!(!ranking.iter().any(|k| k == "with"), "{ranking:?}");
}
#[test]
fn a_conventional_name_key_outranks_a_merely_distinct_one() {
let root = value_of(
r#"{"env": [
{"name": "HOME", "value": "/root"},
{"name": "PATH", "value": "/bin"}
]}"#,
Format::Json,
);
let page = page_of(&root, &[key("env")]);
assert_eq!(
page.items
.iter()
.filter(|i| i.inset == 0)
.map(|i| i.title.clone())
.collect::<Vec<_>>(),
vec![Some("HOME".into()), Some("PATH".into())]
);
}
#[test]
fn a_mapping_entry_is_never_titled() {
let root = sample();
assert!(page_of(&root, &[]).items.iter().all(|i| i.title.is_none()));
let tags = page_of(&root, &[key("server"), key("tags")]);
assert!(tags.items.iter().all(|i| i.title.is_none()));
}
#[test]
fn a_sequence_renders_its_items_uniformly() {
let root = value_of(STEPS, Format::Json);
let page = page_of(&root, &[key("steps")]);
assert!(
page.items
.iter()
.all(|i| matches!(i.kind, ItemKind::Drill { .. })),
"{:?}",
shape(&page)
);
let flat = value_of(
r#"{"steps": [{"run": "a"}, {"run": "b", "shell": "sh"}]}"#,
Format::Json,
);
let page = page_of(&flat, &[key("steps")]);
assert_eq!(
shape(&page),
vec![
("[0]".into(), 0, "group"),
("run".into(), 1, "scalar"),
("[1]".into(), 0, "group"),
("run".into(), 1, "scalar"),
("shell".into(), 1, "scalar"),
]
);
}
#[test]
fn a_titled_item_carries_its_title_into_its_own_breadcrumb() {
let root = value_of(STEPS, Format::Json);
let page = page_of(&root, &[key("steps"), Seg::Index(3)]);
assert_eq!(page.title.as_deref(), Some("cargo xtask ci"));
assert_eq!(page.breadcrumb("‹document›"), "steps › cargo xtask ci");
assert!(page.items.iter().all(|i| i.title.is_none()));
}
#[test]
fn a_multi_line_value_is_cut_to_its_first_line() {
let root = value_of(
"{\"steps\": [{\"run\": \"set -e\\ncargo test\\n\"}]}",
Format::Json,
);
let page = page_of(&root, &[key("steps")]);
assert_eq!(page.items[0].title.as_deref(), Some("set -e …"));
assert!(!page.items[0].preview.contains('\n'));
}
#[test]
fn a_container_that_fits_on_the_row_shows_its_contents_not_a_count() {
let root = value_of(
r#"{"on": {"push": {"branches": ["main"]}, "pull_request": null}}"#,
Format::Json,
);
let page = page_of(&root, &[key("on")]);
let push = &page.items[0];
assert!(matches!(push.kind, ItemKind::Drill { count: 1 }));
assert_eq!(push.summary.as_deref(), Some("{branches: [main]}"));
}
#[test]
fn a_container_too_long_to_summarise_falls_back_to_being_counted() {
let long = "x".repeat(SUMMARY_BUDGET);
let root = value_of(
&format!(r#"{{"outer": {{"a": {{"b": "{long}"}}}}}}"#),
Format::Json,
);
let page = page_of(&root, &[key("outer")]);
assert!(page.items[0].summary.is_none());
let root = value_of(r#"{"outer": {"a": {"b": "x"}}}"#, Format::Json);
assert_eq!(
page_of(&root, &[key("outer")]).items[0].summary.as_deref(),
Some("{b: x}")
);
}
#[test]
fn a_titled_row_does_not_spend_its_width_saying_its_title_twice() {
let root = list_of(8);
let page = page_of(&root, &[key("repo")]);
let first = &page.items[0];
assert_eq!(first.title.as_deref(), Some("r0"));
assert_eq!(first.summary.as_deref(), Some("{lang: rust}"));
assert!(matches!(first.kind, ItemKind::Drill { count: 2 }));
assert_eq!(
page_of(&root, &[key("repo"), Seg::Index(0)])
.items
.iter()
.map(|i| i.label.as_str())
.collect::<Vec<_>>(),
["name", "lang"]
);
}
#[test]
fn a_row_whose_only_field_is_its_title_falls_back_to_being_counted() {
let items: Vec<String> = (0..8).map(|i| format!(r#"{{"name": "r{i}"}}"#)).collect();
let root = value_of(
&format!(r#"{{"repo": [{}]}}"#, items.join(", ")),
Format::Json,
);
let page = budgeted(
&root,
&[key("repo")],
InlineBudget {
page_rows: 4,
..InlineBudget::default()
},
);
assert_eq!(page.items[0].title.as_deref(), Some("r0"));
assert!(page.items[0].summary.is_none());
assert!(matches!(page.items[0].kind, ItemKind::Drill { count: 1 }));
}
#[test]
fn compression_and_a_title_cannot_meet() {
let root = value_of(
r#"{"repo": [{"only": {"name": "inner", "lang": "rust", "a": 1,
"b": 2, "c": 3, "d": 4, "e": 5}}]}"#,
Format::Json,
);
let row = &page_of(&root, &[key("repo")]).items[0];
assert!(row.is_compressed(), "{:?}", row.chain_labels());
assert_eq!(row.chain_labels(), ["[0]", "only"]);
assert!(row.title.is_none());
assert!(
row.summary
.as_deref()
.is_some_and(|f| f.starts_with("{name: inner")),
"{:?}",
row.summary
);
}
#[test]
fn a_mapping_entry_summarises_whole_because_nothing_titled_it() {
let root = value_of(r#"{"a": {"name": "x", "lang": "rust"}}"#, Format::Json);
let page = budgeted(
&root,
&[],
InlineBudget {
rows: 1,
..InlineBudget::default()
},
);
assert_eq!(
page.items[0].summary.as_deref(),
Some("{name: x, lang: rust}")
);
}
#[test]
fn a_scalar_is_never_summarised() {
let root = sample();
let page = page_of(&root, &[]);
assert!(
page.items
.iter()
.filter(|i| matches!(i.kind, ItemKind::Scalar))
.all(|i| i.summary.is_none())
);
}
#[test]
fn a_group_header_is_not_a_drill() {
let root = sample();
let page = page_of(&root, &[key("server")]);
let limits = page
.items
.iter()
.find(|i| i.label == "limits")
.expect("limits");
assert!(matches!(limits.kind, ItemKind::GroupHeader { .. }));
assert!(limits.is_container());
assert!(!limits.is_drill());
assert!(!page.has_drills());
}
#[test]
fn a_demoted_key_is_marked_but_still_listed_in_document_order() {
let root = sample();
let page = demoting(&root, &[], &["version"]);
assert_eq!(
shape(&page)
.iter()
.map(|(l, _, _)| l.as_str())
.collect::<Vec<_>>(),
["title", "version", "enabled", "server"]
);
let demoted: Vec<&str> = page
.items
.iter()
.filter(|i| i.demoted)
.map(|i| i.label.as_str())
.collect();
assert_eq!(demoted, ["version"]);
}
#[test]
fn partitioning_folds_the_demoted_run_to_the_end_and_keeps_both_orders() {
let root = sample();
let page = demoting(&root, &[], &["title", "server"]);
let (primary, advanced) = page.partitioned();
assert_eq!(
primary.iter().map(|i| i.label.as_str()).collect::<Vec<_>>(),
["version", "enabled"]
);
assert_eq!(
advanced
.iter()
.map(|i| i.label.as_str())
.collect::<Vec<_>>(),
["title", "server"]
);
}
#[test]
fn demotion_covers_the_whole_subtree_so_drilling_in_stays_demoted() {
let root = sample();
let at_root = demoting(&root, &[], &["server"]);
assert!(
at_root
.items
.iter()
.find(|i| i.label == "server")
.unwrap()
.demoted
);
let inside = demoting(&root, &[key("server")], &["server"]);
assert!(inside.demoted);
assert!(inside.items.iter().all(|i| i.demoted));
assert!(inside.items.iter().any(|i| i.inset == 1));
let deeper = demoting(&root, &[key("server"), key("limits")], &["server"]);
assert!(deeper.demoted);
assert!(deeper.items.iter().all(|i| i.demoted));
}
#[test]
fn demotion_is_root_scoped_so_a_nested_key_of_the_same_name_is_untouched() {
let root = value_of(
r#"{"host": "managed", "server": {"host": "localhost", "port": 8080}}"#,
Format::Json,
);
let page = demoting(&root, &[], &["host"]);
assert!(
page.items
.iter()
.find(|i| i.label == "host")
.unwrap()
.demoted
);
assert!(
!page
.items
.iter()
.find(|i| i.label == "server")
.unwrap()
.demoted
);
let inside = demoting(&root, &[key("server")], &["host"]);
assert!(!inside.demoted);
assert!(inside.items.iter().all(|i| !i.demoted));
}
#[test]
fn a_group_header_and_its_inlined_members_never_land_on_opposite_sides() {
let root = sample();
let page = demoting(&root, &[key("server")], &["server"]);
let (primary, advanced) = page.partitioned();
assert!(primary.is_empty());
let labels: Vec<&str> = advanced.iter().map(|i| i.label.as_str()).collect();
let header = labels.iter().position(|l| *l == "limits").expect("limits");
assert_eq!(&labels[header..], ["limits", "max_connections", "timeout"]);
}
const LONE: &str = r#"{
"exports": {"journal": {"label": "Public Journal",
"gate": {"field": "audience", "value": "public"}}},
"diaryx": {"publish": {"audiences": [{"name": "public", "gates": []}]}},
"plain": {"a": 1, "b": 2}
}"#;
#[test]
fn a_row_whose_page_would_hold_only_it_names_the_chain_instead() {
let root = value_of(LONE, Format::Json);
let page = page_of(&root, &[]);
let exports = &page.items[0];
assert!(exports.is_compressed());
assert_eq!(exports.chain_labels(), ["exports", "journal"]);
assert!(matches!(exports.kind, ItemKind::Drill { count: 2 }));
assert_eq!(exports.descend_to, vec![key("exports"), key("journal")]);
assert_eq!(exports.path, vec![key("exports")]);
}
#[test]
fn compression_follows_the_chain_as_far_as_it_goes_including_a_lone_seq_item() {
let root = value_of(LONE, Format::Json);
let diaryx = &page_of(&root, &[])
.items
.iter()
.find(|i| i.label == "diaryx")
.expect("diaryx")
.clone();
assert_eq!(
diaryx.chain_labels(),
["diaryx", "publish", "audiences", "[0]"]
);
assert!(matches!(diaryx.kind, ItemKind::Drill { count: 2 }));
}
#[test]
fn a_page_with_something_to_say_is_never_compressed_past() {
let root = value_of(LONE, Format::Json);
let page = page_of(&root, &[]);
let plain = page.items.iter().find(|i| i.label == "plain").unwrap();
assert!(!plain.is_compressed());
assert_eq!(plain.chain_labels(), ["plain"]);
assert_eq!(plain.descend_to, plain.path);
let root = value_of(r#"{"outer": {"only": 1}}"#, Format::Json);
let outer = &page_of(&root, &[]).items[0];
assert!(!outer.is_compressed());
}
#[test]
fn an_uncompressed_row_descends_to_where_it_already_points() {
let root = sample();
for focus in [vec![], vec![key("server")]] {
for item in &page_of(&root, &focus).items {
assert_eq!(item.descend_to, item.path, "{}", item.label);
assert_eq!(item.chain_labels(), std::slice::from_ref(&item.label));
}
}
}
#[test]
fn only_a_mapping_entry_can_be_renamed() {
let root = sample();
let page = page_of(&root, &[key("server"), key("tags")]);
assert!(page.items.iter().all(|i| !i.can_rename()));
assert!(page_of(&root, &[]).items.iter().all(|i| i.can_rename()));
}
}