Permalink
Browse files

Merged

  • Loading branch information...
janm committed Oct 22, 2012
2 parents 8b63ed8 + e6a2d90 commit 1e34e4d6e8e70278c5372da694a5113591eb9a46
@@ -3,9 +3,13 @@ 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 org.cakesolutions.akkapatterns.core.application._
import cc.spray.directives.JavaUUID
import akka.pattern.ask
+import org.cakesolutions.akkapatterns.core.application.RegisterCustomer
+import org.cakesolutions.akkapatterns.domain.Customer
+import org.cakesolutions.akkapatterns.core.application.Get
+import org.cakesolutions.akkapatterns.core.application.FindAll
/**
* @author janmachacek
@@ -24,8 +28,8 @@ class CustomerService(implicit val actorSystem: ActorSystem) extends Directives
completeWith((customerActor ? FindAll()).mapTo[List[Customer]])
} ~
post {
- content(as[Customer]) { customer =>
- completeWith((customerActor ? Insert(customer)).mapTo[Customer])
+ content(as[RegisterCustomer]) { rc =>
+ completeWith((customerActor ? rc).mapTo[Either[NotRegisteredCustomer, RegisteredCustomer]])
}
}
}
@@ -52,4 +52,17 @@ trait LiftJSON {
case x: UUID => JString(x.toString)
}
}
+
+ class StringBuilderMarshallingContent(sb: StringBuilder) extends MarshallingContext {
+
+ def marshalTo(content: HttpContent) {
+ if (sb.length > 0) sb.append(",")
+ sb.append(new String(content.buffer))
+ }
+
+ def handleError(error: Throwable) {}
+
+ def startChunkedMessage(contentType: ContentType) = throw new UnsupportedOperationException
+ }
+
}
@@ -0,0 +1,24 @@
+package org.cakesolutions.akkapatterns.api
+
+import akka.actor.ActorSystem
+import cc.spray.Directives
+import org.cakesolutions.akkapatterns.domain.User
+import org.cakesolutions.akkapatterns.core.application.{NotRegisteredUser, RegisteredUser}
+import akka.pattern.ask
+
+/**
+ * @author janmachacek
+ */
+class UserService(implicit val actorSystem: ActorSystem) extends Directives with Marshallers with Unmarshallers with DefaultTimeout with LiftJSON {
+ def userActor = actorSystem.actorFor("/user/application/user")
+
+ val route =
+ path("user" / "register") {
+ post {
+ content(as[User]) { user =>
+ completeWith((userActor ? RegisteredUser(user)).mapTo[Either[NotRegisteredUser, RegisteredUser]])
+ }
+ }
+ }
+
+}
@@ -1,7 +1,9 @@
package org.cakesolutions.akkapatterns.api
-import org.cakesolutions.akkapatterns.domain.Customer
+import org.cakesolutions.akkapatterns.domain.{User, Customer}
import cc.spray.http.HttpMethods._
+import org.cakesolutions.akkapatterns.core.application.{RegisteredCustomer, RegisterCustomer}
+import java.util.UUID
/**
* @author janmachacek
@@ -21,10 +23,15 @@ class CustomerServiceSpec extends DefaultApiSpecification {
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"))
+ "Registering a customer" in {
+ val rc = RegisterCustomer(
+ joeBloggs,
+ User(UUID.randomUUID(), "janm", "Like I'll tell you!"))
+ val registered = perform[RegisterCustomer, RegisteredCustomer](POST, "/customers", rc)
- customer must_== joeBloggs
+ (registered.customer must_== joeBloggs) and
+ (registered.user.username must_== "janm") and
+ (registered.user.password must_!= "Like I'll tell you")
}
}
@@ -61,6 +61,20 @@ trait ApiSpecification extends Specification with SpecConfiguration with RootSpr
obj
}
+ protected def perform[In, Out](method: HttpMethod, url: String, in: In)
+ (implicit root: ActorRef, marshaller: Marshaller[In], unmarshaller: Unmarshaller[Out]): Out = {
+ marshaller(t => Some(t)) match {
+ case MarshalWith(f) =>
+ val sb = new StringBuilder()
+ val ctx = new StringBuilderMarshallingContent(sb)
+ f(ctx)(in)
+
+ perform[Out](method, url, Some(HttpContent(ContentType(MediaTypes.`application/json`), sb.toString())))
+ case CantMarshal(_) =>
+ throw new Exception("Cant marshal " + in)
+ }
+ }
+
}
/**
View
@@ -37,6 +37,11 @@
</dependency>
-->
+ <dependency>
+ <groupId>org.scalaz</groupId>
+ <artifactId>scalaz-core_${scala.version}</artifactId>
+ </dependency>
+
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-testkit_${scala.version}</artifactId>
@@ -2,6 +2,8 @@ package org.cakesolutions.akkapatterns.core.application
import org.cakesolutions.akkapatterns.core.{Started, Stop, Start}
import akka.actor.{Props, Actor}
+import org.cakesolutions.akkapatterns.domain.Configured
+import com.mongodb.casbah.MongoDB
case class GetImplementation()
case class Implementation(title: String, version: String, build: String)
@@ -23,6 +25,7 @@ class ApplicationActor extends Actor {
case Start() =>
context.actorOf(Props[CustomerActor], "customer")
+ context.actorOf(Props[UserActor], "user")
sender ! Started()
@@ -37,3 +40,9 @@ class ApplicationActor extends Actor {
}
}
+
+trait MongoCollections extends Configured {
+ def customers = configured[MongoDB].apply("customers")
+ def users = configured[MongoDB].apply("users")
+
+}
@@ -0,0 +1,125 @@
+package org.cakesolutions.akkapatterns.core.application
+
+import com.mongodb.casbah.Imports._
+import java.util.UUID
+import org.cakesolutions.akkapatterns.domain.{User, Address, Customer}
+
+/**
+ * Contains type classes that deserialize records from Casbah into "our" types.
+ */
+trait CasbahDeserializers {
+ type CasbahDeserializer[A] = DBObject => A
+
+ /**
+ * Convenience method that picks the ``CasbahDeserializer`` for the type ``A``
+ * @param deserializer implicitly given deserializer
+ * @tparam A the type A
+ * @return the deserializer for ``A``
+ */
+ def casbahDeserializer[A](implicit deserializer: CasbahDeserializer[A]) = deserializer
+
+ private def inner[A: CasbahDeserializer](o: DBObject, field: String): A = casbahDeserializer[A].apply(o.as[DBObject](field))
+
+ private def innerList[A: CasbahDeserializer](o: DBObject, field: String): Seq[A] = {
+ val deserializer = casbahDeserializer[A]
+ o.as[MongoDBList](field).map {
+ inner => deserializer(inner.asInstanceOf[DBObject])
+ }
+ }
+
+ implicit object AddressDeserializer extends CasbahDeserializer[Address] {
+ def apply(o: DBObject) =
+ Address(o.as[String]("line1"), o.as[String]("line2"), o.as[String]("line3"))
+ }
+
+ implicit object CustomerDeserializer extends CasbahDeserializer[Customer] {
+ def apply(o: DBObject) =
+ Customer(o.as[String]("firstName"), o.as[String]("lastName"), o.as[String]("email"),
+ innerList[Address](o, "addresses"), o.as[UUID]("id"))
+ }
+
+ implicit object UserDeserializer extends CasbahDeserializer[User] {
+ def apply(o: DBObject) = User(o.as[UUID]("id"), o.as[String]("username"), o.as[String]("password"))
+ }
+
+}
+
+/**
+ * Contains type classes that serialize "our" types into Casbah records.
+ */
+trait CasbahSerializers {
+ type CasbahSerializer[A] = A => DBObject
+
+ /**
+ * Convenience method that picks the ``CasbahSerializer`` for the type ``A``
+ * @param serializer implicitly given serializer
+ * @tparam A the type A
+ * @return the serializer for ``A``
+ */
+ def casbahSerializer[A](implicit serializer: CasbahSerializer[A]) = serializer
+
+ implicit object AddressSerializer extends CasbahSerializer[Address] {
+ def apply(address: Address) = {
+ val builder = MongoDBObject.newBuilder
+
+ builder += "line1" -> address.line1
+ builder += "line2" -> address.line2
+ builder += "line3" -> address.line2
+
+ builder.result()
+ }
+ }
+
+ implicit object UserSerializer extends CasbahSerializer[User] {
+ def apply(user: User) = {
+ val builder = MongoDBObject.newBuilder
+
+ builder += "username" -> user.username
+ builder += "password" -> user.password
+ builder += "id" -> user.id
+
+ builder.result()
+ }
+ }
+
+ implicit object CustomerSerializer extends CasbahSerializer[Customer] {
+ def apply(customer: Customer) = {
+ val builder = MongoDBObject.newBuilder
+
+ builder += "firstName" -> customer.firstName
+ builder += "lastName" -> customer.lastName
+ builder += "email" -> customer.email
+ builder += "addresses" -> customer.addresses.map(AddressSerializer(_))
+ builder += "id" -> customer.id
+
+ builder.result()
+ }
+ }
+
+}
+
+/**
+ * Contains convenience functions that can be used to find "entities-by-id"
+ */
+trait SearchExpressions {
+
+ def entityId(id: UUID) = MongoDBObject("id" -> id)
+
+// def entityId(id: UUID) = MongoDBObject("id" -> id, "active" -> true)
+
+}
+
+/**
+ * Mix this trait into your classes to gain the functionality of the serializers, deserializers and mappers.
+ */
+trait TypedCasbah extends CasbahDeserializers with CasbahSerializers {
+
+ final def serialize[A: CasbahSerializer](a: A) = casbahSerializer[A].apply(a)
+
+ final def deserialize[A: CasbahDeserializer](o: DBObject) = casbahDeserializer[A].apply(o)
+
+ final def mapper[A: CasbahDeserializer] = {
+ (o: DBObject) => deserialize[A](o)
+ }
+
+}
@@ -2,27 +2,71 @@ package org.cakesolutions.akkapatterns.core.application
import akka.actor.Actor
import java.util.UUID
-import org.cakesolutions.akkapatterns.domain.{Configured, Customer}
+import org.cakesolutions.akkapatterns.domain.{User, Configured, Customer}
+import com.mongodb.casbah.{MongoCollection, MongoDB}
+import org.specs2.internal.scalaz.Identity
+import org.cakesolutions.akkapatterns.domain
-case class Get(id: UUID)
-case class FindAll()
-case class Insert(customer: Customer)
+/**
+ * Registers a customer and a user. After registering, we have a user account for the given customer.
+ *
+ * @param customer the customer
+ * @param user the user
+ */
+case class RegisterCustomer(customer: Customer, user: User)
+
+/**
+ * Reply to successful customer registration
+ * @param customer the newly registered customer
+ * @param user the newly registered user
+ */
+case class RegisteredCustomer(customer: Customer, user: User)
/**
- * @author janmachacek
+ * Reply to unsuccessful customer registration
+ * @param code the error code for the failure reason
*/
-class CustomerActor extends Actor with Configured {
+case class NotRegisteredCustomer(code: String) extends Failure
+/**
+ * CRUD operations for the [[org.cakesolutions.akkapatterns.domain.Customer]]s
+ */
+trait CustomerOperations extends TypedCasbah with SearchExpressions {
+ def customers: MongoCollection
+
+ def getCustomer(id: domain.Identity) = customers.findOne(entityId(id)).map(mapper[Customer])
- def receive = {
+ def findAllCustomers() = customers.find().map(mapper[Customer]).toList
+
+ def insertCustomer(customer: Customer) = {
+ customers += serialize(customer)
+ customer
+ }
+
+ def registerCustomer(customer: Customer)(ru: RegisteredUser): Either[Failure, RegisteredCustomer] = {
+ customers += serialize(customer)
+ Right(RegisteredCustomer(customer, ru.user))
+ }
+
+}
+
+class CustomerActor extends Actor with Configured with CustomerOperations with UserOperations with MongoCollections {
+
+ protected def receive = {
case Get(id) =>
- sender ! None //customers.findOne(entityId(id)).map(mapper[Customer])
+ sender ! getCustomer(id)
case FindAll() =>
- sender ! List() //customers.find().map(mapper[Customer]).toList
+ sender ! findAllCustomers()
+
+ case Insert(customer: Customer) =>
+ sender ! insertCustomer(customer)
+
+ case RegisterCustomer(customer, user) =>
+ import scalaz._
+ import Scalaz._
+
+ sender ! (registerUser(user) >>= registerCustomer(customer))
- case Insert(customer) =>
- // customers += serialize(customer)
- sender ! customer
}
}
Oops, something went wrong.

0 comments on commit 1e34e4d

Please sign in to comment.