Skip to content

Commit

Permalink
Merged develop changes.
Browse files Browse the repository at this point in the history
  • Loading branch information
alexflav23 committed Sep 21, 2014
2 parents 8cf74e3 + b707402 commit 94c21c7
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 23 deletions.
39 changes: 20 additions & 19 deletions README.md
Expand Up @@ -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])
}
}
```
Expand All @@ -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)
```
5 changes: 1 addition & 4 deletions project/Build.scala
Expand Up @@ -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"

Expand Down Expand Up @@ -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(
Expand Down
@@ -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
@@ -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)
@@ -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()
}

}

0 comments on commit 94c21c7

Please sign in to comment.