From bc664bbc653523543db16d5893d362e967ecc06c Mon Sep 17 00:00:00 2001 From: Richard Irons <115992270+RichardIrons-neo4j@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:33:27 +0000 Subject: [PATCH] Invariant capable records (#780) * Refactor records to allow case-insensitive lookups * minor formatting, added test * remove List<> from every record; test coverage * removed unnecessary wrapper * undo accidental rename * undo wrong refactor * small reformats * Review notes, fix testkit test * Make IRecord implement IReadOnlyDictionary * Add Get, TryGet and case insensitive versions to Record types * Add Get and TryGet to IEntity * Fix MatchesRecord tesst helper function * Review notes; removed duplicate method * Minor Record tidyups * Review notes and new AsRecord method --- .../Mapping/BuiltMapperTests.cs | 5 +- .../Mapping/DefaultMapperTests.cs | 26 +-- .../Mapping/DictAsRecordTests.cs | 10 +- .../Mapping/LabelCaptureTests.cs | 14 +- .../Mapping/MappedListCreatorTests.cs | 4 +- .../Mapping/MappingProviderTests.cs | 18 +- .../MappingSourceDelegateBuilderTests.cs | 15 +- .../Mapping/RecordMappingTests.cs | 93 ++++++--- .../Mapping/RecordNodeExtensionsTests.cs | 126 ------------ .../Mapping/RecordPathFinderTests.cs | 129 ++++++++---- .../Internal/InternalRxResultTests.cs | 5 +- .../Reactive/Utils/Utils.cs | 18 +- .../Result/RecordSetTests.cs | 5 +- .../Neo4j.Driver.Tests/Result/RecordTests.cs | 188 ++++++++++++++++++ .../Result/ResultCursorTests.cs | 11 +- .../Neo4j.Driver.Tests/Result/ResultTests.cs | 9 +- .../ResultCursorExtensionsTests.cs | 8 +- .../Neo4j.Driver.Tests/TestUtil/TestRecord.cs | 34 ++++ .../Neo4j.Driver/Internal/Result/Record.cs | 96 +++++++-- .../Internal/Result/ResultCursorBuilder.cs | 30 ++- .../Neo4j.Driver/Internal/Types/Node.cs | 20 ++ .../Internal/Types/Relationship.cs | 19 ++ .../Preview/Mapping/DictAsRecord.cs | 78 +++++++- .../Preview/Mapping/EntityExtensions.cs | 32 +++ .../Preview/Mapping/RecordEntityExtensions.cs | 167 ---------------- .../Preview/Mapping/RecordExtensions.cs | 36 ++++ .../Preview/Mapping/RecordPathFinder.cs | 67 ++++--- Neo4j.Driver/Neo4j.Driver/Public/IRecord.cs | 45 ++++- .../Neo4j.Driver/Public/Types/IEntity.cs | 15 ++ 29 files changed, 835 insertions(+), 488 deletions(-) delete mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordNodeExtensionsTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Result/RecordTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/TestRecord.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Preview/Mapping/EntityExtensions.cs delete mode 100644 Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordEntityExtensions.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordExtensions.cs diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/BuiltMapperTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/BuiltMapperTests.cs index 5f24401aa..c7871d2c1 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/BuiltMapperTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/BuiltMapperTests.cs @@ -16,10 +16,9 @@ using System; using FluentAssertions; using Neo4j.Driver.Preview.Mapping; +using Neo4j.Driver.Tests.TestUtil; using Xunit; -using Record = Neo4j.Driver.Internal.Result.Record; - namespace Neo4j.Driver.Tests.Mapping; public class BuiltMapperTests @@ -49,7 +48,7 @@ public void ShouldUseConstructorWhenInstructed() var constructor = typeof(NoParameterlessConstructor).GetConstructors()[0]; mapper.AddConstructorMapping(constructor); - var result = mapper.Map(new Record(new[] { "value" }, new object[] { 48 })); + var result = mapper.Map(TestRecord.Create(new[] { "value" }, new object[] { 48 })); result.Value.Should().Be(48); } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DefaultMapperTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DefaultMapperTests.cs index 41ac2c645..b6c516548 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DefaultMapperTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DefaultMapperTests.cs @@ -18,8 +18,8 @@ using FluentAssertions; using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Preview.Mapping; +using Neo4j.Driver.Tests.TestUtil; using Xunit; -using Record = Neo4j.Driver.Internal.Result.Record; namespace Neo4j.Driver.Tests.Mapping; @@ -36,7 +36,7 @@ public void ShouldMapSimpleClass() { var mapper = DefaultMapper.Get(); - var record = new Record(new[] { "id", "name" }, new object[] { 1, "Foo" }); + var record = TestRecord.Create(new[] { "id", "name" }, new object[] { 1, "Foo" }); var result = mapper.Map(record); result.Id.Should().Be(1); @@ -60,7 +60,7 @@ public void ShouldMapConstructorClass() { var mapper = DefaultMapper.Get(); - var record = new Record(new[] { "id", "name" }, new object[] { 1, "Foo" }); + var record = TestRecord.Create(new[] { "id", "name" }, new object[] { 1, "Foo" }); var result = mapper.Map(record); result.Id.Should().Be(1); @@ -91,7 +91,7 @@ public void ShouldMapNonDefaultConstructorClass() { var mapper = DefaultMapper.Get(); - var record = new Record(new[] { "id", "name" }, new object[] { 1, "Foo" }); + var record = TestRecord.Create(new[] { "id", "name" }, new object[] { 1, "Foo" }); var result = mapper.Map(record); result.Id.Should().Be(1); @@ -116,7 +116,7 @@ public Person( public void ShouldMapFromInsideDictionaries() { var dict = new Dictionary { { "name", "Dani" }, { "born", 1977 } }; - var record = new Record(new[] { "Person" }, new object[] { dict }); + var record = TestRecord.Create(new[] { "Person" }, new object[] { dict }); var mapper = DefaultMapper.Get(); var person = mapper.Map(record); person.Name.Should().Be("Dani"); @@ -126,7 +126,7 @@ public void ShouldMapFromInsideDictionaries() [Fact] public void ShouldThrowWhenConstructorParametersUnavailable() { - var record = new Record(new[] { "something" }, new object[] { 69 }); + var record = TestRecord.Create(new[] { "something" }, new object[] { 69 }); var mapper = DefaultMapper.Get(); var act = () => mapper.Map(record); act.Should().Throw(); @@ -135,7 +135,7 @@ public void ShouldThrowWhenConstructorParametersUnavailable() [Fact] public void ShouldMapFromNodesInRecords() { - var record = new Record( + var record = TestRecord.Create( new[] { "person" }, new object[] { @@ -166,7 +166,7 @@ public NaturalPhenomenon(string name, List components) [Fact] public void ShouldMapListsThroughConstructor() { - var record = new Record( + var record = TestRecord.Create( new[] { "name", "components" }, new object[] { "Hurricane", new List { "wind", "rain" } }); @@ -191,7 +191,7 @@ public NaturalPhenomenonCommaSeparated(string name, string components) [Fact] public void ShouldMapCommaSeparatedListsThroughConstructor() { - var record = new Record( + var record = TestRecord.Create( new[] { "name", "components" }, new object[] { "Hurricane", new List { "wind", "rain" } }); @@ -216,7 +216,7 @@ public HistoricalPhenomenon(NaturalPhenomenon phenomenon, int year) [Fact] public void ShouldMapNestedObjectsThroughConstructor() { - var record = new Record( + var record = TestRecord.Create( new[] { "phenomenon", "year" }, new object[] { @@ -267,7 +267,7 @@ public void ShouldMapListsOfNodesThroughConstructor() var phenomena = new List { firstPhenomenon, secondPhenomenon, thirdPhenomenon }; - var record = new Record( + var record = TestRecord.Create( new[] { "year", "phenomena" }, new object[] { 2021, phenomena }); @@ -299,7 +299,7 @@ public ClassWithProperties(int year, string occurrence) [Fact] public void ShouldSetPropertiesNotSetInConstructor() { - var record = new Record( + var record = TestRecord.Create( new[] { "year", "occurrence", "description", "something" }, new object[] { 2020, "PANDEMIC", "Covid-19", "something" }); @@ -331,7 +331,7 @@ public ClassWithPropertiesWithMappingHints( [Fact] public void ShouldSetPropertiesNotSetInConstructorWithMappingHints() { - var record = new Record( + var record = TestRecord.Create( new[] { "year", "occurrence", "description", "something" }, new object[] { 2021, "PANDEMIC", "Covid-19", "something" }); diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs index c3a48df19..a192e9ff5 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs @@ -18,8 +18,8 @@ using FluentAssertions; using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Preview.Mapping; +using Neo4j.Driver.Tests.TestUtil; using Xunit; -using Record = Neo4j.Driver.Internal.Result.Record; namespace Neo4j.Driver.Tests.Mapping; @@ -28,7 +28,7 @@ public class DictAsRecordTests [Fact] public void ShouldUsePropertiesOfDict() { - var originalRecord = new Record(new[] { "name", "age" }, new object[] { "Bob", 42 }); + var originalRecord = TestRecord.Create(new[] { "name", "age" }, new object[] { "Bob", 42 }); var dict = new Dictionary { { "key1", "value1" }, @@ -43,13 +43,13 @@ public void ShouldUsePropertiesOfDict() subject[0].Should().Be("value1"); subject[1].Should().Be("value2"); subject["key1"].Should().Be("value1"); - subject["key2"].Should().Be("value2"); + subject["KEY2"].Should().Be("value2"); } [Fact] public void ShouldUsePropertiesOfEntity() { - var originalRecord = new Record(new[] { "name", "age" }, new object[] { "Bob", 42 }); + var originalRecord = TestRecord.Create(new[] { "name", "age" }, new object[] { "Bob", 42 }); var entity = new Node( 1, new[] { "Person" }, @@ -63,7 +63,7 @@ public void ShouldUsePropertiesOfEntity() subject[0].Should().Be("value1"); subject[1].Should().Be("value2"); subject["key1"].Should().Be("value1"); - subject["key2"].Should().Be("value2"); + subject["KEY2"].Should().Be("value2"); } [Fact] diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/LabelCaptureTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/LabelCaptureTests.cs index 0608ae09c..441b19aeb 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/LabelCaptureTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/LabelCaptureTests.cs @@ -18,8 +18,8 @@ using FluentAssertions; using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Preview.Mapping; +using Neo4j.Driver.Tests.TestUtil; using Xunit; -using Record = Neo4j.Driver.Internal.Result.Record; namespace Neo4j.Driver.Tests.Mapping; @@ -46,7 +46,7 @@ public LabelCaptureTests() public void ShouldCaptureSingleNodeLabel() { var node = new Node(1, new[] { "Test" }, new Dictionary()); - var record = new Record(new[] { "Person" }, new object[] { node }); + var record = TestRecord.Create(new[] { "Person" }, new object[] { node }); var mapped = record.AsObject(); @@ -57,7 +57,7 @@ public void ShouldCaptureSingleNodeLabel() public void ShouldCaptureMultipleNodeLabelsIntoString() { var node = new Node(1, new[] { "Alpha", "Bravo", "Charlie" }, new Dictionary()); - var record = new Record(new[] { "Person" }, new object[] { node }); + var record = TestRecord.Create(new[] { "Person" }, new object[] { node }); var mapped = record.AsObject(); @@ -68,7 +68,7 @@ public void ShouldCaptureMultipleNodeLabelsIntoString() public void ShouldCaptureRelationshipType() { var node = new Relationship(1, 2, 3, "ACTED_IN", new Dictionary()); - var record = new Record(new[] { "Relationship" }, new object[] { node }); + var record = TestRecord.Create(new[] { "Relationship" }, new object[] { node }); var mapped = record.AsObject(); @@ -105,7 +105,7 @@ public void ShouldCaptureAndConvertLabels() { RecordObjectMapping.RegisterProvider(); var node = new Node(1, new[] { "Alpha", "Bravo", "Charlie" }, new Dictionary()); - var record = new Record(new[] { "Person" }, new object[] { node }); + var record = TestRecord.Create(new[] { "Person" }, new object[] { node }); var mapped = record.AsObject(); @@ -118,10 +118,10 @@ public void ShouldCaptureAndConvertRelationshipType() { RecordObjectMapping.RegisterProvider(); var node = new Relationship(1, 2, 3, "ACTED_IN", new Dictionary()); - var record = new Record(new[] { "Relationship" }, new object[] { node }); + var record = TestRecord.Create(new[] { "Relationship" }, new object[] { node }); var mapped = record.AsObject(); mapped.RelationshipType.Should().Be("acted_in"); } -} \ No newline at end of file +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappedListCreatorTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappedListCreatorTests.cs index c067e64f2..6df16271b 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappedListCreatorTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappedListCreatorTests.cs @@ -72,7 +72,9 @@ public void ShouldCreateListOfMappedObjectsFromDictionaries() [Fact] public void ShouldCreateListOfMappedObjectsFromNodes() { - var list = new List { Mock.Of(), Mock.Of(), Mock.Of() }; + var mockEntity = new Mock(); + mockEntity.Setup(x => x.Properties).Returns(new Dictionary()); + var list = new List { mockEntity.Object, mockEntity.Object, mockEntity.Object }; var record = Mock.Of(); var people = new List { new("Alan", 99), new("Basil", 999), new("David", 9999) }; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingProviderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingProviderTests.cs index 5a1ba98f0..f1991bb57 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingProviderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingProviderTests.cs @@ -15,8 +15,8 @@ using FluentAssertions; using Neo4j.Driver.Preview.Mapping; +using Neo4j.Driver.Tests.TestUtil; using Xunit; -using Record = Neo4j.Driver.Internal.Result.Record; namespace Neo4j.Driver.Tests.Mapping; @@ -60,14 +60,14 @@ public void CreateMappers(IMappingRegistry registry) .MapWholeObject( r => new SecondTestObject { - Number = r.GetValue("intValue") + 1, - Text = r.GetValue("stringValue").ToLower() + Number = r.Get("intValue") + 1, + Text = r.Get("stringValue").ToLower() })) .RegisterMapping(_ => {}) .RegisterMapping( b => b .UseDefaultMapping() - .Map(x => x.Age, r => r.GetValue("active") - r.GetValue("born"))); + .Map(x => x.Age, r => r.Get("active") - r.Get("born"))); } } @@ -79,7 +79,7 @@ public MappingProviderTests() [Fact] public void ShouldOverrideDefaultMapping() { - var record = new Record(new[] { "stringValue", "intValue" }, new object[] { "test", 69 }); + var record = TestRecord.Create(new[] { "stringValue", "intValue" }, new object[] { "test", 69 }); RecordObjectMapping.RegisterProvider(); var obj = record.AsObject(); @@ -91,7 +91,7 @@ public void ShouldOverrideDefaultMapping() [Fact] public void ShouldUseWholeObjectMapping() { - var record = new Record(new[] { "stringValue", "intValue" }, new object[] { "TEST", 100 }); + var record = TestRecord.Create(new[] { "stringValue", "intValue" }, new object[] { "TEST", 100 }); RecordObjectMapping.RegisterProvider(); var obj = record.AsObject(); @@ -103,7 +103,7 @@ public void ShouldUseWholeObjectMapping() [Fact] public void ShouldNotUseDefaultMapperIfEmptyMappingConfigInProvider() { - var record = new Record(new[] { "stringValue", "intValue" }, new object[] { "TEST", 100 }); + var record = TestRecord.Create(new[] { "stringValue", "intValue" }, new object[] { "TEST", 100 }); RecordObjectMapping.RegisterProvider(); var obj = record.AsObject(); @@ -115,7 +115,7 @@ public void ShouldNotUseDefaultMapperIfEmptyMappingConfigInProvider() [Fact] public void ShouldMapPropertiesFromRecordIfRequired() { - var record = new Record(new[] { "name", "born", "active" }, new object[] { "Bob", 1977, 2000 }); + var record = TestRecord.Create(new[] { "name", "born", "active" }, new object[] { "Bob", 1977, 2000 }); RecordObjectMapping.RegisterProvider(); var obj = record.AsObject(); @@ -123,4 +123,4 @@ public void ShouldMapPropertiesFromRecordIfRequired() obj.Name.Should().Be("Bob"); obj.Age.Should().Be(23); } -} \ No newline at end of file +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingSourceDelegateBuilderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingSourceDelegateBuilderTests.cs index 558d7022e..6d01b88f3 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingSourceDelegateBuilderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingSourceDelegateBuilderTests.cs @@ -17,8 +17,8 @@ using FluentAssertions; using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Preview.Mapping; +using Neo4j.Driver.Tests.TestUtil; using Xunit; -using Record = Neo4j.Driver.Internal.Result.Record; namespace Neo4j.Driver.Tests.Mapping; @@ -27,7 +27,7 @@ public class MappingSourceDelegateBuilderTests [Fact] public void ShouldGetSimplePaths() { - var record = new Record(new[] { "a" }, new object[] { "b" }); + var record = TestRecord.Create(new[] { "a" }, new object[] { "b" }); var getter = new MappingSourceDelegateBuilder(); var mappingSource = new EntityMappingInfo("a", EntityMappingSource.Property); @@ -41,7 +41,7 @@ public void ShouldGetSimplePaths() [Fact] public void ShouldReturnFalseWhenPathNotFound() { - var record = new Record(new[] { "a" }, new object[] { "b" }); + var record = TestRecord.Create(new[] { "a" }, new object[] { "b" }); var getter = new MappingSourceDelegateBuilder(); var mappingSource = new EntityMappingInfo("c", EntityMappingSource.Property); @@ -55,9 +55,10 @@ public void ShouldReturnFalseWhenPathNotFound() public void ShouldGetNodeLabels() { var node = new Node(1, new[] { "Actor", "Director" }, new Dictionary()); - var record = new Record(new[] { "a" }, new object[] { node }); + var record = TestRecord.Create(new[] { "a" }, new object[] { node }); var getter = new MappingSourceDelegateBuilder(); - var mappingSource = new EntityMappingInfo("a", EntityMappingSource.NodeLabel); + var mappingSource = new EntityMappingInfo("zzz", EntityMappingSource.NodeLabel); + mappingSource = mappingSource with { Path = "a" }; var mappingDelegate = getter.GetMappingDelegate(mappingSource); var found = mappingDelegate(record, out var value); @@ -70,7 +71,7 @@ public void ShouldGetNodeLabels() public void ShouldGetRelationshipType() { var rel = new Relationship(1, 2, 3, "ACTED_IN", new Dictionary()); - var record = new Record(new[] { "a" }, new object[] { rel }); + var record = TestRecord.Create(new[] { "a" }, new object[] { rel }); var getter = new MappingSourceDelegateBuilder(); var mappingSource = new EntityMappingInfo("a", EntityMappingSource.RelationshipType); @@ -80,4 +81,4 @@ public void ShouldGetRelationshipType() found.Should().BeTrue(); value.Should().Be("ACTED_IN"); } -} \ No newline at end of file +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordMappingTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordMappingTests.cs index 27c92ed59..7ed85e048 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordMappingTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordMappingTests.cs @@ -19,8 +19,8 @@ using FluentAssertions; using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Preview.Mapping; +using Neo4j.Driver.Tests.TestUtil; using Xunit; -using Record = Neo4j.Driver.Internal.Result.Record; namespace Neo4j.Driver.Tests.Mapping; @@ -36,7 +36,7 @@ private class TestPerson [Fact] public void ShouldMapPrimitives() { - var record = new Record(new[] { "name", "born" }, new object[] { "Bob", 1977 }); + var record = TestRecord.Create(new[] { "name", "born" }, new object[] { "Bob", 1977 }); var person = record.AsObject(); person.Name.Should().Be("Bob"); person.Born.Should().Be(1977); @@ -45,7 +45,7 @@ public void ShouldMapPrimitives() [Fact] public void ShouldMapList() { - var record = new Record(new[] { "hobbies" }, new object[] { new List { "Coding", "Swimming" } }); + var record = TestRecord.Create(new[] { "hobbies" }, new object[] { new List { "Coding", "Swimming" } }); var person = record.AsObject(); person.Hobbies.Should().BeEquivalentTo("Coding", "Swimming"); } @@ -63,7 +63,7 @@ private class PersonInDict public void ShouldMapFromInsideDictionaries() { var dict = new Dictionary { { "name", "Dani" }, { "born", 1977 } }; - var record = new Record(new[] { "Person" }, new object[] { dict }); + var record = TestRecord.Create(new[] { "Person" }, new object[] { dict }); var person = record.AsObject(); person.Name.Should().Be("Dani"); person.Born.Should().Be(1977); @@ -72,7 +72,7 @@ public void ShouldMapFromInsideDictionaries() [Fact] public void ShouldLeaveDefaultsIfFieldAbsent() { - var record = new Record(new[] { "born" }, new object[] { 1977 }); + var record = TestRecord.Create(new[] { "born" }, new object[] { 1977 }); var person = RecordObjectMapping.Map(record, typeof(TestPerson)) as TestPerson; person.Should().NotBeNull(); person!.Name.Should().Be("A. Test Name"); @@ -165,7 +165,7 @@ public void ShouldMapComplexObjects() var moviesDict = new List> { movie4, movie5 }; - var record = new Record( + var record = TestRecord.Create( new[] { "person", "movies", "titles", "moviesDict" }, new object[] { person, movieNodes, stringList, moviesDict }); @@ -203,9 +203,9 @@ public void ShouldMapAllRecords() { Task>> GetRecordsAsync() { - var record1 = new Record(new[] { "name", }, new object[] { "Bob", }); - var record2 = new Record(new[] { "name", "born" }, new object[] { "Alice", 1988 }); - var record3 = new Record(new[] { "name", "born" }, new object[] { "Eve", 1999 }); + var record1 = TestRecord.Create(new[] { "name", }, new object[] { "Bob", }); + var record2 = TestRecord.Create(new[] { "name", "born" }, new object[] { "Alice", 1988 }); + var record3 = TestRecord.Create(new[] { "name", "born" }, new object[] { "Eve", 1999 }); var result = new EagerResult>( new List { record1, record2, record3 }, @@ -229,9 +229,9 @@ public async Task ShouldMapAllRecordsFromCursor() { async IAsyncEnumerable GetRecordsAsync() { - var record1 = new Record(new[] { "name", }, new object[] { "Bob", }); - var record2 = new Record(new[] { "name", "born" }, new object[] { "Alice", 1988 }); - var record3 = new Record(new[] { "name", "born" }, new object[] { "Eve", 1999 }); + var record1 = TestRecord.Create(new[] { "name", }, new object[] { "Bob", }); + var record2 = TestRecord.Create(new[] { "name", "born" }, new object[] { "Alice", 1988 }); + var record3 = TestRecord.Create(new[] { "name", "born" }, new object[] { "Eve", 1999 }); var result = new List { record1, record2, record3 }; @@ -255,9 +255,9 @@ public async Task ShouldMapRecordsAsyncEnumerable() { async IAsyncEnumerable GetRecordsAsync() { - var record1 = new Record(new[] { "name", }, new object[] { "Bob", }); - var record2 = new Record(new[] { "name", "born" }, new object[] { "Alice", 1988 }); - var record3 = new Record(new[] { "name", "born" }, new object[] { "Eve", 1999 }); + var record1 = TestRecord.Create(new[] { "name", }, new object[] { "Bob", }); + var record2 = TestRecord.Create(new[] { "name", "born" }, new object[] { "Alice", 1988 }); + var record3 = TestRecord.Create(new[] { "name", "born" }, new object[] { "Eve", 1999 }); var result = new List { record1, record2, record3 }; @@ -326,7 +326,7 @@ public void ShouldMapSubNodesWithAbsolutePaths() { "title", "Mona Lisa" } }); - var carAndPaintingRecord = new Record(new[] { "car", "painting" }, new object[] { carNode, paintingNode }); + var carAndPaintingRecord = TestRecord.Create(new[] { "car", "painting" }, new object[] { carNode, paintingNode }); var mappedObject = carAndPaintingRecord.AsObject(); @@ -346,7 +346,7 @@ private class PersonWithoutBornSetter [Fact] public void DefaultMapperShouldIgnorePropertiesWithoutSetter() { - var record = new Record(new[] { "name", "born" }, new object[] { "Bob", 1977 }); + var record = TestRecord.Create(new[] { "name", "born" }, new object[] { "Bob", 1977 }); var person = record.AsObject(); person.Name.Should().Be("Bob"); person.Born.Should().Be(1999); @@ -363,7 +363,7 @@ private class TestPersonWithoutBornMapped [Fact] public void ShouldIgnorePropertiesWithDoNotMapAttribute() { - var record = new Record(new[] { "name", "born" }, new object[] { "Bob", 1977 }); + var record = TestRecord.Create(new[] { "name", "born" }, new object[] { "Bob", 1977 }); var person = record.AsObject(); person.Name.Should().Be("Bob"); person.Born.Should().Be(9999); @@ -385,8 +385,8 @@ public void ShouldMapEntitiesWithListsOfNodes() { var bookNodeList = new List { - new Node(0, new[] { "Book" }, new Dictionary { { "title", "The Green Man" } }), - new Node(0, new[] { "Book" }, new Dictionary { { "title", "The Thin End" } }) + new(0, new[] { "Book" }, new Dictionary { { "title", "The Green Man" } }), + new(0, new[] { "Book" }, new Dictionary { { "title", "The Thin End" } }) }; var authorNode = new Node( @@ -394,7 +394,7 @@ public void ShouldMapEntitiesWithListsOfNodes() new[] { "Author" }, new Dictionary { { "name", "Kate Grenville" }, { "books", bookNodeList } }); - var record = new Record(new[] { "author" }, new object[] { authorNode }); + var record = TestRecord.Create(new[] { "author" }, new object[] { authorNode }); var mappedObject = record.AsObject(); @@ -412,7 +412,7 @@ private record Song( [Fact] public void ShouldMapToRecords() { - var record = new Record( + var record = TestRecord.Create( new[] { "recordingArtist", "title", "year" }, new object[] { "The Beatles", "Yellow Submarine", 1966 }); @@ -425,7 +425,7 @@ public void ShouldMapToRecords() [Fact] public void ShouldFailMappingToRecordsWithNulls() { - var record = new Record( + var record = TestRecord.Create( new[] { "recordingArtist", "title", "year" }, new object[] { "The Beatles", null, 1966 }); @@ -437,7 +437,7 @@ public void ShouldFailMappingToRecordsWithNulls() [Fact] public void ShouldFailMappingToRecordsWithMissingFields() { - var record = new Record( + var record = TestRecord.Create( new[] { "recordingArtist", "year" }, new object[] { "The Beatles", 1966 }); @@ -455,7 +455,7 @@ private class ClassWithInitProperties [Fact] public void ShouldMapToInitProperties() { - var record = new Record(new[] { "name", "age" }, new object[] { "Bob", 1977 }); + var record = TestRecord.Create(new[] { "name", "age" }, new object[] { "Bob", 1977 }); var person = record.AsObject(); person.Name.Should().Be("Bob"); person.Age.Should().Be(1977); @@ -470,7 +470,7 @@ private class ClassWithDefaultConstructor(string forename, int age) [Fact] public void ShouldMapToDefaultConstructorParameters() { - var record = new Record(new[] { "forename", "age" }, new object[] { "Bob", 1977 }); + var record = TestRecord.Create(new[] { "forename", "age" }, new object[] { "Bob", 1977 }); var person = record.AsObject(); person.Name.Should().Be("Bob"); person.Age.Should().Be(1977); @@ -485,9 +485,48 @@ private class ClassWithDefaultConstructorWithAttributes([MappingSource("forename [Fact] public void ShouldMapToDefaultConstructorParametersWithAttributes() { - var record = new Record(new[] { "forename", "age" }, new object[] { "Bob", 1977 }); + var record = TestRecord.Create(new[] { "forename", "age" }, new object[] { "Bob", 1977 }); var person = record.AsObject(); person.Name.Should().Be("Bob"); person.Age.Should().Be(1977); } + + [Fact] + public void ShouldFindPropertiesInNodes() + { + var node = new Node( + 0, + new[] { "Person" }, + new Dictionary { { "name", "Bob" }, { "born", 1977 } }); + + var record = TestRecord.Create(new[] { "person" }, new object[] { node }); + var person = record.AsObject(); + person.Name.Should().Be("Bob"); + person.Born.Should().Be(1977); + } + + [Fact] + public void ShouldFindPropertiesInDictionaries() + { + var dict = new Dictionary { { "name", "Bob" }, { "born", 1977 } }; + var record = TestRecord.Create(new[] { "person" }, new object[] { dict }); + var person = record.AsObject(); + person.Name.Should().Be("Bob"); + person.Born.Should().Be(1977); + } + + [Fact] + public void ShouldMapEntityToObjectThroughAsRecord() + { + var node = new Node( + 0, + new[] { "Person" }, + new Dictionary { { "name", "Bob" }, { "born", 1977 } }); + + var record = node.AsRecord(); + + var person = record.AsObject(); + person.Name.Should().Be("Bob"); + person.Born.Should().Be(1977); + } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordNodeExtensionsTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordNodeExtensionsTests.cs deleted file mode 100644 index 86142b2a6..000000000 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordNodeExtensionsTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) "Neo4j" -// Neo4j Sweden AB [https://neo4j.com] -// -// Licensed under the Apache License, Version 2.0 (the "License"). -// You may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System.Collections.Generic; -using FluentAssertions; -using Neo4j.Driver.Internal.Types; -using Neo4j.Driver.Preview.Mapping; -using Xunit; -using Record = Neo4j.Driver.Internal.Result.Record; - -namespace Neo4j.Driver.Tests.Mapping; - -public class RecordNodeExtensionsTests -{ - [Fact] - public void ShouldGetTypedValueFromRecord_string() - { - var record = new Record(new[] { "key" }, new object[] { "value" }); - record.GetString("key").Should().Be("value"); - } - - [Fact] - public void ShouldGetTypedValueFromRecord_int() - { - var record = new Record(new[] { "key" }, new object[] { 1L }); - record.GetInt("key").Should().Be(1); - } - - [Fact] - public void ShouldGetTypedValueFromRecord_long() - { - var record = new Record(new[] { "key" }, new object[] { 1L }); - record.GetLong("key").Should().Be(1L); - } - - [Fact] - public void ShouldGetTypedValueFromRecord_double() - { - var record = new Record(new[] { "key" }, new object[] { 1.0 }); - record.GetDouble("key").Should().Be(1.0); - } - - [Fact] - public void ShouldGetTypedValueFromRecord_float() - { - var record = new Record(new[] { "key" }, new object[] { 1.0 }); - record.GetFloat("key").Should().Be(1.0f); - } - - [Fact] - public void ShouldGetTypedValueFromRecord_bool() - { - var record = new Record(new[] { "key" }, new object[] { true }); - record.GetBool("key").Should().Be(true); - } - - [Fact] - public void ShouldGetTypedValueFromRecord_entity() - { - var node = new Node(1, new[] { "Node" }, new Dictionary { { "key", "value" } }); - var record = new Record(new[] { "key" }, new object[] { node }); - record.GetEntity("key").Should().Be(node); - } - - [Fact] - public void ShouldGetValueFromEntity_string() - { - var node = new Node(1, new[] { "Node" }, new Dictionary { { "key", "value" } }); - node.GetString("key").Should().Be("value"); - } - - [Fact] - public void ShouldGetValueFromEntity_int() - { - var node = new Node(1, new[] { "Node" }, new Dictionary { { "key", 1L } }); - node.GetInt("key").Should().Be(1); - } - - [Fact] - public void ShouldGetValueFromEntity_long() - { - var node = new Node(1, new[] { "Node" }, new Dictionary { { "key", 1L } }); - node.GetLong("key").Should().Be(1L); - } - - [Fact] - public void ShouldGetValueFromEntity_double() - { - var node = new Node(1, new[] { "Node" }, new Dictionary { { "key", 1.0 } }); - node.GetDouble("key").Should().Be(1.0); - } - - [Fact] - public void ShouldGetValueFromEntity_float() - { - var node = new Node(1, new[] { "Node" }, new Dictionary { { "key", 1.0 } }); - node.GetFloat("key").Should().Be(1.0f); - } - - [Fact] - public void ShouldGetValueFromEntity_bool() - { - var node = new Node(1, new[] { "Node" }, new Dictionary { { "key", true } }); - node.GetBool("key").Should().Be(true); - } - - [Fact] - public void ShouldGetValueFromEntity_Dictionary() - { - var dictionary = new Dictionary { { "key", "value" } }; - var node = new Node(1, new[] { "Node" }, new Dictionary { { "field", dictionary } }); - node.GetValue>("field").Should().BeEquivalentTo(dictionary); - } -} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs index ad6d99463..a70842962 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs @@ -15,74 +15,133 @@ using System.Collections.Generic; using FluentAssertions; -using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Preview.Mapping; +using Neo4j.Driver.Tests.TestUtil; using Xunit; -using Record = Neo4j.Driver.Internal.Result.Record; namespace Neo4j.Driver.Tests.Mapping; public class RecordPathFinderTests { [Fact] - public void ShouldFindSimplePath() + public void TryGetValueByPath_PathMatchesFieldName_ReturnsTrue() { - var record = new Record(new[] { "a" }, new object[] { "b" }); - var finder = new RecordPathFinder(); + var recordPathFinder = new RecordPathFinder(); + var record = TestRecord.Create(new[] { "testField" }, new object[] { "testValue" }); - var found = finder.TryGetValueByPath(record, "a", out var value); + var result = recordPathFinder.TryGetValueByPath(record, "TesTfIELd", out var value); - found.Should().BeTrue(); - value.Should().Be("b"); + result.Should().BeTrue(); + value.Should().Be("testValue"); } [Fact] - public void ShouldReturnFalseWhenPathNotFound() + public void TryGetValueByPath_PathDoesNotMatchFieldName_ReturnsFalse() { - var record = new Record(new[] { "a" }, new object[] { "b" }); - var finder = new RecordPathFinder(); + var recordPathFinder = new RecordPathFinder(); + var record = TestRecord.Create(new[] { "testField" }, new object[] { "testValue" }); - var found = finder.TryGetValueByPath(record, "c", out var value); + var result = recordPathFinder.TryGetValueByPath(record, "nonExistentField", out var value); - found.Should().BeFalse(); + result.Should().BeFalse(); + value.Should().BeNull(); } [Fact] - public void ShouldFindSimplePathNestedInANode() + public void TryGetValueByPath_PathContainsDotAndMatchesFieldNameAndPropertyName_ReturnsTrue() { - var node = new Node(1, new[] { "Test" }, new Dictionary() { { "name", "Bob" } }); - var record = new Record(new[] { "person" }, new object[] { node }); - var finder = new RecordPathFinder(); + var recordPathFinder = new RecordPathFinder(); + var record = TestRecord.Create( + new[] { "testField" }, + new object[] { new Dictionary { { "testProperty", "testValue" } } }); - var found = finder.TryGetValueByPath(record, "name", out var value); + var result = recordPathFinder.TryGetValueByPath(record, "TESTfield.testProperty", out var value); - found.Should().BeTrue(); - value.Should().Be("Bob"); + result.Should().BeTrue(); + value.Should().Be("testValue"); } [Fact] - public void ShouldFindComplexPathNestedInANode() + public void TryGetValueByPath_PathContainsDotButDoesNotMatchFieldName_ReturnsFalse() { - var node = new Node(1, new[] { "Test" }, new Dictionary() { { "name", "Bob" } }); - var record = new Record(new[] { "person" }, new object[] { node }); - var finder = new RecordPathFinder(); + var recordPathFinder = new RecordPathFinder(); + var record = TestRecord.Create( + new[] { "testField" }, + new object[] { new Dictionary { { "testProperty", "testValue" } } }); - var found = finder.TryGetValueByPath(record, "person.name", out var value); + var result = recordPathFinder.TryGetValueByPath(record, "nonExistentField.testProperty", out var value); - found.Should().BeTrue(); - value.Should().Be("Bob"); + result.Should().BeFalse(); + value.Should().BeNull(); } [Fact] - public void ShouldFindComplexPathNestedInADictionary() + public void TryGetValueByPath_PathContainsDotMatchesFieldNameButNotPropertyName_ReturnsFalse() { - var dictionary = new Dictionary() { { "name", "Bob" } }; - var record = new Record(new[] { "person" }, new object[] { dictionary }); - var finder = new RecordPathFinder(); + var recordPathFinder = new RecordPathFinder(); + var record = TestRecord.Create( + new[] { "testField" }, + new object[] { new Dictionary { { "testProperty", "testValue" } } }); - var found = finder.TryGetValueByPath(record, "person.name", out var value); + var result = recordPathFinder.TryGetValueByPath(record, "testField.nonExistentProperty", out var value); - found.Should().BeTrue(); - value.Should().Be("Bob"); + result.Should().BeFalse(); + value.Should().BeNull(); } -} \ No newline at end of file + + [Fact] + public void TryGetValueByPath_PathContainsDotAndMatchesFieldNameAndPropertyName_ReturnsTrue_CaseInsensitive() + { + var recordPathFinder = new RecordPathFinder(); + var record = TestRecord.Create( + new[] { "testField" }, + new object[] { new Dictionary { { "testProperty", "testValue" } } }); + + var result = recordPathFinder.TryGetValueByPath(record, "testField.TESTproperty", out var value); + + result.Should().BeTrue(); + value.Should().Be("testValue"); + } + + [Fact] + public void TryGetValueByPath_PathContainsDotButDoesNotMatchFieldName_ReturnsFalse_CaseInsensitive() + { + var recordPathFinder = new RecordPathFinder(); + var record = TestRecord.Create( + new[] { "testField" }, + new object[] { new Dictionary { { "testProperty", "testValue" } } }); + + var result = recordPathFinder.TryGetValueByPath(record, "nonExistentField.testProperty", out var value); + + result.Should().BeFalse(); + value.Should().BeNull(); + } + + [Fact] + public void TryGetValueByPath_PathContainsDotMatchesFieldNameButNotPropertyName_ReturnsFalse_CaseInsensitive() + { + var recordPathFinder = new RecordPathFinder(); + var record = TestRecord.Create( + new[] { "testField" }, + new object[] { new Dictionary { { "testProperty", "testValue" } } }); + + var result = recordPathFinder.TryGetValueByPath(record, "testField.nonExistentProperty", out var value); + + result.Should().BeFalse(); + value.Should().BeNull(); + } + + [Fact] + public void TryGetValueByPath_PathContainsDotMatchesFieldButNotEntity_ReturnsFalse_CaseInsensitive() + { + var recordPathFinder = new RecordPathFinder(); + var record = TestRecord.Create( + new[] { "testField" }, + new[] { "testValue" }); + + var result = recordPathFinder.TryGetValueByPath(record, "testField.nonExistentProperty", out var value); + + result.Should().BeFalse(); + value.Should().BeNull(); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Reactive/Internal/InternalRxResultTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Reactive/Internal/InternalRxResultTests.cs index f2971844e..2c9a382b3 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Reactive/Internal/InternalRxResultTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Reactive/Internal/InternalRxResultTests.cs @@ -26,6 +26,7 @@ using Neo4j.Driver.Internal.Result; using Neo4j.Driver.Tests.Reactive.Utils; using Neo4j.Driver.Tests.Result; +using Neo4j.Driver.Tests.TestUtil; using Xunit; using Xunit.Abstractions; using static Microsoft.Reactive.Testing.ReactiveTest; @@ -405,7 +406,7 @@ public static IEnumerable CreateRecords(string[] fields, int recordCoun Thread.Sleep(delayMs); } - yield return new Record( + yield return TestRecord.Create( fields, Enumerable.Range(1, fields.Length).Select(f => $"{i:D3}_{f:D2}").Cast().ToArray()); } @@ -538,7 +539,7 @@ IEnumerable GenerateRecords() { for (var r = 1; r <= recordCount; r++) { - yield return new Record( + yield return TestRecord.Create( keys, Enumerable.Range(1, keyCount).Select(f => $"{r:D3}_{f:D2}").Cast().ToArray()); } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Reactive/Utils/Utils.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Reactive/Utils/Utils.cs index a232f7241..bd6d460c2 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Reactive/Utils/Utils.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Reactive/Utils/Utils.cs @@ -14,12 +14,10 @@ // limitations under the License. using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using FluentAssertions; using FluentAssertions.Equivalency; -using Neo4j.Driver.Internal; +using Neo4j.Driver.Tests.TestUtil; using static Neo4j.Driver.Tests.TestUtil.Assertions; namespace Neo4j.Driver.Tests.Reactive.Utils; @@ -40,13 +38,7 @@ public static object Record(string[] keys, params object[] fields) throw new ArgumentOutOfRangeException(nameof(keys), $"{nameof(keys)} should contain at least 1 item."); } - return new - { - Keys = keys, - Values = Enumerable.Range(0, keys.Length) - .Select(i => new KeyValuePair(keys[i], fields[i])) - .ToDictionary() - }; + return TestRecord.Create(keys, fields); } public static Func MatchesKeys(params string[] keys) @@ -56,7 +48,11 @@ public static Func MatchesKeys(params string[] keys) public static Func MatchesRecord(string[] keys, params object[] fields) { - return r => Matches(() => r.Should().BeEquivalentTo(Record(keys, fields))); + return Matches(rec => + { + rec.Keys.Should().BeEquivalentTo(keys); + rec.Values.Values.Should().BeEquivalentTo(fields); + }); } public static Func MatchesSummary( diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Result/RecordSetTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Result/RecordSetTests.cs index 443b71190..dc3021b28 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Result/RecordSetTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Result/RecordSetTests.cs @@ -20,6 +20,7 @@ using System.Threading.Tasks; using FluentAssertions; using Neo4j.Driver.Internal; +using Neo4j.Driver.Tests.TestUtil; using Xunit; using Record = Neo4j.Driver.Internal.Result.Record; @@ -146,7 +147,7 @@ public static IList CreateRecords(int recordSize, int keySize = 1) public static IList CreateRecords(int recordSize, string[] keys) { return Enumerable.Range(0, recordSize) - .Select(i => new Record(keys, keys.Select(k => $"record{i}:{k}").Cast().ToArray())) + .Select(i => TestRecord.Create(keys, keys.Select(k => $"record{i}:{k}").Cast().ToArray())) .Cast() .ToList(); } @@ -179,7 +180,7 @@ public async Task ShouldReturnRecordsAddedLatter() var cursor = new ListBasedRecordCursor(keys, () => records); // I add a new record after RecordSet is created - var newRecord = new Record(keys, new object[] { "record5:key0" }); + var newRecord = TestRecord.Create(keys, new object[] { "record5:key0" }); records.Add(newRecord); var i = 0; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Result/RecordTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Result/RecordTests.cs new file mode 100644 index 000000000..f70b641e1 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Result/RecordTests.cs @@ -0,0 +1,188 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using FluentAssertions; +using Xunit; +using Record = Neo4j.Driver.Internal.Result.Record; + +namespace Neo4j.Driver.Tests.Result; + +public class RecordTests +{ + private readonly Record _record; + + public RecordTests() + { + var fieldLookup = new Dictionary { { "Key1", 0 }, { "Key2", 1 } }; + var invariantFieldLookup = new Dictionary(fieldLookup, StringComparer.InvariantCultureIgnoreCase); + var fieldValues = new object[] { "Value1", "Value2" }; + _record = new Record(fieldLookup, invariantFieldLookup, fieldValues); + } + + [Fact] + public void Indexer_IntParameter_ReturnsCorrectValue() + { + _record[0].Should().Be("Value1"); + _record[1].Should().Be("Value2"); + } + + [Fact] + public void Indexer_StringParameter_ReturnsCorrectValue() + { + _record["Key1"].Should().Be("Value1"); + _record["Key2"].Should().Be("Value2"); + } + + [Fact] + public void Indexer_StringParameter_IsCaseSensitive() + { + var act = () => _record["KEY1"]; + act.Should().Throw(); + } + + [Fact] + public void Values_ReturnsCorrectDictionary() + { + var expectedDictionary = new Dictionary { { "Key1", "Value1" }, { "Key2", "Value2" } }; + _record.Values.Should().Equal(expectedDictionary); + } + + [Fact] + public void Keys_ReturnsCorrectList() + { + var expectedKeys = new List { "Key1", "Key2" }; + _record.Keys.Should().Equal(expectedKeys); + } + + [Fact] + public void Get_IsCaseSensitive() + { + _record.Get("Key1").Should().Be("Value1"); + _record.Invoking(r => r.Get("KEY1")).Should().Throw(); + _record.Get("Key2").Should().Be("Value2"); + _record.Invoking(r => r.Get("KEY2")).Should().Throw(); + } + + [Fact] + public void TryGet_IsCaseSensitive() + { + _record.TryGet("Key1", out var value).Should().BeTrue(); + value.Should().Be("Value1"); + _record.TryGet("KEY1", out _).Should().BeFalse(); + _record.TryGet("Key2", out value).Should().BeTrue(); + value.Should().Be("Value2"); + _record.TryGet("KEY2", out _).Should().BeFalse(); + } + + [Fact] + public void GetCaseInsensitive_IsCaseInsensitive() + { + _record.GetCaseInsensitive("Key1").Should().Be("Value1"); + _record.GetCaseInsensitive("KEY1").Should().Be("Value1"); + _record.GetCaseInsensitive("Key2").Should().Be("Value2"); + _record.GetCaseInsensitive("KEY2").Should().Be("Value2"); + } + + [Fact] + public void TryGetCaseInsensitive_IsCaseInsensitive() + { + _record.TryGetCaseInsensitive("Key1", out var value).Should().BeTrue(); + value.Should().Be("Value1"); + _record.TryGetCaseInsensitive("KEY1", out value).Should().BeTrue(); + value.Should().Be("Value1"); + _record.TryGetCaseInsensitive("Key2", out value).Should().BeTrue(); + value.Should().Be("Value2"); + _record.TryGetCaseInsensitive("KEY2", out value).Should().BeTrue(); + value.Should().Be("Value2"); + } + + [Fact] + public void TryGet_WithNonExistentKey_ReturnsFalse() + { + _record.TryGet("nonexistent", out var _).Should().BeFalse(); + } + + [Fact] + public void TryGetCaseInsensitive_WithNonExistentKey_ReturnsFalse() + { + _record.TryGetCaseInsensitive("nonexistent", out var _).Should().BeFalse(); + } + + [Fact] + public void GetEnumerator_ReturnsCorrectEnumerator() + { + var enumerable = _record as IEnumerable>; + using var enumerator = enumerable.GetEnumerator(); + enumerator.MoveNext(); + enumerator.Current.Should().Be(new KeyValuePair("Key1", "Value1")); + enumerator.MoveNext(); + enumerator.Current.Should().Be(new KeyValuePair("Key2", "Value2")); + enumerator.MoveNext().Should().BeFalse(); + } + + [Fact] + public void GetEnumerator_EnumeratesCorrectly() + { + var enumerable = _record as IEnumerable>; + var expected = new List> + { + new("Key1", "Value1"), + new("Key2", "Value2") + }; + + enumerable.Should().Equal(expected); + } + + [Fact] + public void ContainsKey_ReturnsCorrectResult() + { + var dictionary = _record as IReadOnlyDictionary; + dictionary.ContainsKey("Key1").Should().BeTrue(); + dictionary.ContainsKey("NonExistentKey").Should().BeFalse(); + } + + [Fact] + public void TryGetValue_ReturnsCorrectResult() + { + var dictionary = _record as IReadOnlyDictionary; + dictionary.TryGetValue("Key1", out var value).Should().BeTrue(); + value.Should().Be("Value1"); + dictionary.TryGetValue("NonExistentKey", out _).Should().BeFalse(); + } + + [Fact] + public void Keys_ReturnsCorrectKeys() + { + var dictionary = _record as IReadOnlyDictionary; + var expectedKeys = new List { "Key1", "Key2" }; + dictionary.Keys.Should().Equal(expectedKeys); + } + + [Fact] + public void Values_ReturnsCorrectValues() + { + var dictionary = _record as IReadOnlyDictionary; + dictionary.Values.Should().Equal("Value1", "Value2"); + } + + [Fact] + public void Count_ReturnsCorrectCount() + { + var dictionary = _record as IReadOnlyDictionary; + dictionary.Count.Should().Be(2); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Result/ResultCursorTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Result/ResultCursorTests.cs index eef96189a..13d04e298 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Result/ResultCursorTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Result/ResultCursorTests.cs @@ -21,6 +21,7 @@ using FluentAssertions; using Moq; using Neo4j.Driver.Internal.Result; +using Neo4j.Driver.Tests.TestUtil; using Xunit; using Xunit.Abstractions; using Record = Neo4j.Driver.Internal.Result.Record; @@ -169,7 +170,7 @@ public TestRecordYielder(int count, int total, ITestOutputHelper output) public static string[] Keys => new[] { "Test", "Keys" }; - public IEnumerable Records + public IEnumerable Records { get { @@ -190,7 +191,7 @@ public IEnumerable Records } } - public IEnumerable RecordsWithAutoLoad + public IEnumerable RecordsWithAutoLoad { get { @@ -222,7 +223,7 @@ private void Add(int count) { for (var i = 0; i < count; i++) { - _records.Add(new Record(Keys, new object[] { "Test", 123 })); + _records.Add(TestRecord.Create(Keys, new object[] { "Test", 123 })); } } } @@ -348,7 +349,7 @@ public async void ShouldReturnSameRecordIfPeekedTwice() peeked1.Should().NotBeNull(); var peeked2 = await result.PeekAsync(); peeked2.Should().NotBeNull(); - peeked2.Should().Be(peeked1); + peeked2.Should().BeSameAs(peeked1); } } @@ -364,7 +365,7 @@ public async void FetchAsyncAndCurrentWillReturnPeekedAfterPeek() read.Should().BeTrue(); var record = result.Current; record.Should().NotBeNull(); - record.Should().Be(peeked); + record.Should().BeSameAs(peeked); } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Result/ResultTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Result/ResultTests.cs index a6edf664a..2d01f3883 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Result/ResultTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Result/ResultTests.cs @@ -21,6 +21,7 @@ using System.Threading.Tasks; using FluentAssertions; using Neo4j.Driver.Internal; +using Neo4j.Driver.Tests.TestUtil; using Xunit; using Xunit.Abstractions; using Record = Neo4j.Driver.Internal.Result.Record; @@ -207,7 +208,7 @@ public IEnumerable Records while (i == _records.Count) { _output.WriteLine( - $"{DateTime.Now.ToString("HH:mm:ss.fff")} -> Waiting for more Records"); + $"{DateTime.Now:HH:mm:ss.fff} -> Waiting for more Records"); Thread.Sleep(50); } @@ -229,11 +230,11 @@ public IEnumerable RecordsWithAutoLoad while (i == _records.Count) { _output.WriteLine( - $"{DateTime.Now.ToString("HH:mm:ss.fff")} -> Waiting for more Records"); + $"{DateTime.Now:HH:mm:ss.fff} -> Waiting for more Records"); Thread.Sleep(500); AddNew(1); - _output.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} -> Record arrived"); + _output.WriteLine($"{DateTime.Now:HH:mm:ss.fff} -> Record arrived"); } yield return _records[i]; @@ -252,7 +253,7 @@ private void Add(int count) { for (var i = 0; i < count; i++) { - _records.Add(new Record(Keys, new object[] { "Test", 123 })); + _records.Add(TestRecord.Create(Keys, new object[] { "Test", 123 })); } } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/ResultCursorExtensionsTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/ResultCursorExtensionsTests.cs index 635153533..d1ce8a5f5 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/ResultCursorExtensionsTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/ResultCursorExtensionsTests.cs @@ -49,7 +49,7 @@ public async Task ShouldReturnSingleRecord() .Returns(enumerator.Object); var record = await mockCursor.Object.SingleAsync(); - record.Should().Be(mockRecord.Object); + record.Should().BeSameAs(mockRecord.Object); } [Fact] @@ -161,8 +161,8 @@ public async Task ShouldReturnList() var list = await mockCursor.Object.ToListAsync(); list.Count.Should().Be(2); - list[0].Should().Be(record0); - list[1].Should().Be(record1); + list[0].Should().BeSameAs(record0); + list[1].Should().BeSameAs(record1); } [Fact] @@ -212,7 +212,7 @@ await mockCursor.Object.ForEachAsync( r => { index++; - r.Should().Be(index == 1 ? mockRecord.Object : mockRecord2.Object); + r.Should().BeSameAs(index == 1 ? mockRecord.Object : mockRecord2.Object); }); } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/TestRecord.cs b/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/TestRecord.cs new file mode 100644 index 000000000..979ef1652 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/TestRecord.cs @@ -0,0 +1,34 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Neo4j.Driver.Internal.Result; + +namespace Neo4j.Driver.Tests.TestUtil; + +internal static class TestRecord +{ + public static Record Create(string[] keys, object[] values) + { + var lookup = keys + .Select((key, index) => (key, index)) + .ToDictionary(pair => pair.key, pair => pair.index); + + var invariantLookup = new Dictionary(lookup, StringComparer.InvariantCultureIgnoreCase); + return new Record(lookup, invariantLookup, values); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Result/Record.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Result/Record.cs index 4d097eece..9df090ac2 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Result/Record.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Result/Record.cs @@ -13,34 +13,100 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Collections; using System.Collections.Generic; +using System.Linq; namespace Neo4j.Driver.Internal.Result; -internal class Record : IRecord +internal sealed class Record : IRecord { - public Record(string[] keys, object[] values) + private readonly IReadOnlyDictionary _fieldLookup; + private readonly IReadOnlyDictionary _invariantFieldLookup; + private readonly object[] _fieldValues; + private IReadOnlyList _keys; + + public Record( + IReadOnlyDictionary fieldLookup, + IReadOnlyDictionary invariantFieldLookup, + object[] values) + { + _fieldLookup = fieldLookup; + _invariantFieldLookup = invariantFieldLookup; + _fieldValues = values; + } + + /// + public object this[int index] => _fieldValues[index]; + + /// + public object this[string key] => _fieldValues[_fieldLookup[key]]; + + /// + public T Get(string key) { - if (keys.Length != values.Length) + return _fieldValues[_fieldLookup[key]].As(); + } + + /// + public bool TryGet(string key, out T value) + { + if (_fieldLookup.TryGetValue(key, out var index)) { - throw new ProtocolException( - $"{nameof(keys)} length ({keys.Length}) does not equal to {nameof(values)} length ({values.Length})"); + value = _fieldValues[index].As(); + return true; } - - var valueKeys = new Dictionary(keys.Length); - for (var i = 0; i < keys.Length; i++) + value = default; + return false; + } + + /// + public T GetCaseInsensitive(string key) + { + return _fieldValues[_invariantFieldLookup[key]].As(); + } + + /// + public bool TryGetCaseInsensitive(string key, out T value) + { + if (_invariantFieldLookup.TryGetValue(key, out var index)) { - valueKeys.Add(keys[i], values[i]); + value = _fieldValues[index].As(); + return true; } - Values = valueKeys; - Keys = keys; + value = default; + return false; + } + + /// + public IReadOnlyList Keys => _keys ??= _fieldLookup.Keys.ToList(); + + /// + public IReadOnlyDictionary Values => this; + + /// + bool IReadOnlyDictionary.ContainsKey(string key) => _fieldLookup.ContainsKey(key); + + /// + bool IReadOnlyDictionary.TryGetValue(string key, out object value) => TryGet(key, out value); + + /// + IEnumerable IReadOnlyDictionary.Keys => Keys; + + /// + IEnumerable IReadOnlyDictionary.Values => _fieldValues; + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + return Keys.Select((key, i) => new KeyValuePair(key, _fieldValues[i])).GetEnumerator(); } - public object this[int index] => Values[Keys[index]]; - public object this[string key] => Values[key]; + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator(); - public IReadOnlyDictionary Values { get; } - public IReadOnlyList Keys { get; } + /// + int IReadOnlyCollection>.Count => _fieldLookup.Count; } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Result/ResultCursorBuilder.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Result/ResultCursorBuilder.cs index fcc828f63..e9fc5a309 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Result/ResultCursorBuilder.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Result/ResultCursorBuilder.cs @@ -15,6 +15,8 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Neo4j.Driver.Internal.MessageHandling; @@ -35,7 +37,9 @@ internal class ResultCursorBuilder : IResultCursorBuilder private readonly IResultResourceHandler _resourceHandler; private readonly IInternalAsyncTransaction _transaction; private readonly SummaryBuilder _summaryBuilder; - private string[] _fields; + + private Dictionary _fieldLookup; + private Dictionary _invariantFieldLookup; private IResponsePipelineError _pendingError; private long _queryId; @@ -65,7 +69,8 @@ public ResultCursorBuilder( _state = (int)(reactive ? State.RunRequested : State.RunAndRecordsRequested); _queryId = NoQueryId; - _fields = null; + _fieldLookup = null; + _invariantFieldLookup = null; _fetchSize = fetchSize; _autoPullHandler = new AutoPullHandler(_fetchSize); } @@ -83,7 +88,7 @@ public async ValueTask GetKeysAsync() await AdvanceAsync().ConfigureAwait(false); } - return _fields ?? Array.Empty(); + return _fieldLookup?.Keys.ToArray() ?? Array.Empty(); } public async ValueTask NextRecordAsync() @@ -139,7 +144,20 @@ public async ValueTask ConsumeAsync() public void RunCompleted(long queryId, string[] fields, IResponsePipelineError error) { _queryId = queryId; - _fields = fields; + + if (fields is not null) + { + _invariantFieldLookup = new Dictionary( + fields.Length, + StringComparer.InvariantCultureIgnoreCase); + + _fieldLookup = new Dictionary(fields.Length); + for (var i = 0; i < fields.Length; i++) + { + _invariantFieldLookup.Add(fields[i], i); + _fieldLookup.Add(fields[i], i); + } + } CheckAndUpdateState(State.RunCompleted, State.RunRequested); } @@ -151,7 +169,7 @@ public void PullCompleted(bool hasMore, IResponsePipelineError error) public void PushRecord(object[] fieldValues) { - _records.Enqueue(new Record(_fields, fieldValues)); + _records.Enqueue(new Record(_fieldLookup, _invariantFieldLookup, fieldValues)); _autoPullHandler.TryDisableAutoPull(_records.Count); UpdateState(State.RecordsStreaming); @@ -172,7 +190,7 @@ private void ClearRecords() private void AssertTransactionValid() { _pendingError?.EnsureThrown(); - if (_transaction.IsErrored(out var error) ) + if (_transaction.IsErrored(out var error)) { throw new TransactionTerminatedException(error); } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Types/Node.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Types/Node.cs index e8b20e6fb..1d9b7f39a 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Types/Node.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Types/Node.cs @@ -43,6 +43,26 @@ public Node(long id, string elementId, IReadOnlyList labels, IReadOnlyDi public string ElementId { get; } public IReadOnlyList Labels { get; } + + /// + public T Get(string key) + { + return Properties[key].As(); + } + + /// + public bool TryGet(string key, out T value) + { + if (Properties.TryGetValue(key, out var obj)) + { + value = obj.As(); + return true; + } + + value = default; + return false; + } + public IReadOnlyDictionary Properties { get; } public object this[string key] => Properties[key]; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Types/Relationship.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Types/Relationship.cs index 051f6c782..a92886508 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Types/Relationship.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Types/Relationship.cs @@ -79,6 +79,25 @@ public Relationship( public IReadOnlyDictionary Properties { get; } public object this[string key] => Properties[key]; + /// + public T Get(string key) + { + return Properties[key].As(); + } + + /// + public bool TryGet(string key, out T value) + { + if (Properties.TryGetValue(key, out var obj)) + { + value = obj.As(); + return true; + } + + value = default; + return false; + } + public bool Equals(IRelationship other) { if (other == null) diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/DictAsRecord.cs b/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/DictAsRecord.cs index 3190bf133..8cabf8bc5 100644 --- a/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/DictAsRecord.cs +++ b/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/DictAsRecord.cs @@ -16,24 +16,31 @@ // limitations under the License. using System; +using System.Collections; using System.Collections.Generic; using System.Linq; namespace Neo4j.Driver.Preview.Mapping; -internal class DictAsRecord : IRecord +internal sealed class DictAsRecord : IRecord { private readonly IReadOnlyDictionary _dict; public DictAsRecord(object dict, IRecord record) { - _dict = dict switch + var readOnlyDictionary = dict switch { - IEntity entity => entity.Properties, + IEntity entity => entity.Properties ?? new Dictionary(), IReadOnlyDictionary dictionary => dictionary, _ => throw new InvalidOperationException($"Cannot create a DictAsRecord from a {dict.GetType().Name}") }; + // this is only used by the default mapper so make it case insensitive + _dict = readOnlyDictionary.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value, + StringComparer.InvariantCultureIgnoreCase); + Record = record; } @@ -41,6 +48,71 @@ public DictAsRecord(object dict, IRecord record) public object this[int index] => _dict.TryGetValue(_dict.Keys.ElementAt(index), out var obj) ? obj : null; public object this[string key] => _dict.TryGetValue(key, out var obj) ? obj : null; + + /// + public T Get(string key) + { + return GetCaseInsensitive(key); + } + + /// + public bool TryGet(string key, out T value) + { + return TryGetCaseInsensitive(key, out value); + } + + /// + public T GetCaseInsensitive(string key) + { + return _dict[key].As(); + } + + /// + public bool TryGetCaseInsensitive(string key, out T value) + { + if (_dict.TryGetValue(key, out var obj)) + { + value = obj.As(); + return true; + } + + value = default; + return false; + } + + public bool TryGetValueByCaseInsensitiveKey(string key, out object value) + { + return _dict.TryGetValue(key, out value); + } + public IReadOnlyDictionary Values => _dict; public IReadOnlyList Keys => _dict.Keys.ToList(); + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + return _dict.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return _dict.GetEnumerator(); + } + + /// + bool IReadOnlyDictionary.ContainsKey(string key) => _dict.ContainsKey(key); + + /// + bool IReadOnlyDictionary.TryGetValue(string key, out object value) => + _dict.TryGetValue(key, out value); + + /// + int IReadOnlyCollection>.Count => _dict.Count; + + /// + IEnumerable IReadOnlyDictionary.Keys => _dict.Keys; + + /// + IEnumerable IReadOnlyDictionary.Values => _dict.Values; } diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/EntityExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/EntityExtensions.cs new file mode 100644 index 000000000..95f6c8394 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/EntityExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Neo4j.Driver.Preview.Mapping; + +/// +/// Contains extensions for entities such as nodes and relationships. +/// +public static class EntityExtensions +{ + /// + /// Converts the entity to a record. + /// + /// The entity to convert. + /// The record. + public static IRecord AsRecord(this IEntity entity) + { + return new DictAsRecord(entity, null); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordEntityExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordEntityExtensions.cs deleted file mode 100644 index 6c01410ca..000000000 --- a/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordEntityExtensions.cs +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) "Neo4j" -// Neo4j Sweden AB [http://neo4j.com] -// -// This file is part of Neo4j. -// -// Licensed under the Apache License, Version 2.0 (the "License"). -// You may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Neo4j.Driver.Preview.Mapping; - -/// -/// Contains extensions for accessing values simply from records and entities. -/// -public static class RecordEntityExtensions -{ - /// - /// Converts the record to an object of the given type according to the global mapping configuration. - /// - /// - /// The record to convert. - /// The type to map to. - /// The mapped object. - public static T AsObject(this IRecord record) - { - return RecordObjectMapping.Map(record); - } - - /// - /// Gets the value of the given key from the record, converting it to the given type. - /// - /// The record to get the value from. - /// The key of the value. - /// The type to convert to. - /// The converted value. - public static T GetValue(this IRecord record, string key) - { - return record.Values.TryGetValue(key, out var value) ? value.As() : default; - } - - /// - /// Gets the identified by the given key from the record. - /// - /// The record to get the entity from. - /// The key of the entity. - /// The entity. - public static IEntity GetEntity(this IRecord record, string key) - { - return record.GetValue(key); - } - - /// - /// Gets the value of the given key from the entity, converting it to the given type. - /// - /// The entity to get the value from. - /// The key of the value. - /// The type to convert to. - /// The converted value. - public static T GetValue(this IEntity entity, string key) - { - return entity.Properties.TryGetValue(key, out var value) ? value.As() : default; - } - - /// - /// Gets the value of the given key from the entity, converting it to a string. - /// - /// The record to get the value from. - /// The key of the value. - /// The converted value. - public static string GetString(this IRecord record, string key) => record.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to an int. - /// - /// The record to get the value from. - /// The key of the value. - /// The converted value. - public static int GetInt(this IRecord record, string key) => record.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to a long. - /// - /// The record to get the value from. - /// The key of the value. - /// The converted value. - public static long GetLong(this IRecord record, string key) => record.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to a double. - /// - /// The record to get the value from. - /// The key of the value. - /// The converted value. - public static double GetDouble(this IRecord record, string key) => record.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to a float. - /// - /// The record to get the value from. - /// The key of the value. - /// The converted value. - public static float GetFloat(this IRecord record, string key) => record.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to a bool. - /// - /// The record to get the value from. - /// The key of the value. - /// The converted value. - public static bool GetBool(this IRecord record, string key) => record.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to a string. - /// - /// The entity to get the value from. - /// The key of the value. - /// The converted value. - public static string GetString(this IEntity entity, string key) => entity.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to an int. - /// - /// The entity to get the value from. - /// The key of the value. - /// The converted value. - public static int GetInt(this IEntity entity, string key) => entity.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to a long. - /// - /// The entity to get the value from. - /// The key of the value. - /// The converted value. - public static long GetLong(this IEntity entity, string key) => entity.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to a double. - /// - /// The entity to get the value from. - /// The key of the value. - /// The converted value. - public static double GetDouble(this IEntity entity, string key) => entity.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to a float. - /// - /// The entity to get the value from. - /// The key of the value. - /// The converted value. - public static float GetFloat(this IEntity entity, string key) => entity.GetValue(key); - - /// - /// Gets the value of the given key from the entity, converting it to a bool. - /// - /// The entity to get the value from. - /// The key of the value. - /// The converted value. - public static bool GetBool(this IEntity entity, string key) => entity.GetValue(key); -} diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordExtensions.cs new file mode 100644 index 000000000..0869e7a1e --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordExtensions.cs @@ -0,0 +1,36 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Neo4j.Driver.Preview.Mapping; + +/// +/// Contains extensions for accessing values simply from records and entities. +/// +public static class RecordExtensions +{ + /// + /// Converts the record to an object of the given type according to the global mapping configuration. + /// + /// + /// The record to convert. + /// The type to map to. + /// The mapped object. + public static T AsObject(this IRecord record) + { + return RecordObjectMapping.Map(record); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordPathFinder.cs b/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordPathFinder.cs index eafe407bd..b67c79bef 100644 --- a/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordPathFinder.cs +++ b/Neo4j.Driver/Neo4j.Driver/Preview/Mapping/RecordPathFinder.cs @@ -25,51 +25,62 @@ internal interface IRecordPathFinder internal class RecordPathFinder : IRecordPathFinder { - private bool PathCompare(string path, string field) - { - return string.Equals(path, field, StringComparison.InvariantCultureIgnoreCase); - } - /// public bool TryGetValueByPath(IRecord record, string path, out object value) { value = null; - foreach (var field in record.Keys) + if(record is null) + { + return false; + } + + // if the path matches a field name, we can return the value directly + if (record.TryGetCaseInsensitive(path, out value)) + { + return true; + } + + // if there's a dot in the path, we can try to split it and check if the first part + // matches a field name and the second part matches a property name + var dotIndex = path.IndexOf('.'); + if (dotIndex > 0) { - if (PathCompare(path, field)) + var field = path.Substring(0, dotIndex); + var property = path.Substring(dotIndex + 1); + + if (!record.TryGetCaseInsensitive(field, out object fieldValue)) { - // we can return the value directly if the field name matches the path - value = record[field]; - return true; + return false; } - // if the field contains an entity or dictionary we can drill down and - // check if the path matches any of the properties - var properties = record[field] switch + var dictAsRecord = fieldValue switch { - IEntity entity => entity.Properties, - IReadOnlyDictionary dict => dict, + IEntity entity => new DictAsRecord(entity.Properties, record), + IReadOnlyDictionary dict => new DictAsRecord(dict, record), _ => null }; - if (properties is null) + if (dictAsRecord is not null) { - // if the field is not an entity or dictionary we can't drill down further - continue; + return dictAsRecord.TryGetValueByCaseInsensitiveKey(property, out value); } + } - foreach (var property in properties) + // check any values on the record that are entities or dictionaries, in case + // the path is a property on one of those + foreach (var key in record.Keys) + { + var dictAsRecord = record[key] switch { - // if there is a property that matches the path in the dictionary, or if the path - // matches the field name + property name, we can return the value - if ( - PathCompare(path, property.Key) || - PathCompare(path, $"{field}.{property.Key}")) - { - value = property.Value; - return true; - } + IEntity entity => new DictAsRecord(entity.Properties, record), + IReadOnlyDictionary dict => new DictAsRecord(dict, record), + _ => null + }; + + if(dictAsRecord is not null && TryGetValueByPath(dictAsRecord, path, out value)) + { + return true; } } diff --git a/Neo4j.Driver/Neo4j.Driver/Public/IRecord.cs b/Neo4j.Driver/Neo4j.Driver/Public/IRecord.cs index b258adb2c..f0ea88148 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/IRecord.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/IRecord.cs @@ -18,21 +18,50 @@ namespace Neo4j.Driver; /// A record contains ordered key and value pairs -public interface IRecord +public interface IRecord : IReadOnlyDictionary { /// Gets the value at the given index. - /// The index + /// The index. /// The value specified with the given index. object this[int index] { get; } - /// Gets the value specified by the given key. - /// The key - /// the value specified with the given key. - object this[string key] { get; } + /// Gets the value specified by the given key and converts it to the given type. + /// The key. + /// The type to convert to. + /// The converted value. + T Get(string key); + + /// + /// Tries to get the value specified by the given key and converts it to the given type. + /// + /// The key. + /// The value, if the key was found. + /// The type to convert to. + /// true if the value is found; false otherwise. + bool TryGet (string key, out T value); + + /// + /// Gets the value specified by the given key and converts it to the given type. The key is not case + /// sensitive. + /// + /// The key. + /// The type to convert to. + /// The converted value. + T GetCaseInsensitive(string key); + + /// + /// Tries to get the value specified by the given key and converts it to the given type. The key is not case + /// sensitive. + /// + /// The key. + /// The value, if the key was found. + /// The type to convert to. + /// true if the value is found; false otherwise. + bool TryGetCaseInsensitive(string key, out T value); /// Gets the key and value pairs in a . - IReadOnlyDictionary Values { get; } + new IReadOnlyDictionary Values { get; } /// Gets the keys in a . - IReadOnlyList Keys { get; } + new IReadOnlyList Keys { get; } } diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Types/IEntity.cs b/Neo4j.Driver/Neo4j.Driver/Public/Types/IEntity.cs index 00bcde583..4187f7959 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Types/IEntity.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/Types/IEntity.cs @@ -29,6 +29,21 @@ public interface IEntity /// The value specified by the given key in . object this[string key] { get; } + /// Gets the value that has the specified key in and casts it to + /// the specified type. + /// The key. + /// The type to cast the value to. + /// The value specified by the given key in . + T Get(string key); + + /// Tries to get the value that has the specified key in and casts it to + /// the specified type. + /// The key. + /// The value if it exists. + /// The type to cast the value to. + /// true if the value exists, false otherwise. + bool TryGet(string key, out T value); + /// Gets the properties of the entity. IReadOnlyDictionary Properties { get; }