prebindgen 0.4.1

Separate FFI implementation and language-specific binding into different crates
Documentation
//! Serialization utilities for reading and writing records.

use std::{
    borrow::Borrow,
    fs::{self, OpenOptions},
    io::Write,
    path::Path,
};

use crate::api::record::Record;

/// Write a collection of records to a file in JSON-lines format
pub fn write_to_jsonl_file<P: AsRef<Path>, R: Borrow<Record>>(
    file_path: P,
    records: &[R],
) -> Result<(), Box<dyn std::error::Error>> {
    let Ok(mut file) = OpenOptions::new().create(true).append(true).open(file_path) else {
        return Err("Failed to open file".into());
    };
    // Check if file is empty (just created or was deleted)
    for record in records {
        let json_line = record.borrow().to_jsonl_string()?;
        writeln!(file, "{json_line}")?;
    }
    file.flush()?;
    Ok(())
}

/// Read records from a JSON-lines file
pub fn read_jsonl_file<P: AsRef<Path>>(
    file_path: P,
) -> Result<Vec<Record>, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(&file_path)?;
    let mut records = Vec::new();

    // Parse JSON-lines format: each line is a separate JSON object
    for (line_num, line) in content.lines().enumerate() {
        let line = line.trim();
        if line.is_empty() {
            continue; // Skip empty lines
        }

        let record: Record = serde_json::from_str(line)
            .map_err(|e| format!("{}:{}: {}", file_path.as_ref().display(), line_num + 1, e))?;

        records.push(record);
    }

    Ok(records)
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::NamedTempFile;

    use super::*;
    use crate::api::record::RecordKind;

    #[test]
    fn test_jsonl_round_trip() {
        // Create some test records
        let records = vec![
            Record::new(
                RecordKind::Struct,
                "TestStruct".to_string(),
                "pub struct TestStruct { }".to_string(),
                Default::default(),
                None,
            ),
            Record::new(
                RecordKind::Function,
                "test_func".to_string(),
                "pub fn test_func() { }".to_string(),
                Default::default(),
                None,
            ),
            Record::new(
                RecordKind::Enum,
                "TestEnum".to_string(),
                "pub enum TestEnum { A, B }".to_string(),
                Default::default(),
                None,
            ),
        ];

        // Create a temporary file
        let temp_file = NamedTempFile::new().unwrap();
        let temp_path = temp_file.path();

        // Write records to JSONL file
        write_to_jsonl_file(temp_path, &records).unwrap();

        // Read records back
        let loaded_records = read_jsonl_file(temp_path).unwrap();

        // Verify they match
        assert_eq!(records.len(), loaded_records.len());
        for (original, loaded) in records.iter().zip(loaded_records.iter()) {
            assert_eq!(original.kind, loaded.kind);
            assert_eq!(original.name, loaded.name);
            assert_eq!(original.content, loaded.content);
        }
    }

    #[test]
    fn test_jsonl_file_format() {
        // Create a test record
        let record = Record::new(
            RecordKind::Struct,
            "Test".to_string(),
            "pub struct Test { }".to_string(),
            Default::default(),
            None,
        );

        // Create a temporary file
        let temp_file = NamedTempFile::new().unwrap();
        let temp_path = temp_file.path();

        // Write record to JSONL file
        write_to_jsonl_file(temp_path, &[record]).unwrap();

        // Read raw content and verify format
        let content = fs::read_to_string(temp_path).unwrap();
        let lines: Vec<&str> = content.lines().collect();

        // Should have exactly one line
        assert_eq!(lines.len(), 1);

        // Line should be valid JSON
        let parsed: Record = serde_json::from_str(lines[0]).unwrap();
        assert_eq!(parsed.name, "Test");
        assert_eq!(parsed.kind, RecordKind::Struct);

        // Test record with cfg feature field
        let record_with_cfg = Record::new(
            RecordKind::Function,
            "test_func".to_string(),
            "pub fn test_func() { }".to_string(),
            Default::default(),
            Some("feature = \"unstable\"".to_string()),
        );

        write_to_jsonl_file(temp_path, &[record_with_cfg]).unwrap();
        let content = fs::read_to_string(temp_path).unwrap();
        let lines: Vec<&str> = content.lines().collect();
        dbg!(&lines);
        assert_eq!(lines.len(), 2);

        let parsed: Record = serde_json::from_str(lines[1]).unwrap();
        assert_eq!(parsed.cfg, Some("feature = \"unstable\"".to_string()));
    }
}