Skip to content

Commit d4b8ee9

Browse files
authored
allow to optionally name relationships, and generate accessors and traversals (#112)
* WIP - works for relations on base nodes * refactor: move accessor name impl to codegen, not into (user defined) schema * works for node names: pass info through via NeighborInfoForNode * pure refactor * mostly refactor: use same impl for base nodes and nodes * allow to optionally name relationships, and generate accessors and traversals
1 parent 7d8e728 commit d4b8ee9

File tree

5 files changed

+126
-64
lines changed

5 files changed

+126
-64
lines changed

codegen/src/main/scala/overflowdb/codegen/CodeGen.scala

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package overflowdb.codegen
22

33
import better.files._
44
import overflowdb.codegen.CodeGen.ConstantContext
5+
import overflowdb.schema.EdgeType.Cardinality
56
import overflowdb.schema.Property.ValueType
67
import overflowdb.schema._
78

@@ -457,7 +458,11 @@ class CodeGen(schema: Schema) {
457458
val neighbor = adjacentNode.neighbor
458459
val entireNodeHierarchy: Set[AbstractNodeType] = neighbor.subtypes(schema.allNodeTypes.toSet) ++ (neighbor.extendzRecursively :+ neighbor)
459460
entireNodeHierarchy.map { neighbor =>
460-
val accessorName = s"_${camelCase(neighbor.name)}Via${edge.className.capitalize}${camelCaseCaps(direction.toString)}"
461+
val accessorName = {
462+
if (adjacentNode.customStepName.isEmpty)
463+
s"_${camelCase(neighbor.name)}Via${edge.className.capitalize}${camelCaseCaps(direction.toString)}"
464+
else adjacentNode.customStepName
465+
}
461466
val cardinality = adjacentNode.cardinality
462467
val appendix = cardinality match {
463468
case EdgeType.Cardinality.One => ".next()"
@@ -474,7 +479,6 @@ class CodeGen(schema: Schema) {
474479
}.mkString("\n")
475480
}
476481

477-
478482
val companionObject = {
479483
val propertyNames = nodeBaseType.properties.map(_.name)
480484
val propertyNameDefs = propertyNames.map { name =>
@@ -566,7 +570,7 @@ class CodeGen(schema: Schema) {
566570

567571
// only edge and neighbor node matter, not the cardinality
568572
val inheritedLookup: Set[(EdgeType, AbstractNodeType)] =
569-
inherited.map(_.adjacentNode).map { case AdjacentNode(viaEdge, neighbor, _) => (viaEdge, neighbor) }.toSet
573+
inherited.map(_.adjacentNode).map { adjacentNode => (adjacentNode.viaEdge, adjacentNode.neighbor) }.toSet
570574

571575
val direct = adjacentNodes(nodeType).map { adjacentNode =>
572576
val isInherited = inheritedLookup.contains((adjacentNode.viaEdge, adjacentNode.neighbor))
@@ -578,7 +582,7 @@ class CodeGen(schema: Schema) {
578582
def createNeighborInfos(neighborContexts: Seq[AjacentNodeWithInheritanceStatus], direction: Direction.Value): Seq[NeighborInfoForEdge] = {
579583
neighborContexts.groupBy(_.adjacentNode.viaEdge).map { case (edge, neighborContexts) =>
580584
val neighborInfoForNodes = neighborContexts.map { case AjacentNodeWithInheritanceStatus(adjacentNode, isInherited) =>
581-
NeighborInfoForNode(adjacentNode.neighbor, edge, direction, adjacentNode.cardinality, isInherited)
585+
NeighborInfoForNode(adjacentNode.neighbor, edge, direction, adjacentNode.cardinality, isInherited, Option(adjacentNode.customStepName).filter(_.nonEmpty))
582586
}
583587
NeighborInfoForEdge(edge, neighborInfoForNodes, nextOffsetPos)
584588
}.toSeq
@@ -888,7 +892,7 @@ class CodeGen(schema: Schema) {
888892
val edgeAccessorName = neighborAccessorNameForEdge(neighborInfo.edge, direction)
889893
val nodeDelegators = neighborInfo.nodeInfos.collect {
890894
case neighborNodeInfo if !neighborNodeInfo.isInherited =>
891-
val accessorNameForNode = neighborNodeInfo.accessorName
895+
val accessorNameForNode = accessorName(neighborNodeInfo)
892896
s"def $accessorNameForNode: ${neighborNodeInfo.returnType} = get().$accessorNameForNode"
893897
}.mkString("\n")
894898

@@ -949,7 +953,7 @@ class CodeGen(schema: Schema) {
949953
case EdgeType.Cardinality.ZeroOrOne => s".nextOption()"
950954
case _ => ""
951955
}
952-
s"def ${neighborNodeInfo.accessorName}: ${neighborNodeInfo.returnType} = $edgeAccessorName.collectAll[${neighborNodeInfo.neighborNode.className}]$appendix"
956+
s"def ${accessorName(neighborNodeInfo)}: ${neighborNodeInfo.returnType} = $edgeAccessorName.collectAll[${neighborNodeInfo.neighborNode.className}]$appendix"
953957
}.mkString("\n")
954958

955959
s"""def $edgeAccessorName: overflowdb.traversal.Traversal[$neighborType] = overflowdb.traversal.Traversal(createAdjacentNodeIteratorByOffSet[$neighborType]($offsetPosition))
@@ -1117,13 +1121,6 @@ class CodeGen(schema: Schema) {
11171121
}
11181122

11191123
protected def writeNodeTraversalFiles(outputDir: File): Seq[File] = {
1120-
val staticHeader =
1121-
s"""package $traversalsPackage
1122-
|
1123-
|import overflowdb.traversal.Traversal
1124-
|import $nodesPackage._
1125-
|""".stripMargin
1126-
11271124
lazy val nodeTraversalImplicits = {
11281125
def implicitForNodeType(name: String) = {
11291126
val traversalName = s"${name}TraversalExtGen"
@@ -1152,9 +1149,25 @@ class CodeGen(schema: Schema) {
11521149
|""".stripMargin
11531150
}
11541151

1155-
def generatePropertyTraversals(className: String, properties: Seq[Property[_]]): String = {
1152+
def generateCustomStepNameTraversals(nodeType: AbstractNodeType): String = {
1153+
nodeType.edges
1154+
.filter(_.customStepName.nonEmpty)
1155+
.sortBy(_.customStepName)
1156+
.map { case AdjacentNode(viaEdge, neighbor, cardinality, customStepName) =>
1157+
val mapOrFlatMap = cardinality match {
1158+
case Cardinality.One => "map"
1159+
case Cardinality.ZeroOrOne | Cardinality.List => "flatMap"
1160+
}
1161+
s"""/** traverse to ${neighbor.name} via ${viaEdge.name} - this relationship was given a customStepName in the schema */
1162+
|def $customStepName: Traversal[${neighbor.className}] =
1163+
| traversal.$mapOrFlatMap(_.$customStepName)
1164+
|""".stripMargin
1165+
}.mkString("\n")
1166+
}
1167+
1168+
def generatePropertyTraversals(properties: Seq[Property[_]]): String = {
11561169
import Property.Cardinality
1157-
val propertyTraversals = properties.map { property =>
1170+
properties.map { property =>
11581171
val nameCamelCase = camelCase(property.name)
11591172
val baseType = typeFor(property)
11601173
val cardinality = property.cardinality
@@ -1494,31 +1507,23 @@ class CodeGen(schema: Schema) {
14941507
| $filterSteps
14951508
|""".stripMargin
14961509
}.mkString("\n")
1497-
1498-
s"""
1499-
|/** Traversal steps for $className */
1500-
|class ${className}TraversalExtGen[NodeType <: $className](val traversal: Traversal[NodeType]) extends AnyVal {
1501-
|
1502-
|$propertyTraversals
1503-
|
1504-
|}""".stripMargin
15051510
}
15061511

1507-
def generateNodeBaseTypeSource(nodeBaseType: NodeBaseType): String = {
1512+
def generateNodeTraversalExt(nodeType: AbstractNodeType): String = {
1513+
val className = nodeType.className
15081514
s"""package $traversalsPackage
15091515
|
15101516
|import overflowdb.traversal.Traversal
15111517
|import $nodesPackage._
15121518
|
1513-
|${generatePropertyTraversals(nodeBaseType.className, nodeBaseType.properties)}
1519+
|/** Traversal steps for $className */
1520+
|class ${className}TraversalExtGen[NodeType <: $className](val traversal: Traversal[NodeType]) extends AnyVal {
15141521
|
1515-
|""".stripMargin
1516-
}
1517-
1518-
def generateNodeSource(nodeType: NodeType) = {
1519-
s"""$staticHeader
1520-
|${generatePropertyTraversals(nodeType.className, nodeType.properties)}
1521-
|""".stripMargin
1522+
|${generateCustomStepNameTraversals(nodeType)}
1523+
|
1524+
|${generatePropertyTraversals(nodeType.properties)}
1525+
|
1526+
|}""".stripMargin
15221527
}
15231528

15241529
val packageObject =
@@ -1532,13 +1537,8 @@ class CodeGen(schema: Schema) {
15321537
baseDir.createDirectories()
15331538
results.append(baseDir.createChild("package.scala").write(packageObject))
15341539
results.append(baseDir.createChild("NodeTraversalImplicits.scala").write(nodeTraversalImplicits))
1535-
schema.nodeBaseTypes.foreach { nodeBaseTrait =>
1536-
val src = generateNodeBaseTypeSource(nodeBaseTrait)
1537-
val srcFile = nodeBaseTrait.className + ".scala"
1538-
results.append(baseDir.createChild(srcFile).write(src))
1539-
}
1540-
schema.nodeTypes.foreach { nodeType =>
1541-
val src = generateNodeSource(nodeType)
1540+
schema.allNodeTypes.foreach { nodeType =>
1541+
val src = generateNodeTraversalExt(nodeType)
15421542
val srcFile = nodeType.className + ".scala"
15431543
results.append(baseDir.createChild(srcFile).write(src))
15441544
}

codegen/src/main/scala/overflowdb/codegen/Helpers.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ object Helpers {
5555
}
5656
}
5757

58+
def accessorName(neighborInfoForNode: NeighborInfoForNode): String = {
59+
neighborInfoForNode.customStepName.getOrElse {
60+
val neighborNodeName = neighborInfoForNode.neighborNode.name
61+
val edgeName = neighborInfoForNode.edge.className
62+
val direction = neighborInfoForNode.direction.toString
63+
s"_${camelCase(neighborNodeName)}Via$edgeName${camelCaseCaps(direction)}"
64+
}
65+
}
66+
5867
def isNodeBaseTrait(baseTraits: Seq[NodeBaseType], nodeName: String): Boolean =
5968
nodeName == DefaultNodeTypes.AbstractNodeName || baseTraits.map(_.name).contains(nodeName)
6069

codegen/src/main/scala/overflowdb/schema/Schema.scala

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,22 @@ abstract class AbstractNodeType(val name: String, val comment: Option[String], v
7474
def addOutEdge(edge: EdgeType,
7575
inNode: AbstractNodeType,
7676
cardinalityOut: EdgeType.Cardinality = EdgeType.Cardinality.List,
77-
cardinalityIn: EdgeType.Cardinality = EdgeType.Cardinality.List): this.type = {
78-
_outEdges.add(AdjacentNode(edge, inNode, cardinalityOut))
79-
inNode._inEdges.add(AdjacentNode(edge, this, cardinalityIn))
77+
cardinalityIn: EdgeType.Cardinality = EdgeType.Cardinality.List,
78+
stepNameOut: String = "",
79+
stepNameIn: String = ""): this.type = {
80+
_outEdges.add(AdjacentNode(edge, inNode, cardinalityOut, stepNameOut))
81+
inNode._inEdges.add(AdjacentNode(edge, this, cardinalityIn, stepNameIn))
8082
this
8183
}
8284

8385
def addInEdge(edge: EdgeType,
8486
outNode: AbstractNodeType,
8587
cardinalityIn: EdgeType.Cardinality = EdgeType.Cardinality.List,
86-
cardinalityOut: EdgeType.Cardinality = EdgeType.Cardinality.List): this.type = {
87-
_inEdges.add(AdjacentNode(edge, outNode, cardinalityIn))
88-
outNode._outEdges.add(AdjacentNode(edge, this, cardinalityOut))
88+
cardinalityOut: EdgeType.Cardinality = EdgeType.Cardinality.List,
89+
stepNameIn: String = "",
90+
stepNameOut: String = ""): this.type = {
91+
_inEdges.add(AdjacentNode(edge, outNode, cardinalityIn, stepNameIn))
92+
outNode._outEdges.add(AdjacentNode(edge, this, cardinalityOut, stepNameOut))
8993
this
9094
}
9195

@@ -100,6 +104,9 @@ abstract class AbstractNodeType(val name: String, val comment: Option[String], v
100104
case Direction.IN => inEdges
101105
case Direction.OUT => outEdges
102106
}
107+
108+
def edges: Seq[AdjacentNode] =
109+
outEdges ++ inEdges
103110
}
104111

105112
class NodeType(name: String, comment: Option[String], schemaInfo: SchemaInfo)
@@ -134,7 +141,7 @@ class NodeBaseType(name: String, comment: Option[String], schemaInfo: SchemaInfo
134141
override def toString = s"NodeBaseType($name)"
135142
}
136143

137-
case class AdjacentNode(viaEdge: EdgeType, neighbor: AbstractNodeType, cardinality: EdgeType.Cardinality)
144+
case class AdjacentNode(viaEdge: EdgeType, neighbor: AbstractNodeType, cardinality: EdgeType.Cardinality, customStepName: String)
138145

139146
case class ContainedNode(nodeType: AbstractNodeType, localName: String, cardinality: Property.Cardinality)
140147

@@ -245,15 +252,14 @@ case class NeighborInfoForNode(
245252
edge: EdgeType,
246253
direction: Direction.Value,
247254
cardinality: EdgeType.Cardinality,
248-
isInherited: Boolean) {
249-
250-
lazy val accessorName = s"_${camelCase(neighborNode.name)}Via${edge.className}${camelCaseCaps(direction.toString)}"
255+
isInherited: Boolean,
256+
customStepName: Option[String] = None) {
251257

252258
/** handling some accidental complexity within the schema: if a relationship is defined on a base node and
253259
* separately on a concrete node, with different cardinalities, we need to use the highest cardinality */
254260
lazy val consolidatedCardinality: EdgeType.Cardinality = {
255261
val inheritedCardinalities = neighborNode.extendzRecursively.flatMap(_.inEdges).collect {
256-
case AdjacentNode(viaEdge, neighbor, cardinality)
262+
case AdjacentNode(viaEdge, neighbor, cardinality, _)
257263
if viaEdge == edge && neighbor == neighborNode => cardinality
258264
}
259265
val allCardinalities = cardinality +: inheritedCardinalities

integration-tests/schemas/src/main/scala/TestSchema02.scala

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class TestSchema02 extends TestSchema {
1010

1111
val order = builder.addProperty("ORDER", ValueType.Int, "General ordering property.")
1212

13-
val node1Base = builder.addNodeBaseType(
13+
val baseNode = builder.addNodeBaseType(
1414
name = "BASE_NODE",
1515
comment = "base node"
1616
).addProperty(name)
@@ -19,7 +19,7 @@ class TestSchema02 extends TestSchema {
1919
name = "NODE1",
2020
comment = "sample node 1"
2121
).addProperties(order)
22-
.extendz(node1Base)
22+
.extendz(baseNode)
2323

2424
val node2 = builder.addNodeType(
2525
name = "NODE2",
@@ -34,15 +34,19 @@ class TestSchema02 extends TestSchema {
3434
name = "EDGE2",
3535
comment = "sample edge 2").addProperty(name)
3636

37-
node1Base.addOutEdge(
37+
baseNode.addOutEdge(
3838
edge = edge1,
3939
inNode = node2,
4040
cardinalityOut = EdgeType.Cardinality.List,
41-
cardinalityIn = EdgeType.Cardinality.ZeroOrOne)
41+
cardinalityIn = EdgeType.Cardinality.ZeroOrOne,
42+
stepNameOut = "customStepName1",
43+
stepNameIn = "customStepName1Inverse")
4244

4345
node2.addOutEdge(
4446
edge = edge2,
45-
inNode = node1Base,
47+
inNode = baseNode,
4648
cardinalityOut = EdgeType.Cardinality.One,
47-
cardinalityIn = EdgeType.Cardinality.One)
49+
cardinalityIn = EdgeType.Cardinality.One,
50+
stepNameOut = "customStepName2",
51+
stepNameIn = "customStepName2Inverse")
4852
}

integration-tests/tests/src/test/scala/Schema02Test.scala

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,8 @@ class Schema02Test extends AnyWordSpec with Matchers {
8686
newNode.productElement(1) shouldBe "A"
8787
newNode.productPrefix shouldBe "NewNode1"
8888
}
89-
9089
}
9190

92-
9391
"working with a concrete sample graph" can {
9492
val graph = TestSchema.empty.graph
9593

@@ -98,20 +96,65 @@ class Schema02Test extends AnyWordSpec with Matchers {
9896
node1.addEdge(Edge1.Label, node2)
9997
node2.addEdge(Edge2.Label, node1, PropertyNames.NAME, "edge 02")
10098

101-
"lookup and traverse nodes/edges/properties" in {
102-
def baseNodeTraversal = graph.nodes(Node1.Label).cast[BaseNode]
103-
val baseNode = baseNodeTraversal.head
99+
def baseNodeTraversal = graph.nodes(Node1.Label).cast[BaseNode]
100+
def node1Traversal = graph.nodes(Node1.Label).cast[Node1]
101+
def node2Traversal = graph.nodes(Node2.Label).cast[Node2]
102+
103+
"lookup and traverse nodes/edges via domain specific dsl" in {
104+
def baseNode = baseNodeTraversal.head
105+
def node1 = node1Traversal.head
106+
def node2 = node2Traversal.head
107+
108+
baseNode.label shouldBe Node1.Label
109+
node1.label shouldBe Node1.Label
110+
node2.label shouldBe Node2.Label
111+
104112
baseNode.edge2In.l shouldBe Seq(node2)
105113
baseNode.edge1Out.l shouldBe Seq(node2)
106-
baseNode._node2ViaEdge2In shouldBe node2
107-
baseNode._node2ViaEdge1Out.l shouldBe Seq(node2)
114+
}
115+
116+
"generate custom defined stepNames from schema definition" in {
117+
def baseNode = baseNodeTraversal.head
118+
def node1 = node1Traversal.head
119+
def node2 = node2Traversal.head
120+
121+
val baseNodeToNode2: Traversal[Node2] = baseNode.customStepName1
122+
baseNodeToNode2.l shouldBe Seq(node2)
123+
val baseNodeTraversalToNode2: Traversal[Node2] = baseNodeTraversal.customStepName1
124+
baseNodeTraversalToNode2.l shouldBe Seq(node2)
125+
126+
val baseNodeToNode2ViaEdge2: Node2 = baseNode.customStepName2Inverse
127+
baseNodeToNode2ViaEdge2 shouldBe node2
128+
val baseNodeTraversalToNode2ViaEdge2: Traversal[Node2] = baseNodeTraversal.customStepName2Inverse
129+
baseNodeTraversalToNode2ViaEdge2.l shouldBe Seq(node2)
130+
131+
val node1ToNode2: Traversal[Node2] = node1.customStepName1
132+
node1ToNode2.l shouldBe Seq(node2)
133+
val node1TraversalToNode2: Traversal[Node2] = node1Traversal.customStepName1
134+
node1TraversalToNode2.l shouldBe Seq(node2)
135+
136+
val node1ToNode2ViaEdge2: Node2 = node1.customStepName2Inverse
137+
node1ToNode2ViaEdge2 shouldBe node2
138+
val node1TraversalToNode2ViaEdge2: Traversal[Node2] = node1Traversal.customStepName2Inverse
139+
node1TraversalToNode2ViaEdge2.l shouldBe Seq(node2)
140+
141+
val node2ToBaseNodeViaEdge2: BaseNode = node2.customStepName2
142+
node2ToBaseNodeViaEdge2 shouldBe node1
143+
val node2TraversalToBaseNodeViaEdge2: Traversal[BaseNode] = node2Traversal.customStepName2
144+
node2TraversalToBaseNodeViaEdge2.l shouldBe Seq(node1)
145+
146+
val node2ToBaseNodeViaEdge1: Option[BaseNode] = node2.customStepName1Inverse
147+
node2ToBaseNodeViaEdge1 shouldBe Some(node1)
148+
val node2TraversalToBaseNodeViaEdge1: Traversal[BaseNode] = node2Traversal.customStepName1Inverse
149+
node2TraversalToBaseNodeViaEdge1.l shouldBe Seq(node1)
150+
}
108151

152+
"property filters" in {
109153
baseNodeTraversal.name.l shouldBe Seq("node 01")
110154
baseNodeTraversal.name(".*").size shouldBe 1
111155
baseNodeTraversal.nameExact("node 01").size shouldBe 1
112156
baseNodeTraversal.nameNot("abc").size shouldBe 1
113157

114-
def node1Traversal = graph.nodes(Node1.Label).cast[Node1]
115158
node1Traversal.order.l shouldBe Seq(4)
116159
node1Traversal.orderGt(3).size shouldBe 1
117160
node1Traversal.orderLt(4).size shouldBe 0

0 commit comments

Comments
 (0)