use std::collections::BTreeMap;
use super::query::ResultRow;
use crate::adapter::net::behavior::tag::{AxisSeparator, Tag, TaxonomyAxis};
pub fn synthetic_row_view(row: &ResultRow) -> (Vec<Tag>, BTreeMap<String, String>) {
let mut tags: Vec<Tag> = Vec::new();
let mut metadata: BTreeMap<String, String> = BTreeMap::new();
let origin_str = format!("{:016x}", row.origin);
let seq_str = row.seq.0.to_string();
push_field(&mut tags, &mut metadata, "origin", &origin_str);
push_field(&mut tags, &mut metadata, "seq", &seq_str);
if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&row.payload) {
flatten_json("", &value, &mut tags, &mut metadata);
}
(tags, metadata)
}
fn push_field(
tags: &mut Vec<Tag>,
metadata: &mut BTreeMap<String, String>,
key: &str,
value: &str,
) {
tags.push(Tag::AxisValue {
axis: TaxonomyAxis::Dataforts,
key: key.to_string(),
value: value.to_string(),
separator: AxisSeparator::Eq,
});
metadata.insert(key.to_string(), value.to_string());
}
fn flatten_json(
prefix: &str,
value: &serde_json::Value,
tags: &mut Vec<Tag>,
metadata: &mut BTreeMap<String, String>,
) {
use serde_json::Value::*;
match value {
Object(map) => {
for (k, v) in map {
let next = if prefix.is_empty() {
k.clone()
} else {
format!("{prefix}.{k}")
};
flatten_json(&next, v, tags, metadata);
}
}
Array(_) => {}
String(s) => {
if !prefix.is_empty() {
push_field(tags, metadata, prefix, s);
}
}
Number(n) => {
if !prefix.is_empty() {
push_field(tags, metadata, prefix, &n.to_string());
}
}
Bool(b) => {
if !prefix.is_empty() {
push_field(tags, metadata, prefix, &b.to_string());
}
}
Null => {}
}
}
pub fn extract_numeric(row: &ResultRow, path: &str) -> Option<f64> {
match path {
"origin" => Some(row.origin as f64),
"seq" => Some(row.seq.0 as f64),
_ => extract_numeric_from_payload(&row.payload, path),
}
}
fn extract_numeric_from_payload(payload: &[u8], path: &str) -> Option<f64> {
let value: serde_json::Value = serde_json::from_slice(payload).ok()?;
let leaf = walk_json_path(&value, path)?;
coerce_numeric(leaf)
}
fn walk_json_path<'a>(value: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
let mut cur = value;
for segment in path.split('.') {
cur = cur.get(segment)?;
}
Some(cur)
}
fn coerce_numeric(value: &serde_json::Value) -> Option<f64> {
use serde_json::Value::*;
match value {
Number(n) => n.as_f64(),
Bool(true) => Some(1.0),
Bool(false) => Some(0.0),
_ => None,
}
}
pub fn extract_string_projection(row: &ResultRow, path: &str) -> Option<String> {
match path {
"origin" => Some(format!("{:016x}", row.origin)),
"seq" => Some(row.seq.0.to_string()),
_ => extract_string_from_payload(&row.payload, path),
}
}
fn extract_string_from_payload(payload: &[u8], path: &str) -> Option<String> {
let value: serde_json::Value = serde_json::from_slice(payload).ok()?;
let leaf = walk_json_path(&value, path)?;
Some(leaf.to_string())
}
#[cfg(test)]
mod tests {
use super::super::query::SeqNum;
use super::*;
fn row_with_payload(origin: u64, seq: u64, payload: &str) -> ResultRow {
ResultRow {
origin,
seq: SeqNum(seq),
payload: payload.as_bytes().to_vec(),
}
}
fn tag_value(tags: &[Tag], key: &str) -> Option<String> {
tags.iter().find_map(|t| match t {
Tag::AxisValue {
axis: TaxonomyAxis::Dataforts,
key: k,
value,
..
} if k == key => Some(value.clone()),
_ => None,
})
}
#[test]
fn origin_and_seq_are_always_synthesized() {
let row = row_with_payload(0xABCD_EF01_2345_6789, 42, "");
let (tags, metadata) = synthetic_row_view(&row);
assert_eq!(
tag_value(&tags, "origin"),
Some("abcdef0123456789".to_string())
);
assert_eq!(tag_value(&tags, "seq"), Some("42".to_string()));
assert_eq!(
metadata.get("origin"),
Some(&"abcdef0123456789".to_string())
);
assert_eq!(metadata.get("seq"), Some(&"42".to_string()));
}
#[test]
fn non_json_payload_is_opaque_and_does_not_panic() {
let row = row_with_payload(0x1, 0, "this is not json");
let (tags, _) = synthetic_row_view(&row);
assert!(tag_value(&tags, "origin").is_some());
assert!(tag_value(&tags, "seq").is_some());
assert_eq!(tags.len(), 2);
}
#[test]
fn flat_json_object_flattens_top_level_scalars() {
let row = row_with_payload(
0x1,
0,
r#"{"severity":"high","count":3,"ok":true,"unused":null}"#,
);
let (tags, metadata) = synthetic_row_view(&row);
assert_eq!(tag_value(&tags, "severity"), Some("high".to_string()));
assert_eq!(tag_value(&tags, "count"), Some("3".to_string()));
assert_eq!(tag_value(&tags, "ok"), Some("true".to_string()));
assert!(tag_value(&tags, "unused").is_none());
assert_eq!(metadata.get("count"), Some(&"3".to_string()));
}
#[test]
fn nested_json_flattens_with_dotted_paths() {
let row = row_with_payload(0x1, 0, r#"{"a":{"b":{"c":"deep"}},"flat":1}"#);
let (tags, _) = synthetic_row_view(&row);
assert_eq!(tag_value(&tags, "a.b.c"), Some("deep".to_string()));
assert_eq!(tag_value(&tags, "flat"), Some("1".to_string()));
}
#[test]
fn arrays_are_skipped_in_phase_e2() {
let row = row_with_payload(0x1, 0, r#"{"items":["x","y","z"],"name":"keep"}"#);
let (tags, _) = synthetic_row_view(&row);
assert_eq!(tag_value(&tags, "name"), Some("keep".to_string()));
assert!(tags
.iter()
.all(|t| !matches!(t, Tag::AxisValue { key, .. } if key.starts_with("items"))));
}
#[test]
fn non_object_json_root_falls_back_to_intrinsic_only() {
let row = row_with_payload(0x1, 0, r#"["a","b","c"]"#);
let (tags, _) = synthetic_row_view(&row);
assert_eq!(tags.len(), 2); }
}