fastc 0.1.0

A safe C-like language that compiles to C11
Documentation
//! Lock file management for reproducible builds
//!
//! The lock file (fastc.lock) records exact versions of dependencies
//! to ensure reproducible builds.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;

/// A lock file recording exact dependency versions
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Lockfile {
    /// Packages in the lock file
    #[serde(default, rename = "package")]
    pub packages: Vec<LockedPackage>,
}

/// A locked package with exact version information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockedPackage {
    /// Package name
    pub name: String,
    /// Package version
    pub version: String,
    /// Source specification
    pub source: String,
    /// Resolved Git commit hash (if applicable)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resolved: Option<String>,
    /// Dependencies of this package
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub dependencies: Vec<String>,
}

impl Lockfile {
    /// Create a new empty lockfile
    pub fn new() -> Self {
        Self::default()
    }

    /// Load a lockfile from a path
    pub fn load(path: &Path) -> Result<Self, LockfileError> {
        let content = std::fs::read_to_string(path).map_err(|e| LockfileError::Io {
            path: path.to_path_buf(),
            error: e.to_string(),
        })?;

        toml::from_str(&content).map_err(|e| LockfileError::Parse {
            path: path.to_path_buf(),
            error: e.to_string(),
        })
    }

    /// Save the lockfile to a path
    pub fn save(&self, path: &Path) -> Result<(), LockfileError> {
        let content = self.to_string();
        std::fs::write(path, content).map_err(|e| LockfileError::Io {
            path: path.to_path_buf(),
            error: e.to_string(),
        })
    }

    /// Add or update a package in the lockfile
    pub fn add_package(&mut self, pkg: LockedPackage) {
        // Remove existing entry for this package
        self.packages.retain(|p| p.name != pkg.name);
        self.packages.push(pkg);
        // Keep sorted for deterministic output
        self.packages.sort_by(|a, b| a.name.cmp(&b.name));
    }

    /// Get a package by name
    pub fn get_package(&self, name: &str) -> Option<&LockedPackage> {
        self.packages.iter().find(|p| p.name == name)
    }

    /// Check if a package is locked
    pub fn is_locked(&self, name: &str) -> bool {
        self.packages.iter().any(|p| p.name == name)
    }

    /// Create a lookup map for quick access
    pub fn as_map(&self) -> HashMap<String, &LockedPackage> {
        self.packages.iter().map(|p| (p.name.clone(), p)).collect()
    }
}

impl std::fmt::Display for Lockfile {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "# This file is auto-generated by fastc. Do not edit.")?;
        writeln!(f, "# Commit this file to version control for reproducible builds.")?;
        writeln!(f)?;

        for pkg in &self.packages {
            writeln!(f, "[[package]]")?;
            writeln!(f, "name = \"{}\"", pkg.name)?;
            writeln!(f, "version = \"{}\"", pkg.version)?;
            writeln!(f, "source = \"{}\"", pkg.source)?;
            if let Some(resolved) = &pkg.resolved {
                writeln!(f, "resolved = \"{}\"", resolved)?;
            }
            if !pkg.dependencies.is_empty() {
                write!(f, "dependencies = [")?;
                for (i, dep) in pkg.dependencies.iter().enumerate() {
                    if i > 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "\"{}\"", dep)?;
                }
                writeln!(f, "]")?;
            }
            writeln!(f)?;
        }

        Ok(())
    }
}

/// Errors that can occur with lockfiles
#[derive(Debug)]
pub enum LockfileError {
    /// IO error
    Io {
        path: std::path::PathBuf,
        error: String,
    },
    /// Parse error
    Parse {
        path: std::path::PathBuf,
        error: String,
    },
}

impl std::fmt::Display for LockfileError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LockfileError::Io { path, error } => {
                write!(f, "failed to read {}: {}", path.display(), error)
            }
            LockfileError::Parse { path, error } => {
                write!(f, "failed to parse {}: {}", path.display(), error)
            }
        }
    }
}

impl std::error::Error for LockfileError {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_lockfile_roundtrip() {
        let mut lockfile = Lockfile::new();
        lockfile.add_package(LockedPackage {
            name: "mylib".to_string(),
            version: "1.0.0".to_string(),
            source: "git+https://github.com/user/mylib?tag=v1.0.0".to_string(),
            resolved: Some("abc123def456".to_string()),
            dependencies: vec!["utils".to_string()],
        });
        lockfile.add_package(LockedPackage {
            name: "utils".to_string(),
            version: "0.5.0".to_string(),
            source: "git+https://github.com/user/utils?branch=main".to_string(),
            resolved: Some("789xyz".to_string()),
            dependencies: vec![],
        });

        let serialized = lockfile.to_string();
        let parsed: Lockfile = toml::from_str(&serialized).unwrap();

        assert_eq!(parsed.packages.len(), 2);
        assert_eq!(parsed.packages[0].name, "mylib");
        assert_eq!(parsed.packages[1].name, "utils");
    }

    #[test]
    fn test_add_package_replaces_existing() {
        let mut lockfile = Lockfile::new();

        lockfile.add_package(LockedPackage {
            name: "test".to_string(),
            version: "1.0.0".to_string(),
            source: "old".to_string(),
            resolved: None,
            dependencies: vec![],
        });

        lockfile.add_package(LockedPackage {
            name: "test".to_string(),
            version: "2.0.0".to_string(),
            source: "new".to_string(),
            resolved: None,
            dependencies: vec![],
        });

        assert_eq!(lockfile.packages.len(), 1);
        assert_eq!(lockfile.packages[0].version, "2.0.0");
    }
}