Skip to content

Commit

Permalink
Merge pull request #3 from CasperCBroeren/main
Browse files Browse the repository at this point in the history
Records can parse relations and callers can access data of the relation
  • Loading branch information
cskardon authored Dec 3, 2021
2 parents a714324 + c71f4fe commit d2dfb33
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 8 deletions.
14 changes: 11 additions & 3 deletions Neo4j.Driver.Extensions/RecordExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,17 @@ public static class RecordExtensions

if (!string.IsNullOrWhiteSpace(identifier))
{
return record[identifier].TryAs<INode>(out var node)
? node.ToObject<T>()
: record[identifier].As<IDictionary<string, object>>().ToObject<T>();
if (record[identifier].TryAs<INode>(out var node))
{
return node.ToObject<T>();
}

if(record[identifier].TryAs<IRelationship>(out var relationship))
{
return relationship.ToObject<T>();
}

return record[identifier].As<IDictionary<string, object>>().ToObject<T>();
}

var obj = new T();
Expand Down
133 changes: 133 additions & 0 deletions Neo4j.Driver.Extensions/RelationshipExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
namespace Neo4j.Driver.Extensions
{
using System;
using System.Collections.Generic;
using System.Linq;
using EnsureThat;
using Neo4j.Driver;

/// <summary>
/// A collection of extensions for the <see cref="IRelationship"/> interface.
/// These should allow a user to deserialize things in an easier way.
/// </summary>
public static class RelationshipExtensions
{
/// <summary>
/// Gets a value from an <see cref="IRelationship" /> instance.
/// </summary>
/// <typeparam name="T">The <see cref="Type" /> to attempt to get the property as.</typeparam>
/// <param name="relationship">The <see cref="IRelationship" /> instance to pull the property from.</param>
/// <param name="propertyName">The name of the property to get.</param>
/// <returns>The converted <typeparamref name="T" /> or <c>default</c></returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="relationship"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="propertyName"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="propertyName"/> is an empty string or whitespace.</exception>
/// <exception cref="FormatException">
/// If any of the properties on the <paramref name="relationship" /> can't be cast to their
/// <typeparamref name="T" /> equivalents.
/// </exception>
public static T GetValue<T>(this IRelationship relationship, string propertyName)
{
Ensure.That(relationship).IsNotNull();
Ensure.That(propertyName).IsNotNullOrWhiteSpace();

return relationship.Properties.ContainsKey(propertyName)
? relationship.Properties[propertyName].As<T>()
: default;
}

/// <summary>
/// Gets a value from an <see cref="IRelationship" /> instance. Will throw a <see cref="KeyNotFoundException"/> if the <paramref name="propertyName"/> isn't on the <paramref name="relationship"/>.
/// </summary>
/// <typeparam name="T">The <see cref="Type" /> to attempt to get the property as.</typeparam>
/// <param name="relationship">The <see cref="IRelationship" /> instance to pull the property from.</param>
/// <param name="propertyName">The name of the property to get.</param>
/// <returns>The converted <typeparamref name="T" /> or <c>default</c></returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="relationship"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="propertyName"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="propertyName"/> is an empty string or whitespace.</exception>
/// <exception cref="FormatException">
/// If any of the properties on the <paramref name="relationship" /> can't be cast to their
/// <typeparamref name="T" /> equivalents.
/// </exception>
/// <exception cref="KeyNotFoundException">Thrown if <paramref name="propertyName"/> is not in the <paramref name="relationship"/>.</exception>
public static T GetValueStrict<T>(this IRelationship relationship, string propertyName)
{
Ensure.That(relationship).IsNotNull();
Ensure.That(propertyName).IsNotNullOrWhiteSpace();

if(relationship.Properties.ContainsKey(propertyName))
return relationship.Properties[propertyName].As<T>();

throw new KeyNotFoundException($"'{propertyName}' is not in the Relationship.");
}


/// <summary>
/// Gets a value from an <see cref="IRelationship" /> instance, by executing
/// the <see cref="GetValue{T}" /> method via reflection.
/// </summary>
/// <remarks>This exists primarily to allow the <see cref="ValueExtensions.As{T}(object)" /> method to be used to cast.</remarks>
/// <param name="relationship">The <see cref="IRelationship" /> instance to pull the property from.</param>
/// <param name="propertyName">The name of the property to get.</param>
/// <param name="propertyType">The <see cref="Type" /> to convert the property to.</param>
/// <param name="strict">If <c>true</c> this will throw <see cref="KeyNotFoundException"/> if properties aren't found on the <see cref="relationship"/>.</param>
/// <returns>The converted value, as an <see cref="object" />.</returns>
/// <exception cref="ArgumentNullException">If the <paramref name="relationship" /> is null.</exception>
/// <exception cref="ArgumentNullException">If the <paramref name="propertyName" /> is null.</exception>
/// <exception cref="ArgumentNullException">If the <paramref name="propertyType" /> is null.</exception>
/// <exception cref="InvalidCastException">
/// If any of the properties on the <paramref name="relationship" /> can't be cast to their
/// <typeparamref name="T" /> equivalents.
/// </exception>
private static object GetValue(this IRelationship relationship, string propertyName, Type propertyType, bool strict = false)
{
Ensure.That(relationship).IsNotNull();
Ensure.That(propertyName).IsNotNull();
Ensure.That(propertyType).IsNotNull();

var method = strict
? typeof(RelationshipExtensions).GetMethod(nameof(GetValueStrict))
: typeof(RelationshipExtensions).GetMethod(nameof(GetValue));

var generic = method?.MakeGenericMethod(propertyType);

return generic?.Invoke(relationship, new object[] {relationship, propertyName});
}

/// <summary>
/// Attempts to cast the given <see cref="IRelationship" /> instance into a complex type.
/// </summary>
/// <typeparam name="T">The type to try to cast to.</typeparam>
/// <param name="relationship">The <see cref="IRelationship" /> instance to cast from.</param>
/// <returns>An instance of <typeparamref name="T" /> with it's properties set.</returns>
/// <exception cref="ArgumentNullException">If the <paramref name="relationship" /> is null.</exception>
/// <exception cref="InvalidCastException">
/// If any of the properties on the <paramref name="relationship" /> can't be cast to their
/// <typeparamref name="T" /> equivalents.
/// </exception>
public static T ToObject<T>(this IRelationship relationship) where T : new()
{
Ensure.That(relationship).IsNotNull();

var properties = typeof(T).GetProperties().Where(p => p.CanWrite);
var obj = new T();
foreach (var property in properties)
{
var propertyName = property.Name;
var neo4jProperty = property.GetNeo4jPropertyAttribute();
if (neo4jProperty != null)
{
if (neo4jProperty.Ignore)
continue;

propertyName = neo4jProperty.Name ?? propertyName;
}

property.SetValue(obj, relationship.GetValue(propertyName, property.PropertyType));
}

return obj;
}
}
}
30 changes: 25 additions & 5 deletions Neo4j.Drivers.Extensions.Tests/RecordExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
namespace Neo4j.Drivers.Extensions.Tests
using System.ComponentModel;

