Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree parsing and overhaul #38

Open
BenjaBobs opened this issue Nov 2, 2018 · 4 comments
Open

Tree parsing and overhaul #38

BenjaBobs opened this issue Nov 2, 2018 · 4 comments

Comments

@BenjaBobs
Copy link
Collaborator

BenjaBobs commented Nov 2, 2018

Hi @evo-terren
I'm currently working on a way to deserialize trees (much like Vertex.ToObject<T>()) since I need it for my project, but it has turned out to be quite complex.
With that I also made some changes to how the schema bound graph traversals work, and I'd like your input on it, especially in regard of whether you want it as part of the project.
#39
Anyway here's the gist of it:

Schema changes

Introduction of interfaces:

public interface IVertex
{
}

public interface IEdge
{
}

public interface IHasOutV<TOutV>
    where TOutV : IVertex
{
}

public interface IHasInV<TInV>
    where TInV : IVertex
{
}

public interface IEdge<TOutV, TInV> : IHasOutV<TOutV>, IHasInV<TInV>, IEdge
    where TOutV : IVertex
    where TInV : IVertex
{
}

This way, entities don't have to inherit from VertexBase/EdgeBase to be usable in the traversal.
The benefit of this is that it frees up the inheritance chain.

Label changes

Depending on the data model size, the amount of edges can become quite big and as such the amount of classes needed.
To help alleviate this I've introduced property labels:

private static readonly ConcurrentDictionary<PropertyInfo, string> _propertyLabelLookup = new ConcurrentDictionary<PropertyInfo, string>();

/// <summary>
/// Gets the name of the label for a property.
/// Defaults to the name of the property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>Returns the label name.</returns>
public static string GetLabelName(PropertyInfo property)
{
    return _propertyLabelLookup.GetOrAdd(property, p =>
    {
        var attr = p.GetCustomAttribute<LabelAttribute>() ?? p.PropertyType.GetCustomAttribute<LabelAttribute>();

        return attr?.Name ?? p.Name;
    });
}

As well as a set of generic types, ie:

/// <summary>
/// A many-to-many edge between two vertices.
/// </summary>
/// <typeparam name="TOutV">The Out vertex type</typeparam>
/// <typeparam name="TInV">The In vertex type</typeparam>
public class ManyToManyEdge<TOutV, TInV> : IEdge<TOutV, TInV>
    where TOutV : IVertex
    where TInV : IVertex
{
    /// <summary>
    /// The list of Out vertices.
    /// </summary>
    public List<TOutV> OutV { get; set; }

    /// <summary>
    /// The list of In vertices.
    /// </summary>
    public List<TInV> InV { get; set; }
}

Which enables a schema definition syntax like this:

public class Person : IVertex
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [Label("purchased")]
    public ManyToManyEdge<Person, Product> Purchased { get; set; }
}

public class Product : IVertex
{
    public string Name { get; set; }
    public int Price { get; set; }

    [Label("purchased")]
    public ManyToManyEdge<Person, Product> PurchasedBy { get; set; }
}

And since the schema traversal are based on IVertex and IEdge<TOutV,TInv>, it is still possible to create custom edges that implement these interfaces or inhereit from predefined ones such as ManyToManyEdge<TOutV, TInV>.

Tree conversion

The trouble of converting trees into specific types are that the edges and vertices need to be connected properly.
For that I made some interfaces and a naive implementation, such that there is an option for adding specialized implementations.

/// <summary>
/// A connector for <see cref="IVertex"/>.
/// Can connect a <see cref="IVertex"/> to an <see cref="IEdge"/>.
/// </summary>
public interface IVertexConnector : ITreeConnector
{
    /// <summary>
    /// Connects the <paramref name="vertex"/> to the <paramref name="edge"/>
    /// </summary>
    /// <param name="edge">The edge.</param>
    /// <param name="vertex">The vertex to connect to the edge.</param>
    /// <returns>True if succesful.</returns>
    bool ConnectVertex(IEdge edge, IVertex vertex);
}

/// <summary>
/// A connector for <see cref="IEdge"/>.
/// Can connect a <see cref="IEdge"/> to a <see cref="IVertex"/>.
/// </summary>
public interface IEdgeConnector : ITreeConnector
{
    /// <summary>
    /// Connects the <paramref name="edge"/> to the <paramref name="vertex"/> using the specified <paramref name="property"/>.
    /// </summary>
    /// <param name="vertex">The vertex.</param>
    /// <param name="edge">The edge to connect to the vertex.</param>
    /// <param name="property">The vertex property that holds the edge.</param>
    /// <returns></returns>
    bool ConnectEdge(IVertex vertex, IEdge edge, PropertyInfo property);
}

