Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Composing functions in actors

  • Loading branch information...
commit e6a2d90d0fc9f999a63e6fda4a387f8b6e93c29e 1 parent adc3fea
Jan Machacek authored September 14, 2012
10  maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala
@@ -3,9 +3,13 @@ package org.cakesolutions.akkapatterns.api
3 3
 import akka.actor.ActorSystem
4 4
 import cc.spray.Directives
5 5
 import org.cakesolutions.akkapatterns.domain.Customer
6  
-import org.cakesolutions.akkapatterns.core.application.{Insert, FindAll, Get}
  6
+import org.cakesolutions.akkapatterns.core.application._
7 7
 import cc.spray.directives.JavaUUID
8 8
 import akka.pattern.ask
  9
+import org.cakesolutions.akkapatterns.core.application.RegisterCustomer
  10
+import org.cakesolutions.akkapatterns.domain.Customer
  11
+import org.cakesolutions.akkapatterns.core.application.Get
  12
+import org.cakesolutions.akkapatterns.core.application.FindAll
9 13
 
10 14
 /**
11 15
  * @author janmachacek
@@ -24,8 +28,8 @@ class CustomerService(implicit val actorSystem: ActorSystem) extends Directives
24 28
         completeWith((customerActor ? FindAll()).mapTo[List[Customer]])
25 29
       } ~
26 30
       post {
27  
-        content(as[Customer]) { customer =>
28  
-          completeWith((customerActor ? Insert(customer)).mapTo[Customer])
  31
+        content(as[RegisterCustomer]) { rc =>
  32
+          completeWith((customerActor ? rc).mapTo[Either[NotRegisteredCustomer, RegisteredCustomer]])
29 33
         }
30 34
       }
31 35
     }
13  maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala
@@ -52,4 +52,17 @@ trait LiftJSON {
52 52
       case x: UUID => JString(x.toString)
53 53
     }
54 54
   }
  55
+
  56
+  class StringBuilderMarshallingContent(sb: StringBuilder) extends MarshallingContext {
  57
+
  58
+    def marshalTo(content: HttpContent) {
  59
+      if (sb.length > 0) sb.append(",")
  60
+      sb.append(new String(content.buffer))
  61
+    }
  62
+
  63
+    def handleError(error: Throwable) {}
  64
+
  65
+    def startChunkedMessage(contentType: ContentType) = throw new UnsupportedOperationException
  66
+  }
  67
+
55 68
 }
24  maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/user.scala
... ...
@@ -0,0 +1,24 @@
  1
+package org.cakesolutions.akkapatterns.api
  2
+
  3
+import akka.actor.ActorSystem
  4
+import cc.spray.Directives
  5
+import org.cakesolutions.akkapatterns.domain.User
  6
+import org.cakesolutions.akkapatterns.core.application.{NotRegisteredUser, RegisteredUser}
  7
+import akka.pattern.ask
  8
+
  9
+/**
  10
+ * @author janmachacek
  11
+ */
  12
+class UserService(implicit val actorSystem: ActorSystem) extends Directives with Marshallers with Unmarshallers with DefaultTimeout with LiftJSON {
  13
+  def userActor = actorSystem.actorFor("/user/application/user")
  14
+
  15
+  val route =
  16
+    path("user" / "register") {
  17
+      post {
  18
+        content(as[User]) { user =>
  19
+          completeWith((userActor ? RegisteredUser(user)).mapTo[Either[NotRegisteredUser, RegisteredUser]])
  20
+        }
  21
+      }
  22
+    }
  23
+
  24
+}
15  maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/CustomerServiceSpec.scala
... ...
@@ -1,7 +1,9 @@
1 1
 package org.cakesolutions.akkapatterns.api
2 2
 
3  
-import org.cakesolutions.akkapatterns.domain.Customer
  3
+import org.cakesolutions.akkapatterns.domain.{User, Customer}
4 4
 import cc.spray.http.HttpMethods._
  5
+import org.cakesolutions.akkapatterns.core.application.{RegisteredCustomer, RegisterCustomer}
  6
+import java.util.UUID
5 7
 