namespace Neo4j.Drivers.Extensions.Tests
{
using System;
using System.ComponentModel;
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using Neo4j.Driver;
using Neo4j.Driver.Extensions;

using Xunit;

public class RecordExtensionsTests
Expand Down Expand Up @@ -37,6 +41,22 @@ public void TreatsRecordAsDictionary_WhenIdentifierSupplied()
foo.StringPropertyWithAttribute.Should().Be(stringPropertyWithAttributeValue);
}

[Fact]
public void TreatsRecordAsRelationShip_WhenIdentifierSupplied()
{
const string identifier = "foo";
const string stringPropertyValue = "baa";

var data = new Dictionary<string, object> { { "StringProperty", stringPropertyValue } };
var mock = new Mock<IRecord>();
var mockRelation = new Mock<IRelationship>(MockBehavior.Loose);
mockRelation.SetupGet(x => x.Properties).Returns(data);
mock.Setup(x => x[identifier]).Returns(mockRelation.Object);

var foo = mock.Object.ToObject<Foo>(identifier);
foo.StringProperty.Should().Be(stringPropertyValue);
}

[Fact]
public void AssumesAllTheResponseIsAnObject_WhenIdentifierNotSupplied()
{
Expand All @@ -45,7 +65,7 @@ public void AssumesAllTheResponseIsAnObject_WhenIdentifierNotSupplied()

var mock = new Mock<IRecord>();
mock.Setup(x => x.Keys).Returns(new List<string> { "StringProperty", "stringPropertyWithAttribute" });
mock.Setup(x => x.Values).Returns( new Dictionary<string, object> {
mock.Setup(x => x.Values).Returns(new Dictionary<string, object> {
{"StringProperty", stringPropertyValue},
{"stringPropertyWithAttribute", stringPropertyWithAttributeValue},
});
Expand Down Expand Up @@ -158,8 +178,8 @@ public void ReturnsDefault_WhenIdentifierNotThere()
mock.Setup(x => x.Keys).Returns(new List<string>());
mock.Setup(x => x.Values).Returns(new Dictionary<string, object>());

var ex = Assert.Throws<KeyNotFoundException>(() => mock.Object.GetValueStrict<int>("not-there"));
ex.Should().NotBeNull();
var ex = Assert.Throws<KeyNotFoundException>(() => mock.Object.GetValueStrict<int>("not-there"));
ex.Should().NotBeNull();
}

[Fact]
Expand All @@ -173,7 +193,7 @@ public void ReturnsCorrectValue()
var mock = new Mock<IRecord>();


mock.Setup(x => x.Keys).Returns(new List<string>{stringIdentifier, intIdentifier});
mock.Setup(x => x.Keys).Returns(new List<string> { stringIdentifier, intIdentifier });
mock.Setup(x => x.Values).Returns(new Dictionary<string, object>
{
{stringIdentifier, expectedStringValue},
Expand Down

0 comments on commit d2dfb33

Please sign in to comment.