hypersteeldb 0.5.5

A database that compiles questions instead of guessing answers: typed vocabulary discovered from your documents, queries type-checked before they run, roaring-bitmap set algebra over reified hyperedges, and Dempster-Shafer evidence with an explicit conflict guard.
Documentation
//! Generic JSON / NDJSON projector — the DuckDB `read_json` analog.
//!
//! Accepts either a top-level array of objects, or NDJSON (one object per line). Each object becomes
//! one situation; nested keys are flattened to a dotted path and each scalar leaf emits a
//! `path/value` token (arrays emit one token per element under the same path). The compact JSON is
//! kept for display.

use crate::projector::{slug, CorpusKind, Projector, Situation};
use serde_json::Value;
use std::path::PathBuf;

pub struct JsonProjector {
    path: PathBuf,
}

impl JsonProjector {
    pub fn open(path: impl Into<PathBuf>) -> JsonProjector {
        JsonProjector { path: path.into() }
    }
}

/// Walk a value, emitting `path/value` tokens for every scalar leaf.
fn flatten(prefix: &str, v: &Value, out: &mut Vec<String>) {
    match v {
        Value::Object(map) => {
            for (k, child) in map {
                let key = slug(k);
                let path = if prefix.is_empty() { key } else { format!("{prefix}/{key}") };
                flatten(&path, child, out);
            }
        }
        Value::Array(items) => {
            for item in items {
                flatten(prefix, item, out);
            }
        }
        Value::String(s) => {
            let sv = slug(s);
            if !sv.is_empty() && !prefix.is_empty() {
                out.push(format!("{prefix}/{sv}"));
            }
        }
        Value::Number(n) => {
            if !prefix.is_empty() {
                out.push(format!("{prefix}/{}", slug(&n.to_string())));
            }
        }
        Value::Bool(b) => {
            if !prefix.is_empty() {
                out.push(format!("{prefix}/{b}"));
            }
        }
        Value::Null => {}
    }
}

fn situation_of(v: &Value) -> Situation {
    let mut tokens = Vec::new();
    flatten("", v, &mut tokens);
    tokens.sort();
    tokens.dedup();
    Situation::new(tokens, vec![v.to_string()])
}

impl Projector for JsonProjector {
    fn columns(&self) -> Vec<String> {
        vec!["json".to_string()]
    }
    fn kind(&self) -> CorpusKind {
        CorpusKind::Csv
    }
    fn source(&self) -> String {
        self.path.display().to_string()
    }
    fn project(self: Box<Self>, sink: &mut dyn FnMut(Situation)) -> std::io::Result<()> {
        let text = std::fs::read_to_string(&self.path)?;
        // Prefer a single well-formed document (array or object); fall back to NDJSON.
        match serde_json::from_str::<Value>(&text) {
            Ok(Value::Array(items)) => {
                for item in &items {
                    sink(situation_of(item));
                }
            }
            Ok(other) => sink(situation_of(&other)),
            Err(_) => {
                for line in text.lines() {
                    let line = line.trim();
                    if line.is_empty() {
                        continue;
                    }
                    if let Ok(v) = serde_json::from_str::<Value>(line) {
                        sink(situation_of(&v));
                    }
                }
            }
        }
        Ok(())
    }
}