Skip to content
This repository has been archived by the owner on Feb 5, 2022. It is now read-only.

Commit

Permalink
(core): add edge id (#27)
Browse files Browse the repository at this point in the history
- making the edge identifiable allows for multigraphs
- allow to add edge value with node ids
- remove implicit name overlap in json support

Co-authored-by: Andreas Drobisch <[email protected]>
  • Loading branch information
adrobisch and Andreas Drobisch authored Nov 1, 2021
1 parent 69c708a commit eed5543
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package com.flowtick.graphs.cat
import com.flowtick.graphs._
import com.flowtick.graphs.defaults._
import cats.implicits._
import org.scalatest.diagrams.Diagrams
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class GraphCatsSpec extends AnyFlatSpec with Matchers {
class GraphCatsSpec extends AnyFlatSpec with Matchers with Diagrams {
import com.flowtick.graphs.cat.instances._

"Graph Monoid" should "combine graphs" in {
Expand Down
49 changes: 26 additions & 23 deletions cats/shared/src/main/scala/com/flowtick/graphs/cat/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,42 @@ package object cat {
* @tparam N
* node type
*/
class GraphMonoid[E, N](nodeId: Identifiable[N]) extends Monoid[Graph[E, N]] {
override def empty: Graph[E, N] = Graph.empty[E, N](nodeId)
class GraphMonoid[E, N](nodeId: Identifiable[N], edgeId: Identifiable[E])
extends Monoid[Graph[E, N]] {
override def empty: Graph[E, N] = Graph.empty[E, N](nodeId, edgeId)

override def combine(x: Graph[E, N], y: Graph[E, N]): Graph[E, N] =
(x.edges ++ y.edges)
.foldLeft(Graph.empty[E, N](nodeId))(_ withEdge _)
.foldLeft(Graph.empty[E, N](nodeId, edgeId))(_ withEdge _)
.withNodes(x.nodes ++ y.nodes)
}

object GraphMonoid {
def apply[E, N](implicit nodeId: Identifiable[N]) = new GraphMonoid[E, N](nodeId)
def apply[E, N](implicit nodeId: Identifiable[N], edgeId: Identifiable[E]) =
new GraphMonoid[E, N](nodeId, edgeId)
}

class GraphNodeApplicative[E](implicit id: Identifiable[Any]) extends Applicative[Graph[E, *]] {
override def pure[A](x: A): Graph[E, A] = Graph(nodes = Set(Node.of(x)(id)))

override def ap[A, B](functionGraph: Graph[E, A => B])(graph: Graph[E, A]): Graph[E, B] =
GraphMonoid[E, B].combineAll(graph.nodes.map(node => {
Graph
.empty[E, B]
.addNodes(
functionGraph.nodes
.filter(node => graph.outgoing(node.id).isEmpty && graph.incoming(node.id).isEmpty)
.map(_.value(node.value))
)
.withEdges(
functionGraph.nodes.flatMap(f =>
graph
.outgoing(node.id)
.flatMap(edge => {
for {
toNode <- graph.findNode(edge.to)
} yield Edge.of(edge.value, id(f.value(node.value)), id(f.value(toNode.value)))
})
)
)
functionGraph.nodes.foldLeft(Graph.empty[E, B]) { case (acc, f) =>
graph.outgoing(node.id).foldLeft(acc) { case (result, edge) =>
val fromNode = f.value(node.value)
graph
.findNode(edge.to)
.map(existingToNode => {
val toNode = f.value(existingToNode.value)

result
.addNode(fromNode)
.addNode(toNode)
.withEdgeValue(edge.value, id(fromNode), id(toNode))
})
.getOrElse(result)
}
}
}))
}

Expand Down Expand Up @@ -96,7 +96,10 @@ package object cat {
}

trait GraphInstances {
implicit def graphMonoid[E, N](implicit nodeId: Identifiable[N]): Monoid[Graph[E, N]] =
implicit def graphMonoid[E, N](implicit
nodeId: Identifiable[N],
edgeId: Identifiable[E]
): Monoid[Graph[E, N]] =
GraphMonoid[E, N]
implicit def graphNodeApplicative[E](implicit id: Identifiable[Any]): Applicative[Graph[E, *]] =
GraphApplicative[E]
Expand Down
8 changes: 4 additions & 4 deletions core/jvm/src/test/scala/com/flowtick/graphs/GraphSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class GraphSpec extends AnyFlatSpec with Matchers {

it should "have nodes after adding an edge" in {
val intGraph =
Graph.empty[Option[Unit], Int].withEdgeValue(None, Node.of(1), Node.of(2))
Graph.empty[Option[Unit], Int].addEdge(None, 1, 2)
intGraph.nodes should contain theSameElementsAs List(
Node("1", 1),
Node("2", 2)
Expand All @@ -109,7 +109,7 @@ class GraphSpec extends AnyFlatSpec with Matchers {
.addNode(node3)

intGraph.removeNodeValue(node3) should be(
Graph.empty[Unit, Int].withEdgeValue((), Node.of(1), Node.of(2))
Graph.empty[Unit, Int].addEdge((), 1, 2)
)

val expected = Graph
Expand All @@ -123,8 +123,8 @@ class GraphSpec extends AnyFlatSpec with Matchers {
it should "remove edges" in {
val intGraph = Graph
.empty[Unit, Int]
.withEdgeValue((), Node.of(1), Node.of(2))
.withNode(Node.of(3))
.addEdge((), 1, 2)
.addNode(3)

val expected = Graph
.empty[Unit, Int]
Expand Down
24 changes: 14 additions & 10 deletions core/shared/src/main/scala/com/flowtick/graphs/Graph.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,17 @@ final case class Relation[+E, +N](
to: Node[N],
symmetric: Boolean = false
) {
def toEdges: Iterable[Edge[E]] =
def toEdges(implicit id: Identifiable[E]): Iterable[Edge[E]] =
if (symmetric)
Iterable(Edge.of(value, from.id, to.id), Edge.of(value, to.id, from.id))
else Iterable(Edge.of(value, from.id, to.id))
}

object Edge {
def of[E, N](value: E, from: String, to: String): Edge[E] =
Edge(s"$from-$to", value, from, to)
def unit[N](from: String, to: String): Edge[Unit] = Edge.of((), from, to)
def of[E](value: E, from: String, to: String)(implicit id: Identifiable[E]): Edge[E] =
Edge(s"$from-${id(value)}-$to", value, from, to)
def unit(from: String, to: String)(implicit id: Identifiable[Unit]): Edge[Unit] =
Edge.of((), from, to)
}

final case class Node[+N](id: String, value: N) {
Expand All @@ -88,6 +89,7 @@ object Node {

private[graphs] final case class GraphInstance[E, N](
nodeId: Identifiable[N],
edgeId: Identifiable[E],
nodesById: scala.collection.Map[String, Node[N]] =
scala.collection.immutable.TreeMap.empty[String, Node[N]],
incomingById: scala.collection.Map[String, Set[String]] =
Expand Down Expand Up @@ -195,6 +197,7 @@ private[graphs] final case class GraphInstance[E, N](
// #graph
trait Graph[E, N] {
def nodeId: Identifiable[N]
def edgeId: Identifiable[E]

def edges: Iterable[Edge[E]]
def nodes: Iterable[Node[N]]
Expand Down Expand Up @@ -228,15 +231,15 @@ trait Graph[E, N] {
def addNodes(nodeValues: Iterable[N]): Graph[E, N] = nodeValues.foldLeft(this)(_ addNode _)

def addEdge(value: E, from: N, to: N): Graph[E, N] =
withEdge(Edge.of(value, nodeId(from), nodeId(to)))
withEdge(Edge.of(value, nodeId(from), nodeId(to))(edgeId))
.addNode(from)
.addNode(to)

def withEdge(edge: Edge[E]): Graph[E, N]
def withNode(node: Node[N]): Graph[E, N]

def withEdgeValue(value: E, from: Node[N], to: Node[N]): Graph[E, N] =
withEdge(Edge.of(value, from.id, to.id)).withNode(from).withNode(to)
def withEdgeValue(value: E, fromId: String, toId: String): Graph[E, N] =
withEdge(Edge.of(value, fromId, toId)(edgeId))

def withNodes(nodes: Iterable[Node[N]]): Graph[E, N] =
nodes.foldLeft(this)(_ withNode _)
Expand All @@ -251,10 +254,11 @@ object Graph {
def apply[E, N](
edges: Iterable[Edge[E]] = Iterable.empty,
nodes: Iterable[Node[N]] = Iterable.empty
)(implicit nodeId: Identifiable[N]): Graph[E, N] =
)(implicit nodeId: Identifiable[N], edgeId: Identifiable[E]): Graph[E, N] =
empty[E, N].withEdges(edges).withNodes(nodes)

def empty[E, N](implicit nodeId: Identifiable[N]): Graph[E, N] = GraphInstance[E, N](nodeId)
def empty[E, N](implicit nodeId: Identifiable[N], edgeId: Identifiable[E]): Graph[E, N] =
GraphInstance[E, N](nodeId, edgeId)

/** utility method to create a unit typed graph from iterable relations
*
Expand All @@ -269,7 +273,7 @@ object Graph {
*/
def fromEdges[E, N](
relations: Iterable[Relation[E, N]]
)(implicit nodeId: Identifiable[N]): Graph[E, N] =
)(implicit nodeId: Identifiable[N], edgeId: Identifiable[E]): Graph[E, N] =
relations.foldLeft(empty[E, N]) { (acc, relation) =>
acc
.withNode(relation.from)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ package object defaults {
implicit val identifiableInt: Identifiable[Int] =
Identifiable.identify(int => int.toString)

private final case class IdentifiableOption[T](id: Identifiable[T])
extends Identifiable[Option[T]] {
override def apply(value: Option[T]): String = value.map(id(_)).getOrElse("none")
}

implicit def identifiableOption[T](implicit id: Identifiable[T]): Identifiable[Option[T]] =
IdentifiableOption(id)

object id {
implicit val identifyAny: Identifiable[Any] = (value: Any) => value.toString
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ object EditorGraphNode {
}

object EditorGraphEdge {
implicit val identifiableEditorEdge = new Identifiable[EditorGraphEdge] {
override def apply(value: EditorGraphEdge): String = value.id
}

implicit val editorEdgeStyleRef: StyleRef[Edge[EditorGraphEdge]] =
new StyleRef[Edge[EditorGraphEdge]] {
override def id(element: Edge[EditorGraphEdge]): Option[String] = Some(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import cats.data.ValidatedNel
import com.flowtick.graphs.Graph
import com.flowtick.graphs.graphml.{Datatype, GraphMLKey}
import com.flowtick.graphs.json.schema.Schema
import com.flowtick.graphs.layout.{GraphLayout, GraphLayoutLike, GraphLayouts}
import com.flowtick.graphs.layout.{GraphLayoutLike, GraphLayouts}
import com.flowtick.graphs.style._
import io.circe.Json

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ class GraphMLDatatypeSpec extends AnyFlatSpec with Matchers {
) should contain theSameElementsAs graphML.graph.nodes.map(_.id)
parsedGraph.graph.edges.map(
_.id
) should contain theSameElementsAs graphML.graph.edges.map(_.id)
) should contain theSameElementsAs graphML.graph.edges.map { edge =>
s"${edge.from}-${edge.to}"
}

case Left(errors) => fail(s"parsing errors ${errors.toString}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ final case class GraphMLProperty(
class GraphMLDatatype[E, N](
nodeDataType: Datatype[GraphMLNode[N]],
edgeDataType: Datatype[GraphMLEdge[E]]
)(implicit edgeLabel: Labeled[Edge[GraphMLEdge[E]], String])
extends Datatype[GraphMLGraph[E, N]] {
)(implicit
edgeLabel: Labeled[Edge[GraphMLEdge[E]], String],
nodeId: Identifiable[GraphMLNode[N]],
edgeId: Identifiable[GraphMLEdge[E]]
) extends Datatype[GraphMLGraph[E, N]] {
val nodeTargetHint = Some("node")
val edgeTargetHint = Some("edge")
val metaTargetHint = Some("meta")
Expand Down Expand Up @@ -130,7 +133,7 @@ class GraphMLDatatype[E, N](
resources: Seq[GraphMLResource]
): Validated[NonEmptyList[Throwable], GraphMLGraph[E, N]] =
parseGraphNodes(graph, graphKeys).andThen { parsedGraph =>
parseEdges(parsedGraph.edgesXml, parsedGraph.nodes, graphKeys).andThen { edges =>
parseEdges(parsedGraph.edgesXml, graphKeys).andThen { edges =>
valid(
GraphMLGraph(
Graph(edges, parsedGraph.nodes.values),
Expand All @@ -142,7 +145,6 @@ class GraphMLDatatype[E, N](

protected def parseEdges(
edgeXmlNodes: List[scala.xml.Node],
nodes: scala.collection.Map[String, Node[GraphMLNode[N]]],
keys: scala.collection.Map[String, GraphMLKey]
): Validated[NonEmptyList[Throwable], List[Edge[GraphMLEdge[E]]]] = {
edgeXmlNodes.map { edgeNode =>
Expand Down Expand Up @@ -224,7 +226,8 @@ class GraphMLDatatype[E, N](

object GraphMLDatatype {
def apply[E, N](implicit
identifiable: Identifiable[GraphMLNode[N]],
nodeId: Identifiable[GraphMLNode[N]],
edgeId: Identifiable[GraphMLEdge[E]],
edgeLabel: Labeled[Edge[GraphMLEdge[E]], String],
nodeDataType: Datatype[N],
edgeDataType: Datatype[E]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.flowtick.graphs.graphml

import com.flowtick.graphs.layout.{DefaultGeometry, EdgePath, Geometry}
import com.flowtick.graphs.{Edge, Graph, Node, Relation}
import com.flowtick.graphs.{Edge, Graph, Identifiable, Node, Relation}
import com.flowtick.graphs.style._

final case class GraphMLKey(
Expand Down Expand Up @@ -96,7 +96,10 @@ final case class GraphMLMeta(
)

object GraphML {
def empty[E, N]: GraphMLGraph[E, N] = GraphMLGraph[E, N](
def empty[E, N](implicit
nodeId: Identifiable[GraphMLNode[N]],
edgeId: Identifiable[GraphMLEdge[E]]
): GraphMLGraph[E, N] = GraphMLGraph[E, N](
Graph.empty[GraphMLEdge[E], GraphMLNode[N]],
GraphMLMeta()
)
Expand All @@ -106,12 +109,18 @@ object GraphML {
edges: Iterable[Edge[GraphMLEdge[E]]],
nodes: Iterable[Node[GraphMLNode[N]]] = Iterable.empty,
keys: Seq[GraphMLKey] = Seq.empty
)(implicit
nodeId: Identifiable[GraphMLNode[N]],
edgeId: Identifiable[GraphMLEdge[E]]
): GraphMLGraph[E, N] = {
GraphMLGraph(Graph(edges = edges, nodes = nodes), GraphMLMeta(keys = keys))
}

def fromEdges[E, N](
edges: Iterable[Relation[GraphMLEdge[E], GraphMLNode[N]]]
)(implicit
nodeId: Identifiable[GraphMLNode[N]],
edgeId: Identifiable[GraphMLEdge[E]]
): GraphMLGraph[E, N] =
GraphMLGraph(Graph.fromEdges(edges), GraphMLMeta())
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import xmls.XMLS
import scala.collection.GenTraversable
import scala.reflect.ClassTag
import scala.util.{Either, Left, Right}
import scala.xml.{Elem, NodeSeq, Text}
import scala.xml.{Elem, NodeSeq}

package object graphml {
trait Serializer[T] {
Expand Down Expand Up @@ -333,7 +333,8 @@ package object graphml {
}

implicit def graphMLDataType[E, N](implicit
identifiable: Identifiable[GraphMLNode[N]],
nodeId: Identifiable[GraphMLNode[N]],
edgeId: Identifiable[GraphMLEdge[E]],
edgeLabel: Labeled[Edge[GraphMLEdge[E]], String],
nodeDataType: Datatype[N],
edgeDataType: Datatype[E]
Expand Down Expand Up @@ -362,6 +363,9 @@ package object graphml {
implicit def graphMLNodeIdentifiable[N]: Identifiable[GraphMLNode[N]] =
(node: GraphMLNode[N]) => node.id

implicit def graphMLEdgeIdentifiable[E]: Identifiable[GraphMLEdge[E]] =
(edge: GraphMLEdge[E]) => edge.id

implicit def graphMLEdgeLabel[V, N]: Labeled[Edge[GraphMLEdge[V]], String] =
(edge: Edge[GraphMLEdge[V]]) => edge.value.id

Expand Down Expand Up @@ -443,7 +447,7 @@ package object graphml {
labelValue = edgeLabel(edge)
)

Edge.of(mlEdge, edge.from, edge.to)
Edge(edge.id, mlEdge, edge.from, edge.to)
}

GraphMLGraph(Graph(edges = mlEdges, nodes = mlNodes), GraphMLMeta())
Expand Down
Loading

0 comments on commit eed5543

Please sign in to comment.