mdt_core 0.7.0

update markdown content anywhere using comments as template tags
Documentation
use std::collections::BTreeMap;
use std::fs::Metadata;
use std::path::Path;
use std::path::PathBuf;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;

use serde::Deserialize;
use serde::Serialize;

use crate::project::ConsumerEntry;
use crate::project::Project;
use crate::project::ProjectDiagnostic;
use crate::project::ProviderEntry;

pub(crate) const CACHE_SCHEMA_VERSION: u32 = 2;
const CACHE_FILE_NAME: &str = "index-v2.json";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct FileFingerprint {
	pub size: u64,
	pub modified_unix_ms: u64,
	pub content_hash: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CachedFileData {
	pub providers: Vec<ProviderEntry>,
	pub consumers: Vec<ConsumerEntry>,
	pub diagnostics: Vec<ProjectDiagnostic>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub(crate) struct LastScanTelemetry {
	pub timestamp_unix_ms: u64,
	pub full_project_hit: bool,
	pub reused_files: u64,
	pub reparsed_files: u64,
	pub total_files: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub(crate) struct CacheTelemetry {
	pub scan_count: u64,
	pub full_project_hit_count: u64,
	pub reused_file_count_total: u64,
	pub reparsed_file_count_total: u64,
	pub last_scan: Option<LastScanTelemetry>,
}

impl CacheTelemetry {
	pub(crate) fn record_scan(
		&mut self,
		full_project_hit: bool,
		reused_files: usize,
		reparsed_files: usize,
		total_files: usize,
	) {
		let reused_files = u64::try_from(reused_files).unwrap_or(u64::MAX);
		let reparsed_files = u64::try_from(reparsed_files).unwrap_or(u64::MAX);
		let total_files = u64::try_from(total_files).unwrap_or(u64::MAX);

		self.scan_count = self.scan_count.saturating_add(1);
		if full_project_hit {
			self.full_project_hit_count = self.full_project_hit_count.saturating_add(1);
		}
		self.reused_file_count_total = self.reused_file_count_total.saturating_add(reused_files);
		self.reparsed_file_count_total = self
			.reparsed_file_count_total
			.saturating_add(reparsed_files);
		self.last_scan = Some(LastScanTelemetry {
			timestamp_unix_ms: now_unix_ms(),
			full_project_hit,
			reused_files,
			reparsed_files,
			total_files,
		});
	}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ProjectIndexCache {
	pub schema_version: u32,
	pub project_key: String,
	pub files: BTreeMap<String, FileFingerprint>,
	pub file_data: BTreeMap<String, CachedFileData>,
	#[serde(default)]
	pub telemetry: CacheTelemetry,
	pub project: Project,
}

impl ProjectIndexCache {
	pub(crate) fn new(
		project_key: String,
		files: BTreeMap<String, FileFingerprint>,
		file_data: BTreeMap<String, CachedFileData>,
		project: Project,
	) -> Self {
		Self {
			schema_version: CACHE_SCHEMA_VERSION,
			project_key,
			files,
			file_data,
			telemetry: CacheTelemetry::default(),
			project,
		}
	}
}

fn now_unix_ms() -> u64 {
	SystemTime::now()
		.duration_since(UNIX_EPOCH)
		.map_or(0, |duration| {
			duration.as_millis().try_into().unwrap_or(u64::MAX)
		})
}

pub(crate) fn cache_path(root: &Path) -> PathBuf {
	root.join(".mdt").join("cache").join(CACHE_FILE_NAME)
}

pub(crate) fn relative_file_key(root: &Path, file: &Path) -> String {
	file.strip_prefix(root)
		.unwrap_or(file)
		.to_string_lossy()
		.replace('\\', "/")
}

pub(crate) fn build_file_fingerprint(
	metadata: &Metadata,
	content_hash: Option<u64>,
) -> FileFingerprint {
	let modified_unix_ms = metadata
		.modified()
		.ok()
		.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
		.and_then(|duration| duration.as_millis().try_into().ok())
		.unwrap_or(0);

	FileFingerprint {
		size: metadata.len(),
		modified_unix_ms,
		content_hash,
	}
}

pub(crate) fn load(root: &Path, project_key: &str) -> Option<ProjectIndexCache> {
	let cache_path = cache_path(root);
	let bytes = std::fs::read(cache_path).ok()?;
	let cache: ProjectIndexCache = serde_json::from_slice(&bytes).ok()?;

	if cache.schema_version != CACHE_SCHEMA_VERSION {
		return None;
	}

	if cache.project_key != project_key {
		return None;
	}

	Some(cache)
}

pub(crate) fn save(root: &Path, cache: &ProjectIndexCache) {
	let cache_path = cache_path(root);
	let Some(cache_dir) = cache_path.parent() else {
		return;
	};

	if std::fs::create_dir_all(cache_dir).is_err() {
		return;
	}

	let Ok(payload) = serde_json::to_vec_pretty(cache) else {
		return;
	};

	let temp_path = cache_path.with_extension(format!(
		"json.tmp-{}-{}",
		std::process::id(),
		SystemTime::now()
			.duration_since(UNIX_EPOCH)
			.map_or(0, |duration| duration.as_nanos())
	));

	if std::fs::write(&temp_path, payload).is_err() {
		return;
	}

	if std::fs::rename(&temp_path, &cache_path).is_err() {
		let _ = std::fs::remove_file(temp_path);
	}
}