diff --git a/csharp/.changeset/add-named-types-decorator.md b/csharp/.changeset/add-named-types-decorator.md new file mode 100644 index 0000000..6b3dc46 --- /dev/null +++ b/csharp/.changeset/add-named-types-decorator.md @@ -0,0 +1,5 @@ +--- +'Foundation.Data.Doublets.Cli': minor +--- + +Added a universal `NamedTypesDecorator` that implements both links operations and named type lookups, with automatic cleanup and uniqueness checks for external-reference names. diff --git a/csharp/Foundation.Data.Doublets.Cli.Tests/NamedTypesDecoratorTests.cs b/csharp/Foundation.Data.Doublets.Cli.Tests/NamedTypesDecoratorTests.cs new file mode 100644 index 0000000..529b77c --- /dev/null +++ b/csharp/Foundation.Data.Doublets.Cli.Tests/NamedTypesDecoratorTests.cs @@ -0,0 +1,235 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using Xunit; +using Platform.Memory; +using Platform.Data.Doublets; +using Platform.Data.Doublets.Memory.United.Generic; +using Foundation.Data.Doublets.Cli; +using DoubletLink = Platform.Data.Doublets.Link; + +namespace Foundation.Data.Doublets.Cli.Tests +{ + public class NamedTypesDecoratorTests : IDisposable + { + private readonly string _tempDbPath; + private readonly string _tempNamesDbPath; + + public NamedTypesDecoratorTests() + { + _tempDbPath = Path.GetTempFileName(); + _tempNamesDbPath = Path.GetTempFileName(); + } + + public void Dispose() + { + if (File.Exists(_tempDbPath)) File.Delete(_tempDbPath); + if (File.Exists(_tempNamesDbPath)) File.Delete(_tempNamesDbPath); + } + + private static void RunTestWithLinks(Action> testAction) + { + var tempFile = Path.GetTempFileName(); + try + { + using var memory = new FileMappedResizableDirectMemory(tempFile, UnitedMemoryLinks.DefaultLinksSizeStep); + using var unitedMemoryLinks = new UnitedMemoryLinks(memory); + var linksDecoratedWithAutomaticUniquenessResolution = unitedMemoryLinks.DecorateWithAutomaticUniquenessAndUsagesResolution(); + testAction(linksDecoratedWithAutomaticUniquenessResolution); + } + finally + { + if (File.Exists(tempFile)) File.Delete(tempFile); + } + } + + [Fact] + public void NamedTypesDecorator_ImplementsILinks() + { + RunTestWithLinks(links => + { + var decorator = new NamedTypesDecorator(links, _tempNamesDbPath); + + Assert.True(decorator is ILinks); + }); + } + + [Fact] + public void NamedTypesDecorator_ImplementsINamedTypes() + { + RunTestWithLinks(links => + { + var decorator = new NamedTypesDecorator(links, _tempNamesDbPath); + + Assert.True(decorator is INamedTypes); + }); + } + + [Fact] + public void NamedTypesDecorator_CanSetAndGetNames() + { + RunTestWithLinks(links => + { + var decorator = new NamedTypesDecorator(links, _tempNamesDbPath); + + var link1 = decorator.GetOrCreate(10u, 20u); + var link2 = decorator.GetOrCreate(30u, 40u); + + var nameLink1 = decorator.SetName(link1, "TestLink1"); + var nameLink2 = decorator.SetName(link2, "TestLink2"); + + Assert.NotEqual(links.Constants.Null, nameLink1); + Assert.NotEqual(links.Constants.Null, nameLink2); + + var retrievedName1 = decorator.GetName(link1); + var retrievedName2 = decorator.GetName(link2); + + Assert.Equal("TestLink1", retrievedName1); + Assert.Equal("TestLink2", retrievedName2); + }); + } + + [Fact] + public void NamedTypesDecorator_CanGetLinkByName() + { + RunTestWithLinks(links => + { + var decorator = new NamedTypesDecorator(links, _tempNamesDbPath); + + var link = decorator.GetOrCreate(50u, 60u); + decorator.SetName(link, "UniqueTestName"); + + var retrievedLink = decorator.GetByName("UniqueTestName"); + + Assert.Equal(link, retrievedLink); + }); + } + + [Fact] + public void NamedTypesDecorator_CanRemoveNames() + { + RunTestWithLinks(links => + { + var decorator = new NamedTypesDecorator(links, _tempNamesDbPath); + + var link = decorator.GetOrCreate(70u, 80u); + decorator.SetName(link, "TemporaryName"); + + var nameBeforeRemoval = decorator.GetName(link); + Assert.Equal("TemporaryName", nameBeforeRemoval); + + decorator.RemoveName(link); + + var nameAfterRemoval = decorator.GetName(link); + Assert.Null(nameAfterRemoval); + + var linkByName = decorator.GetByName("TemporaryName"); + Assert.Equal(links.Constants.Null, linkByName); + }); + } + + [Fact] + public void NamedTypesDecorator_CanOverwriteNames() + { + RunTestWithLinks(links => + { + var decorator = new NamedTypesDecorator(links, _tempNamesDbPath); + + var link = decorator.GetOrCreate(90u, 100u); + decorator.SetName(link, "FirstName"); + + var firstRetrievedName = decorator.GetName(link); + Assert.Equal("FirstName", firstRetrievedName); + + decorator.SetName(link, "SecondName"); + + var secondRetrievedName = decorator.GetName(link); + Assert.Equal("SecondName", secondRetrievedName); + + var linkByFirstName = decorator.GetByName("FirstName"); + Assert.Equal(links.Constants.Null, linkByFirstName); + + var linkBySecondName = decorator.GetByName("SecondName"); + Assert.Equal(link, linkBySecondName); + }); + } + + [Fact] + public void NamedTypesDecorator_ReassigningExistingNameMovesNameToNewLink() + { + RunTestWithLinks(links => + { + var decorator = new NamedTypesDecorator(links, _tempNamesDbPath); + + var firstLink = decorator.GetOrCreate(10u, 11u); + var secondLink = decorator.GetOrCreate(20u, 21u); + + decorator.SetName(firstLink, "SharedName"); + decorator.SetName(secondLink, "SharedName"); + + Assert.Null(decorator.GetName(firstLink)); + Assert.Equal("SharedName", decorator.GetName(secondLink)); + Assert.Equal(secondLink, decorator.GetByName("SharedName")); + }); + } + + [Fact] + public void NamedTypesDecorator_DeleteRemovesAssociatedNames() + { + RunTestWithLinks(links => + { + var decorator = new NamedTypesDecorator(links, _tempNamesDbPath); + + var link = decorator.GetOrCreate(110u, 120u); + decorator.SetName(link, "LinkToDelete"); + + var nameBeforeDeletion = decorator.GetName(link); + Assert.Equal("LinkToDelete", nameBeforeDeletion); + + decorator.Delete(new uint[] { link }, null); + + var linkByName = decorator.GetByName("LinkToDelete"); + Assert.Equal(links.Constants.Null, linkByName); + }); + } + + [Fact] + public void NamedTypesDecorator_CanConstructFromDatabaseFilename() + { + var tempDbFile = Path.GetTempFileName(); + var expectedNamesDb = NamedTypesDecorator.MakeNamesDatabaseFilename(tempDbFile); + + try + { + var decorator = new NamedTypesDecorator(tempDbFile); + var link = decorator.GetOrCreate(1u, 2u); + + decorator.SetName(link, "FromFile"); + + Assert.Equal(expectedNamesDb, decorator.NamedLinksDatabaseFileName); + Assert.Equal(link, decorator.GetByName("FromFile")); + } + finally + { + if (File.Exists(tempDbFile)) File.Delete(tempDbFile); + if (File.Exists(expectedNamesDb)) File.Delete(expectedNamesDb); + } + } + + [Fact] + public void NamedTypesDecorator_HandlesNonexistentNames() + { + RunTestWithLinks(links => + { + var decorator = new NamedTypesDecorator(links, _tempNamesDbPath); + + var linkByNonexistentName = decorator.GetByName("NonexistentName"); + Assert.Equal(links.Constants.Null, linkByNonexistentName); + + var nameOfNonexistentLink = decorator.GetName(999999u); + Assert.Null(nameOfNonexistentLink); + }); + } + } +} diff --git a/csharp/Foundation.Data.Doublets.Cli/INamedTypes.cs b/csharp/Foundation.Data.Doublets.Cli/INamedTypes.cs new file mode 100644 index 0000000..54b1b62 --- /dev/null +++ b/csharp/Foundation.Data.Doublets.Cli/INamedTypes.cs @@ -0,0 +1,13 @@ +using System.Numerics; + +namespace Foundation.Data.Doublets.Cli +{ + public interface INamedTypes + where TLinkAddress : IUnsignedNumber + { + string? GetName(TLinkAddress link); + TLinkAddress SetName(TLinkAddress link, string name); + TLinkAddress GetByName(string name); + void RemoveName(TLinkAddress link); + } +} \ No newline at end of file diff --git a/csharp/Foundation.Data.Doublets.Cli/NamedLinks.cs b/csharp/Foundation.Data.Doublets.Cli/NamedLinks.cs index 819a6cd..8288268 100644 --- a/csharp/Foundation.Data.Doublets.Cli/NamedLinks.cs +++ b/csharp/Foundation.Data.Doublets.Cli/NamedLinks.cs @@ -103,17 +103,12 @@ public void RemoveName(TLinkAddress link) var nameCandidate = _links.GetTarget(nameCandidatePair); if (_links.GetSource(nameCandidate).Equals(_nameType)) { - // Remove the name link _links.Delete(nameCandidatePair); - // Remove the nameType->nameSequence link if not used elsewhere - var nameSequence = _links.GetTarget(nameCandidate); - var nameTypeToNameSequenceLink = nameCandidate; - // Check if this nameType->nameSequence is used elsewhere - var queryNameType = new Link(any, _nameType, nameSequence); - var linksToNameSequence = _links.All(queryNameType).ToList(); - if (linksToNameSequence.Count == 1) // only this one exists + + var nameStillUsed = _links.All(new Link(any, any, nameCandidate)).Any(); + if (!nameStillUsed) { - _links.Delete(nameTypeToNameSequenceLink); + _links.Delete(nameCandidate); } } } @@ -122,20 +117,6 @@ public void RemoveName(TLinkAddress link) public void RemoveNameByExternalReference(TLinkAddress externalReference) { var reference = new Hybrid(externalReference, isExternal: true); - // Get the name for this external reference - var name = GetName(reference); - if (name != null) - { - // Remove the mapping from name to external reference - var nameSequence = _createString(name); - var nameTypeToNameSequenceLink = _links.SearchOrDefault(_nameType, nameSequence); - if (!nameTypeToNameSequenceLink.Equals(_links.Constants.Null)) - { - // Remove the nameType->nameSequence link if it exists - _links.Delete(nameTypeToNameSequenceLink); - } - } - // Remove the name link (externalRef->nameType->nameSequence) RemoveName(reference); } } diff --git a/csharp/Foundation.Data.Doublets.Cli/NamedTypesDecorator.cs b/csharp/Foundation.Data.Doublets.Cli/NamedTypesDecorator.cs new file mode 100644 index 0000000..c837f1d --- /dev/null +++ b/csharp/Foundation.Data.Doublets.Cli/NamedTypesDecorator.cs @@ -0,0 +1,141 @@ +using System.Numerics; +using Platform.Delegates; +using Platform.Memory; +using Platform.Data; +using Platform.Data.Doublets; +using Platform.Data.Doublets.Decorators; +using Platform.Data.Doublets.Memory; +using Platform.Data.Doublets.Memory.United.Generic; +using System; +using System.Linq; +using System.Collections.Generic; + +namespace Foundation.Data.Doublets.Cli +{ + public class NamedTypesDecorator : LinksDecoratorBase, INamedTypes + where TLinkAddress : struct, + IUnsignedNumber, + IComparisonOperators, + IShiftOperators, + IBitwiseOperators, + IMinMaxValue + { + private readonly bool _tracingEnabled; + public readonly NamedLinks NamedLinks; + public readonly string NamedLinksDatabaseFileName; + + public static ILinks MakeLinks(string databaseFilename) + { + var links = new UnitedMemoryLinks(databaseFilename); + return links.DecorateWithAutomaticUniquenessAndUsagesResolution(); + } + + public static string MakeNamesDatabaseFilename(string databaseFilename) + { + var filenameWithoutExtension = Path.GetFileNameWithoutExtension(databaseFilename); + var directory = Path.GetDirectoryName(databaseFilename); + var namesDatabaseFilename = Path.Combine(directory ?? string.Empty, $"{filenameWithoutExtension}.names.links"); + return namesDatabaseFilename; + } + + public NamedTypesDecorator(ILinks links, string namesDatabaseFilename, bool tracingEnabled = false) : base(links) + { + _tracingEnabled = tracingEnabled; + if (_tracingEnabled) Console.WriteLine($"[Trace] Constructing NamedTypesDecorator with names DB: {namesDatabaseFilename}"); + var namesConstants = new LinksConstants(enableExternalReferencesSupport: true); + var namesMemory = new FileMappedResizableDirectMemory(namesDatabaseFilename, UnitedMemoryLinks.DefaultLinksSizeStep); + var namesLinks = new UnitedMemoryLinks(namesMemory, UnitedMemoryLinks.DefaultLinksSizeStep, namesConstants, IndexTreeType.Default); + var decoratedNamesLinks = namesLinks.DecorateWithAutomaticUniquenessAndUsagesResolution(); + NamedLinks = new UnicodeStringStorage(decoratedNamesLinks).NamedLinks; + NamedLinksDatabaseFileName = namesDatabaseFilename; + } + + public NamedTypesDecorator(string databaseFilename, bool tracingEnabled = false) + : this(MakeLinks(databaseFilename), MakeNamesDatabaseFilename(databaseFilename), tracingEnabled) + { + } + + public string? GetName(TLinkAddress link) + { + if (_tracingEnabled) Console.WriteLine($"[Trace] GetName called for link: {link}"); + var result = NamedLinks.GetNameByExternalReference(link); + if (_tracingEnabled) Console.WriteLine($"[Trace] GetName result: {result}"); + return result; + } + + public TLinkAddress SetName(TLinkAddress link, string name) + { + if (_tracingEnabled) Console.WriteLine($"[Trace] SetName called for link: {link} with name: '{name}'"); + var existingLinkWithName = NamedLinks.GetExternalReferenceByName(name); + if (!existingLinkWithName.Equals(_links.Constants.Null) && !existingLinkWithName.Equals(link)) + { + RemoveName(existingLinkWithName); + } + RemoveName(link); + var result = NamedLinks.SetNameForExternalReference(link, name); + if (_tracingEnabled) Console.WriteLine($"[Trace] SetName result: {result}"); + return result; + } + + public TLinkAddress GetByName(string name) + { + if (_tracingEnabled) Console.WriteLine($"[Trace] GetByName called for name: '{name}'"); + var result = NamedLinks.GetExternalReferenceByName(name); + if (_tracingEnabled) Console.WriteLine($"[Trace] GetByName result: {result}"); + return result; + } + + public void RemoveName(TLinkAddress link) + { + if (_tracingEnabled) Console.WriteLine($"[Trace] RemoveName called for link: {link}"); + NamedLinks.RemoveNameByExternalReference(link); + if (_tracingEnabled) Console.WriteLine($"[Trace] RemoveName completed for link: {link}"); + } + + public override TLinkAddress Update(IList? restriction, IList? substitution, WriteHandler? handler) + { + if (_tracingEnabled) + { + var req = restriction == null ? "null" : string.Join(",", restriction); + Console.WriteLine($"[Trace] Update called with restriction: [{req}]"); + } + var @continue = _links.Constants.Continue; + var result = _links.Update(restriction, substitution, (IList? before, IList? after) => + { + if (_tracingEnabled) Console.WriteLine($"[Trace] Debug: handlerWrapper invoked - before={(before == null ? "null" : string.Join(",", before))}, after={(after == null ? "null" : string.Join(",", after))}"); + if (before != null && after == null) + { + var deletedLinkIndex = _links.GetIndex(link: before); + RemoveName(deletedLinkIndex); + } + return handler == null ? @continue : handler(before, after); + }); + if (_tracingEnabled) Console.WriteLine($"[Trace] Update result: {result}"); + return result; + } + + public override TLinkAddress Delete(IList? restriction, WriteHandler? handler) + { + if (_tracingEnabled) + { + var formattedRestriction = restriction == null ? "null" : string.Join(",", restriction); + Console.WriteLine($"[Trace] Delete called with restriction: [{formattedRestriction}]"); + } + if (_tracingEnabled) Console.WriteLine($"[Trace] Debug: this._links is of type: {_links.GetType()}"); + var @continue = _links.Constants.Continue; + if (_tracingEnabled) Console.WriteLine($"[Trace] Debug: Calling underlying _links.Delete"); + TLinkAddress result = _links.Delete(restriction, (IList? before, IList? after) => + { + if (_tracingEnabled) Console.WriteLine($"[Trace] Debug: handlerWrapper invoked - before={(before == null ? "null" : string.Join(",", before))}, after={(after == null ? "null" : string.Join(",", after))}"); + if (before != null && after == null) + { + var deletedLinkIndex = _links.GetIndex(link: before); + RemoveName(deletedLinkIndex); + } + return handler == null ? @continue : handler(before, after); + }); + if (_tracingEnabled) Console.WriteLine($"[Trace] Debug: Delete result: {result}"); + return result; + } + } +} diff --git a/rust/changelog.d/20260430_070900_named_types_decorator.md b/rust/changelog.d/20260430_070900_named_types_decorator.md new file mode 100644 index 0000000..95a2ee3 --- /dev/null +++ b/rust/changelog.d/20260430_070900_named_types_decorator.md @@ -0,0 +1,5 @@ +--- +bump: minor +--- + +Added Rust `NamedTypes` and `NamedTypesDecorator` parity for link storage with external-reference names, name cleanup on delete, and unique name reassignment behavior. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index c1560e4..5771433 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -22,6 +22,7 @@ mod link; mod link_storage; mod lino_link; mod named_links; +mod named_types; mod parser; mod pinned_types; mod query_processor; @@ -36,6 +37,7 @@ pub use link::{DoubletsLink, Link}; pub use link_storage::LinkStorage; pub use lino_link::LinoLink; pub use named_links::NamedLinks; +pub use named_types::{NamedTypes, NamedTypesDecorator}; pub use parser::Parser; pub use pinned_types::PinnedTypes; pub use query_processor::{QueryOptions, QueryProcessor}; diff --git a/rust/src/named_types.rs b/rust/src/named_types.rs new file mode 100644 index 0000000..57a8704 --- /dev/null +++ b/rust/src/named_types.rs @@ -0,0 +1,211 @@ +//! Named type decorator for Rust link storage. +//! +//! This mirrors the C# `NamedTypesDecorator` role: link operations are +//! delegated to the wrapped `LinkStorage`, while names are stored as external +//! references in a separate links database. + +use std::path::{Path, PathBuf}; + +use anyhow::Result; + +use crate::link::Link; +use crate::link_storage::LinkStorage; +use crate::named_links::NamedLinks; + +pub trait NamedTypes { + fn get_name(&mut self, link: u32) -> Result>; + fn set_name(&mut self, link: u32, name: &str) -> Result; + fn get_by_name(&mut self, name: &str) -> Result>; + fn remove_name(&mut self, link: u32) -> Result<()>; +} + +pub struct NamedTypesDecorator { + links: LinkStorage, + names_links: LinkStorage, + trace: bool, +} + +impl NamedTypesDecorator { + pub fn new

(database_filename: P, trace: bool) -> Result + where + P: AsRef, + { + let names_database_filename = + Self::make_names_database_filename(database_filename.as_ref()); + Self::with_names_database_path(database_filename, names_database_filename, trace) + } + + pub fn with_names_database_path( + database_filename: P, + names_database_filename: N, + trace: bool, + ) -> Result + where + P: AsRef, + N: AsRef, + { + let database_path = path_to_string(database_filename.as_ref()); + let names_database_path = path_to_string(names_database_filename.as_ref()); + let links = LinkStorage::new(&database_path, trace)?; + let names_links = LinkStorage::new(&names_database_path, trace)?; + Ok(Self::from_link_storages_with_trace( + links, + names_links, + trace, + )) + } + + pub fn from_link_storages(links: LinkStorage, names_links: LinkStorage) -> Self { + Self::from_link_storages_with_trace(links, names_links, false) + } + + pub fn make_names_database_filename

(database_filename: P) -> PathBuf + where + P: AsRef, + { + let path = database_filename.as_ref(); + let filename_without_extension = path + .file_stem() + .and_then(|value| value.to_str()) + .unwrap_or_default(); + let names_filename = format!("{filename_without_extension}.names.links"); + + path.parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .map(|parent| parent.join(&names_filename)) + .unwrap_or_else(|| PathBuf::from(names_filename)) + } + + pub fn links(&self) -> &LinkStorage { + &self.links + } + + pub fn links_mut(&mut self) -> &mut LinkStorage { + &mut self.links + } + + pub fn names_links(&self) -> &LinkStorage { + &self.names_links + } + + pub fn names_links_mut(&mut self) -> &mut LinkStorage { + &mut self.names_links + } + + pub fn into_link_storages(self) -> (LinkStorage, LinkStorage) { + (self.links, self.names_links) + } + + pub fn save(&self) -> Result<()> { + self.links.save()?; + self.names_links.save()?; + Ok(()) + } + + pub fn create(&mut self, source: u32, target: u32) -> u32 { + self.links.create(source, target) + } + + pub fn ensure_created(&mut self, id: u32) -> u32 { + self.links.ensure_created(id) + } + + pub fn get(&self, id: u32) -> Option<&Link> { + self.links.get(id) + } + + pub fn exists(&self, id: u32) -> bool { + self.links.exists(id) + } + + pub fn update(&mut self, id: u32, source: u32, target: u32) -> Result { + self.links.update(id, source, target) + } + + pub fn delete(&mut self, id: u32) -> Result { + let deleted = self.links.delete(id)?; + self.remove_name(id)?; + Ok(deleted) + } + + pub fn all(&self) -> Vec<&Link> { + self.links.all() + } + + pub fn query( + &self, + index: Option, + source: Option, + target: Option, + ) -> Vec<&Link> { + self.links.query(index, source, target) + } + + pub fn search(&self, source: u32, target: u32) -> Option { + self.links.search(source, target) + } + + pub fn get_or_create(&mut self, source: u32, target: u32) -> u32 { + self.links.get_or_create(source, target) + } + + fn from_link_storages_with_trace( + links: LinkStorage, + names_links: LinkStorage, + trace: bool, + ) -> Self { + Self { + links, + names_links, + trace, + } + } + + fn with_named_links( + &mut self, + action: impl FnOnce(&mut NamedLinks<'_>) -> Result, + ) -> Result { + let mut named_links = NamedLinks::new(&mut self.names_links)?; + action(&mut named_links) + } +} + +impl NamedTypes for NamedTypesDecorator { + fn get_name(&mut self, link: u32) -> Result> { + if self.trace { + eprintln!("[TRACE] NamedTypesDecorator get_name for link {link}"); + } + self.with_named_links(|named_links| named_links.get_name_by_external_reference(link)) + } + + fn set_name(&mut self, link: u32, name: &str) -> Result { + if self.trace { + eprintln!("[TRACE] NamedTypesDecorator set_name for link {link}: {name}"); + } + if let Some(existing_link_with_name) = self.get_by_name(name)? { + if existing_link_with_name != link { + self.remove_name(existing_link_with_name)?; + } + } + self.remove_name(link)?; + self.with_named_links(|named_links| named_links.set_name_for_external_reference(link, name)) + } + + fn get_by_name(&mut self, name: &str) -> Result> { + if self.trace { + eprintln!("[TRACE] NamedTypesDecorator get_by_name for name {name}"); + } + self.with_named_links(|named_links| named_links.get_external_reference_by_name(name)) + } + + fn remove_name(&mut self, link: u32) -> Result<()> { + if self.trace { + eprintln!("[TRACE] NamedTypesDecorator remove_name for link {link}"); + } + self.with_named_links(|named_links| named_links.remove_name_by_external_reference(link)) + } +} + +fn path_to_string(path: &Path) -> String { + path.to_string_lossy().into_owned() +} diff --git a/rust/tests/named_types_decorator_tests.rs b/rust/tests/named_types_decorator_tests.rs new file mode 100644 index 0000000..0ad12bc --- /dev/null +++ b/rust/tests/named_types_decorator_tests.rs @@ -0,0 +1,129 @@ +use anyhow::Result; +use link_cli::{LinkStorage, NamedTypes, NamedTypesDecorator}; +use tempfile::NamedTempFile; + +#[test] +fn decorator_exposes_link_storage_operations_and_named_types() -> Result<()> { + let db_file = NamedTempFile::new()?; + let names_file = NamedTempFile::new()?; + let db_path = db_file.path().to_str().unwrap(); + let names_path = names_file.path().to_str().unwrap(); + + let mut decorator = NamedTypesDecorator::with_names_database_path(db_path, names_path, false)?; + + let link = decorator.get_or_create(10, 20); + let name_link = decorator.set_name(link, "TestLink")?; + + assert!(decorator.links().exists(link)); + assert!(decorator.names_links().exists(name_link)); + assert_eq!(Some("TestLink".to_string()), decorator.get_name(link)?); + assert_eq!(Some(link), decorator.get_by_name("TestLink")?); + + Ok(()) +} + +#[test] +fn delete_removes_associated_name_from_names_database() -> Result<()> { + let db_file = NamedTempFile::new()?; + let names_file = NamedTempFile::new()?; + let db_path = db_file.path().to_str().unwrap(); + let names_path = names_file.path().to_str().unwrap(); + + let mut decorator = NamedTypesDecorator::with_names_database_path(db_path, names_path, false)?; + let link = decorator.create(1, 2); + decorator.set_name(link, "TemporaryName")?; + + assert_eq!(Some(link), decorator.get_by_name("TemporaryName")?); + + let deleted = decorator.delete(link)?; + + assert_eq!(link, deleted.index); + assert!(!decorator.links().exists(link)); + assert_eq!(None, decorator.get_name(link)?); + assert_eq!(None, decorator.get_by_name("TemporaryName")?); + + Ok(()) +} + +#[test] +fn setting_second_name_replaces_first_name() -> Result<()> { + let db_file = NamedTempFile::new()?; + let names_file = NamedTempFile::new()?; + let db_path = db_file.path().to_str().unwrap(); + let names_path = names_file.path().to_str().unwrap(); + + let mut decorator = NamedTypesDecorator::with_names_database_path(db_path, names_path, false)?; + let link = decorator.create(1, 1); + + decorator.set_name(link, "First")?; + decorator.set_name(link, "Second")?; + + assert_eq!(Some("Second".to_string()), decorator.get_name(link)?); + assert_eq!(None, decorator.get_by_name("First")?); + assert_eq!(Some(link), decorator.get_by_name("Second")?); + + Ok(()) +} + +#[test] +fn reassigning_existing_name_moves_name_to_new_link() -> Result<()> { + let db_file = NamedTempFile::new()?; + let names_file = NamedTempFile::new()?; + let db_path = db_file.path().to_str().unwrap(); + let names_path = names_file.path().to_str().unwrap(); + + let mut decorator = NamedTypesDecorator::with_names_database_path(db_path, names_path, false)?; + let first_link = decorator.create(10, 11); + let second_link = decorator.create(20, 21); + + decorator.set_name(first_link, "SharedName")?; + decorator.set_name(second_link, "SharedName")?; + + assert_eq!(None, decorator.get_name(first_link)?); + assert_eq!( + Some("SharedName".to_string()), + decorator.get_name(second_link)? + ); + assert_eq!(Some(second_link), decorator.get_by_name("SharedName")?); + + Ok(()) +} + +#[test] +fn default_names_database_path_matches_csharp_convention() { + assert_eq!( + "test.names.links", + NamedTypesDecorator::make_names_database_filename("test.db") + .to_str() + .unwrap() + ); + assert_eq!( + "a.b.names.links", + NamedTypesDecorator::make_names_database_filename("a.b.c") + .to_str() + .unwrap() + ); +} + +#[test] +fn decorator_can_be_built_from_existing_link_storages() -> Result<()> { + let db_file = NamedTempFile::new()?; + let names_file = NamedTempFile::new()?; + let db_path = db_file.path().to_str().unwrap(); + let names_path = names_file.path().to_str().unwrap(); + + let links = LinkStorage::new(db_path, false)?; + let names_links = LinkStorage::new(names_path, false)?; + let mut decorator = NamedTypesDecorator::from_link_storages(links, names_links); + + let link = decorator.create(5, 6); + decorator.set_name(link, "ExistingStores")?; + + assert_eq!(Some(link), decorator.get_by_name("ExistingStores")?); + assert_eq!( + Some("ExistingStores".to_string()), + decorator.get_name(link)? + ); + + Ok(()) +}