Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
267 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
72 changes: 72 additions & 0 deletions
72
reactiveneo-dsl/src/main/scala/com/websudos/neo/client/JsonParser.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
86 changes: 86 additions & 0 deletions
86
reactiveneo-dsl/src/main/scala/com/websudos/neo/client/RestClient.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
88 changes: 88 additions & 0 deletions
88
reactiveneo-dsl/src/test/scala/com/websudos/neo/client/RestClientTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
|
||
} |