6 8
 /**
7 9
  * @author janmachacek
@@ -21,10 +23,15 @@ class CustomerServiceSpec extends DefaultApiSpecification {
21 23
     customers must contain (janMachacek)
22 24
   }
23 25
 
24  
-  "Saving a customer gets the saved one" in {
25  
-    val customer = perform[Customer](POST, "/customers", jsonContent("/org/cakesolutions/akkapatterns/api/customers-post.json"))
  26
+  "Registering a customer" in {
  27
+    val rc = RegisterCustomer(
  28
+      joeBloggs,
  29
+      User(UUID.randomUUID(), "janm", "Like I'll tell you!"))
  30
+    val registered = perform[RegisterCustomer, RegisteredCustomer](POST, "/customers", rc)
26 31
 
27  
-    customer must_== joeBloggs
  32
+    (registered.customer must_== joeBloggs) and
  33
+    (registered.user.username must_== "janm") and
  34
+    (registered.user.password must_!= "Like I'll tell you")
28 35
   }
29 36
 
30 37
 }
14  maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/support.scala
@@ -61,6 +61,20 @@ trait ApiSpecification extends Specification with SpecConfiguration with RootSpr
61 61
     obj
62 62
   }
63 63
 
  64
+  protected def perform[In, Out](method: HttpMethod, url: String, in: In)
  65
+                            (implicit root: ActorRef, marshaller: Marshaller[In], unmarshaller: Unmarshaller[Out]): Out = {
  66
+    marshaller(t => Some(t)) match {
  67
+      case MarshalWith(f) =>
  68
+        val sb = new StringBuilder()
  69
+        val ctx = new StringBuilderMarshallingContent(sb)
  70
+        f(ctx)(in)
  71
+
  72
+        perform[Out](method, url, Some(HttpContent(ContentType(MediaTypes.`application/json`), sb.toString())))
  73
+      case CantMarshal(_) =>
  74
+        throw new Exception("Cant marshal " + in)
  75
+    }
  76
+  }
  77
+
64 78
 }
65 79
 
66 80
 /**
5  maven/core/pom.xml
@@ -37,6 +37,11 @@
37 37
         </dependency>
38 38
 
39 39
         <dependency>
  40
+            <groupId>org.scalaz</groupId>
  41
+            <artifactId>scalaz-core_${scala.version}</artifactId>
  42
+        </dependency>
  43
+
  44
+        <dependency>
40 45
             <groupId>com.typesafe.akka</groupId>
41 46
             <artifactId>akka-testkit</artifactId>
42 47
             <scope>test</scope>
9  maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/application.scala
@@ -2,6 +2,8 @@ package org.cakesolutions.akkapatterns.core.application
2 2
 
3 3
 import org.cakesolutions.akkapatterns.core.{Started, Stop, Start}
4 4
 import akka.actor.{Props, Actor}
  5
+import org.cakesolutions.akkapatterns.domain.Configured
  6
+import com.mongodb.casbah.MongoDB
5 7
 
6 8
 case class GetImplementation()
7 9
 case class Implementation(title: String, version: String, build: String)
@@ -23,6 +25,7 @@ class ApplicationActor extends Actor {
23 25
 
24 26
     case Start() =>
25 27
       context.actorOf(Props[CustomerActor], "customer")
  28
+      context.actorOf(Props[UserActor], "user")
26 29
 
27 30
       sender ! Started()
28 31
 
@@ -37,3 +40,9 @@ class ApplicationActor extends Actor {
37 40
   }
38 41
 
39 42
 }
  43
+
  44
+trait MongoCollections extends Configured {
  45
+  def customers = configured[MongoDB].apply("customers")
  46
+  def users = configured[MongoDB].apply("users")
  47
+
  48
+}
18  maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/casbah.scala
@@ -2,7 +2,7 @@ package org.cakesolutions.akkapatterns.core.application
2 2
 
3 3
 import com.mongodb.casbah.Imports._
4 4
 import java.util.UUID
5  
-import org.cakesolutions.akkapatterns.domain.{Address, Customer}
  5
+import org.cakesolutions.akkapatterns.domain.{User, Address, Customer}
6 6
 
7 7
 /**
8 8
  * Contains type classes that deserialize records from Casbah into "our" types.
@@ -38,6 +38,10 @@ trait CasbahDeserializers {
38 38
         innerList[Address](o, "addresses"), o.as[UUID]("id"))
39 39
   }
40 40
 
  41
+  implicit object UserDeserializer extends CasbahDeserializer[User] {
  42
+    def apply(o: DBObject) = User(o.as[UUID]("id"), o.as[String]("username"), o.as[String]("password"))
  43
+  }
  44
+
41 45
 }
42 46
 
43 47
 /**
@@ -66,6 +70,18 @@ trait CasbahSerializers {
66 70
     }
67 71
   }
68 72
 
  73
+  implicit object UserSerializer extends CasbahSerializer[User] {
  74
+    def apply(user: User) = {
  75
+      val builder = MongoDBObject.newBuilder
  76
+
  77
+      builder += "username" -> user.username
  78
+      builder += "password" -> user.password
  79
+      builder += "id" -> user.id
  80
+
  81
+      builder.result()
  82
+    }
  83
+  }
  84
+
69 85
   implicit object CustomerSerializer extends CasbahSerializer[Customer] {
70 86
     def apply(customer: Customer) = {
71 87
       val builder = MongoDBObject.newBuilder
68  maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/customer.scala
@@ -2,29 +2,71 @@ package org.cakesolutions.akkapatterns.core.application
2 2
 
3 3
 import akka.actor.Actor
4 4
 import java.util.UUID
5  
-import org.cakesolutions.akkapatterns.domain.{Configured, Customer}
6  
-import com.mongodb.casbah.MongoDB
  5
+import org.cakesolutions.akkapatterns.domain.{User, Configured, Customer}
  6
+import com.mongodb.casbah.{MongoCollection, MongoDB}
  7
+import org.specs2.internal.scalaz.Identity
  8
+import org.cakesolutions.akkapatterns.domain
7 9
 
8  
-case class Get(id: UUID)
9  
-case class FindAll()
10  
-case class Insert(customer: Customer)
  10
+/**
  11
+ * Registers a customer and a user. After registering, we have a user account for the given customer.
  12
+ *
  13
+ * @param customer the customer
  14
+ * @param user the user
  15
+ */
  16
