From 4a6c074fe0b4d016dc9e19f761ef1df404f63714 Mon Sep 17 00:00:00 2001 From: Martin Kleppmann Date: Sun, 14 Jun 2009 23:00:56 +0100 Subject: [PATCH] 'Stats' example query resource with model + specs --- src/main/scala/com/example/models/Stats.scala | 36 ++++++++ .../com/example/restapi/StatsResource.scala | 24 ++++++ .../scala/com/example/models/StatsSpec.scala | 83 +++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 src/main/scala/com/example/models/Stats.scala create mode 100644 src/main/scala/com/example/restapi/StatsResource.scala create mode 100644 src/test/scala/com/example/models/StatsSpec.scala diff --git a/src/main/scala/com/example/models/Stats.scala b/src/main/scala/com/example/models/Stats.scala new file mode 100644 index 0000000..d905918 --- /dev/null +++ b/src/main/scala/com/example/models/Stats.scala @@ -0,0 +1,36 @@ +package com.example.models + +import java.util.logging.Logger +import org.codehaus.jettison.json.JSONObject +import org.neo4j.api.core._ +import com.eptcomputing.neo4j.IteratorConverters + +/** + * Example of a read-only model object. It is initialised with a Neo4j node and counts how many + * other nodes are reachable from that node by outgoing "KNOWS" relationships, grouped by length + * of relationship chain. + * + * Example tests for this class are given in StatsSpec. + */ +class Stats(startNode: Node) extends IteratorConverters { + private val log = Logger.getLogger(this.getClass.getName) + + // Traverse the graph + private val traverser = startNode.traverse(Traverser.Order.BREADTH_FIRST, StopEvaluator.END_OF_GRAPH, + ReturnableEvaluator.ALL_BUT_START_NODE, Predicates.KNOWS, Direction.OUTGOING) + + private var countByDepth = Map[Int,Int]() + + // Count how many times each depth has occurred + for (node <- traverser) { + val depth = traverser.currentPosition.depth + countByDepth += depth -> (countByDepth.getOrElse(depth, 0) + 1) + } + + /** Converts this object to JSON. */ + def toJSON = { + val json = new JSONObject + for ((key, value) <- countByDepth) json.put("depth_" + key, value) + json + } +} diff --git a/src/main/scala/com/example/restapi/StatsResource.scala b/src/main/scala/com/example/restapi/StatsResource.scala new file mode 100644 index 0000000..5039ad0 --- /dev/null +++ b/src/main/scala/com/example/restapi/StatsResource.scala @@ -0,0 +1,24 @@ +package com.example.restapi + +import javax.ws.rs.Path +import org.codehaus.jettison.json.JSONObject +import org.neo4j.api.core.{NeoService, Node} + +import com.example.models.Stats + +/** + * Example of a read-only resource which performs a query (graph traversal) and returns + * a result object as JSON. This class defines the API, while the Stats model + * class implements the actual query. + */ +@Path("/stats") +class StatsResource extends com.eptcomputing.neo4j.rest.NeoResource { + + def read(neo: NeoService, node: Node) = new Stats(node).toJSON + + def create(neo: NeoService, json: JSONObject) = throw new Exception("not allowed") + + def update(neo: NeoService, existing: Node, newValue: JSONObject) = throw new Exception("not allowed") + + def delete(neo: NeoService, node: Node) = throw new Exception("not allowed") +} diff --git a/src/test/scala/com/example/models/StatsSpec.scala b/src/test/scala/com/example/models/StatsSpec.scala new file mode 100644 index 0000000..68e3c21 --- /dev/null +++ b/src/test/scala/com/example/models/StatsSpec.scala @@ -0,0 +1,83 @@ +package com.example.models + +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.junit.runner.RunWith +import com.jteigen.scalatest.JUnit4Runner + +import org.codehaus.jettison.json.JSONObject +import org.neo4j.api.core._ + +import com.eptcomputing.neo4j.{NeoConverters, NeoServer} + +/** + * This is an example of how you can write good and expressive tests/specs using + * the scalatest spec framework. This spec tests the Stats model + * directly, without going through the REST API. For an example of testing full + * API request/response cycles, see NeoResourceTest. + */ +@RunWith(classOf[JUnit4Runner]) +class StatsSpec extends Spec with ShouldMatchers with NeoConverters { + + import Predicates._ + + describe("Stats model") { + + it("should return an empty object if the node has no relationships") { + NeoServer.exec { neo => + val node = neo.createNode + new Stats(node).toJSON.length should equal(0) + } + } + + it("should return the number of neighbours") { + NeoServer.exec { neo => + val node = neo.createNode + (1 to 3) foreach { _ => node --| KNOWS --> neo.createNode } + new Stats(node).toJSON.getInt("depth_1") should equal(3) + } + } + + it("should not count a node twice") { + NeoServer.exec { neo => + val start = neo.createNode + val end = neo.createNode + start --| "KNOWS" --> neo.createNode --| "KNOWS" --> end + start --| "KNOWS" --> neo.createNode --| "KNOWS" --> end + val stats = new Stats(start).toJSON + stats.getInt("depth_1") should equal(2) + stats.getInt("depth_2") should equal(1) + } + } + + it("should not follow inbound relationships") { + NeoServer.exec { neo => + val node = neo.createNode + neo.createNode --| KNOWS --> node --| KNOWS --> neo.createNode + new Stats(node).toJSON.getInt("depth_1") should equal(1) + } + } + + it("should not follow relationships of a different type") { + NeoServer.exec { neo => + val node = neo.createNode + node --| "KNOWS" --> neo.createNode + node --| "LIKES" --> neo.createNode + new Stats(node).toJSON.getInt("depth_1") should equal(1) + } + } + + it("should count only the shortest path to each reachable node") { + NeoServer.exec { neo => + val start = neo.createNode + val reachByTwoPaths = neo.createNode + start --| KNOWS --> neo.createNode --| KNOWS --> reachByTwoPaths + start --| KNOWS --> neo.createNode --| KNOWS --> neo.createNode --| KNOWS --> reachByTwoPaths + val stats = new Stats(start).toJSON + stats.getInt("depth_1") should equal(2) + stats.getInt("depth_2") should equal(2) + stats.has("depth_3") should equal(false) + } + } + } +}