use crate::Value;
use crate::exec::{Origins, Prov};
use std::collections::HashSet;
pub struct ProvCtx<'a> {
pub prov: Option<&'a Prov>,
pub resolve: &'a dyn Fn(&Origins) -> Option<kaiv::Provenance>,
}
fn no_provenance(_: &Origins) -> Option<kaiv::Provenance> {
None
}
impl<'a> ProvCtx<'a> {
pub fn none() -> ProvCtx<'static> {
ProvCtx {
prov: None,
resolve: &no_provenance,
}
}
fn part(&self, key: &str) -> Option<Prov> {
self.prov.map(|p| p.part(key))
}
fn item(&self, i: usize) -> Option<Prov> {
self.prov.map(|p| p.item(i))
}
fn with(&self, prov: Option<&'a Prov>) -> ProvCtx<'a> {
ProvCtx {
prov,
resolve: self.resolve,
}
}
fn leaf(&self) -> Option<kaiv::Provenance> {
self.prov.and_then(|p| (self.resolve)(&p.origins))
}
}
pub fn ident_of(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut dash = false;
for c in s.chars() {
if c.is_ascii_alphanumeric() || c == '_' {
out.push(c);
dash = false;
} else if !dash && !out.is_empty() {
out.push('-');
dash = true;
}
}
let trimmed = out.trim_end_matches('-').to_string();
if trimmed.is_empty() {
"value".to_string()
} else {
trimmed
}
}
pub fn unique_ident(field: &str, used: &mut HashSet<String>) -> String {
let mut id = ident_of(field);
if !used.insert(id.clone()) {
let mut k = 2;
loop {
let candidate = format!("{id}-{k}");
if used.insert(candidate.clone()) {
id = candidate;
break;
}
k += 1;
}
}
id
}
fn kaiv_scalar(v: &Value) -> (&'static str, String) {
match v {
Value::Null => ("null", String::new()),
Value::Bool(b) => ("bool", b.to_string()),
Value::Int(n) => ("int", n.to_string()),
Value::Float(f) => ("float", f.to_string()),
Value::Str(s) => ("str", s.clone()),
Value::List(_) | Value::Record(_) => ("str", v.to_json()),
Value::Instant { .. } | Value::Duration { .. } | Value::Quantity { .. } => {
("str", v.to_string())
}
}
}
fn estr(e: kaiv::PipelineError) -> String {
format!("emitting kaiv: {e}")
}
pub fn kaiv_leaf(
b: &mut kaiv::KaivBuilder,
namepath: &str,
value: &Value,
prov: Option<&kaiv::Provenance>,
) -> Result<(), String> {
match value {
Value::Quantity {
value: bv,
base,
written,
} => {
let (v, u) = written.clone().unwrap_or((*bv, base.clone()));
if b.leaf_with_unit(namepath, "float", Some(&u), &v.to_string(), prov)
.is_ok()
{
return Ok(());
}
}
Value::Duration { secs, nanos } => {
let v = *secs as f64 + *nanos as f64 / 1e9;
if b.leaf_with_unit(namepath, "float", Some("s"), &v.to_string(), prov)
.is_ok()
{
return Ok(());
}
}
Value::Instant {
secs,
nanos,
offset_min,
} => {
let ty = if offset_min.is_some() {
"std/time/datetime"
} else if *nanos == 0 && secs.rem_euclid(86400) == 0 {
"std/time/date"
} else {
"std/time/localdatetime"
};
b.declare_types("std/time").map_err(estr)?;
if b.leaf(namepath, ty, &value.to_string(), prov).is_ok() {
return Ok(());
}
}
_ => {}
}
let (t, payload) = kaiv_scalar(value);
if b.leaf(namepath, t, &payload, prov).is_err() {
b.leaf(namepath, "str", &value.to_json(), prov)
.map_err(estr)?;
}
Ok(())
}
pub fn kaiv_put(
b: &mut kaiv::KaivBuilder,
base: &str,
field: &str,
value: &Value,
prov: &ProvCtx,
used: &mut HashSet<String>,
) -> Result<(), String> {
let id = unique_ident(field, used);
match value {
Value::Record(fields) if !fields.is_empty() => {
let sub = format!("{base}/{id}");
let mut used = HashSet::new();
for (k, v) in fields {
let part = prov.part(k);
kaiv_put(b, &sub, k, v, &prov.with(part.as_ref()), &mut used)?;
}
Ok(())
}
Value::List(items) if !items.is_empty() => {
let sub = format!("{base}/@{id}");
for (n, item) in items.iter().enumerate() {
let part = prov.item(n);
let ctx = prov.with(part.as_ref());
match item {
Value::Record(fields) if !fields.is_empty() => {
let elem = format!("{sub}/{n}");
let mut used = HashSet::new();
for (k, v) in fields {
let fp = ctx.part(k);
kaiv_put(b, &elem, k, v, &ctx.with(fp.as_ref()), &mut used)?;
}
}
_ => kaiv_leaf(b, &format!("{sub}::{n}"), item, ctx.leaf().as_ref())?,
}
}
Ok(())
}
_ => kaiv_leaf(b, &format!("{base}::{id}"), value, prov.leaf().as_ref()),
}
}
pub fn document(items: &[Value]) -> Result<String, String> {
let mut b = kaiv::KaivBuilder::new();
let prov = ProvCtx::none();
for (i, v) in items.iter().enumerate() {
let base = format!("/@results/{i}");
let mut used = HashSet::new();
match v {
Value::Record(fields) => {
for (k, val) in fields {
kaiv_put(&mut b, &base, k, val, &prov, &mut used)?;
}
}
other => kaiv_put(&mut b, &base, "value", other, &prov, &mut used)?,
}
}
b.finish().map_err(estr)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn document_places_records_and_scalars() {
let items = vec![
Value::Record(vec![
("name".into(), Value::Str("Ada".into())),
(
"tags".into(),
Value::List(vec![Value::Int(1), Value::Int(2)]),
),
]),
Value::Int(7),
];
let doc = document(&items).unwrap();
assert!(doc.starts_with(".!kaiv"));
assert!(doc.contains("+:=name=Ada\n"), "record entry: {doc}");
assert!(doc.contains("/@results/0/@tags+=2\n"), "list: {doc}");
assert!(doc.contains("value=7\n"), "scalar: {doc}");
}
#[test]
fn document_keeps_units() {
let items = vec![Value::Quantity {
value: 3.5,
base: "km".into(),
written: None,
}];
let doc = document(&items).unwrap();
assert!(doc.contains("!float:km"), "unit annotation kept: {doc}");
}
}