use crate::error::{AppError, AppResult};
use serde::Serialize;
use serde_json::{Map, Value};
const RESULT_ARRAY_KEYS: [&str; 6] = ["results", "items", "rows", "matches", "data", "entries"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FilterOp {
Equals,
NotEquals,
Contains,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Filter {
pub path: String,
pub op: FilterOp,
pub value: String,
}
impl Filter {
pub fn parse(raw: &str) -> AppResult<Self> {
let (path, op, value) = if let Some((k, v)) = raw.split_once("!=") {
(k, FilterOp::NotEquals, v)
} else if let Some((k, v)) = raw.split_once("==") {
(k, FilterOp::Equals, v)
} else if let Some((k, v)) = raw.split_once('~') {
(k, FilterOp::Contains, v)
} else if let Some((k, v)) = raw.split_once('=') {
(k, FilterOp::Equals, v)
} else {
return Err(AppError::InvalidUsage(format!(
"--filter {raw:?}: expected key=value, key!=value or key~substring"
)));
};
let path = path.trim();
if path.is_empty() {
return Err(AppError::InvalidUsage(format!(
"--filter {raw:?}: the key on the left of the operator is empty"
)));
}
Ok(Self {
path: path.to_string(),
op,
value: value.to_string(),
})
}
#[must_use]
pub fn matches(&self, element: &Value) -> bool {
let Some(found) = lookup_path(element, &self.path) else {
return matches!(self.op, FilterOp::NotEquals);
};
let rendered = render_scalar(found);
match self.op {
FilterOp::Equals => rendered == self.value,
FilterOp::NotEquals => rendered != self.value,
FilterOp::Contains => rendered.contains(&self.value),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct SurfaceReport {
pub input_count: usize,
pub output_count: usize,
pub limited: bool,
pub content_truncated: bool,
pub output_truncated: bool,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct SurfaceOptions {
pub select: Vec<String>,
pub filters: Vec<Filter>,
pub limit: Option<usize>,
pub sort: Option<String>,
pub dedupe_by: Option<String>,
pub count_only: bool,
pub truncate_content: Option<usize>,
pub max_output_bytes: Option<usize>,
}
impl SurfaceOptions {
#[must_use]
pub fn is_active(&self) -> bool {
!self.select.is_empty()
|| !self.filters.is_empty()
|| self.limit.is_some()
|| self.sort.is_some()
|| self.dedupe_by.is_some()
|| self.count_only
|| self.truncate_content.is_some()
|| self.max_output_bytes.is_some()
}
#[must_use]
pub fn apply(&self, envelope: Value) -> (Value, SurfaceReport) {
if is_error_envelope(&envelope) {
return (envelope, SurfaceReport::default());
}
match locate_rows(&envelope) {
Some(key) => self.apply_to_rows(envelope, &key),
None => self.apply_to_single(envelope),
}
}
fn apply_to_rows(&self, mut envelope: Value, key: &str) -> (Value, SurfaceReport) {
let mut report = SurfaceReport::default();
let rows = envelope
.get_mut(key)
.and_then(Value::as_array_mut)
.map(std::mem::take)
.unwrap_or_default();
report.input_count = rows.len();
let rows = self.reduce_rows(rows, &mut report);
report.output_count = rows.len();
if self.count_only {
let mut out = json_count(report.output_count);
attach_report(&mut out, report);
return (out, report);
}
if let Some(slot) = envelope.get_mut(key) {
*slot = Value::Array(rows);
}
if let Some(max) = self.truncate_content {
report.content_truncated = truncate_strings(&mut envelope, max);
}
attach_report(&mut envelope, report);
if let Some(cap) = self.max_output_bytes {
let dropped = enforce_byte_cap(&mut envelope, key, cap, &mut report);
if dropped > 0 {
report.output_truncated = true;
report.limited = true;
report.output_count = report.output_count.saturating_sub(dropped);
attach_report(&mut envelope, report);
}
}
(envelope, report)
}
fn apply_to_single(&self, envelope: Value) -> (Value, SurfaceReport) {
let mut report = SurfaceReport {
input_count: 1,
..SurfaceReport::default()
};
let mut rows = self.reduce_rows(vec![envelope], &mut report);
report.output_count = rows.len();
if self.count_only {
let mut out = json_count(report.output_count);
attach_report(&mut out, report);
return (out, report);
}
let mut out = rows.pop().unwrap_or_else(|| Value::Object(Map::new()));
if let Some(max) = self.truncate_content {
report.content_truncated = truncate_strings(&mut out, max);
}
if let Some(cap) = self.max_output_bytes {
if serialised_len(&out) > cap {
report.output_truncated = true;
}
}
attach_report(&mut out, report);
(out, report)
}
fn reduce_rows(&self, rows: Vec<Value>, report: &mut SurfaceReport) -> Vec<Value> {
let mut rows: Vec<Value> = rows
.into_iter()
.filter(|row| self.filters.iter().all(|f| f.matches(row)))
.collect();
if let Some(path) = &self.sort {
rows.sort_by(|a, b| compare_by_path(a, b, path));
}
if let Some(path) = &self.dedupe_by {
let mut seen: Vec<String> = Vec::new();
rows.retain(|row| match lookup_path(row, path) {
None => true,
Some(v) => {
let key = render_scalar(v);
if seen.contains(&key) {
false
} else {
seen.push(key);
true
}
}
});
}
if let Some(limit) = self.limit {
if rows.len() > limit {
rows.truncate(limit);
report.limited = true;
}
}
if !self.select.is_empty() {
rows = rows.iter().map(|row| project(row, &self.select)).collect();
}
rows
}
}
fn is_error_envelope(value: &Value) -> bool {
value.get("error") == Some(&Value::Bool(true)) || value.get("ok") == Some(&Value::Bool(false))
}
fn locate_rows(envelope: &Value) -> Option<String> {
let obj = envelope.as_object()?;
RESULT_ARRAY_KEYS
.iter()
.find(|key| obj.get(**key).is_some_and(Value::is_array))
.map(|key| (*key).to_string())
}
fn json_count(n: usize) -> Value {
let mut map = Map::new();
map.insert("count".to_string(), Value::from(n));
Value::Object(map)
}
fn attach_report(envelope: &mut Value, report: SurfaceReport) {
if let Some(obj) = envelope.as_object_mut() {
if let Ok(v) = serde_json::to_value(report) {
obj.insert("agent_surface".to_string(), v);
}
}
}
#[must_use]
pub fn lookup_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
let mut cursor = value;
for segment in path.split('.') {
cursor = match cursor {
Value::Object(map) => map.get(segment)?,
Value::Array(items) => items.get(segment.parse::<usize>().ok()?)?,
_ => return None,
};
}
Some(cursor)
}
fn render_scalar(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
Value::Null => String::new(),
other => other.to_string(),
}
}
fn project(element: &Value, paths: &[String]) -> Value {
let mut out = Map::new();
for path in paths {
if let Some(found) = lookup_path(element, path) {
insert_path(&mut out, path, found.clone());
}
}
Value::Object(out)
}
fn insert_path(root: &mut Map<String, Value>, path: &str, value: Value) {
let mut segments = path.split('.').peekable();
let mut cursor = root;
while let Some(segment) = segments.next() {
if segments.peek().is_none() {
cursor.insert(segment.to_string(), value);
return;
}
let entry = cursor
.entry(segment.to_string())
.or_insert_with(|| Value::Object(Map::new()));
if !entry.is_object() {
*entry = Value::Object(Map::new());
}
match entry.as_object_mut() {
Some(map) => cursor = map,
None => return,
}
}
}
fn compare_by_path(a: &Value, b: &Value, path: &str) -> std::cmp::Ordering {
use std::cmp::Ordering;
match (lookup_path(a, path), lookup_path(b, path)) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
(Some(x), Some(y)) => match (x.as_f64(), y.as_f64()) {
(Some(nx), Some(ny)) => nx.partial_cmp(&ny).unwrap_or(Ordering::Equal),
_ => render_scalar(x).cmp(&render_scalar(y)),
},
}
}
fn truncate_strings(value: &mut Value, max: usize) -> bool {
match value {
Value::String(s) => {
if s.chars().count() > max {
let cut: String = s.chars().take(max).collect();
*s = cut;
true
} else {
false
}
}
Value::Array(items) => items.iter_mut().fold(false, |acc, item| {
let hit = truncate_strings(item, max);
acc || hit
}),
Value::Object(map) => map.iter_mut().fold(false, |acc, (_, item)| {
let hit = truncate_strings(item, max);
acc || hit
}),
_ => false,
}
}
fn serialised_len(value: &Value) -> usize {
serde_json::to_vec(value).map_or(usize::MAX, |v| v.len())
}
fn enforce_byte_cap(
envelope: &mut Value,
key: &str,
cap: usize,
report: &mut SurfaceReport,
) -> usize {
let mut dropped = 0usize;
while serialised_len(envelope) > cap {
let Some(rows) = envelope.get_mut(key).and_then(Value::as_array_mut) else {
break;
};
if rows.pop().is_none() {
break;
}
dropped += 1;
let mut projected = *report;
projected.output_truncated = true;
projected.limited = true;
projected.output_count = projected.output_count.saturating_sub(dropped);
attach_report(envelope, projected);
}
dropped
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn rows(n: usize) -> Value {
let items: Vec<Value> = (0..n)
.map(|i| json!({ "id": i, "name": format!("n{i}") }))
.collect();
json!({ "results": items })
}
#[test]
fn inactive_options_are_a_no_op() {
let opts = SurfaceOptions::default();
assert!(!opts.is_active());
}
#[test]
fn filter_parses_every_operator() {
assert_eq!(Filter::parse("a=b").expect("parses").op, FilterOp::Equals);
assert_eq!(Filter::parse("a==b").expect("parses").op, FilterOp::Equals);
assert_eq!(
Filter::parse("a!=b").expect("parses").op,
FilterOp::NotEquals
);
assert_eq!(Filter::parse("a~b").expect("parses").op, FilterOp::Contains);
}
#[test]
fn malformed_filter_is_a_usage_error_not_an_empty_set() {
let err = Filter::parse("no_operator_here").unwrap_err();
assert!(matches!(err, AppError::InvalidUsage(_)));
assert!(Filter::parse("=missing_key").is_err());
}
#[test]
fn filter_conjoins_with_and() {
let opts = SurfaceOptions {
filters: vec![
Filter::parse("id!=0").expect("parses"),
Filter::parse("name~n").expect("parses"),
],
..SurfaceOptions::default()
};
let (out, report) = opts.apply(rows(3));
assert_eq!(report.input_count, 3);
assert_eq!(report.output_count, 2);
assert_eq!(out["results"].as_array().map(Vec::len), Some(2));
}
#[test]
fn error_envelope_is_never_silenced_by_a_filter() {
let opts = SurfaceOptions {
filters: vec![Filter::parse("id=999").expect("parses")],
..SurfaceOptions::default()
};
let envelope = json!({ "error": true, "code": 70, "message": "boom" });
let (out, _) = opts.apply(envelope.clone());
assert_eq!(out, envelope, "an error envelope must pass through intact");
}
#[test]
fn ok_false_envelope_is_never_silenced() {
let opts = SurfaceOptions {
count_only: true,
..SurfaceOptions::default()
};
let envelope = json!({ "ok": false, "message": "boom" });
let (out, _) = opts.apply(envelope.clone());
assert_eq!(out, envelope);
}
#[test]
fn select_skips_absent_keys_instead_of_emitting_null() {
let opts = SurfaceOptions {
select: vec!["id".to_string(), "absent".to_string()],
..SurfaceOptions::default()
};
let (out, _) = opts.apply(rows(1));
let first = &out["results"][0];
assert!(first.get("id").is_some());
assert!(
first.get("absent").is_none(),
"absent key must be skipped, not null: {first}"
);
}
#[test]
fn select_supports_dotted_paths() {
let opts = SurfaceOptions {
select: vec!["info.provider".to_string()],
..SurfaceOptions::default()
};
let envelope = json!({ "results": [{ "info": { "provider": "cache", "x": 1 } }] });
let (out, _) = opts.apply(envelope);
assert_eq!(out["results"][0]["info"]["provider"], json!("cache"));
assert!(out["results"][0]["info"].get("x").is_none());
}
#[test]
fn sort_is_ascending_numeric_and_puts_absent_keys_last() {
let opts = SurfaceOptions {
sort: Some("id".to_string()),
..SurfaceOptions::default()
};
let envelope = json!({ "results": [{"id": 10}, {"other": 1}, {"id": 2}] });
let (out, _) = opts.apply(envelope);
let arr = out["results"].as_array().cloned().unwrap_or_default();
assert_eq!(arr[0]["id"], json!(2));
assert_eq!(arr[1]["id"], json!(10));
assert!(arr[2].get("id").is_none());
}
#[test]
fn dedupe_keeps_first_and_always_keeps_elements_without_the_key() {
let opts = SurfaceOptions {
dedupe_by: Some("id".to_string()),
..SurfaceOptions::default()
};
let envelope =
json!({ "results": [{"id":1,"t":"a"},{"id":1,"t":"b"},{"t":"c"},{"t":"d"}] });
let (out, report) = opts.apply(envelope);
assert_eq!(report.output_count, 3);
assert_eq!(out["results"][0]["t"], json!("a"));
}
#[test]
fn limit_reports_that_it_cut() {
let opts = SurfaceOptions {
limit: Some(2),
..SurfaceOptions::default()
};
let (_, report) = opts.apply(rows(5));
assert_eq!(report.output_count, 2);
assert!(report.limited);
}
#[test]
fn count_only_counts_after_filter_dedupe_and_limit() {
let opts = SurfaceOptions {
filters: vec![Filter::parse("id!=0").expect("parses")],
limit: Some(2),
count_only: true,
..SurfaceOptions::default()
};
let (out, _) = opts.apply(rows(10));
assert_eq!(out["count"], json!(2));
assert!(out.get("results").is_none());
}
#[test]
fn truncate_content_counts_characters_and_never_splits_utf8() {
let opts = SurfaceOptions {
truncate_content: Some(3),
..SurfaceOptions::default()
};
let envelope = json!({ "results": [{ "t": "áéíóú" }] });
let (out, report) = opts.apply(envelope);
assert!(report.content_truncated);
let s = out["results"][0]["t"].as_str().unwrap_or_default();
assert_eq!(s.chars().count(), 3);
assert_eq!(s, "áéí");
}
#[test]
fn max_output_bytes_drops_whole_elements_and_leaves_valid_json() {
const CAP: usize = 400;
let opts = SurfaceOptions {
max_output_bytes: Some(CAP),
..SurfaceOptions::default()
};
let (out, report) = opts.apply(rows(40));
assert!(report.output_truncated);
let text = serde_json::to_string(&out).expect("still serialises");
assert!(
text.len() <= CAP,
"the cap must bind the whole envelope, agent_surface included, got {}",
text.len()
);
serde_json::from_str::<Value>(&text).expect("the emitted text must still parse");
let kept = out["results"].as_array().map(Vec::len).unwrap_or_default();
assert!(kept > 0 && kept < 40, "some rows must survive, got {kept}");
assert_eq!(report.output_count, kept);
}
#[test]
fn a_cap_below_the_skeleton_empties_the_rows_and_still_parses() {
let opts = SurfaceOptions {
max_output_bytes: Some(1),
..SurfaceOptions::default()
};
let (out, report) = opts.apply(rows(5));
assert!(report.output_truncated);
assert_eq!(report.output_count, 0);
assert_eq!(out["results"].as_array().map(Vec::len), Some(0));
let text = serde_json::to_string(&out).expect("still serialises");
serde_json::from_str::<Value>(&text).expect("the emitted text must still parse");
}
#[test]
fn agent_surface_block_is_attached_when_active() {
let opts = SurfaceOptions {
limit: Some(1),
..SurfaceOptions::default()
};
let (out, _) = opts.apply(rows(3));
let block = &out["agent_surface"];
assert_eq!(block["input_count"], json!(3));
assert_eq!(block["output_count"], json!(1));
assert_eq!(block["limited"], json!(true));
}
#[test]
fn single_object_envelope_is_projected_as_one_element() {
let opts = SurfaceOptions {
select: vec!["video_id".to_string()],
..SurfaceOptions::default()
};
let envelope = json!({ "video_id": "abc", "content": "long body" });
let (out, report) = opts.apply(envelope);
assert_eq!(report.input_count, 1);
assert_eq!(report.output_count, 1);
assert_eq!(out["video_id"], json!("abc"));
assert!(out.get("content").is_none());
}
#[test]
fn lookup_path_indexes_arrays_by_number() {
let v = json!({ "a": [{ "b": 7 }] });
assert_eq!(lookup_path(&v, "a.0.b"), Some(&json!(7)));
assert_eq!(lookup_path(&v, "a.9.b"), None);
}
}