source-map-php 0.1.3

CLI-first PHP code search indexer for Laravel and Hyperf repositories
Documentation
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProjectRecord {
    pub name: String,
    pub repo_path: String,
    pub index_prefix: String,
    pub framework: String,
    pub meili_host: String,
    pub last_run_id: String,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProjectRegistry {
    #[serde(default)]
    pub projects: Vec<ProjectRecord>,
}

impl ProjectRegistry {
    pub fn load(path: &Path) -> Result<Self> {
        if !path.exists() {
            return Ok(Self::default());
        }
        let raw = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
        serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))
    }

    pub fn save(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("create parent directory for {}", path.display()))?;
        }
        fs::write(path, serde_json::to_vec_pretty(self)?)
            .with_context(|| format!("write {}", path.display()))?;
        Ok(())
    }

    pub fn upsert(&mut self, record: ProjectRecord) {
        if let Some(existing) = self
            .projects
            .iter_mut()
            .find(|item| item.repo_path == record.repo_path || item.name == record.name)
        {
            *existing = record;
        } else {
            self.projects.push(record);
        }
        self.projects.sort_by(|left, right| {
            left.name
                .cmp(&right.name)
                .then(left.repo_path.cmp(&right.repo_path))
        });
    }

    pub fn resolve<'a>(&'a self, selector: &str) -> Option<&'a ProjectRecord> {
        let selector_path = canonicalized(selector);
        self.projects.iter().find(|record| {
            record.name == selector
                || record.repo_path == selector
                || selector_path
                    .as_deref()
                    .is_some_and(|path| path == record.repo_path)
        })
    }

    pub fn remove(&mut self, selector: &str) -> Option<ProjectRecord> {
        let selector_path = canonicalized(selector);
        let index = self.projects.iter().position(|record| {
            record.name == selector
                || record.repo_path == selector
                || selector_path
                    .as_deref()
                    .is_some_and(|path| path == record.repo_path)
        })?;
        Some(self.projects.remove(index))
    }
}

pub fn default_project_registry_path() -> PathBuf {
    env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("~"))
        .join(".config/meilisearch/project.json")
}

fn canonicalized(value: &str) -> Option<String> {
    fs::canonicalize(value)
        .ok()
        .map(|path| path.to_string_lossy().into_owned())
}

#[cfg(test)]
mod tests {
    use tempfile::tempdir;

    use super::{ProjectRecord, ProjectRegistry};
    use chrono::Utc;

    fn record(name: &str, repo_path: &str) -> ProjectRecord {
        ProjectRecord {
            name: name.to_string(),
            repo_path: repo_path.to_string(),
            index_prefix: name.to_string(),
            framework: "hyperf".to_string(),
            meili_host: "http://127.0.0.1:7700".to_string(),
            last_run_id: "run".to_string(),
            updated_at: Utc::now(),
        }
    }

    #[test]
    fn resolves_by_name_or_path() {
        let dir = tempdir().unwrap();
        let repo = dir.path().join("staff-api");
        std::fs::create_dir_all(&repo).unwrap();

        let mut registry = ProjectRegistry::default();
        registry.upsert(record("staff-api", &repo.to_string_lossy()));

        assert!(registry.resolve("staff-api").is_some());
        assert!(registry.resolve(&repo.to_string_lossy()).is_some());
    }

    #[test]
    fn upsert_replaces_existing_repo_entry() {
        let mut registry = ProjectRegistry::default();
        registry.upsert(record("staff-api", "/tmp/staff-api"));
        let mut updated = record("staff-api-prod", "/tmp/staff-api");
        updated.index_prefix = "staff-api-prod".to_string();
        registry.upsert(updated.clone());

        assert_eq!(registry.projects.len(), 1);
        assert_eq!(registry.projects[0].name, "staff-api-prod");
        assert_eq!(registry.projects[0].index_prefix, "staff-api-prod");
    }

    #[test]
    fn remove_deletes_matching_record() {
        let mut registry = ProjectRegistry::default();
        registry.upsert(record("staff-api", "/tmp/staff-api"));
        registry.upsert(record("front-api", "/tmp/front-api"));

        let removed = registry.remove("staff-api").unwrap();

        assert_eq!(removed.name, "staff-api");
        assert_eq!(registry.projects.len(), 1);
        assert_eq!(registry.projects[0].name, "front-api");
    }
}