use std::path::Path;
use serde::Serialize;
use zhao_core::model::{LineageEdge, ParsedProject, Upstream};
use crate::report::Report;
#[derive(Debug, Serialize)]
pub struct RunMetadata<'a> {
#[serde(flatten)]
pub report: &'a Report,
pub lineage_edges: Vec<LineageEdgeJson>,
}
impl<'a> RunMetadata<'a> {
pub fn new(report: &'a Report, current: &ParsedProject) -> Self {
Self {
report,
lineage_edges: current.edges.iter().map(LineageEdgeJson::from).collect(),
}
}
}
#[derive(Debug, Serialize)]
pub struct LineageEdgeJson {
pub upstream: UpstreamJson,
pub downstream: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<ColumnLineageJson>,
}
impl From<&LineageEdge> for LineageEdgeJson {
fn from(edge: &LineageEdge) -> Self {
Self {
upstream: UpstreamJson::from(&edge.upstream),
downstream: edge.downstream.to_string(),
column: edge.column.as_ref().map(|column| ColumnLineageJson {
upstream_column: column.upstream_column.to_string(),
downstream_column: column.downstream_column.to_string(),
}),
}
}
}
#[derive(Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum UpstreamJson {
Node {
id: String,
},
Origin {
id: String,
},
}
impl From<&Upstream> for UpstreamJson {
fn from(upstream: &Upstream) -> Self {
match upstream {
Upstream::Node(id) => UpstreamJson::Node { id: id.to_string() },
Upstream::Origin(id) => UpstreamJson::Origin { id: id.to_string() },
}
}
}
#[derive(Debug, Serialize)]
pub struct ColumnLineageJson {
pub upstream_column: String,
pub downstream_column: String,
}
pub fn write(metadata: &RunMetadata, project_dir: &Path) -> Result<(), String> {
let dir = project_dir.join("target").join("zhao");
std::fs::create_dir_all(&dir)
.map_err(|err| format!("could not create {}: {err}", dir.display()))?;
let json = serde_json::to_string_pretty(metadata)
.map_err(|err| format!("could not serialize run metadata as JSON: {err}"))?;
let mut temp_file = tempfile::NamedTempFile::new_in(&dir)
.map_err(|err| format!("could not create a temp file in {}: {err}", dir.display()))?;
std::io::Write::write_all(&mut temp_file, json.as_bytes())
.map_err(|err| format!("could not write run metadata: {err}"))?;
let path = dir.join("run-metadata.json");
temp_file
.persist(&path)
.map_err(|err| format!("could not finalize {}: {err}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use zhao_core::model::{ColumnLineage, ColumnName, NodeId, OriginId};
#[test]
fn lineage_edge_json_carries_node_upstream_and_column_detail() {
let edge = LineageEdge {
upstream: Upstream::Node(NodeId::new("model.a")),
downstream: NodeId::new("model.b"),
column: Some(ColumnLineage {
upstream_column: ColumnName::new("id"),
downstream_column: ColumnName::new("a_id"),
}),
};
let json = LineageEdgeJson::from(&edge);
assert!(matches!(json.upstream, UpstreamJson::Node { id } if id == "model.a"));
assert_eq!(json.downstream, "model.b");
let column = json.column.expect("column detail should be present");
assert_eq!(column.upstream_column, "id");
assert_eq!(column.downstream_column, "a_id");
}
#[test]
fn lineage_edge_json_carries_origin_upstream_and_no_column_detail() {
let edge = LineageEdge {
upstream: Upstream::Origin(OriginId::new("source.raw.customers")),
downstream: NodeId::new("model.b"),
column: None,
};
let json = LineageEdgeJson::from(&edge);
assert!(
matches!(json.upstream, UpstreamJson::Origin { id } if id == "source.raw.customers")
);
assert!(json.column.is_none());
}
#[test]
fn run_metadata_flattens_report_fields_alongside_lineage_edges() {
let report = Report::new(&[], &[]);
let current = ParsedProject {
seed_node_ids: Default::default(),
nodes: Vec::new(),
origins: Vec::new(),
edges: vec![LineageEdge {
upstream: Upstream::Node(NodeId::new("model.a")),
downstream: NodeId::new("model.b"),
column: None,
}],
};
let metadata = RunMetadata::new(&report, ¤t);
let json = serde_json::to_value(&metadata).expect("should serialize");
assert!(json.get("changes").is_some(), "{json}");
assert!(json.get("findings").is_some(), "{json}");
assert_eq!(
json.get("lineage_edges")
.and_then(|edges| edges.as_array())
.map(Vec::len),
Some(1)
);
}
}