diff --git a/README.md b/README.md index c96d3c3..2b40fc8 100644 --- a/README.md +++ b/README.md @@ -26,37 +26,31 @@ case class Person(name: String, age: Int) Reactiveneo node definition ``` -import com.websudos.reactiveneo.dsl._ +import com.websudos.neo._ class PersonNode extends Node[PersonNode, Person] { - object name extends Attribute[String] with Index + object name extends StringNode with Index - object age extends Attribute[Int] + object name extends IntNode - def fromRecord(record: NodeRecord): Person = { - Person(record.value[name], record.value[age]) + def fromNode(node: Node[Person]): Person = { + Person(name, age) } } ``` +When no custom mapping required +``` +class PersonNode extends DefaultNode[Person] +``` ## Relationships -case class Studied - ``` -import com.websudos.reactiveneo.dsl._ - -class StudiedRelationship extends Relationship[StudiedRelationship, Studied] { +class MyRelationship extends Relationship { - object year extends Attribute[Int] - - def fromRecord(record: NodeRecord): Person = { - Studied(record.value[year]) - } - } ``` @@ -67,7 +61,14 @@ class StudiedRelationship extends Relationship[StudiedRelationship, Studied] { # Querying ``` -matches[PersonNode](_.name := "Martin"). - inRelation(StudiedRelationship.any, PersonNode.criteria(name eq "Robert")). - return( case(person1, rel, person2) => person1.name) +match(node[Person]).where { p => + p.name === "Samantha" +}.return(p) +``` + +Multi node query +``` +match(node[Person], node[Person]).where { case (p1, p2) => + p1.age === p2.age +}.return(p1, p2) ``` diff --git a/project/Build.scala b/project/Build.scala index f5a4388..f4426f2 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -26,7 +26,6 @@ object reactiveneo extends Build { val finagleVersion = "6.17.0" val playVersion = "2.3.3" val scalazVersion = "7.0.6" - val neo4jVersion = "2.1.4" val publishUrl = "http://maven.websudos.co.uk" @@ -145,15 +144,13 @@ object reactiveneo extends Build { ).settings( name := "reactiveneo-dsl", libraryDependencies ++= Seq( - "com.chuusai" % "shapeless_2.10.4" % "2.0.0", + "com.chuusai" % "shapeless_2.10.4" % "2.0.0", "org.scala-lang" % "scala-reflect" % "2.10.4", "com.twitter" %% "finagle-http" % finagleVersion, "com.twitter" %% "util-core" % finagleVersion, "joda-time" % "joda-time" % "2.3", "org.joda" % "joda-convert" % "1.6", - "org.neo4j" % "neo4j" % neo4jVersion, "com.typesafe.play" %% "play-json" % playVersion, - "org.neo4j" % "neo4j" % neo4jVersion, "net.liftweb" %% "lift-json" % "2.6-M4" % "test, provided" ) ).dependsOn( diff --git a/reactiveneo-dsl/src/main/scala/com/websudos/neo/client/JsonParser.scala b/reactiveneo-dsl/src/main/scala/com/websudos/neo/client/JsonParser.scala new file mode 100644 index 0000000..4ec6b9d --- /dev/null +++ b/reactiveneo-dsl/src/main/scala/com/websudos/neo/client/JsonParser.scala @@ -0,0 +1,72 @@ +/* + * 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.neo.client + +import java.nio.charset.Charset + +import org.jboss.netty.handler.codec.http.{HttpResponse, HttpResponseStatus} +import play.api.data.validation.ValidationError +import play.api.libs.json._ + +import scala.collection.immutable.Seq + +/** + * Parser abstraction to used to parse JSON format of HttpResult content. To use this base class implementation of + * a `reads` method needs to be provided. + */ +abstract class JsonParser[R] extends ResultParser[R] { + + /** + * Implementation of of converter from JsValue to target type. + * @return Returns converted value. + */ + def reads: Reads[R] + + private def parseJson(s: String): R = { + val json = Json.parse(s) + reads.reads(json) match { + case JsSuccess(value, _) => value + case e: JsError => throw new JsonValidationException(buildErrorMessage(e)) + } + } + + + private[this] def singleErrorMessage(error: (JsPath, scala.Seq[ValidationError])) = { + val (path: JsPath, errors: Seq[ValidationError]) = error + val message = errors.foldLeft(errors.head.message)((acc,err) => s"$acc,${err.message}") + s"Errors at $path: $message" + } + + private[this] def buildErrorMessage(error: JsError) = { + error.errors.tail.foldLeft(singleErrorMessage(error.errors.head))((acc,err) => s"acc,${singleErrorMessage(err)}") + } + + override def parseResult(response: HttpResponse): R = { + if(response.getStatus.getCode == HttpResponseStatus.OK.getCode) { + parseJson(response.getContent.toString(Charset.forName("UTF-8"))) + } else { + throw new InvalidResponseException(s"Response status <${response.getStatus}> is not valid") + } + } + +} + +/** + * Exception indicating a problem when decoding resulting object value from JSON tree. + * @param msg Error message. + */ +class JsonValidationException(msg: String) extends Exception + +class InvalidResponseException(msg: String) extends Exception \ No newline at end of file diff --git a/reactiveneo-dsl/src/main/scala/com/websudos/neo/client/RestClient.scala b/reactiveneo-dsl/src/main/scala/com/websudos/neo/client/RestClient.scala new file mode 100644 index 0000000..28a1a37 --- /dev/null +++ b/reactiveneo-dsl/src/main/scala/com/websudos/neo/client/RestClient.scala @@ -0,0 +1,86 @@ +/* + * 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.neo.client + +import com.twitter.finagle.{Http, Service} +import com.twitter.util.TimeConversions._ +import com.twitter.util.{Future, JavaTimer, Timer} +import com.typesafe.scalalogging.slf4j.StrictLogging +import org.jboss.netty.handler.codec.http._ + +import scala.concurrent.duration.FiniteDuration + +object RestClient { + + implicit lazy val dummyParser = new DummyParser +} + +/** + * REST client implementation based on Finagle RPC. + */ +class RestClient(config: ClientConfiguration) extends StrictLogging { + + + lazy val client: Service[HttpRequest, HttpResponse] = + Http.newService(s"${config.server}:${config.port}") + + implicit lazy val timer: Timer = new JavaTimer() + + /** + * Execute the request with given parser. + * @param path Path to execute the request against. + * @param timeout Timeout to apply + * @param parser Parser used to parse the result. + * @tparam R Type of result + * @return Returns future of parsed result object. + */ + def makeRequest[R](path: String, timeout: FiniteDuration = config.defaultTimeout) + (implicit parser: ResultParser[R]): Future[R] = { + val request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path) + + val response: Future[HttpResponse] = client(request) + response onSuccess { resp: HttpResponse => + logger.debug("GET success: " + resp) + } + response.raiseWithin(timeout.toMillis.milliseconds).map(parser.parseResult) + } + +} + +/** + * Result parser is used to parse REST response object to a meaningful business object. + * @tparam R type of resulting object. + */ +trait ResultParser[+R] { + + /** + * Parse the HttpResponse object to a business object. In case of response status being invalid or response data + * corrupted Left with corresponding message should be returned. Otherwise the funciton should return Right + * + * @param response HttpResponse object. + * @return Result of parsing. + */ + def parseResult(response: HttpResponse): R +} + +/** + * Dummy parser used when no parsing is required. + */ +class DummyParser extends ResultParser[HttpResponse] { + override def parseResult(response: HttpResponse): HttpResponse = response +} + + +case class ClientConfiguration(server: String, port: Int, defaultTimeout: FiniteDuration) diff --git a/reactiveneo-dsl/src/test/scala/com/websudos/neo/client/RestClientTest.scala b/reactiveneo-dsl/src/test/scala/com/websudos/neo/client/RestClientTest.scala new file mode 100644 index 0000000..eb18e56 --- /dev/null +++ b/reactiveneo-dsl/src/test/scala/com/websudos/neo/client/RestClientTest.scala @@ -0,0 +1,88 @@ +/* + * 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.neo.client + +import java.net.InetSocketAddress +import java.nio.charset.Charset +import java.util.concurrent.TimeUnit + +import com.newzly.util.testing.AsyncAssertionsHelper._ +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.neo.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 scala.concurrent.duration.FiniteDuration + +class RestClientTest extends FlatSpec with Matchers with BeforeAndAfter { + + 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 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): String = { + response.getContent.toString(Charset.forName("UTF-8")) + } + } + val result: Future[String] = client.makeRequest("/") + result successful { res => + res should equal("neo") + } + } + + after { + server.close() + } + +}