Skip to content

Commit

Permalink
Add method post endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Dave Shiga committed May 4, 2015
1 parent 64f04c9 commit 47e0bb2
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class ServerInitializer(val config: Config) extends LazyLogging {

def startAllServices() {
startWebServiceActors()
createAgoraDao()
}

def stopAllServices() {
Expand Down Expand Up @@ -64,8 +63,4 @@ class ServerInitializer(val config: Config) extends LazyLogging {
case ex: Throwable => logger.error("Exception ignored while shutting down.", ex)
}
}

private def createAgoraDao() {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ trait AgoraDao {

def find(entity: AgoraEntity): Seq[AgoraEntity]

def findSingle(entity: AgoraEntity): AgoraEntity
def findSingle(entity: AgoraEntity): Option[AgoraEntity]

def findSingle(namespace: String, name: String, id: Int): AgoraEntity
def findSingle(namespace: String, name: String, id: Int): Option[AgoraEntity]
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ class AgoraMongoDao(collection: MongoCollection) extends AgoraDao {
//insert the entity
val dbEntityToInsert = EntityToMongoDbObject(entity)
collection.insert(dbEntityToInsert)
findSingle(entity)
findSingle(entity) match {
case None => throw new Exception("failed to find inserted entity?")
case foundEntity => foundEntity.get
}
}

def getNextId(entity: AgoraEntity): Int = {
Expand Down Expand Up @@ -67,16 +70,16 @@ class AgoraMongoDao(collection: MongoCollection) extends AgoraDao {

override def find(entity: AgoraEntity): Seq[AgoraEntity] = find(EntityToMongoDbObject(entity))

override def findSingle(entity: AgoraEntity): AgoraEntity = {
override def findSingle(entity: AgoraEntity): Option[AgoraEntity] = {
val entityVector = find(EntityToMongoDbObject(entity))
entityVector.length match {
case 1 => entityVector.head
case 0 => throw new Exception("Found 0 documents matching: " + entity.toString)
case 1 => Some(entityVector.head)
case 0 => None
case _ => throw new Exception("Found > 1 documents matching: " + entity.toString)
}
}

override def findSingle(namespace: String, name: String, id: Int): AgoraEntity = {
override def findSingle(namespace: String, name: String, id: Int): Option[AgoraEntity] = {
val entity = AgoraEntity(namespace = Option(namespace), name = Option(name), id = Option(id))
findSingle(entity)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,39 @@ object AgoraEntity {
Marshaller.of[AgoraEntity](`application/json`) { (value, contentType, context) =>
context.marshalTo(HttpEntity(contentType, grater[AgoraEntity].toCompactJSON(value)))
}

def fromAgoraAddRequest(agoraAddRequest: AgoraAddRequest, createDate: Option[Date]) = {
new AgoraEntity(namespace = agoraAddRequest.namespace,
name = agoraAddRequest.name,
synopsis = agoraAddRequest.synopsis,
documentation = agoraAddRequest.documentation,
owner = agoraAddRequest.owner,
createDate = createDate,
payload = agoraAddRequest.payload)
}

}

@ApiModel(value = "Methods Query Response")
case class AgoraEntity(@(ApiModelProperty@field)(required = true, value = "The method id")
var id: Option[Int] = None,
@(ApiModelProperty@field)(required = true, value = "The namespace to which the method belongs")
object AgoraAddRequest {
implicit val AgoraAddRequestUnmarshaller =
Unmarshaller[AgoraAddRequest](`application/json`) {
case HttpEntity.NonEmpty(contentType, data) grater[AgoraAddRequest].fromJSON(data.asString)
case HttpEntity.Empty new AgoraAddRequest()
}

implicit val AgoraAddRequestMarshaller =
Marshaller.of[AgoraAddRequest](`application/json`) { (value, contentType, context) =>
context.marshalTo(HttpEntity(contentType, grater[AgoraAddRequest].toCompactJSON(value)))
}
}

@ApiModel(value = "Agora Method")
case class AgoraEntity(@(ApiModelProperty@field)(required = true, value = "The namespace to which the method belongs")
namespace: Option[String] = None,
@(ApiModelProperty@field)(required = true, value = "The method name ")
name: Option[String] = None,
@(ApiModelProperty@field)(required = true, value = "The method id")
var id: Option[Int] = None,
@(ApiModelProperty@field)(required = true, value = "A short description of the method")
synopsis: Option[String] = None,
@(ApiModelProperty@field)(required = true, value = "Method documentation")
Expand All @@ -44,3 +68,18 @@ case class AgoraEntity(@(ApiModelProperty@field)(required = true, value = "The m
@(ApiModelProperty@field)(required = true, value = "The method payload")
payload: Option[String] = None
)

@ApiModel(value = "Request to add method")
case class AgoraAddRequest(@(ApiModelProperty@field)(required = true, value = "The namespace to which the method belongs")
namespace: Option[String] = None,
@(ApiModelProperty@field)(required = true, value = "The method name ")
name: Option[String] = None,
@(ApiModelProperty@field)(required = true, value = "A short description of the method")
synopsis: Option[String] = None,
@(ApiModelProperty@field)(required = true, value = "Method documentation")
documentation: Option[String] = None,
@(ApiModelProperty@field)(required = true, value = "User who owns this method in the methods repo")
owner: Option[String] = None, // TODO: remove (use authenticated user)
@(ApiModelProperty@field)(required = true, value = "The method payload")
payload: Option[String] = None
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.gettyimages.spray.swagger.SwaggerHttpService
import com.wordnik.swagger.model.ApiInfo
import org.broadinstitute.dsde.agora.server.AgoraConfig
import org.broadinstitute.dsde.agora.server.webservice.methods.MethodsService
import org.broadinstitute.dsde.agora.server.webservice.util.StandardServiceHandlerProps
import org.broadinstitute.dsde.agora.server.webservice.util.ServiceHandlerProps
import spray.routing._

import scala.reflect.runtime.universe._
Expand All @@ -21,7 +21,7 @@ class ApiServiceActor extends HttpServiceActor {
def actorRefFactory = context
}

val methodsService = new MethodsService with ActorRefFactoryContext with StandardServiceHandlerProps
val methodsService = new MethodsService with ActorRefFactoryContext with ServiceHandlerProps

def possibleRoutes = methodsService.routes ~ swaggerService.routes ~
get {
Expand Down Expand Up @@ -55,4 +55,4 @@ class ApiServiceActor extends HttpServiceActor {
AgoraConfig.SwaggerConfig.licenseUrl)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.broadinstitute.dsde.agora.server.webservice.methods

import java.util.Date

import akka.actor.Actor
import com.novus.salat._
import com.novus.salat.global._
import org.broadinstitute.dsde.agora.server.dataaccess.AgoraDao
import org.broadinstitute.dsde.agora.server.model.{AgoraAddRequest, AgoraEntity}
import org.broadinstitute.dsde.agora.server.webservice.util.ServiceMessages
import spray.routing.RequestContext

/**
* Created by dshiga on 5/4/15.
*/
class MethodsAddHandler extends Actor {

implicit val system = context.system

def receive = {
case ServiceMessages.Add(requestContext: RequestContext, agoraAddRequest: AgoraAddRequest) =>
add(requestContext, agoraAddRequest)
context.stop(self)
}

private def add(requestContext: RequestContext, agoraAddRequest: AgoraAddRequest): Unit = {
val agoraEntity = AgoraEntity.fromAgoraAddRequest(agoraAddRequest, createDate = Option(new Date()))
val method = AgoraDao.createAgoraDao.insert(agoraEntity)
context.parent ! grater[AgoraEntity].toJSON(method)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import akka.actor.Actor
import org.broadinstitute.dsde.agora.server.dataaccess.AgoraDao
import org.broadinstitute.dsde.agora.server.model.AgoraEntity
import org.broadinstitute.dsde.agora.server.webservice.util.ServiceMessages
import org.broadinstitute.dsde.agora.server.webservice.PerRequest._
import spray.http.StatusCodes._
import spray.routing.RequestContext
import com.novus.salat._
import com.novus.salat.global._
Expand All @@ -23,7 +25,9 @@ class MethodsQueryHandler extends Actor {
}

def query(requestContext: RequestContext, namespace: String, name: String, id: Int): Unit = {
val method = AgoraDao.createAgoraDao.findSingle(namespace, name, id)
context.parent ! grater[AgoraEntity].toJSON(method)
AgoraDao.createAgoraDao.findSingle(namespace, name, id) match {
case None => context.parent ! RequestComplete(NotFound, "Method: " + namespace + "/" + name + "/" + id + " not found")
case Some(method) => context.parent ! grater[AgoraEntity].toJSON(method)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.broadinstitute.dsde.agora.server.webservice.methods

import com.wordnik.swagger.annotations._
import org.broadinstitute.dsde.agora.server.model.AgoraEntity
import org.broadinstitute.dsde.agora.server.model.{AgoraAddRequest, AgoraEntity}
import org.broadinstitute.dsde.agora.server.webservice.util.{ApiUtil, ServiceHandlerProps, ServiceMessages}
import org.broadinstitute.dsde.agora.server.webservice.PerRequestCreator
import spray.routing.HttpService
Expand All @@ -10,7 +10,7 @@ import spray.routing.HttpService
trait MethodsService extends HttpService with PerRequestCreator {
this: ServiceHandlerProps => // Require a concrete ServiceHandlerProps creator to be mixed in

val routes = queryRoute
val routes = queryRoute ~ postRoute

@ApiOperation(value = "Query a method from the method repository by namespace, name, and id",
nickname = "methods",
Expand All @@ -30,10 +30,34 @@ trait MethodsService extends HttpService with PerRequestCreator {
))
def queryRoute =
path(ApiUtil.Methods.path / Segment / Segment / Segment) { (namespace, name, id) =>
get {
requestContext =>
perRequest(requestContext, methodsQueryHandlerProps, ServiceMessages.Query(requestContext, namespace, name, id.toInt))
get {
requestContext =>
perRequest(requestContext, methodsQueryHandlerProps, ServiceMessages.Query(requestContext, namespace, name, id.toInt))
}
}

@ApiOperation(value = "Add a method to the method repository",
nickname = "add",
httpMethod = "POST",
produces = "application/json",
response = classOf[AgoraEntity],
notes = "API is rapidly changing.")
@ApiImplicitParams(Array(
new ApiImplicitParam(name = "body", required = true, dataType = "org.broadinstitute.dsde.agora.server.model.AgoraAddRequest", paramType = "body", value = "Agora Add Request") ))
@ApiResponses(Array(
new ApiResponse(code = 200, message = "Successful Request"),
new ApiResponse(code = 400, message = "Malformed Input"),
new ApiResponse(code = 500, message = "Internal Error")
))
def postRoute =
path(ApiUtil.Methods.path) {
post {
entity(as[AgoraAddRequest]) { agoraAddRequest =>
requestContext =>
perRequest(requestContext, methodsAddHandlerProps, ServiceMessages.Add(requestContext, agoraAddRequest))
}
}
}


}
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
package org.broadinstitute.dsde.agora.server.webservice.util

import akka.actor.Props
import org.broadinstitute.dsde.agora.server.webservice.methods.MethodsQueryHandler
import org.broadinstitute.dsde.agora.server.webservice.methods.{MethodsAddHandler, MethodsQueryHandler}

/**
* Generic trait to safely provide props for creating service handler actors.
* Override to supply specific service handler implementations (ie Standard, Mock)
*/
trait ServiceHandlerProps {
def methodsQueryHandlerProps: Props
def methodsQueryHandlerProps = Props(new MethodsQueryHandler())

def methodsAddHandlerProps = Props(new MethodsAddHandler())
}

/**
* Provides props for the safe creation of standard service handler actors
*/
trait StandardServiceHandlerProps extends ServiceHandlerProps{
override def methodsQueryHandlerProps = Props(new MethodsQueryHandler())
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package org.broadinstitute.dsde.agora.server.webservice.util

import org.broadinstitute.dsde.agora.server.model.AgoraAddRequest
import spray.routing.RequestContext

/**
* Case classes representing messages to pass to service handler actors
*/
object ServiceMessages {
case class Query(requestContext: RequestContext, namespace: String, name: String, id: Int)

case class Add(requestContext: RequestContext, agoraAddRequest: AgoraAddRequest)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package org.broadinstitute.dsde.agora.server

import org.broadinstitute.dsde.agora.server.dataaccess.AgoraDao
import org.broadinstitute.dsde.agora.server.model.AgoraEntity
import org.broadinstitute.dsde.agora.server.model.{AgoraAddRequest, AgoraEntity}
import org.broadinstitute.dsde.agora.server.model.AgoraEntity._
import org.broadinstitute.dsde.agora.server.webservice.methods.MethodsService
import org.broadinstitute.dsde.agora.server.webservice.util.{ApiUtil, StandardServiceHandlerProps}
import org.broadinstitute.dsde.agora.server.webservice.util.{ApiUtil, ServiceHandlerProps}
import org.scalatest.{FlatSpec, Matchers}
import spray.routing.Directives
import spray.testkit.ScalatestRouteTest
import spray.httpx.marshalling._


class ApiServiceSpec extends FlatSpec with Matchers with Directives with ScalatestRouteTest {
Expand All @@ -17,24 +18,47 @@ class ApiServiceSpec extends FlatSpec with Matchers with Directives with Scalate
def actorRefFactory = system
}

val methodsService = new MethodsService with ActorRefFactoryContext with StandardServiceHandlerProps
val methodsService = new MethodsService with ActorRefFactoryContext with ServiceHandlerProps

def fixture =
new {
val agoraDao = AgoraDao.createAgoraDao
}

val namespace = "broad"
val name = "testMethod"
val synopsis = "This is a test method"
val documentation = "This is the documentation"
val owner = "bob the builder"
val payload = "echo 'hello world'"
val testMethod = AgoraEntity(namespace = Option(namespace), name = Option(name))

"Agora" should "return information about a method, including metadata " in {
val testFixture = fixture

val namespace = "broad"
val name = "testMethod"
val testMethod = AgoraEntity(namespace = Option(namespace), name = Option(name))

val insertedEntity = testFixture.agoraDao.insert(testMethod)

Get(ApiUtil.Methods.withLeadingSlash + "/" + namespace + "/" + name + "/" + insertedEntity.id.get) ~> methodsService.queryRoute ~> check {
responseAs[AgoraEntity] === insertedEntity
}
}

"Agora" should "create and return a method" in {
val testFixture = fixture

val agoraAddRequest = new AgoraAddRequest()

Post(ApiUtil.Methods.withLeadingSlash, marshal(testMethod)) ~> methodsService.postRoute ~> check {
val response = responseAs[AgoraEntity]
response.namespace === namespace
response.name === name
response.synopsis === synopsis
response.documentation === documentation
response.owner === owner
response.payload === payload
response.id === 1
response.createDate != null
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class MethodsDbTest extends FlatSpec with BeforeAndAfterAll {

testFixture.agoraDao.insert(agoraTestMethod)

val entity = testFixture.agoraDao.findSingle(agoraTestMethod)
val entity = testFixture.agoraDao.findSingle(agoraTestMethod).get

assert(entity == agoraTestMethod)
}
Expand All @@ -39,7 +39,7 @@ class MethodsDbTest extends FlatSpec with BeforeAndAfterAll {
//NB: agoraTestMethod has already been stored.
val queryEntity = new AgoraEntity(namespace = Option("broadinstitute"), name = Option("echo"), id = agoraTestMethod.id)

val entity = testFixture.agoraDao.findSingle(queryEntity)
val entity = testFixture.agoraDao.findSingle(queryEntity).get

assert(entity == agoraTestMethod)
}
Expand All @@ -52,9 +52,9 @@ class MethodsDbTest extends FlatSpec with BeforeAndAfterAll {
val previousVersionEntity = agoraTestMethod.copy()
previousVersionEntity.id = Option(agoraTestMethod.id.get - 1)

val entity1 = testFixture.agoraDao.findSingle(previousVersionEntity)
val entity2 = testFixture.agoraDao.findSingle(agoraTestMethod)
val entity1 = testFixture.agoraDao.findSingle(previousVersionEntity).get
val entity2 = testFixture.agoraDao.findSingle(agoraTestMethod).get

assert(entity1.id != entity2.id)
assert(entity1.id.get == entity2.id.get - 1)
}
}
}

0 comments on commit 47e0bb2

Please sign in to comment.