use serde::Serialize;
use crate::freshness::record::RECORD_VERSION;
use crate::graph::GRAPH_SCHEMA_VERSION;
use super::snapshot::Snapshot;
pub(crate) const FORMAT: &str = "devscout-facts";
pub(crate) const SCHEMA_VERSION: u32 = 1;
const EXTRACTED: &str = "extracted";
const NOT_EXTRACTED: &str = "not-extracted";
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FileDigest {
path: String,
digest: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SourceProvenance {
indexed_head: Option<String>,
scope: Vec<String>,
dirty_at_map: Vec<FileDigest>,
file_count: u64,
file_digests: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct AnalysisProvenance {
devscout_version: &'static str,
graph_schema_version: u32,
fragment_generation: u32,
index_state_version: u32,
config_inputs: Vec<FileDigest>,
compiler_facts: String,
semantic_lane: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct IndexProvenance {
generation: String,
indexed_at: String,
}
#[derive(Serialize)]
struct Provenance {
source: SourceProvenance,
analysis: AnalysisProvenance,
index: IndexProvenance,
}
#[derive(Serialize, Clone, Copy)]
pub(super) struct LanguageCoverage {
csharp: &'static str,
typescript: &'static str,
rust: &'static str,
}
#[derive(Serialize)]
pub(super) struct Coverage {
definition: LanguageCoverage,
edge: LanguageCoverage,
#[serde(rename = "publish-site")]
publish_site: LanguageCoverage,
#[serde(rename = "handler-registration")]
handler_registration: LanguageCoverage,
#[serde(rename = "di-registration")]
di_registration: LanguageCoverage,
#[serde(rename = "unresolved-import")]
unresolved_import: LanguageCoverage,
#[serde(rename = "test-file")]
test_file: LanguageCoverage,
#[serde(rename = "transport-address")]
transport_address: LanguageCoverage,
#[serde(rename = "http-route")]
http_route: LanguageCoverage,
#[serde(rename = "http-client")]
http_client: LanguageCoverage,
}
pub(super) fn coverage() -> Coverage {
let all = LanguageCoverage {
csharp: EXTRACTED,
typescript: EXTRACTED,
rust: EXTRACTED,
};
let csharp_only = LanguageCoverage {
csharp: EXTRACTED,
typescript: NOT_EXTRACTED,
rust: NOT_EXTRACTED,
};
let typescript_only = LanguageCoverage {
csharp: NOT_EXTRACTED,
typescript: EXTRACTED,
rust: NOT_EXTRACTED,
};
let none = LanguageCoverage {
csharp: NOT_EXTRACTED,
typescript: NOT_EXTRACTED,
rust: NOT_EXTRACTED,
};
Coverage {
definition: all,
edge: all,
publish_site: csharp_only,
handler_registration: csharp_only,
di_registration: csharp_only,
unresolved_import: typescript_only,
test_file: all,
transport_address: csharp_only,
http_route: none,
http_client: none,
}
}
fn spelled(digest: String) -> String {
format!("sha256:{digest}")
}
fn filed(path: &str, digest: Option<&String>) -> FileDigest {
FileDigest {
path: path.to_string(),
digest: digest.map(|d| spelled(d.clone())),
}
}
fn source_provenance(snapshot: &Snapshot) -> SourceProvenance {
let record = &snapshot.record;
let config_inputs = &record.config_inputs;
let dirty_at_map = record
.dirty_at_map
.clone()
.unwrap_or_default()
.iter()
.map(|path| {
let digest = snapshot
.digests
.files
.get(path)
.or_else(|| config_inputs.get(path));
filed(path, digest)
})
.collect();
let pairs: Vec<(String, String)> = snapshot
.digests
.files
.iter()
.map(|(path, digest)| (path.clone(), spelled(digest.clone())))
.collect();
let table = serde_json::to_string(&pairs).expect("a digest table always serializes");
SourceProvenance {
indexed_head: record.indexed_head.clone(),
scope: record.scope.clone(),
dirty_at_map,
file_count: record.file_count,
file_digests: spelled(crate::freshness::record::sha256_hex(table.as_bytes())),
}
}
fn analysis_provenance(snapshot: &Snapshot) -> AnalysisProvenance {
AnalysisProvenance {
devscout_version: env!("CARGO_PKG_VERSION"),
graph_schema_version: GRAPH_SCHEMA_VERSION,
fragment_generation: snapshot.fragment_generation,
index_state_version: RECORD_VERSION,
config_inputs: snapshot
.record
.config_inputs
.iter()
.map(|(path, digest)| filed(path, Some(digest)))
.collect(),
compiler_facts: snapshot
.compiler_facts
.clone()
.unwrap_or_else(|| "none".to_string()),
semantic_lane: snapshot
.graph
.stats
.lane
.as_ref()
.map_or_else(|| "none".to_string(), |lane| lane.state().to_string()),
}
}
fn provenance(snapshot: &Snapshot) -> Provenance {
Provenance {
source: source_provenance(snapshot),
analysis: analysis_provenance(snapshot),
index: IndexProvenance {
generation: snapshot.record.generation.clone(),
indexed_at: snapshot.record.indexed_at.clone(),
},
}
}
pub(crate) fn line(snapshot: &Snapshot) -> String {
format!(
"{{\"format\":{},\"schemaVersion\":{SCHEMA_VERSION},\"provenance\":{},\"coverage\":{},\"freshness\":{}}}",
serde_json::to_string(FORMAT).expect("a constant string always serializes"),
serde_json::to_string(&provenance(snapshot)).expect("provenance always serializes"),
serde_json::to_string(&coverage()).expect("the coverage table always serializes"),
crate::cli::freshness_json(&snapshot.freshness).to_json_string(),
)
}