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 manifest.bump_version();
331
332 crate::schema::validate_manifest(&manifest)
334 .map_err(|errors| NapError::ManifestValidationError(errors.join("; ")))?;
335
336 self.write_manifest(&manifest)?;
338
339 let commit_message = format!("Create {entity_type} '{name}' ({entity_id})");
341 let commit_hash = self.vcs.commit(&self.root, &commit_message, author)?;
342
343 info!(
344 manifest_id = %manifest.id,
345 commit_hash = %commit_hash,
346 "created entity"
347 );
348
349 Ok((manifest, commit_hash))
350 }
351
352 pub fn commit_manifest(
354 &self,
355 manifest: &mut Manifest,
356 message: &str,
357 author: &str,
358 changes: Vec<Change>,
359 ) -> Result<Commit, NapError> {
360 crate::schema::validate_manifest(manifest)
362 .map_err(|errors| NapError::ManifestValidationError(errors.join("; ")))?;
363
364 manifest.bump_version();
366
367 self.write_manifest(manifest)?;
369
370 let manifest_hash = manifest.content_hash()?.as_str().to_string();
372 let parent = Some(self.vcs.head_hash(&self.root)?);
373
374 let vcs_hash = self.vcs.commit(&self.root, message, author)?;
376
377 let nap_commit = Commit::new(parent, author, message, &manifest_hash, changes);
379
380 debug!(
381 manifest_id = %manifest.id,
382 version = manifest.version,
383 nap_commit_id = %nap_commit.id,
384 vcs_hash = %vcs_hash,
385 "manifest committed"
386 );
387
388 Ok(nap_commit)
389 }
390
391 pub fn history(
393 &self,
394 entity_type: &EntityType,
395 entity_id: &str,
396 limit: usize,
397 ) -> Result<Vec<crate::vcs::CommitInfo>, NapError> {
398 let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
399 let file_path = uri.manifest_path();
400 self.vcs.log(&self.root, Some(&file_path), limit)
401 }
402
403 pub fn list_entities(&self, entity_type: &EntityType) -> Result<Vec<String>, NapError> {
405 let dir = self.root.join(entity_type.directory_name());
406 if !dir.exists() {
407 return Err(NapError::EntityTypeNotFound(entity_type.to_string()));
408 }
409
410 let mut entities = Vec::new();
411 for entry in std::fs::read_dir(&dir)? {
412 let entry = entry?;
413 let path = entry.path();
414 if path.extension().and_then(|e| e.to_str()) == Some("yaml")
415 && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
416 {
417 entities.push(stem.to_string());
418 }
419 }
420 entities.sort();
421 Ok(entities)
422 }
423
424 pub fn list_entity_types(&self) -> Result<Vec<EntityType>, NapError> {
430 let mut types = Vec::new();
431 for entry in std::fs::read_dir(&self.root)? {
432 let entry = entry?;
433 let path = entry.path();
434 if !path.is_dir() {
435 continue;
436 }
437 let dir_name = match path.file_name().and_then(|n| n.to_str()) {
439 Some(name) => name,
440 None => continue,
441 };
442 if dir_name.starts_with('.') || dir_name == "target" {
443 continue;
444 }
445
446 if path.join(ENTITY_TYPE_MARKER).exists() {
448 types.push(EntityType::new(dir_name));
449 continue;
450 }
451
452 let has_yaml = std::fs::read_dir(&path)
454 .ok()
455 .and_then(|entries| {
456 entries
457 .filter_map(|e| e.ok())
458 .find(|e| e.path().extension().and_then(|ext| ext.to_str()) == Some("yaml"))
459 .map(|_| true)
460 })
461 .unwrap_or(false);
462
463 if has_yaml {
464 types.push(EntityType::new(dir_name));
465 }
466 }
467 types.sort_by(|a, b| a.as_str().cmp(b.as_str()));
468 Ok(types)
469 }
470
471 pub fn delete_entity(
473 &self,
474 entity_type: &EntityType,
475 entity_id: &str,
476 author: &str,
477 ) -> Result<String, NapError> {
478 let path = self.manifest_path(entity_type, entity_id);
479 if !path.exists() {
480 return Err(NapError::ManifestNotFound(path.display().to_string()));
481 }
482
483 std::fs::remove_file(&path)?;
484
485 let message = format!("Delete {entity_type} '{entity_id}'");
486 let hash = self.vcs.commit(&self.root, &message, author)?;
487 info!(entity_type = %entity_type, entity_id = %entity_id, "deleted entity");
488 Ok(hash)
489 }
490
491 pub fn create_branch(&self, name: &str) -> Result<(), NapError> {
493 self.vcs.create_branch(&self.root, name)
494 }
495
496 pub fn switch_branch(&self, name: &str) -> Result<(), NapError> {
498 self.vcs.switch_branch(&self.root, name)
499 }
500
501 pub fn list_branches(&self) -> Result<Vec<String>, NapError> {
503 self.vcs.list_branches(&self.root)
504 }
505
506 pub fn revert_commit(&self, commit_hash: &str, author: &str) -> Result<String, NapError> {
508 let new_hash = self.vcs.revert(&self.root, commit_hash)?;
509
510 info!(
511 commit = %commit_hash,
512 revert = %new_hash,
513 author = %author,
514 "commit reverted"
515 );
516
517 Ok(new_hash)
518 }
519
520 pub fn head_hash(&self) -> Result<String, NapError> {
522 self.vcs.head_hash(&self.root)
523 }
524
525 pub fn resolve_branch_head(&self, branch: &str) -> Result<String, NapError> {
527 self.vcs.resolve_branch_head(&self.root, branch)
528 }
529
530 pub fn add_remote(&self, name: &str, url: &str) -> Result<(), NapError> {
534 self.vcs.add_remote(&self.root, name, url)
535 }
536
537 pub fn remove_remote(&self, name: &str) -> Result<(), NapError> {
539 self.vcs.remove_remote(&self.root, name)
540 }
541
542 pub fn list_remotes(&self) -> Result<Vec<(String, String)>, NapError> {
544 self.vcs.list_remotes(&self.root)
545 }
546
547 pub fn push(&self, remote: Option<&str>, branch: Option<&str>) -> Result<(), NapError> {
549 self.vcs.push(&self.root, remote, branch)
550 }
551
552 pub fn pull(&self, remote: Option<&str>, branch: Option<&str>) -> Result<(), NapError> {
554 self.vcs.pull(&self.root, remote, branch)
555 }
556
557 pub fn vcs(&self) -> &dyn VcsBackend {
559 self.vcs.as_ref()
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use crate::test_utils::{MockBackend, mock_repo};
567 use tempfile::TempDir;
568
569 #[test]
570 fn test_mock_backend_contract() {
571 crate::test_utils::contract::run_repository_contract(MockBackend::new());
572 }
573
574 #[test]
575 fn test_init_creates_structure() {
576 let tmp = TempDir::new().unwrap();
577 let repo = mock_repo(&tmp);
578
579 assert!(repo.root.join("repository.yaml").exists());
580 }
581
582 #[test]
583 fn test_create_and_read_entity() {
584 let tmp = TempDir::new().unwrap();
585 let repo = mock_repo(&tmp);
586
587 let (manifest, hash) = repo
588 .create_entity(
589 &EntityType::new("character"),
590 "hero",
591 "The Hero",
592 "test-author",
593 )
594 .unwrap();
595
596 assert_eq!(manifest.name, "The Hero");
597 assert_eq!(manifest.entity_type.as_str(), "character");
598 assert_eq!(manifest.version, 1);
599 assert_eq!(repo.head_hash().unwrap(), hash);
600
601 let read_back = repo
603 .read_manifest(&EntityType::new("character"), "hero")
604 .unwrap();
605 assert_eq!(read_back.name, "The Hero");
606 assert_eq!(read_back.version, 1);
607 }
608
609 #[test]
610 fn test_create_entity_auto_creates_type_directory() {
611 let tmp = TempDir::new().unwrap();
612 let repo = mock_repo(&tmp);
613
614 repo.create_entity(&EntityType::new("pokemon"), "pikachu", "Pikachu", "test")
616 .unwrap();
617
618 assert!(repo.root.join("pokemon").exists());
620 assert!(repo.root.join("pokemon").join(".entity-type").exists());
621 assert!(repo.root.join("pokemon/pikachu.yaml").exists());
622 }
623
624 #[test]
625 fn test_list_entity_types() {
626 let tmp = TempDir::new().unwrap();
627 let repo = mock_repo(&tmp);
628
629 repo.create_entity(&EntityType::new("character"), "hero", "Hero", "author")
631 .unwrap();
632 repo.create_entity(&EntityType::new("location"), "village", "Village", "author")
633 .unwrap();
634 repo.create_entity(&EntityType::new("pokemon"), "pikachu", "Pikachu", "author")
635 .unwrap();
636
637 let types = repo.list_entity_types().unwrap();
638 assert!(types.contains(&EntityType::new("character")));
639 assert!(types.contains(&EntityType::new("location")));
640 assert!(types.contains(&EntityType::new("pokemon")));
641 }
642
643 #[test]
644 fn test_list_entities() {
645 let tmp = TempDir::new().unwrap();
646 let repo = mock_repo(&tmp);
647
648 repo.create_entity(&EntityType::new("character"), "alice", "Alice", "author")
649 .unwrap();
650 repo.create_entity(&EntityType::new("character"), "bob", "Bob", "author")
651 .unwrap();
652
653 let chars = repo.list_entities(&EntityType::new("character")).unwrap();
654 assert_eq!(chars, vec!["alice", "bob"]);
655 }
656
657 #[test]
658 fn test_commit_manifest_updates() {
659 let tmp = TempDir::new().unwrap();
660 let repo = mock_repo(&tmp);
661
662 let (mut manifest, create_hash) = repo
663 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
664 .unwrap();
665
666 manifest.set_property("species", serde_yaml::Value::String("elf".to_string()));
668 let changes = vec![Change::set("properties.species", None, "elf".to_string())];
669 let commit = repo
670 .commit_manifest(&mut manifest, "set species to elf", "author", changes)
671 .unwrap();
672
673 assert!(!commit.id.is_empty());
674 assert_eq!(commit.message, "set species to elf");
675 assert_eq!(commit.parent.as_deref(), Some(create_hash.as_str()));
676
677 let read_back = repo
679 .read_manifest(&EntityType::new("character"), "hero")
680 .unwrap();
681 assert!(read_back.version >= 2);
682 }
683
684 #[test]
685 fn test_history() {
686 let tmp = TempDir::new().unwrap();
687 let repo = mock_repo(&tmp);
688
689 let (mut manifest, _) = repo
690 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
691 .unwrap();
692
693 manifest.set_property(
694 "name",
695 serde_yaml::Value::String("Updated Hero".to_string()),
696 );
697 repo.commit_manifest(&mut manifest, "update name", "author", vec![])
698 .unwrap();
699
700 let hist = repo
701 .history(&EntityType::new("character"), "hero", 10)
702 .unwrap();
703 assert!(hist.len() >= 2);
704 }
705
706 #[test]
707 fn test_revert_commit() {
708 let tmp = TempDir::new().unwrap();
709 let repo = mock_repo(&tmp);
710
711 let (mut manifest, _) = repo
713 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
714 .unwrap();
715 assert_eq!(manifest.name, "The Hero");
716
717 manifest.set_property("species", serde_yaml::Value::String("elf".to_string()));
719 let changes = vec![Change::set("properties.species", None, "elf".to_string())];
720 repo.commit_manifest(&mut manifest, "set species to elf", "author", changes)
721 .unwrap();
722 let update_hash = repo.head_hash().unwrap();
723
724 let revert_hash = repo.revert_commit(&update_hash, "author").unwrap();
726 assert!(!revert_hash.is_empty());
727
728 let manifest_path = repo.manifest_path(&EntityType::new("character"), "hero");
730 let manifest_yaml = std::fs::read_to_string(&manifest_path).unwrap();
731 assert!(!manifest_yaml.contains("\nhead:"));
732
733 let hist = repo
735 .history(&EntityType::new("character"), "hero", 10)
736 .unwrap();
737 assert!(hist.iter().any(|c| c.id == revert_hash));
738 }
739}
740
741#[cfg(all(test, feature = "lore-integration"))]
745mod lore_integration_tests {
746 use super::*;
747 use crate::vcs_lore::LoreBackend;
748 use std::time::{SystemTime, UNIX_EPOCH};
749 use tempfile::TempDir;
750
751 fn unique_suffix() -> u64 {
752 SystemTime::now()
753 .duration_since(UNIX_EPOCH)
754 .unwrap()
755 .as_nanos() as u64
756 }
757
758 fn setup_lore_repo() -> (TempDir, Repository) {
759 let repository = format!("ri-{}", unique_suffix());
760 let tmp = TempDir::new().unwrap();
761 let repo_path = tmp.path().join(&repository);
762 let repo =
763 Repository::init(&repo_path, &repository, Box::new(LoreBackend::from_env())).unwrap();
764 (tmp, repo)
765 }
766
767 #[test]
768 fn test_lore_init_creates_structure() {
769 let (_tmp, repo) = setup_lore_repo();
770 assert!(repo.root.join(".nap").exists());
771 assert!(repo.root.join("repository.yaml").exists());
772 }
773
774 #[test]
775 fn test_lore_create_and_read_entity() {
776 let (_tmp, repo) = setup_lore_repo();
777
778 let (manifest, _hash) = repo
779 .create_entity(
780 &EntityType::new("character"),
781 "hero",
782 "Test Hero",
783 "integration-test",
784 )
785 .unwrap();
786 assert_eq!(manifest.name, "Test Hero");
787
788 let read_back = repo
789 .read_manifest(&EntityType::new("character"), "hero")
790 .unwrap();
791 assert_eq!(read_back.name, "Test Hero");
792 }
793
794 #[test]
795 fn test_lore_commit_and_branch() {
796 let (_tmp, repo) = setup_lore_repo();
797
798 let (mut manifest, _) = repo
799 .create_entity(
800 &EntityType::new("character"),
801 "hero",
802 "Test Hero",
803 "integration-test",
804 )
805 .unwrap();
806
807 manifest.set_property("species", serde_yaml::Value::String("human".to_string()));
808 let changes = vec![Change::set("properties.species", None, "human".to_string())];
809 repo.commit_manifest(&mut manifest, "add species", "integration-test", changes)
810 .unwrap();
811
812 let read_back = repo
813 .read_manifest(&EntityType::new("character"), "hero")
814 .unwrap();
815 assert_eq!(read_back.version, 2);
816
817 repo.create_branch("feature-branch").unwrap();
818 let branches = repo.list_branches().unwrap();
819 assert!(branches.contains(&"feature-branch".to_string()));
820 }
821
822 #[test]
823 fn test_lore_delete_entity() {
824 let (_tmp, repo) = setup_lore_repo();
825
826 repo.create_entity(
827 &EntityType::new("character"),
828 "hero",
829 "Test Hero",
830 "integration-test",
831 )
832 .unwrap();
833
834 repo.delete_entity(&EntityType::new("character"), "hero", "integration-test")
835 .unwrap();
836
837 let entities = repo.list_entities(&EntityType::new("character")).unwrap();
838 assert!(!entities.contains(&"hero".to_string()));
839 }
840
841 #[test]
842 fn test_lore_history() {
843 let (_tmp, repo) = setup_lore_repo();
844
845 let (mut manifest, _) = repo
846 .create_entity(
847 &EntityType::new("character"),
848 "hero",
849 "Test Hero",
850 "integration-test",
851 )
852 .unwrap();
853
854 manifest.set_property(
855 "name",
856 serde_yaml::Value::String("Updated Hero".to_string()),
857 );
858 repo.commit_manifest(&mut manifest, "update name", "integration-test", vec![])
859 .unwrap();
860
861 let hist = repo
862 .history(&EntityType::new("character"), "hero", 10)
863 .unwrap();
864 assert!(hist.len() >= 2);
865 }
866}