Skip to main content

romance_core/
manifest.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4use std::collections::BTreeMap;
5use std::path::Path;
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct Manifest {
9    pub romance_version: String,
10    pub created_at: String,
11    pub updated_at: String,
12    pub project_name: String,
13    pub files: BTreeMap<String, FileRecord>,
14}
15
16#[derive(Debug, Serialize, Deserialize)]
17pub struct FileRecord {
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub template: Option<String>,
20    pub category: FileCategory,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub entity_name: Option<String>,
23    pub generated_hash: String,
24    pub generated_at: String,
25    pub generated_by_version: String,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29#[serde(rename_all = "snake_case")]
30pub enum FileCategory {
31    Scaffold,
32    Entity,
33    Marker,
34    Static,
35}
36
37impl Manifest {
38    pub fn new(project_name: &str, romance_version: &str) -> Self {
39        let now = chrono::Utc::now().to_rfc3339();
40        Self {
41            romance_version: romance_version.to_string(),
42            created_at: now.clone(),
43            updated_at: now,
44            project_name: project_name.to_string(),
45            files: BTreeMap::new(),
46        }
47    }
48
49    pub fn load(project_dir: &Path) -> Result<Self> {
50        let path = project_dir.join(".romance/manifest.json");
51        let content = std::fs::read_to_string(&path)?;
52        let manifest: Manifest = serde_json::from_str(&content)?;
53        Ok(manifest)
54    }
55
56    pub fn save(&self, project_dir: &Path) -> Result<()> {
57        let dir = project_dir.join(".romance");
58        std::fs::create_dir_all(&dir)?;
59        let path = dir.join("manifest.json");
60        let content = serde_json::to_string_pretty(self)?;
61        std::fs::write(&path, content)?;
62        Ok(())
63    }
64
65    pub fn exists(project_dir: &Path) -> bool {
66        project_dir.join(".romance/manifest.json").exists()
67    }
68
69    pub fn record_file(
70        &mut self,
71        output_path: &str,
72        template: Option<&str>,
73        category: FileCategory,
74        content: &str,
75        entity_name: Option<&str>,
76    ) {
77        self.files.insert(
78            output_path.to_string(),
79            FileRecord {
80                template: template.map(|s| s.to_string()),
81                category,
82                entity_name: entity_name.map(|s| s.to_string()),
83                generated_hash: content_hash(content),
84                generated_at: chrono::Utc::now().to_rfc3339(),
85                generated_by_version: self.romance_version.clone(),
86            },
87        );
88    }
89}
90
91/// Compute SHA-256 hex digest of content.
92pub fn content_hash(content: &str) -> String {
93    let mut hasher = Sha256::new();
94    hasher.update(content.as_bytes());
95    format!("sha256:{:x}", hasher.finalize())
96}