pub(crate) fn camel_wrapper_id(line: &str) -> Option<&str> {
let s = line.trim_start().strip_prefix('{')?;
let s = s.trim_start().strip_prefix('"')?;
let key = &s[..s.find('"')?];
match key {
"timestamp" | "prio" | "src" | "dst" | "pgn" | "description" | "fields" => None,
_ => Some(key),
}
}
const RECORD_KEYS: [&str; 7] = [
"timestamp",
"prio",
"src",
"dst",
"pgn",
"description",
"fields",
];
pub(crate) fn has_key(msg: &str, field: &str) -> bool {
msg.match_indices(field).any(|(i, _)| {
i > 0 && msg.as_bytes()[i - 1] == b'"' && msg[i + field.len()..].starts_with("\":")
})
}
pub(crate) fn uses_camel_keys(line: &str, fields: &[crate::engine::types::FieldInfo]) -> bool {
for fi in fields {
if fi.id == fi.name || RECORD_KEYS.contains(&fi.id) || RECORD_KEYS.contains(&fi.name) {
continue;
}
if has_key(line, fi.id) {
return true;
}
if has_key(line, fi.name) {
return false;
}
}
false
}
pub fn value<'a>(msg: &'a str, field: &str) -> Option<&'a str> {
let needle = format!("\"{field}\":");
let mut start = msg.find(&needle)? + needle.len();
let bytes = msg.as_bytes();
while start < bytes.len() && (bytes[start] == b' ' || bytes[start] == b'\t') {
start += 1;
}
if start >= bytes.len() {
return None;
}
let quoted = bytes[start] == b'"';
if quoted {
let body_start = start + 1;
let mut idx = body_start;
while idx < bytes.len() && bytes[idx] != b'"' {
if bytes[idx] == b'\\' && idx + 1 < bytes.len() {
idx += 2;
continue;
}
idx += 1;
}
return Some(&msg[body_start..idx]);
}
if bytes[start] == b'{' {
let mut depth = 0;
let mut idx = start;
while idx < bytes.len() {
match bytes[idx] {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return Some(&msg[start..=idx]);
}
}
_ => {}
}
idx += 1;
}
return None;
}
if bytes[start] == b'[' {
let mut depth = 0;
let mut idx = start;
while idx < bytes.len() {
match bytes[idx] {
b'[' => depth += 1,
b']' => {
depth -= 1;
if depth == 0 {
return Some(&msg[start..=idx]);
}
}
_ => {}
}
idx += 1;
}
return None;
}
let mut idx = start;
while idx < bytes.len() && !matches!(bytes[idx], b',' | b'}' | b' ' | b'\t' | b'\n' | b'\r') {
idx += 1;
}
Some(&msg[start..idx])
}
pub fn number(msg: &str, field: &str) -> Option<f64> {
let v = value(msg, field)?;
if v == "null" || v.is_empty() {
return None;
}
if v.starts_with('{') {
return number(v, "value");
}
v.trim().parse().ok()
}
pub fn int(msg: &str, field: &str) -> Option<i64> {
let v = value(msg, field)?;
if v.starts_with('{') {
return int(v, "value");
}
v.trim().parse().ok()
}
pub fn lookup_int(msg: &str, field: &str) -> Option<i64> {
if let Some(obj) = value(msg, field) {
if obj.starts_with('{') {
return int(obj, "value");
}
if let Ok(n) = obj.trim().parse() {
return Some(n);
}
}
None
}
pub fn lookup_text<'a>(msg: &'a str, field: &str) -> Option<&'a str> {
let v = value(msg, field)?;
if v.starts_with('{') {
value(v, "value").or_else(|| value(v, "name"))
} else {
Some(v)
}
}
pub fn array_span(msg: &str, field: &str) -> Option<(usize, usize)> {
let needle = format!("\"{field}\":");
let mut start = msg.find(&needle)? + needle.len();
let bytes = msg.as_bytes();
while start < bytes.len() && (bytes[start] == b' ' || bytes[start] == b'\t') {
start += 1;
}
if start >= bytes.len() || bytes[start] != b'[' {
return None;
}
let mut depth = 0;
let mut idx = start;
while idx < bytes.len() {
match bytes[idx] {
b'[' => depth += 1,
b']' => {
depth -= 1;
if depth == 0 {
return Some((start, idx + 1));
}
}
b'"' => {
idx += 1;
while idx < bytes.len() && bytes[idx] != b'"' {
if bytes[idx] == b'\\' && idx + 1 < bytes.len() {
idx += 2;
continue;
}
idx += 1;
}
}
_ => {}
}
idx += 1;
}
None
}
pub fn split_object_array(arr: &str) -> Vec<&str> {
let bytes = arr.as_bytes();
if bytes.first() != Some(&b'[') {
return Vec::new();
}
let mut out = Vec::new();
let mut idx = 1;
while idx < bytes.len() {
match bytes[idx] {
b' ' | b'\t' | b',' => idx += 1,
b']' => break,
b'{' => {
let start = idx;
let mut depth = 0;
while idx < bytes.len() {
match bytes[idx] {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
idx += 1;
break;
}
}
b'"' => {
idx += 1;
while idx < bytes.len() && bytes[idx] != b'"' {
if bytes[idx] == b'\\' && idx + 1 < bytes.len() {
idx += 2;
continue;
}
idx += 1;
}
}
_ => {}
}
idx += 1;
}
out.push(&arr[start..idx]);
}
_ => idx += 1,
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_number() {
let m = r#"{"pgn":127251,"src":7,"fields":{"Rate":-0.357703}}"#;
assert_eq!(int(m, "pgn"), Some(127251));
assert_eq!(int(m, "src"), Some(7));
assert_eq!(number(m, "Rate"), Some(-0.357703));
}
#[test]
fn extracts_string() {
let m = r#"{"timestamp":"2026-01-01","fields":{"Reference":"Magnetic"}}"#;
assert_eq!(value(m, "timestamp"), Some("2026-01-01"));
assert_eq!(value(m, "Reference"), Some("Magnetic"));
}
#[test]
fn handles_nv_lookup_object() {
let m = r#"{"fields":{"Reference":{"value":1,"name":"Magnetic"}}}"#;
assert_eq!(int(m, "Reference"), Some(1));
assert_eq!(lookup_int(m, "Reference"), Some(1));
}
#[test]
fn missing_returns_none() {
let m = r#"{"foo":1}"#;
assert_eq!(value(m, "bar"), None);
assert_eq!(number(m, "bar"), None);
}
#[test]
fn lookup_text_prefers_numeric_value() {
let m = r#"{"Reference":{"value":1,"name":"Magnetic"}}"#;
assert_eq!(lookup_text(m, "Reference"), Some("1"));
}
#[test]
fn lookup_text_handles_key_true_objects() {
let m = r#"{"Instance":{"value":0,"key":true}}"#;
assert_eq!(lookup_text(m, "Instance"), Some("0"));
}
#[test]
fn lookup_text_falls_back_to_name_when_value_missing() {
let m = r#"{"Reference":{"name":"Magnetic"}}"#;
assert_eq!(lookup_text(m, "Reference"), Some("Magnetic"));
}
#[test]
fn lookup_text_returns_bare_value_when_no_object() {
let m = r#"{"User ID":"244180106"}"#;
assert_eq!(lookup_text(m, "User ID"), Some("244180106"));
}
#[test]
fn array_span_locates_top_level_list() {
let line = r#"{"pgn":130824,"fields":{"list":[{"a":1},{"a":2}]}}"#;
let (s, e) = array_span(line, "list").expect("list found");
assert_eq!(&line[s..e], "[{\"a\":1},{\"a\":2}]");
}
#[test]
fn array_span_skips_brackets_inside_strings() {
let line = r#"{"fields":{"Note":"a]b","list":[{"a":1}]}}"#;
let (s, e) = array_span(line, "list").expect("list span");
assert_eq!(&line[s..e], "[{\"a\":1}]");
}
#[test]
fn split_object_array_breaks_top_level_objects_only() {
let arr = r#"[{"a":1,"b":[2,3]},{"a":4},{"a":{"nested":5}}]"#;
let elems = split_object_array(arr);
assert_eq!(
elems,
vec![
r#"{"a":1,"b":[2,3]}"#,
r#"{"a":4}"#,
r#"{"a":{"nested":5}}"#,
]
);
}
#[test]
fn split_object_array_handles_empty_and_whitespace() {
assert!(split_object_array("[]").is_empty());
assert_eq!(
split_object_array("[ {\"a\":1} , {\"a\":2} ]"),
vec!["{\"a\":1}", "{\"a\":2}"]
);
}
}