#[derive(Debug, Clone, PartialEq)]
pub struct FilterPred {
pub key: String,
pub value: serde_json::Value,
pub not_equal: bool,
}
#[derive(Debug, Clone, Default)]
pub struct OutputShape {
pub select: Option<Vec<String>>,
pub filters: Vec<FilterPred>,
pub limit: Option<u64>,
}
impl OutputShape {
#[inline]
pub fn is_identity(&self) -> bool {
self.select.is_none() && self.filters.is_empty() && self.limit.is_none()
}
pub fn from_cli(
select: Option<&str>,
filters: &[String],
limit: Option<u64>,
) -> Result<Self, crate::error::AtomwriteError> {
let select = match select.map(str::trim).filter(|s| !s.is_empty()) {
Some(csv) => {
let keys: Vec<String> = csv
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
if keys.is_empty() {
None
} else {
Some(keys)
}
}
None => None,
};
let mut preds = Vec::with_capacity(filters.len());
for f in filters {
preds.push(parse_filter_expr(f)?);
}
Ok(Self {
select,
filters: preds,
limit,
})
}
}
pub fn parse_filter_expr(raw: &str) -> Result<FilterPred, crate::error::AtomwriteError> {
let s = raw.trim();
if s.is_empty() {
return Err(crate::error::AtomwriteError::InvalidInput {
reason: "empty --filter expression".into(),
});
}
let (key, val_raw, not_equal) = if let Some(rest) = s.split_once("!=") {
(rest.0, rest.1, true)
} else if let Some(rest) = s.split_once("==") {
(rest.0, rest.1, false)
} else if let Some(rest) = s.split_once('=') {
(rest.0, rest.1, false)
} else {
return Err(crate::error::AtomwriteError::InvalidInput {
reason: format!(
"invalid --filter {raw:?}: expected key=value, key==value, or key!=value"
),
});
};
let key = key.trim();
if key.is_empty() {
return Err(crate::error::AtomwriteError::InvalidInput {
reason: format!("invalid --filter {raw:?}: empty key"),
});
}
let value = parse_filter_value(val_raw.trim());
Ok(FilterPred {
key: key.to_string(),
value,
not_equal,
})
}
fn parse_filter_value(raw: &str) -> serde_json::Value {
match raw {
"true" => serde_json::Value::Bool(true),
"false" => serde_json::Value::Bool(false),
"null" => serde_json::Value::Null,
s if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 => {
serde_json::Value::String(s[1..s.len() - 1].to_string())
}
s => {
if let Ok(n) = s.parse::<i64>() {
serde_json::Value::Number(n.into())
} else if let Ok(n) = s.parse::<u64>() {
serde_json::Value::Number(n.into())
} else if let Ok(n) = s.parse::<f64>() {
serde_json::Number::from_f64(n)
.map(serde_json::Value::Number)
.unwrap_or_else(|| serde_json::Value::String(s.to_string()))
} else {
serde_json::Value::String(s.to_string())
}
}
}
}
pub fn shape_value(
v: serde_json::Value,
shape: &OutputShape,
) -> Option<serde_json::Value> {
if !shape.filters.is_empty() {
let obj = v.as_object()?;
for pred in &shape.filters {
let actual = obj.get(&pred.key).cloned().unwrap_or(serde_json::Value::Null);
let eq = json_values_equal(&actual, &pred.value);
if pred.not_equal {
if eq {
return None;
}
} else if !eq {
return None;
}
}
}
Some(project_select(v, shape))
}
pub fn shape_error_value(v: serde_json::Value, shape: &OutputShape) -> serde_json::Value {
project_select(v, shape)
}
fn project_select(mut v: serde_json::Value, shape: &OutputShape) -> serde_json::Value {
if let Some(keys) = &shape.select {
let Some(obj) = v.as_object_mut() else {
return v;
};
let mut out = serde_json::Map::new();
for k in keys {
if let Some(val) = obj.remove(k) {
out.insert(k.clone(), val);
}
}
return serde_json::Value::Object(out);
}
v
}
fn json_values_equal(a: &serde_json::Value, b: &serde_json::Value) -> bool {
match (a, b) {
(serde_json::Value::Number(x), serde_json::Value::Number(y)) => {
if let (Some(xf), Some(yf)) = (x.as_f64(), y.as_f64()) {
(xf - yf).abs() < f64::EPSILON
} else {
x == y
}
}
_ => a == b,
}
}
#[cfg(test)]
mod output_shape_tests {
use super::*;
use serde_json::json;
#[test]
fn parse_eq_bool() {
let p = parse_filter_expr("ok=false").unwrap();
assert_eq!(p.key, "ok");
assert_eq!(p.value, json!(false));
assert!(!p.not_equal);
}
#[test]
fn parse_neq_string() {
let p = parse_filter_expr("code!=WORKSPACE_JAIL").unwrap();
assert!(p.not_equal);
assert_eq!(p.value, json!("WORKSPACE_JAIL"));
}
#[test]
fn project_select_omits_missing() {
let shape = OutputShape {
select: Some(vec!["ok".into(), "missing".into(), "code".into()]),
filters: vec![],
limit: None,
};
let out = shape_value(json!({"ok": false, "code": "X", "noise": 1}), &shape).unwrap();
assert_eq!(out, json!({"ok": false, "code": "X"}));
}
#[test]
fn filter_drops_non_match() {
let shape = OutputShape {
select: None,
filters: vec![parse_filter_expr("ok=false").unwrap()],
limit: None,
};
assert!(shape_value(json!({"ok": true}), &shape).is_none());
assert!(shape_value(json!({"ok": false}), &shape).is_some());
}
#[test]
fn identity_when_empty() {
assert!(OutputShape::default().is_identity());
}
#[test]
fn error_shape_ignores_filter_keeps_select() {
let shape = OutputShape {
select: Some(vec!["ok".into(), "code".into()]),
filters: vec![parse_filter_expr("ok=true").unwrap()],
limit: Some(0),
};
let err = json!({"ok": false, "error": true, "code": "WORKSPACE_JAIL", "noise": 1});
assert!(shape_value(err.clone(), &shape).is_none());
let out = shape_error_value(err, &shape);
assert_eq!(out, json!({"ok": false, "code": "WORKSPACE_JAIL"}));
}
}