Permalink
Browse files

API-first REST; with MongoDB configuration and Casbah typeclasses for…

… persistence
  • Loading branch information...
1 parent 38f5614 commit cfc81695d4f70a53eec9c9f8573c06c291d3892a @janm janm committed Jul 29, 2012
Showing with 655 additions and 330 deletions.
  1. +6 −0 maven/api/pom.xml
  2. +10 −0 maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-UUID-get.json
  3. +10 −0 maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-get.json
  4. +9 −0 maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-post.json
  5. +0 −24 maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/addressbook.scala
  6. +5 −2 maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/boot.scala
  7. +33 −0 maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala
  8. +47 −0 maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/dummy.scala
  9. +1 −2 maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/home.scala
  10. +24 −5 maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala
  11. +30 −0 maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/CustomerServiceSpec.scala
  12. +1 −3 maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/HomeServiceSpec.scala
  13. +0 −26 maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/RootSprayTest.scala
  14. +69 −0 maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/support.scala
  15. +20 −4 maven/core/pom.xml
  16. +0 −20 maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/addressbook.scala
  17. +2 −44 maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/application.scala
  18. +0 −15 maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/bomb.scala
  19. +109 −0 maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/casbah.scala
  20. +30 −0 maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/customer.scala
  21. +0 −5 maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/exceptions.scala
  22. +0 −12 maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/messagesender.scala
  23. +0 −23 maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/notification.scala
  24. +0 −34 maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/configuration.scala
  25. +43 −0 maven/core/src/test/scala/org/cakesolutions/akkapatterns/core/CustomerActorSpec.scala
  26. +0 −27 maven/core/src/test/scala/org/cakesolutions/akkapatterns/core/application/AddressBookActorSpec.scala
  27. +0 −29 maven/core/src/test/scala/org/cakesolutions/akkapatterns/core/application/BombActorSpec.scala
  28. +0 −21 ...n/core/src/test/scala/org/cakesolutions/akkapatterns/core/application/NotificationActorSpec.scala
  29. +0 −21 maven/core/src/test/scala/org/cakesolutions/akkapatterns/core/application/SpecConfiguration.scala
  30. +0 −3 maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/Address.scala
  31. +43 −0 maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/configuration.scala
  32. +9 −0 maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/customer.scala
  33. +3 −1 maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/domain.scala
  34. +9 −0 maven/main/pom.xml
  35. +5 −9 maven/main/src/main/scala/org/cakesolutions/akkapatterns/main/Main.scala
  36. +22 −0 maven/parent/pom.xml
  37. +2 −0 maven/pom.xml
  38. +59 −0 maven/test/pom.xml
  39. +14 −0 maven/test/src/main/resources/org/cakesolutions/akkapatterns/mongodb-base.js
  40. +20 −0 maven/test/src/main/scala/org/cakesolutions/akkapatterns/test/DefaultTestData.scala
  41. +20 −0 maven/test/src/main/scala/org/cakesolutions/akkapatterns/test/SpecConfiguration.scala
