Skip to main content

steeldb/projectors/
json_proj.rs

1//! Generic JSON / NDJSON projector — the DuckDB `read_json` analog.
2//!
3//! Accepts either a top-level array of objects, or NDJSON (one object per line). Each object becomes
4//! one situation; nested keys are flattened to a dotted path and each scalar leaf emits a
5//! `path/value` token (arrays emit one token per element under the same path). The compact JSON is
6//! kept for display.
7
8use crate::projector::{slug, CorpusKind, Projector, Situation};
9use serde_json::Value;
10use std::path::PathBuf;
11
12pub struct JsonProjector {
13    path: PathBuf,
14}
15
16impl JsonProjector {
17    pub fn open(path: impl Into<PathBuf>) -> JsonProjector {
18        JsonProjector { path: path.into() }
19    }
20}
21
22/// Walk a value, emitting `path/value` tokens for every scalar leaf.
23fn flatten(prefix: &str, v: &Value, out: &mut Vec<String>) {
24    match v {
25        Value::Object(map) => {
26            for (k, child) in map {
27                let key = slug(k);
28                let path = if prefix.is_empty() { key } else { format!("{prefix}/{key}") };
29                flatten(&path, child, out);
30            }
31        }
32        Value::Array(items) => {
33            for item in items {
34                flatten(prefix, item, out);
35            }
36        }
37        Value::String(s) => {
38            let sv = slug(s);
39            if !sv.is_empty() && !prefix.is_empty() {
40                out.push(format!("{prefix}/{sv}"));
41            }
42        }
43        Value::Number(n) => {
44            if !prefix.is_empty() {
45                out.push(format!("{prefix}/{}", slug(&n.to_string())));
46            }
47        }
48        Value::Bool(b) => {
49            if !prefix.is_empty() {
50                out.push(format!("{prefix}/{b}"));
51            }
52        }
53        Value::Null => {}
54    }
55}
56
57fn situation_of(v: &Value) -> Situation {
58    let mut tokens = Vec::new();
59    flatten("", v, &mut tokens);
60    tokens.sort();
61    tokens.dedup();
62    Situation::new(tokens, vec![v.to_string()])
63}
64
65impl Projector for JsonProjector {
66    fn columns(&self) -> Vec<String> {
67        vec!["json".to_string()]
68    }
69    fn kind(&self) -> CorpusKind {
70        CorpusKind::Csv
71    }
72    fn source(&self) -> String {
73        self.path.display().to_string()
74    }
75    fn project(self: Box<Self>, sink: &mut dyn FnMut(Situation)) -> std::io::Result<()> {
76        let text = std::fs::read_to_string(&self.path)?;
77        // Prefer a single well-formed document (array or object); fall back to NDJSON.
78        match serde_json::from_str::<Value>(&text) {
79            Ok(Value::Array(items)) => {
80                for item in &items {
81                    sink(situation_of(item));
82                }
83            }
84            Ok(other) => sink(situation_of(&other)),
85            Err(_) => {
86                for line in text.lines() {
87                    let line = line.trim();
88                    if line.is_empty() {
89                        continue;
90                    }
91                    if let Ok(v) = serde_json::from_str::<Value>(line) {
92                        sink(situation_of(&v));
93                    }
94                }
95            }
96        }
97        Ok(())
98    }
99}