use std::collections::BTreeMap;
use std::sync::Arc;
use serde_json::{Map, Value};
use super::super::field::{
Child, Entries, Field, FieldKind, FieldSet, Flattened, flatten_values, segment,
};
use super::fields::{entries_text, render_json};
use super::{PaneRow, PathStep};
#[derive(Clone, Default)]
pub(super) struct EntryRows {
shapes: Arc<BTreeMap<String, Flattened>>,
rows: Vec<EntryRow>,
roots: BTreeMap<usize, Vec<usize>>,
leaves: BTreeMap<String, usize>,
items: BTreeMap<String, Value>,
}
#[derive(Clone)]
pub(in crate::lookout) struct EntryRow {
pub(in crate::lookout) depth: usize,
pub(in crate::lookout) label: String,
pub(in crate::lookout) body: EntryBody,
pub(super) parent: PaneRow,
}
#[derive(Clone)]
pub(in crate::lookout) enum EntryBody {
Node {
path: String,
children: Vec<usize>,
entry: bool,
},
Leaf {
field: Box<Field>,
stored: Value,
at: Vec<PathStep>,
entry_len: usize,
entry: String,
text: String,
},
}
impl EntryRows {
pub(super) fn build(
fields: &FieldSet,
paths: &BTreeMap<String, Vec<String>>,
values: &Map<String, Value>,
shapes: BTreeMap<String, Flattened>,
) -> Self {
Self::built(fields, paths, values, Arc::new(shapes))
}
pub(super) fn rebuilt(
&self,
fields: &FieldSet,
paths: &BTreeMap<String, Vec<String>>,
values: &Map<String, Value>,
) -> Self {
Self::built(fields, paths, values, Arc::clone(&self.shapes))
}
fn built(
fields: &FieldSet,
paths: &BTreeMap<String, Vec<String>>,
values: &Map<String, Value>,
shapes: Arc<BTreeMap<String, Flattened>>,
) -> Self {
let mut built = Self {
shapes: Arc::clone(&shapes),
..Self::default()
};
for (index, field) in fields.fields().iter().enumerate() {
let FieldKind::Entries(kind) = field.kind else {
continue;
};
let (Some(shape), Some(path)) = (shapes.get(&field.key), paths.get(&field.key)) else {
continue;
};
let at = Where {
kind,
shape,
base: &field.key,
steps: path.iter().cloned().map(PathStep::Key).collect(),
depth: path.len(),
parent: PaneRow::Field(index),
};
let roots = built.push_entries(&at, values.get(&field.key));
built.roots.insert(index, roots);
}
built
}
pub(super) fn row(&self, index: usize) -> Option<&EntryRow> {
self.rows.get(index)
}
pub(super) fn roots(&self, field: usize) -> Option<&[usize]> {
self.roots.get(&field).map(Vec::as_slice)
}
pub(super) fn leaf(&self, key: &str) -> Option<EntryLeaf<'_>> {
match &self.rows.get(*self.leaves.get(key)?)?.body {
EntryBody::Leaf {
field,
stored,
at,
entry_len,
..
} => Some(EntryLeaf {
field,
stored,
at,
entry_len: *entry_len,
}),
EntryBody::Node { .. } => None,
}
}
pub(super) fn names_same_entry(&self, key: &str, next: &Self) -> bool {
let Some(EntryBody::Leaf { at, entry, .. }) = self.leaf_body(key) else {
return true;
};
let by_position = at.iter().any(|step| matches!(step, PathStep::Index(_)));
!by_position || self.items.get(entry) == next.items.get(entry)
}
pub(super) fn leaf_body(&self, key: &str) -> Option<&EntryBody> {
self.rows.get(*self.leaves.get(key)?).map(|row| &row.body)
}
pub(super) fn node_paths(&self) -> impl Iterator<Item = &str> {
self.rows.iter().filter_map(|row| match &row.body {
EntryBody::Node { path, .. } => Some(path.as_str()),
EntryBody::Leaf { .. } => None,
})
}
fn push_entries(&mut self, at: &Where<'_>, value: Option<&Value>) -> Vec<usize> {
let mut entries: Vec<(String, String, PathStep, &Value)> = Vec::new();
match (at.kind, value) {
(Entries::Array, Some(Value::Array(items))) => {
for (index, item) in items.iter().enumerate() {
let path = format!("{}[{index}]", at.base);
let label = array_label(at.shape, item, index);
entries.push((path, label, PathStep::Index(index), item));
}
}
(Entries::Map, Some(Value::Object(map))) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
let quoted = serde_json::to_string(key).unwrap_or_default();
let path = format!("{}[{quoted}]", at.base);
entries.push((path, plain(key), PathStep::Key(key.clone()), &map[key]));
}
}
_ => {}
}
let mut roots = Vec::new();
for (path, label, step, item) in entries {
let index = self.rows.len();
self.rows.push(EntryRow {
depth: at.depth,
label,
body: EntryBody::Node {
path: path.clone(),
children: Vec::new(),
entry: true,
},
parent: at.parent,
});
self.items.insert(path.clone(), item.clone());
let table = item.as_object().cloned().unwrap_or_default();
let values = flatten_values(&table, &at.shape.paths);
let mut steps = at.steps.clone();
steps.push(step);
let children = self.push_children(
at.shape,
&values,
&at.shape.outline.roots,
&Under {
depth: at.depth,
entry: &path,
steps: &steps,
parent: PaneRow::Entry(index),
},
);
self.set_children(index, children);
roots.push(index);
}
roots
}
fn push_children(
&mut self,
shape: &Flattened,
values: &Map<String, Value>,
children: &[Child],
under: &Under<'_>,
) -> Vec<usize> {
let mut pushed = Vec::new();
for child in children {
let index = self.rows.len();
match *child {
Child::Leaf(field) => {
let Some(field) = shape.fields.fields().get(field) else {
continue;
};
let Some(path) = shape.paths.get(&field.key) else {
continue;
};
let value = values.get(&field.key);
let counted = matches!(field.kind, FieldKind::Entries(_));
let key = format!("{}.{}", under.entry, field.key);
let mut at = under.steps.to_vec();
at.extend(path.iter().cloned().map(PathStep::Key));
let mut field = field.clone();
field.key.clone_from(&key);
self.leaves.insert(key, index);
self.rows.push(EntryRow {
depth: under.depth + path.len(),
label: path.last().map(|last| segment(last)).unwrap_or_default(),
body: EntryBody::Leaf {
text: if counted {
entries_text(value)
} else {
render_json(value.unwrap_or(&Value::Null))
},
stored: value.cloned().unwrap_or(Value::Null),
at,
entry_len: under.steps.len(),
entry: under.entry.to_owned(),
field: Box::new(field),
},
parent: under.parent,
});
}
Child::Table(table) => {
let Some(table) = shape.outline.tables.get(table) else {
continue;
};
let path = format!("{}.{}", under.entry, table.path);
self.rows.push(EntryRow {
depth: under.depth + 1 + table.depth,
label: table.key.clone(),
body: EntryBody::Node {
path: path.clone(),
children: Vec::new(),
entry: false,
},
parent: under.parent,
});
let below = self.push_children(
shape,
values,
&table.children,
&Under {
depth: under.depth,
entry: under.entry,
steps: under.steps,
parent: PaneRow::Entry(index),
},
);
self.set_children(index, below);
}
}
pushed.push(index);
}
pushed
}
fn set_children(&mut self, index: usize, below: Vec<usize>) {
if let Some(EntryRow {
body: EntryBody::Node { children, .. },
..
}) = self.rows.get_mut(index)
{
*children = below;
}
}
}
pub(super) struct EntryLeaf<'a> {
pub(super) field: &'a Field,
pub(super) stored: &'a Value,
pub(super) at: &'a [PathStep],
pub(super) entry_len: usize,
}
struct Where<'a> {
kind: Entries,
shape: &'a Flattened,
base: &'a str,
steps: Vec<PathStep>,
depth: usize,
parent: PaneRow,
}
struct Under<'a> {
depth: usize,
entry: &'a str,
steps: &'a [PathStep],
parent: PaneRow,
}
fn plain(text: &str) -> String {
text.chars()
.map(|c| if c.is_control() { ' ' } else { c })
.collect()
}
fn array_label(shape: &Flattened, item: &Value, index: usize) -> String {
["name", "id"]
.into_iter()
.find_map(|key| {
let field = shape.fields.by_key(key)?;
if field.secret || !matches!(field.kind, FieldKind::Text | FieldKind::Suggested(_)) {
return None;
}
item.get(key)?.as_str().filter(|text| !text.is_empty())
})
.map_or_else(|| format!("#{index}"), plain)
}
#[cfg(test)]
mod tests {
use super::super::fixtures::{entries_pane, entries_schema};
use super::super::{ConfigPane, PaneRow};
use super::EntryBody;
use serde_json::json;
#[test]
fn an_entries_row_reads_as_a_count() {
let section =
|text: &str| ConfigPane::dog("paddock".into(), None, entries_schema(), text.to_owned());
let one = section("[[clients]]\nkey = \"hunter2\"\n");
let none = section("clients = []\n");
let absent = section("retries = 3\n");
let table = |value: serde_json::Value| {
let table = json!({ "clients": value });
ConfigPane::sheep_dog(
"web".into(),
"paddock".into(),
&entries_schema(),
table.as_object().cloned().expect("a table"),
)
};
assert_eq!(entries_pane().value("clients"), "3 entries");
assert_eq!(one.value("clients"), "1 entry");
assert_eq!(none.value("clients"), "0 entries");
assert_eq!(absent.value("clients"), "(unset)");
assert_eq!(table(json!(null)).value("clients"), "(unset)");
assert_eq!(entries_pane().value("models"), "2 entries");
assert_eq!(entries_pane().display_value("clients"), "3 entries");
}
fn labels(pane: &mut ConfigPane, key: &str) -> Vec<String> {
pane.move_to_key(key);
pane.open_table();
pane.rows()
.into_iter()
.filter_map(|row| match row {
PaneRow::Entry(index) => pane.entry_row(index).filter(|entry| entry.depth == 1),
_ => None,
})
.map(|entry| entry.label.clone())
.collect()
}
#[test]
fn a_map_is_labelled_by_its_keys_and_an_array_by_name_then_id_then_its_index() {
assert_eq!(labels(&mut entries_pane(), "models"), ["large", "small"]);
assert_eq!(
labels(&mut entries_pane(), "clients"),
["alpha", "b-2", "#2"]
);
}
#[test]
fn a_secret_name_never_labels_an_entry() {
let schema = json!({
"properties": { "hosts": { "type": "array", "items": { "$ref": "#/$defs/Host" } } },
"$defs": { "Host": { "type": "object", "properties": {
"name": { "type": "string", "x-shep-secret": true },
} } },
});
let mut pane = ConfigPane::dog(
"paddock".into(),
None,
schema,
"[[hosts]]\nname = \"hunter2\"\n".to_owned(),
);
assert_eq!(labels(&mut pane, "hosts"), ["#0"]);
}
#[test]
fn a_secret_entries_field_masks_unmarked_leaves_and_never_labels_by_name() {
let schema = json!({
"properties": { "clients": {
"type": "array",
"items": { "$ref": "#/$defs/Client" },
"x-shep-secret": true,
} },
"$defs": { "Client": { "type": "object", "properties": {
"name": { "type": "string" },
"key": { "type": "string" },
} } },
});
let mut pane = ConfigPane::dog(
"paddock".into(),
None,
schema,
"[[clients]]\nname = \"alpha\"\nkey = \"hunter2\"\n".to_owned(),
);
assert_eq!(labels(&mut pane, "clients"), ["#0"]);
pane.move_by(1);
pane.open_table();
let masked: Vec<bool> = pane
.rows()
.into_iter()
.filter_map(|row| match row {
PaneRow::Entry(index) => pane.entry_row(index),
_ => None,
})
.filter_map(|row| match &row.body {
EntryBody::Leaf { field, .. } => Some(field.secret),
EntryBody::Node { .. } => None,
})
.collect();
assert_eq!(masked, [true, true]);
}
#[test]
fn an_entries_field_inside_an_entry_is_a_count_row_with_no_children() {
let schema = json!({
"properties": { "clients": { "type": "array", "items": { "$ref": "#/$defs/Client" } } },
"$defs": {
"Client": { "type": "object", "properties": {
"peers": { "type": "array", "items": { "$ref": "#/$defs/Peer" } },
} },
"Peer": { "type": "object", "properties": { "key": { "type": "string" } } },
},
});
let mut pane = ConfigPane::dog(
"paddock".into(),
None,
schema,
"[[clients]]\n[[clients.peers]]\nkey = \"a\"\n[[clients.peers]]\nkey = \"b\"\n"
.to_owned(),
);
pane.move_to_key("clients");
pane.open_table();
pane.move_by(1);
pane.open_table();
let rows = pane.rows();
assert_eq!(rows.len(), 3, "clients, #0 and peers: {rows:?}");
let Some(PaneRow::Entry(peers)) = rows.last().copied() else {
panic!("the last row is peers: {rows:?}");
};
let peers = pane.entry_row(peers).expect("the row exists");
assert!(
matches!(&peers.body, EntryBody::Leaf { text, .. } if text == "2 entries"),
"a count and not a node"
);
}
}