use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::paths::{atomic_write_json, graph_dir};
pub const IMPORTED_EDGES_FORMAT: &str = "flowtrace-edges";
pub const IMPORTED_EDGES_SCHEMA_VERSION: u64 = 1;
pub const IMPORTED_EDGE_KINDS: &[&str] = &["calls", "consumes", "enqueues", "publishes", "tests"];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EdgeEnd {
#[serde(default)]
pub repo: Option<String>,
#[serde(rename = "ref", default)]
pub reference: Option<String>,
#[serde(default)]
pub file: Option<String>,
#[serde(default)]
pub line: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImportRecord {
pub kind: String,
pub from: EdgeEnd,
pub to: EdgeEnd,
#[serde(default)]
pub key: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Provenance {
pub id: String,
#[serde(default)]
pub producer: String,
#[serde(rename = "formatVersion", default)]
pub format_version: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImportedEdges {
pub schema_version: u64,
pub mapped_repo: String,
pub provenance: Provenance,
pub edges: Vec<ImportRecord>,
}
pub const IMPORTED_EDGES_ARTIFACT_SCHEMA_VERSION: u64 = 1;
fn field_str<'a>(obj: &'a serde_json::Map<String, Value>, key: &str) -> Option<&'a str> {
obj.get(key).and_then(Value::as_str)
}
pub fn parse_imported_edges(bytes: &[u8], mapped_repo: &str) -> Result<ImportedEdges, String> {
let value: Value = serde_json::from_slice(bytes).map_err(|e| format!("malformed JSON: {e}"))?;
let obj = value
.as_object()
.ok_or_else(|| "malformed JSON: expected an object at the top level".to_string())?;
let format = field_str(obj, "format").unwrap_or("<missing>");
if format != IMPORTED_EDGES_FORMAT {
return Err(format!(
"unsupported format \"{format}\", expected \"{IMPORTED_EDGES_FORMAT}\""
));
}
let schema_version = obj.get("schemaVersion").and_then(Value::as_u64);
if schema_version != Some(IMPORTED_EDGES_SCHEMA_VERSION) {
let got = obj
.get("schemaVersion")
.map(ToString::to_string)
.unwrap_or_else(|| "<missing>".to_string());
return Err(format!(
"unsupported schemaVersion {got}, expected {IMPORTED_EDGES_SCHEMA_VERSION}"
));
}
let provenance_value = obj
.get("provenance")
.ok_or_else(|| "missing \"provenance\" block".to_string())?;
let provenance: Provenance = serde_json::from_value(provenance_value.clone())
.map_err(|e| format!("malformed \"provenance\" block: {e}"))?;
let edges_value = obj
.get("edges")
.and_then(Value::as_array)
.ok_or_else(|| "missing \"edges\" array".to_string())?;
let mut edges = Vec::with_capacity(edges_value.len());
for (i, raw) in edges_value.iter().enumerate() {
let rec_obj = raw
.as_object()
.ok_or_else(|| format!("edges[{i}] is not an object"))?;
let kind =
field_str(rec_obj, "kind").ok_or_else(|| format!("edges[{i}] missing \"kind\""))?;
if !IMPORTED_EDGE_KINDS.contains(&kind) {
let allowed = IMPORTED_EDGE_KINDS.join("|");
return Err(format!(
"edges[{i}] has kind \"{kind}\", not one of {allowed}"
));
}
let from_value = rec_obj
.get("from")
.ok_or_else(|| format!("edges[{i}] missing \"from\""))?;
let to_value = rec_obj
.get("to")
.ok_or_else(|| format!("edges[{i}] missing \"to\""))?;
let from: EdgeEnd = serde_json::from_value(from_value.clone())
.map_err(|e| format!("edges[{i}].from is malformed: {e}"))?;
let to: EdgeEnd = serde_json::from_value(to_value.clone())
.map_err(|e| format!("edges[{i}].to is malformed: {e}"))?;
let key = field_str(rec_obj, "key").map(str::to_string);
edges.push(ImportRecord {
kind: kind.to_string(),
from,
to,
key,
});
}
Ok(ImportedEdges {
schema_version: IMPORTED_EDGES_ARTIFACT_SCHEMA_VERSION,
mapped_repo: mapped_repo.to_string(),
provenance,
edges,
})
}
pub fn imported_edges_json_path(root: &Path) -> PathBuf {
graph_dir(root).join("imported-edges.json")
}
pub fn read_imported_edges(root: &Path) -> Option<ImportedEdges> {
let text = fs::read_to_string(imported_edges_json_path(root)).ok()?;
serde_json::from_str(&text).ok()
}
pub fn write_imported_edges(root: &Path, data: &ImportedEdges) -> io::Result<()> {
atomic_write_json(&imported_edges_json_path(root), data)
}