1use std::path::{Path, PathBuf};
22
23use tracing::{debug, info};
24
25use crate::commit::{Change, Commit};
26use crate::error::NapError;
27use crate::manifest::Manifest;
28use crate::resolver::ResolveConfig;
29use crate::types::EntityType;
30use crate::uri::NapUri;
31use crate::vcs::VcsBackend;
32
33pub const ENTITY_TYPE_MARKER: &str = ".entity-type";
35
36pub struct Repository {
38 pub root: PathBuf,
40 pub repository: String,
42 vcs: Box<dyn VcsBackend>,
44}
45
46impl Repository {
47 pub fn open(path: &Path, vcs: Box<dyn VcsBackend>) -> Result<Self, NapError> {
49 if !path.join("repository.yaml").exists() && !path.join("repository.yaml").exists() {
51 return Err(NapError::RepositoryNotFound(path.display().to_string()));
52 }
53
54 let repository = path
55 .file_name()
56 .and_then(|n| n.to_str())
57 .unwrap_or("unknown")
58 .to_string();
59
60 debug!(
61 path = %path.display(),
62 repository = %repository,
63 "opened NAP repository"
64 );
65
66 Ok(Self {
67 root: path.to_path_buf(),
68 repository,
69 vcs,
70 })
71 }
72
73 pub fn read_resolve_config(&self) -> ResolveConfig {
75 let repo_yaml_path = self.root.join("repository.yaml");
77 let universe_yaml_path = self.root.join("repository.yaml");
78 let config_path = if repo_yaml_path.exists() {
79 repo_yaml_path
80 } else if universe_yaml_path.exists() {
81 universe_yaml_path
82 } else {
83 debug!("no repository.yaml or repository.yaml found, using default ResolveConfig");
84 return ResolveConfig::default();
85 };
86
87 let yaml_content = match std::fs::read_to_string(&config_path) {
88 Ok(content) => content,
89 Err(e) => {
90 debug!(
91 path = %config_path.display(),
92 error = %e,
93 "failed to read config, using default ResolveConfig"
94 );
95 return ResolveConfig::default();
96 }
97 };
98
99 let parsed: serde_yaml::Value = match serde_yaml::from_str(&yaml_content) {
101 Ok(value) => value,
102 Err(e) => {
103 debug!(
104 path = %config_path.display(),
105 error = %e,
106 "failed to parse config, using default ResolveConfig"
107 );
108 return ResolveConfig::default();
109 }
110 };
111
112 let default_branch = parsed
114 .get("metadata")
115 .and_then(|metadata| metadata.get("nap"))
116 .and_then(|nap| nap.get("default_branch"))
117 .and_then(|branch| branch.as_str())
118 .map(|s| s.to_string());
119
120 if default_branch.is_none() {
122 debug!(
123 path = %config_path.display(),
124 "nap metadata not found, auto-creating with default_branch = 'main'"
125 );
126
127 let mut manifest = match Manifest::from_file(&config_path) {
129 Ok(m) => m,
130 Err(e) => {
131 debug!(
132 path = %config_path.display(),
133 error = %e,
134 "failed to read manifest, using default ResolveConfig"
135 );
136 return ResolveConfig::default();
137 }
138 };
139
140 manifest.metadata.insert(
142 "nap".to_string(),
143 serde_yaml::to_value(serde_json::json!({
144 "default_branch": "main"
145 }))
146 .unwrap(),
147 );
148
149 if let Err(e) = manifest.to_file(&config_path) {
151 debug!(
152 path = %config_path.display(),
153 error = %e,
154 "failed to write nap metadata, using default ResolveConfig"
155 );
156 return ResolveConfig::default();
157 }
158
159 return ResolveConfig {
160 default_branch: Some("main".to_string()),
161 };
162 }
163
164 ResolveConfig { default_branch }
165 }
166
167 pub fn init(path: &Path, repository: &str, vcs: Box<dyn VcsBackend>) -> Result<Self, NapError> {
169 let repo_root = path.to_path_buf();
170 if repo_root.join("repository.yaml").exists() || repo_root.join("repository.yaml").exists()
171 {
172 return Err(NapError::RepositoryAlreadyExists(
173 repo_root.display().to_string(),
174 ));
175 }
176
177 info!(
178 path = %repo_root.display(),
179 repository = %repository,
180 "initializing NAP repository"
181 );
182
183 std::fs::create_dir_all(&repo_root)?;
185
186 let mut repo_manifest = Manifest::new(
188 repository,
189 EntityType::new("world"),
190 repository,
191 &format!("{repository} Repository"),
192 );
193
194 repo_manifest.metadata.insert(
196 "nap".to_string(),
197 serde_yaml::to_value(serde_json::json!({
198 "default_branch": "main"
199 }))
200 .unwrap(),
201 );
202
203 repo_manifest.to_file(&repo_root.join("repository.yaml"))?;
204
205 vcs.init(&repo_root)?;
207
208 vcs.commit(
210 &repo_root,
211 &format!("Initialize {repository} repository"),
212 "nap-init",
213 )?;
214
215 info!(
216 path = %repo_root.display(),
217 repository = %repository,
218 "NAP repository initialized successfully"
219 );
220
221 Ok(Self {
222 root: repo_root,
223 repository: repository.to_string(),
224 vcs,
225 })
226 }
227
228 pub fn manifest_path(&self, entity_type: &EntityType, entity_id: &str) -> PathBuf {
230 let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
231 self.root.join(uri.manifest_path())
232 }
233
234 pub fn read_manifest(
236 &self,
237 entity_type: &EntityType,
238 entity_id: &str,
239 ) -> Result<Manifest, NapError> {
240 let path = self.manifest_path(entity_type, entity_id);
241 debug!(
242 path = %path.display(),
243 entity_type = %entity_type,
244 entity_id = %entity_id,
245 "reading manifest"
246 );
247 Manifest::from_file(&path)
248 }
249
250 pub fn read_manifest_at_ref(
252 &self,
253 entity_type: &EntityType,
254 entity_id: &str,
255 reference: &str,
256 ) -> Result<Manifest, NapError> {
257 let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
258 let file_path = uri.manifest_path();
259
260 debug!(
261 file_path = %file_path,
262 reference = %reference,
263 "reading manifest at ref"
264 );
265
266 let content = self
267 .vcs
268 .read_file_at_ref(&self.root, &file_path, Some(reference))?;
269 Manifest::from_yaml(&content)
270 }
271
272 pub fn write_manifest(&self, manifest: &Manifest) -> Result<PathBuf, NapError> {
274 let uri: NapUri = manifest.id.parse()?;
275 let entity_type = uri.entity_type.clone();
276
277 self.ensure_entity_type_dir(&entity_type)?;
279
280 let path = self.root.join(uri.manifest_path());
281
282 debug!(
283 path = %path.display(),
284 manifest_id = %manifest.id,
285 "writing manifest"
286 );
287
288 manifest.to_file(&path)?;
289 Ok(path)
290 }
291
292 fn ensure_entity_type_dir(&self, entity_type: &EntityType) -> Result<(), NapError> {
294 let dir = self.root.join(entity_type.directory_name());
295 if !dir.exists() {
296 std::fs::create_dir_all(&dir)?;
297 let marker = dir.join(ENTITY_TYPE_MARKER);
299 std::fs::write(&marker, "")?;
300 debug!(
301 entity_type = %entity_type,
302 path = %dir.display(),
303 "created entity type directory with marker"
304 );
305 }
306 Ok(())
307 }
308
309 pub fn create_entity(
311 &self,
312 entity_type: &EntityType,
313 entity_id: &str,
314 name: &str,
315 author: &str,
316 ) -> Result<(Manifest, String), NapError> {
317 self.ensure_entity_type_dir(entity_type)?;
319
320 let mut manifest = Manifest::new(&self.repository, entity_type.clone(), entity_id, name);
321
322 let path = self.manifest_path(entity_type, entity_id);
324 if path.exists() {
325 return Err(NapError::Other(format!(
326 "entity '{entity_id}' of type '{entity_type}' already exists"
327 )));
328 }
329
330 crate::schema::validate_manifest(&manifest)
332 .map_err(|errors| NapError::ManifestValidationError(errors.join("; ")))?;
333
334 self.write_manifest(&manifest)?;
336
337 let commit_message = format!("Create {entity_type} '{name}' ({entity_id})");
339 let commit_hash = self.vcs.commit(&self.root, &commit_message, author)?;
340
341 manifest.head = Some(commit_hash.clone());
343 manifest.bump_version();
344 self.write_manifest(&manifest)?;
345
346 info!(
347 manifest_id = %manifest.id,
348 commit_hash = %commit_hash,
349 "created entity"
350 );
351
352 Ok((manifest, commit_hash))
353 }
354
355 pub fn commit_manifest(
357 &self,
358 manifest: &mut Manifest,
359 message: &str,
360 author: &str,
361 changes: Vec<Change>,
362 ) -> Result<Commit, NapError> {
363 crate::schema::validate_manifest(manifest)
365 .map_err(|errors| NapError::ManifestValidationError(errors.join("; ")))?;
366
367 manifest.bump_version();
369
370 self.write_manifest(manifest)?;
372
373 let manifest_hash = manifest.content_hash()?.as_str().to_string();
375
376 let vcs_hash = self.vcs.commit(&self.root, message, author)?;
378
379 let nap_commit = Commit::new(
381 manifest.head.clone(),
382 author,
383 message,
384 &manifest_hash,
385 changes,
386 );
387
388 manifest.head = Some(vcs_hash.clone());
391 self.write_manifest(manifest)?;
392
393 debug!(
394 manifest_id = %manifest.id,
395 version = manifest.version,
396 nap_commit_id = %nap_commit.id,
397 vcs_hash = %vcs_hash,
398 "manifest committed"
399 );
400
401 Ok(nap_commit)
402 }
403
404 pub fn history(
406 &self,
407 entity_type: &EntityType,
408 entity_id: &str,
409 limit: usize,
410 ) -> Result<Vec<crate::vcs::CommitInfo>, NapError> {
411 let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
412 let file_path = uri.manifest_path();
413 self.vcs.log(&self.root, Some(&file_path), limit)
414 }
415
416 pub fn list_entities(&self, entity_type: &EntityType) -> Result<Vec<String>, NapError> {
418 let dir = self.root.join(entity_type.directory_name());
419 if !dir.exists() {
420 return Err(NapError::EntityTypeNotFound(entity_type.to_string()));
421 }
422
423 let mut entities = Vec::new();
424 for entry in std::fs::read_dir(&dir)? {
425 let entry = entry?;
426 let path = entry.path();
427 if path.extension().and_then(|e| e.to_str()) == Some("yaml")
428 && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
429 {
430 entities.push(stem.to_string());
431 }
432 }
433 entities.sort();
434 Ok(entities)
435 }
436
437 pub fn list_entity_types(&self) -> Result<Vec<EntityType>, NapError> {
443 let mut types = Vec::new();
444 for entry in std::fs::read_dir(&self.root)? {
445 let entry = entry?;
446 let path = entry.path();
447 if !path.is_dir() {
448 continue;
449 }
450 let dir_name = match path.file_name().and_then(|n| n.to_str()) {
452 Some(name) => name,
453 None => continue,
454 };
455 if dir_name.starts_with('.') || dir_name == "target" {
456 continue;
457 }
458
459 if path.join(ENTITY_TYPE_MARKER).exists() {
461 types.push(EntityType::new(dir_name));
462 continue;
463 }
464
465 let has_yaml = std::fs::read_dir(&path)
467 .ok()
468 .and_then(|entries| {
469 entries
470 .filter_map(|e| e.ok())
471 .find(|e| e.path().extension().and_then(|ext| ext.to_str()) == Some("yaml"))
472 .map(|_| true)
473 })
474 .unwrap_or(false);
475
476 if has_yaml {
477 types.push(EntityType::new(dir_name));
478 }
479 }
480 types.sort_by(|a, b| a.as_str().cmp(b.as_str()));
481 Ok(types)
482 }
483
484 pub fn delete_entity(
486 &self,
487 entity_type: &EntityType,
488 entity_id: &str,
489 author: &str,
490 ) -> Result<String, NapError> {
491 let path = self.manifest_path(entity_type, entity_id);
492 if !path.exists() {
493 return Err(NapError::ManifestNotFound(path.display().to_string()));
494 }
495
496 std::fs::remove_file(&path)?;
497
498 let message = format!("Delete {entity_type} '{entity_id}'");
499 let hash = self.vcs.commit(&self.root, &message, author)?;
500 info!(entity_type = %entity_type, entity_id = %entity_id, "deleted entity");
501 Ok(hash)
502 }
503
504 pub fn create_branch(&self, name: &str) -> Result<(), NapError> {
506 self.vcs.create_branch(&self.root, name)
507 }
508
509 pub fn switch_branch(&self, name: &str) -> Result<(), NapError> {
511 self.vcs.switch_branch(&self.root, name)
512 }
513
514 pub fn list_branches(&self) -> Result<Vec<String>, NapError> {
516 self.vcs.list_branches(&self.root)
517 }
518
519 pub fn revert_commit(&self, commit_hash: &str, author: &str) -> Result<String, NapError> {
521 let new_hash = self.vcs.revert(&self.root, commit_hash)?;
522
523 for entity_type in self.list_entity_types()? {
525 if let Ok(ids) = self.list_entities(&entity_type) {
526 for id in &ids {
527 if let Ok(mut manifest) = self.read_manifest(&entity_type, id) {
528 manifest.head = Some(new_hash.clone());
529 self.write_manifest(&manifest)?;
530 }
531 }
532 }
533 }
534
535 info!(
536 commit = %commit_hash,
537 revert = %new_hash,
538 author = %author,
539 "commit reverted"
540 );
541
542 Ok(new_hash)
543 }
544
545 pub fn head_hash(&self) -> Result<String, NapError> {
547 self.vcs.head_hash(&self.root)
548 }
549
550 pub fn resolve_branch_head(&self, branch: &str) -> Result<String, NapError> {
552 self.vcs.resolve_branch_head(&self.root, branch)
553 }
554
555 pub fn add_remote(&self, name: &str, url: &str) -> Result<(), NapError> {
559 self.vcs.add_remote(&self.root, name, url)
560 }
561
562 pub fn remove_remote(&self, name: &str) -> Result<(), NapError> {
564 self.vcs.remove_remote(&self.root, name)
565 }
566
567 pub fn list_remotes(&self) -> Result<Vec<(String, String)>, NapError> {
569 self.vcs.list_remotes(&self.root)
570 }
571
572 pub fn push(&self, remote: Option<&str>, branch: Option<&str>) -> Result<(), NapError> {
574 self.vcs.push(&self.root, remote, branch)
575 }
576
577 pub fn pull(&self, remote: Option<&str>, branch: Option<&str>) -> Result<(), NapError> {
579 self.vcs.pull(&self.root, remote, branch)
580 }
581
582 pub fn vcs(&self) -> &dyn VcsBackend {
584 self.vcs.as_ref()
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use crate::test_utils::{MockBackend, mock_repo};
592 use tempfile::TempDir;
593
594 #[test]
595 fn test_mock_backend_contract() {
596 crate::test_utils::contract::run_repository_contract(MockBackend::new());
597 }
598
599 #[test]
600 fn test_init_creates_structure() {
601 let tmp = TempDir::new().unwrap();
602 let repo = mock_repo(&tmp);
603
604 assert!(repo.root.join("repository.yaml").exists());
605 }
606
607 #[test]
608 fn test_create_and_read_entity() {
609 let tmp = TempDir::new().unwrap();
610 let repo = mock_repo(&tmp);
611
612 let (manifest, _hash) = repo
613 .create_entity(
614 &EntityType::new("character"),
615 "hero",
616 "The Hero",
617 "test-author",
618 )
619 .unwrap();
620
621 assert_eq!(manifest.name, "The Hero");
622 assert_eq!(manifest.entity_type.as_str(), "character");
623
624 let read_back = repo
626 .read_manifest(&EntityType::new("character"), "hero")
627 .unwrap();
628 assert_eq!(read_back.name, "The Hero");
629 }
630
631 #[test]
632 fn test_create_entity_auto_creates_type_directory() {
633 let tmp = TempDir::new().unwrap();
634 let repo = mock_repo(&tmp);
635
636 repo.create_entity(&EntityType::new("pokemon"), "pikachu", "Pikachu", "test")
638 .unwrap();
639
640 assert!(repo.root.join("pokemon").exists());
642 assert!(repo.root.join("pokemon").join(".entity-type").exists());
643 assert!(repo.root.join("pokemon/pikachu.yaml").exists());
644 }
645
646 #[test]
647 fn test_list_entity_types() {
648 let tmp = TempDir::new().unwrap();
649 let repo = mock_repo(&tmp);
650
651 repo.create_entity(&EntityType::new("character"), "hero", "Hero", "author")
653 .unwrap();
654 repo.create_entity(&EntityType::new("location"), "village", "Village", "author")
655 .unwrap();
656 repo.create_entity(&EntityType::new("pokemon"), "pikachu", "Pikachu", "author")
657 .unwrap();
658
659 let types = repo.list_entity_types().unwrap();
660 assert!(types.contains(&EntityType::new("character")));
661 assert!(types.contains(&EntityType::new("location")));
662 assert!(types.contains(&EntityType::new("pokemon")));
663 }
664
665 #[test]
666 fn test_list_entities() {
667 let tmp = TempDir::new().unwrap();
668 let repo = mock_repo(&tmp);
669
670 repo.create_entity(&EntityType::new("character"), "alice", "Alice", "author")
671 .unwrap();
672 repo.create_entity(&EntityType::new("character"), "bob", "Bob", "author")
673 .unwrap();
674
675 let chars = repo.list_entities(&EntityType::new("character")).unwrap();
676 assert_eq!(chars, vec!["alice", "bob"]);
677 }
678
679 #[test]
680 fn test_commit_manifest_updates() {
681 let tmp = TempDir::new().unwrap();
682 let repo = mock_repo(&tmp);
683
684 let (mut manifest, _) = repo
685 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
686 .unwrap();
687
688 manifest.set_property("species", serde_yaml::Value::String("elf".to_string()));
690 let changes = vec![Change::set("properties.species", None, "elf".to_string())];
691 let commit = repo
692 .commit_manifest(&mut manifest, "set species to elf", "author", changes)
693 .unwrap();
694
695 assert!(!commit.id.is_empty());
696 assert_eq!(commit.message, "set species to elf");
697
698 let read_back = repo
700 .read_manifest(&EntityType::new("character"), "hero")
701 .unwrap();
702 assert!(read_back.version >= 2);
703 }
704
705 #[test]
706 fn test_history() {
707 let tmp = TempDir::new().unwrap();
708 let repo = mock_repo(&tmp);
709
710 let (mut manifest, _) = repo
711 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
712 .unwrap();
713
714 manifest.set_property(
715 "name",
716 serde_yaml::Value::String("Updated Hero".to_string()),
717 );
718 repo.commit_manifest(&mut manifest, "update name", "author", vec![])
719 .unwrap();
720
721 let hist = repo
722 .history(&EntityType::new("character"), "hero", 10)
723 .unwrap();
724 assert!(hist.len() >= 2);
725 }
726
727 #[test]
728 fn test_revert_commit() {
729 let tmp = TempDir::new().unwrap();
730 let repo = mock_repo(&tmp);
731
732 let (mut manifest, _) = repo
734 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
735 .unwrap();
736 assert_eq!(manifest.name, "The Hero");
737
738 manifest.set_property("species", serde_yaml::Value::String("elf".to_string()));
740 let changes = vec![Change::set("properties.species", None, "elf".to_string())];
741 let _commit = repo
742 .commit_manifest(&mut manifest, "set species to elf", "author", changes)
743 .unwrap();
744
745 let read_back = repo
747 .read_manifest(&EntityType::new("character"), "hero")
748 .unwrap();
749 assert_eq!(
750 read_back.properties.get("species").and_then(|v| v.as_str()),
751 Some("elf")
752 );
753
754 let vcs_hash = read_back
756 .head
757 .as_ref()
758 .expect("head should be set after commit");
759
760 let revert_hash = repo.revert_commit(vcs_hash, "author").unwrap();
762 assert!(!revert_hash.is_empty());
763
764 let after_revert = repo
766 .read_manifest(&EntityType::new("character"), "hero")
767 .unwrap();
768 assert_eq!(after_revert.head.as_deref(), Some(revert_hash.as_str()));
769
770 let hist = repo
772 .history(&EntityType::new("character"), "hero", 10)
773 .unwrap();
774 assert!(hist.iter().any(|c| c.id == revert_hash));
775 }
776}
777
778#[cfg(all(test, feature = "lore-integration"))]
782mod lore_integration_tests {
783 use super::*;
784 use crate::vcs_lore::LoreBackend;
785 use std::time::{SystemTime, UNIX_EPOCH};
786 use tempfile::TempDir;
787
788 fn unique_suffix() -> u64 {
789 SystemTime::now()
790 .duration_since(UNIX_EPOCH)
791 .unwrap()
792 .as_nanos() as u64
793 }
794
795 fn setup_lore_repo() -> (TempDir, Repository) {
796 let repository = format!("ri-{}", unique_suffix());
797 let tmp = TempDir::new().unwrap();
798 let repo_path = tmp.path().join(&repository);
799 let repo =
800 Repository::init(&repo_path, &repository, Box::new(LoreBackend::from_env())).unwrap();
801 (tmp, repo)
802 }
803
804 #[test]
805 fn test_lore_init_creates_structure() {
806 let (_tmp, repo) = setup_lore_repo();
807 assert!(repo.root.join(".nap").exists());
808 assert!(repo.root.join("repository.yaml").exists());
809 }
810
811 #[test]
812 fn test_lore_create_and_read_entity() {
813 let (_tmp, repo) = setup_lore_repo();
814
815 let (manifest, _hash) = repo
816 .create_entity(
817 &EntityType::new("character"),
818 "hero",
819 "Test Hero",
820 "integration-test",
821 )
822 .unwrap();
823 assert_eq!(manifest.name, "Test Hero");
824
825 let read_back = repo
826 .read_manifest(&EntityType::new("character"), "hero")
827 .unwrap();
828 assert_eq!(read_back.name, "Test Hero");
829 }
830
831 #[test]
832 fn test_lore_commit_and_branch() {
833 let (_tmp, repo) = setup_lore_repo();
834
835 let (mut manifest, _) = repo
836 .create_entity(
837 &EntityType::new("character"),
838 "hero",
839 "Test Hero",
840 "integration-test",
841 )
842 .unwrap();
843
844 manifest.set_property("species", serde_yaml::Value::String("human".to_string()));
845 let changes = vec![Change::set("properties.species", None, "human".to_string())];
846 repo.commit_manifest(&mut manifest, "add species", "integration-test", changes)
847 .unwrap();
848
849 let read_back = repo
850 .read_manifest(&EntityType::new("character"), "hero")
851 .unwrap();
852 assert_eq!(read_back.version, 2);
853
854 repo.create_branch("feature-branch").unwrap();
855 let branches = repo.list_branches().unwrap();
856 assert!(branches.contains(&"feature-branch".to_string()));
857 }
858
859 #[test]
860 fn test_lore_delete_entity() {
861 let (_tmp, repo) = setup_lore_repo();
862
863 repo.create_entity(
864 &EntityType::new("character"),
865 "hero",
866 "Test Hero",
867 "integration-test",
868 )
869 .unwrap();
870
871 repo.delete_entity(&EntityType::new("character"), "hero", "integration-test")
872 .unwrap();
873
874 let entities = repo.list_entities(&EntityType::new("character")).unwrap();
875 assert!(!entities.contains(&"hero".to_string()));
876 }
877
878 #[test]
879 fn test_lore_history() {
880 let (_tmp, repo) = setup_lore_repo();
881
882 let (mut manifest, _) = repo
883 .create_entity(
884 &EntityType::new("character"),
885 "hero",
886 "Test Hero",
887 "integration-test",
888 )
889 .unwrap();
890
891 manifest.set_property(
892 "name",
893 serde_yaml::Value::String("Updated Hero".to_string()),
894 );
895 repo.commit_manifest(&mut manifest, "update name", "integration-test", vec![])
896 .unwrap();
897
898 let hist = repo
899 .history(&EntityType::new("character"), "hero", 10)
900 .unwrap();
901 assert!(hist.len() >= 2);
902 }
903}