From 6a08cae3caca39f0edb0cec45d240911e2223ec2 Mon Sep 17 00:00:00 2001 From: Flavian Alexandru Date: Sun, 10 May 2015 17:21:10 +0100 Subject: [PATCH] Adding support for Scala 2.10 and 2.11 --- .travis.yml | 7 +- README.md | 33 +++++- .../{Build.scala => ReactiveNeoBuild.scala} | 64 +++++------ .../attribute/AbstractAttribute.scala | 21 +++- .../reactiveneo/client/RestCall.scala | 107 ++++++++++++++++++ .../reactiveneo/client/RestClient.scala | 3 + .../reactiveneo/client/ServerCall.scala | 45 +------- .../websudos/reactiveneo/dsl/MatchQuery.scala | 50 ++++---- .../reactiveneo/dsl/ReturnExpression.scala | 11 +- .../reactiveneo/query/QueryRecord.scala | 2 +- .../client/CypherResultParserTest.scala | 9 ++ ...erverCallTest.scala => RestCallTest.scala} | 12 +- .../reactiveneo/client/RestClientSpec.scala | 90 +++++++++++++++ .../reactiveneo/client/RestClientTest.scala | 68 ++--------- .../reactiveneo/dsl/TestNodeRecord.scala | 2 +- .../reactiveneo/dsl/TestRelationRecord.scala | 2 +- .../reactiveneo/RequiresNeo4jServer.scala | 22 ++++ .../reactiveneo/client/ServerMock.scala | 6 +- .../reactiveneo/client/TestNeo4jServer.scala | 59 ++++++++++ .../reactiveneo/client/ServerMockTest.scala | 7 +- .../client/TestNeo4jServerTest.scala | 47 ++++++++ 21 files changed, 474 insertions(+), 193 deletions(-) rename project/{Build.scala => ReactiveNeoBuild.scala} (79%) create mode 100644 reactiveneo-dsl/src/main/scala/com/websudos/reactiveneo/client/RestCall.scala rename reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/{ServerCallTest.scala => RestCallTest.scala} (80%) create mode 100644 reactiveneo-dsl/src/test/scala/com/websudos/reactiveneo/client/RestClientSpec.scala create mode 100644 reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/RequiresNeo4jServer.scala create mode 100644 reactiveneo-testing/src/main/scala/com/websudos/reactiveneo/client/TestNeo4jServer.scala create mode 100644 reactiveneo-testing/src/test/scala/com/websudos/reactiveneo/client/TestNeo4jServerTest.scala diff --git a/.travis.yml b/.travis.yml index ee03297..f254a15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,8 @@ language: scala scala: - - "2.10.4" + - "2.10.5" + - "2.11.6" # Emails to notify notifications: @@ -25,5 +26,7 @@ jdk: - oraclejdk7 - openjdk7 -script: "sbt test" +before_script: travis_retry sbt ++$TRAVIS_SCALA_VERSION update + +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/ReactiveNeoBuild.scala similarity index 79% rename from project/Build.scala rename to project/ReactiveNeoBuild.scala index 1314beb..a8993a2 100644 --- a/project/Build.scala +++ b/project/ReactiveNeoBuild.scala @@ -19,17 +19,19 @@ import sbt._ import scoverage.ScoverageSbtPlugin.instrumentSettings import org.scalastyle.sbt.ScalastylePlugin -object reactiveneo extends Build { +object ReactiveNeoBuild extends Build { - val UtilVersion = "0.4.0" - val ScalatestVersion = "2.2.0-M1" - val FinagleVersion = "6.20.0" + val UtilVersion = "0.8.0" + val ScalatestVersion = "2.2.4" + val ShapelessVersion = "2.2.0-RC4" + val FinagleVersion = "6.25.0" + val TwitterUtilVersion = "6.24.0" + val FinagleZookeeperVersion = "6.24.0" val playVersion = "2.3.4" val ScalazVersion = "7.1.0" + val Neo4jVersion = "2.1.7" - val publishUrl = "http://maven.websudos.co.uk" - - val mavenPublishSettings : Seq[Def.Setting[_]] = Seq( + val publishSettings : Seq[Def.Setting[_]] = Seq( credentials += Credentials(Path.userHome / ".ivy2" / ".credentials"), publishMavenStyle := true, publishTo <<= version.apply { @@ -43,7 +45,7 @@ object reactiveneo extends Build { publishArtifact in Test := false, pomIncludeRepository := { _ => true }, pomExtra := - https://github.com/websudosuk/reactiveneo + https://github.com/websudos/reactiveneo Apache License, Version 2.0 @@ -52,12 +54,12 @@ object reactiveneo extends Build { - git@github.com:websudosuk/reactiveneo.git - scm:git:git@github.com:websudosuk/reactiveneo.git + git@github.com:websudos/reactiveneo.git + scm:git:git@github.com:websudos/reactiveneo.git - benjumanji + bjankie1 Bartosz Jankiewicz http://github.com/bjankie1 @@ -69,24 +71,11 @@ object reactiveneo extends Build { ) - val publishSettings : Seq[Def.Setting[_]] = Seq( - credentials += Credentials(Path.userHome / ".ivy2" / ".credentials"), - publishTo <<= version { (v: String) => { - if (v.trim.endsWith("SNAPSHOT")) - Some("snapshots" at publishUrl + "/ext-snapshot-local") - else - Some("releases" at publishUrl + "/ext-release-local") - } - }, - publishMavenStyle := true, - publishArtifact in Test := false, - pomIncludeRepository := { _ => true } - ) - val sharedSettings: Seq[Def.Setting[_]] = Seq( organization := "com.websudos", - version := "0.1.2", - scalaVersion := "2.10.4", + version := "0.2.0", + scalaVersion := "2.10.5", + crossScalaVersions := Seq("2.10.5", "2.11.6"), resolvers ++= Seq( "Typesafe repository snapshots" at "http://repo.typesafe.com/typesafe/snapshots/", "Typesafe repository releases" at "http://repo.typesafe.com/typesafe/releases/", @@ -112,13 +101,12 @@ object reactiveneo extends Build { "-unchecked" ), libraryDependencies ++= Seq( - "com.chuusai" % "shapeless_2.10.4" % "2.0.0", + "com.chuusai" %% "shapeless" % ShapelessVersion, "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") @@ -145,10 +133,10 @@ object reactiveneo extends Build { ).settings( name := "reactiveneo-dsl", libraryDependencies ++= Seq( - "com.chuusai" % "shapeless_2.10.4" % "2.0.0", + "com.chuusai" %% "shapeless" % ShapelessVersion, "org.scala-lang" % "scala-reflect" % "2.10.4", "com.twitter" %% "finagle-http" % FinagleVersion, - "com.twitter" %% "util-core" % FinagleVersion, + "com.twitter" %% "util-core" % TwitterUtilVersion, "joda-time" % "joda-time" % "2.3", "org.joda" % "joda-convert" % "1.6", "com.typesafe.play" %% "play-json" % playVersion, @@ -166,7 +154,7 @@ object reactiveneo extends Build { name := "reactiveneo-zookeeper", libraryDependencies ++= Seq( "com.twitter" %% "finagle-serversets" % FinagleVersion, - "com.twitter" %% "finagle-zookeeper" % FinagleVersion + "com.twitter" %% "finagle-zookeeper" % FinagleZookeeperVersion ) ) @@ -177,13 +165,13 @@ object reactiveneo extends Build { ).settings( name := "reactiveneo-testing", libraryDependencies ++= Seq( - "com.twitter" %% "util-core" % FinagleVersion, + "com.twitter" %% "util-core" % TwitterUtilVersion, "com.websudos" %% "util-testing" % UtilVersion, - "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 + "org.scalatest" %% "scalatest" % ScalatestVersion, + "org.neo4j" % "neo4j-cypher" % Neo4jVersion % "compile", + "org.neo4j" % "neo4j-kernel" % Neo4jVersion % "compile", + "org.neo4j" % "neo4j-kernel" % Neo4jVersion % "compile" classifier "tests" ) ).dependsOn( reactiveneoZookeeper 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 80% 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..57baa23 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 @@ -15,12 +15,12 @@ package com.websudos.reactiveneo.client 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 { val testNode = new TestNode @@ -40,9 +40,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) + } + + } + +}