+case class RegisterCustomer(customer: Customer, user: User)
  17
+
  18
+/**
  19
+ * Reply to successful customer registration
  20
+ * @param customer the newly registered customer
  21
+ * @param user the newly registered user
  22
+ */
  23
+case class RegisteredCustomer(customer: Customer, user: User)
11 24
 
12 25
 /**
13  
- * @author janmachacek
  26
+ * Reply to unsuccessful customer registration
  27
+ * @param code the error code for the failure reason
14 28
  */
15  
-class CustomerActor extends Actor with Configured with TypedCasbah with SearchExpressions {
  29
+case class NotRegisteredCustomer(code: String) extends Failure
16 30
 
17  
-  def customers = configured[MongoDB].apply("customers")
  31
+/**
  32
+ * CRUD operations for the [[org.cakesolutions.akkapatterns.domain.Customer]]s
  33
+ */
  34
+trait CustomerOperations extends TypedCasbah with SearchExpressions {
  35
+  def customers: MongoCollection
  36
+
  37
+  def getCustomer(id: domain.Identity) = customers.findOne(entityId(id)).map(mapper[Customer])
  38
+
  39
+  def findAllCustomers() = customers.find().map(mapper[Customer]).toList
  40
+
  41
+  def insertCustomer(customer: Customer) = {
  42
+    customers += serialize(customer)
  43
+    customer
  44
+  }
  45
+
  46
+  def registerCustomer(customer: Customer)(ru: RegisteredUser): Either[Failure, RegisteredCustomer] = {
  47
+    customers += serialize(customer)
  48
+    Right(RegisteredCustomer(customer, ru.user))
  49
+  }
  50
+
  51
+}
  52
+
  53
+class CustomerActor extends Actor with Configured with CustomerOperations with UserOperations with MongoCollections {
18 54
 
19 55
   protected def receive = {
20 56
     case Get(id) =>
21  
-      sender ! customers.findOne(entityId(id)).map(mapper[Customer])
  57
+      sender ! getCustomer(id)
22 58
 
23 59
     case FindAll() =>
24  
-      sender ! customers.find().map(mapper[Customer]).toList
  60
+      sender ! findAllCustomers()
  61
+
  62
+    case Insert(customer: Customer) =>
  63
+      sender ! insertCustomer(customer)
  64
+
  65
+    case RegisterCustomer(customer, user) =>
  66
+      import scalaz._
  67
+      import Scalaz._
  68
+
  69
+      sender ! (registerUser(user) >>= registerCustomer(customer))
25 70
 
26  
-    case Insert(customer) =>
27  
-      customers += serialize(customer)
28  
-      sender ! customer
29 71
   }
30 72
 }
42  maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/messages.scala
... ...
@@ -0,0 +1,42 @@
  1
+package org.cakesolutions.akkapatterns.core.application
  2
+
  3
+import java.util.UUID
  4
+
  5
+/**
  6
+ * Base type for failures
  7
+ */
  8
+trait Failure {
  9
+  /**
  10
+   * The error code for the failure
  11
+   * @return the error code
  12
+   */
  13
+  def code: String
  14
+}
  15
+
  16
+/**
  17
+ * Gets an entity identified by ``id``
  18
+ *
  19
+ * @param id the identity
  20
+ */
  21
+case class Get(id: UUID)
  22
+
  23
+/**
  24
+ * Finds all entities
  25
+ */
  26
+case class FindAll()
  27
+
  28
+/**
  29
+ * Inserts the given entity
  30
+ *
  31
+ * @param entity the entity to be inserted
  32
+ * @tparam A the type of A
  33
+ */
  34
+case class Insert[A](entity: A)
  35
+
  36
+/**
  37
+ * Updates the given entity
  38
+ *
  39
+ * @param entity the entity to update
  40
+ * @tparam A the type of A
  41
+ */
  42
+case class Update[A](entity: A)
72  maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/user.scala
... ...
@@ -0,0 +1,72 @@
  1
+package org.cakesolutions.akkapatterns.core.application
  2
+
  3
+import akka.actor.Actor
  4
+import com.mongodb.casbah.MongoCollection
  5
+import org.cakesolutions.akkapatterns.domain
  6
+import domain.User
  7
+import com.mongodb.casbah.commons.MongoDBObject
  8
+import java.security.MessageDigest
  9
+
  10
+/**
  11
+ * Finds a user by the given username
  12
+ *
  13
+ * @param username the username
  14
+ */
  15
+case class GetUserByUsername(username: String)
  16
+
  17
+/**
  18
+ * Registers a user. Checks the password complexity and that the username is not duplicate
  19
+ *
  20
+ * @param user the user to be registered
  21
+ */
  22
+case class Register(user: User)
  23
+
  24
+/**
  25
+ * Successfully registered a user
  26
+ *
  27
+ * @param user the user that's just been registered
  28
+ */
  29
+case class RegisteredUser(user: User)
  30
+
  31
+/**
  32
+ * Unsuccessful registration with the error code
  33
+ * @param code the error code
  34
+ */
  35
+case class NotRegisteredUser(code: String) extends Failure
  36
+
  37
+
  38
+trait UserOperations extends TypedCasbah with SearchExpressions {
  39
+  def users: MongoCollection
  40
+  def sha1 = MessageDigest.getInstance("SHA1")
  41
+
  42
+  def getUser(id: domain.Identity) = users.findOne(entityId(id)).map(mapper[User])
  43
+
  44
+  def getUserByUsername(username: String) = users.findOne(MongoDBObject("username" -> username)).map(mapper[User])
  45
+
  46
+  def registerUser(user: User): Either[Failure, RegisteredUser] = {
  47
+    getUserByUsername(user.username) match {
  48
+      case None =>
  49
+        val hashedPassword = java.util.Arrays.toString(sha1.digest(user.password.getBytes))
  50
+        val userToRegister = user.copy(password = hashedPassword)
  51
+        users += serialize(userToRegister)
  52
+        Right(RegisteredUser(userToRegister))
  53
+      case Some(_existingUser) =>
  54
+        Left(NotRegisteredUser("User.duplicateUsername"))
  55
+    }
  56
+  }
  57
+
  58
+}
  59
+
  60
+class UserActor extends Actor with UserOperations with MongoCollections {
  61
+
  62
+  protected def receive = {
  63
+    case Get(id) =>
  64
+      sender ! getUser(id)
  65
+
  66
+    case GetUserByUsername(username) =>
  67
+      sender ! getUserByUsername(username)
  68
+
  69
+    case RegisteredUser(user) =>
  70
+      sender ! registerUser(user)
  71
+  }
  72
+}
6  maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/user.scala
... ...
@@ -0,0 +1,6 @@
  1
+package org.cakesolutions.akkapatterns.domain
  2
+
  3
+/**
  4
+ * @author janmachacek
  5
+ */
  6
+case class User(id: Identity, username: String, password: String)

0 notes on commit e6a2d90

Please sign in to comment.
Something went wrong with that request. Please try again.