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
91pub 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}