With that follows a default AutoConnector, which can make some qualified guesses as to how to merge.
I then added the following method to Tree:

/// <summary>
/// Convert the parsed tree into the specified type.
/// </summary>
/// <typeparam name="T">The type to convert to.</typeparam>
/// <param name="treeConnectors">Custom connectors to use for attaching edges and vertices.</param>
/// <returns></returns>
public T[] ToObject<T>(List<ITreeConnector> treeConnectors = null)
    where T : IVertex
{
    var parser = new TreeParser(treeConnectors ?? new List<ITreeConnector>());

    var vertices = RootVertexNodes.Select(n => parser.GetVertex(n, typeof(T))).Cast<T>();

    return vertices.ToArray();
}

The TreeParser does the actual connecting of the vertices and edges, and defaults to AutoConnector if no other implementations are provided.
I updated the TreeJsonConverter_deserializes_a_tree_that_includes_edges() test and can confirm that it works, using the schema example from above.

[Fact]
private void TreeJsonConverter_deserializes_a_tree_that_includes_edges()
{
    var settings = new JsonSerializerSettings
    {
        Converters = new JsonConverter[]
        {
            new TreeJsonConverter(),
            new IEdgeJsonConverter(),
            new ElementJsonConverter(),
            new IVertexJsonConverter(),
        },
        DateFormatHandling = DateFormatHandling.IsoDateFormat,
        DateParseHandling = DateParseHandling.DateTimeOffset,
        DateTimeZoneHandling = DateTimeZoneHandling.Utc
    };

    var tree = JsonConvert.DeserializeObject<Tree>(TEST_TREE, settings);

    tree.RootVertexNodes.Should().NotBeNullOrEmpty();
    tree.RootVertexNodes[0].Vertex.Properties.Should().ContainKeys("firstName", "lastName");

    tree.RootVertexNodes[0].EdgeNodes.Should().NotBeNullOrEmpty();
    tree.RootVertexNodes[0].EdgeNodes[0].Edge.Label.Should().Be("purchased");

    tree.RootVertexNodes[0].EdgeNodes[0].VertexNode.Vertex.Should().NotBeNull();
    tree.RootVertexNodes[0].EdgeNodes[0].VertexNode.Vertex.Label.Should().Be("product");
    tree.RootVertexNodes[0].EdgeNodes[0].VertexNode.Vertex.Properties.Should().ContainKeys("name", "price");

    var test = tree.ToObject<Person>();

    test.Length.Should().Be(1);
    test[0].FirstName.Should().Be("John");
    test[0].LastName.Should().Be("Doe");

    test[0].Purchased.Should().NotBeNull();
    test[0].Purchased.InV.Should().ContainItemsAssignableTo<Product>();
    test[0].Purchased.InV[0].Name.Should().Be("gold");
    test[0].Purchased.InV[1].Name.Should().Be("silver");
}

The code is currently not the most beautiful in the world, but I was hoping you'd provide some input about the whole thing. 😄
Also sorry about making it such a huge blob of changes, I know it kind of got out of hand.

@evo-terren
Copy link
Owner

Hey @BenjaBobs,

Wow! You have been busy. I seem to remember some very specific reason why I chose to use abstract classes instead of interfaces, but I can't for the life of me remember why that was.

Please give me a few days (maybe even a week) to digest all of this so that I can compile the feedback you deserve. (I'll also have the other members of my team take a look.) I hope this isn't too long of a time for you.

Thanks for all your hard work! Glad to see someone is getting some good use out of this code.

@BenjaBobs
Copy link
Collaborator Author

Thanks for having a look!
Take the time you need, I likely won't be able to work on this again for the next two weeks due to team prioritization, but I'll read and reply.
And thanks a lot for doing all the ground work! :)

@ankitbko
Copy link

@evo-terren Since PR is merged, wanted to know why is Vertex and Edge not deriving from IVertex and IEdge respectively?

@BenjaBobs
Copy link
Collaborator Author

I just had a quick look at the code, and I gotta say I don't remember. It might be an oversight.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants