diff --git a/.travis.yml b/.travis.yml
index ee03297..58f2373 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -25,5 +25,5 @@ jdk:
- oraclejdk7
- openjdk7
-script: "sbt test"
+script: "sbt \"test-only * -- -l RequiresNeo4jServer\""
diff --git a/README.md b/README.md
index 14ca0e6..55bef6b 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,17 @@ class PersonRelation extends Relationship[PersonRelation, Person] {
# Querying
Back to top
+## Connection
+
+Prerequisite to making Neo4j requests is REST endpoint definition. This is achived using RestConnection class.
+
+```
+scala> implicit val service = RestConnection("localhost", 7474)
+service: RestConnection
+```
+
+## Making requests
+
In this example all nodes of Person type are returned.
```
scala> val personNodes = Person().returns(case p ~~ _ => p).execute
@@ -108,20 +119,23 @@ personNodes: Future[Seq[Person]]
Query for a person that has a relationship to another person
```
-scala> val personNodes = (Person() :->: Person()).returns(case p1 ~~ _ => p).execute
+scala> val personNodes = (Person() :->: Person())
+ .returns(case p1 ~~ _ => p).execute
personNodes: Future[Seq[Person]]
```
Query for a person that has a relationship to another person with given name
```
-scala> val personNodes = (Person() :->: Person(_.name := "James")).returns(case p ~~ _ => p).execute
+scala> val personNodes = (Person() :->: Person(_.name := "James"))
+ .returns(case p ~~ _ => p).execute
personNodes: Future[Seq[Person]]
```
Query for a person that has a relationship to another person
```
-scala> val personNodes = (Person() :<-: WorkRelationship() :->: Person()).returns(case p1 ~~ r ~~ p2 ~~ _ => p1).execute
+scala> val personNodes = (Person() :<-: WorkRelationship() :->: Person())
+ .returns(case p1 ~~ r ~~ p2 ~~ _ => p1).execute
personNodes: Future[Seq[Person]]
```
@@ -132,3 +146,16 @@ scala> val personNodes = (Person() :-: WorkRelationship(_.company := "ABC") :->:
.returns(case p1 ~~ _ => p1).execute
personNodes: Future[Seq[Person]]
```
+
+## An arbitrary Cypher query
+Cypher is a rich language and whenever you need to use it directly escaping the abstraction layer it's still possible
+with ReactiveNeo. Use the same REST connection object with an arbitrary Cypher query.
+```
+scala> val query = "MATCH (n:Person) RETURN n"
+query: String
+implicit val parser: Reads[Person] = ((__ \ "name").read[String] and (__ \ "age").read[Int])(Person)
+
+parser: Reads[Person]
+val result = service.makeRequest[Person](query).execute
+result: Future[Seq[Person]]
+```
\ No newline at end of file
diff --git a/project/Build.scala b/project/Build.scala
index 1314beb..3572b95 100644
--- a/project/Build.scala
+++ b/project/Build.scala
@@ -21,11 +21,15 @@ import org.scalastyle.sbt.ScalastylePlugin
object reactiveneo extends Build {
- val UtilVersion = "0.4.0"
- val ScalatestVersion = "2.2.0-M1"
- val FinagleVersion = "6.20.0"
- val playVersion = "2.3.4"
+ val scalaMajorVersion = "2.11"
+ val scalaMinorVersion = "6"
+ val scalaFullVersion = scalaMajorVersion + "." + scalaMinorVersion
+
+ val FinagleVersion = "6.24.0"
+ val playVersion = "2.3.7"
+ val ScalatestVersion = "2.2.1"
val ScalazVersion = "7.1.0"
+ val Neo4jVersion = "2.1.7"
val publishUrl = "http://maven.websudos.co.uk"
@@ -57,7 +61,7 @@ object reactiveneo extends Build {
- benjumanji
+ bjankie1
Bartosz Jankiewicz
http://github.com/bjankie1
@@ -86,7 +90,7 @@ object reactiveneo extends Build {
val sharedSettings: Seq[Def.Setting[_]] = Seq(
organization := "com.websudos",
version := "0.1.2",
- scalaVersion := "2.10.4",
+ scalaVersion := scalaFullVersion,
resolvers ++= Seq(
"Typesafe repository snapshots" at "http://repo.typesafe.com/typesafe/snapshots/",
"Typesafe repository releases" at "http://repo.typesafe.com/typesafe/releases/",
@@ -112,13 +116,11 @@ object reactiveneo extends Build {
"-unchecked"
),
libraryDependencies ++= Seq(
- "com.chuusai" % "shapeless_2.10.4" % "2.0.0",
"com.github.nscala-time" %% "nscala-time" % "1.0.0",
"com.typesafe.scala-logging" %% "scala-logging-slf4j" % "2.1.2",
- "com.websudos" %% "util-testing" % UtilVersion % "test",
"org.scalaz" %% "scalaz-scalacheck-binding" % ScalazVersion % "test",
"org.scalatest" %% "scalatest" % ScalatestVersion % "test, provided",
- "org.scalamock" %% "scalamock-scalatest-support" % "3.0.1" % "test"
+ "org.scalamock" %% "scalamock-scalatest-support" % "3.2.1" % "test"
),
fork in Test := true,
javaOptions in Test ++= Seq("-Xmx2G")
@@ -132,60 +134,45 @@ object reactiveneo extends Build {
name := "ReactiveNeo"
).aggregate(
reactiveneoDsl,
- reactiveneoTesting,
- reactiveneoZookeeper
+ reactiveneoTesting
)
lazy val reactiveneoDsl = Project(
id = "reactiveneo-dsl",
base = file("reactiveneo-dsl"),
- settings = Defaults.coreDefaultSettings ++
- sharedSettings ++
- publishSettings
+ settings = Defaults.coreDefaultSettings ++ sharedSettings ++ publishSettings
).settings(
name := "reactiveneo-dsl",
libraryDependencies ++= Seq(
- "com.chuusai" % "shapeless_2.10.4" % "2.0.0",
- "org.scala-lang" % "scala-reflect" % "2.10.4",
+ "org.scala-lang" % "scala-reflect" % scalaFullVersion,
"com.twitter" %% "finagle-http" % FinagleVersion,
- "com.twitter" %% "util-core" % FinagleVersion,
+ "com.twitter" %% "util-core" % "6.23.0",
"joda-time" % "joda-time" % "2.3",
"org.joda" % "joda-convert" % "1.6",
"com.typesafe.play" %% "play-json" % playVersion,
- "net.liftweb" %% "lift-json" % "2.6-M4" % "test, provided"
+ "org.neo4j" % "neo4j-cypher" % Neo4jVersion
)
).dependsOn(
reactiveneoTesting % "test, provided"
)
- lazy val reactiveneoZookeeper = Project(
- id = "reactiveneo-zookeeper",
- base = file("reactiveneo-zookeeper"),
- settings = Defaults.coreDefaultSettings ++ sharedSettings
- ).settings(
- name := "reactiveneo-zookeeper",
- libraryDependencies ++= Seq(
- "com.twitter" %% "finagle-serversets" % FinagleVersion,
- "com.twitter" %% "finagle-zookeeper" % FinagleVersion
- )
- )
-
lazy val reactiveneoTesting = Project(
id = "reactiveneo-testing",
base = file("reactiveneo-testing"),
- settings = Defaults.coreDefaultSettings ++ sharedSettings
+ settings = Defaults.coreDefaultSettings ++ sharedSettings ++ publishSettings
).settings(
name := "reactiveneo-testing",
libraryDependencies ++= Seq(
- "com.twitter" %% "util-core" % FinagleVersion,
- "com.websudos" %% "util-testing" % UtilVersion,
+ "com.twitter" %% "util-core" % "6.23.0",
+ "com.twitter" %% "finagle-http" % FinagleVersion,
"org.scalatest" %% "scalatest" % ScalatestVersion,
- "org.scalacheck" %% "scalacheck" % "1.11.3",
"org.fluttercode.datafactory" % "datafactory" % "0.8",
- "com.twitter" %% "finagle-http" % FinagleVersion,
- "com.twitter" %% "util-core" % FinagleVersion
- )
- ).dependsOn(
- reactiveneoZookeeper
+ "org.neo4j" % "neo4j-cypher" % Neo4jVersion % "compile",
+ "org.neo4j" % "neo4j-kernel" % Neo4jVersion % "compile",
+ "org.neo4j" % "neo4j-kernel" % Neo4jVersion % "compile" classifier "tests"
+ ),
+ fork in Test := true,
+ javaOptions in Test ++= Seq("-Xmx2G")
)
+
}
diff --git a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/attribute/AbstractAttribute.scala b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/attribute/AbstractAttribute.scala
index c3c82ba..b890e91 100644
--- a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/attribute/AbstractAttribute.scala
+++ b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/attribute/AbstractAttribute.scala
@@ -31,7 +31,7 @@ abstract class AbstractAttribute[@specialized(Int, Double, Float, Long, Boolean,
* @param query Query result data.
* @return Decoded attribute value.
*/
- def apply(query: QueryRecord): T
+ def apply(query: QueryRecord): Option[T]
}
@@ -47,10 +47,21 @@ abstract class Attribute[Owner <: GraphObject[Owner, R], R, T](val owner: GraphO
class StringAttribute[Owner <: GraphObject[Owner, R], R](graphObject: GraphObject[Owner, R])
extends Attribute[Owner, R, String](graphObject) {
- override def apply(query: QueryRecord): String = {
- query[String](name).get
+ override def apply(query: QueryRecord): Option[String] = {
+ query[String](name)
}
+}
+
+/**
+ * Long attribute definition.
+ */
+class LongAttribute[Owner <: GraphObject[Owner, R], R](graphObject: GraphObject[Owner, R])
+ extends Attribute[Owner, R, Long](graphObject) {
+
+ override def apply(query: QueryRecord): Option[Long] = {
+ query[Long](name)
+ }
}
@@ -58,8 +69,8 @@ class StringAttribute[Owner <: GraphObject[Owner, R], R](graphObject: GraphObjec
class IntegerAttribute[Owner <: GraphObject[Owner, R], R](graphObject: GraphObject[Owner, R])
extends Attribute[Owner, R, Int](graphObject) {
- override def apply(query: QueryRecord): Int = {
- query[Int](name).get
+ override def apply(query: QueryRecord): Option[Int] = {
+ query[Int](name)
}
}
diff --git a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/RestCall.scala b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/RestCall.scala
new file mode 100644
index 0000000..046348c
--- /dev/null
+++ b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/RestCall.scala
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2014 websudos ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.websudos.reactiveneo.client
+
+import java.util.concurrent.TimeUnit
+
+import com.typesafe.scalalogging.slf4j.LazyLogging
+import com.websudos.reactiveneo.dsl.MatchQuery
+import org.jboss.netty.handler.codec.http.HttpMethod
+import play.api.libs.json.Reads
+
+import scala.concurrent.Future
+import scala.concurrent.duration.FiniteDuration
+
+/**
+ * REST API endpoints definitions.
+ * @param path Server query path.
+ * @param method HTTP method, with POST as default.
+ */
+case class RestEndpoint(path: String, method: HttpMethod = HttpMethod.POST)
+object SingleTransaction extends RestEndpoint("/db/data/transaction/commit")
+object BeginTransaction extends RestEndpoint("/db/data/transaction")
+class ContinueInTransaction(transactionId: Int) extends RestEndpoint(s"/db/data/transaction/$transactionId")
+class CommitTransaction(transactionId: Int) extends RestEndpoint(s"/db/data/transaction/$transactionId/commit")
+class RollbackTransaction(transactionId: Int) extends RestEndpoint(s"/db/data/transaction/$transactionId", HttpMethod.DELETE)
+
+/**
+ * Model of a call to Neo4j server.
+ * @tparam RT Type of result call response.
+ */
+class RestCall[RT](endpoint: RestEndpoint, content: Option[String], resultParser: Reads[RT])(implicit client: RestClient)
+ extends ServerCall[Seq[RT]]
+ with LazyLogging {
+
+ implicit lazy val parser = {
+ val parser = new CypherResultParser[RT]()(resultParser)
+ parser
+ }
+
+ def execute: Future[Seq[RT]] = {
+ val result = client.makeRequest[Seq[RT]](endpoint.path, endpoint.method, content)
+ result
+ }
+
+}
+
+object RestCall {
+
+ def apply[RT](endpoint: RestEndpoint, resultParser: Reads[RT], query: String)(implicit client: RestClient) = {
+ new RestCall[RT](endpoint, Some(query), resultParser)
+ }
+
+ def apply[RT](endpoint: RestEndpoint, resultParser: Reads[RT])(implicit client: RestClient) = {
+ new RestCall[RT](endpoint, None, resultParser)
+ }
+}
+
+/**
+ * Service that prepares and executes rest call
+ */
+class RestConnection(config: ClientConfiguration) {
+
+ implicit def client: RestClient = new RestClient(config)
+
+ def neoStatement( cypher: String ) =
+ s"""{
+ | "statements" : [ {
+ | "statement" : "$cypher"
+ | } ]
+ |}""".stripMargin
+
+ implicit def makeRequest[RT](matchQuery: MatchQuery[_, _, _, _, _, RT]): RestCall[RT] = {
+ val (query, retType) = matchQuery.finalQuery
+ val requestContent = neoStatement(query)
+ val call = RestCall(SingleTransaction, retType.resultParser, requestContent)
+ call
+ }
+
+
+ implicit def makeRequest[RT](cypher: String)(implicit resultParser: Reads[RT]): RestCall[RT] = {
+ val requestContent = neoStatement(cypher)
+ val call = RestCall(SingleTransaction, resultParser, requestContent)
+ call
+ }
+
+}
+
+object RestConnection {
+
+ def apply(host: String, port: Int): RestConnection = {
+ val config = ClientConfiguration("localhost", 7474, FiniteDuration(10, TimeUnit.SECONDS))
+ new RestConnection(config)
+ }
+
+}
\ No newline at end of file
diff --git a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/RestClient.scala b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/RestClient.scala
index 9aac92b..d7e0f27 100644
--- a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/RestClient.scala
+++ b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/RestClient.scala
@@ -56,6 +56,9 @@ class RestClient(config: ClientConfiguration) extends StrictLogging {
content.foreach { body =>
request.setContent(ChannelBuffers.copiedBuffer(body, Charset.forName("UTF-8")))
}
+ request.headers()
+ .add("Content-Type", "application/json")
+ .add("Host",config.server)
val response: util.Future[HttpResponse] = client(request)
response onSuccess { resp: HttpResponse =>
diff --git a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/ServerCall.scala b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/ServerCall.scala
index c9171d9..9b89214 100644
--- a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/ServerCall.scala
+++ b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/ServerCall.scala
@@ -14,50 +14,15 @@
*/
package com.websudos.reactiveneo.client
-import com.websudos.reactiveneo.dsl.ReturnExpression
-import org.jboss.netty.handler.codec.http.HttpMethod
-
import scala.concurrent.Future
/**
- * REST API endpoints definitions.
- * @param path Server query path.
- * @param method HTTP method, with POST as default.
- */
-case class RestEndpoint(path: String, method: HttpMethod = HttpMethod.POST)
-object SingleTransaction extends RestEndpoint("/db/data/transaction/commit")
-object BeginTransaction extends RestEndpoint("/db/data/transaction")
-class ContinueInTransaction(transactionId: Int) extends RestEndpoint(s"/db/data/transaction/$transactionId")
-class CommitTransaction(transactionId: Int) extends RestEndpoint(s"/db/data/transaction/$transactionId/commit")
-class RollbackTransaction(transactionId: Int) extends RestEndpoint(s"/db/data/transaction/$transactionId", HttpMethod.DELETE)
-
-/**
- * Model of a call to Neo4j server.
- * @tparam RT Type of result call response.
+ * Abstraction of communication with a server.
+ *
+ * @tparam RT type of server response
*/
-class ServerCall[RT](endpoint: RestEndpoint, content: Option[String], returnExpression: ReturnExpression[RT])
-(implicit client: RestClient) {
+trait ServerCall[RT] {
- implicit lazy val parser = {
- val parser = new CypherResultParser[RT]()(returnExpression.resultParser)
- parser
- }
-
-
- def execute: Future[Seq[RT]] = {
- val result = client.makeRequest[Seq[RT]](endpoint.path, endpoint.method)
- result
- }
+ def execute(): Future[RT]
}
-
-object ServerCall {
-
- def apply[RT](endpoint: RestEndpoint, returnExpression: ReturnExpression[RT], query: String)(implicit client: RestClient) = {
- new ServerCall[RT](endpoint, Some(query), returnExpression)
- }
-
- def apply[RT](endpoint: RestEndpoint, returnExpression: ReturnExpression[RT])(implicit client: RestClient) = {
- new ServerCall[RT](endpoint, None, returnExpression)
- }
-}
\ No newline at end of file
diff --git a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/dsl/MatchQuery.scala b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/dsl/MatchQuery.scala
index 60b3d38..31b2f4e 100644
--- a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/dsl/MatchQuery.scala
+++ b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/dsl/MatchQuery.scala
@@ -14,7 +14,7 @@
*/
package com.websudos.reactiveneo.dsl
-import com.websudos.reactiveneo.client.{ServerCall, SingleTransaction, RestClient}
+import com.websudos.reactiveneo.client.RestConnection
import com.websudos.reactiveneo.query.{BuiltQuery, CypherKeywords, CypherQueryBuilder}
import scala.annotation.implicitNotFound
@@ -56,53 +56,53 @@ private[reactiveneo] abstract class LimitUnbound extends LimitBind
* @param builtQuery Current query string.
* @param context Map of added node types to corresponding alias value used in RETURN clause.
*/
-private[reactiveneo] class MatchQuery[
- P <: Pattern,
- WB <: WhereBind,
- RB <: ReturnBind,
- OB <: OrderBind,
- LB <: LimitBind,
- RT](pattern: P,
- builtQuery: BuiltQuery,
- context: QueryBuilderContext,
- ret: Option[ReturnExpression[RT]] = None) extends CypherQueryBuilder {
-
+private[reactiveneo] class MatchQuery[P <: Pattern, WB <: WhereBind, RB <: ReturnBind, OB <: OrderBind, LB <: LimitBind, RT](
+ pattern: P,
+ builtQuery: BuiltQuery,
+ context: QueryBuilderContext,
+ ret: Option[ReturnExpression[RT]] = None) extends CypherQueryBuilder {
def query: String = builtQuery.queryString
-
@implicitNotFound("You cannot use two where clauses on a single query")
- final def where(condition: P => Criteria[_])
- (implicit ev: WB =:= WhereUnbound): MatchQuery[P, WhereBound, RB, OB, LB, _] = {
- new MatchQuery[P, WhereBound, RB, OB, LB, Any] (
+ final def where(
+ condition: P => Criteria[_])(implicit ev: WB =:= WhereUnbound): MatchQuery[P, WhereBound, RB, OB, LB, _] = {
+ new MatchQuery[P, WhereBound, RB, OB, LB, Any](
pattern,
where(builtQuery, condition(pattern).clause),
context)
}
- final def returns[URT](ret: P => ReturnExpression[URT]): MatchQuery[P, WB, ReturnBound, OB, LB, URT] = {
- new MatchQuery[P, WB, ReturnBound, OB, LB, URT] (
+ final def returns[URT](ret: P => ReturnExpression[URT]): MatchQuery[P, WB, ReturnBound, OB, LB, URT] = {
+ new MatchQuery[P, WB, ReturnBound, OB, LB, URT](
pattern,
builtQuery.appendSpaced(CypherKeywords.RETURN).appendSpaced(ret(pattern).query(context)),
context,
Some(ret(pattern)))
}
+ /**
+ * Generate final query and result type tuple.
+ */
@implicitNotFound("You need to add return clause to capture the type of result")
- final def execute(implicit client: RestClient, ev: RB =:= ReturnBound): Future[Seq[RT]] = {
- val call = ServerCall(SingleTransaction, ret.get, builtQuery.queryString)
- call.execute
+ final def finalQuery: (String, ReturnExpression[RT]) = {
+ (builtQuery.queryString, ret.get)
}
+ /**
+ * Execute the request against provided REST endpoint
+ * @param connection REST endpoint calling service.
+ * @return Asynchronous response
+ */
+ def execute(implicit connection: RestConnection): Future[Seq[RT]] = connection.makeRequest(this).execute
+
}
private[reactiveneo] object MatchQuery {
-
def createRootQuery[P <: Pattern](
- pattern: P,
- context: QueryBuilderContext):
- MatchQuery[P, WhereUnbound, ReturnUnbound, OrderUnbound, LimitUnbound, _] = {
+ pattern: P,
+ context: QueryBuilderContext): MatchQuery[P, WhereUnbound, ReturnUnbound, OrderUnbound, LimitUnbound, _] = {
pattern.foreach(context.nextLabel(_))
val query = new BuiltQuery(CypherKeywords.MATCH).appendSpaced(pattern.queryClause(context))
new MatchQuery[P, WhereUnbound, ReturnUnbound, OrderUnbound, LimitUnbound, Any](
diff --git a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/dsl/ReturnExpression.scala b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/dsl/ReturnExpression.scala
index a0eb8e0..5c852d7 100644
--- a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/dsl/ReturnExpression.scala
+++ b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/dsl/ReturnExpression.scala
@@ -32,8 +32,8 @@ abstract class ReturnExpression[R] {
def query(context: QueryBuilderContext): BuiltQuery
/**
- * Builds result parser for this expression. This is not a full parser but [[Reads]] for extracting values from
- * a single row of data.
+ * Builds result parser for this expression. This is not a full parser but [[play.api.libs.json.Reads]]
+ * for extracting values from a single row of data.
* @return Returns the result parser.
*/
def resultParser: Reads[R]
@@ -54,11 +54,10 @@ case class ObjectReturnExpression[GO <: GraphObject[GO, R], R](go: GraphObject[G
}
- override def resultParser: Reads[R] = {
- __.read[JsObject].map { obj =>
- go.fromQuery(QueryRecord(obj))
- }
+ override def resultParser: Reads[R] = __.read[JsObject].map { obj =>
+ go.fromQuery(QueryRecord(obj))
}
+
}
diff --git a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/query/QueryRecord.scala b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/query/QueryRecord.scala
index d28feed..806de44 100644
--- a/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/query/QueryRecord.scala
+++ b/reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/query/QueryRecord.scala
@@ -17,7 +17,7 @@ package com.websudos.reactiveneo.query
import play.api.libs.json.{Reads, JsObject}
/**
- * Wraps a single record result of Cypher query.
+ * Wraps a single value of `row` JSON attribute of Cypher query result.
*/
class QueryRecord(obj: JsObject) {
diff --git a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/CypherResultParserTest.scala b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/CypherResultParserTest.scala
index 7992992..63c9493 100644
--- a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/CypherResultParserTest.scala
+++ b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/CypherResultParserTest.scala
@@ -14,6 +14,7 @@
*/
package com.websudos.reactiveneo.client
+import com.websudos.reactiveneo.dsl.{TestNodeRecord, TestNode, ObjectReturnExpression}
import org.scalatest.{Matchers, FlatSpec}
import play.api.libs.json.Reads._
import play.api.libs.json._
@@ -45,6 +46,14 @@ class CypherResultParserTest extends FlatSpec with Matchers {
result shouldEqual Seq("test1", "test2")
}
+ it should "parse zero attribute node" in {
+ val expression = ObjectReturnExpression(TestNode)
+ val parser = new CypherResultParser[TestNodeRecord]()(expression.resultParser)
+ val json = """{"results":[{"columns":["n"],"data":[{"row":[{}]},{"row":[{}]},{"row":[{}]}]}],"errors":[]}"""
+ val js = Json.parse(json)
+ val result = parser.parseResult(js)
+ result should have size 3
+ }
it should "parse error result" in {
diff --git a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/ServerCallTest.scala b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestCallTest.scala
similarity index 74%
rename from reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/ServerCallTest.scala
rename to reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestCallTest.scala
index d614b6f..1c782c8 100644
--- a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/ServerCallTest.scala
+++ b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestCallTest.scala
@@ -14,15 +14,16 @@
*/
package com.websudos.reactiveneo.client
+import com.websudos.reactiveneo.RequiresNeo4jServer
import com.websudos.reactiveneo.dsl.{ObjectReturnExpression, TestNode, TestNodeRecord}
-import org.scalatest.{Matchers, FlatSpec}
-import com.websudos.util.testing._
+import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}
+import org.scalatest.{FlatSpec, Matchers}
+
import scala.concurrent.duration._
-import scala.concurrent.ExecutionContext.Implicits.global
-class ServerCallTest extends FlatSpec with Matchers with ServerMockSugar {
+class RestCallTest extends FlatSpec with Matchers with ServerMockSugar with ScalaFutures with IntegrationPatience {
- it should "execute call and parse result" in {
+ it should "execute call and parse result" taggedAs RequiresNeo4jServer in {
val testNode = new TestNode
val retEx = new ObjectReturnExpression[TestNode, TestNodeRecord](testNode)
withServer( req =>
@@ -40,9 +41,9 @@ class ServerCallTest extends FlatSpec with Matchers with ServerMockSugar {
val configuration = ClientConfiguration(addr.getHostName, addr.getPort, 1 second)
implicit val client = new RestClient(configuration)
- val call = ServerCall(SingleTransaction, retEx, "match (tn: TestNode) return tn")
+ val call = RestCall(SingleTransaction, retEx.resultParser, "match (tn: TestNode) return tn")
val result = call.execute
- result successful { res =>
+ whenReady(result) { res =>
res should have length 1
res.head.name shouldEqual "Test name"
}
diff --git a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestClientSpec.scala b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestClientSpec.scala
new file mode 100644
index 0000000..51196b6
--- /dev/null
+++ b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestClientSpec.scala
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2014 websudos ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.websudos.reactiveneo.client
+
+import com.websudos.reactiveneo.RequiresNeo4jServer
+import com.websudos.reactiveneo.dsl._
+import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}
+import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers}
+import play.api.libs.functional.syntax._
+import play.api.libs.json.Reads._
+import play.api.libs.json._
+
+case class InsertResult(id: Int)
+
+case class Person(name: String, age: Int)
+
+class RestClientSpec extends FeatureSpec with GivenWhenThen with Matchers
+ with ScalaFutures with IntegrationPatience {
+
+ info("As a user")
+ info("I want to be able to make a call to Neo4j server")
+ info("So I can get the data")
+ info("And expect the the result to be parsed for me")
+
+ feature("REST client") {
+
+ scenario("send a simple MATCH query", RequiresNeo4jServer) {
+ Given("started Neo4j server")
+ implicit val service = RestConnection("localhost", 7474)
+ val query: MatchQuery[_, _, _, _, _, TestNodeRecord] = TestNode().returns { case go ~~ _ => go }
+
+ When("REST call is executed")
+ val result = query.execute
+
+ Then("The result should be delivered")
+ whenReady(result) { res =>
+ res should not be empty
+ }
+
+ }
+
+ scenario("send a query and use a custom parser to get the result", RequiresNeo4jServer) {
+ Given("started Neo4j server")
+ val service = RestConnection("localhost", 7474)
+ val query = "CREATE (n) RETURN id(n)"
+ implicit val parsRester: Reads[InsertResult] = __.read[Int].map { arr =>
+ InsertResult(arr)
+ }
+
+ When("REST call is executed")
+ val result = service.makeRequest[InsertResult](query).execute
+
+ Then("The result should be delivered")
+ whenReady(result) { res =>
+ res should not be empty
+ res.head.id shouldBe >(0)
+ }
+ }
+
+ scenario("create a Person node and load it", RequiresNeo4jServer) {
+ Given("started Neo4j server")
+ val service = RestConnection("localhost", 7474)
+ val query = "CREATE (p: Person { name: 'Mike', age: 10 }) RETURN p"
+ implicit val parser: Reads[Person] = ((__ \ "name").read[String] and (__ \ "age").read[Int])(Person)
+
+ When("REST call is executed")
+ val result = service.makeRequest[Person](query).execute
+
+ Then("The result should be delivered")
+ whenReady(result) { res =>
+ res should not be empty
+ res.head.name shouldBe "Mike"
+ }
+ }
+
+ }
+
+}
diff --git a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestClientTest.scala b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestClientTest.scala
index 2dc2298..4b88fe5 100644
--- a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestClientTest.scala
+++ b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestClientTest.scala
@@ -14,75 +14,27 @@
*/
package com.websudos.reactiveneo.client
-import java.net.InetSocketAddress
import java.nio.charset.Charset
import java.util.concurrent.TimeUnit
-import com.websudos.util.testing._
-import com.twitter.finagle.Service
-import com.twitter.finagle.builder.{Server, ServerBuilder}
-import com.twitter.finagle.http.Http
-import com.twitter.io.Charsets.Utf8
-import com.twitter.util.Future
+import com.websudos.reactiveneo.RequiresNeo4jServer
import com.websudos.reactiveneo.client.RestClient._
-import org.jboss.netty.buffer.ChannelBuffers.copiedBuffer
-import org.jboss.netty.handler.codec.http.HttpResponseStatus._
-import org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1
-import org.jboss.netty.handler.codec.http._
import org.scalatest._
-import org.scalatest.concurrent.PatienceConfiguration
-import org.scalatest.time.SpanSugar._
+import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}
import scala.concurrent.duration.FiniteDuration
-import scala.util.Try
-import scala.concurrent.ExecutionContext.Implicits.global
-class RestClientTest extends FlatSpec with Matchers with BeforeAndAfter {
+class RestClientTest extends FlatSpec with Matchers with ScalaFutures with IntegrationPatience {
- implicit val s: PatienceConfiguration.Timeout = timeout(10 seconds)
-
- var server: Server = _
-
- def startServer: Server = {
- class Respond extends Service[HttpRequest, HttpResponse] {
- def apply(request: HttpRequest) = {
- val response = new DefaultHttpResponse(HTTP_1_1, OK)
- response.setContent(copiedBuffer("neo", Utf8))
- Future.value(response)
- }
- }
- ServerBuilder().codec(Http()).bindTo(new InetSocketAddress("localhost", 6666)).name("testserver").build(new Respond)
- }
-
- before {
- server = startServer
- }
-
- it should "execute a request" in {
- val client = new RestClient(ClientConfiguration("localhost", 6666, FiniteDuration(10, TimeUnit.SECONDS)))
- val result = client.makeRequest("/")
- result.successful { res =>
- res.getStatus.getCode should equal(200)
- res.getContent.toString(Charset.forName("UTF-8")) should equal("neo")
- }
+ it should "execute a request" taggedAs RequiresNeo4jServer in {
+ val client = new RestClient(ClientConfiguration("localhost", 7474, FiniteDuration(10, TimeUnit.SECONDS)))
+ val result = client.makeRequest("/")
+ whenReady(result) { res =>
+ res.getStatus.getCode should equal(200)
+ res.getContent.toString(Charset.forName("UTF-8")) should include("http://localhost/db/manage/")
+ res.getContent.toString(Charset.forName("UTF-8")) should not contain "error"
}
-
-
- it should "execute a request with a custom parser" in {
- val client = new RestClient(ClientConfiguration("localhost", 6666, FiniteDuration(10, TimeUnit.SECONDS)))
- implicit val parser = new ResultParser[String] {
- override def parseResult(response: HttpResponse): Try[String] = {
- Try(response.getContent.toString(Charset.forName("UTF-8")))
- }
- }
- val result: scala.concurrent.Future[String] = client.makeRequest("/")
- result successful { res =>
- res should equal("neo")
- }
}
- after {
- server.close()
- }
}
diff --git a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/dsl/TestNodeRecord.scala b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/dsl/TestNodeRecord.scala
index 09d8d7e..84f4bf4 100644
--- a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/dsl/TestNodeRecord.scala
+++ b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/dsl/TestNodeRecord.scala
@@ -25,7 +25,7 @@ class TestNode extends Node[TestNode, TestNodeRecord] {
object name extends StringAttribute(this)
override def fromQuery(data: QueryRecord): TestNodeRecord = {
- TestNodeRecord(name(data))
+ TestNodeRecord(name(data).getOrElse(""))
}
}
diff --git a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/dsl/TestRelationRecord.scala b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/dsl/TestRelationRecord.scala
index 2a411a5..970d207 100644
--- a/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/dsl/TestRelationRecord.scala
+++ b/reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/dsl/TestRelationRecord.scala
@@ -28,7 +28,7 @@ class TestRelationship extends Relationship[TestRelationship, TestRelationRecord
override def fromQuery(data: QueryRecord): TestRelationRecord = {
- TestRelationRecord(year(data))
+ TestRelationRecord(year(data).getOrElse(0))
}
}
diff --git a/reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/RequiresNeo4jServer.scala b/reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/RequiresNeo4jServer.scala
new file mode 100644
index 0000000..e590434
--- /dev/null
+++ b/reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/RequiresNeo4jServer.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2014 websudos ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.websudos.reactiveneo
+
+import org.scalatest.Tag
+
+/**
+ * Tag indicating a running neo4j server is required for a test.
+ */
+object RequiresNeo4jServer extends Tag("RequiresNeo4jServer")
diff --git a/reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/client/ServerMock.scala b/reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/client/ServerMock.scala
index 82e8347..f931604 100644
--- a/reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/client/ServerMock.scala
+++ b/reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/client/ServerMock.scala
@@ -16,7 +16,6 @@ package com.websudos.reactiveneo.client
import java.net.InetSocketAddress
-import com.websudos.util.testing._
import com.twitter.finagle.Service
import com.twitter.finagle.builder.{ServerBuilder, Server}
import com.twitter.finagle.http.Http
@@ -27,6 +26,7 @@ import org.jboss.netty.handler.codec.http.HttpResponseStatus._
import org.jboss.netty.handler.codec.http.HttpVersion._
import org.jboss.netty.handler.codec.http.{DefaultHttpResponse, HttpRequest, HttpResponse}
import org.scalatest.concurrent.PatienceConfiguration
+import org.scalatest.concurrent.PatienceConfiguration.Timeout
import org.scalatest.time.SpanSugar._
/**
@@ -34,7 +34,7 @@ import org.scalatest.time.SpanSugar._
*/
class ServerMock(handler: (HttpRequest) => HttpResponse) {
- implicit val s: PatienceConfiguration.Timeout = timeout(10 seconds)
+ implicit val s: PatienceConfiguration.Timeout = Timeout(10 seconds)
private val address = new InetSocketAddress("localhost", 0)
@@ -60,7 +60,7 @@ class ServerMock(handler: (HttpRequest) => HttpResponse) {
trait ServerMockSugar {
- implicit def fromString(contents: String): DefaultHttpResponse = {
+ implicit def stringResponse(contents: String): DefaultHttpResponse = {
val response = new DefaultHttpResponse(HTTP_1_1, OK)
response.setContent(copiedBuffer(contents, Utf8))
response
diff --git a/reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/client/TestNeo4jServer.scala b/reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/client/TestNeo4jServer.scala
new file mode 100644
index 0000000..5d8d539
--- /dev/null
+++ b/reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/client/TestNeo4jServer.scala
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2014 websudos ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.websudos.reactiveneo.client
+
+import java.nio.charset.Charset
+
+import com.twitter.io.Charsets._
+import org.jboss.netty.buffer.ChannelBuffers._
+import org.jboss.netty.handler.codec.http.DefaultHttpResponse
+import org.jboss.netty.handler.codec.http.HttpResponseStatus._
+import org.jboss.netty.handler.codec.http.HttpVersion._
+import org.neo4j.cypher.ExecutionEngine
+import org.neo4j.graphdb.GraphDatabaseService
+import org.neo4j.test.TestGraphDatabaseFactory
+import org.scalatest.Suite
+
+import scala.util.Try
+
+/**
+ * A base for tests that require embedded Neo4j. It leverages [[com.websudos.reactiveneo.client.ServerMock]]
+ * to handle http requests.
+ */
+trait TestNeo4jServer extends Suite {
+
+ var db: GraphDatabaseService = _
+
+ var server: ServerMock = _
+
+ lazy val port: Int = server.port
+
+ override protected def withFixture(test: NoArgTest) = {
+ db = new TestGraphDatabaseFactory().newImpermanentDatabase()
+ val engine = new ExecutionEngine(db)
+
+ server = new ServerMock(req => {
+ val query = req.getContent.toString(Charset.defaultCharset())
+ val tx = db.beginTx()
+ val result = engine.execute(query)
+ tx.success()
+ val response = new DefaultHttpResponse(HTTP_1_1, OK)
+ response.setContent(copiedBuffer(result.dumpToString(), Utf8))
+ response
+ })
+ try super.withFixture(test) finally db.shutdown()
+ }
+
+}
diff --git a/reactiveneo-testing/src/test/scala/com/websudos/reactiveneo/client/ServerMockTest.scala b/reactiveneo-testing/src/test/scala/com/websudos/reactiveneo/client/ServerMockTest.scala
index fa7935f..1d8a1ca 100644
--- a/reactiveneo-testing/src/test/scala/com/websudos/reactiveneo/client/ServerMockTest.scala
+++ b/reactiveneo-testing/src/test/scala/com/websudos/reactiveneo/client/ServerMockTest.scala
@@ -14,18 +14,17 @@
*/
package com.websudos.reactiveneo.client
-import org.scalatest.{Matchers, FlatSpec}
+import org.scalatest.{ Matchers, FlatSpec }
class ServerMockTest extends FlatSpec with Matchers with ServerMockSugar {
it should "return a listening port" in {
- val server = new ServerMock( _ => "hello")
+ val server = new ServerMock(_ => "hello")
server.port should be > 0
}
-
it should "return a host name" in {
- val server = new ServerMock( _ => "hello")
+ val server = new ServerMock(_ => "hello")
server.host shouldEqual "localhost"
}
}
diff --git a/reactiveneo-testing/src/test/scala/com/websudos/reactiveneo/client/TestNeo4jServerTest.scala b/reactiveneo-testing/src/test/scala/com/websudos/reactiveneo/client/TestNeo4jServerTest.scala
new file mode 100644
index 0000000..9ceb8e2
--- /dev/null
+++ b/reactiveneo-testing/src/test/scala/com/websudos/reactiveneo/client/TestNeo4jServerTest.scala
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2014 websudos ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.websudos.reactiveneo.client
+
+import java.nio.charset.Charset
+
+import com.twitter.finagle.Http
+import com.twitter.util
+import com.typesafe.scalalogging.slf4j.LazyLogging
+import org.jboss.netty.buffer.ChannelBuffers
+import org.jboss.netty.handler.codec.http.{HttpMethod, HttpResponse, HttpVersion, DefaultHttpRequest}
+import org.scalatest.FlatSpec
+
+class TestNeo4jServerTest extends FlatSpec with TestNeo4jServer with LazyLogging {
+
+ //TODO: embedded Neo4j uses different Scala version in cypher library failing this test
+ ignore should "pass a query to embedded server" in {
+ val path = s"http://localhost:$port/db/data/transaction/commit"
+ val query = """{
+ | "statements" : [ {
+ | "statement" : "CREATE (n) RETURN id(n)"
+ | } ]
+ |}""".stripMargin
+ val request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, path)
+ request.setContent(ChannelBuffers.copiedBuffer(query, Charset.forName("UTF-8")))
+
+ val client = Http.newService(s"localhost:$port")
+ val response: util.Future[HttpResponse] = client(request)
+ response onSuccess { resp: HttpResponse =>
+ logger.debug("GET success: " + resp)
+ }
+
+ }
+
+}