use std::collections::HashMap;
use std::sync::Arc;
use crate::adapter::WrappingDispenser;
use crate::adapter::{ExecutionError, OpDispenser, OpResult};
use crate::wrapper_registry::{WrapperName, WrapperRegistration, WrapperSubject};
use nmbrs_workload::bindpoints;
pub const NAME: WrapperName = WrapperName::new("traverse");
fn triggers(s: WrapperSubject) -> bool {
s.op().is_some()
}
fn describe_assignment(_: WrapperSubject) -> Option<String> {
None
}
inventory::submit! {
WrapperRegistration {
name: NAME,
owned_fields: &[],
triggers,
requires_inner: &[],
forbids_outer: &[],
mutually_exclusive_with: &[],
describe_assignment,
levels: &[crate::wrapper_registry::WrapperLevel::Op],
}
}
pub struct TraversalStats {
pub metrics: Arc<crate::activity::ActivityMetrics>,
}
pub struct TraversingDispenser {
inner: Arc<dyn OpDispenser>,
stats: Arc<TraversalStats>,
captures: Vec<bindpoints::CapturePoint>,
spec: Option<nmbrs_workload::model::TraverseSpec>,
}
impl TraversingDispenser {
pub fn wrap(
inner: Arc<dyn OpDispenser>,
template: &nmbrs_workload::model::ParsedOp,
stats: Arc<TraversalStats>,
) -> Arc<dyn OpDispenser> {
Arc::new(Self {
inner,
stats,
captures: template.captures.clone(),
spec: template.traverse.clone(),
})
}
}
#[cfg(test)]
fn extract_captures_from_json(
body: &dyn crate::adapter::ResultBody,
specs: &[bindpoints::CapturePoint],
) -> HashMap<String, polydat::ast::Value> {
extract_captures_rooted(body, specs, None)
}
fn extract_captures_rooted(
body: &dyn crate::adapter::ResultBody,
specs: &[bindpoints::CapturePoint],
base: Option<&str>,
) -> HashMap<String, polydat::ast::Value> {
if specs.is_empty() {
return HashMap::new();
}
let full = body.to_json();
let json = match base.filter(|b| !b.is_empty()) {
None => full,
Some(b) => match full.pointer(b) {
Some(sub) => sub.clone(),
None => return HashMap::new(),
},
};
let mut captures = HashMap::new();
for spec in specs {
if let Some(path) = spec.path.as_deref() {
let sub = json.pointer(path);
let value = if spec.count {
polydat::ast::Value::U64(count_of_subtree(sub))
} else if let Some(agg) = &spec.agg {
aggregate_rows(sub, agg, spec.row_filter.as_ref())
} else {
match sub {
Some(v) => json_subtree_to_value(v),
None => polydat::ast::Value::None,
}
};
captures.insert(spec.as_name.clone(), value);
continue;
}
if spec.slurp {
let collected = slurp_column(&json, &spec.source_name);
captures.insert(
spec.as_name.clone(),
polydat::ast::Value::Json(std::sync::Arc::new(serde_json::Value::Array(collected))),
);
continue;
}
if spec.source_name == "*" {
let target = match &json {
serde_json::Value::Array(rows) => {
rows.first().cloned().unwrap_or(serde_json::Value::Null)
}
other => other.clone(),
};
if let serde_json::Value::Object(map) = target {
for (k, v) in map {
captures.insert(k, json_to_value(&v));
}
}
continue;
}
if let Some(val) = first_row_field(&json, &spec.source_name) {
captures.insert(spec.as_name.clone(), json_to_value(&val));
}
}
captures
}
fn count_of_subtree(v: Option<&serde_json::Value>) -> u64 {
let Some(v) = v else { return 0 };
match v {
serde_json::Value::Array(a) => a.len() as u64,
serde_json::Value::Object(m) => m.len() as u64,
serde_json::Value::Number(n) => n
.as_u64()
.or_else(|| n.as_i64().map(|i| i.max(0) as u64))
.or_else(|| n.as_f64().map(|f| f.max(0.0) as u64))
.unwrap_or(0),
serde_json::Value::Bool(b) => {
if *b {
1
} else {
0
}
}
serde_json::Value::String(s) if s.is_empty() => 0,
serde_json::Value::String(_) => 1,
serde_json::Value::Null => 0,
}
}
fn captures_for_empty_result(
specs: &[bindpoints::CapturePoint],
) -> std::collections::HashMap<String, polydat::ast::Value> {
use bindpoints::CaptureAgg;
let mut out = std::collections::HashMap::new();
for spec in specs {
let value = if spec.count {
polydat::ast::Value::U64(0)
} else {
match &spec.agg {
Some(CaptureAgg::Sum(_)) => polydat::ast::Value::U64(0),
Some(CaptureAgg::Min(_)) | Some(CaptureAgg::Max(_)) => polydat::ast::Value::None,
None => polydat::ast::Value::None,
}
};
out.insert(spec.as_name.clone(), value);
}
out
}
fn publish_capture(ctx: &crate::adapter::ExecCtx<'_>, name: &str, value: polydat::ast::Value) {
if matches!(value, polydat::ast::Value::None) {
let _ = ctx.wires.reset(name);
} else {
let _ = ctx.wires.write(name, value);
}
}
fn aggregate_rows(
sub: Option<&serde_json::Value>,
agg: &bindpoints::CaptureAgg,
row_filter: Option<&(String, String)>,
) -> polydat::ast::Value {
use bindpoints::CaptureAgg;
let rows = match sub {
Some(serde_json::Value::Array(rows)) => rows,
_ => return polydat::ast::Value::None,
};
let field = match agg {
CaptureAgg::Min(f) | CaptureAgg::Max(f) | CaptureAgg::Sum(f) => f.as_str(),
};
let keep = |row: &serde_json::Value| -> bool {
let Some((k, want)) = row_filter else {
return true;
};
match row.get(k.as_str()) {
Some(serde_json::Value::String(s)) => s == want,
Some(other) => other.to_string().trim_matches('"') == want,
None => false,
}
};
let nums: Vec<&serde_json::Number> = rows
.iter()
.filter(|row| keep(row))
.filter_map(|row| row.get(field))
.filter_map(|v| v.as_number())
.collect();
if nums.is_empty() {
return match agg {
CaptureAgg::Sum(_) => polydat::ast::Value::U64(0),
CaptureAgg::Min(_) | CaptureAgg::Max(_) => polydat::ast::Value::None,
};
}
match agg {
CaptureAgg::Sum(_) => {
if nums.iter().all(|n| n.as_u64().is_some()) {
polydat::ast::Value::U64(nums.iter().map(|n| n.as_u64().unwrap()).sum())
} else {
polydat::ast::Value::F64(nums.iter().filter_map(|n| n.as_f64()).sum())
}
}
CaptureAgg::Min(_) | CaptureAgg::Max(_) => {
let want_min = matches!(agg, CaptureAgg::Min(_));
let mut best = nums[0];
for n in &nums[1..] {
let (a, b) = (
n.as_f64().unwrap_or(f64::NAN),
best.as_f64().unwrap_or(f64::NAN),
);
if (want_min && a < b) || (!want_min && a > b) {
best = n;
}
}
json_to_value(&serde_json::Value::Number((*best).clone()))
}
}
}
fn json_subtree_to_value(v: &serde_json::Value) -> polydat::ast::Value {
match v {
serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
polydat::ast::Value::Json(std::sync::Arc::new(v.clone()))
}
scalar => json_to_value(scalar),
}
}
fn first_row_field(json: &serde_json::Value, name: &str) -> Option<serde_json::Value> {
match json {
serde_json::Value::Array(rows) => rows.first().and_then(|row| row.get(name)).cloned(),
serde_json::Value::Object(_) => json.get(name).cloned(),
_ => None,
}
}
fn slurp_column(json: &serde_json::Value, name: &str) -> Vec<serde_json::Value> {
match json {
serde_json::Value::Array(rows) => rows
.iter()
.filter_map(|row| row.get(name).cloned())
.collect(),
serde_json::Value::Object(_) => json.get(name).map(|v| vec![v.clone()]).unwrap_or_default(),
_ => Vec::new(),
}
}
pub(crate) fn json_to_value(v: &serde_json::Value) -> polydat::ast::Value {
match v {
serde_json::Value::Null => polydat::ast::Value::None,
serde_json::Value::Number(n) => {
if let Some(i) = n.as_u64() {
polydat::ast::Value::U64(i)
} else if let Some(f) = n.as_f64() {
polydat::ast::Value::F64(f)
} else {
polydat::ast::Value::Str(n.to_string().into())
}
}
serde_json::Value::Bool(b) => polydat::ast::Value::Bool(*b),
serde_json::Value::String(s) => polydat::ast::Value::Str(s.as_str().into()),
other => polydat::ast::Value::Str(other.to_string().into()),
}
}
impl WrappingDispenser for TraversingDispenser {}
impl OpDispenser for TraversingDispenser {
fn execute<'a>(
&'a self,
cycle: u64,
ctx: &'a crate::fixture::ExecCtx<'a>,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<OpResult, ExecutionError>> + Send + 'a>,
> {
Box::pin(async move {
let result = self.inner.execute(cycle, ctx).await?;
if let Some(body) = &result.body {
self.stats
.metrics
.result_elements
.inc_by(body.element_count());
if let Some(bytes) = body.byte_count() {
self.stats.metrics.result_bytes.inc_by(bytes);
}
}
if !self.captures.is_empty() && result.body.is_none() {
for (name, value) in captures_for_empty_result(&self.captures) {
publish_capture(&ctx, &name, value);
}
}
if !self.captures.is_empty()
&& let Some(body) = &result.body
{
let base = self.spec.as_ref().and_then(|s| s.path.as_deref());
let extracted = extract_captures_rooted(body.as_ref(), &self.captures, base);
let policy = self.spec.as_ref().map(|s| s.on_missing).unwrap_or_default();
if !matches!(policy, nmbrs_workload::model::OnMissing::Ignore) {
for cp in &self.captures {
let missing = match extracted.get(&cp.as_name) {
None => true,
Some(polydat::ast::Value::None) => true,
Some(_) => false,
};
if !missing {
continue;
}
let detail = format!(
"op capture '{}' resolved to nothing{}",
cp.as_name,
match base {
Some(b) => format!(" (traverse path '{b}')"),
None => String::new(),
}
);
match policy {
nmbrs_workload::model::OnMissing::Warn => {
crate::diag!(crate::observer::LogLevel::Warn, "{detail}")
}
nmbrs_workload::model::OnMissing::Error => {
return Err(ExecutionError::Op(crate::adapter::AdapterError {
error_name: "capture_missing".into(),
message: detail,
retryable: false,
}));
}
nmbrs_workload::model::OnMissing::Ignore => {}
}
}
}
for (name, value) in extracted {
publish_capture(&ctx, &name, value);
}
}
Ok(result)
})
}
fn inner_dispenser(&self) -> Option<&dyn OpDispenser> {
Some(self.inner.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::ResultBody;
fn agg_cap(
alias: &str,
agg: Option<bindpoints::CaptureAgg>,
count: bool,
) -> bindpoints::CapturePoint {
bindpoints::CapturePoint {
row_filter: None,
source_name: alias.into(),
as_name: alias.into(),
cast_type: None,
slurp: false,
path: Some(String::new()),
count,
agg,
}
}
#[test]
fn empty_result_publishes_identity_for_count_and_sum() {
let specs = vec![
agg_cap("n", None, true),
agg_cap("s", Some(bindpoints::CaptureAgg::Sum("x".into())), false),
];
let out = super::captures_for_empty_result(&specs);
assert!(
matches!(out.get("n"), Some(polydat::ast::Value::U64(0))),
"counting nothing is 0, not 'leave the old count': {:?}",
out.get("n")
);
assert!(
matches!(out.get("s"), Some(polydat::ast::Value::U64(0))),
"summing nothing is 0: {:?}",
out.get("s")
);
}
#[test]
fn empty_result_clears_min_and_max_rather_than_inventing_a_value() {
let specs = vec![
agg_cap("lo", Some(bindpoints::CaptureAgg::Min("x".into())), false),
agg_cap("hi", Some(bindpoints::CaptureAgg::Max("x".into())), false),
];
let out = super::captures_for_empty_result(&specs);
assert!(
matches!(out.get("lo"), Some(polydat::ast::Value::None)),
"min of nothing must not invent 0: {:?}",
out.get("lo")
);
assert!(
matches!(out.get("hi"), Some(polydat::ast::Value::None)),
"max of nothing must not invent 0: {:?}",
out.get("hi")
);
}
#[test]
fn empty_result_leaves_plain_captures_absent() {
let specs = vec![agg_cap("v", None, false)];
let out = super::captures_for_empty_result(&specs);
assert!(matches!(out.get("v"), Some(polydat::ast::Value::None)));
}
#[test]
fn empty_fold_totality_sum_is_zero_min_max_are_reset_markers() {
let rows = serde_json::json!([{"other": 1}, {"other": 2}]);
let v = super::aggregate_rows(
Some(&rows),
&bindpoints::CaptureAgg::Sum("missing".into()),
None,
);
assert!(
matches!(v, polydat::ast::Value::U64(0)),
"sum-of-nothing is its identity 0: {v:?}"
);
for agg in [
bindpoints::CaptureAgg::Min("missing".into()),
bindpoints::CaptureAgg::Max("missing".into()),
] {
let v = super::aggregate_rows(Some(&rows), &agg, None);
assert!(
matches!(v, polydat::ast::Value::None),
"min/max-of-nothing is the reset marker: {v:?}"
);
}
}
fn cap(source: &str, alias: &str, slurp: bool) -> bindpoints::CapturePoint {
bindpoints::CapturePoint {
row_filter: None,
source_name: source.into(),
as_name: alias.into(),
cast_type: None,
slurp,
path: None,
count: false,
agg: None,
}
}
#[test]
fn parse_captures_from_template() {
let parsed =
bindpoints::parse_capture_points("SELECT [username], [age as user_age] FROM users");
assert_eq!(parsed.captures.len(), 2);
assert_eq!(parsed.captures[0].source_name, "username");
assert_eq!(parsed.captures[0].as_name, "username");
assert!(!parsed.captures[0].slurp);
assert_eq!(parsed.captures[1].source_name, "age");
assert_eq!(parsed.captures[1].as_name, "user_age");
assert_eq!(parsed.raw_template, "SELECT username, age FROM users");
}
#[test]
fn parse_slurp_capture() {
let parsed = bindpoints::parse_capture_points("SELECT [@keys] FROM t");
assert_eq!(parsed.captures.len(), 1);
assert_eq!(parsed.captures[0].source_name, "keys");
assert!(parsed.captures[0].slurp);
assert_eq!(parsed.raw_template, "SELECT keys FROM t");
}
#[derive(Debug)]
struct JsonBody(serde_json::Value);
impl ResultBody for JsonBody {
fn to_json(&self) -> serde_json::Value {
self.0.clone()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[test]
fn extract_from_json_top_level() {
let body = JsonBody(serde_json::json!({
"user_id": 42,
"name": "alice",
"balance": 99.5
}));
let specs = vec![cap("user_id", "uid", false), cap("name", "name", false)];
let captures = extract_captures_from_json(&body, &specs);
assert_eq!(captures.len(), 2);
assert_eq!(captures["uid"].as_u64(), 42);
match &captures["name"] {
polydat::ast::Value::Str(s) => assert_eq!(&**s, "alice"),
other => panic!("expected Str, got {other:?}"),
}
}
#[test]
fn extract_wildcard() {
let body = JsonBody(serde_json::json!({"a": 1, "b": 2}));
let specs = vec![cap("*", "*", false)];
let captures = extract_captures_from_json(&body, &specs);
assert_eq!(captures.len(), 2);
}
#[test]
fn extract_slurp_array_of_rows() {
let body = JsonBody(serde_json::json!([
{"key": 4, "value": 0.5},
{"key": 17, "value": 0.4},
{"key": 42, "value": 0.3},
]));
let specs = vec![cap("key", "key", true)];
let captures = extract_captures_from_json(&body, &specs);
assert_eq!(captures.len(), 1);
match &captures["key"] {
polydat::ast::Value::Json(arc) => {
let serde_json::Value::Array(items) = arc.as_ref() else {
panic!("expected Value::Json(array), got {arc:?}");
};
assert_eq!(items.len(), 3);
assert_eq!(items[0], serde_json::json!(4));
assert_eq!(items[1], serde_json::json!(17));
assert_eq!(items[2], serde_json::json!(42));
}
other => panic!("expected Value::Json(array), got {other:?}"),
}
}
#[test]
fn extract_single_first_row_of_array() {
let body = JsonBody(serde_json::json!([
{"key": 4}, {"key": 17}, {"key": 42},
]));
let specs = vec![cap("key", "first_key", false)];
let captures = extract_captures_from_json(&body, &specs);
assert_eq!(captures.len(), 1);
assert_eq!(captures["first_key"].as_u64(), 4);
}
fn cap_path(name: &str, path: &str, count: bool) -> bindpoints::CapturePoint {
bindpoints::CapturePoint {
source_name: name.into(),
as_name: name.into(),
cast_type: None,
slurp: false,
path: Some(path.into()),
count,
agg: None,
row_filter: None,
}
}
#[test]
fn traverse_path_re_roots_the_document() {
let body = JsonBody(serde_json::json!({
"status": 200,
"value": [{"progress": 7u64}],
}));
let spec = |name: &str, path: &str| bindpoints::CapturePoint {
source_name: name.into(),
as_name: name.into(),
cast_type: None,
slurp: false,
path: Some(path.to_string()),
count: false,
agg: None,
row_filter: None,
};
let long = extract_captures_rooted(&body, &[spec("p", "/value/0/progress")], None);
assert_eq!(long["p"].as_u64(), 7);
let short = extract_captures_rooted(&body, &[spec("p", "/0/progress")], Some("/value"));
assert_eq!(short["p"].as_u64(), 7);
}
#[test]
fn an_unresolvable_traverse_path_yields_nothing() {
let body = JsonBody(serde_json::json!({"value": 1}));
let spec = bindpoints::CapturePoint {
source_name: "x".into(),
as_name: "x".into(),
cast_type: None,
slurp: false,
path: Some("/x".into()),
count: false,
agg: None,
row_filter: None,
};
let caps = extract_captures_rooted(&body, &[spec], Some("/nonesuch"));
assert!(caps.is_empty(), "unresolvable base must not invent values");
}
#[test]
fn aggregate_captures_filter_rows_by_kind() {
let body = JsonBody(serde_json::json!([
{"kind": "compaction", "progress": 47048035855u64},
{"kind": "secondary index build", "progress": 4855601u64},
]));
fn cap_filtered(
name: &str,
field: &str,
filter: Option<(&str, &str)>,
) -> bindpoints::CapturePoint {
bindpoints::CapturePoint {
source_name: name.into(),
as_name: name.into(),
cast_type: None,
slurp: false,
path: Some(String::new()),
count: false,
agg: Some(bindpoints::CaptureAgg::Sum(field.into())),
row_filter: filter.map(|(k, v)| (k.to_string(), v.to_string())),
}
}
let specs = vec![
cap_filtered("all", "progress", None),
cap_filtered("index", "progress", Some(("kind", "secondary index build"))),
cap_filtered("data", "progress", Some(("kind", "compaction"))),
];
let caps = extract_captures_from_json(&body, &specs);
let num = |k: &str| caps[k].as_u64();
assert_eq!(num("index"), 4_855_601, "index build only");
assert_eq!(num("data"), 47_048_035_855, "data compaction only");
assert_eq!(num("all"), num("index") + num("data"));
}
#[test]
fn aggregate_filter_matching_no_rows_folds_to_sum_identity() {
let body = JsonBody(serde_json::json!([
{"kind": "compaction", "progress": 5u64},
]));
let specs = vec![bindpoints::CapturePoint {
source_name: "x".into(),
as_name: "x".into(),
cast_type: None,
slurp: false,
path: Some(String::new()),
count: false,
agg: Some(bindpoints::CaptureAgg::Sum("progress".into())),
row_filter: Some(("kind".into(), "nonesuch".into())),
}];
let caps = extract_captures_from_json(&body, &specs);
assert!(
matches!(caps["x"], polydat::ast::Value::U64(0)),
"a filtered-empty sum folds to its identity 0; got {:?}",
caps["x"]
);
}
#[test]
fn aggregate_captures_fold_rows_with_mixed_units() {
let body = JsonBody(serde_json::json!([
{"completion_ratio": 1.0, "progress": 31357891323u64, "total": 31357891323u64},
{"completion_ratio": 0.4, "progress": 8000000u64, "total": 20000000u64},
]));
fn cap_agg(name: &str, agg: bindpoints::CaptureAgg) -> bindpoints::CapturePoint {
bindpoints::CapturePoint {
source_name: name.into(),
as_name: name.into(),
cast_type: None,
slurp: false,
path: Some(String::new()),
count: false,
agg: Some(agg),
row_filter: None,
}
}
let specs = vec![
cap_agg(
"completion_ratio",
bindpoints::CaptureAgg::Min("completion_ratio".into()),
),
cap_agg(
"max_ratio",
bindpoints::CaptureAgg::Max("completion_ratio".into()),
),
cap_agg("progress", bindpoints::CaptureAgg::Sum("progress".into())),
];
let captures = extract_captures_from_json(&body, &specs);
assert_eq!(captures["completion_ratio"].as_f64(), 0.4);
assert_eq!(captures["max_ratio"].as_f64(), 1.0);
assert_eq!(captures["progress"].as_u64(), 31357891323 + 8000000);
let empty = JsonBody(serde_json::json!([]));
let c2 = extract_captures_from_json(&empty, &specs[..1]);
assert!(matches!(c2["completion_ratio"], polydat::ast::Value::None));
}
#[test]
fn extract_json_pointer_scalar_from_bulk_response() {
let body = JsonBody(serde_json::json!([
{"value": 7, "status": 200},
{"value": [], "status": 200},
{"value": 0, "status": 200},
]));
let specs = vec![
cap_path("sstables", "/0/value", false),
cap_path("pending_for_cf", "/2/value", false),
];
let captures = extract_captures_from_json(&body, &specs);
assert_eq!(captures["sstables"].as_u64(), 7);
assert_eq!(captures["pending_for_cf"].as_u64(), 0);
}
#[test]
fn extract_json_pointer_resolved_null_yields_none_not_string() {
let body = JsonBody(serde_json::json!([
{"value": null, "status": 200},
]));
let specs = vec![cap_path("pending_for_cf", "/0/value", false)];
let captures = extract_captures_from_json(&body, &specs);
assert!(
matches!(captures["pending_for_cf"], polydat::ast::Value::None),
"JSON null at path should yield Value::None, got {:?}",
captures["pending_for_cf"],
);
}
#[test]
fn extract_json_pointer_count_on_resolved_null_returns_zero() {
let body = JsonBody(serde_json::json!([
{"value": null, "status": 200},
]));
let specs = vec![cap_path("pending_for_cf", "/0/value", true)];
let captures = extract_captures_from_json(&body, &specs);
assert_eq!(
captures["pending_for_cf"].as_u64(),
0,
"`:count` on resolved-null should return 0, got {:?}",
captures["pending_for_cf"],
);
}
#[test]
fn extract_json_pointer_count_collapses_array_to_length() {
let body = JsonBody(serde_json::json!([
{"value": 7},
{"value": [
{"compactionId":"a", "keyspace":"ks", "columnfamily":"cf"},
{"compactionId":"b", "keyspace":"ks", "columnfamily":"cf"},
]},
]));
let specs = vec![cap_path("active_count", "/1/value", true)];
let captures = extract_captures_from_json(&body, &specs);
assert_eq!(captures["active_count"].as_u64(), 2);
}
#[test]
fn extract_json_pointer_missing_path_yields_none() {
let body = JsonBody(serde_json::json!({"value": 7}));
let specs = vec![cap_path("not_there", "/missing/path", false)];
let captures = extract_captures_from_json(&body, &specs);
assert!(
matches!(captures["not_there"], polydat::ast::Value::None),
"expected Value::None for unresolvable JSON-Pointer, got {:?}",
captures["not_there"],
);
}
#[test]
fn extract_json_pointer_count_of_missing_path_is_zero() {
let body = JsonBody(serde_json::json!({"value": 7}));
let specs = vec![cap_path("active_count", "/missing/value", true)];
let captures = extract_captures_from_json(&body, &specs);
assert_eq!(captures["active_count"].as_u64(), 0);
}
#[test]
fn extract_json_pointer_structural_sub_tree_captured_as_json() {
let body = JsonBody(serde_json::json!([
{"value": {"keyspace": "ks", "table": "cf", "ssTables": 3}},
]));
let specs = vec![cap_path("state", "/0/value", false)];
let captures = extract_captures_from_json(&body, &specs);
match &captures["state"] {
polydat::ast::Value::Json(arc) => {
assert_eq!(arc.get("keyspace").and_then(|v| v.as_str()), Some("ks"));
assert_eq!(arc.get("ssTables").and_then(|v| v.as_u64()), Some(3));
}
other => panic!("expected Value::Json, got {other:?}"),
}
let _ = count_of_subtree;
let _ = json_subtree_to_value;
}
}