use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
const SUPPORTED_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranspilerMap {
pub version: u32,
pub source_language: String,
pub source_file: String,
pub generated_file: String,
pub mappings: Vec<SourceMapping>,
pub function_map: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceMapping {
pub rust_line: usize,
pub rust_function: String,
pub python_line: usize,
pub python_function: String,
pub python_context: String,
}
impl TranspilerMap {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path_ref = path.as_ref();
if !path_ref.exists() {
bail!("Source map file not found: {}", path_ref.display());
}
let contents = fs::read_to_string(path_ref).context("Failed to read source map file")?;
let map: TranspilerMap =
serde_json::from_str(&contents).context("Invalid source map JSON")?;
if map.version != SUPPORTED_VERSION {
bail!(
"Unsupported source map version: {} (expected {})",
map.version,
SUPPORTED_VERSION
);
}
if map.source_language.is_empty() {
bail!("Invalid source map: missing source_language");
}
if map.source_file.is_empty() {
bail!("Invalid source map: missing source_file");
}
Ok(map)
}
pub fn lookup_line(&self, rust_line: usize) -> Option<&SourceMapping> {
self.mappings.iter().find(|m| m.rust_line == rust_line)
}
pub fn lookup_function(&self, rust_fn: &str) -> Option<&str> {
self.function_map.get(rust_fn).map(String::as_str)
}
pub fn source_language(&self) -> &str {
&self.source_language
}
pub fn source_file(&self) -> &Path {
Path::new(&self.source_file)
}
pub fn generated_file(&self) -> &Path {
Path::new(&self.generated_file)
}
pub fn mapping_count(&self) -> usize {
self.mappings.len()
}
pub fn function_mapping_count(&self) -> usize {
self.function_map.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_temp_source_map(content: &str) -> NamedTempFile {
let mut file = NamedTempFile::new().expect("test");
file.write_all(content.as_bytes()).expect("test");
file.flush().expect("test");
file
}
#[test]
fn test_parse_valid_source_map() {
let map_json = r#"{
"version": 1,
"source_language": "python",
"source_file": "test.py",
"generated_file": "test.rs",
"mappings": [
{
"rust_line": 10,
"rust_function": "main",
"python_line": 5,
"python_function": "main",
"python_context": "def main():"
}
],
"function_map": {
"main": "main"
}
}"#;
let temp_file = create_temp_source_map(map_json);
let map = TranspilerMap::from_file(temp_file.path()).expect("test");
assert_eq!(map.version, 1);
assert_eq!(map.source_language, "python");
assert_eq!(map.source_file, "test.py");
assert_eq!(map.generated_file, "test.rs");
assert_eq!(map.mappings.len(), 1);
assert_eq!(map.function_map.len(), 1);
}
#[test]
fn test_lookup_line() {
let map_json = r#"{
"version": 1,
"source_language": "python",
"source_file": "test.py",
"generated_file": "test.rs",
"mappings": [
{
"rust_line": 192,
"rust_function": "process_data",
"python_line": 143,
"python_function": "process_data",
"python_context": "x = position[0]"
}
],
"function_map": {}
}"#;
let temp_file = create_temp_source_map(map_json);
let map = TranspilerMap::from_file(temp_file.path()).expect("test");
let mapping = map.lookup_line(192).expect("test");
assert_eq!(mapping.python_line, 143);
assert_eq!(mapping.python_function, "process_data");
assert!(map.lookup_line(999).is_none());
}
#[test]
fn test_lookup_function() {
let map_json = r#"{
"version": 1,
"source_language": "python",
"source_file": "test.py",
"generated_file": "test.rs",
"mappings": [],
"function_map": {
"_cse_temp_0": "temporary for: len(data) > 0",
"calculate_distance": "calculate_distance"
}
}"#;
let temp_file = create_temp_source_map(map_json);
let map = TranspilerMap::from_file(temp_file.path()).expect("test");
assert_eq!(
map.lookup_function("_cse_temp_0").expect("test"),
"temporary for: len(data) > 0"
);
assert_eq!(map.lookup_function("calculate_distance").expect("test"), "calculate_distance");
assert!(map.lookup_function("nonexistent").is_none());
}
#[test]
fn test_invalid_json() {
let invalid_json = "{ this is not valid json }";
let temp_file = create_temp_source_map(invalid_json);
let result = TranspilerMap::from_file(temp_file.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid source map JSON"));
}
#[test]
fn test_missing_file() {
let result = TranspilerMap::from_file("/nonexistent/path.json");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Source map file not found"));
}
#[test]
fn test_unsupported_version() {
let map_json = r#"{
"version": 999,
"source_language": "python",
"source_file": "test.py",
"generated_file": "test.rs",
"mappings": [],
"function_map": {}
}"#;
let temp_file = create_temp_source_map(map_json);
let result = TranspilerMap::from_file(temp_file.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Unsupported source map version"));
}
#[test]
fn test_empty_mappings() {
let map_json = r#"{
"version": 1,
"source_language": "python",
"source_file": "empty.py",
"generated_file": "empty.rs",
"mappings": [],
"function_map": {}
}"#;
let temp_file = create_temp_source_map(map_json);
let map = TranspilerMap::from_file(temp_file.path()).expect("test");
assert_eq!(map.mapping_count(), 0);
assert_eq!(map.function_mapping_count(), 0);
}
#[test]
fn test_getters() {
let map_json = r#"{
"version": 1,
"source_language": "typescript",
"source_file": "app.ts",
"generated_file": "app.rs",
"mappings": [
{
"rust_line": 10,
"rust_function": "main",
"python_line": 5,
"python_function": "main",
"python_context": "function main()"
}
],
"function_map": {
"main": "main"
}
}"#;
let temp_file = create_temp_source_map(map_json);
let map = TranspilerMap::from_file(temp_file.path()).expect("test");
assert_eq!(map.source_language(), "typescript");
assert_eq!(map.source_file(), Path::new("app.ts"));
assert_eq!(map.generated_file(), Path::new("app.rs"));
assert_eq!(map.mapping_count(), 1);
assert_eq!(map.function_mapping_count(), 1);
}
#[test]
fn test_c_source_language_decy() {
let map_json = r#"{
"version": 1,
"source_language": "c",
"source_file": "algorithm.c",
"generated_file": "algorithm.rs",
"mappings": [
{
"rust_line": 45,
"rust_function": "sort_array",
"python_line": 23,
"python_function": "sort_array",
"python_context": "for (int i = 0; i < n; i++)"
}
],
"function_map": {
"sort_array": "sort_array",
"_decy_temp_0": "temporary: sizeof(struct data)"
}
}"#;
let temp_file = create_temp_source_map(map_json);
let map = TranspilerMap::from_file(temp_file.path()).expect("test");
assert_eq!(map.source_language(), "c");
assert_eq!(map.source_file(), Path::new("algorithm.c"));
assert_eq!(map.generated_file(), Path::new("algorithm.rs"));
assert_eq!(map.mapping_count(), 1);
assert_eq!(map.function_mapping_count(), 2);
let mapping = map.lookup_line(45).expect("test");
assert_eq!(mapping.python_line, 23);
assert_eq!(mapping.python_function, "sort_array");
assert_eq!(
map.lookup_function("_decy_temp_0").expect("test"),
"temporary: sizeof(struct data)"
);
}
}