View
@@ -22,6 +22,12 @@
<artifactId>domain</artifactId>
<version>0.1.RELEASE-SNAPSHOT</version>
</dependency>
+ <dependency>
+ <groupId>org.cakesolutions.akkapatterns</groupId>
+ <artifactId>test</artifactId>
+ <version>0.1.RELEASE-SNAPSHOT</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>cc.spray</groupId>
@@ -0,0 +1,10 @@
+{
+ "firstName":"Jan",
+ "lastName":"Machacek",
+ "email":"janm@cakesolutions.net",
+ "id":"00000000-0000-0000-0000-000000000000",
+ "addresses":[
+ {"line1":"Magdalen Centre", "line2":"Robert Robinson Avenue", "line3":"Oxford"},
+ {"line1":"Houldsworth Mill", "line2":"Houldsworth Street", "line3":"Reddish"}
+ ]
+}
@@ -0,0 +1,10 @@
+[{
+ "firstName":"Jan",
+ "lastName":"Machacek",
+ "email":"janm@cakesolutions.net",
+ "id":"00000000-0000-0000-0000-000000000000",
+ "addresses":[
+ {"line1":"Magdalen Centre", "line2":"Robert Robinson Avenue", "line3":"Oxford"},
+ {"line1":"Houldsworth Mill", "line2":"Houldsworth Street", "line3":"Reddish"}
+ ]
+}]
@@ -0,0 +1,9 @@
+{
+ "firstName":"Joe",
+ "lastName":"Bloggs",
+ "email":"joe@cakesolutions.net",
+ "id":"00000000-0000-0000-0100-000000000000",
+ "addresses":[
+ {"line1":"123 Winding Road", "line2":"Cowley", "line3":"Oxford"}
+ ]
+}
@@ -1,24 +0,0 @@
-package org.cakesolutions.akkapatterns.api
-
-import akka.actor.ActorSystem
-import cc.spray.Directives
-import org.cakesolutions.akkapatterns.core.application.GetAddresses
-import akka.pattern.ask
-import org.cakesolutions.akkapatterns.domain.Address
-
-/**
- * @author janmachacek
- */
-class AddressBookService(implicit val actorSystem: ActorSystem) extends Directives with Marshallers with DefaultTimeout {
- def addressBook = actorSystem.actorFor("/user/application/addressBook")
-
- val route =
- path("addressbook") {
- get {
- completeWith(
- (addressBook ? GetAddresses("Jan")).mapTo[List[Address]]
- )
- }
- }
-
-}
@@ -1,15 +1,18 @@
package org.cakesolutions.akkapatterns.api
import akka.actor.{ActorRef, Props}
-import cc.spray.{RootService, Route, HttpService, SprayCanRootService}
+import cc.spray.{RootService, Route, HttpService}
import org.cakesolutions.akkapatterns.core.Core
import akka.util.Timeout
trait Api {
this: Core =>
val routes =
- new HomeService().route :: Nil
+ new HomeService().route ::
+ //new DummyService("customers").route ::
+ new CustomerService().route ::
+ Nil
val svc: Route => ActorRef = route => actorSystem.actorOf(Props(new HttpService(route)))
@@ -0,0 +1,33 @@
+package org.cakesolutions.akkapatterns.api
+
+import akka.actor.ActorSystem
+import cc.spray.Directives
+import org.cakesolutions.akkapatterns.domain.Customer
+import org.cakesolutions.akkapatterns.core.application.{Insert, FindAll, Get}
+import cc.spray.directives.JavaUUID
+import akka.pattern.ask
+
+/**
+ * @author janmachacek
+ */
+class CustomerService(implicit val actorSystem: ActorSystem) extends Directives with Marshallers with Unmarshallers with DefaultTimeout with LiftJSON {
+ def customerActor = actorSystem.actorFor("/user/application/customer")
+
+ val route =
+ path("customers" / JavaUUID) { id =>
+ get {
+ completeWith((customerActor ? Get(id)).mapTo[Option[Customer]])
+ }
+ } ~
+ path("customers") {
+ get {
+ completeWith((customerActor ? FindAll()).mapTo[List[Customer]])
+ } ~
+ post {
+ content(as[Customer]) { customer =>
+ completeWith((customerActor ? Insert(customer)).mapTo[Customer])
+ }
+ }
+ }
+
+}
@@ -0,0 +1,47 @@
+package org.cakesolutions.akkapatterns.api
+
+import cc.spray.Directives
+import akka.actor.ActorSystem
+import cc.spray.http._
+import cc.spray.http.MediaTypes._
+import cc.spray.RequestContext
+
+class DummyService(path: String)(implicit val actorSystem: ActorSystem) extends Directives {
+
+ val route = {
+ pathPrefix(path) {
+ x =>
+ x.complete(
+ HttpResponse(StatusCodes.OK, getContent(x, `application/json`)))
+ }
+ }
+
+ private def getContent(ctx: RequestContext, contentType: ContentType): HttpContent = {
+
+ var filename = ctx.request.path
+
+ if (filename.startsWith("/")) filename = filename.drop(1)
+ if (filename.endsWith("/")) filename = filename.dropRight(1)
+ filename = filename.replace("/", "-")
+ filename = filename + "-" + ctx.request.method.toString().toLowerCase + ".json"
+
+ val uidRegex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}".r
+ val fileContent = uidRegex.findFirstIn(filename) match {
+ case Some(uid) =>
+ getFileAsString(uidRegex.replaceAllIn(filename, "UUID"))
+ case None =>
+ getFileAsString(filename)
+ }
+
+ HttpContent(contentType, fileContent)
+ }
+
+ private def getFileAsString(filename: String): String = {
+ try {
+ scala.io.Source.fromInputStream(getClass.getResourceAsStream(filename)).mkString
+ }
+ catch {
+ case _ => "{body of file " + filename + " -- Missing File!}"
+ }
+ }
+}
@@ -5,12 +5,11 @@ import cc.spray.Directives
import cc.spray.directives.Slash
import java.net.InetAddress
import akka.pattern.ask
-import akka.util.Timeout
import org.cakesolutions.akkapatterns.core.application.{PoisonPill, GetImplementation, Implementation}
case class SystemInfo(implementation: Implementation, host: String)
-class HomeService(implicit val actorSystem: ActorSystem) extends Directives with Marshallers with DefaultTimeout {
+class HomeService(implicit val actorSystem: ActorSystem) extends Directives with Marshallers with DefaultTimeout with LiftJSON {
def applicationActor = actorSystem.actorFor("/user/application")
@@ -6,10 +6,10 @@ import cc.spray.http.{HttpContent, ContentType}
import cc.spray.http.MediaTypes._
import net.liftweb.json.Serialization._
import cc.spray.http.ContentTypeRange
+import java.util.UUID
trait Marshallers extends DefaultMarshallers {
- implicit def liftJsonFormats: Formats =
- DefaultFormats + FieldSerializer[AnyRef]()
+ this: LiftJSON =>
implicit def liftJsonMarshaller[A <: Product] = new SimpleMarshaller[A] {
val canMarshalTo = ContentType(`application/json`) :: Nil
@@ -21,9 +21,8 @@ trait Marshallers extends DefaultMarshallers {
}
-trait Unmarshallers extends DefaultMarshallers {
- implicit def liftJsonFormats: Formats =
- DefaultFormats + FieldSerializer[AnyRef]()
+trait Unmarshallers extends DefaultUnmarshallers {
+ this: LiftJSON =>
implicit def liftJsonUnmarshaller[A <: Product : Manifest] = new SimpleUnmarshaller[A] {
val canUnmarshalFrom = ContentTypeRange(`application/json`) :: Nil
@@ -33,4 +32,24 @@ trait Unmarshallers extends DefaultMarshallers {
}
}
+}
+
+trait LiftJSON {
+ implicit def liftJsonFormats: Formats =
+ DefaultFormats + new UUIDSerializer + FieldSerializer[AnyRef]()
+
+ class UUIDSerializer extends Serializer[UUID] {
+ private val UUIDClass = classOf[UUID]
+
+ def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), UUID] = {
+ case (TypeInfo(UUIDClass, _), json) => json match {
+ case JString(s) => UUID.fromString(s)
+ case x => throw new MappingException("Can't convert " + x + " to UUID")
+ }
+ }
+
+ def serialize(implicit format: Formats): PartialFunction[Any, JValue] = {
+ case x: UUID => JString(x.toString)
+ }
+ }
}
@@ -0,0 +1,30 @@
+package org.cakesolutions.akkapatterns.api
+
+import org.cakesolutions.akkapatterns.domain.Customer
+import cc.spray.http.HttpMethods._
+
+/**
+ * @author janmachacek
+ */
+class CustomerServiceSpec extends DefaultApiSpecification {
+ implicit val service = rootService
+
+ "Getting a known customer works" in {
+ val customer = perform[Customer](GET, "/customers/00000000-0000-0000-0000-000000000000")
+
+ customer must_== janMachacek
+ }
+
+ "Finding all customers works" in {
+ val customers = perform[List[Customer]](GET, "/customers")
+
+ customers must contain (janMachacek)
+ }
+
+ "Saving a customer gets the saved one" in {
+ val customer = perform[Customer](POST, "/customers", jsonContent("/org/cakesolutions/akkapatterns/api/customers-post.json"))
+
+ customer must_== joeBloggs
+ }
+
+}
@@ -1,14 +1,12 @@
package org.cakesolutions.akkapatterns.api
-import org.specs2.mutable.Specification
-import org.cakesolutions.akkapatterns.core.Core
import cc.spray.http.HttpMethods._
import cc.spray.http._
import org.specs2.runner.JUnitRunner
import org.junit.runner.RunWith
@RunWith(classOf[JUnitRunner])
-class HomeServiceSpec extends Specification with RootSprayTest with Core with Api with Unmarshallers {
+class HomeServiceSpec extends DefaultApiSpecification {
"root URL shows the System version" in {
testRoot(HttpRequest(GET, "/"))(rootService).response.content.as[SystemInfo] match {
@@ -1,26 +0,0 @@
-package org.cakesolutions.akkapatterns.api
-
-import cc.spray.test.SprayTest
-import akka.util.Duration
-import java.util.concurrent.TimeUnit
-import akka.actor.ActorRef
-import cc.spray.RequestContext
-import cc.spray.http._
-
-trait RootSprayTest extends SprayTest {
- protected def testRoot(request: HttpRequest, timeout: Duration = Duration(10000, TimeUnit.MILLISECONDS))
- (root: ActorRef): ServiceResultWrapper = {
- val routeResult = new RouteResult
- root !
- RequestContext(
- request = request,
- responder = routeResult.requestResponder,
- unmatchedPath = request.path
- )
-
- // since the route might detach we block until the route actually completes or times out
- routeResult.awaitResult(timeout)
- new ServiceResultWrapper(routeResult, timeout)
- }
-
-}
@@ -0,0 +1,69 @@
+package org.cakesolutions.akkapatterns.api
+
+import cc.spray.test.SprayTest
+import akka.util.Duration
+import java.util.concurrent.TimeUnit
+import akka.actor.ActorRef
+import cc.spray.RequestContext
+import cc.spray.http._
+import io.Source
+import org.specs2.mutable.Specification
+import org.cakesolutions.akkapatterns.test.{DefaultTestData, SpecConfiguration}
+import org.cakesolutions.akkapatterns.core.Core
+
+trait RootSprayTest extends SprayTest {
+ protected def testRoot(request: HttpRequest, timeout: Duration = Duration(10000, TimeUnit.MILLISECONDS))
+ (root: ActorRef): ServiceResultWrapper = {
+ val routeResult = new RouteResult
+ root !
+ RequestContext(
+ request = request,
+ responder = routeResult.requestResponder,
+ unmatchedPath = request.path
+ )
+
+ // since the route might detach we block until the route actually completes or times out
+ routeResult.awaitResult(timeout)
+ new ServiceResultWrapper(routeResult, timeout)
+ }
+
+}
+
+trait JsonSource {
+
+ def jsonFor(location: String) = Source.fromInputStream(classOf[JsonSource].getResourceAsStream(location)).mkString
+
+ def jsonContent(location: String) = Some(HttpContent(ContentType(MediaTypes.`application/json`), jsonFor(location)))
+
+}
+
+/**
+ * Convenience trait for API tests
+ */
+trait ApiSpecification extends Specification with SpecConfiguration with RootSprayTest with Core with Api with Unmarshallers with Marshallers with LiftJSON {
+
+ import cc.spray.typeconversion._
+
+ protected def respond(method: HttpMethod, url: String, content: Option[HttpContent] = None)
+ (implicit root: ActorRef) = {
+ val request = HttpRequest(method, url, content = content)
+ testRoot(request)(root).response
+ }
+
+ protected def perform[A](method: HttpMethod, url: String, content: Option[HttpContent] = None)
+ (implicit root: ActorRef, unmarshaller: Unmarshaller[A]): A = {
+ val request = HttpRequest(method, url, content = content)
+ val response = testRoot(request)(root).response.content
+ val obj = response.as[A] match {
+ case Left(e) => throw new Exception(e.toString)
+ case Right(r) => r
+ }
+ obj
+ }
+
+}
+
+/**
+ * Convenience trait for API tests; with default test data
+ */
+trait DefaultApiSpecification extends ApiSpecification with DefaultTestData with JsonSource
Oops, something went wrong.

0 comments on commit cfc8169

Please sign in to comment.